Authoritative component of the integration contract. Its shared scope and versioning policy apply to every section below.
The SDK's /internal entry adds probeWorkerHostAsync,
requireWorkerHostAsync, prepareWorkerSandboxAsync and
launchAgentWorkerAsync. Each one drives the same steps as its existing
synchronous form: the same real-launcher probe, scratch git check and
bubblewrap setup waits, in the same order, with the same deadlines and
refusals. The difference is that it awaits each wait instead of blocking the
caller's event loop. workerHostBoundary gains probeAsync, which answers
through a replaced probe, so one test double still stands in for both.
The synchronous exports are unchanged.
Its launchMaintenanceWorkerAsync (#2790) likewise drives
launchMaintenanceWorker's steps: a separate-UID helper worker's setup is
awaited instead of blocking. Everything before that first wait, the
maintenance admission included, still runs when it is called.
This is internal compatibility wiring under the internal-entry classification below. It changes no ordinary package signature, CLI/MCP/HTTP envelope, frontmatter meaning or credential authority, and requires a patch changeset.
The SDK's /internal entry adds prepareMaintenanceWorker and
prepareMaintenanceGitInput for existing first-party asynchronous host owners.
They prepare one concrete retained-stage command and the existing frozen Git
adapter; they are not an extension point, arbitrary filesystem reader/writer,
wire input or supported package API. Synchronous callers cannot substitute a
stale plan for asynchronous metadata acquisition on a protected stage.
The internal runMaintenanceWorker accepts a synchronous host callback after
preparation and immediately before launching the requested command. Storage
uses it to revalidate current authority and record entry into the operation.
Preparation failure or revoked authority cannot be reported as command entry;
the callback is not passed to the first-party preparation commands.
This is internal compatibility wiring under the internal-entry classification below. It changes no ordinary package signature, CLI/MCP/HTTP envelope, frontmatter meaning or credential authority, and requires a patch changeset rather than an additive public-contract feature.
StartTurnRequest adds optional @internal toolExecution(toolUseId, path), a
server-only callback a backend calls before a call's tool_result with where
that call actually ran: host_bridge (its body ran in the host's handler for
its tool), worker_read or worker_write (a first-party file tool read or
wrote in a worker that loaded no extension code). It is never serialized into a
worker and is not a client, model or permission input. The pi backend reports
it from its own record of each call's RPCs, and only for a worker that loads no
extensions; the Claude backend does not report it. The UI server uses it to
skip the journal observation after a proven read
(governed-writable-workers).
A backend that never calls it, and every call it reports nothing for, keeps
the observation after each result. This is first-party implementation sharing
under the internal-entry classification below and requires a patch changeset.
The existing WorkerSandbox input adds optional @internal egress, a concrete host-created
turn proxy from the SDK's internal startEgressProxy. It is trusted host data,
not a wire field, backend extension callback or destination permission. The
ordinary Claude turn runner consumes an explicitly supplied proxy for its
stored-login and API-billed profiles; the autonomous path retains its existing
selection. An API-billed profile's own key is injected by the relay and its
worker holds a placeholder. The pi backend consumes it as well: its relay
resolves pi's native credential on the host for each dispatch, refuses a
command-sourced or unqualified credential before launch, and sends a
credential only to its provider's built-in origins or to the exact origins
the server's BRAIN_UI_PI_CREDENTIAL_ORIGINS (provider=origin[,origin];...)
lists for that provider. Existing callers omitting it keep their current behavior.
The prepared first-party composition uses the native worker's placeholder credential and host-held OAuth resolution on each actual dispatch. Native initialization, subscription/settings checks and actual model/session behavior remain in the ordinary SDK path. Mutable native project/managed settings are observed before a prompt; a retained immutable startup view instead requires its existing manager acquisition and matching kernel checkpoint identity. Cleanup attempts every owned resource and reports any failure.
This member is first-party implementation sharing, with the internal-entry classification below; it does not add a supported public proxy extension API.
@schlessera/brain-ui-sdk/internal adds startProviderRelay,
parseCliProviderOrigins, relayedProviderKeys, providerUsage,
hostProviderCredential and their types, and the turn proxy gains relayed()
beside inference(). They are first-party implementation sharing with no
compatibility promise. The server starts one relay per governed or strict turn
that has a turn proxy, and one per credentialed maintenance run where the host
has managed native storage. A relay sends a key only to that key's built-in
first-party origin (GEMINI_API_KEY to https://generativelanguage.googleapis.com,
ANTHROPIC_API_KEY to https://api.anthropic.com, OPENAI_API_KEY to
https://api.openai.com) or to an exact origin the server's
BRAIN_UI_CLI_PROVIDER_ORIGINS (KEY=origin[,origin];...) lists for that key.
A custom endpoint or a custom key variable is refused unless listed. The
public protocol adds the TurnBudgetCoverage type (wire).
The server supplies it automatically for governed and strict Claude turns,
and the internal WorkerNoteFacts gains optional egress. A backend sets it
only when it actually routed the worker through that proxy, so the
capabilities note never claims recorded egress for an unrouted worker.
Custom-origin selection and Pi integration are separate from this input. Host-owned exact-origin authorization per
credential remains mandatory for custom OAuth recipients. This input neither
sanitizes arbitrary unrelated provider/command credentials nor completes the
credential capability and maintenance cutover in #1666.
AgentBackend adds optional inferenceOrigin(req: { profileId?: string; sessionId?: string }): Promise<string | undefined>. It returns the exact origin
(scheme://host[:port]) that an ordinary turn on that profile, or resuming
that session, sends inference to through the host inference relay. The server
calls it in the trusted host before admission and names the turn's provider
policy recipient from it: a built-in vendor origin, or one listed in the
server's BRAIN_UI_PROVIDER_RECIPIENT_ORIGINS (name=origin[,origin];...).
A backend that omits it, returns undefined or does not run the relay has no
proven recipient, so every provider folder stays masked for its turns.
The internal WorkerSandbox gains optional @internal inferenceRecipient
({ name, origin }), and providerMasks.index becomes optional: absent
exactly when providerMasks.folders is empty, for a turn whose recipient
is allowed every provider folder. A backend that receives inferenceRecipient
passes its origin to startInferenceRelay as the new internal
recipientOrigin option. The relay then refuses to start toward any other
origin, and a turn without a relay refuses to start. The first-party
Claude and pi backends implement all three.
@schlessera/brain-ui-sdk/protocol (and the root and ./client entries that re-export it) exports CHAT_DELETION_CAPABILITY
("chatDeletion"), ChatDeletionRequest, ChatDeletionReceipt,
ChatDeletionStore, ChatDeletionOutcome, ChatDeletionRetained,
ChatDeletionRefusal and ServerChatRetired, now a member of ServerMessage.
DraftErrorCode and DraftErrorResponse gain DRAFT_CHAT_RETIRED.
@schlessera/brain-ui-sdk/schemas exports serverChatRetiredSchema, and
parseServerMessage accepts the frame. A client that switches exhaustively over
ServerMessage["type"] needs a case for chat_retired; the host sends it only to
connections that declared the capability. In @schlessera/brain-ui-server, WsHost
gains optional chatDeletion and the session catalog optional cleanChatStore and
unfinishedChatRetirements, host wiring reachable from its exports. Semantics are in the
HTTP and
wire components.
BackendCapabilities adds optional historyRemoval?: boolean, and
AgentBackend adds optional removeSessionHistory?(req: SessionHistoryRemovalRequest): Promise<SessionHistoryRemovalResult>, exported
from @schlessera/brain-ui-sdk/server with
SessionHistoryRemovalRequest ({ operationId: string; sessionId: string; signal?: AbortSignal }), SessionHistoryRemovalResult ({ outcome }) and
SessionHistoryRemovalOutcome ("removed" | "partial" | "unknown").
Removal is supported only when the capability is true and the method exists;
the conformance suite in @schlessera/brain-ui-sdk/testing checks that they
agree. A host refuses deletion of a chat on any other backend before it
retires anything; older backends remain valid and simply do not support it.
A host calls it only for a chat its owner explicitly retired, under that
retirement's operation identity, as the
deletion policy allows. Before its first
await the backend fences the session durably and permanently: listSessions
omits it, getHistory returns [], and resume, follow-up, archive and
restore refuse it, after a restart too. It then waits for the session's running
turns and readers to drain and removes every native artifact it owns: live,
archived, staged and conflict copies, subordinate and branch files, display and
prompt metadata and file observations. Only names derived from the validated
session identity are touched; a link, hard-linked file, redirected root or
artifact whose own header names another session is left in place.
The outcome is content-free and maps onto the host's per-store ledger:
removed only when every owned artifact is confirmed absent and the installed
runtime no longer lists the session; partial while owned content may remain,
including when signal aborts the caller's wait first (the removal keeps its
single owner and finishes after a genuine drain); unknown when an unexpected
failure left the state unconfirmed. Every outcome but removed is retried under
the same operation. A second operation for the same session, an invalid
identity or a redirected tombstone root rejects with BackendRequestError.
Previously sent provider bytes are never rewritten, and provider-held copies
are outside it. The first-party Claude and Pi backends implement it.
The intake createInboxIntake returns adds person(input, key, principal):
queue intake for a person's arrival through an authenticated route, outside
any turn. It records that provenance with a digest of the staged bytes, so a
provider-masked autonomous turn may admit the thread; share(...) records the
same, and cli(...) records none. InboxBridgeContext adds optional
qualifyOrigin(): void, run in each inbox_list and inbox_get transaction
before any payload is built; it throws to refuse the read. The server's
autonomous turns pass one that revalidates their origin thread's admission.
Disclosure coverage badge (additive, #1846). The kit's DisclosureProps
adds optional badge?: { icon: IconName; label: string }. Its compact neutral
glyph and accessible label remain outside the truncating summary label, before
the chevron. Existing controlled, seeded and uncontrolled behavior is unchanged.
Hygiene card caller copy (additive, 0.44.0, #2837). HygieneCardProps
adds optional stateText?: Partial<Record<state, string>> and
busyLabel?: string. A named state's text replaces its file default, the
fixed entry included; busyLabel replaces the applying label of the commit
control. Both are plain inert text, wrapped anywhere and never parsed. An
omitted prop or state keeps every existing file default, meaning, state
machine, focus and announcement. The kit has no finding-kind prop: callers
choose the copy from the action and outcome they hold.
The semantic IconName gains enlarge: open content in its dedicated
full-size viewer. It draws lucide's Expand, the glyph the chat Mermaid block
already uses for Open diagram. The framed diagram renderer's header uses it.
No existing key, glyph or component changes.
ButtonProps.reach?: boolean, when the Button has onClick, adds the kit's
.bk-hit class. Under any-pointer: coarse the target then reaches 44px on
each axis, centred on the unchanged paint. Without the prop, without a handler
or under a fine pointer, a Button renders and hit-tests exactly as before.
Callers keep the gap to each interactive neighbour that the design-kit record
states. The .bk-hit class is usable on any element with a handler, from the
same stylesheet.
ButtonProps.ariaDescribedBy?: string forwards the supplied element IDs to
aria-describedby on the button's existing role-bearing element.
ContactAction adds optional disabled, ariaDisabled, ariaLabel,
ariaDescribedBy and loading fields. Accessibility and disabled fields pass
through to the existing Button behavior. ariaDisabled keeps the action
focusable; callers must guard activation while work is pending. disabled
removes it from the tab order. loading displays a decorative spinner while
preserving the action element and its focus; it does not implicitly disable an
action or announce progress. The semantic IconKey adds loading.
Spinner rotation respects reduced motion.
Discovery expansion is documented in the ui-react guide.
Its store state remains behind BrainUiServices.stores, which is explicitly
@internal; no new public graph-store signature, wire endpoint or query limit
is introduced.
The kit adds two public components; D57 records why each exists.
Tray(TrayProps):label(required; what the panel holds and how much), optionalicon,swatch(a CSS colour for a leading mark; never the name),open,onOpenChange,maxHeightandchildren. Its forwarded ref is the native header button. The body stays mounted andhiddenwhile closed.ItemCard(ItemCardProps,ItemCardAction):title(required), optionalmeta,note,errorCount,open,onOpenChange,actions(label,onSelect, optionaldisabledanddanger),noticeandchildren. Its forwarded ref is therole="button"toggle.errorCount > 0replaces the meta with the count and draws the card red. Actions render in a separate⋯menu at every size; no actions means no menu. The body unmounts while closed.
open and onOpenChange keep Disclosure's three modes exactly: neither is
uncontrolled, open alone seeds once, both together hand control to the parent,
and onOpenChange fires on every toggle with the value being moved to.
DisclosureProps is unchanged; its open summary now also carries
aria-controls naming the body. The shared expansion primitive is internal,
not exported, and is no extension seam. No CLI, MCP, wire, storage or HTTP
shape changes; a minor changeset covers the additions.
@schlessera/brain-ui-kit exports StatusIndicator and the types
StatusIndicatorProps, StatusIndicatorOperation, StatusIndicatorFact and
StatusIndicatorState. D58
records the state matrix and the interaction.
StatusIndicatorProps:label(what the status is about),summary(the tooltip sentence, repeated at the top of details),title(the details heading),operationsand an optionalreturnFocuscallback, used when details close after the trigger has gone.StatusIndicatorOperation:id(the consumer's stable identity),state,stateLabel(the state in the consumer's words), and optionalexplanation,facts(StatusIndicatorFact:key,value) andactions(aReactNodeof consumer buttons, rendered in details only).StatusIndicatorState:active,queued,success,attention,unknown,failed,refusedoridle.
The component is presentational. It derives only the displayed state and the
count. The display state is the worst operation's state, in the order refused,
failed, unknown, attention, active, queued, success, idle. Two or more
operations show a count. An empty list renders a reserved 44px slot without a
control, and it is never success. The accessible name is
{label}: {lead stateLabel}[; {n} operations]. Details.
The consumer is responsible for authoritative evidence. Nothing in the
component infers success from time, animation or response order, and opening
details triggers no action. State, freshness, identity, persistence and every
action's authority stay with the consumer. StatusDot is unchanged. No CLI,
MCP, wire, storage or HTTP shape changes; a minor changeset covers the
additions.
BRAIN_UI_MAINTENANCE_TEMP_BYTES and BRAIN_UI_MAINTENANCE_TEMP_BUDGET_BYTES
each default to 268435456 bytes. Both accept positive, safe, page-aligned
integer byte counts; the aggregate must cover one command allowance. These
defaults are independent of agent-session concurrency, agent turn temp,
runtime state and output staging. Invalid configuration refuses the host edge.
Resolved ServerConfig.maintenanceTemp and CronConfig.maintenanceTemp contain
allowanceBytes and budgetBytes. These fields are optional for existing
programmatic callers; environment resolvers always supply them, and absent
callers retain validated SDK defaults. The runner carries the operator
settings into its own control environment, not the scheduled command's
authority. Git transport preserves these resolved limits while retaining its
separate envelope without a brain mount.
Server and cron admission is atomic in a ledger selected from their canonical
configured UI database identity, with that state's existing access authority.
Trusted internal maintenance commands propagate this identity through probes,
streamed/synchronous work and the separate Git transport; no scheduled command
chooses it. Different launcher uids or temporary directories do not split a
shared UI-state domain. Standalone SDK calls and in-memory UI configurations
without shared persistent state retain a private per-uid /tmp domain.
Foreign PID-namespace ownership is conservatively held, not reclaimed as dead.
Optional trusted MaintenanceHost data also reaches first-party backend startup
and short Git/scratch checks through the existing backend module context and
host bridge configuration. It is never selected by a scheduled command or
serialized as worker authority. Standalone helpers retain their existing defaults.
Exhausted capacity
uses the existing worker_state_busy failure class with a maintenance-specific
message; nothing ran and no agent runtime state was lost. Filesystem exhaustion
is ENOSPC. No new CLI, HTTP or wire envelope is introduced. See
maintenance workers
for storage and supported-host requirements.
@schlessera/brain-ui-sdk/tool-contracts (re-exported from /server) adds
APIFY_TOOL_CONTRACTS, the seven brain_apify_* contracts, with
APIFY_OPERATIONS, APIFY_TOOL_NAMES, APIFY_ERROR_CODES, APIFY_GUIDANCE,
apifyOperationOf and the ApifyToolCall, ApifyToolResult,
ApifyEnvelope, ApifyErrorCode, ApifyOperation and ApifyToolName
types. @schlessera/brain-ui-sdk/server adds handleApifyTool(name, input, bridge), which validates the strict input and forwards { tool, input } to
the optional new BackendBridge.apify(call); without it the tool refuses as
provider_disabled. The host binds principal, session, turn, posture and
approvals; a backend sends the parsed input only. Names, schemas and the
envelope are documented in the MCP component.
Both first-party backends register the tools in every interactive session.
Breaking pre-1.0 export curation (#534, ruled on #343 question 1). Each
@schlessera/brain-* package's ordinary entry points (. and its documented
subpaths) export only the supported API. That API includes every type reachable
through a public signature, exported by name or not, and the documented
behavior. A member tagged @internal in its own JSDoc is excluded, as a
private member is.
Entry points named /internal (@schlessera/brain/internal,
@schlessera/brain/internal/graph, @schlessera/brain/internal/apify (additive, #3162),
@schlessera/brain-common/internal/apify and @schlessera/brain-common/internal/apify-operations (additive, #3162),
@schlessera/brain-scrape/internal, @schlessera/brain-render-template/internal,
@schlessera/brain-ui-kit/internal, @schlessera/brain-ui-sdk/internal,
@schlessera/brain-ui-sdk/internal/client, @schlessera/brain-ui-server/internal,
@schlessera/brain-geo/internal and the two backends' /internal)
are first-party implementation sharing with no compatibility guarantee. Use
them only with the same lockstep version, if at all.
Breaking pre-1.0 narrowing (#1053). The surfaces #534 left public because a public declaration reached them, or because their classification was uncertain, follow a maintainer ruling per item:
WsHost(@schlessera/brain-ui-server) stays public as the type ofBrainUiApp.wsHost, whichcreateAppconstructs. Its supported members are the resolved configuration (brainPath,askUserFormLimits,appName,turnTimeoutMs,maxConcurrentSessions,observability), the backendregistryandclose(). Its connection, client-set, catalog, turn, activity, inbox, draft, label and classification members are@internal, as areWsHostOptions.catalog,classifier,activity,inbox,inboxTools,toolPermissionsanddrafts.SessionCatalogandcreateSessionCatalogmove to@schlessera/brain-ui-server/internal: a custom session catalog is no longer supported.- ui-react's store hooks (
useChatStore,useFileStore,useShareStore,useUIStore,useVoiceStore) stay public for deployment shells, typed asShellStoreHook<View>: a selector, plusgetState()andsubscribe()on the default root, overChatShellState,FileShellState,ShareShellState,UIShellStateandVoiceShellState(the ui-react README lists their fields).setStateis no longer typed.activeChatandanyStreamingtakeChatShellState;hasPendingSharetakesShareShellState, andShareIntakeStateis no longer exported. The zustand state shapes (ChatState,UIState…) leave the surface, and so doesBrainUiServices.stores(now@internal), which carried them. - Kept as documented API: core's
repoRelativePathSchema, for module config schemas (canonical consumer configuration); Claude'sMEASURED_RUNTIME, the measured Claude Code and agent SDK pair (backend guide); jobs'runScrape, the direct scrape fallback (jobs README). - Moved to the owning package's
/internalentry: corecollectStatsand itsCollectStatsOptions(BrainStatsandStatsThresholdsstay public as the result shape); the SDK's bridge-tool handlers (handleAskUser,handleAskUserForm,handleAskUserList,handleAskUserRank,handleGetCurrentLocation,handleQueryActivity,handleRequestImageMask,handleShowBlock, withImageMaskHandlerOptionsandLocationHandlerOptions),wrapCommandandexecWrapperSpawnOptionsfrom/server;describeRetry,canonicalModelIdandresolveThinkingLevelfrom every SDK entry (to/internal, and browser-safe to/internal/client);createToolRendererRegistryfrom/client,pruneStoredShares,ShareTargetError,isShareTargetRequestandhandleShareTargetRequestfrom/share-target(to/internal/client;registerShareTargetandreadShareLaunchParamsstay); the kit'sTOKENS,LIGHT_TOKENS,canvasand theirTokenNamekey type; render-template'sapplyExportLinkPolicyandprotectExportLinkDestinations; geo'sMAX_ROUTE_BYTES, in the new@schlessera/brain-geo/internal.
Additive in 0.42.0 (#1703): /server exports handleTurnTemp,
TurnTempAccess and TurnTempPurgeRecord for the scoped usage/purge tool.
Its input contract is exported alongside the other bridge contracts; the
handler's access capability names the admitted allowance, held root,
scratch, excluded/kept directories and already-linked answer text. Each
purge reports its outcome through the supplied record callback.
Additive: the shared backend toolkit (#1399, ruled on #1345). What both
shipped backends need to give the host the same behavior and security posture
is public on @schlessera/brain-ui-sdk/server, each declaration tagged
@experimental until 1.0: the bridge-tool handlers (handleAskUser,
handleAskUserForm, handleAskUserList, handleAskUserRank,
handleGetCurrentLocation, handleQueryActivity, handleRequestImageMask,
handleShowBlock, with ImageMaskHandlerOptions and
LocationHandlerOptions) and BRIDGE_TOOL_POSTURE; the exec wrapper
(wrapCommand, validateExecWrapper, EXEC_WRAPPER_ENV, EXEC_KILLER_ENV);
the subprocess environment filter (filterSubprocessEnv,
parseSubprocessEnvExtra, SubprocessEnvAudience); the lock keys
(BRAIN_LOCK_KEY, bashLockKey); describeRetry, resolveThinkingLevel,
assertLoadedSdk, rtkRewriteCommand; and the bundled
DEFAULT_CONFIRM_BASH_PATTERNS, whose entries may still evolve as a documented
user-visible change. Their behavior is unchanged; they leave
@schlessera/brain-ui-sdk/internal. Helpers only one backend, or the host and
one backend, import stay internal (bashCommand, SUBPROCESS_ENV,
execWrapperSpawnOptions, GIT_LOCK_KEY, canonicalModelId). The
toolkit record has the inventory.
Additive: ui-react share and stats exports (#1382). For shells that
assemble their own surfaces around ui-react, the @schlessera/brain-ui-react
root now exports the share pipeline (shareMarkdown, renderBlockHtml,
buildDiagramShareOptions, inlineMermaidDiagrams), splitFrontmatter with
FrontmatterSplit, and the /stats command's runStats. The project
website's demo uses exactly these, so it builds from the public entry instead of
from ui-react's source tree.
Additive: AppErrorBoundary (#1377). @schlessera/brain-ui-react exports
AppErrorBoundary and AppErrorBoundaryProps (children, plus a reload
hook for tests). A shell wraps its whole tree in it, providers included, so a
render error outside a page shows a reload screen instead of a blank app. The
page-level boundary inside AppShell needs nothing from the shell.
Additive: IconButton and TextButton (#1379). @schlessera/brain-ui-kit
exports two native type="button" primitives and their props. Both forward
refs and data-* hooks, support native disabled and disclosure ARIA, and use
kit focus/hover paint. IconButton supplies mute/danger/overlay tones and
28px small controls that grow to 44px under a coarse pointer. TextButton
supplies link/meta/inherit tones, with a 44px standalone target or inline reach.
This is a minor addition; existing button APIs are unchanged.
Additive: the module-authoring entry @schlessera/brain/module (#1397,
ruled on #537). The helpers a module needs to touch the brain's files the
way core does, @experimental until 1.0: path containment and atomic writes
(safeResolve, resolveWritable, writeFileSafely with
WriteFileSafelyOptions, WriteRefusedError, whose code is "EEXIST" when
a name may not be replaced); the scratch area (SCRATCH_DIR, scratchName,
writeScratchFile, assertScratchWritable, isInScratch, isWriteRefusal,
pruneScratch with ScratchReport, ScratchRemoval and ScratchFailure);
frontmatter and generated regions (splitFrontmatterBlock,
rewriteGeneratedRegion, inertGeneratedText); the taxonomy, corpus walk and
index registry (buildTaxonomy, getMarkdownFiles, runRegistry with
RegistryRun and RegistryProblem, and ValidationIssue); and completion
providers (resolveCompletionProvider, geminiCompletions with
GeminiCompletionConfig, GEMINI_FLASH_MODEL). CommandContext gains an
optional completions, the brain's completions config block as written, so
a module command resolves the configured provider instead of reading core's
process context. First-party modules import this entry and never
@schlessera/brain/internal. Index reads stay behind ctx.queries and
@schlessera/brain/queries. The /internal entry keeps the same names for
the first-party packages that still import them.
Breaking before 1.0: strictest path containment answers (0.45.0, #3422).
safeResolve(root, rel) and resolveWritable(root, rel) take a path relative
to the brain root. They return its canonical path, or null for any of these:
- an absolute
rel, even one inside the root; - a
..segment, even one whose result would stay inside; :or\in a segment, or a NUL;- a dangling symlink, wherever it points;
- a path whose existing components do not all stay inside, such as one that leaves through one link and comes back through another;
- a root whose realpath fails.
Before 0.45.0, core accepted an absolute path inside the root and a .. that
stayed inside. It also followed a dangling link to an inside target and checked
only the final path. A caller holding an absolute path passes it relative to
the root's realpath.
The CLI keeps accepting absolute path arguments inside the brain
(maintainer ruling).
Before resolving one, it converts it with the new, additive
brainRelativeArgument(root, arg): string, also exported from
@schlessera/brain/module for module commands. An absolute argument lexically
under the root as given or under its realpath becomes repo-relative (. for
the root itself). Any other argument comes back unchanged, so safeResolve
still refuses an absolute path outside the brain and one with a .. segment.
The converted path still goes through safeResolve, so an absolute path
through a symlink that leaves the brain is refused. The arguments it covers:
brain read,archiveandprocess<path>;import --stamp;render<path>and--out;eval --out;geotrack paths,--compare,--alongandmap --out;okf export --out;image --out,--refand--mask;diagram export|build<path>and--out;travel photo --to, andtravel route<file>and--to.
Additive frozen configuration root (#2848). @schlessera/brain/module
exports configurationRoot(root: string): string. When root equals the
trusted worker's BRAIN_WORKER_DATA_ROOT and BRAIN_WORKER_CONFIG_ROOT is
set, it returns the configuration root; otherwise it returns root unchanged.
Module configuration reads use this view while content reads use the data
root. Jobs scoring reads both settings/jobs.json and relative criteria
Markdown from this frozen view. This addition ships in a minor.
Additive module state area (0.45.0, #3380). CommandContext gains an
optional stateDir(): string. Core's CLI always provides it to a module
command: it returns <root>/.brain-ui/modules/<module-name>/, the module's
durable state directory, creating it (mode 0700) on first call, together
with .brain-ui/modules/.gitignore (*) when that is absent. root is the
brain root the CLI resolved, which inside a trusted worker is
BRAIN_WORKER_DATA_ROOT. A symlink at .brain-ui, .brain-ui/modules, the
module's directory or that .gitignore refuses with ModuleStateError; a
module name outside ^[a-z0-9][a-z0-9-]{0,63}$ refuses the same way.
@schlessera/brain/module exports the same rule as MODULE_STATE_DIR
(".brain-ui/modules"), moduleStatePath(name) (brain-relative, no disk
access), moduleStateDir(root, name) and ModuleStateError. The path is part
of the module contract: a scheduled module job keeps durable state there, and
in a maintenance worker it is published back through output staging after
each run like any other brain write, so the next run reads it.
Read-only host writes from a module command (0.45.0, #3380). When a module
command throws an EROFS error whose path lies outside the brain root (or
has no path), the CLI exits 2 with a ModuleReadOnlyPathError message on
stderr instead: it names the path, the module and its state area, and, when
BRAIN_WORKER_DATA_ROOT is set, the host execution boundary. An EROFS
inside the brain root keeps the existing read_only_brain refusal. No JSON
shape changes. The cron runner adds a [cron-run] line and prefixes the
recorded error with the same explanation when a worker job exits nonzero with
an unexplained EROFS or "read-only file system" in its stderr.
Additive protected scratch retention (#1743). The existing module helper
pruneScratch(root, now?, options?) accepts optional
options.protectedPaths: readonly string[]. These brain-relative or absolute
paths add retention for exact targets, descendants and ancestors during this
prune, including empty protected directories. Omitting them preserves the
existing age/byte cleanup. They cannot weaken durable conversation pins or
explicit pinnedPaths; protected bytes still count toward reported size and
overage. Hosted mask applications supply their frozen admitted protection,
without reading another policy or changing existing approval authority. This
optional module-helper parameter ships in a minor; its public signature report
is updated with it.
Overlay, document layers and palette semantics (#1378; pre-1.0 minor). @schlessera/brain-ui-kit
exports Overlay, OverlayProps, OverlayVariant, OverlayCloseReason, z
and LAYERS. The controlled primitive supplies sheet, dialog, fullscreen and
panel variants with dismissal reasons, focus management and inert background.
Its optional surfaceRef lets adapters reset destination scroll and focus;
non-modal titled panels expose a script-focusable destination heading. Panels
keep their header X under closedBy="none"; that value suppresses Escape,
native close requests and scrim taps. Sheets and dialogs remove their drawn
close control under none.
BottomSheet optionally draws a dismissal control. CommandPalette no longer
carries dialog semantics (role="dialog" and aria-modal); wrap it in
Overlay to supply modal semantics and focus management. It now renders a
named group; its pixels and callbacks are unchanged.
Additive: transcript detail linkage (#3179). OverlayProps.id?: string
sets the native dialog/panel id so a trigger's aria-controls identifies the
activated overlay. Existing callers and dismissal/focus behavior are unchanged.
The closed TranscriptQualifiers and TranscriptSymbols components live only
on the first-party /internal entry and add no supported extension seam.
Additive: bounded sheet swipe dismissal (#1433). OverlayCloseReason gains
"swipe". Callers branching on the close reason must handle "swipe".
Existing reasons keep their meaning and all props are unchanged, so this is a
minor addition. Touch and pen can dismiss the topmost sheet (or a centred
dialog below 900px) from its non-interactive top 56px, only under
closedBy="any". Body and mouse gestures never dismiss. The D54
gesture ruling defines thresholds,
bounded resistance, cancellation and reduced motion. onClose requests
dismissal once; the caller still owns open and the focus policy. Ineligible
sheets rubber-band at most 12px without requesting dismissal. Dictation maps a
swipe to Stop, keeping the words for review; draining blocks it.
Before 1.0, the versioning rules above apply. From 1.0, removing, renaming or
retyping an ordinary export or a type its signatures reach, or changing its
documented behavior, requires a major version; additions ship in minors.
api-report/ records every entry point's names and every public export's
signature with the types it reaches, and the packaging check holds the built
declarations to the same names. The removed and moved names, with migration
paths, are listed in the boundary decision.
Lineage: this is the public successor of the INTEGRATION.md that lived in
the private brain's scripts directory; shapes are unchanged unless marked.
ProviderInfo.model?: string carries only the explicitly configured main-model
id. An absent value means the runtime chooses it; a profile id is not a model.
The additive server-only BackendActivityEvent variant
{ kind: "main_model", model: string, sessionId: string } reports the executing
main model through the current startTurn bridge. Claude uses root initialization
identity, excluding helper/subagent initialization; Pi uses the runtime session's
model. Per-model usage never selects the interactive or voice main model.
The host admits the journal before executing the backend. Known configured identity populates admission metadata; runtime identity binds only the correlated active turn's summary. Cancelled/finished turns and stale bridges from resumed turns cannot bind. Concurrent sessions retain independent identity. Subsequent governance events use the identity known at emission. Earlier null snapshots stay null in storage and all reads/exports; binding does not amend or enrich them, and historical turns are never backfilled. Pre-runtime refusals remain unknown unless the resolved profile explicitly supplies a model. Nullable governance field shapes are unchanged. This additive public interface requires a minor release.
The optional server-only StartTurnRequest.autonomous request is additive. It
contains { origin: "autonomous", persistence: "none", allowedTools: string[], systemPromptAppend: string } selected by server code. It requires the enforced
no-grant posture and synchronous BackendBridge.checkpointPermission. It rejects
resume, invalid mode declarations and unsupported backends before runtime work.
BackendCapabilities.autonomous?: boolean advertises this specific turn support;
it does not advertise containment or enable autonomous dispatch.
Ordinary requests retain their persistence and session_info/terminal semantics.
Autonomous requests save no SDK session/history and advertise no interactive
session. Their runtime id reaches Activity via the additive
BackendActivityEvent variant autonomous_identity, and scopes content/result
frames internally. Exactly one terminal result ends an established runtime turn;
pre-identity failures may end on a bare error. Principal revocation aborts work
visibly. No client/model wire payload selects this mode or its authority.
The additive server-only BackendActivityEvent variant autonomous_usage
contains { usage: TurnUsage, atToolBoundary: boolean }: cumulative usage of
completed top-level responses and whether the restricted sequential runtime is
still at that response's tool-execution boundary. It never claims a whole-turn
invoice and adds no client frame. Claude counts Messages API message_stop
receipts; pi counts completed assistant message_end receipts, excluding aborted
and error answers. Server checkpoint settlement uses these receipts as described
in the budget guide. Backends without this optional reporting
retain conservative missing-receipt settlement.
The additive server-only BackendActivityEvent variant resource_limit
(#1660) contains { limits, fatal }: each new host resource-limit hit of the
turn's worker, { limit, source, value, events }, reported next to the step
that caused it, and once more with fatal: true when a limit ended the worker.
Both shipped backends report it and fail such a turn with
worker_resource_limit. The host records it on the run's root span and as a
resource_limit governance event (#1651)
(wire). It adds no client frame.
The additive server-only BackendActivityEvent variants policy_block and
policy_confirm (#1661) contain { toolUseId, payload }. The payload names the
Bash tool/input, truncation, original policy-file rule index/pattern/reason and
captured policy hash; confirmation also records its approved, declined,
cancelled or timed-out decision. Both adapters report these through the existing
Activity callback. The host supplies turn/session/actor/profile attribution and
keeps the validated governance receipt after Activity detail pruning. They add
no client frame and confer no execution authority.
The Activity origin vocabulary adds "autonomous" for spans and run rollups.
The SDK schemas accept it, and existing origin filtering selects these runs
without including interactive or cron work. Runtime identity, usage and principal
attribution remain recorded; unknown prices retain their existing unknown meaning.
The synchronous synthetic bridge captures escalation data before no-grant denial
and aborts without parking. Durable Action transitions, reservation admission,
containment and full-system enablement remain separate gated tasks.
StartTurnRequest.turnId?: string is the host's identity for the turn. A
backend names per-turn artifacts with it; without it, the backend mints its own.
The shipped server sets it on every interactive and voice turn.
Two server-only BackendActivityEvent variants report what happened to a file
an answer linked in turn temp, once the worker has exited. promotion carries
{ source, destination, bytes, originalLink, rewrittenLink }: the file was
copied to destination, brain-relative below
.brain/scratch/turns/<turnId>/, and the stored answer now links
rewrittenLink. Its optional aliases (additive, #1698) lists every other
destination the answer wrote for the same file, each as { originalLink, rewrittenLink }. promotion_refused carries { source, originalLink, reason, bytes, limitBytes }, with reason a TurnTempPromotionRefusal
(file_too_large, turn_too_large, not_regular_file, symlink,
multiple_links, missing, io_error or scratch_unavailable); the answer
keeps the link as written. The host stores both as the governance events of the
same names (http-api.md, "Governance event kinds"), without
aliases, and shows no marker in the chat. For each promotion it sends the
turn's clients the rewritten links
(wire), so the open conversation
matches the stored answer.
ServerConfig.inbox.budget? adds { spendUsd: number, turns: number, emergencySpendUsd: number, emergencyTurns: number, timeZone: string, unpricedUsdPerToken: number }. It remains optional for embedded configurations.
resolveServerConfig() supplies defaults of 5 USD, 0 operations, zero emergency
capacity, UTC and 0.01 USD per unpriced token. The six BRAIN_UI_AUTONOMOUS_*
environment settings and validation are specified in the budget guide.
Omission or zero turns cannot enable autonomous dispatch.
The cap governs new admissions against charged spend plus active conservative reservations; observed overruns are retained. It is not a provider-enforced invoice ceiling. One separately dispatched model-bearing operation counts one turn. Actual billing evidence, unknown-cost charging, frozen admission days and crash recovery follow the async decision. The reservation ledger is internal operational storage. Existing HTTP, wire, SDK turn-request, CLI/MCP and content-index shapes remain unchanged; full-v1 containment and system proof still gate production dispatch.
AutonomousTurnOptions.containment?: "restricted" asks a backend to run the
turn inside the restricted envelope for untrusted execution: a private network
namespace with only loopback, an explicit read envelope instead of the host
root, no ambient project configuration, and inference only through a
server-owned relay that holds the credential. BackendCapabilities.restrictedAutonomous?: boolean
advertises that support and is only meaningful with autonomous.
assertTurnPosture gains an optional third argument, supportsRestricted
(default false); with it false, a turn that requests containment rejects with
BackendRequestError before runtime work rather than running uncontained.
runAutonomousTurn always requests containment and refuses a backend without
the capability. Both first-party backends advertise it. A restricted turn also
refuses with BackendRequestError when its profile's credential cannot be held
by the relay: a Claude subscription profile without CLAUDE_CODE_OAUTH_TOKEN,
or a pi profile without an API key for an anthropic-messages,
openai-completions or openai-responses provider. A host that cannot
establish the envelope refuses with failure.errorClass = worker_host_unsupported.
An autonomous request without containment, such as a handoff summary, keeps
its existing meaning. No wire, HTTP, CLI or MCP shape changes, and nothing
dispatches autonomous work in production.
KeyedLockAcquireOptions adds optional priority ("interactive" or
"autonomous"), signal, onYield(key) and yieldAfterMs. Ordinary callers
remain interactive; equal-priority waiters remain FIFO. Interactive waiters
precede queued autonomous work. Cancellation removes a waiting acquisition;
it never forcibly releases an executing body. createKeyedLock() optionally
accepts clock/timer functions for deterministic verification.
AutonomousTurnOptions adds optional synchronous onYield(key),
yieldAfterMs and completedToolCalls (readonly { toolName, input } execution
receipts). Both first-party backends notify once per turn after continuous
same-key interactive contention, checkpoint before abort, then unwind their
actual writers. Different keys do not yield. Completed receipts only restrict
replay; they confer no capability and never bypass the current authority gate.
isCompletedAutonomousToolCall() compares exact JSON calls independently of
object property order. A fresh attempt has no runtime transcript replay.
ServerConfig.inbox adds optional maxAutonomousRuns and yieldAfterMs.
MAX_AUTONOMOUS_RUNS defaults to two and requires a positive integer;
BRAIN_UI_AUTONOMOUS_YIELD_AFTER_MS defaults to 20000 and requires a positive
integer below the normal 30000 ms interactive denial bound. Interactive WS
capacity remains separately reserved. Pool admission occurs atomically with
budget reservation/claim and counts operations, including batches. Yield
preserves spent turns and observed cost, releases unused conservative reserves
after unwind, and recovers work within its existing attempt limit. Exhaustion
leaves failed work and one durable dead-letter Action. Recovery retains
checkpoint and completion receipts in the operational UI database.
These server-only additions do not enable production dispatch or change client wire, CLI/MCP, content-index or frontmatter contracts. Cooperative unwind must finish before the interactive wait bound; a lock is never handed to another writer while the first body can still write. Semantically different tool inputs are evaluated as new calls under current authority, not deduplicated effects.
@schlessera/brain-ui-kit exports RailAct (icon, label, optional
name, effect, cost, why, onClick), and SideRailProps gains two
optional props, acts?: RailAct[] and onOpenPalette?: () => void (D52 §1 in
the design-kit record). Acts render as a vertical
toolbar named Acts, with one roving tab stop. Its accessible names append
the effect, the cost and unavailable: {why}. A why makes the act
aria-disabled and never invokes it. The collapsed rail omits acts that have
an effect, a cost or a why. onOpenPalette renders a button named
All commands with aria-keyshortcuts="Meta+K" in place of the passive ⌘K
cap. A rail given neither prop renders as before.
@schlessera/brain-ui-kit exports ComposerRow (left?, right?), the row
between the message area and the composer, and SessionStrip (sessions,
now, optional keyboardOpen and formatClock), the row's left half (D52
§3–4 in the design-kit record). A WorkingSession is
id, label, state (one of WORKING_STATES: needs-you, failed,
unconfirmed, running, queued, unknown, cant-check, done,
cancelled) and onOpen, with optional need, outcome, queueNote,
reason, startedAt and endedAt. describeWorkingSession(session, now, clock?) returns the WorkingSessionView the strip prints: word, detail,
tone, icon and the accessible name. No age or clock time is printed
from a null or absent timestamp. ListRowProps gains four optional props,
density?: "pill", name, tabStop and onFocus. IconName gains
attention, stopped, unheard, working, unproven, unreachable,
finished and withdrawn. A ListRow given none of the new props renders
as before.
@schlessera/brain-ui-kit extends ApprovalCardProps with optional
onAlwaysAllow?: () => void, children?: ReactNode, wrapHeader?: boolean
and shortcuts?: { allow: string; deny: string }. onAlwaysAllow offers a
remembered-grant action only when supplied; the host determines its eligibility.
children carries actual tool input or permission details. wrapHeader lets
tool names and full targets wrap in the header. shortcuts prints decision
hints while preserving the decision's accessible name; it installs no key
handler. Existing props and callbacks remain compatible. These additions ship
in a minor, with the public declarations recorded in api-report/.
SearchResultCardProps in @schlessera/brain-ui-kit gains optional
segments?: readonly {text: string; hit: boolean}[], title?: string,
type?: string and active?: boolean. Supplied segments replace the
single before/highlight/after snippet and preserve each matched span;
an empty array clears the snippet. Omitted segments retain the existing
single-highlight API. active draws the surrounding list's current
keyboard selection. Existing props and their defaults remain compatible.
The Search panel and both backend search-output renderers adopt the card. They clear absent scores and snippets instead of using presentation defaults. The existing search tool and HTTP result schemas are unchanged; pi's formatted output carries no score. Incompatible, ambiguous, clipped and failed tool output retains a readable original-text fallback. File opening uses the current root's authenticated file viewer; unsafe paths have no open action.
The maintainer-approved minor removes StreamingAnswer's prototype defaults
for phase, target, elapsed time, answer text and cost. Callers supply those
facts explicitly; omitted facts draw no sample values. Only the phase word
is announced in the polite live region, while elapsed ticks and targets remain
outside it. StreamingAnswerProps gains optional pulse?: boolean; false
selects a static status dot. Hosts retaining the former sample presentation
must pass its values explicitly. The runtime uses the component for waiting
status while preserving rich thinking/tool/prose rendering and the composer's
single Stop; stream and cancellation schemas are unchanged. The minor changeset
records this approved migration, and api-report/ records the added prop.
ServerConfig in @schlessera/brain-ui-server gains optional
exec?: ExecWrapperConfig ({ wrapper?: string; killer?: string }), and
createBrainClient's options gain optional exec. resolveServerConfig(env)
always sets exec from that record's BRAIN_UI_EXEC_WRAPPER and
BRAIN_UI_EXEC_KILLER. Every brain CLI and repository-script spawn of an app
goes through its configuration's wrapper, never the process environment's, so
two apps in one process keep separate wrappers. A configuration without
exec keeps the process environment's wrapper, resolved once by
createApp(); a client built without exec resolves it once at construction.
Neither ever spawns unwrapped because the field is absent. A relative wrapper
path now refuses at resolveServerConfig() instead of failing each spawn.
@schlessera/brain-ui-kit exports BrandMark (variant?: "mark" | "lockup",
size?: number, label?: string), which renders the accepted logo (#1423)
inline with the theme's ink and amber tokens. Below 24px the mark uses the
small-size master; without a label it is aria-hidden, with one it is
role="img" with that name. No prop changes the artwork.
The new React-free @schlessera/brain-ui-kit/brand entry exports
BRAND_MASTERS, the file names of the thirteen master SVGs, and
BRAND_ASSET_SPECIFIER. Each listed file resolves as
@schlessera/brain-ui-kit/brand/<file> through the ./brand/* export, which
maps to the published assets/brand/ directory. A listed file's name and
presence are the promise; its bytes change only with a newly accepted design.
The entry also exports BRAND_RASTERS, the raster files: favicon.ico (16,
32 and 48px PNG images), apple-touch-icon.png (180px, opaque),
icon-192.png, icon-512.png and icon-maskable-512.png (512px, the mark
inside the central 80% circle), rendered from those masters, and
social-card.png (1200x630) and social-card@2x.png (2400x1260), reviewed
raster masters (#1428). The social
card was first rendered from a social-card.svg master; that file is removed
before any release shipped it.
WEB_APP_MANIFEST_ICONS is a web app manifest's icons array over them
(src, sizes, type: "image/png", purpose: "any" | "maskable"),
WEB_APP_COLORS its theme_color and background_color (#0c1417), and
HTML_ICON_LINKS the head <link>s (rel, href, optional type and
sizes). Every src and href is a bare brand file name.
EmptyStateProps in @schlessera/brain-ui-kit gains optional
brand?: boolean. Set, the empty state shows BrandMark on a plain surface
tile in place of its icon medallion; icon and the tone's tint are then not
drawn. An empty state without it renders as before.
| Consumer | Surfaces used |
|---|---|
brain-ui (packages/ui-server/src/brain/client.ts, packages/ui-server/src/core-queries.ts, packages/ui-server/src/cron/emit.ts) |
CLI --json commands, the content-index query API through an optional core peer (voice keyterms and the knowledge graph), file paths |
| Coding-agent sessions (MCP) | MCP server tools, CLI |
| Cron on a hosting container | brain maintain, module cron entries (brain jobs scrape …) |
Approved pre-1.0 breaking behavior change (#406): judgment reranking now
requires reranker.enabled: true in canonical brain.config.ts or
brain.config.json. Omitted or false means off, even with credentials, a
provider, --rerank jev, MCP rerank: "jev" or BRAIN_RERANK_MODE=jev.
The boolean is runtime validated; provider is optional (default jev).
The same setting governs CLI search/context/process, MCP search/context,
eval and configured library/backend access. First-party hybridSearch
injection (through the unsupported @schlessera/brain/internal entry) also
requires SearchDeps.rerankerEnabled: true; provider construction through
resolveReranker alone does not activate it. rerankSetup forwards the flag.
Off uses local lifecycle heuristic ordering, or retrieval order for none.
An explicit model-mode request warns and falls back; eval refuses disabled
flag/environment requests with exit 2 and no score, including context eval.
Dry-run previews can be constructed while disabled or keyless and send
nothing. Enabled behavior retains missing-key fallback, deadlines,
cancellation, exclusions, provider-result validation and degraded warnings.
No JSON envelopes, MCP names/schemas or frontmatter fields change.
Migration: add enabled: true to retain former credential-triggered model
ordering. Toggling it off preserves provider/model/key-variable/exclusion and
bound fields; re-enabling reuses them. New CLI processes reload config;
running MCP/embedding hosts restart or rebuild their context from newly loaded
config (TypeScript imports require process restart). See
configuration and the
decision for the retained provider seam
and measurement limits.
Approved pre-1.0 breaking API change (#286):
createApp(options?: CreateAppOptions): Promise<BrainUiApp> from
@schlessera/brain-ui-server replaces the synchronous factory. Consumers
must await createApp(...) before reading the handle or passing its fetch
and websocket to Bun.serve. Startup refusals now reject the promise, so
catch them around the awaited call. There is no synchronous compatibility API.
The experimental BackendModule.probeRuntime?(context: BackendModuleContext)
from @schlessera/brain-ui-sdk/server now returns
Promise<BackendRuntimeReport>. Descriptor authors make their implementation
asynchronous and reject when their required runtime is unavailable. The
server awaits active backend probes, then the brain CLI version probe, before
opening its database or starting application services. Injecting a registry
continues to bypass first-party backend probes without explicit backend
requirements, while retaining the brain CLI check and configuration validation.
Both built-in version probes run only --version, with a five-second deadline
and up to 250 ms of cleanup, subject to event-loop scheduling. At the deadline
cancellation sends SIGKILL through killWrapped, including the configured
group killer while the command is running. Killer-helper execution is bounded
to 200 ms for these probes. Probe settlement never waits indefinitely for exit,
inherited output pipes or the helper; incomplete cleanup is reported as
unconfirmed, not as proof that work stopped. Turn cancellation retains its
existing behavior. Printing a version before timing out does not count as
success. Claude runtime failure refuses startup; an unknown or unreadable
brain CLI version warns and continues by default, and a known incompatible
version refuses. Version report shapes and the default CLI floor are unchanged.
Additive host requirements (#642): CreateAppOptions.versionRequirements
accepts optional brainCli and backends[backendId].sdk/runtime full SemVer
minima. They cannot weaken package constraints. Validation and requested
identity checks precede app resource creation; unsupported injected backend
verification refuses startup explicitly. An explicit content-CLI minimum
refuses unknown versions and revalidates the actual executable before client
and streaming-sync invocations. Backend minima travel through the existing
BackendModuleContext.versionRequirements to probes and construction. The
shared server SDK helper retains every owner/declaration, upper bounds, OR
grouping and normal per-tuple prerelease opt-in. Compatibility remains separate
from measured status and index schemas; existing wire/report meanings hold. Backend
factories also accept the optional pair and enforce owning SDK requirements
on their actual imported copies (#643). Pi's boot entry adds its primary SDK
identity without a runtime or measured field: it has no separately spawned
executable. Existing Claude report fields retain their meanings. With a Claude
runtime requirement, start/resume re-probe the SDK-selected executable and
withhold input until the bounded SDK handshake completes. A contradictory
later init version ends the turn through the existing error/result envelopes;
compatibility adds no wire fields and changes no measured verdict.
Additive option (#558): linkPolicy?: "visible-destinations" shares the
concrete template policy. Omitted preserves existing renderer/CLI behavior.
Enabled retains accepted web/mail navigation with validated visible destination
text, makes refused/unavailable targets inert and checks final screen/print
visibility with document scripts disabled. A destination still obscured/clipped,
or beyond the PNG capture bound, rejects rather than emitting an unchecked
artifact. PDF checks also cover the finished annotation's own complete tagged
disclosure, physical page bounds and a 9-point legibility floor; print clipping
or shrinkage rejects. Documents with accepted links also reject if a content
security policy blocks the protective stylesheet. Existing render budgets apply.
No target is fetched by this policy.
The app's POST /api/render always enables it server-side for both formats,
including raw/full/bare HTML; request content cannot opt out. Its injected
AppRenderer implementations must honor the supplied option's final-visibility
check. The request and response envelopes are unchanged.
The deliberately public createRenderer API of
@schlessera/brain-render-puppeteer accepts RendererOptions and returns a
Renderer: renderPng({ html, width?, linkPolicy? }) and
renderPdf({ html, width?, linkPolicy? })
resolve to Buffer; shutdown() is terminal. Width is clamped to
320–4096 CSS pixels. The package README documents the remaining options.
RenderOptions, ENV_VARS and EnvVarSpec complete the package's public
entry; see package entry points.
Pre-1.0 breaking behavior change (#72): renderTimeoutMs now bounds page
creation, setup and PNG/PDF production, excluding queue waiting and browser
acquisition. queueTimeoutMs bounds waiting for a concurrency slot;
browserTimeoutMs bounds acquiring the shared browser, including a cold
launch. Defaults are 30_000, 60_000 and 30_000 ms respectively, so the maximum
request duration is their sum (120_000 ms), subject to event-loop scheduling.
Budgets are positive integers within the JavaScript timer range. Timeout errors
identify queue, browser acquisition or rendering; exact prose is not a machine
schema. Expired queue entries are removed and timed-out calls release capacity.
Shutdown rejects queued/new calls, drains active phases and bounds browser
closure to another 2_000 ms. It therefore completes within
browserTimeoutMs + renderTimeoutMs + 2_000, subject to scheduling. Default
network denial, disabled scripting and opt-in sandbox removal retain their
policy. The rationale and measurements are in
renderer-budgets.md.
@schlessera/brain-scrape adds ScrapeClientOptions.addressPolicy
("any" | "public-only", default "any") and ScrapeClientOptions.lookup.
It also exports the NonPublicAddressError class and the AddressPolicy,
AddressLookup and ResolvedAddress types. ScrapeClient exposes a readonly
addressPolicy. With "public-only", the client refuses a non-public
address before robots.txt, pacing or the request. It checks the initial
request, every redirect hop and the robots.txt fetch, whatever
respectRobots is set to. Every resolved address must be public. On the
native path the connection goes to the checked address, while the original
host name travels in Host and the TLS server name. The proxy path cannot
pin the address and documents its remaining window in the package README. A
supplied robots cache must carry the same policy, or the constructor
throws a TypeError.
RobotsCacheOptions gains userAgent, maxBytes (default 1 MiB),
maxRedirects (default 5), addressPolicy and lookup, and RobotsCache
exposes a readonly addressPolicy. The network robots.txt fetch now sends
the client's User-Agent instead of Bun's default. It follows redirects one
hop at a time, holding each hop to the policy, and refuses an oversized
body. A refusal is a failed fetch and keeps the existing permissive
outcome. ScrapeClient and createBrowserSession pass their User-Agent
into the cache they construct. Every HTTP redirect to a URL with embedded
credentials is now refused. All of this is additive and ships in a minor.
Browser navigation has no address policy.
@schlessera/brain-scrape's BrowserSession.load checks robots.txt and applies
per-host pacing before dispatching each HTTP(S) main-frame navigation: the
initial URL, every redirect target and subsequent navigation during the load.
BrowserSessionOptions.userAgent is both the sent identity and the robots
matching token, defaulting to the package's identifying User-Agent.
Scripts, images, child frames and page-generated API requests are outside
these checks and pacing. The existing rule against intentional circumvention
still applies; this is not a browser network sandbox.
PageRequest.allowDisallowed is an explicit per-call option, default false,
for an owned host or written site permission. It authorizes only the
requested URL's original origin, never a cross-origin redirect, and never
persists into another call. Ordinary pacing and usable Crawl-delay still
apply. HTTP's respectRobots/SCRAPE_RESPECT_ROBOTS opt-out does not disable
browser enforcement. No session-wide browser override is provided.
Sessions own a robots cache and limiter by default; optional robots and
rateLimiter inputs share the run's objects. ScrapeClient exposes readonly
robots, rateLimiter and userAgent for this purpose. runAdapters and
the jobs runner share these objects when constructing the browser. A supplied
preconstructed BrowserSession retains the policy state chosen by its caller.
RateLimiter.acquire(host, delayMs?, signal?) and an injected clock's
sleep(ms, signal?) accept cancellation without granting a request or
letting another waiter overtake a preceding acquisition.
pageBudgetMs bounds the whole page operation after concurrency admission,
including acquisition, policy waits, navigation and extraction. A refusal
or timeout rejects the load, closes its page and releases capacity. Jobs
report a board whose pages are all refused as not_run with errors and
persist failed, using the existing JSON/status shapes above.
This pre-1.0 tightening replaces unenforced browser navigation and the implicit Chrome User-Agent. Consumers needing a disallowed navigation must provide the authorized per-call option; HTTP behavior is preserved. The scraping-politeness record records the approved request boundary and override policy.
The concrete synchronous @schlessera/brain/queries entry supports
readGraphMeta, readGraphClusters, readGraphNeighborhood,
readGraphDiscovery, readGraphMaintenance, readLinkWalk,
readVoiceVocabulary, listIndexDocuments, findIndexDocuments and
readStorageReferences. The additive Storage operation takes {brainPath, path}
and returns the common query envelope with {sources: [{path, indexedAt}], truncated}.
It limits resolved incoming sources to 200, reads index metadata only, and refuses
a symlinked index. Zero observed sources never establishes that a file is unused.
Its complete API specification is part of this
contract: signatures/reachable exports, validation/defaults/caps/order,
detached result and safe error envelopes, feature compatibility, read-only
per-operation snapshot lifetime and replacement behavior. Ordinary conditional
exports ship TypeScript source, JavaScript and declarations from the same build.
Supported result/behavior additions ship in a minor; changes follow this file's
versioning policy. The SQL-boundary ruling keeps
the direct-SQL guarantees below binding during the separate consumer migrations.
No SQL/handle/provider interface is exposed by the query entry.
The pi backend's brain_list and brain_graph use these results in process,
without loading executable config or providers for those two reads. Their
existing pi text/details envelopes, listing default 20/cap 100, graph
default 1/cap 4, direction, ordering and truncation remain unchanged.
An absent indexed graph path still returns empty edges and nodes.
Index compatibility, WAL snapshot consistency and per-call replacement checks
belong to core. Missing indexes retain the brain index instruction; incompatible
or corrupt indexes request brain index --force; busy indexes request a retry;
unavailable indexes and invalid inputs produce generic errors. Pi throws these
sanitized errors through its existing tool executor error path, never SQLite
diagnostics. Query input validation follows the linked API specification.
@schlessera/brain/internal is an explicitly unsupported first-party entry,
with no compatibility guarantee; use the same lockstep package version. Its
archiveDocument, assembleContext, hybridSearch, indexAll, ingest,
loadVecSupport and openDatabase exports serve pi's remaining search/context
and write implementation. They may accept native handles and are not supported
query APIs. This scoped classification added the internal entry and migrated
pi's imports; the export curation removed these
helpers from the ordinary @schlessera/brain entry, and moved the other
helpers pi and the modules share (path safety, scratch, generated regions,
taxonomy, context and write helpers) to the same internal entry. Modules now
take theirs from the supported @schlessera/brain/module entry
(package entry points).
Existing direct-SQL guarantees remain binding until the explicit retirement.
@schlessera/brain-ui-server reads the knowledge graph and the index-derived
voice vocabulary through this entry, never through its own SQL. It declares
@schlessera/brain in peerDependencies (*, the lockstep convention) with
peerDependenciesMeta marking it optional, so a host that wants the graph
view or keyterms must now install @schlessera/brain itself. Install the same
lockstep version as the server; the server accepts >=0.40.0 <1.0.0, 0.40.0
being the first release whose ./queries entry ships all six operations it
calls. The package ruling
approved this as a pre-1.0 break.
The peer is resolved lazily, once per app. Before use, the server checks that
the resolved @schlessera/brain/package.json names that package, that its
version is in range, that the loaded ./queries entry belongs to the same
installation, and that each feature's operations are functions — the five
readGraph* reads for the graph, readVoiceVocabulary for keyterms. A brain
binary on PATH, handwritten SQL and a per-query subprocess are never
substitutes. Published server declarations do not reference core.
When the package check fails, every other server feature is unaffected:
- All five
/api/graph/*endpoints,metaincluded, answer 503{ "error": "graph_unavailable", "reason": "core_unavailable" }after their existing parameter validation. This is a missing capability, distinct from any index state and never an empty graph. GET /api/voice/keytermsand the session keyterms serve an empty vocabulary,GET /api/voice/overrideskeeps the markdown pronunciation overrides, and the degraded result is never written to the keyterm cache; a cache left by an earlier installation is not served while core is unusable. Providers without keyterm support never resolve core.- One warning per feature records the failed check (
not_installed,identity_mismatch,version_unsupported,operation_missing,load_failed), the required range and any observed version — no paths or native messages.
With usable core, index states keep their established mappings. Graph meta
answers 200 for every index state: core's own description of a pre-graph or
uncomputed index, and available: false, reason: "schema", schemaVersion: 0
with zero counts for a missing, incompatible or corrupt index. Subgraph modes
answer 503 graph_unavailable with reason: "schema" for those, or
"not_computed"; an absent or malformed center/root path is 404 not_found. A
lock wait is 503 { "error": "index_busy" }; any other failure is a sanitized
500 { "error": "internal_error" }. An index that claims the graph layout without
its tables is refused as a whole, so its links-only neighborhood now degrades
with the other modes. Voice keeps the missing-index error, degrades an
incompatible index to overrides without caching, and reports a corrupt, locked
or unreadable index as an error. writeCache no longer persists a degraded
result for any caller, including the post-sync rebuild.
openBrainDb, withBrainDb, BrainDbUnavailableError and
MIN_BRAIN_SCHEMA_VERSION stay exported for embedders' own readers under the
direct-SQL guarantees below; the server's graph and voice paths no longer use
them.
Migrating a host: add @schlessera/brain at the server's version to the
host's dependencies (bun add @schlessera/brain), reinstall and restart. A
host that serves neither the graph view nor keyterm-capable dictation may skip
it. A warning naming core.reason at first graph or keyterm use means the
installation is absent or skewed.
These eleven seams are @experimental until 1.0: breaking changes are
minor-version events, announced in the CHANGELOG. Each declaration carries its
own @experimental tag. Related experimental declarations include:
BackendBridge, BackendCapabilities, StartTurnRequest, RendererPack,
SpeechSession, AsrClientOptions, AsrClientFactory, AsrClientRegistry, LiveConversationSession,
LiveConversationEvent, ConversationScope, ConversationWorkRef, ConversationWorkResult, AdapterResult, ScrapeContext,
RerankCandidate, RerankRequest, Ranked, AdapterStatus,
AdapterRunOptions, RunAdaptersOptions, AdapterOutcome and jobs JobAdapter.
#343 selects deliberate
public signatures, including their reachable types, for 1.0 stability; the
package entry points section states the boundary.
extending/README.md says what each one swaps.
The client/server wire protocol is a machine contract rather than an experimental extension interface, including when an experimental backend implements it. Its revision negotiation and documented compatibility guarantees follow the contract-versioning rules above.
BackendBridge.pruneScratch?(produced?: readonly string[]): Promise<void>
is an optional host hook after a toolkit fallback writes scratch output.
produced contains canonical, slash-separated paths relative to the brain.
The host must retain these files for that prune pass, in addition to its
retained-conversation pins. The mask fallback supplies the mask it just wrote;
its adapter-specific reported path is unchanged. An omitted argument adds no
producer pins, and the hook retains its non-rejecting behavior. This additive
optional parameter does not change the server-owned applyImageMask route.
| Interface | Imported from |
|---|---|
EmbeddingProvider |
@schlessera/brain |
CompletionProvider |
@schlessera/brain |
AgentRunner |
@schlessera/brain |
SkillEmitter |
@schlessera/brain |
Reranker |
@schlessera/brain |
AgentBackend |
@schlessera/brain-ui-sdk/server |
SpeechProvider |
@schlessera/brain-ui-sdk/server |
LiveConversationProvider |
@schlessera/brain-ui-sdk/server |
AsrClient |
@schlessera/brain-ui-sdk/client |
ToolRenderer |
@schlessera/brain-ui-sdk/client |
ContentRenderer |
@schlessera/brain-ui-sdk/client |
SiteAdapter |
@schlessera/brain-scrape |
tests/seam-list.test.ts fails when this table, the one in
extending/README.md and the tags in the source disagree.
The speech stability ruling
selects SpeechProvider and AsrClient, including their reachable authoring
types, for stability at the actual 1.0 transition. They remain experimental
before that transition. The decision preserves
dictation's current meaning beside the separately registered
live conversation interface; no TTS or speech-side tool/permission authority is added.
CreateAppOptions.speechProvider?: SpeechProvider in
@schlessera/brain-ui-server accepts an implementation by value. It takes
precedence over automatic Deepgram discovery; an explicit
config.voice.provider must equal the supplied id. Without a value, existing
built-in rules remain, including opt-in browser speech and failure when no
provider is configured. The mounted protected POST /api/voice/session uses
the supplied implementation. It validates a nonempty trimmed lowercase id,
four boolean capabilities and callable createSession before invoking it.
The returned SpeechSession has a string URL, finite nonnegative expiry,
optional string token and optional string-record params. Empty URL/zero expiry
remain supported for browser/local sessions. Validation, mismatch and provider
failures return the existing 500 { error } envelope without fallback.
The deprecated token route remains Deepgram-specific.
The route builds keyterms only when supported, snapshots provider identity and
capabilities, and publishes VoiceSessionResponse with connection material.
The client's matching AsrClientFactory receives that whole session through
an AsrClientRegistry; UI applications register on their root's asr before
mounting. Unknown ids throw rather than select another client. External ids
should be distinct from the UI's installed built-in factories. A provider using
a built-in id must remain compatible with that built-in client's protocol.
AsrClientOptions, AsrEvent/AsrPartial/AsrFinal, VoiceSessionResponse,
SpeechCapabilities and SpeechSession remain reachable public authoring
types. The signature inventory includes the client factory and registry.
runSpeechProviderContract and runAsrClientContract are published from
@schlessera/brain-ui-sdk/testing with their probe/harness types and the
existing injected ContractTestPrimitives. Keyless probes exercise actual
session transports, capture/recognizer state and connection closure: complete shapes, supported
nonempty keyterms, provider failures, transcript/error callbacks, hard stop,
and a buffered final delivered before graceful drain resolves and capture
closes. These suites are the conformance floor; an adapter must also cover
its protocol-specific authentication, capture and failure behavior. The
authoring guide supplies complete public server/client
examples and points to executable harnesses and real route/Chrome integration.
The existing experimental SpeechProvider adds optional
transcribeRecording({ audio: Uint8Array, contentType: string, keyterms: string[], signal: AbortSignal }): Promise<SavedAudioTranscription>; its result is
{ text: string }. Implementations may omit it. Implementations that supply it
must send unmodified audio server → provider using server-held credentials,
reject failures, and perform no automatic retry. They must not resolve empty
text or send audio directly from the browser. This adds no provider registry
or separate seam. SpeechTranscriptionError(reason, providerStatus?) records
an explicit classification; untyped exceptions, lost/unparseable/invalid
responses and local timeouts become terminal outcome_unknown. Definitive
HTTP error responses are classified from their status: 401/403 authentication,
429 rate limit, 408/504 provider timeout, other 5xx provider error, and other
4xx media/parameter/validation rejection. A contradictory reason cannot
make a nonretryable HTTP response retryable. Providers without an HTTP status
must supply a truthful definitive reason or use outcome_unknown.
SpeechCapabilities.savedAudio?: boolean is additive. Absent/false means
unavailable to clients. The server derives it solely from a callable optional
method for discovery, session responses and uploads; a declared flag that
contradicts that method is invalid. GET /api/voice/capabilities is protected,
read-only, no-store, returns { providerId, capabilities }, and mints no
streaming token or provider request. Selection/validation failures retain 500
{ error }. Deepgram is the only built-in saved-audio provider; Web Speech
omits the method and reports false. Deepgram sends the raw container body to
POST /v1/listen with model=nova-3, smart_format=true, mip_opt_out=true,
server Authorization: Token …, the original media type and no encoding.
Keyterms use a conservative aggregate 500 UTF-8-byte budget including
separators, below the provider's 500-token ceiling. No live vendor call is
part of the conformance proof.
GET/PUT/DELETE /api/voice/recordings/:recordingId/transcription are
supported HTTP operations, specified in the HTTP reference.
All require a usable account principal. Agents/system principals are refused;
ambient modes use their existing account mapping (proxy users are distinct).
The host derives ownership from its existing account key; it never trusts a
client account id. The calling principal is re-resolved inside the claim
transaction after reading the bounded body. Expired authorization cannot read
a completed result. A recording id is host-wide unique, with an immutable
verified hash and owning account, rather than partitioned by login principal.
Cross-account reads and mutations return 404 without result disclosure or
provider dispatch. Reauthentication as the same account reuses the receipt.
RecordingTranscription is { recordingId, sha256: string|null, providerId: string|null, status, attemptId, retryCount, failures, text?, failure?, disposition? }. Status is transcribing | done | failed | outcome_unknown | consumed. Each failure is { reason, retryable, providerStatus? }; reason is provider_error | rate_limit | provider_timeout | media | parameters | validation | authentication | outcome_unknown.
failures retains each failed attempt's classification plus its attemptId.
retryCount counts explicit retries (0 initially, maximum 3); it and the
failure history survive consumption. Disposition is accepted | discarded.
Only done carries transcript text. Hash/provider can be null on a tombstone
that precedes the first upload. Recording UUIDs are canonicalized to lowercase
for every route and receipt, so alternate case spellings share idempotency,
account ownership and tombstones. GET returns the receipt or 404.
Only an immediate transaction inserting a claim may dispatch the provider.
A repeated same-hash done upload returns the same receipt and text. An
in-progress receipt gives 409 transcription_in_progress; a different hash
gives 409 recording_hash_mismatch. A failed plain replay gives 409
transcription_failed. PUT ?retry=<failed attemptId> atomically claims a
new attempt only for a retryable failure: provider 5xx, 429/rate limit or a
definitive provider timeout including Deepgram 504. Each retry is explicitly
user initiated. Media, parameter, validation and authentication failures are
never retried; they give 409 transcription_not_retryable. Explicit terminal
classifications remain terminal even if an adapter attaches a conflicting
transient HTTP status; transient classifications cannot override definitive
authentication or validation HTTP evidence. A stale token gives
409 transcription_retry_stale; once three retries are used, a current token
gives 409 transcription_retry_limit. Simultaneous retries can claim only
one attempt. outcome_unknown gives terminal 409
transcription_outcome_unknown, never any retry. A server restart makes any
leftover transcribing receipt unknown. Completion updates only its still
active attempt, preventing stale replies from overwriting a tombstone.
Provider rejection is a 200 receipt with failed, not a lost HTTP response.
Errors use { error, message, receipt? }; stored conflict/terminal responses
include the receipt. Authentication errors retain the existing guard envelope.
Receipts live indefinitely in the UI's operational database, never brain.db.
They outlive login rows and have no TTL or count-based pruning. DELETE erases
text but retains a consumed tombstone; delayed PUTs give 410
transcription_consumed. A DELETE before the first claim inserts a hashless
tombstone. The guarantee lasts for the lifetime of that operational database;
resetting it is the boundary. Recording ids are random client UUIDs.
The paired UI uploads only after the confirm row's Upload and transcribe
action, and uses a recording-id Web Lock. Reconnect/reload/sign-in never
uploads audio or sends a chat message. Recovery may read GET status; explicit
accept/discard may queue a durable DELETE while offline and replay that deletion
after reconnect. Success is saved to the owning local partition before review
is ready, and audio remains until explicit acceptance/discard. Unassigned
recordings require explicit association before upload. A transport loss during
partial upload retains the original bytes/hash and asks for fresh consent;
an uploaded request with an unconfirmed reply stays awaiting status. Unknown
outcomes and exhausted/nonretryable failures keep audio and offer no Retry.
The root's existing request injection accepts optional onUploadProgress
(callback percentage); its default uses XHR for actual byte-upload progress,
with the same account epoch and 401 guards as fetch. Injected transports that
handle this callback must report transferred bytes, never fabricated completion.
runSpeechProviderContract adds optional-method probes of unchanged bytes,
media type, supported keyterms, nonempty results and rejection. A provider
implementing the method supplies recording() transport observations in its
keyless probe. Existing providers that omit it remain valid.
Under the 2026-09-28 adoption ruling,
all ten jobs boards implement SiteAdapter<RawJob> and production jobs
scraping uses the shared runAdapters runner. The seam remains experimental
until 1.0; the decision records the
boundary and alternatives.
scrape(ctx, options)returnsAdapterResult<T>withitems, requiredstatus, optional opaquecursor, anderrors.AdapterStatusisok,empty,unparseableornot_run.okpermits partial items with errors;emptyrequires positive readable-page evidence, never only zero items. Readable but unrecognized pages areunparseable; no readable result isnot_run. Jobs retains its existingPageLedgerchecks and diagnostic text.AdapterRunOptionssuppliesincremental, previouscursor,queries,proxyand per-sitefetchoverrides. Adapters forward applicable options per request; one source's options never mutate the shared HTTP client. Jobs preserves explicit board pacing floors; a run may request a longer delay.runAdaptersexecutes in selection order, supplies the shared HTTP client, and supplies browser context only to adapters declaringneedsBrowser. Unavailable required transports, thrown options or adapter failures yieldnot_runwithout aborting later sources. A browser created by the runner is closed infinally; a caller-supplied session stays caller-owned. Constructed browsers share the HTTP client's robots cache, limiter and User-Agent unless their explicit browser options override them. Browser navigation retains the existing policy.JobAdapterin module-jobs extends the generic contract with source, tier, detail-host allowlists and detail-fetch metadata. Jobs owns cursor/logging, enrichment, scoring and database cleanup. Every selected source still has the existing CLI report row and status/error/cursor semantics above.
The approved pre-1.0 API break requires consumers to add AdapterResult.status,
use JobAdapter instead of removed ScraperAdapter, and use generic
AdapterResult<RawJob> instead of removed jobs ScrapeResult. Call
scrape(ctx, options) instead of bind(ctx).scrape(opts), pass cursor instead
of lastCursor, read items instead of jobs, and take source identity from
the adapter or runner outcome. ok([]) reports diagnostic unparseable, not
confirmed empty. The CLI { report } envelope does not change.
Additive in #1679: a backend may expose stateRoomAllowanceBytes, its resolved
admission allowance for presentation. A resumed ordinary turn may include a
StateRoomObservation on its result, using its actual admitted inputs; omission
means unknown. The host reports timestamped allowance metadata and the latest
observation without widening any capability or grant. The public protocol
exports stateRoomWarning and stateRoomCopy so context and UI share the
approved single 75% band and factual copy. See the
wire contract.
Breaking pre-1.0 hosted pi execution (#1038, isolated-writer ruling on #674).
Each ordinary or autonomous pi turn runs its agent session, resource loader,
extensions and shell children inside the shared pre-initialization worker
boundary. The authoritative brain is read-only there; shell writes land in fresh
scratch. BRAIN_ROOT keeps CLI reads rooted in the brain, while CLI writes retain
the hosted read_only_brain refusal. write_file, edit_file and curated writes
require the server application route and retain its existing approvals.
The CreatePiBackendOptions fields and AgentBackend, frame, session-list and
history shapes are unchanged. The documented sessionDir default changes from
<brainPath>/.brain-kit-ui/sessions to
~/.local/state/brain-kit/pi/<SHA-256 of canonical brain path>/sessions.
Configured session and native pi agent directories must resolve outside the
brain and its ancestors; aliases refuse. Move existing session JSONL into the
new default or an external configured directory to resume the same ids/history.
The server persists ordinary transcripts over pipes and retains committed edit
receipts across cancellation; autonomous turns remain nonpersistent. No worker
pool, new public option or extension seam is added. Pi still refuses voice turns
because it declares no voice membership. This change does not establish #676's
complete credential/egress containment or enable autonomous production dispatch.
Every conforming AgentBackend must honor a requested
StartTurnRequest.enforceAllowedTools or StartTurnRequest.noGrantSurface, or
reject an unsupported restricted turn with BackendRequestError before
runtime acquisition, protocol frames or effects. Silently ignoring either is
nonconforming. Both inputs remain optional; absent or false retains ordinary
turn defaults. This pre-1.0 tightening follows the
maintainer's ruling
and decision record; it does not freeze all
experimental extension interfaces.
Enforcement requires a permission decision for an off-allowlist tool, including
runtime auto-approval shortcuts. Such requests carry
outsideEnforcedAllowlist: true; a denial prevents the tool body from running
and reaches the model as a denial. noGrantSurface is valid only with
enforceAllowedTools: true; otherwise the turn rejects before execution. In a
valid no-grant turn, both tool grants and command confirmations are denied
promptly without an unanswered bridge request, with a named tool error and
permission_denied activity evidence.
StartTurnRequest.posture: "voice" (additive, #957) follows the same rule: a
conforming backend runs the turn on its declared voice allowlist or rejects it
with BackendRequestError before execution; it never runs it on the ordinary
allowlist. See Live conversation.
runBackendContract in @schlessera/brain-ui-sdk/testing checks this baseline
with a required permission probe and observable tool-body effects. Both
first-party backends demonstrate enforcement with keyless scripted runtimes,
rather than passing solely by rejecting every restricted turn.
runBackendModuleContract checks a defineBackendModule descriptor's nonempty
profile parsing and resolution, confirmation-pattern defaults/disablement,
typed invalid-JSON failures and occupied-id collisions. A backend that passed
the earlier suite may fail these stricter cases; the testing harness's
permission member is newly required.
Module manifests are two-phase: defineModule({ name, configSchema?, setup }),
where setup(validatedConfig) returns the contribution. The contribution is
schema-validated at load — unknown keys are load errors — and module commands
receive { root, json, config, taxonomy }, so a command must NOT re-read
brain.config itself. The explicit brain travel migrate source-edit job
above is an exception: it never computes effective configuration or
serializes evaluated TypeScript. See modules.md.
brain hygiene reconcile joins brain validate's corpus checks into hygiene
detection through a published map from each validation rule to a category and
ID evidence. It never reads a validation message. The map, the per-category
evidence fingerprints and the invalidation rules are defined in
the hygiene review decision.
Two IDs are fixed for review handlers: a broken wiki-link is
hygieneId("broken-link", path, target), whether validation, audit or both
report it, and a missing or invalid required frontmatter field (title,
type, created, updated, tags) is hygieneId("required-field", path, field).
The markdown under context/hygiene/ gains disposition semantics. Each open
entry's generated lines add - **Sources**: and - **Fingerprint**:.
dismissed.md holds dismissed entries under ## Dismissed; _index.md's
counts add - Dismissed: N. A disposition made by dismiss or snooze writes
dismissed-on: or snoozed-on: (an ISO timestamp), an optional reason:, and
disposition-fingerprint:, the fingerprint it applies to. When that
fingerprint no longer matches, reconcile reopens the finding with an
invalidated: receipt. An entry moved by hand has no recorded fingerprint and
keeps its earlier behaviour. A date-only until: keeps its meaning, due on that
day. These files are the only store of a disposition: brain index --force
does not touch them.
brain validate's output and exit status do not change: a dismissed or
snoozed error is still reported and still exits 1.
brain hygiene next --json refreshes the disposable index and reconciles the
markdown log using the same validation join and disposition invalidation as
reconcile. It selects only currently detected, open, non-informational
canonical findings. Unchanged dismissals and future snoozes are excluded;
unchanged snoozes return at their exact due instant. Resolved entries stay out
unless their problem is detected again and reconciliation reopens them.
Informational findings (including TODO/VERIFY) remain in the log and count as
informationalNotShown; they do not prevent a review from completing.
The finding retains id, category, path, stable evidence, message,
severity, raw urgency, sources, fingerprint, and fingerprintFields.
It adds a plain title, line (1-based in the actual file, or null), field
(for field findings, or null), excerpt (one line, at most 60 Unicode code
points around the first literal evidence occurrence, or null when absent or
unreadable), handler (the legacy manual compatibility marker),
firstSeen, and the log's invalidation receipt, if present. Reads for excerpts
stay inside the brain root. Missing fields are located by field name rather
than an invented line. Selection performs no content repair.
Additive in #1475: every selected finding also carries handlers: RepairHandler[]
from the existing fixed repair registry (see bounded repair descriptors).
Broken links offer link-note and link-text, plus link-suggested only for
exactly one deterministic slug, alias or exact-title match. Required fields
carry their typed input schema, including configured type enum values and
string-array tags. Unsupported findings carry the manual descriptor with its
file, line and explanation. This is the complete supported discovery projection;
check still returns only its existing single descriptor. Discovery adds no
content, repair-receipt or disposition write beyond next's existing index
refresh and reconciliation. Identity, fingerprint, ordering, reason and counts
are unchanged. Empty-backlog and configuration/check-blocker envelopes are
unchanged and do not carry a handler array.
Ordering is lexicographic: severity rank, urgency rank, oldest firstSeen,
then canonical ID in code-unit order. Ranks are ordinals, never weighted scores.
The most severe contributing source determines severity; categories and model
judgments assign no priority.
| Source input | Severity class | Rank |
|---|---|---|
Validation level: error; audit/module/extra severity: error |
error |
0 |
Validation level: warning; audit/module/extra severity: warning |
warning |
1 |
| No source severity (including silent edits/index table lag) | unknown |
2 |
Audit/module/extra severity: info |
info |
3; counted, not selected |
| Explicit source urgency | Rank |
|---|---|
immediate |
0 |
overdue |
1 |
due |
2 |
upcoming |
3 |
Absent, unknown, or any unsupported value |
4 (unknown) |
Urgency is known only from a candidate's explicit urgency input (currently
--extra candidates); no deadline, category, message, age or snooze date
is used to infer it. If canonicalization joins multiple source urgency values,
the highest recognized urgency wins; unsupported values contribute unknown.
Raw urgency remains available separately on the finding. Missing first-seen
sorts after dated peers; its age is null. Future dates have age zero.
priorityReason is { severity: "error"|"warning"|"unknown"|"info", urgency: "immediate"|"overdue"|"due"|"upcoming"|"unknown", ageDays: number|null, newerWithSameRank: number, tieBreak: "only-finding"|"severity"|"urgency"|"age"|"identity" }. Age is whole elapsed
24-hour periods since the first-seen UTC day. newerWithSameRank counts eligible
peers with the same severity and urgency ranks and a later first-seen date.
tieBreak names the first differing comparison against the runner-up, or
only-finding when there is none. Neither field changes priority.
Counts are durable backlog totals after reconciliation, including the selected
finding in eligibleRemaining. fixed counts resolved log entries (including
auto-disappeared); it is not a session counter or proof of a handler's
successful repair. dismissed and snoozed count entries in those states.
nextSnoozeDueAt is the earliest known snooze instant, or null; unknown due
times remain snoozed and do not invent a date. informationalNotShown counts
currently detected open informational findings, once per canonical finding.
A configuration blocker has the loader's message, the detected config path
(or null), and line/column only when the original error identifies that
configuration file; loader implementation stack locations are excluded. It
never returns an empty-backlog receipt and writes neither the log nor the
index. Only hygiene next tolerates an invalid config; reconcile, dispositions
and list retain their refusal. With no configuration file, next retains the
CLI's missing-config write refusal. A check blocker lists unavailable checks;
reconciliation still preserves unseen entries under its existing failure
rules, and selection never claims completion from incomplete detection.
One media inventory feeds brain sync's count and hygiene review. It holds
two populations: the working tree's uncommitted MEDIA and LARGE files as
brain sync assess classes them (never Git-ignored or media.ignore files),
and the regular MEDIA/LARGE files under .brain/scratch/ that the scratch
pin snapshot lists, even when media.ignore matches them. The scratch area is
never walked. At most BRAIN_MEDIA_PINNED_PATH_LIMIT (5,000) distinct pinned
paths are read per inventory. A missing, unreadable, redirected or malformed
snapshot admits no scratch file (links.state: "unknown"). A valid snapshot
whose scan is not finished (ready: false) admits its listed paths
("incomplete"). Neither state, nor tooLarge, ever claims that every
conversation link was checked.
Each file is one finding of category media, identified by its exact raw
repo-relative path, with source hygiene/media (informational) and/or
hygiene/large (warning). A file reached only through a pin has
hygiene/scratch-media/scratch-large instead: it appears in review and never
in the count. Its fingerprint fields are sha256, bytes, classes, policy
(the matching media.track/media.ignore entries), refs, pins (the sorted
session keys whose pin list holds that exact path, so an unrelated
conversation never changes it), severity and urgency. refs is the
reference-inventory digest (#2781): none when no note references the file, a
16-hex digest of each reference's note, form and destination as written (line
numbers left out), or too-large/unscannable when the bounded scan was
refused. One scan of the brain's *.md notes outside hidden directories and
context/hygiene/ finds Markdown inline links and images, <…> destinations,
reference definitions, wiki links and embeds, frontmatter values and HTML
src/href/poster; code is never a reference. Its bounds are
BRAIN_MEDIA_REFERENCE_LIMITS (notes 50,000, noteBytes 256 MiB, perNote
4 MiB, scanTime 20 s, refsPerFinding 500; and for an effect's plan,
rewrittenNotes 200 and preview 1 MiB, #2781). Replacing the earlier
unscanned value reopens every media disposition once.
A fifth log state, kept, holds a media original whose permanent copy was
made, in context/hygiene/kept.md under ## Kept as a permanent copy. Each
entry records kept-on:, copy:, copy-sha256:, journal: and
disposition-fingerprint:. It holds while the original's fingerprint is
unchanged and the copy is a contained regular file outside .brain/ and
workspaces/ that media.track keeps. Otherwise reconcile reopens the original
with an invalidated: … kept on <day> · changed: <fields> receipt, naming
copy-missing, copy-untracked, kept-incomplete or the fingerprint fields.
Editing the copy keeps it kept. brain hygiene keep-copy writes the entry
through reconcile's expected-fingerprint request, refusing a copy that would
not hold at once. A pruned original moves the entry to
resolved.md with resolved-by: original-pruned. kept.md is written only
once it holds an entry, and _index.md adds - Kept: N only while it exists.
An existing brain's hygiene files therefore stay byte-identical.
The count is mediaReviewCounts, read-only and shared by both callers. It
does not trust the file an entry sits in:
| Counted media finding's log state | awaitingReview |
Set aside / kept |
|---|---|---|
| open, resolved, or no entry | yes | — |
| dismissed, fingerprint unchanged (or none recorded) | no | setAside |
snoozed, fingerprint unchanged and now < dueAt |
no | setAside |
| snoozed and due, or fingerprint changed | yes | — |
| kept and valid | no | kept |
| kept and invalid | yes | — |
Hygiene review's Leave pending is per-pass operational state on the server,
not a log state. It skips a finding while its fingerprint is unchanged
(hygiene next --defer), and the finding still counts in awaitingReview.
The review state gains pass (a Start after a complete or idle review begins
a new pass and clears the list), deferred: [{ findingId, fingerprint }], a
kept counter, counts.kept, and media: { awaitingReview, setAside, kept }
from the last selection. A stored row without them reads as 0 or [].
Hygiene effects gain operation defer, and outcomes gain status deferred.
A media finding's four governed effects are Delete, Move, Persist here and
Keep a permanent copy. The CLI plans and writes them
(brain hygiene media, cli.md); the UI server owns capture,
compensation, Undo and recovery, in the bounded change journal (Rollback A).
- Capture before mutation. Under the exclusive writer gate, a user
journal turn covers every file the plan lists, and each pre-image (the
media bytes for Delete and Move, each rewritten note, the config) is
retained and verified by SHA-256 before the CLI is asked to write. A
capture the journal's 10 GiB shared cap, or its per-file copy bound, cannot
hold refuses as
capture-capacity, with nothing written. - Per-file compensation. A file is put back only while it still holds
exactly what the effect wrote. Anything else is
conflict(orleft-changedfor a folder no longer empty) and is left alone; the outcome is thenpartial. No all-file atomicity is claimed. - Undo first checks that every retained pre-image still exists and every
file still holds its post-image, and refuses before any write otherwise
(
rollback-evicted,changed-sincewith the path). It then applies the same per-file rule. Undo is not guaranteed for any period: the journal may evict rollback data under pressure. - Recovery after a restart compensates an interrupted apply, or finishes an interrupted Undo, from the durable operation row. It never re-applies.
- Keep a permanent copy records its
keptentry withbrain hygiene keep-copyonly after the copy verified (step 6b). A refused entry compensates: the copy is removed and the config restored. The original's bytes, path and pins are never touched. - Undo of Keep a permanent copy removes its
keptentry in the same Undo, after the copy and config are restored, withbrain hygiene unkeep <id> --journal <journalId>. The original is pending again, without aninvalidated: … copy-missingreceipt. An entry already gone needs nothing; any other answer makes the Undopartialwith reasonkept-entry-unremoved(orkept-entry-unknown). A compensated or recovered copy removes an entry its lost step 6b receipt may have written the same way.
Additive shapes in @schlessera/brain-ui-sdk:
| Surface | Change |
|---|---|
HygieneEffect.operation |
gains media-delete, media-move, media-persist, media-keep-copy; the effect may carry destination and premiseDigest |
InboxOption.media |
HygieneMediaPlan: the CLI plan (action, premiseDigest, destination, files, captureBytes, links, references, rewrites, blockers, configDiff, copy, resultClass, refusals) |
HygieneAction.outcome.status |
gains kept, a confirmed Keep a permanent copy |
HygieneReviewMediaUndoCommand, HygieneReviewRequest |
new (0.45.0): { operation: "undo-media", journalId }, Undo of a confirmed media effect from review (below); HygieneReviewRequest is it or the unchanged HygieneReviewCommand, parsed by the new hygieneReviewMediaUndoCommandSchema and hygieneReviewRequestSchema; hygieneReviewCommandSchema is unchanged |
HygieneReviewRead.mediaUndo |
HygieneMediaReceipt, on the undo-media response only (0.45.0) |
HygieneAction.outcome.media |
HygieneMediaReceipt: { journalId, action, outcome: "pending"|"applied"|"kept"|"compensated"|"partial"|"refused"|"stale"|"undone", reason?, refusals?, files: [{ path, state: "created"|"changed"|"removed"|"restored"|"unchanged"|"left-changed"|"conflict" }], copySha256?, undo: { available, reason?, path? } } |
A media finding's card offers Persist here, Move to a final location…, Keep a
permanent copy… (scratch only) and Delete…, before Leave pending. Persist and
Delete are planned when the card is prepared; Move and Keep a copy when a
destination is previewed (POST /hygiene/review/preview with the path as
input). A plan with refusals keeps them on the option and has no
premiseDigest, so it cannot be confirmed. Confirming applies exactly the
previewed plan: an applied effect resolves the card as fixed, a kept copy
as kept (the review's kept counter); stale and refused leave it
pending; compensated and partial are check_failed with that code. An
old server that cannot parse kept fails closed as check_failed. Recovery
confirms a kept copy only when the log's entry is kept with the same
fingerprint and copy SHA-256; otherwise receipt-unknown. Routes and their
error strings are unchanged.
Undo from review (additive in 0.45.0, maintainer ruling 2026-11-03).
POST /api/hygiene/review with strict
{ "operation": "undo-media", "journalId": "<HygieneMediaReceipt.journalId>" }
undoes the confirmed effect of a resolved media card, for the principal that
applied it, under the same preflights as the CLI Undo: every retained
pre-image still held and no file changed since. The response is the review
read plus mediaUndo, the Undo's own receipt. A refusal is a 200 result,
mediaUndo.outcome: "refused" with reason (changed-since with path,
rollback-evicted, already-undone), and nothing is written. The resolved
card keeps its status and gets the Undo's receipt by inbox replication:
outcome.status: "undone" with media the Undo receipt; check_failed with
code partial when some file could not be put back; or, after a refusal, its
original outcome with media.undo re-read. An undone effect leaves the
review's fixed or kept counter. The finding is pending again and an
active review selects it in order, on a new card. Repeating the command
returns the same receipt and changes nothing. A journalId that names no
resolved media card of this principal is review_failed (500); malformed is
400, as for every command.
A module hygiene check receives HygieneContext<C> = { queries, root, config }.
The raw db content-index connection is removed (pre-1.0 break, ruled in
#696).
queries: ContentIndexQueries is a frozen object core creates per audit, bound to
the brain root resolved once at creation. Its nine methods are those of the
content-index query API with the same options minus
brainPath, and the same validation, defaults, caps, ordering and QueryResult
envelopes. A brainPath supplied anyway (including through a getter), an
argument to readGraphMeta or a later change to ctx.root cannot redirect a
query. Each call opens and closes its own read snapshot; no handle, SQL,
statement or transaction escapes. ContentIndexQueries is a type exported from
@schlessera/brain and @schlessera/brain/queries; core does not export a
constructor. root and parsed config: C are unchanged, and checks may still
return synchronously or asynchronously.
A failed query returns ok: false with its QueryCode; a check should throw on
it, which the audit reports as the existing single module-hygiene warning and
failedChecks entry rather than a clean result. The jobs jobs-stage check does
so with the message opportunity stages could not read the content index (<code>): <action>, and its selection is unchanged: every non-archived,
non-null-status opportunity whose path is exactly status.md or ends in
/status.md with ASCII case folding, in path order and uncapped. Migration: replace
each ctx.db SQL read with the query returning the same rows and keep any path
matching in module code (modules.md).
The direct-SQL guarantees for other readers are unchanged.
Each configured module entry gains optional core-owned enabled: boolean.
Omission means active. Core validates the flag and removes it before validating
the module's domain block; dormant domain config is still validated and retained.
LoadedModule.state is supplied by the loader as "active" | "dormant";
the optional authoring property preserves constructed legacy contexts, where
omission means active. CLI, scheduler metadata, skill discovery, classifier
hints and module hygiene all read that same loaded state. Dormant types,
directory anchors, exclusions and content rules remain registered.
ModuleContribution.instructions?: { text: string } is strictly validated
authoritative context returned by setup(validatedConfig). Text is nonempty
and contains no ownership markers. Modules declaring instructions have a
lowercase name matching ^[a-z][a-z0-9-]{0,30}$; existing manifests without the
field retain their naming behavior. The optional manifest
canBeDormant?: boolean defaults to permitted; false makes CLI disable refuse
with dormancyReason?: string or a default explanation. A declared reason is
nonempty. Existing module authoring calls and required fields remain unchanged.
Toggles preflight source edits and all instruction owners before writes. Legacy mixed generated regions require explicit migration, preserving personal prose outside module-owned spans. No paragraph inference, whole mixed-section deletion or saved-prose restoration occurs. The source-preserving writer changes only a literal entry's flag; an ambiguous executable target is refused rather than serializing its evaluated config. The same-state command can synchronize context after an explicitly reviewed manual flag edit. Validation/ownership failures leave config, managed links and instruction bytes unchanged. Filesystem or emitter failures are reported as failures and require retry; success is not reported for an incomplete context sync. See the format and migration guide.
The list estimate is the sum of characters/4 token estimates of the module's
discoverable model-invocable skill descriptions under existing precedence and
its contributed instruction text. It estimates hypothetical active context even
when dormant, without attributing shared contracts, personal prose or full skill
bodies. It counts the authoritative contribution once, rather than summing copies
in different agent entry files. The existing enabled/available envelope and
declared fields remain; dormant cron metadata is empty. Namespace execution exits
1 with module <name> is dormant — brain module enable <name> without importing
or executing the module command. Running sessions keep their loaded state;
MCP startup registration reads that same loaded state and skips dormant
modules before importing tool definitions. Dormancy is context control, not
permission revocation.
ModuleManifest.settings?: ModuleSettings<C> describes a generic editor over
configSchema: data-only fields, choices, nested records, ordered lists and
record variants, plus optional module CLI actions, computed notes and a pure
migration planner. Descriptions are validated at module load. The leaf kinds
are text, number, toggle, choice, multichoice, tags and weights;
record, list and variant compose them. Unlisted representable schema
fields appear through the generic renderer. Unsupported fields show their
complete JSON value read-only; unchanged JSON subtrees retain their bytes.
Modules do not contribute React settings implementations. Content renderers use the separately declared, build-time-bundled renderer seam. A module using settings has
a lowercase name matching ^[a-z][a-z0-9-]{0,30}$.
Core reads settings/<module-name>.json, a JSON object without the reserved
enabled key. Own object keys merge recursively over the domain block in
brain config; arrays, scalars and null replace. The original configSchema
then parses the combined input and supplies defaults. Both active and dormant
modules use this path, including ordinary config checks and brain validate.
Invalid hand-edited settings fail validation rather than silently falling back.
Config reads expose effective validated module blocks. JSON never replaces
TypeScript logic or rewrites its source. Settings files and their parent may
not alias another path through symlinks; a symlinked brain root remains valid.
brain module settings <name> --json returns:
{ module, key, state, canBeDormant, dormancyReason,
schema, values, inherited, overrides, provenance, inheritedProvenance,
revision, notes, ui: { fields, actions, migration } }
schema is generated from the declaration's Zod input schema, with
unrepresentable leaves shown read-only. values are effective validated
settings; overrides are the unnormalized JSON source object. inherited
shows values without that JSON, and provenance distinguishes default,
brain-config, saved and migrated. revision is an opaque quoted hash of
the loaded config source and settings bytes. Do not derive meaning from it.
--set dotted.key=value parses a JSON value when possible, otherwise a
string; numeric path components address existing arrays. --stdin accepts
the complete overrides object. Both use the same validated writer as the UI.
--revision REV requires a matching revision; CLI omission uses a freshly
read revision. --preview with --set or --stdin validates a draft and
computes module notes without writing. --action <id> invokes the declared
command in its owning module's namespace, using saved config and the existing
dormancy guard; it cannot be combined with a settings save.
Successful saves add changed: boolean and commit: string | null to the
snapshot. A changed save atomically replaces only the module JSON file and
creates exactly one git commit. Structurally unchanged requests preserve all
file bytes and produce no commit. The transaction lock serializes revision
checks, writes and commit. Stale revisions or a busy transaction return 409;
validation returns 422 with { error, status, errors: [{ path, message }] }
and writes nothing. Unavailable modules return 404. A write/commit failure
restores prior files and target index entries, preserving unrelated staged
work. Pre-staged target changes are refused. These errors exit CLI 1.
--migrate --preview runs the module's pure planner and returns its source
preview, values, paths and revision. --migrate --revision REV applies that
reviewed plan through the same validator and transaction. Changed settings
and content are committed together once; stale input, validation, write or
commit failure changes neither source. Jobs moves known scoring keys from
criteria frontmatter into settings/jobs.json.scoring, retaining raw keyword
casing, flat/tiered forms, coercible numbers, odd match values and absent
optionals. Unknown scoring keys and unrelated frontmatter/prose stay in the
document. It proves identical parseScoringConfig outputs before writing.
Legacy frontmatter remains a read fallback until explicitly migrated.
module list --json adds settings, canBeDormant and dormancyReason to
loaded entries in its existing enabled array. A malformed module appears as
an unavailable row with error, while valid neighbors remain readable and
editable; actions still refuse globally invalid config. The Modules editor's
HTTP routes are listed as internal transport in http-api.md.
GET settings supplies ETag; PUT requires If-Match (428 when absent), passes
through field errors and 409 conflicts, and uses the CLI writer. Every route
uses the existing authenticated-principal and origin guards. Migration,
dormancy and actions are separate confirmed requests. Domain saves never
change the core-owned enabled flag, migrate instruction ownership, hot
unload a module or cancel running sessions.
- Containment. Everything the CLI writes stays inside the brain root.
Config-supplied directories, module config paths, and caller-supplied
document paths are repo-relative (no absolute,
~,.., or control characters) and resolved through a symlink-aware canonicalizer, so neither a symlinked directory nor a dangling symlink redirects a write out of the repo. A CLI path argument may also be absolute: one inside the brain root is converted to a repo-relative path first, and one outside it is refused (0.45.0, #3422). - Mutating commands require an initialized brain.
add,import,index,archive,accept-mtime,process,maintain,sync,skills sync, andsetup, plusokf export, exit 1 when nobrain.configis found rather than initializing a stray directory. Read-only commands (includingskills lint) still run. - OKF exports are derived and isolated.
okf exportonly writes inside a taxonomy-excluded directory, wipes that derived directory before generation, and never modifies source concept files.okf checkis read-only and accepts third-party bundle directories. module list --jsoncron entries are shape-constrained. A container entrypoint materializes them into a crontab with root privileges, sonameis kebab-case,scheduleis a 5-field expression, andcommandis a plainbrain …argument string — no newlines or shell metacharacters can appear. A consumer should still re-validate before interpolating, including the module KEY: defense in depth is the contract here, not producer trust.modulesconfig keys must be an npm package specifier or a contained./path, and are canonicalized beforeimport()— a symlink cannot make the loader execute code from outside the root.
Keep one contract test that runs brain search "x" --json and
brain briefing against a fixture brain and asserts the envelope shapes
above — cheap insurance against silent breaks.
exclude.files contains exact relative file paths; exclude.dirs excludes
directories and their descendants, and exclude.segments excludes matching
directory segments at any depth. Directory pruning and output-directory
authorization consult only directory/segment rules, never exact-file rules.
Breaking in the next minor release (approved in #224): configuration
loading rejects exclude.files entries ending in /, which were previously
accepted and could make stats disagree with indexing. The error identifies
the field and value and directs the author to exclude.dirs. To exclude a
whole directory, replace files: ["drafts/"] with dirs: ["drafts"].
Valid exact-file, directory and segment exclusions keep their semantics;
CLI JSON shapes are unchanged.
Pre-1.0 breaking tightening (#251): compileConfirmPatterns and both
first-party backend constructors reject a nonempty confirmBashPatterns list
if no regex compiles, before turn or tool activity. The server's
BRAIN_UI_CONFIRM_BASH regex list follows the same initialization rule. The
error names both configuration fields, preserves invalid-source diagnostics,
and directs the caller to repair the list or intentionally use []; exact
error prose is not a machine schema.
Missing backend configuration still uses the shipped defaults, and explicit
[] still disables confirmation. Mixed lists report and skip invalid entries,
preserving the valid patterns, their matching order and effects. The server's
existing malformed-JSON and structural-entry fallback retains its semantics.
The rationale is recorded in confirm-patterns.md.
Approved pre-1.0 breaking API change (#411): Enrichment.describeAsset
from @schlessera/brain now returns Promise<string | null> rather than
Promise<string>. createEnrichment returns null when its completion
provider lacks vision (without calling complete) or returns empty/whitespace
text for an image or PDF. Non-empty output is trimmed and accepted even if it
equals the title. Errors still reject; CompletionProvider.complete and
Enrichment.generateChunkContext retain their string return types.
Callers handle null before persisting, caching or embedding a description.
Custom implementations return null for no description and non-empty strings
for success; always-successful implementations may retain Promise<string>.
The shipped @schlessera/brain/testing completion contract suite now asserts
null and zero completion calls for no vision, and null for empty output.
Caller migration guidance
includes an example.
The indexer leaves undescribed assets on their document/chunk/FTS placeholders,
keeps them findable by title, and queues neither description cache entries nor
embeddings for them. A later --embeddings run retries unchanged assets. A
null report says only that no description was returned, without attributing
a cause. Existing cached strings are preserved, including title-equal strings;
this change prevents new fallback entries and does not identify old ones.
Use --forget-cache <path> then --embeddings
to regenerate a known bad entry; forgetting resets sidecar and database state
for all assets sharing its bytes. Rebuilding only brain.db can reuse the old
sidecar and is insufficient.
The deliberate toolkit is exported from @schlessera/brain-ui-sdk/server:
| Operation | Public signature types / result |
|---|---|
decideToolPermission |
ToolPermissionDecisionInput → `ToolPermissionApproval |
createToolPermissionRequest |
CreateToolPermissionRequestInput → PermissionRequest |
requestToolPermission |
`Pick<BackendBridge, "requestPermission" |
checkEditedApproval |
EditedApprovalCheckInput → refusal message `string |
compileConfirmPatterns |
readonly ConfirmPatternSource[], invalid-source callback → CompiledConfirmPattern[] |
These operations, their exported types and every signature-reachable bridge, activity and protocol type are intentional authoring surface. They remain experimental until 1.0, then stabilize under the existing versioning policy; this does not schedule or freeze 1.0 early. The API report records the complete signatures and reachable types. The authoring guide shows the binding, and the decision inventory records the exact public/internal classifications under #343 Q1/Q2.
Adapters decide before dispatch, honor denial and use the same snapshotted
updatedInput for checking and application. Missing bridges and no-grant
surfaces deny; no-grant surfaces never ask the host. A throwing denial-activity
reporter cannot change the decision. Bridge rejections remain rejections for
the adapter to translate. Edits introducing a per-use confirmation absent from
the original input are rejected, including a changed full shell command or
archive target; an own __proto__ key is rejected. Allowed safe edits and
unchanged confirmed inputs may execute.
Confirmation sources accept strings and { pattern, effect }, compiled
case-insensitively in source order. Mixed lists report invalid entries and
retain valid ones; a nonempty all-invalid list throws. [] is the intentional
opt-out. Explicit tool-name matching, tool grants versus command confirmations,
archive checks and enforced-turn posture remain promised behavior.
Bundled defaults can change with documented user-visible release notes while preserving those guarantees. Their precise entries and wording are not frozen; policy formats and behavioral guarantees are not exempt from versioning. Subscription-auth action values, mapping and actionable instruction helpers remain public under the existing wire contract; instruction wording may improve.
Pre-1.0 import migration: the SDK's DEFAULT_CONFIRM_BASH_PATTERNS,
ARCHIVING_UPDATE_REASON, archivesDocument, bashCommand,
SubprocessEnvAudience, SUBPROCESS_ENV, filterSubprocessEnv and
parseSubprocessEnvExtra move from /server to /internal. Claude's
DEFAULT_CONFIRM_BASH_PATTERNS alias and VOICE_ALLOWED_TOOLS, and pi's
DEFAULT_PI_ALLOWED_TOOLS and TOOL_RISK, move from their ordinary entries to
those packages' /internal entries. These explicit internal paths are for
first-party implementation sharing at the same lockstep version, with no
compatibility guarantee. Their existence does not define another extension
seam or classify the other exports covered by the package-wide inventory.
Operator environment configuration retains its documented meaning; the
subprocess implementation table is internal.
@schlessera/brain-geo is a concrete leaf library. Its root export supplies
RoutePoint (lat, lon, nullable elevation_m and ISO time), strict
parseGpx, routePoint, great-circle distanceM, travel-compatible
routeMetrics, trimRoute, quantizeRoute and writeGpx. Existing travel
commands retain their JSON types, strict rejection, rounding and serialization.
The travel metric contract
continues to apply; metrics/writing require nonempty valid geometry.
parseTrackGpx(source) adds recovered file geometry. It returns segments,
warnings, kind (track or route), status (ok, partial or no_line),
partial, counts (input, retained, omitted, segments) and omissions.
Each omission names its zero-based selected-source point index, primary
reason and all reasons: latitude/longitude missing-or-invalid or out-of-range.
Counts cover the selected track/route geometry, not foreign metadata or waypoints.
Every omitted point splits the geometry. Valid isolated points stay in the
returned sections as evidence; no_line means no section has two points.
Track sections take precedence when usable, otherwise usable routes do.
With neither, track evidence takes precedence over route evidence when present.
Malformed/unsafe/over-limit inputs throw rather than becoming partial results.
Both parsers use the same guarded XML reader. Recovery never modifies the source, bridges gaps, changes optional unknown values or proves recording/travel from file metadata. Shared ownership and recovery explains why the new policy is separate from travel's strict compatibility entry point.
summarizeTrack(parsed, source) returns the original counts/status, copied geometry,
file source, [west,south,east,north] bounds, start/end, shape, warnings, unknown
field/reason records and method metadata. Its measurements contain distance,
ascent, descent, altitudeMin, altitudeMax, elapsed and movingTime, each
with { value: number | null, unit: "m" | "s", scope: "usable_sections" }.
Distance uses unsimplified great-circle edges on a 6,371,008.8 m sphere. Sections
with fewer than two points remain evidence but do not contribute measurements.
Known zero remains zero. Elevation requires complete eligible section samples;
ascent/descent share three-point-median smoothing and 3 m hysteresis. Elapsed
sums each section's ordered, complete first/last timestamp interval, retaining
pauses and excluding gaps; absent/invalid/decreasing timestamps remain unknown.
Moving time is always unknown with estimator_not_in_scope. No-line geometry has
unknown distance rather than a successful zero. Absent bounds/start/end and
unknown shape carry reasons as well. An optional recording claim retains its
text and verified: false; the library never infers recording from timestamps.
normalizeTrack(sections, kind?) accepts arrays of normalized point objects with
numeric lat/lon, optional elevation_m/time and a default "track" kind.
It returns the same ParsedTrack recovery/count/omission contract as GPX.
Malformed structure rejects; invalid coordinates omit/split, never coerce or
clamp. At most 200,000 points (including omissions) and 200,000 sections are
accepted. Missing/invalid metadata is unknown; caller input remains unchanged.
EARTH_RADIUS_M is the 6,371,008.8 m sphere used by all distance methods.
nearestTrackPoint(track, query, toleranceM) returns status/partial/counts,
distance: {value: number | null, unit: "m"}, closest point: {lat,lon} | null,
location: {section,index,fraction} | null, nullable withinTolerance, unknown
reasons and method metadata with "great_circle_segment", radius, tolerance and
"retained_geometry" scope. Minor arcs include interiors; gaps remain absent.
Retained singletons and repeated points are valid spatial evidence. Empty geometry
and ambiguous antipodal edges yield unknown; invalid queries/tolerances reject.
trackCoverage(A, B, {toleranceM, sampleSpacingM?}) returns status/partial, counts
for both inputs, direction "A_relative_to_B", nullable ratio/covered metres,
usable metres of A, nullable {minimumRatio,maximumRatio} bounds, unknown reasons
and method parameters. Its "arc_length_midpoints" estimate measures only A's
usable sections within tolerance of B's usable sections. Bounds use distance's
1-Lipschitz property to expose uncertainty. The default spacing is
max(0.1,min(5,toleranceM/4)) metres; explicit spacing is >0 and ≤1,000 m.
Tolerances are finite, nonnegative and at most Earth's half-circumference.
100,000 samples / 5,000,000 comparisons bound analysis. Zero usable length in A,
no usable line in B, ambiguous antipodal geometry and analysis-limit exhaustion
return null ratio/covered length/bounds with reasons. Partial inputs remain partial;
no gaps are filled and no denominator is invented. All helpers require validated
ParsedTrack inputs from the shared parser/normalizer.
The root geoConfigSchema, GeoConfig and GeoConfigInput describe concrete
configuration. All services are disabled/unconfigured by default; userAgent
defaults empty. Endpoints must be credential-free HTTP(S) URLs without query or
fragment. Geocoding has enabled, optional url and publicServiceEligible;
the public Nominatim endpoint requires the latter explicit responsibility flag.
Routing configuration holds per-mode prepared datasets and an off-by-default
eligible demo flag; Overpass holds an enabled flag and at most three endpoints.
Optional cacheDir, cacheTtlMs (0–30 days, default one day), timeoutMs and
admissionWaitMs (100 ms–60 s, defaults 5 s) and minimumIntervalMs (0–60 s,
default 1 s) configure the shared concrete clients. Public operator floors cannot
be lowered by configuration. No provider registry is added.
@schlessera/brain-geo/server exports GeoClient, result/candidate/attribution,
error/source and runtime-option types. new GeoClient(config?, runtime?) validates
configuration. Runtime options accept fetchImpl and shared admissionDir; they
default to real fetch and the user's global geo admission directory. The root
geometry/configuration entry point imports no server I/O.
geocode(query) and reverse(lat,lon) return GeoResult<GeocodeCandidate[]> with
status, nullable value/source/error, warnings, attribution and candidate
counts. Success is ok, multiple matches ambiguous, usable mixed replies
partial, and genuine empty matches no_match; disabled and failure statuses are
distinct. A candidate has display/summary/address, numeric point, nullable bounds
and OSM identity, unknown accuracy and its reason. Inputs validate before dispatch.
No-match is recognized from an empty search or the exact Nominatim reverse error;
an unrelated 404 is never fabricated as no-match.
Source reports service/endpoint, fromCache, nullable fetchedAt/cacheAgeMs,
requestSent and transfer: {data,sent}. Cache hits preserve fetch age and indicate
no new transfer. Errors distinguish disabled, configuration, ineligible,
capability, input, timeout, network, http, admission_denied,
admission_timeout, bad_response, response_limit and cache_unavailable, with
optional HTTP/retry details. Valid provider data retains attribution and qualified
accuracy. No transient failure is stored as no-match. Disk caching/admission is
shared across cooperating processes; the geo guide
documents local aggregation, bounds and fail-closed orphan recovery.
GeoClient.route(points, mode) accepts 2–100 ordered numeric points and a
RoutingMode (car/foot/bike). Configured entries carry url through the
route-service prefix, profile, preparedMode, dataset and verification.
The mode must match the declared prepared dataset. Public FOSSGIS entries also
require explicit eligible demo configuration and its verified endpoint/profile.
Otherwise the client returns a capability/eligibility error without transfer.
RoutingResult retains request: {mode,points} and attempts even with no route.
Its nullable CalculatedRoute contains kind: "calculated", unsimplified geometry,
all requested/snapped waypoints and snap distances, ordered legs with nullable
distance/duration, unit-bearing aggregate distance (m) / duration (s), unknown
reasons and provider-calculation method metadata. Missing/invalid estimates remain
null and partial; zero remains zero. Geometry/waypoint/leg shape must match the
request. Calculated duration does not establish movement or recording.
The served source adds dataset name/prepared mode/profile/verification and
fallback: {used,reason,primaryEndpoint}. Cache hits retain these and original
fetch age. Every attempt lists endpoint/cache status/request transfer and error;
a cached demo result can follow a newly transmitted primary attempt. Cache identity
includes prepared dataset/mode metadata. Requested points are snapshotted before
asynchronous work and are never overwritten with provider-snapped points.
Status is ok, partial, genuine no_route, disabled or error. NoSegment has
error code no_segment; OSRM errors retain serviceCode. Invalid query/input and
disabled/capability errors remain distinct from no-route and are never cached as
it. The demo is off until explicitly enabled and eligible. A missing primary or
one genuine availability failure can use one matching demo attempt; denial,
admission/cooldown, invalid/no-segment/no-route and local storage failures cannot.
An endpoint never retries itself as its own fallback. No geometry is fabricated.
GeoClient.poi(PoiQuery) accepts exactly one of near: {lat,lon} or
alongTrack: ParsedTrack, radiusM (1–5,000) and 1–10 AND tag filters. Values
are exact strings or true for tag presence, never arbitrary QL/regex. Track
queries retain section gaps, including isolated points, and refuse beyond 2,000
points, 100 nonempty sections, 64 KiB UTF-8 QL or a radius-expanded 5-degree extent
on either axis/pole crossing. Wide/date-line track envelopes are spatial-budget
errors; valid source coordinates are unchanged. No track simplification occurs.
PoiResult extends OverpassResult<PointOfInterest[]> with query parameters/counts,
nullable recovered-track metadata (partial, original counts/omissions), response
counts, truncated and method/limit metadata. Status is ok, partial, genuine
no_match, disabled or error. A POI retains OSM identity, nullable name, point,
position: node | bounding_box_center, tags, openingHours: {value,interpreted:false},
representative-point distance (m) to the query point/retained track and unknown
reasons. Missing/empty opening-hours text is null/not_mapped; there is no current
open/closed interpretation. Selection uses Overpass around geometry. A way/relation
center is a representative bounding-box center and may lie outside the radius;
its distance is not the distance to its entrance/full geometry. Distance uses the
shared sphere/minor arcs, never an invented gap. Empty/undefined track distance
stays null with a reason.
Responses contain at most 1,000 usable results. A 1,001st sentinel marks a partial
capped answer with one counted omission and warns that more may exist. More than
1,001 elements is response_limit; invalid/duplicate elements are counted omissions,
and wholly malformed nonempty responses fail rather than cache as no-match.
Opening-hours absence alone does not mean malformed data.
OverpassResult<T> retains all endpoint/cache/transfer/error attempts and source
fallback {used,reason,primaryEndpoint}. Ordered endpoint configuration is capped
at three, duplicates are attempted once, and a cached fallback remains visible
after a primary attempt. Genuine availability failures can fall through; admission,
cooldown, quota/resource denials and local storage failures cannot, even to a cached
alternate. HTTP 504 has Overpass resource-admission meaning. JSON resource/quota
remarks likewise refuse and persist cooldown; incomplete timeout remarks are errors,
never genuine empty matches. Attribution accompanies usable OSM replies.
The geo root exports the existing SDK pure coastline helpers/types, without server
I/O. SDK server exports retain their names/types and alias the shared implementation.
CoastlineConfig additively accepts optional canonical geo: GeoConfigInput and
shared runtime admissionDir. Required legacy enabled/url/User-Agent and optional
timeout/fetch injection remain supported. fetchCoastline(request, config) retains
the exact CoastlineResult keys and result-or-empty/partial failure behavior;
enabled:false prevents all requests, including with a canonical config supplied.
Legacy settings adapt to concrete shared cache/admission without wire changes.
GeoClient.coastline(CoastlineRequest) returns CoastlineServiceResult, with the
existing geometry value and per-layer queries: {layer,result}[]. Layers are
CoastlineLayer (coastline, roads, streets); each result is
OverpassResult<CoastlineLayerGeometry> (lines, input/omitted counts). Its source
is the latest successful layer, or latest attempted layer if none succeeded; the
complete query list is authoritative for mixed sources/cache ages/transfers/errors.
Bounds are finite in-range ordered [west,south,east,north], capped at 5 degrees
per axis; width is >0 and ≤16,384. Existing detail/tolerance/geometry algorithms
are preserved. Service geometry is bounded to 10,000 ways/200,000 vertices,
including malformed entries, and shared body/time budgets. Mixed malformed data
or failed layers with usable geometry are partial; no usable data after failure
has null value/error. Genuine empty successful geometry has no_match and empty
value. Refusal/configuration/local storage failures stop later layers. Attribution
and every layer's endpoint/fallback/attempt evidence remain available.
reverseGeocode(coords, ReverseGeocodeConfig) retains exactly
{displayName,summary,address} | null. Required legacy enabled/url/User-Agent
settings remain; additive optional publicServiceEligible, canonical geo,
fetchImpl and shared admissionDir select the concrete client/runtime.
Canonical service/cache configuration wins; legacy enabled:false still prevents
requests. Public Nominatim requires explicit informed eligibility, which is never
inferred from enabled and grants no permission beyond the public-service policy.
An ineligible public request is an existing nullable failure path, preserving raw
coordinates in the location tool. First-party backends default
NOMINATIM_PUBLIC_SERVICE_ELIGIBLE to false; only recognized truthy tokens opt in.
Configured nonpublic endpoints remain available. Responses are validated and
cached by endpoint/exact coordinates; transient failures are not cached as null.
No MCP/tool/result/wire shape or protocol revision changes.
brain.config.ts/JSON optionally accepts geo with the shared GeoConfigInput
shape and defaults. Root omission remains omission: parsing {} produces {},
and new services remain off. BrainConfig is the authoring input type so the
new nested defaults do not require callers to write every optional field.
defineConfig remains a typed identity function; load-time schema validation
still validates the whole configuration. Core's optional geo response-cache
directory uses repoRelativePathSchema and resolves inside the brain through
safeResolve, including symlink containment. repoRelativePathSchema is
exported from @schlessera/brain for a module author's configSchema: a
setting that names a directory inside the brain (finance's clientsDir,
images' imagesDir) validates with it, which refuses absolute, ~,
backslash and .. paths before the module resolves the value under the brain
root.
SDK server additively exports GeoConfig, GeoConfigInput and geoConfigSchema
from the concrete geo library. UI server CoastlineConfig.geo is optional for
existing explicit configurations; BRAIN_GEO_CONFIG_JSON supplies it from the
environment, validated at startup. Relative response-cache paths resolve from
BRAIN_PATH; invalid/empty/malformed configuration fails rather than silently
using legacy public endpoints. Canonical service settings take precedence over
legacy Overpass settings. Legacy disabled prevents requests. Existing permanent
geometry cache keys/results and 5-degree route bounds remain unchanged.
The server environment setting is not forwarded to child processes by default.
Geo server additively exports staticMap(input, client) and GeoClient.staticMap,
plus StaticMapInput, StaticMapTrack, StaticMapPin, StaticMapResult,
StaticMapReason, StaticMapScale and the track/pin/leg evidence types. Input has
optional tracks: {track:ParsedTrack,source:TrackSource,label?}[],
pins: {lat,lon,label}[], already-calculated routes: RoutingResult[],
bbox: [west,south,east,north], title, widthPx and
background: "auto" | "none" | CoastlineServiceResult. Omitted background is auto.
No routing request is made by a map call. A concrete prefetched background sends
no background request; "none" also sends none.
The result has status: ok | partial | no_map, kind: geometry | track_only | none,
nullable reason, nullable png: Uint8Array/svg: string, widthPx, nullable
heightPx/bounds, complete text, title, copied tracks/pins/legs/routes,
nullable background, attribution, warnings and method. Reasons are
unsupported_projection, background_extent, background_unavailable,
background_omitted, no_spatial_input, unsupported_text, render_budget and
renderer_unavailable. Every no-map outcome has null images and preserves complete
text/source/summary evidence. Invalid or oversized input throws.
Track evidence retains one-based index/label, full unsimplified TrackSummary, exact
original omission indices/reasons, and visible/cropped retained-vertex counts.
Pins retain original coordinates/label, one-based index, drawn and nullable reason
outside_viewport | no_artifact. Every requested adjacent route pair has a leg with
route/index/mode/from/to, available and nullable distanceM/durationS; genuine
zero remains zero. Route stops not represented by explicit pins gain numbered pins.
Missing route geometry never creates a line, and imported section gaps remain gaps.
All stops/legs remain in the full legend, including cropped stops.
The method records spherical_web_mercator, source_vertices_in_mercator, the
85.0511287798066-degree latitude limit, gaps: preserved, bundled IBM Plex glyph
outlines and nullable scale {value,unit:m,lengthPx,latitude, method:mercator_at_center_latitude}. Unsupported polar/date-line/wrapping extents
preserve source coordinates rather than clamp them. Projectable bounds wider than
5 degrees per axis skip background querying and produce a plain image. Missing
background also produces a qualified plain image. Used OSM geometry/calculated
routes retain attribution; FOSSGIS-derived graphics additionally carry the
operator's CC BY-SA 2.0 graphics link. No browser, raster tiles, system fonts or
external resources participate in rendering.
Width is 320–2,048 integer pixels, default 1,024. Maximum inputs are 100 tracks, 1,000 explicit pins, 100 route results and 200,000 aggregate source-track/route/stop points, including track omissions. Prefetched background is bounded to 10,000 ways/ 200,000 vertices. Labels and provenance text are bounded and reject controls. A complete image/legend above 32 million pixels, 16,384 height, 200,000 text characters, 16 MiB SVG or 64 MiB PNG gives a text fallback. Unsupported glyphs and unavailable local rasterization have distinct reasons. No stop, leg, omission or attribution is silently truncated.
brain geo adds the five subcommands below without changing old CLI/MCP/wire/
frontmatter contracts. Types are GeoGeocodeOutput, GeoRouteOutput,
GeoPoiOutput, GeoTrackOutput and GeoMapOutput in core's geo command. Each
machine invocation emits one JSON document; usage exceptions remain stderr-only.
| Command | JSON fields |
|---|---|
geo geocode <query> |
Complete GeoResult<GeocodeCandidate[]> plus operation: geocode, request: {query}. |
geo geocode --reverse lat,lon |
Complete same result plus operation: reverse, request: {lat,lon}. |
geo route lat,lon... --mode car|foot|bike |
Complete RoutingResult plus operation: route. |
geo poi --near lat,lon | --along file.gpx --radius-m metres --tag key[=value]... |
Complete PoiResult plus operation: poi, nullable brain-relative sourceFile. |
geo track file.gpx |
Complete TrackSummary plus operation: track, exact omissions, nullable nearest: NearestTrackPoint and nullable comparison: {summary:TrackSummary,omissions,coverage:TrackCoverage}. |
geo map [file.gpx...] --out file.png |
Complete StaticMapResult except png/svg, plus operation: map, nullable artifact: {path,format:png,bytes}. |
Coordinates are latitude,longitude in CLI arguments. Bbox order remains west,
south,east,north. Route mode is required and ordered stops remain intact. POI
requires exactly one spatial form, explicit radius and 1–10 exact AND filters;
repeatable bare keys mean presence and duplicate keys refuse. Track nearest/
comparison requires explicit --tolerance-m; optional --sample-spacing-m needs
comparison. Counts, partial status, model, tolerance, direction and unknown reasons
remain the shared results; originals are never rewritten.
Map accepts repeatable --pin lat,lon,label (commas after the first two belong to
the label), optional repeated routing --point lat,lon with required --mode,
--bbox, --title, --width and --no-background. Routing resolves before the
static map operation; this is explicit requested traffic rather than a hidden
route calculation in rasterization. Output is brain-relative PNG, requires an
initialized brain and explicit --out, and replaces a chosen regular file.
Containment checks include symlinks and repeat after asynchronous resolution;
scratch uses its existing genuine/ignored-directory write and pruning rules.
No-map returns null artifact and does not create its destination. Complete text/
provenance/omissions remain in the JSON while binary PNG/SVG contents are omitted.
Aggregate map input counts omitted source points as well as retained geometry.
Human mode prints source, fetch/cache age, all transfer attempts, dataset/fallback,
unknown estimates and applicable attribution. Exit 0 covers valid/partial results,
genuine no-match/no-route and documented map/text fallback; input/usage is 1 and a
failed service command/storage/local renderer is 2. Typed service input errors may
emit their one JSON result with exit 1; parse/path/usage exceptions print only to
stderr. brain render retains its existing envelope/network-denied behavior:
callers inline the already-created local PNG as data before invoking export.
ClientChatMessage.files?: { kind: "file"; path: string }[] references validated
originals in the existing share staging directory. The host resolves and reparses
those originals before first, queued, native follow-up and retry dispatch. A client
cannot supply trusted coordinates, measurements or a replacement source path.
Images remain in attachments; at most SHARE_MAX_FILES (10) files and images,
with SHARE_MAX_TOTAL_BYTES (50,000,000) total decoded/original bytes, may accompany
one message. Existing image-specific limits still apply. Bad references fail as
ATTACHMENT_REJECTED, before a backend runs.
The paired UI transports POST /api/track-upload and GET /api/tracks are
internal HTTP routes; their
published socket/block behavior remains this additive contract.
POST /api/track-upload uses the existing authenticated, same-origin multipart
boundary, concurrency and total-body caps. It admits validated GPX, KML 2.2 and
the supported GeoJSON subset only. TRACK_MAX_FILE_BYTES is 20 MiB, matching the
shared parser; a larger track returns 413 {error:"file_too_large",limit}.
Ordinary JSON, PDF, CSV, unsupported formats, malformed or unsafe structures
return 422 {error:"unsupported_track",message}. A title/text without a track
cannot bypass admission. A cancelled uncommitted stage is removed. Originals are
written unchanged through the existing atomic staging path; no knowledge-base
content is created. Existing POST /api/share retains generic file intake and
never labels an unvalidated file as a track.
An initial accepted retry receipt can include canonical files for the new
local user row; receipt status queries still disclose no original input.
SharedFileMeta additively carries incomingName?, detected?: "gpx"|"kml"|"geojson" and summary?: TrackFileSummary. Validated nameless,
extensionless or generic .bin, .dat, .tmp, .xml and .json tracks get a
detected format extension in their sanitized, collision-safe staged name.
sha256? identifies a validated original; exact retries check the original
before accepting and refuse changed or unavailable files. Incoming display
name/MIME, detected format and actual staged name/path remain
separate bounded, inert metadata. TrackFileSummary is the shared TrackSummary
without geometry, plus input waypoint count and omitted waypoint count. Replayed
user messages add files?: SharedFileMeta[]; the operational UI database retains
this metadata, not authoritative content or original bytes. Only a byte-identical
recorded server context is removed from replay text. User-authored lookalikes
remain user text.
GET /api/tracks?path=<staged reference> returns TrackFileView: the shared
ImportedTrack plus canonical file: SharedFileMeta, recomputed from the original.
Unavailable, expired or unsupported originals return 422 {error: "track_unavailable",message}; a missing/overlong path returns 400. Reads enforce
containment, regular-file/no-symlink checks and actual byte limits.
Retry for a file-backed request is advertised only after the server checks the original bytes, before consuming eligibility. The session catalog is internal to the server (#1053).
show_block adds {kind:"track",source:{path:string},title?:string}. The source
is a staged reference only: no model-authored geometry, viewport, metric or
provenance field. The client resolves the original through the route above.
The classifier does not infer track blocks from prose. TrackMap is a
presentation-only kit component; MapView.fitPoints fits a track envelope
without extra pins, and MapPin.marker distinguishes start/end shapes and their
S/E merge. Existing place-map behavior stays unchanged.
Imports recover invalid coordinates into separate usable sections with exact
counts/reasons. Every omission and original section boundary breaks both drawing
and measurement. Valid zero, repeated and polar coordinates remain source
coordinates. Optional invalid/missing elevation and timestamps remain unknown;
elapsed includes pauses within sections and excludes gaps, with incomplete or
non-monotonic required times unknown. Moving time remains unavailable. Elevation
uses the shared three-point median/3 m hysteresis and complete eligible altitude
input. Values cover usable_sections; recovered values say partial. Formats and
timestamps never establish recorded travel: coordinates are file-provided.
parseImportedTrack in @schlessera/brain-geo owns adapters: GPX track/route and
waypoints; KML LineString, MultiGeometry and Point, with no NetworkLink, Model or
polygon import; GeoJSON LineString, MultiLineString, Point/MultiPoint and their
Feature/collection wrappers, with no alternate CRS or polygons. XML is strict,
UTF-8/ASCII, entity/DTD-free; input is bounded to 20 MiB, 200,000 total line and
waypoint points and 128 nesting levels. The original is never rewritten. Shared
measurement input and displayed lines are unsimplified in this cut; all retained
points remain drawn unless the entire projection is unsupported.
The static map retains a labeled track-only line when background geography is unavailable or its drawn envelope exceeds either 5-degree query axis. It sends no oversized geography request. A Mercator/padded frame outside the supported latitude/longitude range gives a clear reason with the complete summary, waypoints and original reference, without clamping/wrapping source points. PNG/PDF sharing resolves file and optional geometry before composing static HTML; the scriptless renderer performs no network fetch. An expired original refuses track export rather than drawing an empty frame.
A host's manifest must advertise MIME types together with extensions. Extend its
existing generic/image entries with these track entries; use the SDK worker
handler at the same action. The generated-host change is tracked separately in
brain-hosting-template#10.
The example is checked through registerShareTarget and actual multipart parsing.
{
"share_target": {
"action": "/share-target",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [{
"name": "files",
"accept": [
"application/gpx+xml", ".gpx",
"application/vnd.google-earth.kml+xml", ".kml",
"application/geo+json", ".geojson",
"application/json", ".json"
]
}]
}
}
}A .json or generic MIME/name is identified by validated contents. Ordinary JSON
still receives no track summary. See the GPX schema,
KML reference and
GeoJSON RFC 7946 for source formats;
the supported subset and recovery policy above govern this import contract.
Explicitly declared built-in pi provider/model profiles are validated against
the supported catalog, then resolved from the same configured native
ModelRuntime used for inference. New ordinary/autonomous turns honor native
models.json endpoint configuration. Ordinary persisted resumes use that runtime
without overriding their saved model and still refuse native model fallback.
The brain's transcript directory is independent of native agent-directory
model/auth/cache discovery. No new profile endpoint field, schema, custom-model
discovery or provider seam is introduced.
On every inference request, a native authentication-supplied endpoint wins;
otherwise the configured model endpoint is used. A configured proxy is
conditional on native authentication not supplying an endpoint. A differing
native auth endpoint alone is not a configuration refusal. The backend neither
forces native credentials to a configured URL nor duplicates credential
discovery/refresh, subscription/account routing or billing ownership. Native
refresh may contact authentication servers. Selected provider/model identities
and declared picker profiles stay fixed. A resident session retains its runtime;
no hot-reload promise for models.json is added.
This newly honored routing replaces previously ignored endpoint configuration
for new sessions. Review native model/auth configuration when adopting the
change. The 2026-10-03 endpoint policy records
the prior maintainer ruling and approved migration: contract/breaking, a
CONTRACT: commit and same-commit backend/contract guidance plus a named minor
changeset before 1.0 (major from 1.0). No dependency change or release date is
selected. Autonomous nonpersistence, exact tool authority, loaded-runtime
compatibility and the containment/dispatch gates remain binding; endpoint
support does not enable autonomous execution.
brain schedule add|list|cancel|reconcile|due and the authenticated host
routes below store and inspect scheduled tasks. They never run one. No
dispatcher is wired: every stored task reports executionAvailable: false with
blockedReason: "dispatch_disabled" until the Queue runtime trigger (#915)
exists and #689 enables it.
Bridge tools (#916) and PWA review (#917) remain prepared below, not shipped.
Types, bounds and receipts are the ones in the
prepared contract, with the
refinements this section lists; where the two differ, this section wins.
Storage. The definition is context/scheduled-tasks/definitions/<taskId>.md
under the canonical brain root; cancellation moves it to
context/scheduled-tasks/retired/. Core excludes context/scheduled-tasks
from indexing by default (configuration), so a
forced brain index neither reads nor removes definitions. Frontmatter values
are canonical JSON (valid YAML flow syntax) in the fixed order
schedule_schema, id, when, scope, limits, notifyOnSuccess; the body
is the exact prompt. A file that is not byte-identical to the approved snapshot
quarantines the task (definition_drift); the host never rewrites it. The
approved snapshot, fingerprint, approval, receipts and occurrences live in the
UI operational database and travel with the operational backup.
A restore pauses every enabled task as restore_pending and marks each
outstanding occurrence unknown; only operator reconciliation (below) reopens
them.
Publication is refused (unsupported_capability) when Git would ignore the
definition path.
Supported tools. The host accepts only brain_read, validated against the
core tool's registered input schema; its path must be an approved target.
Any other tool, a model alias, or non-empty egress is invalid_request
(no supported tool uses the network). Targets must be exact, normalized,
non-hidden brain-relative paths outside context/scheduled-tasks,
context/policies, brain.config* and brain.db*, with no symlinked
component. A variable input may not vary a target path.
Execution policy. The reviewed executionPolicy is the host's default
backend ID, its preferred profile ID (or null) and the operator-configured
BRAIN_UI_SCHEDULE_INFERENCE_ORIGINS. While that variable is unset, proposals
are refused with unsupported_capability. Any change to the policy before
approval or publication is definition_conflict; propose again.
HTTP refinements. POST /api/schedules/proposals accepts an optional
clientTimeZone beside {key, definition}: the CLI's --client-time-zone.
An invalid explicit or client zone is invalid_request; neither falls back.
POST /api/schedules takes {proposalId, approvalId?}: the stored approval
for that proposal is used when approvalId is omitted, which lets a creator
publish after an operator approved elsewhere. Only the proposal's creator can
publish it. Approval requires an owner principal (password mode) or the
ambient principal (other modes); a delegated agent credential gets
unauthorized. Delegated principals see and cancel only the tasks they
created; operators see all. The occurrence stop route is not mounted yet.
All responses carry Cache-Control: no-store.
CLI. Syntax and envelopes are the prepared ones. add --approve prints the
whole materialized envelope, execution policy and fingerprint on stderr and
grants only when the operator types approve on an interactive terminal; with
no TTY, or any other answer, it exits 1 with approval_required and the stored
proposal ID. --attempt-timeout-ms and --max-operations form one reviewed
limits object; a missing half takes its default. The scope file is a
regular, non-symlink file of at most 8 KiB holding one JSON object. In JSON
mode, argument errors are {ok:false,error:{code:"invalid_request",...}}, exit
1; credential_file_invalid exits 1; server_unavailable,
unsupported_capability and invalid_response (including any redirect, which
is never followed) exit 2. Every success envelope is schema-checked before it
is printed.
Operator reconciliation. POST /api/schedules/:id/reconcile takes
{key, decision: "reopen"} and answers 200 {ok: true, changed, task}. It is
the verified operator decision the record requires before a task paused as
restore_pending or unknown_effect may run again: only an operator
principal (the same rule as approval) may make it; a delegated agent gets
unauthorized, or not_found for a task it cannot see. The host still
requires the definition file to match the approved bytes and the host's
execution policy to match the approved one (otherwise definition_conflict),
and a usable creator (otherwise unauthorized). It refuses while the operational restore marker is pending
(unsupported_capability). When a completed restore named a different brain
directory, the first reconciliation binds the ledger to it; the root identity
and every fingerprint are unchanged. Unknown occurrences keep their outcome
and their due instants stay consumed, so nothing is replayed; a one-off with
nothing left to run, or a recurrence past its endAt, becomes expired
instead of active. Keys are receipts as for cancel: a matched replay, or a
task with nothing to reconcile, answers changed: false; a key reused for
another task is key_conflict. brain schedule reconcile ID --key KEY shows
the task and its last occurrence on stderr and sends the decision only when the
operator types reopen on an interactive terminal; otherwise it exits 1 with
approval_required. Its JSON is the route's envelope.
Occurrence admission (not wired). The UI server package contains the
private admission boundary #915 will drive. It turns a task's latest fresh due
instant into one occurrence and its first Queue item in one immediate
transaction; concurrent callers in any process admit it once, and instants
arriving while work is outstanding are skipped as busy. Admission and every
start require the creator and the approving operator to still be usable
(otherwise blockedReason: "authority_unusable") and the host's current
execution policy to equal the approved one (otherwise "backend_unavailable",
which list and due also report). A start refused for one of these reasons
makes no model call and ends that occurrence failed, so a later due instant
can run once the cause is fixed. At or after an approved endAt, queued
or retrying work expires instead of starting. The Queue claim reserves
budget as for any autonomous work, and the start transaction is the
cancel-versus-start boundary. Each attempt gets the approved
attemptTimeoutMs (at most 600000 ms, never past the occurrence's 24-hour
expiry), and its claim is released only after the backend unwinds. Retries,
yields and continuation items share the occurrence's maxOperations. An
attempt that died before its backend acquired the reservation retries; one
that died after it is unknown, is never replayed, and pauses its task as
unknown_effect. A one-off ends completed, failed or expired with its
occurrence. No route, CLI command or core runner reaches this code, and the
production app constructs none of it.
Selected design; partly implemented. The scheduled-tasks
record supplies the six approved policies and this
bounded specification for #914–#917. Scheduled tasks (#914)
ships the definition store, approval, CLI and HTTP routes except occurrence stop.
The bridge tools, occurrence stop, runtime dispatch and Activity/notification
correlation below remain unimplemented, and no permission grant or dispatch exists.
Consumers must not infer availability from these examples. Implementation adds
its actual schemas/exports/routes/receipts, compatibility tests and minor
changesets in CONTRACT: commits; #689 still gates production execution.
Existing contract shapes above are unchanged.
All new request/nested objects are strict: reject unknown fields, duplicate
JSON/YAML keys, nonfinite numbers, invalid UTF-8 and NUL. Decoded control
characters are rejected in IDs, paths, origins, names, operation and cursor;
textual prompt/tool-input values may contain LF/TAB. IDs and keys match [A-Za-z0-9][A-Za-z0-9._:-]{0,127};
IDs are host-minted and keys are caller-chosen per authenticated principal/root/
operation. Date values are valid offset/Z ISO instants, normalized to UTC Z;
responses use integer milliseconds for durations such as attemptTimeoutMs
and integer counts for counters, not interchangeable date/string values.
The JSON request definition is exactly:
type ScheduleDefinitionInput = {
prompt: string;
when: { kind: "at"; at: string; timeZone?: string }
| { kind: "cron"; cron: string; timeZone?: string; endAt?: string | null };
scope: {
operation: string;
tools: { name: string; inputs: JsonObject }[];
targets: string[];
egress: string[];
variableInputs: {
pointer: string;
bound: { kind: "enum"; values: JsonScalar[] }
| { kind: "string"; maxBytes: number }
| { kind: "number"; min: number; max: number };
}[];
};
limits?: { attemptTimeoutMs: number; maxOperations: number };
notifyOnSuccess?: boolean;
};JsonScalar is null/boolean/string/finite number; JsonObject is bounded JSON
with no prototype/authority fields and recursively validated against the selected
registered tool's actual input schema. Max JSON nesting is 8, arrays 32 members,
objects 32 own keys. Scope is at most 8 KiB serialized; prompt is nonempty after
whitespace validation and at most 16 KiB UTF-8. Preserve its approved text.
Operation is nonempty, at most 256 UTF-8 bytes. At most 16 tools, 32 exact
brain-relative target paths (256 bytes each), 16 exact normalized tool-egress
origins (256 bytes each), and 32 variable leaves are allowed. No command/template
interpolation or arbitrary executable schemas. A variable pointer names an
existing tool-input leaf; enum contains 1–32 bounded scalars, strings have a
positive maxBytes <= 16384, numeric min/max are finite and ordered. It cannot
change tool names, target paths, egress, timing, limits, actor or authority.
All undeclared leaves retain exact approved values. Reject duplicates and
unregistered/unsupported inputs; string bounds do not bypass tool validation.
when is an exclusive union, with the validated/resolved IANA zone persisted
on both forms. CLI can supply a validated client zone explicitly; the bridge
obtains its fallback zone from verified turn ClientEnvironment, never model
text. If neither is usable, disclose UTC. An explicit invalid zone is an error.
Optional recurring endAt is a future offset/Z instant at first publication;
omission materializes to null. It is an immutable scheduling window, not a
live-attempt stop: at now >= endAt invalidate future/unstarted work and stale
continuations, report configuration expired, and retain a started attempt under
its original authority/deadline/freshness. Due eligibility also requires
evaluatedAt < endAt when set. Cron is <= 128 bytes and follows the record's five-field numeric grammar,
including Sunday 0/7 and bounded lists/ranges/positive star/range steps. If either
day field begins with *, both day predicates must match, including its step;
otherwise either may match. Reject extra fields, impossible calendars and
unsupported extensions.
First creation requires a future offset/Z one-off. A matched recorded retry
returns its original receipt even after that date passes.
Missing limits materialize to {attemptTimeoutMs:600000,maxOperations:3};
provided values are positive safe integers with timeout <= 600000 and
maxOperations <= 3. Missing notifyOnSuccess materializes to false. These values
are reviewed and immutable. They grant neither budget capacity nor execution.
Definition files have exactly schedule_schema:1, host id, materialized when,
scope, limits, notifyOnSuccess as frontmatter and prompt as the body.
Max file size is 32 KiB; the canonical fingerprint covers all materialized fields,
exact prompt, host root/creator and the approved host-selected execution policy.
The file carries no actor/approval/runtime/control-state authority. Task definition
is the retained approved snapshot, not a claim that a drifted raw file is approved;
definition_drift/missing-file status quarantines it without silently rewriting it.
executionPolicy is server-owned {backendId:string, profileId:string|null, inferenceOrigins:string[]} (each ID <= 128 bytes, at most 16 normalized origins).
Operator review sees it alongside scope; callers cannot select its fields, prices,
billing identity or emergency reserve. Runtime/profile changes cannot widen the
approved tool/input/target/audience bounds. Current backend availability,
conservative billing evidence and daily admission still apply.
type ScheduleDefinition = Omit<ScheduleDefinitionInput, "when" | "limits" | "notifyOnSuccess"> & {
when: { kind: "at"; at: string; timeZone: string }
| { kind: "cron"; cron: string; timeZone: string; endAt: string | null };
limits: { attemptTimeoutMs: number; maxOperations: number };
notifyOnSuccess: boolean;
};
type OccurrenceState = "queued" | "running" | "unwinding" | "waiting_for_action"
| "retrying" | "completed" | "failed" | "cancelled" | "expired" | "unknown";
type ScheduleResult = { state: "available" | "pruned" | "unavailable";
text: string | null }; // UTF-8 text <= 4096 bytes; available implies text != null
// pruned/unavailable require text == null; a reference is not result availability.
type ScheduleOccurrence = { id: string; taskId: string; dueAt: string;
expiresAt: string; state: OccurrenceState; operationsUsed: number;
maxOperations: number; runIds: string[]; result: ScheduleResult };
type BlockedReason = "dispatch_disabled" | "budget_disabled" | "capacity"
| "authority_unusable" | "definition_drift" | "backend_unavailable"
| "unknown_effect" | "occurrence_limit" | "restore_pending";
type ScheduleTask = { id: string; definition: ScheduleDefinition;
creatorPrincipalId: string; createdAt: string;
zoneSource: "explicit" | "client" | "utc_fallback";
state: "publishing" | "active" | "paused" | "cancelled" | "completed" | "failed" | "expired";
executionAvailable: boolean; blockedReason: BlockedReason | null;
nextDueAt: string | null; lastOccurrence: ScheduleOccurrence | null;
compensationPending: boolean };
type DueCandidate = { taskId: string; occurrenceId: string; dueAt: string;
expiresAt: string; admittable: boolean; blockedReason: BlockedReason | null };Returned definition includes all defaults/resolved zone, not optional omissions.
Host-owned zoneSource reports the original resolution, including disclosed UTC fallback.
A publishing task cannot dispatch. active describes a valid enabled definition,
not successful/available execution. paused includes unknown-effect/drift refusal;
completed/failed/expired are terminal one-off states; expired also records an
ended recurring configuration without inventing a successful occurrence. Recurring occurrence failure does
not erase future configuration; unknown effects pause it. Cancelled tombstones
remain inspectable. nextDueAt is null when no future occurrence is eligible;
lastOccurrence is null before an actual durable occurrence. operationsUsed
is an integer 0..maxOperations and runIds contains at most three real attempt IDs;
unstarted scheduling facts do not fabricate Activity roots. Only running/unwinding
states describe a started attempt. Request keys, task IDs, original-due occurrence
IDs, Queue item IDs and run IDs remain distinct.
Due candidates are read-only computed eligibility at evaluatedAt, not claims,
persisted occurrences or a promise of execution. The same unconsumed candidate
can recur in successive queries. One outstanding occurrence suppresses a new
candidate; otherwise choose the latest unconsumed due instant with
dueAt <= evaluatedAt < expiresAt (expiresAt = dueAt + 24h). Authority/budget/
backend/restore refusal is visible in admittable/blockedReason. Querying due
never advances the scheduler cursor, changes counters or creates model work.
List/due requests accept limit (default 25, integer 1..100) and optional opaque cursor <= 1024 bytes. List supports only state (one Task state) and id (exact task ID); due supports only the paging inputs. Responses are bounded to 512 KiB; a page may stop before its requested limit to respect that byte cap, with a cursor that advances past the last returned row. One entry must fit; never return an unchanged cursor/empty page that cannot progress. Stable ordering is createdAt/id for list, dueAt/taskId for due. Host-signed cursors bind root, authorized actor, operation/filters, initial creation cutoff/evaluatedAt and last ordering tuple, with 15-minute expiry. Invalid/mismatched cursors fail; no client SQL/offset. State/authority may change between pages and must be revalidated for mutations; pagination does not promise a frozen operational snapshot.
Exact response field types are:
type AddScheduleResult = { ok: true; created: boolean; task: ScheduleTask };
type ListSchedulesResult = { ok: true; tasks: ScheduleTask[]; nextCursor: string | null };
type CancelScheduleResult = { ok: true; changed: boolean; task: ScheduleTask;
runningOccurrences: ScheduleOccurrence[] }; // at most one started occurrence
type DueSchedulesResult = { ok: true; due: DueCandidate[]; evaluatedAt: string;
nextCursor: string | null };
type StopOccurrenceResult = { ok: true; requested: boolean; occurrence: ScheduleOccurrence };Proposal success is {ok:true,proposal:ScheduleProposal} (the exact fields/types
specified below, including materialized ScheduleDefinition); approval success is {ok:true,approvalId:string}. All success
booleans above are literal/boolean as shown; no additional opaque transport
wrapper. runningOccurrences contains only actually started running/unwinding
work, even if the task is now cancelled.
Creation has proposal, verified review and publication stages in the same host. A proposal stores no authoritative definition and enables no execution. Proposed supported HTTP endpoints are all mounted behind normal auth/root/principal checks:
| Method/path | Exact input | Exact success |
|---|---|---|
| POST /api/schedules/proposals | {key, definition} |
{ok:true,proposal}; 201 new, 200 matched replay. |
| POST /api/schedules/proposals/:id/approve | {fingerprint,decision:"approve"} |
{ok:true,approvalId}; 200. |
| POST /api/schedules | {proposalId,approvalId} |
{ok:true,created,task}; 201 new, 200 matched replay. |
| GET /api/schedules | list paging/filter query | {ok:true,tasks,nextCursor}; 200. |
| POST /api/schedules/:id/cancel | {key} |
{ok:true,changed,task,runningOccurrences}; 200. |
| GET /api/schedules/due | due paging query | {ok:true,due,evaluatedAt,nextCursor}; 200. |
| POST /api/schedules/occurrences/:id/stop | {key} |
{ok:true,requested,occurrence}; 200. |
proposal is exactly {id,taskId,key,definition,zoneSource,executionPolicy,fingerprint,expiresAt, approvalState}; fingerprint is 64 lowercase hex, expiry is 15 minutes from initial
proposal, approvalState is pending/approved. Proposal id and its reserved future taskId are
distinct host-minted IDs; neither is execution. The fingerprint
uses the materialized stored definition {schedule_schema:1,id:taskId,prompt,when, scope,limits,notifyOnSuccess}, rootIdentity, creatorPrincipalId and executionPolicy. Canonical inputs/key are bound to the
authenticated creator/root before approval. Matched retries return the original
resolved definition/zone/expiry; changed payload conflicts. Expired unconsumed
proposals require a new request/approval, without reviving a retired task.
Only a verified operator decision may create approvalId: normal auth alone, agent credentials, a fingerprint, or an "approved" payload is insufficient. The approve route enforces the existing server-resolved operator authority (owner-kind for principal-cookie mode, the configured operator in ambient modes), checks current proposal/creator, and records actor/channel/time and the immutable fingerprint/nonce. Bridge requests obtain this record through the trusted host permission/Action path, not a model-visible approve tool. CLI explicit review uses its operator credential and confirmed exact fingerprint; a delegated credential can propose/query but cannot self-approve. Stored receipt/provenance, not a claimed input channel or User-Agent, decides eligibility. Unavailable operator provenance refuses approval honestly.
approvalId is opaque (ID bounds above), linked to that exact proposal/root/creator/ operator/fingerprint, expires with an unconsumed proposal and is consumed once by publication. The host rechecks current actor authority and matching definition/ execution policy before publishing, including first-publication future one-off and recurring endAt validation. Concurrent uses return the one original receipt or conflict; no broad reusable grant. No prompt/actor/profile/trust/billing field is accepted on publish/cancel/stop. Replay reads the original publication receipt before consumed-token/past-date checks, still under current authentication. It returns the task's current retained state; never recreates a cancelled file, resets counters or transfers creator authority to a replacement credential.
Cancel keys bind exact task/action within caller/root. Repeated matching cancel returns changed:false and current retained task/runningOccurrences; a changed payload conflicts. Cancellation atomically invalidates future/unstarted work and stale Action continuations, journals retirement and keeps receipts. Started attempts retain their approved snapshot and current authority/expiry limits; an authorized move of unchanged definition bytes is not drift or a running stop. It returns started running/unwinding occurrences honestly, not "agent stopped". Stop keys separately bind an occurrence/action; requested means cancellation requested, not backend drained or prior effects undone. Query until terminal; release only after actual unwind. Pending future cancellation is independent.
Prepared core syntax (all use explicit --server, optional --credential-file and --json; argument flags are strict):
brain schedule add --key KEY --prompt TEXT (--at ISO|--cron EXPR)
[--time-zone ZONE] [--client-time-zone ZONE] [--end-at ISO] --scope-file FILE
[--attempt-timeout-ms N] [--max-operations N] [--notify-success] [--approve]
brain schedule list [--id ID] [--state STATE] [--limit N] [--cursor CURSOR]
brain schedule cancel ID --key KEY
brain schedule due [--limit N] [--cursor CURSOR]
--end-at is accepted only with --cron. scope-file contains the strict scope object, at most 8 KiB, is a regular
non-symlink file and grants nothing. CLI add submits a proposal; --approve displays
the entire materialized envelope/execution policy/fingerprint and requires explicit
operator confirmation before the authenticated approval request. No TTY/affirmative
operator confirmation means no automatic grant. A pre-approved matching proposal
can publish on a later add retry without --approve; pending proposals return
approval_required with the bounded proposal ID in the message, allowing the
operator to identify its stored review; no approval secret is exposed. A terminal prompt does not replace the host's operator checks.
JSON successful add is {ok:true,created:boolean,task:ScheduleTask}; list/cancel/
due use the exact endpoint envelopes. Creation is not execution.
Credential audience/private-file/no-redirect/10-second request rules follow queue intake, with a 512 KiB response bound for these new schedule responses. No provider key, ambient cookie variable, raw DB, local runner or offline-activation fallback. A store-capable host may accept an approved future schedule while reporting executionAvailable:false; an unavailable store/unsupported approval cannot falsely return created success.
The three future tools are mcp__brain-ui__schedule_task,
mcp__brain-ui__list_scheduled_tasks, mcp__brain-ui__cancel_scheduled_task.
Both actual backend registrations share strict SDK schemas and eager attached
executors. schedule_task input is {key,definition}; the host supplies fallback
client zone, proposal/approval identity and verified creator. The model cannot
supply approvalId, actor, executionPolicy, profile, trust, billing or wider granted
membership. Description: write a self-contained future prompt; propose exact
bounded scope; creation needs operator approval and does not execute work.
list_scheduled_tasks takes only the list query inputs and is read class;
cancel_scheduled_task takes {id,key} and is confirm class, reporting future-only
cancellation and actual runningOccurrences. schedule_task is confirm class and
not auto-allowed in voice or no-grant/unattended membership. CLI/tool/PWA mutation
responses observe the same authoritative records; tool results/errors and model
text never become approval. The PWA consumes list/cancel/detail data and the
separate authenticated stop where supported, within its reviewed design.
Shared errors are {ok:false,error:{code:string,message:string}}, with bounded
message <= 1024 bytes and no credential/prompt/tool/SQL echo. Closed codes:
invalid_request (400), invalid_cursor (400), credential_file_invalid (CLI only),
unauthorized (401/403), not_found (404), key_conflict (409), approval_required (409),
approval_expired (409), definition_conflict (409), server_unavailable (503 or CLI
transport), invalid_response (CLI/schema), unsupported_capability (503).
No undocumented success-shaped error. Query success, including empty due and
inspection of a failed/pruned outcome, exits 0. Validation/auth/conflict/not-found
exits 1. Unavailable/unsupported/invalid response/timeout/redirect/5xx exits 2.
JSON argument errors use invalid_request/exit 1, not an absent envelope. Retry
writes with the original key, never a new key after an ambiguous response.
Complete fictional examples use the pinned Odysseus reference date, 2026-07-12. They are interface examples, not actual CLI/host receipts. The CLI obtains one stored proposal, explicit operator review, then publication through the routes above:
brain schedule add --server https://scheduler.example --credential-file ./operator.json
--key ithaca-review-01 --prompt "Read notes/ithaca.md and report outstanding checks. Do not change files or use network tools."
--cron "0 7 * * 1-5" --time-zone Europe/Athens --scope-file ./scope.json --approve --json
The scope file is exactly the scope in this complete add response. JSON mode
keeps review/confirmation on the terminal/stderr and emits one result on stdout.
The corresponding schedule_task input is {key:"ithaca-review-01",definition}
with this complete definition; approval/proposal IDs are obtained by the host,
not supplied by the model. Successful creation still reports disabled execution:
{
"ok": true,
"created": true,
"task": {
"id": "task_ithaca_review",
"definition": {
"prompt": "Read notes/ithaca.md and report outstanding checks. Do not change files or use network tools.",
"when": {
"kind": "cron",
"cron": "0 7 * * 1-5",
"timeZone": "Europe/Athens",
"endAt": null
},
"scope": {
"operation": "Report outstanding Ithaca checks",
"tools": [
{
"name": "brain_read",
"inputs": {
"path": "notes/ithaca.md"
}
}
],
"targets": [
"notes/ithaca.md"
],
"egress": [],
"variableInputs": []
},
"limits": {
"attemptTimeoutMs": 600000,
"maxOperations": 3
},
"notifyOnSuccess": false
},
"creatorPrincipalId": "principal_example_operator",
"createdAt": "2026-07-12T06:00:00.000Z",
"state": "active",
"executionAvailable": false,
"blockedReason": "dispatch_disabled",
"nextDueAt": "2026-07-13T04:00:00.000Z",
"lastOccurrence": null,
"compensationPending": false,
"zoneSource": "explicit"
}
}A subsequent list reads the same record:
{
"ok": true,
"tasks": [
{
"id": "task_ithaca_review",
"definition": {
"prompt": "Read notes/ithaca.md and report outstanding checks. Do not change files or use network tools.",
"when": {
"kind": "cron",
"cron": "0 7 * * 1-5",
"timeZone": "Europe/Athens",
"endAt": null
},
"scope": {
"operation": "Report outstanding Ithaca checks",
"tools": [
{
"name": "brain_read",
"inputs": {
"path": "notes/ithaca.md"
}
}
],
"targets": [
"notes/ithaca.md"
],
"egress": [],
"variableInputs": []
},
"limits": {
"attemptTimeoutMs": 600000,
"maxOperations": 3
},
"notifyOnSuccess": false
},
"creatorPrincipalId": "principal_example_operator",
"createdAt": "2026-07-12T06:00:00.000Z",
"state": "active",
"executionAvailable": false,
"blockedReason": "dispatch_disabled",
"nextDueAt": "2026-07-13T04:00:00.000Z",
"lastOccurrence": null,
"compensationPending": false,
"zoneSource": "explicit"
}
],
"nextCursor": null
}Cancelling before any attempt started retains the task and invalidates future work. Repeating this key returns changed:false and the same retained identity:
{
"ok": true,
"changed": true,
"task": {
"id": "task_ithaca_review",
"definition": {
"prompt": "Read notes/ithaca.md and report outstanding checks. Do not change files or use network tools.",
"when": {
"kind": "cron",
"cron": "0 7 * * 1-5",
"timeZone": "Europe/Athens",
"endAt": null
},
"scope": {
"operation": "Report outstanding Ithaca checks",
"tools": [
{
"name": "brain_read",
"inputs": {
"path": "notes/ithaca.md"
}
}
],
"targets": [
"notes/ithaca.md"
],
"egress": [],
"variableInputs": []
},
"limits": {
"attemptTimeoutMs": 600000,
"maxOperations": 3
},
"notifyOnSuccess": false
},
"creatorPrincipalId": "principal_example_operator",
"createdAt": "2026-07-12T06:00:00.000Z",
"state": "cancelled",
"executionAvailable": false,
"blockedReason": null,
"nextDueAt": null,
"lastOccurrence": null,
"compensationPending": false,
"zoneSource": "explicit"
},
"runningOccurrences": []
}Empty due and explicit approval failure retain their exact envelopes:
{
"ok": true,
"due": [],
"evaluatedAt": "2026-07-12T06:00:00.000Z",
"nextCursor": null
}{
"ok": false,
"error": {
"code": "approval_required",
"message": "Review the stored schedule proposal before creation."
}
}The implementation adds optional scheduleId/occurrenceId correlation to actual autonomous Activity roots/rollups and authenticated result links, without changing existing origin or outcome meanings. Original-due identity links up to three attempts/continuations. Pruning must leave an honest result/link state and retained occurrence outcome/counters; no invented run for unstarted/skipped work.
Success completion intents default off per schedule; failure/stuck/Actions keep existing semantics. Deduplicate final occurrence notices across intermediate roots/restarts, preserve generic push payloads and durable authenticated results. Existing 20-intents/hour suppression and three failed attempts/five-minute retry backoff remain. Intent/provider acceptance never claims receipt or reading. Coordinated Git plus operational backup retains all new relations/approval/ compensation/receipt/counter/result/notice state and validates it before gated restore/admission. Existing backup version/schema checks must cover those added relations; old tests/images are not schedule recovery proof. Matching restored bytes alone cannot disprove later cancellation/revocation: require authoritative later receipts or verified operator reconciliation and keep uncertain tasks paused. The 24-hour recovery point and unknown-effect investigation remain binding.
brain video watch <url|path> [--question TEXT] [--start T] [--end T] --json
returns { source, engine, model, clip, answer, timestamps, warnings }.
source contains kind: "youtube" | "file", nullable url, path, title,
and durationSeconds. engine is "gemini", model and answer are strings,
clip has nullable numeric start/end in seconds, and timestamps and
warnings are string arrays. Unknown metadata is null; model observations are
never metadata. Timestamps are deduplicated in answer order, inside the clip;
invalid/outside citations produce warnings rather than fabricated evidence.
The answer is returned verbatim. Failure emits no success envelope and exits
nonzero. Disclosure and diagnostics use stderr, preserving JSON stdout.
The experimental ContentPart union adds a kind: "video" variant with
mimeType, optional numeric clip: { start?, end? }, and exactly one source
(uri, path or data). CompletionProvider.capabilities.video?: boolean
treats omission as unsupported, preserving existing providers; built-in Gemini
reports true and Anthropic false. complete adds optional signal: AbortSignal.
Video requests do not use configured completion fallback. Uploads created by
a request are deleted after success or failure; failed cleanup throws.
See the module README for defaults,
source restrictions and privacy disclosure. These are additive minor changes.
These operations require valid configuration, except the read-only
check configuration-blocker. They use #1024's canonical IDs/fingerprints.
Confirmed resolve/Undo and ordinary check require an initialized brain; flags
before the subcommand or positional arguments after -- cannot bypass that
write guard. Repair previews and the configuration-blocker recheck remain read-only.
CLI invocation confirms an effect; server principal authorization and durable
Action execution remain the server's responsibility. No inference runs.
| Operation | Input | JSON result / exit |
|---|---|---|
hygiene resolve <id> --handler <name> --input <json> --expect-fingerprint <fp> --dry-run |
Handler and its typed JSON input | status: "preview", id, handler, diff, previewToken; exit 0. No brain file, index or log is written. |
hygiene resolve <id> --handler <name> --input <json> --expect-fingerprint <fp> --expect-preview <token> |
The same input and the preview's opaque premise token | status: "fixed", id, changedFiles; exit 0, only after real file validation and detection succeed. stale, refused, check_failed exit 1. |
hygiene undo <undoToken> --dry-run |
Opaque receipt token from a failed post-check | status: "preview", id, inverse diff; exit 0, writes nothing. |
hygiene undo <undoToken> |
Confirm that inverse effect | status: "undone", id, changedFiles; exit 0. stale or refused exit 1 and writes no content. |
hygiene check <id> |
A logged finding ID | status: "still_detected", id, handler, or status: "not_detected", id, changedFiles; exit 0. check_failed or refused exit 1. Still detected and failed checks write no brain file or log. |
hygiene check configuration-blocker |
Reserved configuration blocker identity | status: "blocked" (exit 1) or "ready" (exit 0), id, handler; blocked also has reason (loader diagnostic). Both are read-only. Other hygiene commands still refuse invalid configuration. |
A diff carries path (brain-relative), before and after (complete exact
UTF-8 text), and changes: [{before, after, line}] (every bounded replacement,
1-based line). A handler carries name, category, kind (choice, field,
manual, blocker), input: {type, values?, example?}, effect, postCheck,
path, line, explanation, and optional suggestedPath. input.type is
none, path, string, enum, date or strings.
The fixed handler set is:
link-suggested: inputnull; available only for exactly one contained note matching the target's slug, alias or exact title. No fuzzy match.link-note: input a brain-relative path to an existing, included Markdown document inside the brain. Outside paths, symlink escapes, symlink documents, excluded/non-document/missing targets and unrepresentable link paths refuse.link-text: inputnull; remove this target's link tokens, keeping each display label (the original target when unlabelled). Code and frontmatter tokens are preserved. Link-to-note effects retain labels and heading suffixes.required-field: typed input for the field named by the finding:title(non-empty string),type(configured taxonomy enum),created/updated(real calendar dateYYYY-MM-DD),tags(non-empty string array of lowercase, hyphenated tags). Only that key is edited; a minimal-editor refusal never falls back to serializing frontmatter. Other validation categories are manual.manual: no content effect; file/line and explanation for an explicit edit, thencheck.blocker: reload configuration before starting review.
Content effects refuse non-UTF-8 source files (reason: "not-utf8") rather than
normalizing unrelated bytes. Refusals carry reason; invalid field input additionally carries
fieldError: {field, message} with format/enum guidance. Stale results carry
reason and write no content or log. The fingerprint binds category evidence;
previewToken additionally binds the handler, input and exact replacement
bytes (including repeated token multiplicity), excluding unrelated file bytes.
An unrelated edit after preview is preserved. An apply without a preview token
is a usage error (exit 1). Neither token is an authority grant.
check_failed carries code, undoToken, changedFiles; the written effect
remains, the finding stays open and nothing rolls back automatically. Undo
requires the complete file to still match the effect's written bytes, offers
its own inverse preview, and restores the exact pre-effect bytes only upon
explicit invocation. Repeated Undo becomes stale. Repair receipts are Markdown
under context/hygiene/repairs/<opaque-token>.md; they hold the before/after
text and handler with standard valid frontmatter drawn from the configured
taxonomy, with no authoritative state in brain.db.
Only a successful repair records resolved-by: handler:<name>; a successful
manual recheck records resolved-by: check. Other findings keep their state
and evidence. Reconciliation still reopens a resolved finding detected again.
File validation failure, detector failure or unavailable resolution recording
cannot report fixed. Unexpected storage/publication errors use the standard
internal-error exit 2; an interrupted receipt requires recheck/reconciliation,
not blind replay of a content write. The compare-then-rename writer retains its existing
last-read-to-rename race limitation; these commands supply no multi-file
transaction or server-level replay/authorization guarantee.
The authenticated host exposes GET /api/hygiene/review and
POST /api/hygiene/review with strict { "operation": "start" | "pause" | "resume" | "refresh" }
(and, additive in 0.45.0, undo-media: see media review effects).
Both return { "review": HygieneReviewState, "action": InboxActionItem | null }.
The read is operational state; start/resume select through the supported
brain hygiene next --json boundary without inference. Owner and ambient
principals may operate the review; delegated agent and system principals cannot.
The normal auth/origin/body limits apply. Missing auth is 401, narrower authority
refusal is 403, malformed commands are 400 and unavailable/failed CLI reads are
review_failed (500). No new WebSocket capability is implied: clients discover
these HTTP operations through this contract; Actions use existing inbox replication.
HygieneReviewState has version: 1, status (idle, active, paused,
complete, blocked), nonnegative position, fixed, dismissed, snoozed,
optional pendingActionId, optional C2 counts and optional CLI blocker.
Additive in 0.41.0 (#1490): optional pauseReason is
{ kind: "actions-limit", retiredActionId, retirementReceiptId }.
When the shared hard Actions cap drops the pending review card (including a
snoozed card), its retirement and the review pause commit together. The review
clears pendingActionId, returns action: null, and retains the dropped Action
and shared FYI receipt through pauseReason. The three disposition counters and
hygiene Markdown dispositions are unchanged; the original suppression remains.
Read, reload, recovery and lifecycle sweeps never re-admit this card.
An explicit human start/resume re-admits the unchanged canonical finding under a
fresh operational Action and thread identity, with freshly generated previews,
through normal admission and the same hard cap. It clears the reason only when
selection succeeds. Concurrent/repeated requests retain one pending card. If the
fresh card immediately loses at the cap, the review pauses again with its new
retirement receipt; there is no retry loop. A human Pause during in-flight
Resume cancels that admission. Selection still reconciles through
next; if that selects a different finding, the supported list read confirms
that the retired fingerprint is still open before its retained finding is used.
Changed or no-longer-open evidence follows the ordinary fresh selection instead.
CLI failure preserves the paused reason for a later explicit attempt; a CLI
blocker can coexist with it. Operational backups validate the referenced dropped
hygiene Action and matching FYI receipt as well as pending/snoozed pointers.
If a confirmed CLI operation is already in flight at retirement, its journal
remains backed up with the dropped card and matching retirement receipt. Its
actual result settles that journal without changing the terminal card or review
disposition counters. Recovery checks an uncertain operation through the existing
read/check path; it never repeats the write or re-admits the card. Retirement
itself does not revoke principal authority or cancel an already confirmed write.
A lost CLI receipt retains the started journal and releases the in-process guard,
including when the card was dropped during dispatch. A later check-only recovery
settles it without updating the terminal card or repeating the content write.
Position counts presentations in this durable review, including a due snooze's
return. The three counters count confirmed review dispositions; C2 counts retain
their distinct backlog meaning. A reload or another device reads the same state.
Manual pause neither disposes the finding nor changes its pending Action. Start/resume
returns an existing pending Action. An empty selection returns deferred counts
without an Action; a configuration/check blocker returns the CLI blocker instead.
refresh (#1493) explicitly re-reads the pending canonical finding through
brain hygiene next --finding <id> --dry-run --json. This targeted form keeps
the existing finding/counts envelope and returns current evidence/handlers for
that detected open noninformational finding, even when another finding would
win ordinary selection. An absent/ineligible finding is null. --finding
requires a nonempty ID and --dry-run; malformed usage exits 1. Dry-run refreshes
the disposable index but writes no Markdown, including hygiene logs. Counts
reflect the existing durable log against current detection, without reconciling
its dispositions. Ordinary next retains its selection and reconciliation.
If the fingerprint changes, the server prepares current handler previews and
rechecks the finding after preparation. In one transaction it retires the old
Action as dropped with a superseded outcome and replacement ID, and admits
one new pending Action. Each Action retains its immutable finding identity,
fingerprint and stored resolution effects. A fingerprint returning to an earlier
value creates another new Action, never revives a terminal one. Supersession
records no resolution effect, repair, dismissal, snooze or cap suppression; it
writes no Markdown, changes no review counters or position, and never runs cap
retirement of other cards. Existing inbox replication publishes both cards.
An unchanged fingerprint is a no-op returning the current card. A disappeared
finding or intervening edit leaves the old card pending with stale; unavailable
checks leave it pending with refused. An in-progress effect or concurrent Action
revision refuses refresh (review_failed, 500) without replacement. Authority is
rechecked after I/O. Every old-card preview or confirmation is refused, including
requests from another device. A fresh card still needs a separately confirmed
repair. Reload/restart retain the superseded receipt and new pointer. Refresh
preserves a normal Pause; a review with no pending card (including cap retirement)
requires explicit Start/Resume for admission, never Refresh.
An Action may carry optional hygiene:
{ findingId, fingerprint, finding, outcome? }. finding retains the supported
C2/C3 projection, including the complete handlers descriptors, priority reason,
provenance, presentation and invalidation receipt. outcome is
{ version: 1, status, reason?, code?, undoToken?, fieldError?, supersededBy? }; its statuses are
applying, fixed, stale, refused, check_failed, still_detected,
not_detected, undone, dismissed, snoozed, superseded. A superseded
outcome also carries supersededBy, the replacement Action ID. The Action's ordinary version
advances whenever its outcome or confirmed options change, emitting the existing
upsert delta. Failed/uncertain outcomes survive restart and stay pending.
Stored options may carry input (the CLI descriptor: type is none, string,
enum, date, strings or path, plus optional values/example) and
preview ({ path, before, after, changes: [{ before, after, line }] }).
A new v1 hygiene resolution effect carries operation (resolve, check,
undo, dismiss, snooze), findingId, fingerprint, and optional handler,
input, previewToken, undoToken. This kind is admitted only by the concrete
human-review coordinator, never generic model escalation. Strict validation
rejects extra authority fields. All existing effect kinds retain their meaning.
POST /api/hygiene/review/preview takes strict
{ itemId, optionId, expectedVersion, input }, where input is null, a string
(maximum 4096 characters), or at most 128 strings (maximum 256 characters each).
It runs C3's read-only dry-run, validates the current principal and item version
again after I/O, then stores a new immutable option revision and publishes the
updated Action. A successful response is that Action; failed handler validation
is a persisted refused/stale outcome on the pending Action. Missing auth is 401,
authority refusal 403, malformed data 400 and stale/unavailable preview
preview_refused (409). Diff before/after/replacement strings follow
MAX_PROMPT_CHARS; changes are bounded to 1024. Oversized previews are refused,
never truncated into a confirmation. Input-free repairs are previewed at start;
field/picker repairs require this endpoint before confirmation.
inbox_resolve adds optional input with those same bounds. It must equal the
value bound to the stored preview; it never supplies an effect, fingerprint or
new authority. Resolution uses the existing principal/option/frozen-revision
checks and its generic refusal envelope. A content repair requires both the
stored fingerprint and preview token, passed to C3. Only fixed completes a
repair; stale, refused, check_failed and still_detected never advance.
Done, check again completes only on not_detected. check_failed with an
undo token adds a separate Undo change option with C3's inverse preview;
confirmed Undo remains pending and invalidates forward previews. Nothing applies
Undo automatically. Generic inbox_snooze refuses hygiene Actions: select their
stored later option so the shared server calendar time reaches both Action
waitUntil and markdown until: (CLI list.dueAt is the instant, while legacy
list.until remains the day). Optional dismissal feedback uses existing reason.
A confirmed fixed/dismissed/snoozed disposition admits at most one next Action while active. Identical replays create no next item and run no second repair; conflicting terminal responses are refused. Due hygiene snoozes remain in Later until C2 selects them; the generic sweep cannot create a second pending hygiene card beside the current one. This review does not enqueue agent work.
The operational database journals confirmed dispatch before CLI I/O. Boot, or
an explicit check after an unknown receipt, recovers interrupted repairs by
reading the recorded finding/fingerprint and invoking check, never by replaying
resolve. A not_detected recovery records fixed; a changed fingerprint stays
stale. Interrupted dispositions are verified against CLI log state and their
exact due instant. An uncertain inverse remains check_failed; it is not
reapplied. Neither leaving nor disconnecting stops an already confirmed effect.
When the host runs managed reference worktrees and core's hygiene functions
load, the same review also selects host-only findings about those worktrees.
They are never written to context/hygiene/ and never appear in
brain hygiene list or any other CLI output; their findings and dispositions
live in the host's operational database
(decision).
- A worktree is at most one finding, with
category: "managed-worktree",path: "reference-worktree://<worktree id>", an ID and fingerprint from core'scanonicalFindings, and an additiveworktreeobject (id,domain,state,changedPaths,removal; a public worktree addsrepository,revision,commitandsample). A private worktree's card names no repository, revision, commit, changed path or principal. - Its repair handler is
delete-worktree(inputnone), offered for retained unfinished work and for a failed removal, never for a worktree whose holder, owner or work is unknown. Its previewpathis the logicalreference-worktree/<worktree id>, with an emptyafter. start/resumepick across both backlogs by the CLI's ordinal rules and report summed counts; every operation on a worktree finding is answered by the host and never sent to the CLI. Dismiss, snooze and check keep the CLI's result shapes and fingerprint semantics; a dismissal or snooze never removes a worktree.- A confirmed removal is revalidated by the worktree service immediately before
it deletes: the reviewer's authority, the exact native identity, no live or
unknown holder, the reviewed fingerprint and preview against a fresh
assessment. Any change returns
stale. A removal that fails returnscheck_failedwithcode: "cleanup-failed"and a fixedreason; the same finding then reappears with a new fingerprint.
The owner principal may read what a private worktree's replicated card leaves out (decision).
HygieneReviewReadgains optionalprivateDetailAccess: { itemId }. The host adds it per request, toGET /api/hygiene/reviewand thePOSTcommand response, only when the caller is a usable owner principal and the pending Action is a privatemanaged-worktreefinding. It is never part of the replicated Action; its absence means no access.GET /api/hygiene/review/details/:itemIdreturnsHygieneWorktreeDetailsRead(hygieneWorktreeDetailsReadSchema), withCache-Control: no-store. Every other principal, including the worktree's own delegated agent and any ambient principal, gets 403{ error: "Unauthorized" }; authority is read again after the host read.{ status: "details", details }carriesowner({ label, kind: "agent" | "device" }, ornullfor a removed, revoked or expired principal),repository,revisionandcommit(nullwhen not recorded),changedPaths(nullwhen the work is not established, never0), the recordedsample,unverifiedPathsandassessedAt(epoch milliseconds, ornull).- A card whose finding is no longer open, or that is not a pending private
worktree card, reads
{ status: "gone" }; recorded facts that no longer match the card's fingerprint read{ status: "changed" }. Neither carries a detail value. - The read uses recorded facts only: it never selects, checks, reassesses,
previews or writes, so a finding's fingerprint,
last_seenand state are unchanged. Clients keep the details in memory only. HygieneCardPropsadds optionaldetails?: ReactNode, rendered between the evidence and the fix controls on the full card only. Its content is the caller's; the kit fetches and decides nothing.
Ordinary interactive and voice hosted turns default to the governed writable
profile. Claude's native write tools and Pi's direct file tools are available
subject to their existing tool permissions; voice retains its narrower roster.
createApp accepts optional workerProfile: "governed" | "strict" to tighten
ordinary turns. Autonomous turns remain restricted. Server application tools
keep their approvals and exact-base semantics. Snapshot consumers execute
configuration from complete read-only startup views with a separate validated
write path, as ruled on #1649. In 0.42.0, startup inputs and publication copies shared a
separate bounded output-staging allowance (256 MiB by default), admitted through
the worker ledger; exhausted aggregate capacity refuses as worker_state_busy
with Retry. BRAIN_UI_OUTPUT_STAGING_BYTES and
BRAIN_UI_OUTPUT_STAGING_BUDGET_BYTES configure it. Invalid values refuse boot;
oversized starting copies refused as worker_state_exceeded. Under the native
storage requirement below, inputs use native checkpoints; writes beyond
the filesystem allowance fail with ENOSPC. Internal launch/request fields remain internal.
The maintainer approved managed Btrfs storage and its capacity scope on
2026-10-14. Hosted createApp requires ServerConfig.nativeBrain with
socketPath, timeoutMs and maxDeltaBytes; environment resolution uses
BRAIN_UI_NATIVE_BRAIN_SOCKET, BRAIN_UI_NATIVE_BRAIN_TIMEOUT_MS (30000) and
BRAIN_UI_NATIVE_BRAIN_DELTA_BYTES (67108864). The last two values are
positive finite transport bounds, not storage allowances. Optional leasePath
(BRAIN_UI_NATIVE_BRAIN_LEASE_PATH) selects the persistent checkpoint ownership
ledger; it defaults to DB_PATH.native-leases.sqlite and is required explicitly
for an ephemeral UI database. Keep it across restarts, outside the brain.
Readiness verifies
that the configured brain is the manager's authoritative live subvolume.
Unsupported, absent or mismatched storage refuses startup with setup guidance;
there is no full-copy fallback.
The SDK ships the host-only brain-native-store launcher. It reads one fixed,
root-owned configuration file and serves a Unix socket; it cannot provision,
format or resize storage. Its manager operations and checkpoint identities are
internal implementation details, not a new general filesystem API.
Read-only native checkpoints now supply complete immutable startup inputs. The 256 MiB output-staging allowance still bounds writable staged outputs, but no longer holds full input copies. Native checkpoints instead consume the separate, explicitly configured finite managed-volume capacity. Retained changed-file versions, journal metadata and object-write reservations retain the existing 10 GiB journal cap. No numeric total-volume default is supplied. See the storage decision.
WorkerHostCheck and GovernedStatus gain the optional verified identity and
UID fields described in the HTTP contract.
WorkerNoteFacts.identity optionally carries { kind: "separate_uid", workerUid } from a successful host probe, so capability notes name the actual
worker identity. Existing callers may omit these facts.
For separate-UID native recovery, the fixed root-owned brain-native-store
configuration optionally sets workerUid to a positive UID different from
appUid. It must match the qualified helper's UID. Native journal manager
adapters may supply optional readCheckpoint, statCheckpoint and
listCheckpoint operations; the server validates the fixed reader's UID
before consuming private bytes. Existing adapters remain valid, with explicit
unreadable results when they cannot recover a private input. These operations
retain existing owned checkpoint authority and storage caps; they do not
authorize arbitrary host paths. See the host boundary
for descriptor transfer, supervision, cleanup and best-effort limits.
The root-owned brain-native-store configuration accepts an optional
referencePool object for the separate reference source/worktree volume of
the reference-worktree decision. It
enables no consumer: no acquisition, worktree tool or worker mount uses the
pool in this release, and ServerConfig is unchanged.
| Field | Meaning |
|---|---|
storePath, devicePath, fsid |
Root-owned top-level mount, block device and UUID of a single-device Btrfs volume that is not the brain store. |
combinedLimitBytes |
Combined allowance for source, worktrees and staging; default 10 GiB (10737418240). |
operationLimitBytes |
Per acquisition/snapshot/worktree allowance; default 2 GiB (2147483648). |
emergencyBytes |
Pool-local allocation released only to record recovery outcomes under ENOSPC; outside the combined allowance. |
physicalFloorBytes |
Free space kept on the physical backing; never spendable budget. |
headroomLedgerPath |
Required for sparse loop backing: a file in a root-owned directory on the backing filesystem, shared by every consumer of that physical capacity. |
maxIdentities |
Bound on retained pool identities, default 4096, at most 65536. |
All byte values are explicit positive multiples of 4096; invalid values refuse
startup. The operator provisions the volume with simple quotas enabled
(btrfs quota enable --simple, btrfs-progs 6.7 or later, or
BTRFS_IOC_QUOTA_CTL with BTRFS_QUOTA_CTL_ENABLE_SIMPLE_QUOTA) before any
data is written. Data, metadata and system profiles must each be single or
dup. The mount must be read-write, without synchronous discard or
user_subvol_rm_allowed, and appear only once outside managed root views.
Loop backing must be a root-owned private file of the device's exact size and
must not live on the brain store volume. Unqualified pools report their refusal;
they never block brain storage startup or recovery.
brain-native-store pool status|recovery [--after <id>]|release <id> <config>
queries the running manager over its socket and prints one JSON object with
schema_version: 1. Success has ok: true, action and status,
records/next or record. A manager refusal has ok: false, action,
code and message and exits 3; usage errors exit 2. status reports
qualification and refusal, configured limits, the accounting basis
btrfs-space-info-disk-used+simple-quota-owner with its observation
generation, unique aggregate allocation including profile replication,
emergency allocation, charged, owner-attributed and global bytes, per-profile
space, pending reservations, the reservation factor and available allowance,
plus the physical backing's kind, unbacked bytes, ledger consumers and headroom.
Records carry id, purpose, from, allowanceBytes, state
(reserved, writable, sealed, deleting, deleted or reclaimed),
root, path, ownerBytes and reason. The native manager client gains
poolStatus, poolCreate, poolSeal, poolRelease and listPoolRecovery
with NativePoolStatus/NativePoolRecord results; they are reachable through the
existing client signature and add no request path, host path or general
filesystem API. The lease owner adds the same pool methods, refuses deletion
without a recorded lease, and reports retainedPool from recoverDeadOwners,
which never deletes pool roots.
The native manager client and the lease owner gain poolPublish(id, view, name),
poolUnpublish(view, name) and poolWithdraw(id). view is a caller-generated UUID naming one
sharing domain; name is 1–128 characters of lowercase letters, digits and
._@+-, starting with a letter or digit, and never a UUID. Publish requires a
sealed source root and returns its NativePoolRecord; unpublish returns
{ view, name, removed }; withdraw removes a published root's names, moves it
out of its view and returns its record with view: null. NativePoolRecord gains view: the view whose
directory holds a published root, else null. The lease owner publishes only
roots it holds. Records from earlier pools read as view: null. Neither call
accepts a host path. Worker masks now include the pool's roots and views
directories, and a sealed root's top directory becomes 0755 root:root. The
mechanism is recorded in the
pool decision.
ServerConfig.referenceSource? is { validationMs, refreshMs, operations }.
The host edge resolves it from BRAIN_UI_REFERENCE_VALIDATION_SECONDS and
BRAIN_UI_REFERENCE_REFRESH_SECONDS (default 300 each, 1–86400) and from
BRAIN_UI_REFERENCE_ACQUISITIONS (default 2, 1–16); invalid values refuse
startup. An absent field in a programmatic configuration uses those defaults.
When the native manager reports a reference pool, createApp starts the
reference source service and reconciles owned source roots at boot. It also
cancels and withdraws private source when a grant or principal is revoked. No
HTTP route, tool or worker mount exposes it in this release. The behaviour is
recorded in the acquisition decision.
The native manager client and the lease owner gain worktree checkpoints and
access generations. poolCheckpoint(id, from) returns NativePoolCheckpoint:
a sealed checkpoint record of the writable worktree from, with its native
transaction and readOnly: true. poolDelta(from, to) returns the same
framed metadata delta as delta, for two checkpoints of one worktree.
poolReadCheckpoint, poolStatCheckpoint and poolListCheckpoint read a
checkpoint through the fixed worker-UID reader, with the bounds of
readCheckpoint. poolAccessOpen(access) and poolAccessClose(access) return
NativePoolAccess ({ access, path, roots }); poolAccess(id, access) and
poolPark(id) return the worktree's NativePoolRecord. access is a
caller-generated UUID naming one worker generation. NativePoolRecord.purpose
gains checkpoint, and records gain access, the generation holding a
writable worktree, else null; earlier records read as access: null.
poolCreate refuses the checkpoint purpose and a checkpoint as its source.
poolRelease refuses a writable worktree whose record names an access
generation with invalid, deleting nothing; park it first (#2729).
The lease owner checkpoints only a held worktree, reads and diffs only held
checkpoints, and moves only held worktrees; its lease table migrates in place.
No call accepts a host path, UID or mount. Worker masks now include the pool's
access directory, and brain-native-store pool records carry the new fields.
The mechanism is recorded in the
pool decision.
@schlessera/brain-ui-sdk/tool-contracts (re-exported from /server) gains
REFERENCE_SOURCE_CONTRACT, REFERENCE_WORKTREE_CONTRACT,
REFERENCE_TOOL_CONTRACTS, their tool names and strict input schemas,
REFERENCE_WORKTREE_ACTIONS, REFERENCE_WORKTREE_LIST_LIMIT (20) and the types
ReferenceToolCall, ReferenceToolResult, ReferenceToolRefusalCode,
ReferenceSourceView and ReferenceWorktreeView. Both contracts join
BRIDGE_TOOL_CONTRACTS, after turn_temp, so BRIDGE_TOOL_POSTURE.names and
allowedTools(adapter) end with reference_source and reference_worktree.
SurfaceTools gains referenceSource and referenceWorktree. /server gains
handleReferenceTool(name, input, bridge), which validates the input and
returns { text, isError }, and BackendBridge.referenceAccess?(call), through
which a backend hands the host only the tool name and parsed input. The
tools themselves are specified in the
MCP contract.
The native manager client and lease owner gain poolView(view), which opens
one view's root-owned 0755 directory before its first publication and returns
{ view, path }; it accepts no host path and publishes nothing.
/internal gains WORKER_REFERENCE_ROOT (/tmp/brain-reference), and the
internal WorkerSandbox.references?: { public?, private? } binds those view
directories read-only at public/ and private/ beneath it. The launcher
refuses a view that is not a root-owned 0755 directory outside the brain, and
any view in a restricted, maintenance or transport launch.
createApp sets referenceWorktreeAccess whenever a reference pool exists.
Pi's internal TOOL_RISK classifies reference_source as read and
reference_worktree as mutate.
Hosted admission state (WsHost.governedTurns) and the server's contention lock
(WsHost.workerLock) are internal wiring. They do not extend the supported
embedder surface of WsHost; generated API reports retain that classification.
ModelPickerProps.chat? adds the approved This chat native radio group and
Limits disclosure: profile (governed/strict/null), disabledReason?,
saving?, error?, running?, limits (facts or null), and onProfile?.
Confirmed selection remains visible while saving or after failure; pending
controls preserve focus without accepting a second save. Voice, offline,
policy-locked and unavailable states show their reason read-only. The picker
remains reachable for a fixed model without effort controls. Native keyboard
behavior, 44px rows, Escape and return focus reuse the existing picker.
SessionWorkerProfile and sessionWorkerProfileSchema are additive SDK exports.
The SDK exports PROPOSE_POLICY_CHANGE_CONTRACT, its name/input schema,
PolicyChangeInput, POLICY_CHANGE_REVIEW_SCHEMA and PolicyChangeReview
through tool-contracts and server. BackendBridge.proposePolicyChange?
accepts a native tool-use request id and that input, returning the server-held
review/receipt; absent capability means no hosted policy-proposal tool.
PermissionRequest.kind additionally accepts policy; this never receives
remembered or automatic approval. The machine behavior is specified in the
MCP contract.
ApprovalCardProps.wrapActions? allows long decision labels to wrap into
separate rows while retaining effective 44px targets. Omitted preserves the
existing card layout. AgentPolicyRead carries read-only policy hash/source,
validation notices, effective profiles, protected paths and the host's recorded
worker-resource limits. Parsed budget/scope/egress values are not enforcement
facts. No new extension seam or editor is introduced.
BackendActivityEvent.usage reports cumulative completed-response usage for
ordinary interactive/voice turns; the existing autonomous variants retain
their Queue semantics. budget_backstop reports Claude's SDK budget stop.
StartTurnRequest.maxBudgetUsd passes the host's remaining turn limit to that
SDK. ServerUsage, TurnBudgetReceipt, BudgetObservation and BudgetScope
are the additive public wire types. TurnJournalSummary.budget shares the
existing receipt; its list-price cost never changes the effective billing cost.
TurnFailure.budgetScope supports the reviewed conversation handoff and UTC
day refusal. These are facts on existing backend and protocol surfaces, not a
new policy parser, budget provider or grant interface.
The SDK protocol exports ClientScopeDecision, ServerScopeDecisionReceipt,
ScopeDimension, ScopeCapReceipt and TurnScopeSummary.
TurnJournalSummary.scope is optional. StartTurnRequest.workerStarted is a
server-only parent lifecycle callback; it is never serialized into workers and
never accepts a client/model capability. Both first-party adapters notify it
with the launched worker's concrete freeze/resume/kill/exit handle.
WorkerSandbox.scopePolicy feeds the existing capabilities-note generator from
the immutable admitted policy; WorkerNoteFacts.hardTimeoutMs states the
independent hard deadline. Kit AskUserOption/ChoiceOptionProps.disabled
make pending/offline scope options inert with a readable reason supplied by
the surrounding card. These additions ship in a minor; the wire behavior is
specified separately.
@schlessera/brain-ui-sdk/server exports createTranscriptStore, the existing
TranscriptStore interface, and the additive RecoverableTranscriptStore,
TranscriptDiagnostic, TranscriptRecord, TranscriptMetaRecord,
TranscriptMessageRecord, TranscriptFailure, TranscriptRecoveryProvenance,
TranscriptCorruptionError and TranscriptRecoveryError. The factory returns
RecoverableTranscriptStore; existing third-party TranscriptStore
implementations gain no required members. Healthy create, append, list
and history signatures and results retain their existing shapes.
A malformed JSON/UTF-8 line or unsupported record quarantines that file in
place. Required metadata is the first nonblank record: kind: "meta", an
id matching the filename, nullable string title and finite nonnegative
createdAt. Later records have kind: "message", finite nonnegative ts, a
supported SessionHistoryMessage and optional finite nonnegative costUsd.
Unknown additive properties remain intact. Empty files, missing/wrong metadata
and repeated metadata are corrupt. Blank lines are allowed. Valid final JSON
without a newline is healthy; append supplies a separating newline.
Admission also refuses malformed optional fields that the wire reader would
normalize or discard: a verified raw prefix must remain valid without alteration.
list() excludes quarantined files, without aborting healthy enumeration.
history() and append() on a quarantined file throw
TranscriptCorruptionError (code: "TRANSCRIPT_CORRUPT", diagnostic). Unknown
history remains empty; unknown append still refuses. Filesystem errors other
than absence propagate rather than being classified as transcript corruption.
No operation silently repairs, truncates, relocates or logs original content.
diagnose(sessionId)returns{ sessionId, status: "missing" }for absence. Otherwise it returnssessionId,sourceVersion(SHA-256 of all source bytes),prefixBytes(exclusive verified byte boundary), andrecords(the contiguous admitted records, with original objects). Healthy results havestatus: "healthy", complete: true; quarantined results havestatus: "quarantined", complete: false, failure. The failure carries one-basedline, zero-basedbyteOffset,position: "final" | "interior"andreason: "invalid-json" | "invalid-record" | "invalid-utf8" | "missing-meta". Position describes whether nonblank bytes follow the failed line, not a guessed crash cause. No records after the first failure are admitted.quarantined()explicitly enumerates only quarantined diagnostics, including their qualified prefixes. Ordinary history never presents these as complete.recover(sessionId, { sourceVersion })is explicit authorization for the inspected version. It returns a new session id, with metadata titleRecovered: <original title or source id>andrecoveryprovenance containingsourceSessionId,sourceVersion,prefixBytes,prefixRecords, and the originalfailure. The original identity/bytes remain unchanged. Message lines, timestamps, costs and unknown properties are copied byte for byte; only metadata is replaced, with a trailing separator added if needed. Provenance persists in metadata and is accessible through diagnostics; normalChatSessionand history shapes gain no required fields.
Recovery refuses missing/changed source with TranscriptRecoveryError
(code: "SOURCE_CHANGED"), healthy source or missing valid metadata with
"NOT_RECOVERABLE", and incompatible existing recovery content with
"RECOVERY_CONFLICT" (corrupt existing recovery raises TranscriptCorruptionError).
It hashes the full source at admission and again around publication, refusing
observed concurrent edits. Callers must serialize external transcript edits
with recovery: this is optimistic filesystem validation, not an OS lock against
arbitrary writers. The store itself never appends to the quarantined source.
Recovery validates its complete output, writes and fsyncs a hidden .pending
file, atomically links it without replacing an existing target, then fsyncs the
directory. The target identity is deterministic for source identity/version;
concurrent attempts and retries converge on that identity, including after
subsequent valid appends. Interrupted staging cannot appear in list();
a process crash can leave hidden staging evidence. A failure after publication
may leave a complete labelled recovery, and retry validates it and syncs the
directory instead of creating a duplicate. A different source version requires
a new inspection/authorization and represents a distinct recovery.
Only optional plain API-loop backends use this helper. No shipped backend
currently calls it; Claude/pi keep native persistence. Adopting backends keep
normal listSessions/getHistory delegation, catch the typed per-session
corruption outcome, and invoke diagnostics/recovery only through deliberate
host-owned actions. They expose the returned title unchanged and resume the
new returned identity. No wire frame, HTTP route, UI destination or native
backend recovery interface is added by this SDK change. These supported
additions ship in a minor under the recorded #1806 rulings.
AskUserCardProps in @schlessera/brain-ui-kit gains optional otherText?: string and onOtherChange?: (text: string) => void for a controlled custom
answer draft. Without them the card keeps a local draft. While Other is open,
the persistent Submit action invokes the existing onOtherSubmit callback
with current nonblank text, as Enter does. Blank text disables Submit with an
explanation. showActions={false} and closed exchanges still omit actions.
The client preserves selected predefined answers beside custom text in a
multi-select and the existing grouped-question submission path. No wire,
CLI/MCP or answer-delivery contract changes.
AgentBackend.storageRoots?() is a synchronous host-only observation of already
owned transcript/archive roots: readonly {kind: "transcripts" | "archive"; path: string}[]. It performs no setup or provider discovery. The server consults
only already initialized owners, never passes these paths to a worker bridge,
and converts them into opaque root IDs before transport. Older backends remain
valid and report unavailable transcript accounting. Claude supplies its exact
brain-specific live/archive leaves; Pi supplies its already resolved session root.
@schlessera/brain/storage supports host-shell authors who need bounded live
physical metadata without evaluating a content repository. The operation
readStorageInventory({brainPath, gitRoots?, signal?}) returns a promise of the
version-1 Storage inventory envelope, with the existing 100,000-entry/30-second
limits and isolated/reaped filesystem worker. brainPath is absolute;
gitRoots contains at most sixteen explicitly approved absolute external stores.
Invalid roots reject before traversal. The host must enforce user authorization;
this is not an agent-facing filesystem browser.
This inventory operation executes neither Git nor the brain CLI, and loads no repository
configuration/providers. It reads only fixed Git location markers (.git,
commondir, objects/info/alternates) through bounded non-followed descriptors,
approving external stores before reading their markers. It supports ordinary
worktrees and linked-worktree/alternate layouts; unsupported markers, redirects,
mount transitions and encodings remain partial evidence. It does not implement
Git configuration semantics or discover a repository from parent directories.
Host-selected root aliases retain the inventory's canonical-root semantics.
The ordinary standalone brain storage inventory CLI retains its existing Git
resolver contract and must run through the maintenance boundary when invoked by
a governed server. The HTTP inventory uses the public metadata reader so it
measures the canonical live objects, not a maintenance worker's native copy.
The same structural inventory schema is consumed; available allocation is still
a qualified charge and never a reclaimability guarantee. Bun/types/default
exports resolve source/declarations/build from the same release. Older core
installations without this entry produce unavailable inventory, with no CLI or
internal-import fallback.
@schlessera/brain-ui-sdk/protocol exports StorageMaintenanceKind,
StorageMaintenanceRequest, StorageMaintenanceItem,
StorageMaintenanceObservation, StorageMaintenanceReceipt and
StorageMaintenanceRow. These describe the three fixed host-owner operations,
caller-owned durable identity, qualified state/outcome, headroom disclosure and
independent store/capacity measurements in the
maintenance HTTP contract.
They do not add an agent tool or generic executor. Existing automatic retention
and owner APIs retain their previous defaults.
@schlessera/brain/storage additionally exports STORAGE_ATTACHMENT_LIMITS,
StorageAttachmentRefusal, storageAttachmentPath, inspectStorageAttachment,
removeStorageAttachment, restoreStorageAttachment, storageAttachmentVacant,
inspectStorageAttachmentFacts, and the StorageAttachmentVersion and
StorageAttachmentFacts types. The inventory reader keeps its existing contract.
Raw-file operations bind an absolute canonical Linux root, relative path and
exact descriptor-derived version (hash, size, mode, inode/stamp and parent chain).
They refuse symlinks, aliases, nested mounts and executable/protected targets.
The caller must own exclusive writer coordination, current human authorization,
verified durable recovery and intent before calling removal. Its optional final
assertCurrent callback revalidates authority immediately before unlink or restore;
restoration uses a caller-leased verified original and never overwrites a present
entry. These primitives neither provide a journal nor change its retention policy.
Raw hashing supports cancellation and bounded file size/time.
inspectStorageAttachmentFacts(root, paths, signal?) evaluates Git and bounded
current Markdown. Hosted consumers must run it inside their qualified maintenance
worker and discard outputs. It is not a privileged host-side Git reader, and its
unchecked scopes prevent interpreting an empty reference list as proof of no
consumers. Core semantics have no dependency on the UI database. Server contracts
and bounds are defined by the removal HTTP contract.
@schlessera/brain-ui-sdk/protocol exports StorageRemovalInspection, StorageRemovalReviewRequest,
StorageRemovalStartRequest, StorageRemovalUndoRequest, StorageRemovalFile,
StorageRemovalReview, StorageRemovalItem, StorageRemovalReceipt and
StorageRemovalCapability. They contain relative paths and opaque identities,
never private recovery roots, executable choices or credentials.
@schlessera/brain-ui-kit exports StorageMap, StorageMapItem and
StorageMapProps. Items supply opaque identities, names, ownership
(brain, app, operator), known disjoint bytes, a size/share label and optional
smaller/interactive flags. Props supply those items, CSS-pixel width/height,
an accessible group label and description ID, and onOpen(id)/onUp() callbacks.
The component performs no I/O; callers own the bounded, deduplicated inventory
and equivalent complete list. Unknown and nonpositive sizes have no tile area;
small tiles have no activation target. It preserves one roving tab stop.
The ui-react Settings selection accepts storage. openSettings("storage")
and the existing palette open the same lazy pane after Modules; Models remains
the default. This adds no main destination or keyboard shortcut. Requests and
local selections remain scoped to the current root and authentication lifetime.
AppShell includes a persistent owner-scoped Storage status. It and the lazy
Settings pane consume named server guidance; there is no client warning or
forecast engine. useHashRoutes() accepts #/settings/storage after a current
authorized guidance read, preserves Settings leave guards and clears the link
when Storage closes. These additions create no navigation destination, keyboard
shortcut or new exported component. Evidence is isolated by root, account and
authentication epoch; forbidden responses clear it.
The server-only BackendActivityEvent variant claude_session_usage carries
optional completed: TurnUsage, cumulative: TurnUsage and
reportedCostUsd: number. completed is this host turn's completed root/child
response usage; cumulative and reportedCostUsd are source-qualified runtime
session totals, never turn receipts. Repeated events refine one host turn;
they do not represent additional spend. An absent cumulative total or completed
receipt stays unknown. The host persists a conservative auxiliary high-water
reconciliation separately from main usage, without assigning title-helper
provenance or relying on a model name. Direct backend consumers receive own
completed token counts on result; its absent cost remains unknown until a
host prices those counts. Autonomous Queue semantics are unchanged.
The experimental @schlessera/brain-ui-sdk/client content seam exports
ContentRenderer, ContentRendererProps, ContentRendererPack,
ContentRendererRegistry, ContentRendererTheme, createContentRendererRegistry,
defaultContentRendererRegistry and registerContentRenderers.
Components receive source, optional path, mode (chat | file | share) and
theme (light | dark | print). Exact, case-sensitive language and extension
claims resolve first-registration-wins. Extensions include the leading dot.
Pack object identity dedupes registration; reset clears claims and identities.
A missing lookup returns null.
The React/DOM-independent @schlessera/brain-ui-sdk/render exports
StaticRenderer, StaticRendererPack, StaticRendererRegistry,
ContentRendererTheme, createStaticRendererRegistry, registerStaticRenderers
and staticRendererFor. Static entries carry claims, needsBrowser, and
toStatic(source, { theme, path? }): Promise<string>. The default lookup checks
language before extension on ambiguity. Client and static builds register
independently; neither loads code named by content/runtime configuration.
The experimental @schlessera/brain-ui-sdk/testing exports
contentRendererConformance(pack, fixtures), ContentRendererFixture and
ContentRendererFixtures. A real-browser mount harness lets the suite exercise
client components in every mode/theme and static output in every theme.
The authoring guide specifies the token
block/root ground convention. Conformance tests trusted bundled code; it is
not a sanitizer. Dispatch, manifests and concrete packs are separate epic work.
The additive, trusted StartTurnRequest.applicationAuthority?: "server" field
identifies a deployment whose authoritative mutations use the server route.
The UI server supplies it on every interactive turn, independent of posture,
strict fallback or temporary bridge handler availability. It is session-stable
host configuration, never client/model input or a grant. Claude uses it to keep
legacy project MCP writers shadowed even when applyBrain is temporarily absent;
its fixed in-process application tools then refuse unavailable routes at call
time. Direct unhosted consumers omitting it retain their legacy project MCP
roster. Existing custom bridges without the field retain the prior applyBrain
presence fallback. Autonomous authority/rosters are unchanged.
BrainApplicationResult adds indexStatus?: "pending", and indexed now
means the index covers the result's committed changes: a host that defers the
update reports indexed: false with indexStatus: "pending" and no error, as
specified in the application contract. BackendBridge adds the
optional awaitIndex(toolName). A backend awaits it before admitting any tool,
under the name the runtime calls it, and refuses the tool with the rejection's
message; the host resolves at once for tools that do not read the index. The
Claude and pi backends call it from their every-call admission paths (the
PreToolUse enforcement hook and the shared pi permission decision). A custom
backend that never calls it can read an index that misses applications its
turn already committed.
BackendBridge.awaitIndex takes an optional second argument, the tool's
input where the backend has it: awaitIndex(toolName, input?). The host may
also refuse a read it cannot serve current. The first-party Claude backend
passes tool_input from its PreToolUse enforcement hook, and the host refuses
a Claude Read of a brain path this turn applied, naming mcp__brain__brain_read.
A backend that omits the input keeps the earlier behaviour.
The internal WorkerSandbox gains optional @internal dataTree
(WorkerDataTree: { root, index, indexWritable?, masked? }, exported from
/internal with WORKER_DATA_ROOT, /tmp/brain-worker-data), and
StartTurnRequest gains the matching @internal workerDataTree for the
restricted envelope, as workerProviderMasks. The launcher mounts root
read-only at WORKER_DATA_ROOT and index read-only over its
.brain-index/, repeats the provider masks there, and sets BRAIN_ROOT and
BRAIN_WORKER_DATA_ROOT to WORKER_DATA_ROOT and BRAIN_WORKER_CONFIG_ROOT
to the brain path. Only a worker on an immutable startup view may receive one.
WorkerProviderMasks gains @internal indexWritable: the private index is
now mounted read-only unless the host's own build sets it. The first-party pi
backend reads its brain tools, read_file, grep and brain_read from the
tree and runs its shell there. In @schlessera/brain-ui-server,
GovernedTurnHold adds dataTree?: TurnDataTree. AppliedEffect's
content and base also accept a Uint8Array: an image mask's PNG reaches
the tree through the same commit (additive, #2892).
BackendActivityEvent adds prompt_prefix (prefix: PromptPrefixFingerprint,
resumed: boolean) once before the first request, and first_request_cache
(usage: FirstRequestCacheUsage) once for the first main request's receipt.
Both use the existing BackendBridge.activity channel and are observational,
never permission grants or provider-cache promises. Raw assembly text must be
hashed within the backend. Missing input components/counters stay null.
The host validates digest/counter shapes, copies only documented fields and
retains the first receipt; invalid or duplicate evidence cannot overwrite it.
Title helpers and child channels must never be reported as the first main
request. Their separately retained accounting remains independent.
PromptPrefixFingerprint, FirstRequestCacheUsage and ActivityCacheDiagnostics
are additive protocol types; their HTTP meaning and retention limits are in
cache diagnostics.
ModuleContribution.renderers?: Record<string, ModuleRenderer> is an additive
module input. ModuleRenderer has optional fences: string[] and
extensions: string[], required client: string and optional static: string.
Declarations are strict: unknown fields fail loading. Fence names match
^[a-z][a-z0-9-]*$; extensions start with .. Both entry specifiers
name export subpaths of the declaring module's own package, verified against
its package.json name.
Duplicate active fence/extension claims fail load naming both module owners.
Dormant declarations do not claim dispatch names. Built-in Mermaid reserves
mermaid, mmd, .mermaid and .mmd. Module lint resolves entries and
requires the static entry's named toStatic export to be a function. Ordinary
loading does not execute renderer entries. No runtime browser loading is added.
See modules and the
renderer decision.
The server-only BackendActivityEvent gains claude_title_usage with
coverage: "observed" | "partial" | "unknown" and receipts, an array of
{ receiptId: string; source: "generate_session_title"; usage: TurnUsage; costUsd?: number }. It follows worker/exporter shutdown and precedes release
of the held terminal result. It does not replace or add to the existing
claude_session_usage.completed root/child receipt.
Purpose comes from the installed runtime's supported OTLP API-request event,
not a model name. Receipt identity deduplicates exports/replays; measured
helper usage is allocated only against the matching newly reconciled auxiliary
delta in the session transaction. A separate Claude session title Activity
run retains that usage/cost, while unmatched usage remains unattributed and
main/cache usage retains its existing completed-response semantics. Missing
or unjoinable evidence stays unknown/partial, not a free helper or an inferred
main-model charge. Operator telemetry destinations and opt-outs are preserved;
without a local receipt, ordinary unattributed accounting still applies.
The built-in browser pack claims mermaid / mmd fences and .mermaid /
.mmd source files through the content-renderer registry. Its static entry
requires a browser and produces self-contained vector SVG without HTML
foreignObject labels. Shared PNG/PDF output and browser print use the kit's
opaque white print palette. Light and dark screen palettes remain unchanged.
inlineMermaidDiagrams(markdown, theme?) remains a compatibility entry point;
it now dispatches all bundled browser-only content renderers and defaults to
print. An explicit light or dark argument still selects that palette,
and print is an additive accepted theme. shareMarkdown retains its
inlineMermaid dependency name for existing callers. Failed or oversized
static rendering preserves the source fence and reports a lint warning.
@schlessera/brain-ui-react exports experimental
registerContentRendererPack(pack, { name, version }), accepting the SDK's
ContentRendererPack plus the bundled module manifest name and package version.
Call it before mounting. Registration is build-time trusted code, identity
idempotent, with first-claim dispatch. It never imports server-named code.
@schlessera/brain-ui-sdk exports ContentRendererDeclaration for the
authenticated bootstrap metadata.
StartTurnRequest.persistClaudeMemory is an optional host-only callback for
validated native Claude memory copy-out. The backend supplies a direct note,
its host-owned external directory, exact before/after bytes and before mode,
and a synchronous save closure. A null observation explicitly reports copy-out
that could not be inspected before a note callback. It is never serialized into a worker or
accepted from a client. The first-party server retains/pins versions and a
private pending receipt before invoking the closure, then publishes a steering
change only after the save succeeds. Failed or interrupted finalization remains
partial/unconfirmed. Other backends need not implement Claude native-memory
persistence; this adds no agent history authority or new public request route.
The callback may return a promise (additive, #2814): the first-party server commits its journal records on a write worker, off its event loop, and invokes the save closure only after they commit. A backend awaits the returned value before it persists the next note or ends the turn; a callback that returns nothing behaves exactly as before.
Ruling 1 on #2789
moves the hosted server's journal, Activity and native-lease commits to a
write-worker thread at synchronous=FULL, and makes their callers
asynchronous. On the TurnJournal reachable through WsHost.journal:
writeOperationtakes statements (JournalStatement[]) instead of a callback, and resolves with each statement's change count once they commit as one accounted write.bindSession,persistClaudeMemory,recordClaudeMemory,reserveRevert,finishReserved,withRetainedRemoval,withRetainedVersionandversion().stream()return promises that settle after their commits.bindModelstill answers synchronously, from the active turn's binding.readysettles once startup recovery has committed. It rejects with the refusal thatcreateTurnJournalused to throw.
AwaitableActivityStore is the Activity store as the hosted server holds it.
Its reads are the ActivityStore reads. Its writes may return promises that
settle after their commit. An ActivityStore, whose writes all commit before
they return, satisfies it. Refusal classes and transaction order are
unchanged. These shapes change at a pre-1.0 minor under that ruling.
ActivityStore.patchSpan accepts an optional sessionId to bind an unnamed
active span when the backend reports its session after the first frame. Repeating
the same identity is allowed; changing an established identity or patching a
terminal span returns false without modifying the row. The recorder persists
this identity in both the queryable session column and the existing attributes.
This additive API ships in a minor release.
BackendModuleContext, ClaudeBackendOptions, CreatePiBackendOptions and
BackendRegistryOptions accept optional exec: ExecWrapperConfig.
probeBackendRuntimes accepts the same optional configuration as its sixth
argument. The app passes its resolved ServerConfig.exec to startup probes
and backend construction. Both first-party backends use that host configuration
for runtime probes, Claude SDK workers and Pi grep/bash tools, including tools
inside Pi workers. An explicit empty object disables the optional wrapper;
an absent field preserves ambient-wrapper compatibility. Standalone backend
factories preserve omission, resolving the ambient wrapper and killer at the
existing turn/tool-spawn boundaries; Pi serializes that host fallback into each
worker turn. Explicit host configurations are copied at construction.
A Pi worker's view gains each configured helper file read-only at its configured path, and nothing else of the directory holding it, in ordinary and restricted workers alike. Each helper a worker runs, the Pi wrapper and killer and the ordinary Claude worker's wrapper, must be an absolute path naming a regular file, directly or through a symlink; anything else refuses the turn before any relay, reservation or worker is acquired. Existing argv, environment and permission semantics remain unchanged; the optional wrapper never replaces worker isolation.
BrainClient.modules({ signal }) accepts an optional abort signal. It cancels
metadata waiting for execution admission, including a required CLI version
probe. Once an execution owns its admission locks, its worker, output
publication and cleanup drain normally. Cancellation between a version probe
and the module lookup prevents the latter from starting. Calls without an
option preserve their existing behavior and result shape.
The internal host boundary carries a separate admission signal so existing
execution cancellation, including search worker termination and output drain,
keeps its semantics. The optional BrainClient.modules input is an additive
public TypeScript change and ships in a minor.
createApp() passes each /api/status and /api/modules request's signal,
so a caller that disconnects cancels its queued metadata. close() cancels
every metadata request still waiting for admission as soon as it begins.
Admitted metadata is a shared run: it drains within close()'s 10 s grace
period and is then cancelled with the other shared runs (#2473). The response
shapes and status codes are unchanged.
The @schlessera/brain-backend-pi root exports createPiAuth(options?) again,
with the types its signature reaches: CreatePiAuthOptions, PiAuth,
PiAuthProviderStatus, PiAuthRuntime, PiLoginFlow and PiLoginFlowStatus.
PiAuth is the headless OAuth sign-in service ui-server's /api/pi-auth/*
routes drive through the optional backend loader. The curated root dropped it
in 0.41.0, so those routes threw on every request; restoring it changes no HTTP
shape.
The #3070 contextual conversation amendment adds optional contextualTasks
evidence/open negotiation and LiveConversationSession.bindContextualWork.
ConversationWorkRef and receipts discriminate contextual task/native identity
from reviewed utterance identity; contextual work has no utteranceId. Result
acceptance may explicitly be unknown; returnWork can report acceptance evidence.
Unassigned fragments may carry explicit unavailable timing and native finalization
evidence instead of a provider interval. Exhaustive consumers must handle unknown
work/delivery and voice-context message provenance. These pre-1.0 breaking
changes ship in a minor under the October 28 #3070 ruling; see the normative
wire amendment.
The React root's conversation client exposes outputRecovery independently of
micOn and recoverOutput() for an output-only timeout. attachControls()
returns an idempotent release callback; a same-commit control remount retains
ownership, while releasing the last owner ends conversation at the microtask
boundary. Cleanup is not a manual Mute. The client lifecycle
defines the breaking semantic change from repeated Resume to continuous consent.
Manual Send, reviewed question answers and Discard end input through bounded
output recovery rather than leaving outputRecovery in speaking. Discard
retires the reviewed input identities; late recognition cannot restore them.
These actions and Mute cancel pending automatic review timers, while output recovery
remains independent of microphone consent.
The SDK protocol exports DialogueEvidence, DialogueRecord, DialoguePage,
ClientDialogueRead and ServerDialogueHistory, with runtime schemas in the
existing schemas entry. These are data-only retained history, not provider-native
messages or task admission. Their identities/bounds/coverage and the breaking
retention supersession are defined in the wire component.
Optional history-owner metadata on conversation open/start and plain chat input
is additive; legacy backend session IDs and native provider guards keep their
meaning. No provider history-selection API is delivered by this slice.
Failed-write live text exposes optional ServerConversationEvent.retention and
ServerError.historyOwnerId, plus the existing client projection's explicit
unsaved recognition/output fields (#3177). These are additive evidence and
failure-scoping data. DialogueJournal.voice reports commit success to its
callback; current-epoch text remains observable when the callback reports a
failed write. No provider-facing history or authority interface changes.
LiveConversationOpenOptions.history?: ConversationHistoryEnvelope is an
additive input on the existing live-provider seam. Version 1 contains ordered
HistoricalItem values and coverage with explicit HistoryGapReason values.
Optional host-only newestRefs identifies the possible newest items after exact
causal joins; it qualifies omissions and never enters provider text.
Typed chat (including recorded dictated/submitted-voice source), uncertain
recognized speech, generated voice replies, completed
host facts and gaps remain distinct. HistoricalResultRef preserves the exact
conversation/epoch/request/turn join internally; all source references,
provider identities and operational metadata stay out of provider text.
Recognized speech is never authenticated operator dialogue; generated text
never proves playback. Historical items grant no admission or permission.
The host reads canonical journal pages after authorization and known-chat
admission, joins native backend-owned answers only by exact position witnesses,
and reads completed facts through exact retained receipts. Live user items require recorded source provenance: exact turn identity, or
ordinal matching with equal full native/source duplicate counts when turn IDs
are unavailable. Unproven user items become in-place source_unavailable gaps
with partial coverage; ordinary chat display keeps its existing fallback. Native
assistant text remains historical data. This excludes contextual task wrappers
regardless of journal paging or receipt retention, as well as raw audio, hidden
thinking, tool requests and native response handles. Missing cross-stream position evidence qualifies order, without
replacing full native answers with range copies. A read failure refuses startup;
there is no automatic provider retry or context-free fallback. Authorization,
entry lifetime and owner/generation are rechecked after reads.
Both first-party adapters freeze one historical-data seed per native open.
Selection uses a combined exact serialized UTF-8 text ceiling, framing and
omission markers included: Gemini 24,000 bytes/16,000 fact bytes; GPT 7,500/5,000.
GPT's byte ceiling estimates headroom under its documented 8,192 rendered-token
limit; it is not a tokenizer proof. History previews within one-third of total,
reserving only actual bytes. Resync uses the remainder with the explicit fact
cap; history is selected again from remaining total/fact bytes, excluding only
exact result identities actually emitted by resync. Newest request/state and
newest historical discussion take priority, then whole facts (resync first),
then older discussion. At most 128 historical items are selected. Whole omitted
items leave in-place gap markers and coverage; tailOmitted reports an omitted
newest item, conservatively also qualifying discarded text whose cross-stream
recency is unknown. Partial coverage uses “may be missing” copy rather than a
claim about the exact newest item. These bounds never delete the display archive.
LiveConversationSession.historyCoverage? exposes the adapter's actual bounded
historical selection as {state, tailOmitted}. Third-party providers may ignore
the additive history input but cannot thereby claim existing-chat continuity.
The frozen provider prefix/tools/instructions are never rewritten within an epoch.
The reachable SessionCatalog.attachMessageSources signature also accepts
optional strict?: boolean. The default preserves ordinary display recovery's
existing fallback. true propagates source-read failures for live startup so
internal task carriers cannot be reclassified as typed chat after a read error.
WsHost.prepareHistory(sessionId, messages, strictSources?) performs strict source
joining in the ordinary position, after exact native turn/failure identity joins.
It must not normalize file-bearing user bytes before those prefix digests match.
Unwitnessed native prefixes precede the earliest journal evidence as a selection
policy, not a chronology witness. Later unpositioned runs follow their native
predecessor before the next witness. Exactly witnessed assistant ranges stay at
their recorded positions even when their request is unwitnessed. Unknown order
always carries a gap/partial coverage; missing request records in a partial source
do not prove that a turn predates the journal. Bounded selection preserves the
newest journal discussion and conservatively qualifies discarded ambiguous text.
An exact contextual receipt/turn/submitted-text join excludes a task carrier when
its asynchronous source metadata write failed, without filtering unrelated equal
user text. Any exact unclassified carrier without a provable native answer/nearest-
user join fails the snapshot closed, regardless of task state or later native turns.
Receipt lookup also requires the exact conversation, principal and session.
ConversationProfile and conversationProfileSchema are public SDK exports for
{providerId, capabilities, disclosure}. Hello may advertise this profile and
Start may echo it, with the intentional legacy admission refusal documented in
the wire component.
The host helper ConversationHost.profile(contextualTasks?) supplies the same known
provider-free effective disclosure for hello, admission comparison and opened
receipts. The host refuses undisclosed legacy starts before touching their scope;
the wire section records the deliberate privacy admission compatibility correction.
StorageSettlementIndeterminate is an additive error export from
@schlessera/brain-ui-server. Its operation: string names the bounded wait;
optional cleanup?: Promise<unknown> observes the original owned operation
through its genuine eventual success or failure. Awaiting the error's cleanup
is optional and can remain pending indefinitely. Neither the error nor a
rejected close proves persistence, cancellation, worker exit or resource
release.
BrainUiApp.close() refuses new admission at once and monitors real outstanding
requests on both its shared UI/journal SQLite worker and its separate native-lease
SQLite worker throughout cleanup. Native manager RPCs do not acquire a SQLite
deadline. A request pending at
close gets ten seconds from observation; a later submission gets ten seconds
from submission, including native FIFO queue wait. An unsettled native request
rejects close while retaining the actual cleanup continuation. This is not a
whole-app wall-clock deadline: protected CLI writers may outlast the CLI grace,
healthy reads canceled at the end of that grace still let close resolve, and
Activity preserves its own transport deadline and exact genuine error identity.
Close keeps one public outcome across repeated calls. Borrowers drain in their existing order,
then the actual writer closes before the reader. No worker termination or early
reader close is introduced. A genuine cleanup failure that settles without a native storage deadline
keeps its original identity; later cleanup errors remain observable through
cleanup. The deployment shell owns physical process exit.
WsHost.close() likewise bounds dialogue settlement and rejects indeterminate
storage without releasing the actual journal or writer ownership.
This is an additive diagnostic/lifecycle clarification in a minor release. Existing successful close, FULL durability, archive retention and no-replay guarantees remain. The error supplements existing close rejection rather than turning a timeout into successful disposal.
The maintainer ruling
selects ten minutes of actual audio and a separate 660,000 ms monotonic
backstop. Exported LocalCaptureChunk.startMs/endMs and Recording.durationMs,
savedThroughMs and interruptedAt now measure complete emitted WebM/Opus
packet audio. Unsupported codec/container forms, including audio/mp4, retain
conservative elapsed-wall timestamps for the rest of that recording. Old stored
wall timestamps remain readable without migration. No field, stop reason or
state is added. This semantic change ships in a minor before 1.0.
ComposerProps in @schlessera/brain-ui-kit gains optional
attachLabel?: string, the attach trigger's accessible name. Omitted or empty,
it is "Attach — photo, camera, files" (previously the fixed
"Attach — photo, camera, file"). An actionable trigger also paints that name as
an aria-hidden pointer tooltip; an inert trigger, including a dictating
composer's, has neither. Existing props and defaults are otherwise unchanged,
and no wire schema changes. api-report/ records the added prop.