Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Turn lifecycle and authority

Tento článek zatím není přeložen. Je dostupný v angličtině.

Developer reference

Turn lifecycle and authority

Non-triggering conversation input

BrainService.appendConversationMessage({ userId, session, id, text, display?, voiceCall? }) accepts external conversation input without calling prompt, steering, or creating a model turn. The owner voice-call engine uses id: 'voice-call:' + call.id + ':tail' for the dialogue after its last admitted request. The committed representation is a native user message with the existing voiceCall metadata; settled history, CLI cards, later voice context and the next main-agent request use their existing projections. The optional display separates visible copy from model-facing text through the existing providerText representation. The voice tail passes display: '': the card has no raw English prompt label, remains visible because it carries voiceCall, and native replay still supplies the complete model-facing text.

Acceptance uses the journal-owned brain_conversation_appends outbox, not another transcript. The owner/session lock and unique (session_id,id) key make the first accepted body win across concurrent calls and restarts. While a turn or shutdown checkpoint owns the conversation, the method resolves after this durable acceptance, without awaiting model settlement or reopening sealed persistence. Its flush joins the existing conversation lock queue, so a concurrent manual compaction cannot strand input. The pending body is deliberately absent from getSettledMessages: placing a user message between a native tool call and its result would invalidate provider history.

Conversational readers that must see accepted input immediately use store.transcript.getConversationMessages(sessionId, scope?: 'context' | 'history' | 'appends'), returning TranscriptConversationMessage[]: ordinary BrainTranscriptMessage fields plus source: 'journal' | 'append'. It projects settled branch messages followed by accepted pending appends in acceptance order through the existing display projection. The default scope is retained context; history uses the full active branch; appends reads only outstanding accepted bodies without loading the native branch. A single SQL statement reads both sides on one MVCC snapshot, including inside an existing READ COMMITTED transaction, so a concurrent flush cannot omit or double an accepted message. The voice engine uses this read for its next call's written context even while the main agent is still inside a tool. Native model restore, exports and settled-history readers remain journal-only: this conversational view is not a valid native tool-call history and must never restore PI. The caller retains ownership checks. Cost is linear in the selected settled branch plus outstanding accepted bodies, with no mutation or provider request; with no pending input it projects only the settled branch. Pending rows derive their display time from the native entry timestamp; promoted rows use the journal's ordinary insertion time.

BrainStatusService.streamSnapshot reads the appends scope through the same bound repositories and repeatable-read transaction as its paged settled history. Nonempty projected input appears separately as snapshot.conversationAppends?: BrainMessageView[]; the field is omitted when there are no pending views, not as a compatibility path. These synthetic views consume no history slots or cursor positions. Clients fold settled history, then live events, then conversationAppends, preserving the active turn and keeping subsequent assistant text/tool updates before the inert tail. They never replay these cards as user admissions or idle boundaries. Durable queue acceptance broadcasts resync: conversation-appended to a live conversation immediately, including while a tool is running; no model turn or spinner is started. Once the safe flush promotes the input, a refreshed snapshot finds it once in history and omits the pending field. Promotion between history and outbox reads cannot lose or duplicate the card because both belong to the same SQL snapshot. With no pending input this adds only the indexed outstanding-body lookup, not a full branch scan.

At an idle conversation boundary, after the current native operation and any failed-admission rollback, or after interrupted-tail repair during spawn, flushConversationAppends checks the protected retained branch. Provisional entries or unanswered native tool calls keep the intent pending. A safe flush chains accepted messages onto the current leaf in acceptance order through appendJournalEntries. The journal append, projections, leaf and outbox-body retirement commit atomically. Only an idempotency receipt remains outside the journal. No old entry, sequence or parent link is rewritten. Clear/delete remove the sidecar with the other conversation state, and rekey moves it with its session.

A later real user message cannot overtake accepted input by steering into the running turn. BrainTurnRunner checks transcript.hasPendingConversationAppends(sessionId) after asynchronous steerWire preparation and immediately before native steer admission. This body-free EXISTS uses the outbox's pending-session index. When pending input exists, the user message uses the existing pending-admission chip and serialized send/session locks instead of PI steering. This chip is display only: startSend.admitted remains pending until real native/durable admission, and cancelBeforeAdmission() still succeeds while the request waits. A voice producer consumes its delegated dialogue only after that actual admission; hangup cancels an unadmitted request and retains the whole request in the idempotent owner tail append. The chip disappears before the eventual user echo. The running turn completes its tools and answer normally. Its safe flush appends the accepted input before the fresh follow-up is admitted, so that follow-up's first native model context contains the input exactly once before the user's words, without rewriting the prior prefix. For example, a voice call ending during a long tool followed by a web message preserves the call before that message. With no pending input, ordinary steering stays unchanged apart from this single indexed read; no polling, native mid-tool flush or copied provider context is used.

Boot ordering preserves the native recovery checkpoint. After conversation activity reconciliation and delegation claims, the owner recovery claim first identifies parked/result owners. It drains cold idle append intents only outside that worklist and outside live sessions, native paused queues and computer prerequisites. This drain never spawns a runtime or sends a provider request. The same park/prerequisite check lives in flushConversationAppends, under its caller's existing conversation lock or cold restore boundary. A public append accepted while a cold owner waits offline stays in the outbox; absence of a live runtime is not evidence that recovery has finished.

For a parked conversation, factory restoration still repairs interrupted tool calls first, but does not promote inert input. The silent continuation therefore sees the original native tail: an already-answered turn returns nothing, while an outstanding response continues without replaying an uncertain mutation. Its persistence flush does not append a new user tail before the recovery owner checks completion. When paused input exists, the ordinary replay turn's existing pre-admission flush appends the accepted input before that real queued user entry, after all interrupted tool results. Replay retains its native queue receipt, continuation generation and completed turn's own activity fence. Only that real user pre-admission passes isUserTurn as the helper's fourth argument, beforeUserAdmission; restore and idle/finally drains never bypass the native park/prerequisite check. An append accepted during queued replay stays pending through its final answer until recovery's completion transaction commits, so that transaction still verifies the final assistant answer rather than an inert user tail.

Without a queued admission to drain first, the recovery owner appends only after its transaction has verified the generation, current computer claim and required activity fence, settled that claim/activity, and cleared the park. After COMMIT it takes the existing conversation locks, rechecks that the generation still matches and is no longer parked, then drains through the ordinary append seam. Stop, a new user admission or another computer wait can supersede that post-settlement drain; the new native owner retains the accepted input. Thus a completed park plus a voice tail alone spends no provider turn, and repeated reconnects cannot duplicate the tail or queued input. The outbox body remains available to the atomic conversational read until a safe drain, so deferral never hides accepted input. A cold Stop cancels the continuation and its prerequisite, then takes the same conversation locks to repair interrupted tool results and drain accepted appends before acknowledging. It creates no runtime or model turn; a stale boot worklist sees the cleared park and releases the stopped recovery.

A live flush refreshes the resident native manager and context, then broadcasts resync: conversation-appended; it never emits the ordinary user admission event, which would start a client spinner. Resident refresh also registers restored entry identities with the existing persistence capture barrier, so committed external input cannot consume the next real user's admission.

Without accepted intents, these boundaries perform only bounded session/prerequisite and outbox reads, with no history rebuild or provider request. Pending bodies exist only until safe delivery; receipts live until the conversation is cleared/deleted. Migration 0013_conversation_appends.sql creates this empty sidecar and pending index without rewriting existing history. Rollback needs a forward migration after pending bodies have been drained; dropping the table with pending input would lose accepted work.

Cancelling host preparation before admission

startSend(request) also returns cancelBeforeAdmission(): boolean for a producer ending while its input is still in host preparation, for example a voice call ending during remote memory recall. A successful cancellation immediately rejects admitted with an AbortError and fences the eventual native prompt/steer invocation. It does not wait for the outstanding preparation to finish. completed still owns its eventual rollback/cleanup and must remain observed.

The cancellation returns false once native admission begins or the outcome has settled. It never aborts accepted model work or reports an in-flight commit as cancelled. The voice producer cancels its pending preparations before awaiting their admission outcomes, then durably appends only unconsumed dialogue through the non-triggering seam. No HTTP input schema exposes this host-only handoff.

Initial execution selection

POST /brain/start accepts an optional target. ConversationLifecycle.start applies it only when the resolved conversation is new, through BrainService's same target authorization resolver as a Project switch. It passes the validated ref and directory to the existing ensureLive/spawner path before any startup instruction I/O; the factory persists the ref atomically with the conversation. Local startup therefore reads the selected client's AGENTS.md, and project-model lookup never probes its directory on the server. Explicit resumes and default starts that find an existing row preserve its target unless the stored local computer is offline, as described below. A new explicit managed target requests the existing environment operation after creation. CLI pairing and the browser's new-conversation picker consume this seam; later switches keep the existing boundary. No target keeps the normal account default and adds no extra selection or provider request.

ConversationLifecycle.start reserves default CLI directory selection on a per-user lock before its first asynchronous read, and releases it after the stable client's pending-start claim, before spawning. availableForDefaultStart excludes both attached streams and hasPendingStartClaim candidates, so two CLI launches in the same directory reserve different conversations before either SSE attaches. Failed starts cancel only their own claim; request generations retain the existing stop and supersession behavior.

A bound send carries its client generation. ConversationLifecycle.assertClientCurrent(userId, client, sessionId) is the synchronous admission assertion shared by BrainTurnRunner.send, send preflight and work-mode changes; an unbound request needs no generation check. It throws ClientSessionStoppedError with the diagnostic client session has stopped and stable wire code client_session_stopped. Send preflight, admission, work-mode commands and bound controls return HTTP 409 through sessionRouteError, log at info level and do not report a publisher problem. The browser's existing typedRefusalMessage maps the code to Czech, Slovak and English and preserves the unsent draft. The CLI's POST boundary explains that the terminal stopped and asks the user to reopen the conversation and send again; neither client automatically retries an unaccepted message. ClientAttachments.isCurrentGeneration owns the shared tombstone/generation comparison for request admission and parent SSE resolution. Admission requires the exact target; an SSE reconnect follows the current binding's session. With no binding, send reconstructs it and SSE leaves reconstruction to attach; a binding without a request generation cannot authorize a bound send and does not redirect SSE. BrainTurnRunner.send checks at each admission boundary and inside provider preflight until the provider accepts the turn's first request, so a stop or a newer start that lands while the context is prepared refuses the send before dispatch. After that acceptance the turn belongs to its conversation and later provider requests of the same turn do not repeat the check: SessionTeardownService.stopSession interrupts it with the client-closed cause when the stopping client was its last interactive watcher and keeps it running otherwise. A web detach-only beacon never interrupts accepted work, even with no remaining watcher.

Conversation selection and stream restoration own attachments and new-send admission only. Switching conversations, replacing a client generation and waking a phone preserve the previous conversation's turn, pending question or approval, and goal continuation. ConversationLifecycle.start and ensureLiveForStream never dispose an abandoned runtime: another turn may already have started while their opening work awaited. SessionTeardownService is the sole owner of attachment-driven runtime cleanup, using the shared sessionHasWorkInFlight predicate for detach-only stops and the idle reaper. An abandoned start or stream spawn follows that same 30-minute unwatched-and-idle policy. Active turns, questions, queued input, children, background jobs and active goals keep it live. This adds no timer or provider request; the existing maintenance sweep collects the runtime after work settles.

GoalLoopService schedules and dispatches each active goal's continuation against its own conversation, without consulting the selected conversation or attached viewer count. For example, a web user can start a goal in A and switch to B while A continues until its verdict or budget settles it. Explicit goal controls, Stop/teardown, continuation failures and boot reconciliation retain their existing authority. Once the goal settles and no other work remains, the unwatched runtime enters the normal idle reaper window; switching away neither pauses the goal nor starts that window while the goal is active.

BrainService.bindChannelContext archives the deterministic channel slot before re-keying the selected personal conversation into it. A slot without a durable session row is an archive no-op, including no process-handle or plan move. A present source still uses the journal's transactional re-key and moves its in-memory process ownership only after that commit.

ProjectToolDeps.current and request are asynchronous. The Project tool awaits both the current execution reference and the switch admission status before constructing any list or switch result. The spawner reads current execution from the transcript repository and submits switches through createProjectExecutionBoundary.request; resolved result details stay structured-cloneable for the journal. A rejected request becomes the existing switch refusal rather than a pending result.

Local execution lifetime

Owner opening, bound-stream restoration and fresh owner-turn admission invoke ConversationLifecycle.prepareExecution under the existing session lock, before local execution binds and before a model/vision respawn. A local park marker or checkpointed queue is handled first by the existing resumeParkedConversation terminalization: settle interrupted tools and activity as LOCAL_EXECUTION_LOST, discard the paused queue and clear the marker while the original target is still local. A boot sweep holding an earlier work item therefore cannot continue that work on the server. An existing offline or revoked local target then moves through BrainService.selectProjectExecution to the account's ordered default Project, or the administrator's host when none is usable. The existing cwd journal event records offlineTarget and the destination, even before the first message. History reads require only authorization and the durable journal. A busy owner conversation opens without retargeting; running children and background jobs also keep their current target until that work settles. Mid-turn connection loss and interrupted turn continuation retain the original binding and typed failure. A later reconnect never selects the computer again. See execution targets.

withLocalExecution wraps startup instruction reads and the complete owner/channel turn with SandboxControl.bindLocalTurn. The binding's signal aborts the existing PI session on connection loss, including a model-only turn, through abortSessionWork(session, 'connection-lost'); release follows settlement in finally. That cause records no interrupted event: the turn retains the connection's typed LOCAL_EXECUTION_LOST cause and logs computer unavailability locally rather than reporting it as brain.turn_failed. Accepted-send error publication distinguishes a proven native wait from an unproven failure as described below. Delegated admission pins before runner dispatch and aborts through the existing runner/channel tree. Connection identity is in memory only. A newly connected socket cannot continue work admitted on an older socket. Binding a turn for a computer that has no live connection fails before anything is admitted with LOCAL_TARGET_OFFLINE, which owner routes answer as HTTP 409 (see the HTTP API).

Owner user turns and durable Delegate/Workflow result deliveries both enter BrainTurnRunner.withTurnExecution, which owns the withLocalExecution binding, the live target rebind callback and disconnect abort. The prompt-free TurnContextBuilder.buildScope().run validates the resolved local directory through assertLocalWorkDir before appending a hidden result, just as ordinary prompt preparation does. An offline computer refuses this admission with LOCAL_TARGET_OFFLINE; an admitted connection that disappears reports LOCAL_EXECUTION_LOST. The existing result inbox keeps an undelivered row pending and applies its bounded retry lifecycle, including retry on a later user send after timed attempts are exhausted. It never selects server execution. A batched delivery without a model turn uses the same admission. This adds one local stat per hidden delivery and no provider request of its own.

withTurnExecution certifies a nonterminal computer wait only for local LOCAL_EXECUTION_LOST or LOCAL_TARGET_OFFLINE when parking is eligible and the native session park plus conversation prerequisite prove the captured continuation generation under the existing conversation locks. An attempted/refused park, failed write, superseded generation or removed Project cannot certify it. The internal ComputerContinuationWaitingError in sessionErrors.ts retains the original cause, code, message, status and unknown mutation outcome; it adds no wire or plugin contract. BrainService.publishAcceptedSendFailure consumes this captured outcome without rereading the current park, so delayed publication after resume or Stop cannot append a terminal CHAT_EVENT or emit a terminal SSE error for the earlier wait. Raw local errors and genuine terminal failures retain publication even when another park exists. The accepted operation still rejects and API token/usage settlement still occurs once; this is not a successful send or permission to replay an uncertain mutation. HTTP accepted-send, queued-interrupt and paused-queue replay catches log this proven wait as local-only; real failures retain their existing reporting codes. Native continuation tail rules, Stop, access, connection pins and recovery claim admission remain unchanged.

An admitted owner or channel turn exposes its existing lifetime's rebind callback only while it runs. commitProjectExecution invokes it after target validation and before storing the selection at the existing settled-tool-batch boundary. This also arms turns that started on host or managed execution. Moving targets removes the previous abort subscription and releases that turn pin, then binds the newly selected local connection; leaving local execution removes it. Loss of the current connection still aborts model-only work. Prepared Terminal processes retain their captured connection independently. No selection is replayed and no second project-selection path is introduced.

Runner bindings observe the daemon-owned dispatch lifetime through Sandbox's localExecutionTurn context; the daemon's pins live in core's localExecution host control (paired computers). Startup instruction reads and channel turns retain the same helper and abort behavior; releasing an observer detaches its abort listener, and delegated-call settlement releases the daemon's actual socket pin. Target, dispatch account, current policy account and active session or admitting parent checks still precede binding. No runner selects another socket.

Channel startup enters the existing policy context after delegated-scope and parent validation, before cold session creation, local cwd resolution and cold-history passes. Its contribution account is resolved through contributionOwnerForSession; the same inherited contribution value is passed to the spawner. Sandbox therefore reads the admitted account through currentAccountUserId() even when a delegated child has no live parent or account identity. The full prompt scope still supplies permissions, mode and live target resolution. Unlinked senders receive no contribution account, and startup adds no provider call or separate identity context.

Local target resolution supplies cwd without host filesystem checks. Host cwd stamping, project-model lookup and path-based recall do not inspect the client directory. BrainTurnRunner.send and ConversationLifecycle.start pass a request's client cwd to the spawn, the vision and wire hops and the per-turn stamp only through ConversationLifecycle.clientCwdFor, which drops it for a local conversation unless the request's personal API token is that computer's device token (paired computers). A turn's resolver re-reads the target and its base directory on every call, so a mid-turn Project switch never resolves against the previous machine's directory. Boot uses existing terminal recovery and settlePartialTurn for interrupted local work, preserving verify-before-repeat tool results without automatic model continuation. This lifetime adds one binding per admitted turn and no provider round trip.

Dreaming receives every batch record in full in its directive. A successful child marks all offered ids through the existing unchanged-before-run cutoff after completion authority checks; failed or aborted children add no completion marks. Dream memory-tool reads still omit recall delivery and usage (X13). The reviewed contract is owned by memory dreaming, without a read-proof field in tool results or nested traces.

File tools follow the file and directory parameter convention. ShareFile and ShareImage take file_path; WorkflowStart takes file_path for its JSON definition. Shared image and file references, nested trace metadata, attachment ownership, fork seeds and immutable journal arguments keep their existing contracts. Live and reloaded tool labels use the existing common subject-key reader.

Checkpoint compaction keeps each failed provider request in the local log. inSessionCompaction.ts reports compaction.checkpoint_failed once only when both the bounded native request retries and the warm summary fallback have failed. A successful retry or fallback produces no problem report. Validation failures still terminate through the existing checkpoint failure boundary; encrypted history and provider error text never enter the report.

Native compaction reporting preserves the request's typed cause before PI settlement. The existing model-stream wrapper associates synthetic terminal messages with their original thrown value; CompactionModelRoute observes that cause and clears it at every session_before_compact. The session factory returns compactionFailure to the settlement reducer, so compaction.failed retains local provider refusals and reportable persistence defects despite PI exposing only errorMessage. Channel settlement uses the same modelStreamFailure cause when throwing a failed delegated turn, with the persistence latch taking precedence. SummaryFailure retains an internal terminal cause instead of replacing it with a model refusal. Diagnostic text and PI request execution are unchanged. See Problem report audiences.

Computer prerequisite wake composition

ComputerPrerequisiteCoordinator in src/brain/recovery/computerPrerequisites.ts selects durable native obligations; it is not another recovery queue or transport retry scheduler. Daemon BrainCore connects the authenticated executor attach identity {projectId,targetId,authorityId,generation} to wakeComputer(identity) without holding attachment open for a model turn. The coordinator uses indexed journal.computerPrerequisites(projectId) and exclusive native claim CAS, then routes the typed conversation/call/node/plan reference to its existing origin owner. It copies no transcript, task, result or paused-input queue.

After async claim/assertion boundaries it checks the current connected socket's Project, target, registry authority and numeric generation. A wake carries the exact native claim plus isCurrentConnection(), never a permission grant or execution pin. Each origin owner must reauthorize after its own awaits, reassert the durable token during actual native admission and obtain a fresh execution binding before external dispatch. Returning from that owner does not establish final settlement: native result, envelope or retained-input obligations may remain. Only the existing owner settles/reparks with its durable completion receipts.

A socket lost before dispatch reparks the claim. After that commit, a fresh wakeProject(projectId) observes any replacement connection so attach-before-wait commit cannot lose a wake. Native park/repark callers use the same post-commit wake. Conversation callbacks repeat this fence at their initial session and claim checks: only a successful exact-claim repark re-signals the captured Project, covering an attach scan that already skipped claimed; Stop/new-generation refusals schedule no wake. Later origin-owner handoff and settlement stay unchanged. sweep() belongs to the existing daemon boot recovery/reconstruction order: it revalidates foreign claims and wakes current observations, not BrainCore construction or runner boot. admitsNewWork retains draining refusal. Errors reach the caller's existing logs; no timer, polling loop or second budget is introduced.

Plan wake submission returns void and does not join continuation while its caller may still hold the existing plan kernel lock. Plan execution retains that lock, its native predecessor/location order and both original endpoint checks. The composition installs the existing plan-wake callback; with no daemon coordinator (runner composition), it installs none. See native storage/admission and paired socket observations.

Focused coordinator and real BrainCore/ephemeral-DB component tests exercise this composition, source disposal and held-lock nonblocking submission. They do not prove assembled origin recovery, runner policy IPC or physical-device acceptance; origin-owner adoption and final bootstrap/recovery verification remain separate. No new client admission contract, updater handoff or production activation is implied by this composition.

Owning code

  • src/brain/service/: the BrainService facade (src/brain/brainService.ts, class BrainService) delegates to ConversationLifecycle (lifecycle.ts), LiveSessionSpawner (spawner.ts), BrainTurnRunner (turnRunner.ts), TurnContextBuilder (turnContextBuilder.ts), BrainResultDelivery (resultDelivery.ts), BrainReadiness (brainReadiness.ts), the delegated-run owners (delegatedSession.ts, delegatedRecovery.ts, delegatedSettlement.ts), ChannelSessionService (src/brain/channels.ts) and BrainStatusService (statusService.ts).
  • src/brain/session/: the per-session PI wiring: factory.ts, capabilities.ts (composeSessionTools, gateSessionTool), turnPrompt.ts, projectExecutionBoundary.ts and the conversation event writer chatEvents.ts (see the events page).
  • src/brain/prompt/: stable persona and system-suffix bytes (sessionPersona.ts).
  • packages/db/src/repositories/{journal,transcript,delegation}.ts: the asynchronous JournalRepository, TranscriptRepository and DelegationRepository contracts exported by elowen-db; their PostgreSQL implementations live in the matching packages/db/src/ directories.

The schema is declared by packages/db/migrations/0001_baseline.sql and the later numbered files in packages/db/migrations/.

Post-turn skill review source and isolation

Personal skill review uses the shared delegated fork and PromptComposerRuntime with host-owned purpose skill-review. The spawner supplies SessionSpec.skillReviewCaptureEnabled as an async live eligibility check. The session capture wrapper awaits it for each provider payload before retaining the request; a false result clears any previous source, and an omitted callback leaves capture uninstalled. This is an internal authority, not a model-visible Delegate option. Core captures the settled session/turn/leaf, last actual provider request and immutable provider state before later parent activity can replace them. The journal copy is anchored at the captured settled leaf, even if a new user turn has already changed the parent's active branch. A later read of the parent's active branch is not a valid source.

The request contract preserves the original provider/model, endpoint/API, connection/generation, provider cache key, ordered system/tool declarations, message/tool IDs and cache metadata. The only appended input is the complete final parent output when it was absent from the source input, then one JSON review directive. No generic worker briefing, new turn context, recall, clock/workdir frame or tool-catalog rewrite is allowed. Divergent or unavailable source skips review before a request. The shared source-shape gate supports Anthropic Messages, Codex Responses, OpenAI Responses and OpenAI Completions without a separate measured-route allowlist. It retains all non-history fields from the actual parent request and rejects divergent reconstructed history.

SessionSpec.skillReviewQuota is asynchronous. The session factory awaits its live source-account verdict before each account verification and dispatch; a skip reason prevents the request, while a rejected quota read remains a request failure.

The child has separate local transport identity and mutable provider state. Codex keeps the original provider cache identity and immutable affinity snapshot without sharing a socket identity or writing child affinity into the parent/account jar. The next user turn retains its normal turn-state reset. Anthropic retains the original cache controls and TTLs without updating parent candidate/confirmed positions or adding a fifth marker. Responses children never mutate the parent's previous/pending chain; a source delta using previous_response_id remains unsupported because it does not contain the full inherited prefix. Codex's Responses-shaped payload is not evidence for that chain.

Tool declarations stay present for byte parity, but every execution path is denied before permissions and side effects. A tool attempt aborts the review without another paid model round. Provider-hosted tools that bypass the host execution gate skip. Review is detached after the interactive lock is released, bounded by the settlement abort signal, runner-only and opportunistic. A suspended review must allow admission of the next user turn.

No review directive/result, tool activation, recall marker, compaction state, activity, visible event, title, notification, parent result wake-up or recursive review is written into the parent. Child failures and aborts cannot change the parent's active leaf, provider route, affinity or response chain. Interrupted review is not resumed from a cold prefix after restart. Actual inference charges the author exactly once under origin internal on its captured connection. The learning module owns eligibility, quota, plugin writes and rollout gates; delegation runtime owns runner admission.

Workflow admission awaits ctx.delegatedTurnsOutOfProcess() before choosing the runner dispatch path. WorkflowAddNodes awaits local expansion through addNodesFromSession before inspecting its receipt; node model pins are captured at admission and remain authoritative when the live user-config scope ends. A delegated session's returned promise alone does not establish admission. Progress may be published from the host's session/progress events or after a successful awaited receipt.

Native recovery admission routing

Daemon BrainService.runDelegatedTurn reads fresh-dispatch proof only from the existing host turn ALS. It matches both the actual parent session and the child session derived from the requested channel before forwarding the SAME recoveryAdmission and recoveryConnectionCurrent through ChannelSendOpts. Wrong-parent/child and unrelated nested dispatch receive no proof. These options are daemon-local, not request payload, IPC authority or plugin wire fields.

ChannelSessionService checks the current connection around async rebind and native admission. Its transaction calls the existing run, workflow or conversation begin*RecoveryAttempt; later helper turns only assert the retained claim. A single admission is shared by cold compaction and the resumed request, rather than charging every provider preflight. Origin owners still reauthorize and own their real continuation/publication implementations; routing proof alone does not implement missing-child dispatch or workflow result publication.

BrainService.assertWorkflowNodeDispatch(dispatch, childSessionId) and the callback supplied to the existing PlatformOrchestrator forward the SAME native guard promise unchanged, including rejection. The delegated-session owner alone validates planned child/parent, generation, claim and current connection; this composition adds no validator, claim, map, wire field or dispatch budget. Bounded accepted/refused callback tests use real host ALS with a native-owner stub, not proof of native authorization or protected placement integration.

BrainService.continueWorkflowNodeFromTurn(parentSessionId, input, access) forwards exactly those THREE arguments and the canonical WorkflowNodeContinueInput/WorkflowNodeContinueResult, without the former positional child/text overload. Access types derive from the corresponding canonical bridge method, including advanceWorkflowGeneration's own method. The native continuation implementation and protected dispatch branch remain with their existing delegated-session/platform owners; thin forwarding does not establish their assembled runtime behavior.

The real BrainSessionFactory receives BrainService's required retryPolicy unchanged, including completion-triggered turns. Existing unified delegationCompletionEmitter, resultSteerBoundary and transactional parent journal ACK remain the completion owners; no custom result receipt or duplicate completion ledger is added. See conversation and queued-input recovery.

Native delegated computer waits

DelegatedSessionService.resumeComputerDelegatedCall(wake) is the targeted native consumer of the computer prerequisite coordinator. It rereads the original parent/tool-call/child run, exact native row generation and journal claim, then reuses recoverClaimedRun; there is no alternate runner or recovered-task identity. An already-final child and nested helper settlement are evaluated before computer availability or a continuation cap. Offline is a durable wait, not a final result.

For a continuation on the captured local Project, existing journal parkComputerPrerequisite/reparkComputerPrerequisite retain that native call/node generation and submit computerPrerequisites.wakeProject(projectId). Missing required registry/coordinator fails explicitly. Failed wake submission is logged and leaves the native wait intact. There is no computer wait timer, host fallback, name parser or new execution target. Exact RunRecoveryAdmission or WorkflowRecoveryAdmission, optional prerequisite claim and the daemon-owned current-connection predicate are forwarded unchanged to the existing channel send seam. Provider/facade composition remains a separate owner integration gate. Boot ownership does not consume/reset the finite model budget; actual dispatched recovery admission does. Helper/job-result turns within the admitted call reuse the same claim instead of charging twice.

A transcript-final child may still need its first actual model turn to consume a helper result. Common sendDelegated forwards the original recovery admission for that turn, charges once at provider admission and subsequently forwards only its prerequisite claim. Channel send invokes onRecoveryAdmitted synchronously after the successful native admission CAS, before awaiting the provider response; sendDelegated uses it to set the per-call transient admissionForwarded routing flag immediately. A helper result arriving during that resumed turn can then enter the existing steering queue without replaying the spent admission. A refused admission leaves the flag unchanged. The flag owns neither identity nor budget and is removed with the call; absent a callback, channel admission is unchanged.

continueWorkflowNode(admission, childSessionId, onEvent?, fallbackPrompt?, recoveryConnectionCurrent?, startFresh?) returns answered/continued with reply, empty, or { outcome: 'waiting', prerequisite: ComputerPrerequisite }. Waiting contains the actual committed native row after initial park, exact-claim repark or a helper that already parked this same workflow claim. The canonical journal.computerPrerequisiteForSession read verifies native generation and the explicit parent/workflow/tool-call/node relation. A missing/superseded row cannot be substituted for this node's wait. Existing lost-park CAS refusal still throws SessionWorkAbortedError; no fabricated prerequisite or duplicate waiting identity is introduced. Completed output/helper settlement stays before offline checks. The host-captured wake.isCurrentConnection uses the existing fifth argument, threaded by the targeted workflow callback; a durable claim alone is not socket proof. Workflow identity is explicit, never parsed from a channel name.

The native hidden result sender is DelegatedSessionService.sendDelegatedCustom; the host facade must forward to it rather than keep another sender implementation. It preserves the unified delegationCompletionEmitter() and atomic journal/inbox acknowledgement: settledResultIds is the canonical committed receipt. This sender adds no custom-kind receipt fallback or result-specific delivery ledger. The existing drained.failed outcome remains authoritative for a failed delivery turn even when its inbox acknowledgement already committed. Typed local offline/loss returns CustomDelivery = 'waiting-for-computer' only after parking the same native parent/call or workflow-node generation. Existing valid pre-dispatch waits are read before ordinary hidden delivery and submit the same coordinator wake without another model dispatch. Recovery settlement reuses its held claim. Result IDs remain ordinary inbox receipts, not prerequisite identities. DelegatedSettlement treats PendingResultDrainOutcome.waitingForComputer as typed suspension through DelegatedComputerWaitingError: no redrain/spin, premature child settlement, rescue continuation or separate timer. Job notification forwarding preserves typed local loss/offline errors.

The existing DelegatedChildBridge.advanceWorkflowGeneration(parentSessionId, { workflowId, toolCallId }, currentAccess) reaches the same delegated service via the narrow host facade/ALS bridge. It validates the durable parent/workflow/call relation and unfinished child scopes with scopeExceedsCurrentAccess, then passes that exact native reservation input without an invented expected generation. The trusted workflow engine retains original-principal authority; child-added nodes keep their own spawnedBy. Native storage owns atomic terminal-to-running reservation and duplicate refusal. Boot recovery does not use this explicit user-resume generation change.

Real consumers are the targeted computer coordinator/facade, workflow continuation, helper-result drain and trusted delegated bridge. No parked wait means no wake, model request or added timer; reads use the original indexed native identity. Focused native component regressions cover park/repark, completed answers, helper admission and real generation reservation, not complete provider/facade composition. Same-boot lifecycle = running recovery carries the same exact held prerequisite claim through updateClaimedRunProgress, completeRecoveredRun and markRecoveryRequired. These native writes validate it atomically after awaited model/settlement work; no fake boot ownership transition is needed. Replaced claims cannot write a stale final answer through the ordinary boot-only recordRecoveredResult path. Without a prerequisite claim, legitimate boot recovery retains its existing recovering-only ownership and stolen-boot handling.

For an uncreated planned child, the sixth daemon-local startFresh callback reuses the existing daemon placement, not a runner-start fallback. Before guest preparation the service rereads the native parent/workflow/tool-call/node, generation/admission version and exact planned child relation. A captured local Project additionally requires its exact claim and current socket predicate; otherwise the same native row is parked/reparked before invoking the callback. The Project-indexed native prerequisite reader supplies the actual waiting row while no child exists; no synthetic child, claim or empty-success fallback is used.

The host-only assertWorkflowNodeDispatch(dispatch, childSessionId): Promise<void> exposes that same native preparation fence to the existing platform orchestrator. It reuses plannedWorkflowParent and the continuation's shared child-absence, claim and socket checks; it does not introduce a second query authority. After awaited native/connection proof it checks the same captured socket predicate. A stale generation, claim, existing child or socket throws SessionWorkAbortedError; policy errors propagate unchanged. The assertion neither parks nor charges. The continuation still turns a local stale-socket refusal into its original durable wait, while final provider CAS owns admission. The platform must require the assertion for an exact protected parent/child match before cwd/dependency/target preparation and recheck that same predicate after later awaits. Ordinary and nonmatching placement is unchanged; this host seam adds no wire field, dispatcher or runner-failure fallback.

The callback runs inside the existing runWithWorkflowNodeDispatch ALS with that same admission/predicate and preserved turn token. Preparation costs no attempt; the existing channel provider admission owns the only native CAS charge. Creating the real child preserves the same workflow claim. Nested result turns reuse that claim through the existing call context. Only the canonical shared isLocalComputerUnavailable follows native Error.cause wrappers for offline/loss; policy, missing-directory and unsupported failures remain errors.

The live bridge uses continueWorkflowNodeFromTurn(parentSessionId, input, access) with canonical WorkflowNodeContinueInput/WorkflowNodeContinueResult; no status: unavailable compatibility shape remains. Native parent/node/planned-child and generation/admission-version proof applies before either existing or missing children. Existing child scopes, or a delegated parent's scope before child creation, must fit the current ceiling verbatim and retain their durable spawner. A read-only caller cannot turn a writable child into a new model request. The original local Project must remain executable by its account.

For an ordinary live missing child, current Project placement/access and local readiness are preflight only: online returns empty, without creating a child, claim or provider admission, so the workflow engine invokes its original fresh callback once. Offline returns the actual committed native wait. The native epoch is checked again after awaited readiness, so Stop cannot turn into fresh work. This live path neither invents nor relaxes the daemon automatic callback's held claim/socket proof or the host-only platform assertion.

After dependency preparation or the original node operation actually reports typed LOCAL_TARGET_OFFLINE/LOCAL_EXECUTION_LOST, the engine may submit computerUnavailable: true on that same canonical input. This data-only intent never grants authority: native identity/epoch, scope and Project access are reproved before waitForWorkflowComputer parks/reparks the same node and returns its real row. This arm never dispatches, even if the socket already reconnected; it also covers a child created before the operation failed. No new wait map, admission charge, operation replay or fabricated waiting result is introduced.

The live input has no startFresh callback or host authority fields, and none are serialized over runner RPC. The brain facade and existing platform dispatch branch own real dispatch propagation; the plugin host owns the live bridge adapter.

Focused native tests prove pre-preparation waiting, same-claim child creation, callback ALS identity, native admission and wrapped preparation loss. Complete provider/facade, helper-delivery and fresh/live origin assembly remain separate gates. The native generation reservation and filesystem Resume checkpoint are not one atomic transaction; the workflow engine must reconcile that separate crash window before complete explicit-Resume acceptance is claimed.

Delegated fork publication and closure

BrainSessionFactory.create commits new-child admission with fork_copy_pending, awaits seedForkBranch outside its writer callback, then prepares archive restoration and interrupted-turn settlement before rehydrating PI. Fork copying pins the captured parent's read snapshot and writes bounded 128-entry / 256-KiB batches with real loop yields and asynchronous writer admission. Before invoking copyJournalBranch, seedForkBranch prepares a hidden Build mode entry through chatEventMessage and returns its JSON alongside journalContextSeeds from the ancestry callback. A final transaction publishes the leaf, own-sequence boundary, Build mode and public visibility. The journal assigns the prepared entry's parent to the active leaf or inserts it before the pending tail, matching recordColdChatEvent; the prepared parentId: null does not choose its final position. No caller can rehydrate or display a partially copied branch. Only daemon boot discards interrupted private children through the existing whole-session drain before delegation recovery claims them.

The shared TranscriptRepository.assertForkContextAvailable admission refuses completed host-owned dream contexts, using finishedDreamContextSql, and every context already marked closed by retention. DelegateContinue awaits that check before raising a progress row; session preparation and fork sources check through the same predicate. Regular done/error delegates and offline archives remain continuable. The existing checkpoint sweep can reclaim an old, idle terminal dream's inherited prefix after all parent results are settled. It preserves the child's own messages, final answer and DelegateRead; only own roots are detached from retired ancestry. See persistence budgets and recovery.

Journal commit ordering

persistence.ts::createSessionPersistenceProjector captures PI's new native entries synchronously through journalWriter.ts::captureJournalEntries, then serializes their database writes on its per-session queue. Each Database.transaction filters IDs already stored and derives delivery receipts from the captured native entries without reparsing SQL rows. Image/audio hashing, content-addressed file publication, admission consumption and native context record construction happen first. Media is externalized exactly once during capture; the SQL append only stamps assistant messages with the configured provider identity and keeps the captured media references. The prepared entries, final active leaf and delegated-delivery receipts then commit together through the existing journal repository append boundary. PI IDs, parent links, append order, provisional flags and pure branch moves retain the same diff semantics.

Preparation is synchronous and finishes before acquiring a PostgreSQL transaction. A failed SQL commit leaves the journal unchanged and may leave an unreferenced content-addressed file; the existing daily attachment sweep reclaims it after its one-hour grace. The store still serializes and inserts the turn's durable SQL bodies atomically, so a large text tool result continues to cost SQL time proportional to its bytes.

A terminal, non-aborted overflow compaction_end after an errored attempt latches an unbooked recovery failure in the same projector. A queued continuation cannot erase it with its own agent_end: the next agent_settled sends one failureOnly settlement through onTurnSettled, then clears the latch. Successful compact-and-retry and cancelled recovery add no failure. Without the optional accounting callback the journal still settles normally.

The projector's flush() waits for its queue watermark and refreshes a compacted resident manager only after that watermark commits and no newer captured append is waiting. The retained journal remains the source of the rebuilt tree. rehydrate installs a lazy message property: the first message reader restores referenced media and the runtime provider identity, then retains that runtime object. Compaction refresh therefore does not read and base64-encode every retained image. Rehydration opens referenced image files only to check availability and file type, without reading their bytes. It awaits durable retirement for missing images before installing the lazy message, so readers receive the canonical replacement after the repair commits. Interrupted-turn plans settle pending entries only when their captured branch contains provisional rows; a never-spoken conversation returns its empty tail without submitting a journal write.

slowSync labels identify media externalization/content-addressed writes, retained-entry parsing, runtime media restoration, recovery serialization and export parsing. The synthetic journal benchmark also compares text-only and media-bearing commits with the same retained context and tool-result size.

Transcript exports

BrainService.exportSession awaits BrainStatusService.sessionForRead before exporting an explicit conversation, including retained children and administrator oversight. It passes the authorized session identity and BrainDeps.workerDatabase inputs (databaseUrl, instanceRoot, poolSize) through exportSessionInWorker and the existing forkWorker, without a computation deadline or a background priority drop. These inputs travel over typed IPC. The request signal cancels a disconnected caller through the existing TERM/KILL grace; the caller owns async cleanup even when the child fails. exportSession.ts owns the single HTML renderer and PI-compatible JSONL projection. HTML shapes the display transcript directly and does not rehydrate or write an unused JSONL file. JSONL restores image bytes or the existing missing-image placeholder without running live replay's durable retirement repair.

The typed export request extends backgroundWorkerProtocol.ts. session/exportWorker.ts opens its own PostgreSQL pool through createBackgroundWorkerDatabase, with role worker and non-authoritative configuration inputs. It rechecks the caller, explicit administrator scope and the authorized session's owner/creation/spill identity inside the same read-only REPEATABLE READ transaction used for transcript and delegated-run reads. Deletion before the snapshot returns the same unknown-session error; deletion after the snapshot does not tear its output. The worker closes its pool before writing the requested artifact into the core-created private output directory. The IPC reply carries filename, content type and UTF-8 byte length, never the transcript body or an arbitrary output path.

The download route shares nodeResponseStream and requestScopedStream in src/api/routes/pluginResponse.ts with plugin HTTP responses. It reads the private artifact with backpressure, keeping the existing filename, content type and HTTP content length. SessionExport.cleanup() returns a promise, and the stream awaits it once after EOF, source failure, cancellation or a request disconnect, including a disconnect while the renderer is still running. HEAD cancels the artifact without a body. Cleanup and stream failures are reported rather than silently treated as successful downloads.

Conversation listing reads

BrainService.listSessions, listContextSessions, listConversationRefs, listManagedSessions and listManagedSessionIds are asynchronous. HTTP and platform callers await one complete read-dispatch snapshot; the existing wire arrays, page windows, account/admin scope and row ordering are preserved. LiveSessionRegistry.childEntries() supplies the live delegation edges, including cold parents, before the worker selects progress. The status service merges current live/session/attachment/elicitation state after that read without per-row database queries. Workflow-engine liveness and this boot's pending resume claims retain their precedence.

Status reads call await BrainDeps.snapshotReads() before opening their read-only REPEATABLE READ transaction. The daemon captures OAuth credential-key presence in that preparation phase, using the same readOAuthCredentialKeys snapshot as account-route preparation. It returns a binder that receives the transaction's repositories: config projects config.get() and config.brainProviders() through projectBrainConfig with the captured keys, while policy uses resolvePolicy with the bound project repositories. Neither callback reads credential files or uses the root pool. The facade binds durable elicitation reads to those same repositories. Preparation failure opens no transaction and returns no status; SQL failure publishes no mixed snapshot.

Destructive clear and rewind prepare their journal mutation on the same bound SERIALIZABLE transaction that applies it. Core shapes the active reply boundary, tool-call identities, released workflow artifacts and the hidden mode-restoration entry; the journal repository verifies the raw branch and authority fence before writing. Clear captures spill ownership before retiring history and cleans host files only after commit. Both paths await neutral activity reset and its publication before acknowledging. Checkpoint conversion runs through the source session's existing providerPreflight.run, including a fresh recovery-inbox check before each request; it retains the source account verification and the existing persistence guard. Model permission checks finish before checkpoint preparation or compaction-route admission. delegationRoot awaits each transcript ancestor, and Project awaits the current selection and switch request. Dream calls await live account authority before tool execution. Fast-mode route lookup, step-context cadence and request-capture settings are asynchronous; deferModelStream preserves PI's synchronous stream contract while those reads finish. Conversation activity receives the bound journal as its write store and a separate transcript.getSessionActivity reader for neutral resets; the full repository bundle is not an activity store. Open, move, reset and settlement await both the durable transition and activity publication. The facade passes the explicit workerDatabase.instanceRoot to the factory and spawner tool gates; settlement also awaits the surface's completion-notification callback. Authorized dispatch catches the database package's exported ProviderRoutingConflict, so a configuration change during checkpoint preparation re-evaluates admission without repeating the already committed portable cut.

The dashboard awaits the same reference listing, then a single digestMaterial read for its bounded selected conversations. The provider-visible material keeps its original bytes and order. The activity route refreshes a last-good tool snapshot through the same read worker while requests continue to use the old snapshot. Persistence and backfill details live in store persistence.

Personal-token turn admission

BrainDeps.apiTokens supplies the existing ApiTokenStore to the owner turn runner. HTTP authenticates a token once and passes its trusted id and request clock in TurnRequest.apiToken; request bodies cannot supply these values. Both POST /brain/send and POST /v1/agent/runs use service/apiTokenTurn.ts, which owns reservation, conversation binding, admission and exactly one settlement. API runs create that shared reservation before asynchronous conversation/model preparation and hand it to the runner; preparation failure, failed stream attachment and a facade preflight refusal release it without counting a turn. The completed session instance supplies the outcome even when cancellation has already removed it from the live-session registry. Ordinary chat reserves after resolving its current conversation and before mutating turn state. An unwired token store refuses a token-bearing turn; browser sessions reserve nothing.

Idle owner sends reserve the existing per-conversation send lock at runner entry, before asynchronous facade preflight, repository reads or publication. This preserves arrival order for concurrent web sends and bound CLI sends; authorization still runs before turn mutation. Cold default routing serializes conversation-id resolution before joining that same lock. Streaming steering bypasses the idle queue unless a mode switch is pending. Recovery notices inside the reserved turn use the locked restoration path rather than reacquiring its send lock. No additional admission queue is maintained.

The runner binds the reservation under its existing send lock before provider preparation, marks it admitted at PI acceptance and finishes it before releasing that lock. The optional onApiTokenUsage callback supplies the agent-run response after durable settlement. A 202, HTTP disconnect or steering completion cannot release the initiating turn's slot. Steering still checks token cost and concurrency ceilings, excluding only the same token's already counted run in that conversation; it releases its temporary reservation and leaves all spend on the initiating run. A failed or cancelled provider run counts as an error, including when PI resolves its prompt with an error or abort instead of throwing. A pre-admission failure releases its reservation. Failed settlement propagates and leaves the durable row for closeInterruptedRuns at boot, which marks interrupted usage unverifiable for cost limits. There is no settlement retry or second accounting store. src/api/auth.ts remains the sole per-request RPM counter.

POST /brain/interrupt-queued passes the same trusted HTTP token id, request clock, origin and surface through BrainService.interruptQueued to the promoted turn. That seam takes one request object, not positional identity arguments. After PI abort returns, it waits for the original send lock to settle before calling startSend: the original reservation must finish before a replacement can reserve a slot when maxConcurrentTurns is one. The parent-abort fence stays closed throughout. Promotion and re-steering use the existing ApiTokenTurn owner; no token identity is persisted in queue records. A cost refusal uses the standard HTTP limit response and restores the unconsumed backlog without admitting a prompt or user row.

Data flow of a turn

Plugin text enters through registerText(placement, value, options) and one owner-attributed PluginRegistry.textContributions list. textFor(placement) projects the existing consumers in registration order. Global system-prompt entries pass through sessionPromptFragments, which filters the Skills owner when SkillLoad is hidden; loader-owned platform entries stay separate at composition and lead promptAppend only for their registered platform. Turn providers retain before-user or after-user position, input transforms retain their capability check, and step providers retain stable <plugin>/<ordinal> journal keys. The registration change adds no render or journal write and does not change frozen context bytes. Migrated consumers declare requiresSharedApi at the shared API level they use and requiresCore as the first core artifact that provides it.

The exact surface differs for owner chat, a platform adapter, automation, or a delegated child, but they converge on the same brain execution model:

  1. An API, CLI, web proxy, platform adapter, scheduler, or parent delegation submits a turn request.
  2. The relevant service authenticates or receives verified identity metadata, resolves the account contribution, Project policy, conversation, model, and execution reference, and serializes admission for that session.
  3. ConversationLifecycle resolves or creates the durable session and LiveSessionSpawner creates or rehydrates the in-memory PI session.
  4. The spawner authorizes the account the conversation runs on through the one dispatch seam, resolves the working directory and context-file policy (the root file plus the unconditional instruction modules, and the on-demand modules documented in Project instruction modules), then calls prompt/sessionPersona.ts for stable persona and suffix bytes, service/codeModeComposition.ts for the nested plugin composition and visibility, and session/liveRecallWiring.ts for caller-owned recall options. composeSessionTools() remains the single tool visibility and execute-time wrapper boundary.
  5. TurnContextBuilder resolves fresh memory, plugin context, permissions, mode directives, working-directory reminders, session notices, and running-child reminders. The resulting wire frames are placed around the clean user text.
  6. The PI agent loop sends the provider request, receives assistant events, and invokes tools. Tool calls run inside the current identity, Project, execution, and permission scope. Code-mode nested calls use the same wrappers.
  7. The daemon streams bounded events to attached clients and commits native user, assistant and tool-result journal entries to PostgreSQL as they settle.
  8. Turn settlement records usage, activity, origin attribution, cards, artifacts, memory-curation work, and delegated-result delivery. Background results are delivered to their durable parent path.
  9. The live session remains reusable or is later evicted and rebuilt from durable state.

BrainTurnRunner's existing internal onTurnCompleted callback receives TurnCompletion.activity, a Pick<ConversationActivitySnapshot, 'turnId' | 'seq'>. For requests with that callback, the runner captures the admitted activity under its send/admission lock immediately after opened.begin() and before the turn runs; missing activity is an invariant failure. It does not read whatever activity is current after unlocking. BrainPauseRecovery.replayPausedQueue returns the last completed replay's captured fence through this callback, including when a later queued send fails; empty or uncompleted replay supplies no replacement. The parked recovery transaction checks that receipt against current activity before settlement. Automation retains its existing explicit outcome checks and terminal idle receipt; its public result does not change.

The account a conversation runs on

LiveSessionSpawner authorizes every conversation route through brain/authorizedDispatch.ts. Selection states the logical provider and exact model; the dispatch owner selects and admits its account. session/routedRuntime.ts re-verifies the admitted connection and generation before every real request, using a pinned ordinary attempt. Pause, revoked grants, removal or a new login refuse the next request. A distinct compaction model has its own authorized account and descriptor through the same wrapper stack.

session/providerRequestRecorder.ts captures the producing connection at request creation, stamps it on that request's terminal assistant message before PI persistence, and passes it to attempt capture. brain/settledUsage.ts groups each message by this request-time identity. A later move from A to B cannot bill B's answer to A or rewrite A's earlier tool-step usage. Chat and summary settlement use the existing journal and usage_by_provider_day counter, with no additional accounting store.

The recorder also classifies a failure the provider dialect did not: a call thrown below it, or an error event from PI's lazy request setup (auth resolution before the payload, which always says error), becomes aborted when the request signal is aborted. A Stop that lands between steps, for example while a question is pending, is therefore recorded as an interruption, not reported as provider.request_failed.

Before requesting checkpoint conversion, spawn admission compares the already-authorized destination's live credentials and route against the checkpoint through nativeCheckpointReplayable. A matching API/provider/endpoint/account replays the checkpoint directly, even when the conversation has no historical connection binding. This allows a scheduled owner conversation to resume on its original ChatGPT account; the ordinary authorized admission then records the selected binding. No source account is inferred or conversion request sent. A different account or provider still requires the source conversion below.

Native checkpoint conversion uses the source conversation's durable connection and generation through checkpointSource, without target admission or affinity mutation. Missing source identity is a refusal, never a guess from current account inventory. The factory's convertNativeCheckpoint(verifySource) temporarily applies source authorization to every conversion request, including resident sessions, then restores ordinary request verification in finally.

Model wire changes between turns

The spawner starts the shared wireApiProbe in the background after ordinary chat admission when resolveWireApi reports an unknown custom auto model. Turn startup never awaits network detection; the admitted session immediately uses the default wire. The probe uses the same principal, spend gate and usage accounting as the initiating request. Refused and deferred probes keep process-local diagnostic status and do not fail the chat. A genuine authentication error still surfaces from the real request itself. Automatic probes share one in-flight promise per provider/model and endpoint, with a 30-second backoff for refused, deferred and all-absent outcomes.

A session slot carries its spawn-time API, base URL and compatibility in SessionRouteCheckRequest.wire. verifyAccountRoute and verifyConversionSource refresh account authorization, credentials, pricing and capabilities while retaining those wire fields. Account failover also retains the slot's wire. Code mode, hosted search, cache encoding and retention stay coherent for the whole run.

Before the next ordinary owner or channel turn, LiveSessionSpawner.changedWireApi starts a needed probe without waiting and compares the currently published wire with the session model. Once a background probe has stored its detection, a changed API or base URL records a typed wire-api chat event, then respawns the same conversation id from its journal. Owner chat preserves its thinking level, work directory and temporary vision-return profile. No prompt cache or remote Responses continuation is copied to the fresh runtime. Native OAuth transports are excluded; Codex checkpoint conversion remains on prepareNativeCheckpointRoute.

At PI's awaited failed-run settlement boundary, the factory passes the last recorded attempt evidence to SessionSpec.onProviderFailure. A route-absent verdict clears only the matching detected auto model through invalidateWireApi, with no replay or account move. Provider and model pins, and a detection replaced while the old wire was running, are retained.

Account retry at settlement

session/accountFailover.ts::installAccountFailover runs at PI's awaited agent_before_settle boundary, after the last assistant message and inside the turn's policy scope, including delegated children. The session factory passes its core account policy through the resource loader for ordinary account routes, including single-account providers. Plugin lifecycle hooks remain observational and contribute neither continuation nor transcript edits. The policy reads the failed request's structured evidence through the shared classifier. That evidence includes the dialect's own stream failure events: ProviderRequestRecorder passes PI's public onProviderStreamEvent callback and records an error or response.failed event (providerFailureEventOf in session/providerFailure.ts), because a stream that already answered 200, and every WebSocket attempt, states its failure only there. Retrying on the same account is PI's native auto-retry, configured from the operator's Limits (retryMaxRetries, retryBaseDelayMs): PI repeats a transient failure such as a Codex server_error or a dropped WebSocket before the run settles, so the boundary sees only what PI declined (an exhausted subscription, a refusal) or a spent budget. The boundary only moves accounts: a verdict that proves an account limit continues on another eligible account, at most three per series. It removes only the failed assistant entry; completed tools and results stay in the prefix. The entire account series is retired only on agent_settled, so independent turns may use a recovered account without allowing loops between attempts. Every recorded request sets supported provider SDK maxRetries: 0, so PI's loop is the only retry budget.

Request retry policy and dispatch evidence

session/retryPolicy.ts installs the required strict canonical policy on each AgentSession through native SettingsManager.applyOverrides and the awaited Agent.prepareRequest seam. Installation follows setSteeringMode('all'), whose native setter otherwise resets transient overrides. Read current policy only at an original request boundary; freeze its revision across both native retry sleep and repeated dispatch. sessionRetryPolicy(session) synchronously exposes that pinned policy and refuses an uninstalled session. Skill-review model retries stay disabled, and supported provider SDK retries stay zero. PI alone owns same-account chat retry; no outer helper loop wraps the session or repeats unknown mutations. The wrapper awaits the prior public PrepareRequest callback and returns Promise<AgentRequestUpdate | undefined>, normalizing only void/no-op to undefined. Synchronous/asynchronous update objects retain identity; absent callbacks and rejections preserve their native meaning. This adds no cast, retry owner, policy read or timing/dispatch change beyond the original boundary above.

onSessionRetryDispatch(session, listener): () => void observes actual HTTP retry fetch dispatches after the original, not retryAttempt or a scheduled auto_retry_start.attempt. The outer factory runtime installs observeSessionRetryDispatch at PI's public StreamOptions.fetch boundary after auth, payload capture and awaited preflight barriers, preserving the existing recorder/evidence/watchdog stack. Abort checks precede the synchronous callback, which fires immediately before fetch; it is neither a first-token notification nor an onPayload approximation. The counter resets at the next original prepareRequest. The disposer removes the listener; session disposal clears all. The existing CompactionModelRoute signal excludes summaries from chat dispatch counts. No synthetic PI event, provider registry or private dependency hook is used.

Observability is explicitly HTTP-only: PI 1.3's public fetch option does not affect WebSocket transports. Physical/native WebSocket dispatch counts remain UNKNOWN, not zero or a scheduled attempt number. onPayload can precede validation, serialization, connection/reconnect and durable capture, so it cannot fill that gap.

Native PI chat also has a scheduling limitation: final HTTP Retry-After hints are NOT honored with provider SDK retries zero. Its public agent retry delay accepts only capped exponential settings, not response hints; AssistantMessage exposes error text, not a typed scheduling hint. Changing provider maxRetryDelayMs does not fix this missing public seam. Do not infer hints from prose, claim support for long server minima on this native chat path, patch PI or add an outer retry loop.

Sessionless helper, image and embedding requests instead call the existing runAttemptSeries with one captured policy for the whole authorized series, including account moves. Its total dispatch cap is min(4, 1 + maxRetries), retaining the existing three-account/one-same-account-repeat constraints. Before every repeated admission it waits with canonical timing and the shared abort-aware helper. A typed transient Retry-After is a minimum on the eligible same-account repeat, not clipped by the 60-second exponential cap. The real embedding regression proves a 120000 ms minimum; this does NOT establish native PI chat hint support. The canonical helper's overflow-safe long wait is required for hints beyond the platform timer maximum. Every attempt still reauthorizes/account-admits; open outcomes never grant replay.

Compaction captures the same timing once per summary but retains its independent compactionRetries budget and abort-aware canonical waits. It neither borrows the chat retry maximum nor contributes chat dispatch callbacks. Required policy getters reach session factory, core/sessionless inference, image and embeddings without an old-policy/default fallback. Assembly/runner forwarding remains its existing owners' responsibility. No session/listener or request means no retry timer or model cost; repeated authorized requests retain ordinary per-attempt accounting. Focused session, compaction, helper/image and embedding tests cover these component seams, not full bootstrap, every transport or physical-device acceptance.

Scheduled owner permissions

TurnContextBuilder.scopeOptions treats a host-stamped automation: 'scheduled' request as noninteractive when calling PermissionApprovalService.turnPermissions. Personal cron jobs and plugin-owned scheduled conversations retain their owner's identity and project, but have no requestApproval callback: ask-ruled tools follow the account's unattendedAsks, including when YOLO is off. Human owner turns keep their approval callback. The existing resolveAsk gate owns both outcomes; the builder adds no independent permission rule or prompt composition.

Turn composition and reminder envelopes

session/turnPrompt.ts::composeTurn(parts, surface, memoryBodies) is the pure assembly seam used by TurnContextBuilder and ChannelSessionService, including their steering paths. Callers resolve context and delivery callbacks first. composeTurn delegates ordered pieces to composeTurnWire, adds memory provenance only when non-empty, and returns the clean user text with its typed journal context. It performs no I/O and commits nothing.

A descriptor with an ambient key carries the resolved skills decision, optional permission decision and admitted turn state. Each carried block stores its digest even when its rendered bytes are empty or suppressed as already delivered. A steer omits ambient and writes no new turn state. Owner turns carry permissions; channel turns omit them because unattendedAsks has no approval channel to describe. Owner steers carry running-child reminders; same-sender channel steers intentionally omit runningSubagents. These omissions are surface data, while piece order and state assembly have one owner. No ambient descriptor means no ambient pieces or digests; no running-child text means no running-child piece.

shared/text.ts::systemReminder(body, instruction?) builds the existing multiline envelope through wrapUntrusted. It preserves body whitespace and optionally appends the existing <instruction> line. The body is already composed trusted context; callers neutralize untrusted source text with escapeUntrustedFrameBody before interpolation, preserving existing template prose. Runtime reminder producers and the seven mode templates use it; mode templates now own only their inner content and turnModeContext adds the envelope after rendering. The Files plugin's two reference-compatible inline Read warnings retain their single-line format. buildPostCompactionContext also neutralizes closing active-plan tags in plan text, so a plan cannot close either containing envelope. Ordinary rendered prompt bytes stay unchanged.

The three composition owners are internal functions used by LiveSessionSpawner, not new plugin APIs. renderSessionPersona renders the persona templates with the instance owner's identity and the composing account's style, and a shared room appends its platform overlay after them. sessionPersonaAppend keeps model identity, the existing tool catalog, plugin fragments, caller suffix and escaped account instructions in that order. A fork retains owner-chat bytes; a scheduled prompt omits product and room overlay. Without a plugin route that supplies a work template, the shipped templates stand. Composition runs once per spawn and adds no provider call.

The conversational order is elowen, elowen-harness, elowen-engineering, then the selected work template. prompts/elowen.md owns identity, memory, approval preparation and communication style; prompts/elowen-harness.md owns tool selection, session guidance, context and delegation; prompts/elowen-engineering.md is the canonical authority, trust-boundary, engineering and evidence foundation; prompts/elowen-work.md owns conversational task handling, recovery and writing. Implementation duties in the foundation apply only when implementing; planning and review evaluate against the same guidance. The code-mode route replaces only the work slot, so it retains engineering. The selected personality fills {{personality}}; account instructions remain an escaped suffix. Memory conventions still come from prompts/memory/conventions.md through MEMORY_CONVENTIONS, shared with curation and dreaming.

Fresh typed and generic delegated workers instead render elowen-engineering and delegated/worker. The shared foundation supplies capability authority and untrusted-data handling; the worker contract adds tool discovery, skills, file-read and continuation guidance. They need no conversational harness, advisor persona, memory-writing instructions, chat style or shared-room overlay. The existing suffix still carries model identity, applicable tool catalog and plugin fragments, role/context from extraAppend, then account instructions; project context and cwd follow through the resource loader. Workers return evidence and blockers to the parent without an interactive-user workflow. sessionPromptFragments omits fragments registered with interactiveOnly:true only for these workers, while preserving the existing SkillLoad visibility projection and fragment order. AskUser is the bundled interactive consumer. Missing options keep the existing fragment. This is prompt projection, not a tool restriction. Forks retain the parent's exact prefix through forkPrefix.ts; scheduled, dreaming and internal skill-review routes keep their existing composition paths. No recalled memories are added to workers.

Tool guidance describes how to choose an available capability, while tool definitions and the active catalog supply names and schemas. Product documentation answers feature and configuration questions; the owning typed tool, then ElowenApi with endpoint search and schema discovery, handles live state and actions. The prompt follows the active discovery route without introducing another catalog or changing permission gates. The core workflow scopes engineering guidance to software and infrastructure changes, requires traceable sources and calculations for research, checks final file content and relevant layout before delivery through the current surface, and checks destination or operation status before repeating a remote write whose outcome is unknown. These are prompt policies, not automatic verification or a runtime retry mechanism.

CodeModeRouteInput.model carries only the optional useResponsesLite fact consumed by the code-mode route. The provider retains its API, endpoint, type and enablement fields; PI model compatibility remains with the provider runtime. No model API or grammar-support field is projected into this route input.

createCodeModeComposition first validates the optional plugin route with empty tool lists, composes only fully gated nested definitions, then validates exposure against the final tool and wrapper names. Without a control or eligible route the session keeps the direct tool surface. Its delayed session, trace and ownership getters follow spawn ordering; principal and notification liveness stay per-call/per-turn. Notifications steer only the captured principal's active streaming turn, and native agent_start resumes that principal's queued notifications. The shared deferred catalog remains in prompt/toolCatalog.ts. Before handing tools to the code-mode composer, the spawner filters ordinary delegated sessions through toolPermitted and the same spawn-fixed delegatedVisibilityToolPolicy used for skill advertisement: the captured scope intersected with the composing account's grant. Consequently the exec description, nested tools and ALL_TOOLS catalogue contain only that delegated view, including deferred tools. Memory dreaming is one consumer. Filtering happens once at session build, adds no request, and leaves cached declarations stable across calls. Owner and shared-room sessions keep their existing per-turn behavior; forks keep the parent's cached declarations. Without a delegated scope or an eligible code-mode route, this restriction adds no composition step.

createLiveRecallWiring builds the factory's existing LiveRecallSpec and installs memory-tool delivery accounting after the live session exists. It excludes delegated children and ownerless sessions; owner chat recalls for its owner, while a room recalls only for the verified current writer and global categories. Budget, user toggle and retained delivery tracker are read on each pass. Retrieval resolves Project/category scope before its first await and keeps the existing dedup predicate. Without a memory service no recall extension is installed; an unwired budget disables searches. Retrieval costs remain bounded by the existing pass/count/byte budget.

Policy refusals use the shared result builder in session/capabilities.ts and return isError: true with details.refusedByPolicy: true. PI forwards the error verdict to the model without treating it as a session failure. Nested code-mode calls reject error results and preserve a policy refusal result as the error cause, so the trace sink keeps its policy warning metadata. Trace records read the result's isError flag unless an explicit event verdict is supplied.

PI's prompt preflight callback reports the accepted dispositions handled, queued and started. A rejection throws and rolls back unadmitted state. When rollback removes an already-persisted input, TurnAdmission.rollbackPending reloads the surviving journal through refreshJournalSession and refreshes PI's context before another client may continue that live conversation; the next append must never name a deleted parent. PI acceptance and provider delivery are distinct. All prompt dispositions accept the operation, but handled owes no PI user entry: its pending journal admission and discard claim are removed. Only started confirms provider-only one-shot notices and memory deliveries. enqueueMirrored stages attachment and echo identity before PI's synchronous queue events, retains queued inputs, removes handled inputs and returns PI's disposition. A delegated echo carries its asynchronous confirmHandled callback through every enqueue and survivor requeue. The shared queue path awaits the existing deliveredDelegatedSteer transition before admission resolves and propagates any rejection to the caller. The transition prevents duplicate settlement and cannot revive a dropped receipt.

After asynchronous steer preparation, BrainTurnRunner.sendTurn rechecks the live session identity, streaming or native-compaction-check state, and pending mode selections immediately before native queue admission. If the observed turn ended or its session was replaced, the input follows the ordinary send-lock path and re-resolves its destination there. Owner activity is prepared with deferred begin for either route; only a fresh turn begins and settles it, so a send crossing settlement owns its own activity sequence instead of leaving an idle PI queue. Web and CLI sends share this boundary.

ChannelSessionService.send is admission, the delegated-call bracket and the steering attempt, followed by the session lock. Inside the lock prepareChannelSession readies the live session (history seed, dispose on a model or classification change, spawn, direct and work-dir persistence), saveResumeEnvelope writes the durable resume envelope, runChannelTurn runs the turn from an explicit context object, and finishChannelTurn drops what the turn leaves behind. The order of those effects is load-bearing: the park marker is cleared before the lock, the envelope is written before the turn and cleared in the finally, the curator runs only after the lock, and turnSender and turnWriterUserId are set and reset in the same window.

Prompt ordering is deliberate. Stable session content is composed at spawn time; volatile turn context is placed around the user message. The first line of the spawn-time append region names the model the session runs on (modelIdentityLine in prompt/sessionPersona.ts: You are powered by the model named {name}. The exact model ID is {provider}/{model}.), from the same resolved route the session uses, so it changes only when a respawn changes the model; the reasoning level changes without a respawn and is told by its chat event instead. Turn context is stored as one native custom_message entry per contribution, with customType: 'elowen.turn-context'. Its strict details contain kind, anchorId, placement and order; memory records carry exact memoryBodies pairs of id and body, without a separate id list. The user entry contains the person's words and attachment references. providerText preserves PI's accepted expansion when it differs from display words. Accepted image blocks remain in content as immutable content-addressed references, preserving PI's resized bytes on restore and fork; images retains original attachments for display only.

composeTurnWire returns words for prompt() and a transient TurnContextPlan for the existing admission queue. PI owns entry IDs and parents. The journal append barrier commits the user or hidden host entry and its context records in one transaction. Admissions are consumed in native append order, distinguished by user versus custom-message type, without comparing text. Handled inputs and failures drop unconsumed admission metadata. Queued user echoes retain their plan until PI delivers them.

TurnContextPlan and mid-turn memory attachment carry exact memoryBodies pairs of id and body, never a parallel memoryIds list. BrainSessionFactory appends a memory or step record using the anchor's native journal identity; missing identity is an invariant error. The optional fifth append argument is memoryBodies. Memory ids used for accounting remain separate from durable prompt records. Owner and channel admission share this plan, and live recall uses the same native append. With no memory bodies no memoryBodies property is written. Migration 58 retires historical stored id lists before the strict reader runs; existing historical context kinds remain readable.

turnContextRecords.ts owns CONTEXT_CLEARED_TYPE with the persisted value elowen.context-cleared. JournalRepository.clearContextRecords and coldToolResultClearing use it when retiring a context record into an inert native custom entry, preserving its identity and parent link. No clearing means no marker is written. The constant introduces no property-name registry, migration, new storage or additional work; contextCleared properties and SQL JSON paths stay unchanged.

session/turnContextRecords.ts owns the sole wire composition through the existing agent.transformContext hook. Lead blocks, words and trail blocks form separate text blocks inside the same user message, followed by images. The hook and read-only consumers derive every message's identity from SessionManager.buildSessionProjection and its source entries, including freshly projected hidden custom messages. Context handlers receive these identities before extension cloning; no private PI identity map or text matching is used. Live recall and step-context handlers use the same native append path during the context hook, before the final projection; they do not own transcript copies or splice text. Every mid-turn record projects as its own hidden message after its fixed native anchor, so another record at the same anchor extends the existing sequence. A native compaction cut that retains a record beyond its summarized anchor keeps that record at its native position.

registerText('step-context', fn) optionally accepts afterTools. Those providers wait the existing administrator interval of non-thinking assistant messages since both watched activity and their own reminder. The journal's optional stepProviders field records stable plugin registration keys; retained anchors determine the interval on restoration. Cold cleanup removes reminder bodies but keeps their minimal cadence data in the existing retired native entry. stepReminderRecords reads both forms, and records whose anchors compaction removed cannot suppress a new reminder. Providers omitted by the shared body clamp are not recorded as reminded and may run on the next step. Direct calls and validated nested tool traces reset activity age. Other providers retain the original per-turn tool-call cadence. No reminder rewrites an earlier cached message. Registry Todo is the real consumer; its task policy remains plugin-owned, with only generic progress guidance in the shared harness.

runColdHistoryPasses awaits TranscriptRepository.coldHistoryActivity(sessionId) before any image, context-record or tool-result rewrite. Owner turns and channel turns share this gate under their existing turn lock. The store reader returns the session and recursive fork activity from one read snapshot; ordinary delegated children do not share the prefix. After the await, streaming, compaction, a pending abort or sessionHasWorkInFlight skips the pass. A change to interactedAt, the request TTL or the provider retention floor also invalidates the read. The gate temporarily subscribes to the existing onJournalChanged seam so a live chat-event append or rewrite during the read skips this turn's cleanup; it unsubscribes on success and failure. Reader errors use the existing cold-pass warning and leave history and paid compaction untouched. The optional cold-start summary is a provider request of the turn that is about to run, so runColdStartCompaction enters the session's providerPreflight.run with the caller's ColdToolResultClearingDeps.beforeProviderRequest: the owner runner passes its beforeProviderRequest(live), the channel service the same fresh recovery check as /compact. A refused preflight is logged as compaction.cold_start_failed and the turn's own request meets the same check. Cache TTL, retention floor and the one-minute safety margin are unchanged. File-backed queries run on the shared store read worker; memory databases use the identical dispatch locally. The worker adds a read round trip and still traverses the full fork ancestry, without a new cache, timeout or index.

Cold tool-result clearing passes only replacement content and a typed clearedToolResult marker to JournalRepository.clearToolResultRows. The repository merges that marker into the locked durable details; the live session retains its restored media details separately. Copying live details back would replace image/audio references with provider data URLs and make the next rehydration fail. The existing journal rewrite transaction still refreshes attachment and branch projections; no schema migration or permissive restore fallback is involved.

Warm requests never rewrite records. The existing cold and quiescent gate retires eligible context records to inert native custom entries, preserving IDs and parents while removing bodies and memory annotations. Mid-turn memory and step records are eligible even after the newest settled turn; skills and permissions remain. Clearing a full mode reminder marks its user or host anchor contextCleared, so the next directive is complete.

Context consumer audit: warm compaction uses the same transform; standalone compaction uses the pure projection over its native span. Token estimates, prefill measurement and context diagnostics use sessionContextMessages. Native rehydration restores words, records and entry provenance; delegated forks copy retained native context plus durable ancestry seeds and use the same projection, while peer forks retain the full selected journal branch. The HTML export omits hidden context and internal metadata. Authorized lossless JSONL exports and administrative journal diagnostics retain complete entries, including encrypted native checkpoints, just as they retain encrypted reasoning blocks. The normal transcript query removes details.nativeCheckpoint before projection; web, CLI, platform and HTML views show only the compaction divider. Titles, curator input, search and the web/CLI transcript read clean user words; the shared transcript query excludes typed context records before pagination. Billing and response-status readers use assistant usage and outcomes and need no injected text. Administrative journal diagnostics intentionally retain whole typed entries.

The read-only sessionContextMessages accessor requires only Pick<AgentSession, 'sessionManager'>. Consumers call sessionContextMessages({ sessionManager }) to compose the current native branch through the same source-entry provenance projection used by provider requests. Tests use it after journal rehydration in storedContextMessages and on the actual fake session manager in latestContextText, so lead/trail blocks stay inside their native user or host message and after-message records retain their order. With no context records, it returns the clean native projected messages without internal anchor metadata. It performs an in-memory projection of the current context and branch, with no provider call or durable write; it does not run live extension handlers or discover context outside that branch. The BrainService memory integration tests are a concrete consumer.

Boot migration 57 atomically converts annotated rows on every physical branch and moves historical replay attachment references into user content, preserving user IDs, timestamps, provisional state, origin and usage epochs. It redirects child links and active leaves through deterministic record IDs, invalidates the derived state projection, and logs progress counts every 128 conversations. These are progress checkpoints inside one transaction: a crash rolls back the entire migration and the next boot retries it. Runtime has no old-frame reader or stripping scanner.

A message the person sends while a turn is running is steered into it with the steered trail frame (STEERED_MESSAGE_REMINDER in session/turnPrompt.ts, composed through composeTurnWire like every other part): a reminder that the words arrived while the agent worked and must be addressed before it finishes. The owner's steer gets it from TurnContextBuilder.steerWire, a room's same-sender steer from the channel path, and both hand the typed context plan to the queue mirror through the echo (QueuedUserEcho.contextPlan), so the row written at PI's delivery replays the same bytes after a restore. The frame is strippable scaffolding; the row itself keeps only the person's words. A pause checkpoint and an Esc promotion keep the clean words too, because the fresh turn they become composes its own frames. A delegating agent's follow-up (DelegateContinue) is not the user's and carries no such frame.

Every live session carries the spawner's required planModeToolNames set; mode policy reads its native getAllTools catalog directly. Cache watch requires the native session subscription seam.

Plan mode admits exactly one set, built at spawn by planModeToolNames (src/brain/toolMetadata.ts): the read-only set the read-only agent boundary uses (readOnlyToolNames in src/brain/readOnlyAgentTools.ts: core READ_ONLY_AGENT_TOOLS plus every plugin's validated provides.readOnlyTools, PluginRegistry.readOnlyToolNames), plus the host defaults in CORE_TOOL_DEFAULTS. Those defaults are the clamped writers (Write/Edit to the session plan file, Bash with the non-destructive shell rules), the read-only delegation tools (Delegate, DelegateContinue, ExitPlanMode, exec, wait) and the conversationOnly tools that act only on the conversation (AskUserQuestion, ShowView, ShareImage, ShareFile, ToolSearch). A tool registered external (the MCP bridge) is left out even under its plugin's mcp__* read-only declaration, which vouches for a remote server's whole tool list; read-only delegated children still receive those tools. Every other composed tool is denied at execute time, including nested code-mode calls, which pass the same gate. A task list's own writes are admitted because the todo plugin declares them read-only: they change only the agent's working list. There is no other plan-safety declaration; manifests no longer carry planSafe, and registration options carry no planMode.

Managed Plan turns use the same core file and shell policy as host turns. planLocation and isPlanLocation in managedArtifacts.ts supply the single plan path and exact write allowance. currentAccess().planFile carries the own-plan allowance to Sandbox; read-only children never receive it. Plans remain only in the managed container, including after ExitPlanMode; queued lifecycle operations run before plan reads, so clear cannot resurrect an old guest plan. Core output spilling and cold clearing use their separate artifact control even in Plan mode. See Projects and execution targets for migration, rollout and rollback.

DelegateContinue from a read-only turn (plan mode, or a read-only child) runs its entire call under readOnlyContinuationScope (src/brain/delegatedScope.ts): the read-only set intersected with the child's own allow-list, the caller's boundary clamped by buildReadOnlyBoundary with the child's own deny and ask rules re-asserted, and readOnlyOrigin: 'imposed'. DelegatedSessionService.continueSubagent binds the minted scope to the exact durable call row in brain_subagent_runs.read_only_scope before its first send. sendDelegated reads that same column for every turn of the open call, including background-job results, the pending-helper-result drain, settlement follow-ups and boot recovery. Progress upserts and recovery claims preserve the scope. ChannelSessionService executes each turn under it (ChannelSendOpts.readOnlyTurnScope), while the child's permanent scope still composes and identifies the session. A planning caller cannot steer an active child; a Build caller cannot steer or follow up a still-open read-only call and must wait for it to finish. A hidden helper result is not a caller's message: while the call's own turn runs, ChannelSessionService.send steers it into that turn before the read-only refusal, since the only turn that can be running then is the clamped one. Once the call is terminal or parked, a later Build continuation uses the child's permanent access. The nullable additive migration does not change existing calls. scopeExceedsCurrentAccess skips the captured-boundary comparison for a read-only caller because that boundary is not what runs; every other axis still has to fit. Promotion (write_access) remains refused from a read-only turn.

Current conversation mode is read only by TranscriptRepository.conversationMode(sessionId) from the native active-branch mode event projection. Untouched conversations start in Build. BrainService.switchWorkMode is the only authorized switch: it rechecks ownership and complete client generation under the existing send and canonical session locks, commits the native event through the live or cold journal writer, and returns BrainWorkModeSwitchResult. After a 1,500 ms wait, a blocked request acknowledges queued: true with current control, never the requested selection. For an explicit or active conversation, the pending counter and send-lock reservation happen synchronously before asynchronous authorization. The initial authorization must succeed before even a queued acknowledgement; authorization is rechecked inside the canonical session lock before any mutation. Requests apply in call order at the turn boundary through those existing locks; the durable mode event signals completion. Fast unchanged selections write nothing; a queued unchanged request records its completion. Late failures use the existing conversation error seam, not a success notification. A pending switch fences newly sent owner messages into a fresh turn after the switch, with immediate queue acceptance through the existing pending-admission chips; already-admitted PI steering and hidden nudges keep the running turn's scope. Neither CLI nor web delays sends behind the switch HTTP response. Headless startup requests waitForCompletion: true on the same command. The pending counter stores no mode value and is only an admission fence. Status, snapshots, prompt composition and execution policy all use that reader. Owner and channel scopes share applyConversationModePolicy for tool denials and applyConversationModePermissions for the Plan shell clamp, including sessions without account permission settings; captured denies remain last and independent of the selected mode. A live turn retains its admitted scope until it settles. CLI, web and platform controls render the same durable chat-event; there is no mode field in TurnRequest, queue echoes, /brain/send or /v1/agent/runs. Headless mode flags persistently switch before sending. Scheduled sends, cron, goal-loop sends, background-process nudges and API runs follow their target conversation, including visible ordinary Plan write refusals. Queued messages select mode at admission, not enqueue. Delegated children and workflow nodes start with no parent mode selection or owner-chat mode directive; a child turn that starts while its delegating conversation is in Plan receives only the sub-agent plan directive (prompts/delegated/plan-mode.md). Delegated forks keep the copied transcript prefix but re-establish Build through a hidden native initializer when that prefix selected another mode. Their immutable captured permissions, readOnlyOrigin and tool policy govern execution instead; runWithPolicy carries that provenance to currentAccess() so further delegation cannot drop an imposed read-only boundary. Platform Plan turns use the platform/plan-mode full and sparse templates without owner-only approval tools; owner chat uses cli/plan-mode. Forks copy active-branch evidence and clear retains the selection with a hidden initializer. Rewind captures the selected mode before removing its checked reply tail and restores changed evidence as a hidden native initializer in that same write transaction. Every history rewrite notification (compacted) carries the authoritative workMode, including cold rewinds, so CLI and web refresh their control mirror immediately rather than keeping a deleted event's mode. Initializers use the cold writer's interrupted-tail placement and emit no user notification. Their model-facing text says This conversation is in X mode. rather than attributing a hidden initialization to a user switch; visible switch events keep their user-action wording. The PostgreSQL append and insert owner is packages/db/src/journal/writer.ts, reached through JournalRepository; TranscriptRepository.pendingTailStart locates interrupted-tail placement. Protected journal transactions retain sequence, usage-epoch and parent validation, and publish change notifications only after commit. Runtime never infers selection from historical turns or pending plans.

The full Plan directive follows the reference planning workflow and its variant is chosen in code by turnModeContext (session/modeContext.ts). The first turn in a mode and every fifth turn after it get the full directive, and the turns between get the sparse one. The count restarts on a mode change, after a compaction, or when the cold pass strips the framing of the last full directive; Workflow mode follows the same rule. planStore.planTurnContext supplies the Plan File Info line (path plus whether the file exists, or that its state is unknown). Phase 1 and Phase 2 come from the subagent control's planModeWorkflow slot when the turn may call Delegate, fed the operator ceilings RuntimeLimits.planModeExploreAgents and planModePlanAgents (0–5, default 3, Settings → Runtime); a phase the plugin leaves out (ceiling 0, no Delegate, plugin disabled) uses core's prompts/plan-mode/initial-understanding.md or design.md, in which the agent explores or designs itself. The plan sub-agent type is read-only and returns its proposal; only the planning conversation writes the plan file. The owner directive keeps the one Elowen addition to the reference text, that pure research is answered without ExitPlanMode; the room directive ends with presenting the plan to the room instead of Phase 5. The plugin text is computed only for full directives, so sparse turns cost nothing extra.

What a turn still has to say is decided from the journal, never from fields on the live session. Every composed turn stores turnState (TurnState in session/turnPrompt.ts: mode, reasoning level, how many turns since the mode's full directive, the permission and skill digests, and the id of the newest compaction it was composed after) on the entry that carried the prompt: the user row, or for a room's hidden host message its details. The next turn reads the newest such state on the active branch with TranscriptRepository.turnBaseline and decides from it whether a mode is being entered, whether the full directive is due, whether an ambient block (session/ambientBlock.ts) is already in context, and whether a compaction since then calls for re-orientation (compactedSince, compared by id because PI can compact at the start of the call that sends a turn). The full directive is also due again when the cold pass stripped the framing of the row that last sent it (TurnBaseline.modeDirectiveStripped), because the sparse line says the full text is earlier in the conversation. The state is inert to provider serializers, rolls back with a refused turn's row, follows branch switches, and reads the same for a live session and a respawned copy, so both send the same next request. Example: two plan-mode turns with a respawn between them; the third turn gets the sparse directive and no repeated permission summary, because the replayed rows already carry both. Rows written before this state existed read as "nothing shown yet", so the first turn after an upgrade restates everything once.

Plan decisions belong to the daemon, not to browser storage or a CLI decided-ID cache. Web and CLI post { sessionId, planId, decision: 'implement' | 'dismiss' } to POST /brain/plan/decision. The ordinary owner-conversation authorization applies before any history read or mutation; strict Zod validation refuses malformed bodies. BrainStatusService.turnControl supplies the same explicit BrainStreamControl as snapshots. The decision must match the current pending tool-call ID and an idle plan-mode conversation, otherwise the route returns 409 and current control. Competing decisions serialize through durable dismissal or ordinary send admission. Dismiss holds the existing send and conversation locks; implement uses BrainService.startSend with refuseIfBusy and the sole approval prompt IMPLEMENT_PLAN_PROMPT in src/shared/planTool.ts. The host-only asynchronous TurnRequest.beforeAdmission guard rechecks control under the send lock, then selects Build through the same switchWorkMode operation before preparing a user row. If admission fails, its awaited rollback selects Plan through that operation; a failed decision waits for rollback settlement before returning. An admitted implementation remains Build even if its provider later fails. Without a guard, ordinary send behavior is unchanged; a thrown guard refuses admission without a prompt. No extra model request, table or client-local decision memory is introduced.

Dismiss records { v: 1, kind: 'plan', planId, decision: 'dismissed' } through the existing chat-event writer, including its model reminder and localized web/CLI event row. pendingSubmittedPlan scans only the newest turn; a matching later dismissal retires that plan, a different ID or earlier dismissal does not, and a later new plan becomes pending normally. A fresh service over the same journal therefore restores dismissed and undecided plans correctly and keeps the stored plan mode. A plan chat event triggers the existing status reconciliation lane on every attached surface. With no pending plan there is no decision card.

The CLI observes ChatState.controlRevision through watchControl; applyControl applies the daemon's complete projection, including native status, foreground/background liveness, mode and pending plan together so status and snapshots expose no intermediate combination. StreamCoordinator reconciles at most one picker for the authoritative plan ID, only after atomic snapshot replay, and closes it on a remote decision, plan change, child navigation or teardown. openPicker returns an idempotent close handle that restores the editor without selecting or cancelling; only user Esc calls optional onCancel. The plan flow treats Cancel and Esc as daemon dismissals, and notifies its owner after request/status reconciliation so failed decisions can restore the same pending picker. Late responses are fenced by the existing control revision on CLI and hydration stamp on web. A stale web decision re-reads daemon status. The CLI applies the control returned with a stale decision under the same revision fence and shows localized feedback without another status request. The bounded picker adds no polling or decision persistence.

applySubagentUpdate awaits the registry's child-claim change and its activity publication before publishing nonterminal progress. Terminal publication belongs to the process-owned DelegationRepository.onSubagentRunSettled observer: completion, cancellation, terminal upsert, boot healing, supersession and recovery-required writers notify it only after the outer transaction commits. Rejected/idempotent writes and rollbacks notify nothing. BrainService re-reads the durable call by toolCallId, releases its progress claim and publishes the existing subagent event through publishToSession(..., true); it does not end an actual child call or acknowledge its result. Runner commits send only the parent/call identity in run-settled IPC, including settlements between turns, and the daemon uses the same publisher. An attached runner drill-in forwards these daemon-owned terminal subagent events alongside its existing daemon control events; nonterminal progress remains exclusively on the runner stream. IPC publication failures are reported as delegation.settlement_publication_failed. No progress emitter, timer or second client registry is required. CLI and web already apply these events to hydrated call rows and replace their latest transcript window from durable snapshots on reconnect. Terminal results awaiting acknowledgement remain visible as pending delivery, not running; acknowledged terminal calls disappear from the rail. Owner completion ingress also returns the asynchronous publisher's result, so a publication failure rejects its caller. Durable result-write failures likewise propagate, leaving the transaction rolled back and the run recoverable; the Subagent plugin reports the failure and a new boot may complete the result once.

Isolated lifecycle tests must wire the same repository settlement observer when they omit BrainService; a terminal write no longer calls the progress sink directly. Runner fixtures provide both run and steer observers and assert ready before dispatch. runnerDatabase.test.ts holds each observer registration separately to verify that readiness waits for both, while runnerSteer.test.ts checks the committed run's IPC identity.

BrainResultDelivery in src/brain/service/resultDelivery.ts owns the durable result inbox drain, per-parent shared promises and bounded retry timers. BrainTurnRunner injects the store, event publisher and hidden custom-message sender while retaining parent-turn locks and admission. Background children, detached calls and whole-workflow summaries enter through acceptDelegationCompletion(parentSessionId, userId, completion). The discriminated DelegationCompletion.kind selects completeSubagentRun or enqueueWorkflowResult, then shares control publication and one drain. Both return the typed accepted/already-terminal/rejected outcome; a missing workflow link returns { status: 'rejected', reason: 'persistence' }. Owner and channel scopes bind this ingress as completeDelegation, exposed to plugins by the single delegationCompletionEmitter(). Boot workflow recovery uses the same ingress without a turn-scoped emitter. Only the whole-DAG summary enters the parent transcript; node progress remains in the workflow rail.

For owner and delegated conversations, delivery has one authority: transcript.settledResultIds(sessionId: string, resultIds: readonly string[]): Promise<Set<string>>, implemented beside journalEntryIds in packages/db/src/transcript/reads.ts. It returns requested result IDs carried in details.resultId by settled custom_message journal entries with pending = 0. Provisional and discarded entries do not count. The session-indexed read includes pre-compaction history and uses the bound PostgreSQL executor for the candidate batch; an empty candidate list returns an empty set. Live PI messages are not delivery evidence because compaction can remove them.

Persistence acknowledges ordinary results in the same transaction that appends their settled custom_message entry with details.resultId. After commit, its OnResultDelivered callback receives (sessionId, [{ resultId, toolCallId, kind }]); the spawner binds it through BrainTurnRunner.onResultDelivered to BrainResultDelivery.publishResultDelivery, the only publisher of the acknowledged sub-agent event. The publisher re-reads durable run snapshots; workflow results have a journal badge but no sub-agent run row to publish. Without a publication callback, the durable acknowledgement still hydrates correctly, but no live delivery event is emitted. A failed journal append leaves both transcript and inbox unchanged.

Each drain first checks settled IDs to repair older pending ordinary results without sending them again. Remaining owner results form one batch: all but the last append with triggerTurn: false; the last starts one parent turn. If the parent is streaming, the sender uses PI's FIFO steering queue instead.

The drain does not wait out the turn it started. A parent has one drain at a time, and a wake that finds it running (acceptDelegationCompletion, the delegated settlement loop, a Stop) wakes it and joins its outcome. The drain registers the result it sends with a turn as in flight; the result's commit, reported through publishResultDelivery before the turn's first provider request, tells it the parent has taken the result and runs that turn. From then on the drain only owes that send's settlement: it keeps rescanning the inbox on every wake, and each result the parent gains meanwhile goes through the ordinary sender, which steers it into the running turn between steps (delegation.result_steered). A delegated parent, whose every result send carries a turn, therefore gets one turn for the first result and steers for the rest, like an owner's batch. When nothing is left to place, the drain waits for the next wake or for a taken send to end, settles that send (landed, a failure, an abort) exactly as a directly awaited one, and ends only with no taken send and nothing to place. A wake without a running drain starts a new one, so a result that arrives after the turn ended runs as the next idle delivery. queuedResultIds(session) in session/steerCustomMessage.ts reads agent.peekQueuedMessages(), selecting only DELIVERED_RESULT custom messages with string details.resultId. Owner and delegated senders share this queue read; there is no host-side in-flight set or custom steer receipt. A queued result stays pending until ingestion commits it. Queue emptiness alone is not a safe admission boundary: PI dequeues into its private pendingMessages before asynchronous next-step preparation, including compaction, and journals that batch only after preparation finishes.

installResultSteerBoundary(live) registers once per native session before owner/channel runs. The existing agent_start and turn_end events close a liveness fence before PI can dequeue; the next assistant message_start or final agent_settled opens it after the persistence append barrier flushes. The fence stores no result IDs and proves no delivery. Both streaming senders capture resultSteerBoundary(live) before SQL, await its readiness, then check its revision synchronously before enqueue. A crossed step invalidates the reads: the sender reads the settled journal again instead of resending a result temporarily absent from both queue and journal. Native retries retain the fence until the next assistant or final settlement. The cost is one session-lifetime subscription and a persistence flush at each fenced assistant/settlement boundary.

Stop-cleared, uncommitted results stay in the durable inbox. A preparation in progress is not released merely because a queue-update is empty; actual run settlement releases waiting senders. Restart recovery replays the pending inbox into a fresh session, with no volatile fence carried over. User steers and results retain their enqueue order; PI's all-mode queue still batches results already admitted during the same tool round. A new result arriving during preparation can wait for the following native queue boundary. Injection cannot preempt an active tool or code-mode cell. The owner steer branch logs delegation.result_steered as local-only diagnostics.

Non-steered sends must produce a settled journal entry; a missing entry uses the existing bounded failure/retry path. Acknowledged results stay acknowledged even if a later delivery or the parent's answer fails.

A failed sub-agent boot recovery enqueues an ordinary error result. It wakes an idle owner parent through the same batched delivery path, or steers a running parent. The parent model checks whether the last step completed before choosing whether to call DelegateContinue; no new user message is required. Recovery failures acknowledge at journal commit like other results and are never restored after compaction. A pending failure cannot release or clear a parked owner turn: boot delivery answers it, or the existing parked continuation resumes it. Stop, park attempt budgets and native computer-prerequisite waits retain their authority.

The drain returns { answered, delivered, aborted?, failed?, waitingForComputer? }. answered records whether the last delivery returned landed, with an empty drain requiring no answer. Shared promise joiners can drain late arrivals; they never put acknowledged rows back into the inbox. Turn cleanup flushes persistence and re-drains any remaining pending rows, including results a Stop removed from PI's queue before ingestion. Delegated non-user parents use the same settled-journal proof and PI queue deduplication; sendDelegatedCustom distinguishes a silent committed turn from an uncommitted steer through the result inbox's delivery state, not brain_delegated_steers. Retry scheduling retains the five-attempt limit and existing backoff, with no poller or timer for delegated parents and no late turn on a terminal child. Delegated settlement imports PendingResultDrainOutcome from this module; platform recovery imports the shared subagentResultReminder.

Native computer waits during result delivery

CustomDelivery = 'waiting-for-computer' means the native recipient committed or confirmed its own same-generation prerequisite, not that delivery succeeded. BrainResultDelivery never invents waiting from an offline exception. It stops the batch with waitingForComputer: true and answered: false, without spending a delivery attempt or arming a result retry timer. Ordinary shared-promise joiners without a native wake retain that wait without another send. A reconnect joiner carrying a canonical wake waits for the older map-owned promise and its cleanup, then drains the existing serialized tail with that SAME wake instead of inheriting the older drain's offline observation. Any new native wait from the tail remains waiting even without a new receipt. An empty tail does not inherit the old waiting flag; any already-delivered unanswered continuation stays with parked-conversation recovery. Existing failed and aborted outcomes still short-circuit before a tail drain. Genuine delivery failures retain the five-attempt budget and retry behavior.

The optional third drain argument is the SAME canonical ComputerPrerequisiteWake<ConversationContinuationReference> through body, tail and the sender's seventh argument. Existing BrainTurnRunner forwarding and parked-conversation recovery carry that exact object to native admission; waiting suspends before failed-result wake handling or rescue continuation. Delegated and workflow recipients retain their own native call/node claim, not a conversation wake converted into another authority.

All parents still prove delivery through settledResultIds and the transactional parent journal/inbox ACK. If ingestion already acknowledged an ordinary result before the model continuation parks, the drain retains its delivered reference, does not resend it and does not treat a second ACK refusal as missing delivery. The user-action consumption gate and post-commit delivery publisher are unchanged. Unified delegationCompletionEmitter and resultSteerBoundary remain the completion and PI queue owners; no custom result receipt, ledger, claim map or poller is added.

BrainReadiness in src/brain/service/brainReadiness.ts awaits current configuration before resolving the default model and the connectivity probe exposed by the BrainService facade. Construct it with live config, runtime, the authorized dispatch (authorizeAttempt), the credentialed-endpoint resolver (routeCredentials), optional session creator and working-directory getters. It requires no conversation store or live-session registry. The admin provider test uses the same composer-bound PI session path as chat, with no tools, extensions, skills, context files or disk-backed settings, and it runs on the runtime of the ACCOUNT the dispatch authorized: the probe states the acting account it runs for, so it never runs as the instance because a caller named nobody, and a paused, unauthorized or credential-less account refuses with the reason instead of being tested through another one. It makes one request, caps the wait at 20 seconds, aborts and disposes the throwaway session, and redacts that account's own key before truncating public errors. Missing configuration reports unavailable; normal conversation status/history reads do not create a probe session.

Delegated text follow-ups use the existing brain_delegated_steers receipt, brain_subagent_results inbox, and subagent event seam. The PI journal append confirms a text follow-up's delivery in the same transaction as its child message entry through echoId (commitJournal in src/brain/persistence.ts). Hidden delegated results instead use details.resultId and the atomic journal/inbox acknowledgement described above; they create no custom-kind steer receipt or details.delegatedSteerId. Existing custom receipts are inert history. A text receipt still queued when its turn ends is dropped. A settled receipt publishes the joined run snapshot locally or through the runner's settlement frame; a late run row republishes its current durable state, and stale queued events cannot downgrade a terminal display. Hook a parent surface into onDelegatedSteerSettled and read getSubagentRuns before publishing; with no listener or tool row, the durable receipt still hydrates later without a phantom event. A delivered confirmation does not wake the parent: on its next ordinary turn the existing runningSubagents after-user frame carries a bounded <delegated-follow-ups> block. Only the sender who can address that child sees it. PI's successful provider preflight marks notice_reported once; a preflight failure leaves it pending, while a later provider error cannot duplicate it. A dropped text follow-up instead enters the existing result inbox atomically and wakes the parent with the warning that it did not reach the child. When a child result is already pending, the drop is appended without triggering a separate turn, so the result turn sees both; platform resume uses the same result reminders. Migration preserves pending drop inbox rows and marks historic acknowledged drops as reported. For example, a delivered DelegateContinue updates its original tool row and informs the parent below the next user's words, never by rewriting the earlier tool result or cached system context.

Delegation admission precedes runner execution. SubagentDispatch passes the same validated delegatedChannelSendOpts to ChannelSessionService.sendRemote, which creates the durable child session and immutable scope through JournalRepository.createSession and emits session before calling the pool. The Delegate listener awaits persistence of its existing running run row; the workflow listener journals and snapshots its node's sessionId. Thus sparedChildSessionIds also covers background children waiting in the pool, without a separate queued-child registry. Foreground calls still await settlement and Ctrl+B changes the same run row to background.

The pool retains its FairQueue and producer rotation. Enqueue emits session progress with status: 'queued' and detail: 'queued: waiting for a free sub-agent slot'; dispatch emits status: 'running' and clears that detail. The shared progress fold carries these into the existing run/DAG detail, used by DelegateStatus, DelegateList and CLI/web rails. These are execution hints, not new durable lifecycle states. A background Delegate returns its job id and queued receipt without waiting for capacity; the stall watchdog excludes that wait. Stop removes queued work through the existing removeWhere abort path and settles it through ordinary completion. With the pool disabled or unavailable, the same admission feeds in-process execution.

Boot claims queued runs under the existing running/recovering lifecycle. A never-started Delegate has an empty transcript and is terminalized with the existing "had not started" notice in the parent's durable inbox; the clipped display task is not replayed as a new request. A workflow retains its full node task and channel/session pair in the existing recovery journal and resumes an empty child through the stored-scope fallback prompt. Admission adds only bounded session/progress writes and session events, no polling, timers or extra registry. Without a progress listener the child identity still exists, but no run or workflow projection is manufactured.

Restart continuation is owned by session/continueTurn.ts::continueInterruptedTurn. Owner chat calls it inside BrainTurnRunner.continueInterrupted under the existing conversation lock and turn scope. It acquires the outer send lock before inspecting or restoring the live runtime, then acquires the inner session lock for continuation. A computer reconnect may wake while the losing turn still owns admission and has not yet disposed its parked runtime; restoring before that owner finishes would leave the continuation with a vanished session. ensureLive retains its own inner spawn lock, so it runs between the two acquisitions, never inside the non-reentrant session lock. Platform and delegated recovery call the same function through ChannelSessionService.runChannelTurn. Final tails do no work, unfinished assistant fragments are retired through the canonical branch and durable discard, and unanswered interactive views still refuse admission. A continuable tail starts the public sendCustomMessage({ customType: 'elowen.continuation', content: [], display: false }, { triggerTurn: true }) path. PI owns threshold compaction, before_agent_start with messageType: 'custom', prompt/tool declaration, pending nextTurn messages, busy/Stop, retry and the single agent_settled; the host neither calls private run/preflight members nor copies the session loop.

The internal empty trigger is authorized because PI has no public session-level start with an empty message batch. BrainSessionFactory supplies continuationExtension(sessionManager) through the existing resource-loader extension seam, first among context_with_system handlers. It appends a native context_edit omission for each still-projected continuation trigger and removes only those messages from the outgoing list. The full-context hook preserves every system declaration's original position and bytes: the ordinary context hook would rebuild system state at the front after removing a message and change the cached prefix. Recall and step handlers therefore receive the original participant sequence. Hidden triggers and their omissions remain internal journal entries; product transcript readers render no new user bubble, and PI's native projection keeps them out of later provider requests, compaction, forks and restored context. If restart interrupts admission before the omission, the same extension omits that retained trigger before the next request. After the awaited native run, the public refreshContext() reconciles runtime messages with the canonical projection. No recovery note, extra provider-visible message, queue, polling or PI 1.1 compatibility path is introduced. Without a continuation, the extension writes nothing; each actual continuation adds one empty hidden entry and one omission through the existing journal barrier.

Restart settlement uses PI's native model projection, not the display transcript. outstandingToolCalls(store, sessionId, scope) in src/brain/persistence.ts reads the active, compaction-aware context after context_edit omissions and content replacements. Restart settlement, delegation-result folding and parked-parent waits select pending, which reads only contributions from provisional source entries. Settled cancelled batches are never answered by recovery: PI fills their missing results at request time. Fork placeholders select context to close all inherited calls, preserving the fork boundary's existing behavior. Only assistant calls PI owes results for count: error and aborted attempts return before tool dispatch; length attempts still receive PI's validation-error results without executing truncated arguments. A later projected result closes its call. settlePartialTurn writes interrupted results only for those outstanding calls and classifies the projected tail, so a restart during retry backoff continues from the preceding valid message. Omitted provisional entries still settle, and retired fragments retain their usage. A fragment at the journal leaf is discarded as before; if native edits or settings follow it, settlement appends a native omission instead, preserving their ancestry and state.

restoreJournalSession in src/brain/journalWriter.ts applies the same call rule to PI's buildSessionProjection on each resident manager. It removes only result contributions whose call no preceding projected assistant introduced, including calls removed by native edits or belonging to failed attempts. buildSessionContext consumes that repaired projection too. Source entries, parent links, the active leaf and PostgreSQL journal rows remain intact: existing orphan recovery outputs heal on read without a migration or an extra journal write. The filter is reapplied after retained-context refresh and native branch/edit changes. With no orphan results it preserves all messages and provenance; its cost is one linear pass over the retained projection. Restart settlement, delegation-result folding, parked-parent waits and fork placeholders consume this single rule; for example, a paused exec that was actually dispatched gets its verify-before-repeat result, while an omitted errored exec attempt gets none.

Delegated orchestration keeps three internal owners. DelegatedSessionService authorizes handles, dispatches turns and holds the parent-child call edge. DelegatedRecovery awaits durable boot claims and owns, deepest-first recovery waits, claim-preserving progress and atomic completion. DelegatedSettlement owns interrupted transcript classification, final-answer recognition and the open call's follow-up/result waits. Its interrupted-child final-answer shortcut reads the chronological tail of the protected latest-turn snapshot, including both provisional assistant work and settled inputs; only trailing display-only chat events are ignored. A helper result committed after a provisional final-looking assistant answer keeps recovery continuable even when its inbox row is already acknowledged. A later partial assistant is retired by the existing native tail preparation, and boot recovery continues the child on that committed input instead of completing it with the older answer. The facade constructs them with the existing store/live registry and only the callbacks each needs; recovery identity reads retain live configuration/runtime getters. An absent recovery delivery/publish callback leaves the durable inbox/progress authoritative. Settlement uses PendingResultDrainOutcome directly from BrainResultDelivery; folded recovery results are acknowledged in the same transcript-settlement transaction. No extra worker, queue, timeout budget or acknowledgement path is introduced.

DelegatedRecovery.recoverClaimedRun checks the child session before transcript settlement or the respawn cap. If the session vanished after its run was claimed, recovery records one idempotent error result with an empty sessionId in the parent's inbox and terminalizes the run atomically; it performs no child journal write. Unexpected recovery errors are logged. When the boot no longer owns a live recovery claim, only a typed DelegationAbortedError may finish as a cancellation; other errors propagate to the recovery coordinator. A still-owned claim follows the existing recovery_required path with an ordinary durable error result that wakes the parent. The child itself is not retried automatically; the parent may verify its last step and use DelegateContinue.

The owner's targeted child Stop (POST /brain/abort → BrainService.abort → DelegatedSessionService.stopForOwner) schedules drainPendingSubagentResults for the durable parent after stopSubagent completes its local or runner-held teardown. The cancellation already occupies the result inbox; the child's later completion returns already-terminal and creates no second result or wake. The Stop response does not wait for the parent's model turn. The existing drain joins concurrent deliveries, batches pending owner results with only the last triggering a turn, and uses settled custom_message entries with details.resultId (settledResultIds) as its only delivery authority. A terminal sub-agent parent still retires orphaned deliveries instead of waking. For example, stopping a background inspection after the owner conversation has answered now lets that conversation report the cancellation. A no-op Stop starts no drain; parent-issued DelegateStop still uses stopSubagent directly and adds no wake. No poller, separate result or acknowledgement path is introduced.

packages/db/src/delegation/payloads.ts owns the pure progress, result and workflow normalizers used by the PostgreSQL DelegationRepository implementation. They retain the existing reject-before-write behavior, field bounds and serialized shapes; relation checks, boot/steer claims and transaction orchestration stay in the store. For example, workflow snapshots validate every node and output artifact before a durable progress update.

plugins/subagent/lib/delegation.mjs owns concrete tool narrowing, principal derivation and dependency context chunks. Delegate and workflow import the same pure functions; workflow registration receives only the current agent-type catalog and host channel handler, without caller-supplied replacements for permission or context rules. Empty explicit tool lists are refused, chunks keep the existing character/count budgets, and empty dependency input adds no context. DependencyResultReferenceError (plugins/subagent/lib/errors.mjs) identifies failures preparing a host-validated dependency Read reference. An ordinary child error, even with identical wording, remains a node execution error in the workflow summary; a reference preparation failure retains the delivery-failure/WorkflowResume path.

Background workflow completion reads its inline summary budget from the required ctx.toolResultInlineBytes() host method. The plugin has no duplicate default; summaries above that budget use the turn-captured output writer, and persistence failures become error completions.

session/shareToolOutput.ts publishes captured child output through publishWorkflowArtifact and awaits its asynchronous registration callback after verifying both new and adopted copies. PluginContext.shareWorkflowOutputs supplies the durable workflow-output registration; without a callback, publication only verifies and returns the artifact. A cleared workflow rejects registration before publication resolves. Rejection removes only the file created by that call; an adopted copy remains for its existing owners. Managed concurrent publishers await the creating call's registration before independently verifying and registering their own references.

Workflow engine coverage is split by behavior: tests/plugins/workflowEngine/execution.test.ts owns execution, liveness, start limits and stop guards; tests/plugins/workflowEngine/ contains definitions, dependency handovers, expansion, snapshots, background delivery and model selection; tests/plugins/workflowRecovery.test.ts owns restart and resume; tests/api/workflowOwnerRoutes.test.ts owns owner-only stop and result routes. Each file calls createWorkflowFixture() from workflowEngine/fixtures.ts once. The fixture owns a unique scratch directory, guest definition map, transcript results, persisted output and mutable task gate, removes its files and clears its state in afterAll, and preserves the harness gate reset. Recovery engines created within one file share that fixture to simulate a reboot over the same journal and child results. Behavior-specific runners stay local to their tests; the real workflow registration and dependency chunker remain the implementation under test.

A delegated call (Delegate, an idle DelegateContinue, a recovery respawn, a workflow node continuation, including a recovered or continued child that had already answered) is done when the child's turn has ended, the child has no live helpers, and no helper result is waiting in its inbox; nothing else settles it. DelegatedSettlement.settleDelegatedReply, called through the DelegatedSessionService facade, owns that definition in the process that ran the turn: the daemon, or the sub-agent runner for a runner-hosted child. A helper is live while the LiveSessionRegistry holds its edge (in-process or mirrored from a runner), while this boot's recovery claims it (recovering with this boot's owner_boot_id), or while a workflow it started is live by the one rule status reads also use, BrainStatusService.workflowLive: a boot resume still owed for the DAG outranks the engine, otherwise the engine's isWorkflowLive answers, and when the engine cannot be asked this boot's recovery claim on the workflow row does. A running run row nothing holds is not live: a dead helper becomes terminal through its own lifecycle (boot recovery, runner exit), never through the age of its row. A background job the child started (the terminal plugin's Bash with run_in_background, backgroundMode: "job" or no mode) is a helper too, and its liveness comes from the process registry of the process that ran the child's turns, which is the one holding the process: a runner-hosted child's jobs live in that runner's registry, an in-daemon child's in the daemon's. While processRegistry.runningJobCountForSession(child) is above zero the call waits on waitForSessionJobsIdle, which the registry settles when the last job exits or is killed. An exited job the child has not been told about is an unprocessed result: the settlement wakes the child inside the same call with a hidden background-jobs-finished system message (backgroundJobsReminder: id, command, exit code; the output stays behind ProcessOutput), sent through sendDelegated with internalSystem exactly like a helper's result, so the handle is still known to that turn. A job the call waited on that leaves the registry without exiting (killed from the process panel or API while no turn of the child ran) is reported the same way as stopped before it finished, with no output, so the call does not settle on the child's interim reply. DelegatedSettlement remembers which exited jobs it reported, so a later call of the same child is not woken for them again; a wake turn that fails fails the call and leaves the job reportable to the next one. A service never holds the call, and neither does a foreground handle. While it waits on jobs the settlement publishes {type:'session', status:'running', detail:'waiting for its background job'} to the call's sink on entry and on every keepalive, which also crosses the runner IPC (DelegatedProgressEvent carries status and detail), so the delegating plugin's row shows the wait and its stall watchdog sees a sign of life. Jobs live only in the process that holds them, so the wait is also durable: when the settlement starts waiting on a child's jobs it sets the child row's jobs_awaited_at marker (JournalRepository.markJobsAwaited), the same kind of row-level marker the shutdown park uses, and clears it once the child has been woken on its ended jobs with none still running, or when the last call of that process on the child ends. A settlement that finds the marker set but not written by its own process (a boot recovery of the call, whether the child had already answered or is respawned, a workflow node continuation, or a later call after a runner died) knows the jobs died with that process: before the call may settle it wakes the child inside the call with the hidden JOBS_ENDED_BY_RESTART_REMINDER (same background-jobs-finished delivery), clears the marker once that turn has run, and the call answers with the child's new reply, never the interim one. A wake turn that fails fails the call and keeps the marker for the child's next call. A job belongs to the call it was started in: DelegatedSettlement.runSettledCall, which both runDelegatedTurn and the daemon's settledCall use, stops the child's still-running jobs in this process (ProcessRegistry.killRunningJobs) when the call ends without settling (its turn or its settlement failed, or a stop), so no job outlives its call and a later continuation, which may run in another process, never meets a job whose handle it cannot see. A stop reaches the jobs first: ChannelSessionService.abortTree kills an aborted sub-agent's running jobs in the process holding them, which releases the wait at once, and the settlement then ends the call as the stop. Because a live job always sits inside an open call, the runner holding it keeps the channel in runningChannels (a release answers busy) and a turn in flight (the pool never reaps it). The delegating plugin runs no job wait of its own: its ctx.processes is the delegating process's registry, which cannot see a runner-hosted child's jobs. There is no time budget; the 30-second keepalive and the 250 ms to 5 s backoff only set the polling cadence. Each helper result wakes the child inside the same call through the ordinary drain, so the turn is tracked by the call's run row, rail and stop. The drain counts an answer with no visible text as processed; only a turn that appended no answer at all, or failed, is an unprocessed result. An empty reply means the result was steered into a running turn only when that send made a new receipt (delegatedSteerForResult returns the newest receipt's id), so an older dropped receipt cannot keep the result pending and send it again. A failed delivery turn fails the call with a message telling the parent to continue the child; the result stays in the inbox for that next call, and TurnRunner arms no retry timer for a sub-agent parent, so no turn ever runs on a child outside a call. A stop that ends a delivery turn (DelegationAbortedError) is not a failed attempt: the drain leaves the row's count alone, stops, and hands the error to the settlement, which ends the call as the stop. A sub-agent whose every call is terminal, finished or stopped by DelegateStop or its parent's stop, is never woken either: the result drain retires its pending results with the boot sweep's rule, discardOrphanedDeliveries scoped to that parent, so a torn-down helper's result runs no turn. Owner conversations have no run row and are unaffected. The settlement can still end in that failure or a stop, never by giving up. Helpers keep the gap closed from their side: the Delegate plugin records a background result (deliverCompletion) before it publishes the terminal progress row that ends the helper's liveness, and a finished background workflow stays live until its summary has been handed to the result sink. Settlement asks the liveness rule only about the workflow rows that can still be live (openWorkflows): a running row, or a background run whose summary_owed flag is still set; a running background snapshot raises the flag and enqueuing the summary into the result inbox clears it. The engine stops reporting a DAG live once it is done handing the summary over, including when the sink refused it, so a row that still owes its summary while the engine says the DAG is gone is a lost result: the settlement fails the call with that, like a failed delivery turn, and writes the debt off (writeOffWorkflowSummary) so the child's next call is not failed for it again.

A follow-up always lands inside a tracked delegated call or starts one. DelegatedSessionService.continueSubagent picks the route from the child's state: no open call means an idle continuation (its own tracked call, {status:'reply'}); a turn streaming in the daemon takes a local steer; otherwise the committed brain_delegated_steers row goes to the runner holding the child, which steers it into its running turn or, when the child is between turns inside a call the runner settles, hands it to DelegatedSessionService.followUpSettlingChild; a call settling in the daemon takes the same hand-off directly. Each of these returns {status:'queued', messageId}, and the answer arrives once, through the original call. The hand-off runs the follow-up as the child's next turn inside the open call, under the caller's per-turn tool denies (ChannelSendOpts.extraDeny; over the runner IPC the steer frame's deny, whose malformed value refuses the frame) and, in the runner, under the open call's host-RPC turn. The receipt id is that turn's admission echoId (ChannelSendOpts.delegatedSteerId), so the PI journal append confirms it like any other follow-up; a turn that fails drops the receipt with the ordinary notice and fails the call. Only the process running the settlement can see that state, which is why the runner, not the daemon, decides. A child that is active but neither steerable nor settling (queued for a runner slot, starting up) refuses with a retry hint. A child this boot's restart recovery has claimed (recovering under this boot's owner_boot_id) but not yet opened a call on, because the recovery first waits for the recovery of the child's own sub-agents, refuses too: the recovery delivers its answer, and an idle continuation in that window would run beside the recovery's continuation on one transcript and park the run as recovery_required with cannot continue a turn that is already running. A runner that holds no open call for the child drops the row with the ordinary notice. A channel turn that ends drops only the receipts it queued itself (hidden results, and text rows in its own queue mirror); a row in transit to it over runner IPC or waiting for its lock is resolved by its own transport. Example: a lead sub-agent waits on three workers, and its parent sends "stop waiting and report"; the lead runs that as its next turn, stops the workers, and the original delegation delivers the report.

Delegated progress carries optional agentType, the chosen type's catalog name, separately from the task's name. The current catalog has no separate display label. Untyped and fork delegations omit it. applySubagentUpdate preserves the recorded type on later continuations; the existing run state JSON validates and stores it without a schema migration. Workflow nodes carry the same optional display field in their durable snapshots. A child's fixed-session stream includes session.delegation: { agentType?, status }, projected from its parent's existing run or workflow rows, so reopened CLI and web views use the shared childReadOnlyText narrative without a catalog fetch or model call. The same projection publishes a display-only delegation frame on the child's existing stream whenever the parent's persisted subagent or workflow progress changes. Only the child's own type and outcome are forwarded, never parent history or sibling data. Runner taps retain the daemon's existing control lane for these frames, and the daemon overlays the same authoritative delegation projection on runner snapshots. These display frames are broadcast, not journaled, because the atomic snapshot already carries their current state. Turn idle, tool activity and provider errors do not decide the delegation outcome: a child can remain running while waiting for nested children or background jobs. Ordinary conversations omit that metadata. This is display-only and changes no permissions or tool execution.

A delegated child has one name for its whole life: the one it was first delegated under (Delegate's name, the name derived from its original task, or its workflow node's name). packages/plugin-shared/subagentName.mjs supplies the pure label derivation to the Subagent plugin and store readers, preserving the five-word and forty-character bounds without changing saved names. DelegationReads.originalName (packages/db/src/delegation/reads.ts) is the durable origin-selection rule, and DelegationRepository.upsertSubagentRun in packages/db/src/delegation/runs.ts stamps its answer onto each new progress snapshot inside the parent/child transaction. A DelegateContinue therefore retains the child's original label, also after a restart. PostgreSQL reads workflow state for the parent in row order and selects the matching child's task; otherwise it reads that child's original run states in row order and derives the name with the same shared helper. Parent/child ownership is checked before returning the public delegatedChildName result. Running reminders, live anchors and explicit history continue to consume the same repository projections.

Conversation display and control

ConversationReset.clearConversation serializes /clear through the existing conversation locks and busy checks. JournalRepository.clearSessionHistory immediately removes the active branch, session spend and small context sidecars while recording journal/provider sequence bounds in the existing deletion marker. The conversation id, model, mode, execution boundary and persistent goal survive. Before its admission transaction, BrainSessionFactory.create awaits the existing managedArtifacts.drainSessionDeletion(database, sessionId) seam. Whole-session deletion completes its bounded journal and provider-request drain before the id can be reused. A clear starts that same drain in the background and lets the same conversation respawn without waiting for old bodies or capture payloads to be reclaimed. Cleanup failures during whole-session deletion reject admission. The journal repository's drainSessionDeletion method only reads the marker; the core seam owns completion. New entries live above the marker; raw readers and late tool-trace settlement cannot pick up old entries with reused PI or tool-call ids. Trailing-turn rewind keeps its suffix-only contract through the awaited JournalRepository.rewindJournalTail transaction and cannot restore a retired leaf. A rewind the person can act on (compacting, settling, queued, pending question, active goal, unsettled turn, or a sub-agent, workflow or undelivered result started by the removed reply) is refused with the one RewindRefusedError from elowen-db, which ConversationReset's live pre-checks and the journal transaction's durable re-checks both throw; sessionRouteError answers it as an unlogged 409 { error, code } with a stable rewind_* code that the web maps to a localized message. Every other rewind failure stays a logged 500. Marker semantics, capture reference preservation and reassignment limits are documented in store persistence.

Native ShowView carries model-authored html, optional css/js, a required plain-text summary, and optional waitForAction. The strict boundary caps the complete UTF-8 document at 48 KiB and each turn at three views. Successful results use details.view/viewId; only this core tool produces a view event/segment. Direct events, message hydration and nested ToolTrace records use that same document. CLI, HTML export and all platform delivery paths render only the summary. Waiting calls are refused on non-web turns rather than pretending the user can click. Ordinary native account grants apply; model-only interaction exposure keeps the definition immediate; no grant migration or plugin UI API bump is introduced. Documents ride the existing native call/result history; no prompt block is injected.

Waiting views extend ElicitationRegistry rather than adding another waiter. brain_view_waits stores one pending document per session, its call id, optional code-mode reporting call id and a write-once answer. The schema's existing additive boot path creates the table and index without a numbered data migration. Parking writes the restart marker in the same transaction before publishing. The view has no timer; shutdown leaves it parked and boot recovery skips provider continuation until an answer exists. Atomic snapshots restore the pending view even without a live replay. POST /brain/view-answer validates bounded text and session ownership, persists before settlement, and resolves the original tool Promise or the existing interrupted-turn recovery path. Recovery answers the original call with a real native journal tool result, not a user message, and deletes the wait after journaling. A resident PI session refreshes through the existing journal proxy before continuing. New owner input is refused as busy while a durable view row remains, including after restart when PI is not streaming and after a click until the answer is journaled; admission preserves the original recovery marker and never opens another provider turn over the unresolved call. Native append and late trace-replacement boundaries retire answered wait rows only after their result is durable. Stop also cancels a recovered wait without a live session and settles its outstanding native call before clearing the park marker and paused queue. Stop, replacement and deletion cancel through the existing elicitation lifecycle; moving or detaching a viewer preserves a view wait. Ordinary question/approval behavior is unchanged. View waits mark blocked/question control without exposing an empty AskUserQuestion picker to text-only clients.

ShowView is a model-only interaction boundary, like AskUserQuestion and ExitPlanMode: it stays direct to PI, never enters the script catalog or deferred set, and never counts toward the automatic deferral threshold. Composition appends its definition after the existing core groups without changing any pre-existing definition or its relative order. Code-mode cells therefore cannot open a view wait or bind its result to an already-yielded wrapper. ShowView passes its execution AbortSignal to the same registry; cancelling a native call closes that view and settles the invocation without a click. Parking for a daemon restart does not abort the signal and retains the durable wait. beginDrain calls ElicitationRegistry.cancelAll('daemon restarting'), which rejects ordinary questions and leaves parked views in place. Non-waiting views may run local controls, but their action bridge sends nothing. A sample ShowView({html:'<button id="pick">Choose</button>',js:'document.querySelector("#pick").onclick=()=>elowen.action("chosen")',summary:'Choose a variant',waitForAction:true}) returns the action only to the agent.

The pure browser-safe transcript projection is src/brain/transcript.ts. The architecture guard names the exact pure display dependency graph and rejects daemon runtime or Node builtin dependencies inside it; erased contract imports remain allowed. Web bundles a byte-identical importless mirror pinned in tests/contract/webMirrorContracts.test.ts, with erased wire types only across the source boundary; CLI uses TranscriptModel for targeted indexing, revision journals and terminal painting, not a second semantic reducer. Browser-only canonical exports carry the existing externalConsumer tag because Knip cannot follow byte-pinned mirror consumption; no module-wide dead-code exemption is used. Complete BrainEvent payloads preserve attachment provenance, command/output associations, model-step boundaries and history identity. Prepending replaces synthetic running-work anchors only when every real anchor is present and carries newer progress into those rows. Unrelated token frames retain child-panel projection identity. The shared workflow view preserves the live event's background flag so detached work never becomes foreground work after projection.

settleTail(turns) replaces only a streaming assistant tail, clears its composing metadata and returns whether it changed. The marker, user and step folds use it before another turn displaces the tail; TranscriptModel uses the same function before appendLocalTurn and publishes exactly one tail mutation when changed. Empty, non-assistant and settled tails retain identity and return false. The operation visits only the tail and mutates the supplied turn array; it does not end overall thinking activity or persist journal data. transcriptFoldParity checks the browser fold and indexed TranscriptModel against the immutable canonical projection. ChatTurn.id is an optional source journal identity for stable browser keys and prepend deduplication. Durable user/event ids may arrive live; live assistant turns receive no stored id during settlement, and only assistant turns constructed from stored history carry one. Plans are read from ToolItem through submittedPlanOf, not inferred from assistant text. TranscriptModel exposes actual turns through turnAt and no longer maintains a parallel last-assistant-text cache.

BrainStatusService projects pendingAsk and pendingPlan alongside workMode from the sole TranscriptRepository.conversationMode authority. The newest-user-turn journal policy defines pending-plan lifetime; a newer user turn retires the earlier decision. Status and atomic stream snapshots serve that same authority. Clients do not reconstruct it from displayed history, and implementation selects Build through the serialized switch before admission, restoring Plan if admission fails. Control and sidecar revisions fence status requests started before newer streamed state, including cards and queue. Web control reads are ordered by issuance and target session. CLI questions are retained per viewed session; a read-only child cannot answer or dismiss the parent's question, which is restored on return. Plan decisions wait for parent work to settle. Resync reconnects for an atomic snapshot instead of overwriting a running turn with durable-only history.

Authorized status/snapshot composition uses one execution projection for projectRef and project location. A delegated placement overrides the parent/root directory; absent identity stays absent while a child loads. Host paths and branch reads remain constrained by the viewer policy. project.branch is absent, never null or a placeholder, when there is no branch to name; a local conversation's branch is read on its computer while it is connected (see the branch paragraph in daemon lifecycle). The server's switchable-target listing supplies exact executionRef, icon and account order to web, CLI and platform pickers. Browser project registrations are not execution authority, and selection revalidates the chosen target.

The three-view budget counts admitted displays in the current policy turn. Without a view elicitor, a waiting call is refused before reservation; a non-waiting call returns its summary and view details. An elicitor refusal with no details.view releases its reservation. Once a waiting view has been displayed, later native-call cancellation still consumes that slot. Owner CLI turns use the same native ShowView tool and refuse waiting through the turn-bound elicitor, so they can recover with a non-waiting view in the same turn. No separate quota store or error-retry layer is introduced.

beginDrain calls ElicitationRegistry.cancelAll('daemon restarting'). Global cancellation rejects ordinary questions and queued approvals but always leaves native view promises and durable wait rows intact, regardless of the diagnostic reason. The method has no view-lifecycle option. Explicit stop, replacement and session deletion use the existing session-specific cancellation path when a view must be settled and removed. With no parked views, global cancellation only affects ordinary elicitation. Preserved views have no timeout; the authenticated web conversation answers them through POST /brain/view-answer, and interrupted-turn recovery reads the view-wait rows once before settling an answered call, refreshing the journal-backed session and continuing.

Direct ShowView waits are durable and recover through ElicitationRegistry. View waits carry no wrapper reporting column.

Fast mode: capability and choice

Two questions, two owners. Whether a route can take Fast at all is resolveFastModeRoute in src/brain/fastMode.ts; it is the only place that classifies a configured provider and its exact request model, and it feeds the fastAvailable flag of the model catalog and of a live conversation. Whether the account wants Fast for a model is fastModels, a per-user list of providerId/model keys in user_settings (UserSettingStore.fastModels / setFastModel; cli-settings field fastModels), the same key convention as autoCompactAtByModel. Fast is in effect for a provider request only when both agree. LiveSessionSpawner composes them per request in fastRouteFor, keyed by the request's own model (a compaction call on another model is judged on that model), for the account driving the request, and hands the result to wrapFastModeRuntime; the account list is read before every call, so a change reaches live conversations without a respawn. BrainStatusView.fast means Fast is active for the conversation's current model (listed and supported) and fastAvailable that it is supported; every statusline reads fast, so an unsupported model never shows FAST. /fast (BrainService.setFast, and setAccountFast for a room) flips only the current model's key and stores nothing for a model without a route.

resolveFastModeRoute splits the decision in two. The configured route and wire API decide the TRANSPORT, in code: the ChatGPT OAuth account (openai-codex-responses, without its Spark and image models), the official OpenAI endpoint over Responses or Completions, an Azure OpenAI Responses endpoint, and the official Anthropic API endpoint with an API key. A relay never qualifies, and neither does a Claude subscription (OAuth), whatever credentials it holds. The MODEL and its wire come from data: fastModelFor (src/brain/fastModelOverlay.ts) answers "does <catalog>/<model> list a Fast mode, and with which request body and headers", and the route carries exactly that body and those headers (applyFastModePayload merges the body into the request; the headers are merged token by token into any header the request already has, which is how Anthropic's beta flag joins the existing one). The data is the experimental.modes.fast block of the models.dev catalog (service_tier: "priority" for OpenAI, speed: "fast" plus the anthropic-beta flag for Anthropic), reduced by reduceFastModels (src/brain/fastModelCatalog.ts, the only place that reads that document) to OpenAI and Anthropic models, the only catalogs a transport above reaches. Prices are not stored: PI prices OpenAI's priority tier itself, and Anthropic's Fast rate stays the fixed 2x of fastModeCostModel (models.dev lists every Anthropic Fast row at that factor). Codex OAuth, official OpenAI and Azure all read the openai rows; Azure has no rows of its own, so a deployment is assumed to be named after the OpenAI model it serves (a -YYYY-MM-DD suffix is ignored). A deployment with a custom name is therefore not offered Fast.

The data has two layers behind that one lookup. The generated baseline MODEL_FAST_CATALOG in modelCapabilityData.ts (refreshed with npm run models:refresh, which reads models.dev with the same reduceFastModels) is what a fresh install and an offline host know. The runtime overlay fast-models.json in the brain directory is written by the background worker in the same pass that refreshes PI's catalog (boot, every four hours, a forced POST /brain/models/refresh), by the same temp-file-and-rename write, under the same 60 s limit and kill on pause; the document is fetched, reduced and dropped inside the worker and only the reduction is stored. When an overlay exists it is the whole answer, so a model models.dev stopped listing stops being offered; the baseline answers only while there is none. A fetch that fails, is not JSON, or lists no Fast model at all keeps the previous overlay and is reported once under the provider name models.dev in the pass's endpointErrors, never as a catalog failure. The daemon re-reads the file after a pass that rewrote it (reloadFastModelOverlay), and every consumer asks fastModelFor per call, so a new model reaches the Account row, the fastAvailable flags and a live conversation's next request without a restart. Whether the provider's backend really accepts a model's Fast body is what models.dev claims, not something Elowen probes.

Fast mode is a per-model key set, not an account-wide switch. A chat room's /fast keys the model the room really runs (its live session, else the invoking account's own preference through the spawner's selection rule), because the chat plugins' own catalog pick is only the instance default; every /fast toggle bumps the cli settings revision so a stale Account tab conflicts instead of overwriting it.

Conversation-role writes share isOfferedChatModel in src/brain/models.ts. PUT /config and PATCH /users/defaults build the same runtime provider view with brainConfigFromElowen and check the selected pair against chatModelCatalog(listBrainModels(...)). The check reads cached catalogs without network requests, includes connected OAuth accounts without a stored provider entry, and refuses a missing or image-only model with 400 even when a provider's manual model list is empty. Config writes also pass connectedOAuthAccounts to the store guard. Account-default writes keep their settings, model role and grants in one transaction; an InvalidDefaultModelError answers 400 after rollback.

Account tool authority

users.allowed_tools is the single account grant for both native and plugin tools. Core tool factories consume names from src/shared/coreToolNames.ts; builtinToolMetas() composes their real definitions and a migration test compares both catalogues. Add a core tool there and in its factory together. src/shared/toolGrants.ts owns exact/prefix matching and account-grant validation; only explicit MCP family patterns are accepted, so a newly composed builtin cannot be picked up by a broad saved prefix. With no account grant, no grantable tool is exposed. Infrastructure envelopes (ToolSearch, code-mode exec and wait) are not grants because discovery and nested calls still pass through the same visibility and execute gates. toolAuthorityForUser resolves a fresh account grant, toolPermitted is the one allow/deny predicate, visibleToolNames and ToolSearch filter the model's catalogue, and the universal gateSessionTool execute wrapper rechecks every call including code-mode nested calls. Its plugin ownership/hook wrapper does not repeat the policy check; personal ownership and plugin vetoes narrow the already-permitted call. Administrators use the same account grant. Plugin grants, access.denyTools from platform adapters, delegated captured restrictions and granular permission rules remain independent narrowing. Version 36 resolves old wildcard grants from the enabled manifest catalogue using the same plugin roots as the daemon's runtime, adds existing built-ins to human accounts, adds none to chatbots, subtracts old denials, and drops the deny column atomically. Administrators' stored grants are made finite the same way. Version 37 grants each existing administrator all grantable built-ins and tools from enabled plugin manifests, the explicit mcp__* family for dynamic connected tools, and every enabled user-grantable plugin, using the same bundled-first roots. Missing or unreadable manifests contribute nothing and never abort boot. Model lists were open then, so v37 left allowed_execs alone. Version 52 closes them: src/shared/execs.ts owns the grant: an empty list allows no model, and the single reserved entry ALL_MODELS_GRANT (*), defined in src/shared/modelGrantRule.ts and re-exported by execs.ts, allows every model the installation offers, now and later; it is valid only as the whole list, and grantsAllModels (same file) reads it only from a list that is exactly that entry. The column is CSV, so isStorableModelGrantEntry refuses any entry holding MODEL_GRANT_SEPARATOR: isValidModelGrant applies it at the API, the store writer, the plugin models field and the migrations, and readModelGrantColumn returns the all-models grant only when the whole stored value is *, so no stored entry can read back as a wildcard. Microsoft provisioning grants only the named known models from a template that mixes * with others. The migration gives every account whose stored column holds no entry at all the all-models grant, so nobody's access changed at the cut. Those historical source migrations judged the raw column through the historical source migration's frozen CSV or JSON split, never the live parser: a list of only retired specs must not read as empty and gain every model. Version 53 retires the external agent programs. A model identity is brain-only: ExecRef is { provider, model }, parseExecRef reads the canonical <provider>/<model> and the legacy elowen: and elowen| spellings, and anything else, such as sonnet, codex:gpt-5.5 or opencode:vendor/model (every removed <program>: prefix is refused before the slash is read), names no model. isOfferableExec is the one offered check and isExecAllowedForUser the one account check. The migration rewrites each stored list to its canonical brain entries and drops the rest, including an entry holding the separator (never split) and a * that was not the whole list; a list left with nothing stays empty, which now means no model, so a sonnet,opus grant ends with no model access rather than every model. There is no instance-level model allow-list: the Settings catalog shows what providers offer and the personal grant narrows it, while the retired allowedExecs and defaults config keys are dropped on load and refused by PUT /config. fullAdministratorGrant carries the all-models grant for the first administrator; every later account starts with an empty list. The web mirrors the token in web/lib/modelGrant.ts, pinned to the AllModelsGrant wire type. Future built-ins and plugins still need explicit grants; future dynamic MCP tools match the migrated explicit family grant. With no legacy column it does nothing; a manifest it cannot read contributes no tools, which narrows the grant rather than widening it; only an administrator legacy denial aborts, because the admin bypass could not keep it. After migration the old standalone allow-list script is removed. For example, adding a new MemorySearch-like factory leaves it unavailable to all existing accounts, including administrators, until explicitly granted.

The host skillCatalog.visibleEntries({ invocation: 'user' }) control uses the same toolAuthorityForUser resolver before exposing manually invocable skills. It reads the authenticated account fresh for each catalog request, requires a registered and plugin-granted SkillLoad, and intersects the account authority with currentToolPolicy() so captured turn restrictions still narrow visibility. An absent account or missing grant returns an empty catalog. This is a catalog projection, not a new execution grant; actual loading retains its existing execution gates. Resolving this view scans the registered tools and the account's current plugin grants, without network access or persistence writes.

The model-reference cascade is split between the durable offer codec (packages/db/src/config/codecs/store/modelReferenceCascade.ts), ConfigStore in packages/db/src/config/configStore.ts, and account writes in packages/db/src/providerConnections/modelReferences.ts. It removes every stored choice that names a model the installation no longer offers. Reading already refuses such a model, so the reference is inert, but an inert entry still breaks writes: the Users modal sends the held grant plus the newly ticked model, and PATCH /users/:id refuses the whole list for the stale entry. ConfigRepository.update runs the settings and account cascade on the same PostgreSQL transaction executor when a patch carries brain.providers; settings and affected account resources commit or roll back together. The account pruner comes from the bound ProviderConnectionsRepository, not an injected store callback. ConfigRepository.sweepModelReferences also runs once at daemon boot, so older references are cleaned on the next update. The boot sweep judges only a provider list it can read: a missing or unparseable settings row is skipped (read() would turn it into defaults with no providers), and a stored entry this build's sanitizer drops, such as a provider type from a newer release after a rollback, keeps its id as a catalog. What counts as offered is durable configuration only (durableModelOffer): every stored provider entry, where an empty models list is a live catalog and keeps everything, plus every OAUTH_BUILTIN id no stored entry claims, also as a catalog; a revoked or disconnected credential is never read as a deletion. Covered: brain.modelOverrides keys and runtime.hostedToolSearch probes are dropped; brain.defaultModel becomes null and is reseeded by initializeBrainDefaultModel (after PUT /config, and at boot when the sweep cleared it); the categorization and dashboard digest roles are cleared to unset; manifest-declared model fields (PluginModelRefFields, image fields excluded) lose the reference in instance and per-account plugin config (pruneModelRefSlice); users.allowed_execs loses the entry (the all-models grant is never touched, and a list pruned to empty grants no model, so it only narrows); The bound account pruner clears chat, vision and compaction pairs whole and drops project pins, per-model compaction thresholds and Fast entries in user_settings; it also removes stale model references from account-preset grants. Each changed account resource advances its revision, so a stale form gets a conflict instead of writing the reference back. Not covered, on purpose: the embedding and voice blocks (validated by their own write paths, not brain choices), live sessions and workflow state (runtime state that falls back on its own), files outside the database (agent definitions, plugin-owned data such as cron jobs) and every history or usage table. With nothing stale a pass writes nothing; with something removed it logs one config info line with the counts. Cost: one scan of users, account_presets, the model keys of user_settings and the declared plugins' user_plugin_config rows per provider save and per boot. Example: removing mimo-v2.6-pro from a provider's model list removes opencode/mimo-v2.6-pro from every account that held it, so the next save of that account's models succeeds.

PI owns parsing and validation of model-authored tool arguments. Its provider stream repairs invalid JSON string escapes while preserving the remaining fields; an incomplete final JSON object carries argumentsError, so native tool validation returns an error to the model without executing that call. Elowen does not rewrite the provider stop reason or install a recovery wrapper. tests/brain/piToolArguments.test.ts exercises both cases through the real Anthropic SSE parser and a native session tool loop; the repaired call still passes through the ordinary tool gates. This adds no inference request beyond the normal continuation after tool results.

composeSessionTools classifies AskUserQuestion and ExitPlanMode as model-only before resolving deferral. They stay directly callable by the model and do not enter deferral candidates, automatic thresholds or the code-mode catalog. Script exclusion uses exposure metadata rather than a second name filter. Without code mode, their ordinary direct interaction behavior is unchanged; classification adds no timers or inference calls.

Local tool discovery uses PI's public createToolSearchExtension(). src/brain/toolSearch/toolSearchTool.ts restricts its catalog and activation API to the current sender's composed, gated and schema-capped tools. Registration keeps the durable name ToolSearch and passes the native definition through gateSessionTool. PI owns BM25 ranking and schema declarations; only select: is intercepted, for exact loading of permitted names. deferralPolicy.ts remains the single policy owner, prompt-side previews live in src/brain/prompt/toolCatalog.ts, and local search uses no embeddings. initialActiveToolNames restricts the SDK's explicit initial loadout. activation.ts projects successful journal loads across sender changes before the current sender's visibility filter is applied; the pre-upgrade journal regression documents why PI's latest declared set alone cannot replace it.

Automatic turn-start and live recall require MemorySearch in that same account grant; post-turn curation requires MemoryAdd. The stored memory toggles may disable a granted operation but cannot turn an ungranted operation on. With no grant, neither operation runs. For example, a chatbot retaining only ShareImage and ShareFile keeps those tools and receives no automatic memory; an administrator needs the same grant.

Agent access to the daemon API

An interactive Always allow choice runs the consented call even if rule persistence fails. resolveAsk logs that failure at the approval seam and explicitly identifies the approval as call-only; no durable rule is claimed.

The permission gate passes the asking call's AbortSignal through resolveAsk and TurnPermissions.requestApproval to ElicitationRegistry.ask, as ShowView does for views. Aborting it withdraws the parked or queued approval like a session cancellation (ask_resolved with reason cancelled), and the call is refused without running the tool. A code-mode nested call is the real case: terminating its cell aborts the call while its approval waits, and a later click can no longer run a tool for a script that is gone. Direct calls see the same signal from PI's run, whose Stop already cancels the session's approvals.

One seam: the core ElowenApi tool (src/brain/tools/elowenApiTool.ts). It is how a turn reaches any daemon endpoint without a typed tool, as the account the turn acts for (currentAccountUserId()) and never wider than the turn.

  • Request shape. src/shared/elowenRequest.ts is the one definition of { method, path, body? }, where body is a JSON object (an untyped body let models send it as a JSON string, which the routes read as no fields); the MCP elowen_request tool registers the same zod shape and the host RPC parser validates with it.
  • Authority. resolveElowenApiAuthority (src/brain/elowenApi.ts) refuses an unlinked sender, a chatbot account, a turn whose owner flag differs from the account's admin flag, and a turn reaching fewer projects than the account's own resolvePolicy answer: the HTTP API authorizes per account and cannot express that narrowing. Plan mode and a read-only delegated child (readOnlyOrigin) are expressible and yield a read-only credential. Tool grants, deny lists and permission rules apply because it is an ordinary tool composed by composeSessionTools; it is in READ_ONLY_AGENT_TOOLS, and therefore admitted in plan mode, because its API access is read-only there.
  • Credential. AgentCredentials (src/api/agentCredentials.ts) mints one ela_ value in daemon memory per request; runElowenApiRequest sends the request to the daemon's own URL through callElowenApi and revokes the value in finally. The bearer gate (src/api/auth.ts) resolves it before any public-route or setup exemption, refuses non-GET/HEAD on a read-only grant, and the session-only gate (isSessionOnly, src/api/middleware.ts) refuses it per credential scope: everything a personal API token is refused, plus login, SSO, impersonation, account management under /users and the account's own permission rules. One table carries both scopes; the catalog reads the same predicate.
  • Runner. A forked sub-agent runner cannot mint: its elowenApi dependency sends ELOWEN_API_RPC over host RPC. hostRpcRefusal re-checks the tool against the child's policy; the daemon's createHostRpc (src/daemon/bootstrap.ts) re-checks liveness and the tool against the child's durable DelegatedExecutionScope, and resolves authority from it (delegatedElowenApiBoundary). resolveElowenApiAuthority reads the account fresh on every call, in-daemon or runner: ElowenApiAccounts.toolAuthority (toolAuthorityForUser) must still grant the tool, and the administrator flag and project reach must still match, so the effective check is the child's durable scope intersected with the account as it is now.
  • Discovery failures. Non-OK catalog responses preserve the HTTP status in text and details.status and set isError: true. Successful catalog bodies remain unchanged; the request action retains its explicit status envelope.
  • Discovery. The tool's search and describe actions read GET /api-catalog/search and GET /api-catalog/describe (src/api/routes/apiCatalog.ts, src/api/routeCatalog.ts). The catalog is derived per request from the Hono router's app.routes and the live plugin registry's apiRoutes/rootApiRoutes, so a new route or a plugin enable/disable changes it with no edit. A core route's summary and optional body schema come from the one declareRoute({ summary, body? }) call it registers with (src/api/validation.ts); parseBody throws when a handler parses a schema its route did not declare, so the two cannot drift. Results are filtered to the caller: admin-only plugin routes, ungranted plugins, session-only routes for token callers, and mutating methods for a read-only credential are left out. A core route's admin gate lives inside its handler, so the catalog cannot see it and the route answers 403 when called; describe says so in a note whenever it returns a core route. GET and HEAD handlers must not change state, because a read-only credential reaches them: the dashboard digest, for example, starts on POST /dash/recap while GET /dash/recap only reads and reports refreshDue.
  • Agent shells. PluginContext.agentShellEnvironment() stamps ELOWEN_AGENT_SHELL on every agent launch (Sandbox execution service, Terminal fallback), from unrestrictedShellAccount: account:<id> only for the account's own non-automated chat with an unrestricted, non-Plan boundary. src/cli/agentShell.ts makes the CLI refuse otherwise, and for account:<id> verifies the cached login with GET /auth/me first.

Absent wiring: without BrainDeps.elowenApi the tool answers that the API is unreachable; without ServerDeps.agentCredentials no ela_ bearer authenticates. Cost: one loopback HTTP request per call; results are ordinary tool results, so large responses spill like any other.

Prompt cache prefix identity and historical breakpoints

src/brain/session/promptCanonical.ts owns hashCanonical(value) for provider payloads and segments. It recursively removes cache_control and folds a message's string content into one text block, preserving the existing JSON field order and SHA-256 digests. cacheBreakpoints uses it for cumulative prefix identity. Hashing does not depend on enabling the monitor. Its weak cache beside each live provider object retains a detached canonical structure and digest and compares that structure again on each call, including field order and array length. Prefix tests use the internal canonicalPayload projection.

cacheWatch keeps session-local weak identity caches for message/tool digests and tool definition digests. Reusing an object does not traverse or serialize its body again; changed content must arrive as a replacement object. Moving cache markers remains neutral. The catalog digest hashes the ordered tool digests, including tools beyond the labeled 256-tool prefix. History remains bounded to 512 segments. On the first message hash, the image projection preserves each unchanged object and array, so messages without inline images reuse the canonical digest already computed by the trailing breakpoint without another JSON serialization. Inline image data becomes a diagnostic fingerprint of decoded byte length, MIME type and the first 128 base64 characters. Anthropic base64 sources and Responses image data URLs use this same bounded representation. Equal-size images sharing that prefix may collide; this diagnostic approximation never affects actual cache breakpoints, media content addresses or provider payloads.

createCachePayloadMonitor hashes the bounded diagnostic segments and correlates a sent Responses payload with the exact object seen by before_provider_request through a weak reference. The wrapResponsesChaining caller passes the payload returned by the extension chain to sentPayload; equal-content requests are distinct, and consumed, discarded or cleared snapshots cannot be annotated. Correlation does not hash the complete history again. The pending queue and 512-segment tracking limit remain bounded. An immediate tool with no own defer_loading field uses its ordinary digest as its definition digest, avoiding a second identical schema hash. Explicit loading flags still require the existing definition projection without that field. Without a monitor the wrapper only prepares the continuation. createResponsesChain uses native JSON normalization for each new input structure, replacing string values with per-snapshot indexes during serialization and restoring the original immutable strings with a JSON reviver. Large text and image values are never encoded and decoded merely to detach the snapshot. Native toJSON, sparse-array, undefined and numeric semantics remain intact. Invariant fields retain their existing JSON snapshot. The detached input is compared with the prior confirmed response, while outgoing requests retain their original input objects or strict continuation suffix. Failed responses and native compaction still clear continuation state.

The existing slowSync seam attributes cache snapshots, Responses continuation preparation, native session projection and turn-context assembly. contextSnapshotOf projects session messages once and reuses them for reported tokens and returned context. These paths use the retained PI context; archived journal size is not a measure of replayed context. turnContextRecords caches the native projection, parsed context records and read-only composed view per live SessionManager. Leaf-entry identity and entry count invalidate on append, branch changes, compaction and resident-manager refresh. Extensions receive fresh message shells and still execute on every provider step. The existing cold history owner calls invalidateSessionContext(manager) after each durable image, record or tool-result rewrite, before mutating the live entries in that synchronous step. With no changes, estimates and diagnostics reuse the same projection; cached read results must be treated as read-only. The existing contextRecords(entries) and stepReminderRecords(entries) accessors also keep weak per-entry parsed snapshots. A newly copied branch array reuses validation of unchanged native records; a new append validates only its new records. The cold invalidator clears both parsed and composed snapshots. factory step-provider cadence and instructionActivation module identity selection use these same accessors, so neither reparses every active record on repeated steps. Their ordered branch scans remain linear; no parallel journal authority or instruction ledger is added.

Mid-turn producers are policy descriptors passed to session/contextRecordDriver.ts::installContextRecordPolicy. Live recall, plugin step reminders and file-access instructions retain their registration order and resolve on the same context_with_system boundary. Each policy resolves its current pass and emits typed frame data: memory selection with count and remaining UTF-8 byte budget, step provider parts with tool/activity cadence and provider identities, whole instruction modules with path/hash identities, or an already frozen deferred recall. The driver owns canonical boundary tracking, default tool-call cadence, envelope rendering, escaping and clipping, append-only anchoring, and the commit through attachContextRecord. The journal keeps the rendered bytes; replay never invokes a provider or renderer again. Without a contribution the driver leaves both transcript and journal untouched. Policies receive only the consumed changed and steering boundary facts; compaction remains a local input to those decisions. The driver owns step-frame cadence types and consumes only the instruction fields it renders. Delivery identity accepts module path/hash structurally, without depending on file loading. attachContextRecord fixes mid-turn placement to after-message, so policies do not pass a redundant placement option. The renderer and its byte limit are imported from the driver directly.

session/contextDelivery.ts owns retained delivery identity: memory id/body versions from records and admitted tool output, the latest instruction hash per relative path, activity reminder positions from active or body-free retired step markers, and SHA-256 over exact rendered ambient bytes. All retention uses the unchanged activeContextEntries cut. Identical frozen content at the same anchor is admitted once across producers; later anchors can carry a new cadence reminder with the same body. The order counter remains the maximum existing order at that anchor plus one. Cold stripping and lead/trail wire placements remain owned by turnContextRecords.ts.

A plugin registered through registerText('step-context', fn), such as a task-list reminder, is a real policy consumer. Its provider still runs only when its tool-call or activity cadence is due. Its escaped body is capped at 1,024 UTF-8 bytes, excluding the envelope, with represented provider keys limited to the included parts. Live recall keeps its existing per-turn byte budget and nonblocking retrieval slot. Whole instruction modules retain their resolver's per-file size limit and original trusted envelope.

An empty or rejected step provider adds no new record and leaves previously frozen records visible; an empty answer still consumes the default cadence. A failed step append rolls cadence back. Live recall skips rejected or abandoned retrievals, frees their slot, and retains earlier deliveries; deferred recall keeps its existing refresh and one-refill rules. An instruction diagnostic or empty resolution appends nothing. An instruction resolver rejection still escapes the hook after the batch was drained; only an append failure requeues that batch. None of these paths rerenders or withdraws a previously frozen record. Memory delivery accounting remains after successful admission, through memoryBlock.commit or the live policy's injection callback, never on retrieval. Slow intervals are diagnostic attribution, not evidence that any one of these phases caused a previously observed event-loop stall.

anthropicCacheBlocks.ts::isAnthropicCacheableHistoryBlock is the shared Messages block classifier for trailing breakpoints and hosted-search marker relocation. Text, images, tool results, tool removals and immediate inline tool_addition definitions can carry a marker; deferred inline definitions cannot. Hosted search moves their marker to the preceding eligible block with its existing TTL. After an accepted 2xx response, the session-local trailing breakpoint remembers the marked message's cumulative system/tools/history prefix and restores its marker on the next append-only request, including a historical immediate addition or removal beyond Anthropic's 20-position lookback. It uses the current request's TTL and respects the four-marker limit. No current history marker, a changed prefix or a failed unconfirmed request supplies no new write to restore; the fork-opening boundary remains the existing explicit exception. These modules add no provider calls or durable state.

anthropicCacheBlocks.ts::countAnthropicCachePositions counts the captured Messages payload for cacheWatch: each contiguous tool_use run is one position and each contiguous tool_result run is one position, only within its content array. Other blocks retain separate positions. The 20-position window includes the breakpoint, so a distance of 19 is inside and 20 is outside the final breakpoint's window. The monitor's fanout category and alert are a possible lookback explanation, not proof: additional breakpoints can still reach earlier confirmed writes. Counting changes no wire markers, hashing, successful-response confirmation, fork behavior or provider requests; OpenAI retains its separate automatic-prefix diagnostics.

PI 1.3.0 owns foreign-reasoning omission through transformMessages: an assistant message from a different provider, API or model loses its thinking blocks, while visible text and tool calls remain. runtimeProviderMessage in persistence.ts only translates Elowen's configured provider identity back to the runtime identity for the matching replay route, so a respawn preserves same-model signed or encrypted reasoning. Chat, warm compaction and standalone compaction spans use the native provider conversion; PI's text-serialized compaction fallback uses serializeConversation, which omits thinking entirely. Elowen adds no reasoning downgrade or strip pass. Route-scoped Anthropic hosted sidecars and account-bound encrypted compaction checkpoints remain separate contracts.

Anthropic hosted replay has three internal owners. anthropicHostedMetadata.ts owns the version-1 journal sidecar, server-block classification, search-pair validation, signed-thinking omission rules and usage readers. Replay and capture import it directly; it imports neither. contextBreakdown reads the captured request input count and loaded tool names from this owner, and cacheWatch uses its hosted-search classification to avoid treating summed server rounds as a cache baseline. With no sidecar these readers preserve ordinary PI usage; a hosted sidecar without a request count returns null rather than using summed billing tokens.

anthropicSseCapture.ts owns the incremental SSE state machine, its existing 16 MiB frame limit, request-token counting and single-body response tap. It forwards the original chunks and settles capture on EOF, cancellation or transport error, attaching no replay content when capture is unsafe. anthropicHostedToolReplay.ts retains route-scoped matching, restore/verification, refused-turn repair and the session stream wrapper. The repair runs on a narrow set of Anthropic 400 refusals, including the system-order wording the Messages endpoint can report for an unpaired search turn that follows a mid-conversation system message, and only ever drops turns the session already marked unreplayable (unpaired or unsafe) together with their tool_result answers. The dropped turn's own effort directive leaves with it; a content-bearing system message that sat directly before it moves to before the next system or assistant message, or to the end, because Anthropic accepts such a message only there. Matching uses PI's exported sanitizeSurrogates, while restoration keeps the original signed content, whitespace, field order and metadata bytes. The session factory installs this wrapper only for its hosted route; without hosted content the provider history remains PI's native projection. Capture observes one stream without an extra provider request or transcript store.

Native compaction and prefix identity

Native compaction sends the live session's composed system, tools and history without a trailing summary instruction. After checkpoint replay and the existing payload extensions, including Codex developer placement, it appends one compaction_trigger and sets max_output_tokens to 20,000. The request goes through the same composer, Fast transform, recorder and provider runtime as chat, with the same reasoning level and cache key; only transport is forced to SSE so the shared Responses response tap can capture the opaque result. It does not depend on Stateful responses. Chaining passes a trigger with its full input unchanged and clears the continuation.

The response tap forwards the original stream, including terminal usage, before checkpoint validation decides success. Its event-aware native caller explicitly requests SSE and parses the frames even when the Codex response omits the SSE media type; Responses chaining retains its existing media-type gate. Exactly one immutable compaction item with nonempty encrypted_content is collected from response.output_item.done; response.completed must still arrive. Codex can leave that terminal response's output snapshot empty because it already streamed the item. A nonempty terminal snapshot must contain exactly that same item. Missing streamed items, differing checkpoints, changed content, missing completion and provider errors remain failures. Invalid or missing checkpoints retry under compactionRetries, then report compaction.native_fallback and use the existing CC summary. The event name uses an underscore to preserve the shared problem-report wire grammar. Abort cancels without starting that fallback. cacheWatch clears pending trigger snapshots at compaction_end and resets its baseline after successful replacement, so the reduced context does not generate a cache-drop alert.

Codex sticky routing

The ChatGPT backend keeps a conversation's prompt cache on the machines it routed that conversation to, and hands the client two pieces of routing state that pi-ai does not replay. src/brain/session/codexRouting.ts replays them for every openai-codex session through PI's existing extension events, without patching PI; factory.ts installs it through the resource loader's codexRouting option with the session's live token.

  • Turn token. The first x-codex-turn-state of a turn, from the WebSocket codex.response.metadata event or an HTTP response header, is sent on every later request of that turn: in client_metadata (before_provider_request) and as a request header (before_provider_headers). agent_settled clears it, so it never reaches the next turn. agent_start is not the boundary: PI emits it again for every agent.continue() inside one run, so the auto-retry after a failed request (for example WebSocket closed 1000) and queued messages keep the turn's token and stay on the backend that holds its cache. A turn's first request, and the second while the token joins the body, go out as full WebSocket requests instead of a delta, because pi-ai compares the whole non-input body. In-session compaction goes through the agent's stream function and payload chain, so a compaction inside a running turn carries the token; one that runs before the turn's first response has none to carry, as in Codex. The captured body in brain_provider_requests shows it under client_metadata.
  • Routing cookie. One process-wide jar holds __oailb and Codex's allowlist of Cloudflare infrastructure cookies, never account cookies. PI opens WebSockets with the global constructor, so a handshake's Set-Cookie is invisible; when the jar holds no live __oailb (Max-Age 3600) the next request first reads the account's /wham/usage (codexUsageRequest in openaiCodexUsage.ts, shared with the usage rail) and fills the jar. Concurrent sessions share one refill; a successful one logs routing cookies refilled (<names>), __oailb valid for N min (names only, never values). A failed refill or a response without __oailb logs a warning and blocks refills for five minutes, so requests go out without the cookie instead of waiting. Request headers are not captured, so that log line is the evidence the jar is filled.

Without the module a request carries neither, and the backend routes it by its own load balancing. Cost: one authenticated GET per process per hour, and at most a five-second wait on the request that triggers it. The behaviour mirrors codex-rs core/src/client.rs (ModelClientSession) and http-client/src/chatgpt_cloudflare_cookies.rs; tests are tests/brain/session/codexRouting.test.ts.

Account model authority during compaction and memory

LiveSessionSpawner resolves a distinct compaction model through resolveBrainModelRoute, which checks the account's live selectionAllowed predicate for the explicit personal pick. No provider supplies an implicit compaction model: without a valid, permitted explicit pick, every provider uses the selected chat model, including ChatGPT OAuth. The route carries its configuration provider ID as compactionSelection, separate from the runtime provider name. The session factory passes one live compactionFallbackAllowed callback to both the in-session summarizer and PI-native createCompactionModelRoute; permittedCompactionModel chooses the captured model only while that callback still permits it. With no distinct model or after revocation, both use the session model. Manual and threshold in-session summaries on that model reuse the chat transcript's system/tool prefix, reasoning level, session cache key and per-request Fast options. Without an active native checkpoint, overflow recovery uses a standalone request on the same model, because the full live context just exceeded its window. A checkpoint instead uses its original Codex model and can retry the native trigger after PI omits the failed attempt, as described below. A distinct explicit compaction model instead uses a standalone request and cannot read the chat model's prompt cache. Every compaction request leaves credentials to ModelRuntime at send time. createCompactionModelRoute is installed even without a distinct model and drops PI's captured apiKey, headers and environment on compaction-marked stream calls, including the native history and later split-turn prefix requests. The in-session summarizer also leaves auth unresolved across repeated attempts. ModelRuntime supplies the current credential and account headers for the actual request model, including cross-provider choices. For example, revoking a chosen cheap model from a running account makes the next summary use its selected chat model without respawning. Memory categorization and icon suggestions pass their account ID into memoryModelInference, which checks the same model-selection authority; a denied workspace model uses the matching active turn's permitted model or skips background inference when there is no such turn. The check is repeated immediately before every request: piInferenceClient re-resolves its route() in decide, and the account-bound route answers null once the grant is gone, so a queued client (for example a post-turn skill review waiting behind the previous one) refuses the request instead of paying for a revoked model.

The in-session summarizer checks output room through pi-ai's public clampMaxTokensToContext utility before sending. This is the provider's own calculation, including the latest applicable assistant usage stamp, trailing messages and its context safety margin. A short retained transcript can still carry usage above the current window; a text-only sum would miss that and approve a request clamped to one output token. If the warm context cannot retain the full compactMaxTokens allowance, the summarizer checks the standalone span. If that also lacks room, it returns the existing too-large fallback without issuing a custom request, and PI serializes the native span. Without an active checkpoint, the summary cap and session thinking level are unchanged; the check adds no inference or retries.

On a native checkpoint branch, PI's text-only fallback cannot replay encrypted history. If the full ceiling does not fit, the existing CC summary request instead uses the provider runtime's remaining output room and buildCompactPrompt's concise instruction for that exact budget. Only the appended instruction changes; the composed history, checkpoint replay, account/model authority and thinking level remain intact. The rebuilt context still has to fit before dispatch. A window clamped to the runtime's one-token minimum remains refused, and a truncated answer remains a failure. For example, a 272k-window checkpoint with about 12k output tokens free can produce a bounded readable summary rather than failing three times without making a summary request.

buildCompactPrompt(customInstructions, maxTokens) uses the request's output allowance, starting from the selected summary model's compactMaxTokens on warm and standalone requests. Below the 20,000-token CC ceiling, it asks only for a concise summary, targeting half the allowance to leave room for reasoning and token-estimation error instead of requesting a scratchpad and full code excerpts. For example, a 4,096-token model gets a 2,048-token summary target. Security constraints, complete job identifiers and custom instructions remain required. At the full ceiling the existing CC prompt is unchanged; threshold sizing measures the same budget-aware instruction. This changes instructions, not provider limits or retry policy: a provider-truncated summary is still rejected and the circuit breaker still applies.

Every compaction request that Elowen sends itself, a warm or standalone text summary and a native checkpoint request alike, runs through retryCompactionRequest in inSessionCompaction.ts. A refused answer and a retryable provider error repeat the identical request after the session's retry backoff, up to compactionRetries times (runtime limit, range 0 to 5, default 2; 0 sends once). When that budget is spent, a failed native checkpoint logs compaction.native_fallback and the text summary runs, and a failed text summary falls back to PI's native compaction. Automatic compaction stops for a conversation after compactionFailureLimit consecutive failed automatic compactions (runtime limit, range 1 to 10, default 3; src/brain/session/compactionCircuitBreaker.ts), and a manual /compact is not affected.

The proactive threshold leaves that room in advance. compactionReserveTokens in src/brain/session/compactionThreshold.ts takes the session model's compactMaxTokens and raises the reserve to at least that allowance plus pi-ai's 4,096-token context safety margin, the summary instruction measured with PI's estimateTokens, and 12,000 tokens for the step that crosses the threshold, because PI checks it only after a step finishes. The floor applies only where it is at most a tenth of the window; smaller windows keep the user's percentage exactly. For example, a 400k window set to 95% compacts at about 90.5% and the warm summary reads the cached prefix, instead of compacting at 95% through an uncached standalone request.

coordinateNativeCompactionChecks in src/brain/session/compactionCheckCoordinator.ts wraps PI's native pre-prompt, turn-boundary and post-run checks so teardown can observe and cancel them. The session factory installs it through installTurnBoundaryAutoCompaction; hosted Anthropic sessions also size threshold checks from the resident provider count rather than summed server-tool billing usage. Only threshold sizing may clone the assistant message. Explicit overflow and recoverable-length checks pass the canonical assistant object and the native tool-result batch unchanged: PI resolves their journal entries by object identity and appends native context_edit omissions before compacting and continuing the same run. For example, an Anthropic request_too_large response is omitted before the compacted request, without an added user prompt. Ordinary sessions keep PI's usage calculation, and absence of the native check leaves the injected session's behavior unchanged. The wrapper adds classification and cancellation bookkeeping, no provider calls, new retries or transcript authority.

Native server-side checkpoints

src/brain/session/nativeCompaction.ts::nativeCompactionRoute is the sole eligibility decision. Only openai-codex-responses on the registered openai-codex provider and the normalized HTTPS chatgpt.com/backend-api endpoint is enabled. Official OpenAI Responses is explicitly disabled pending API-key measurements; xAI, Azure, relays, Completions and other providers keep text summarization. The session hook tries native compaction on the selected summary model without focused manual instructions and with room for the entire 20,000-token output allowance. Large-window threshold reserves leave this room after the step crosses the trigger; smaller windows can remain on text summarization.

PI still owns preparation, firstKeptEntryId, tokensBefore, persistence and active-branch restoration. The hook returns a readable checkpoint-identifying summary, cumulative file lists and details.nativeCheckpoint containing version 1, the opaque item, response ID and provenance: API, registered provider, normalized endpoint, the source model ID as audit metadata and a SHA-256 hash of the current ChatGPT account claim. Replay binds only API, provider, endpoint and account, so models on the same authenticated ChatGPT route can share a checkpoint. Credentials remain resolved at request time and are never saved in the checkpoint. The request recorder bills every terminal attempt once, including a successful empty-content response; the result's usage is the same accumulated accounting, not another charge.

installNativeCompaction wraps the direct agent.onPayload callback before the existing extension chain. It selects only the newest compaction on the active branch, validates its metadata and current provenance, and replaces exactly one PI-framed summary item with the stored checkpoint. The exact summary text comes from PI's public sessionEntryToContextMessages and convertToLlm projection, selecting the compactionSummary message because PI can prepend its system-message snapshot to that projection. No unexported framing constants are copied. PI's retained tail stays unchanged. The same path replays a checkpoint for chat, warm CC fallback and the next native generation; a missing summary, malformed checkpoint or provenance mismatch rejects before transport. PI standalone compaction omits onPayload, so the installer forwards this same callback whenever the active compaction has a checkpoint. This forwarding sits below compactionModelRoute: model selection and grant checks still run before callback insertion. Standalone requests whose differently framed stub cannot be replayed fail before transport instead of replacing encrypted history with a summary of the stub.

This direct callback deliberately replaces the replay-extension design: pinned PI 1.3.0 dist/core/extensions/runner.js catches before_provider_request handler errors in emitBeforeProviderRequest and returns the payload, while dist/core/sdk.js routes the session callback through that runner in transformProviderPayload. A replay extension could therefore continue with the stub after validation failed. One direct wrapper keeps replay and rejection together, before Codex developer placement and before trigger insertion, without changing non-checkpoint requests or the Fast/recorder ordering. At dispatch, replay uses compactionModelRoute.isCompactionSignal to classify PI's own compaction AbortSignal before streaming. The existing route marker is the single scope owner, so replay adds no session lifecycle subscription. Provider rejection and local validation errors on those checkpoint-bearing compaction requests log compaction.checkpoint_failed without recording checkpoint content. Ordinary chat failures keep their original error and terminal usage without this compaction reporting code. No failed request is resent without history. Rolling back to a build without replay support cannot recover context from the readable stub.

On a checkpoint branch, compaction runs on the checkpoint's original Codex model and authenticated account, even after a distinct summary model is granted or chat switches to another compatible model. The source model's current grant and configured availability are rechecked at dispatch and between retries. Overflow recovery may use the native trigger after PI's journal omission of the failed attempt, provided the remaining composed context leaves the full output allowance; otherwise standalone CC still replays the checkpoint. When the custom paths are exhausted, the hook records a classified failure against PI's compaction AbortSignal in the existing compactionModelRoute wrapper. PI's extension result has no error variant and hook exceptions are swallowed, so the signal wrapper throws that failure before any standalone transport. Normal compaction_end failure events still drive the circuit breaker.

LiveSessionSpawner.prepareModelSwitch and its shared route admission are the conversion boundary for model changes, cold respawns and forks. ConversationLifecycle.switchModel awaits admission under the session lock before disposing the current live session; delegated owner switches do so before writing their selection, and the turn runner rechecks an already-live checkpoint before admitting its next provider turn. Compatible ChatGPT models require no conversion. Before an incompatible route/account is admitted, prepareNativeCheckpointRoute validates the original model's live permission and auth, then invokes the factory-owned convertNativeCheckpoint method. It sends one warm CC summary through the same in-session summarizer and retry budget, disables the native trigger for that conversion, and persists a normal text compaction through PI. Source credentials are resolved afresh, never retained for a later account switch. The spawner supplies a beforeAdmission callback to the dispatch owner: it compares the selected target's own runtime with checkpoint provenance, resolves the source from its original durable binding, and finishes the portable journal cut before target admission. Thus a cold checkpoint written on A can convert after the default changes to B; a failed conversion writes neither B's assignment nor the new affinity. The source and target runtimes are passed separately to prepareNativeCheckpointRoute, so same-model accounts cannot be mistaken for the same authenticated route.

PI cannot compact an immediate post-cut branch. Conversion therefore appends one hidden elowen.checkpoint-conversion custom message describing the actual routing intent and temporarily retains only that anchor, restoring the user's retention setting afterwards. On failure a native context_edit omits the intent; the checkpoint stays active. The summary includes the checkpoint and retained tail. A fork requested by a running parent converts its copied branch on the source route before its destination session is built, preserving the parent's journal and the child's delegated scope. The target rehydrates that converted child without copying the parent a second time. A temporary source session for a cold conversation restores its prior durable model selection even if conversion fails.

Missing original auth, an unavailable/revoked source model or an exhausted summary refuses admission with NativeCheckpointConversionError and HTTP code native_checkpoint_conversion_failed. HTTP feedback and queued/error chat notices use the core cs/sk/en dictionaries; queued model refusals retain their selected model/provider so the picker reconciles through the existing event. No target provider request or readable checkpoint stub is sent after refusal.

Lossless checkpoint ciphertext is restricted to authorized JSONL export and administrative diagnostics. src/api/routes/brainDebug.ts applies an is_admin check to the entire /brain/debug/* namespace, including raw requests, segments and the journal transcript, and sets Cache-Control: private, no-store. The web entry lives in SettingsView, which blocks a resolved non-admin account before rendering ConversationDiagnosticsModal; server authorization also prevents access while the account query is unresolved.

Plan and error-result helpers

buildPostCompactionContext shares a private reminder envelope for active and unavailable plans, preserving text. Absent/already-visible plans add nothing; unavailable managed storage keeps its unknown-state reminder. planStore uses PLAN_READ_BOUND_BYTES for central/guest reads. ensurePlanDir creates lazily and logs filesystem failure without returning status; planTurnContext consumes it.

plainErrorResult(text) in tools/toolResult.ts builds shared textResult with empty details and isError true. ShareFile, ShareImage and ShowView use it for ordinary failures without changing policy-refusal semantics or adding I/O. ElowenApi keeps one private catalog projection for search/describe with failure status/details; request responses retain their envelope.

Post-turn memory dreaming

settleTurn carries one more optional owner-turn effect beside the curator: dream = { trigger, input }. TurnRunner arms it only for the account's OWN conversation, and only when the turn is not internal, automated or interrupt-resume; a channel room and a delegated settlement never arm it. Like the curator, the call is detached (void parts.dream.trigger.maybeRun(parts.dream.input).catch(...)), so a dream can never fail a turn that already produced its answer. The trigger only says a check MAY be due: the MemoryDreamingService it points at owns the toggle, the two admin thresholds, the scan throttle, the lock and the watermark, and the fork itself. See Memory dreaming.

Origin usage wire types

UsageOriginGroup, UsageOriginRow and UsageByOriginResult belong to src/shared/wireContract.ts. The admin route verifies its response with satisfies; the origin store derives its output and SQL origin_kind column from the canonical union. The existing rollup, grouping, redaction and permission behavior is unchanged.

Delegated placement and turn collaborators

Delegated read, stop and continuation share the direct parent relation and spawning-principal check. A foreign or unknown handle is refused with DelegateList guidance before activity is revealed. Host callers retain their separately authorized path, including stopping a related child with missing captured scope. Read and continuation still require their own scope checks, and continuation rechecks the current execution ceiling. The owner ancestor walk keeps cycle, account and owner-root validation.

DelegatedSessionService parses the delegated scope from the same session row used to check the parent relation. Write-access promotion passes that original row to journal.promoteDelegatedAccess as the raw authority fence: session id, account, parent, execution reference and delegated scope must still match under the journal lock. A concurrent scope or execution change refuses promotion before delivering the follow-up. Delegated dispatch awaits origin-pin cleanup after draining its event publications, so a failed release remains an observable dispatch failure.

Public conversation activity detail passes through boundedConversationActivityDetail before every raw activity write. openTurn applies it at initial or deferred admission and when moving an admitted turn; settleTurn applies it to terminal detail, and parked-conversation recovery applies it to its independent terminal journal write. The shared policy removes control characters, collapses whitespace and limits the hint to 240 Unicode code points; absent detail becomes empty. The repositories retain raw persistence and compare-and-set ownership. ConversationActivityStore narrows the native transcript reader and journal writers rather than flattening them into a second store. Admission, moves, settlement and neutral reset await both the journal mutation and ConversationActivityChanged publication before returning. A publication failure rejects that operation instead of escaping into a detached read during pool shutdown. SessionTeardown.abortLive drains again after its final native clearQueue, because even an empty queue emits queue_update and captures a control publication. Abort returns only after that work settles.

A requested delegated cwd is checked against the child's scope-derived policy. Scoped host callers remain confined to the selected project's root. An all-access caller with live canExecuteHost permission may place a host child anywhere readable: delegatedWorkDir returns the actual host projectRef alongside workDir, using projectForPath and projectContextRoot to resolve registered projects and linked Git worktrees, or the nameless host target otherwise. PlatformOrchestrator persists both values in the child's immutable scope. A valid subdirectory or linked worktree placement is preserved on every turn, including continuation and boot recovery. The shared placedWorkDirWithin rule is reused by turn resolution and read-only status; host candidates must still resolve to a readable directory. Read-only status and the lower turn resolver use the selected project root for a stale, foreign or vanished persisted placement. Before a placed child can continue, ChannelSessionService revalidates its stored directory through delegatedWorkDir and refuses an invalid placement or a changed host target rather than relocating the child. scopeExceedsCurrentAccess allows differing host refs only for an all-access host parent; managed and scoped target checks remain exact, and current account and ancestor authority still gate resume. The refusal preserves the validator's exact reason and fresh-sub-agent guidance without another existence probe. A missing stored placement retains the existing fallback; hidden nested result delivery remains the deliberate admission exception. Managed containment remains a guest-path shape check and cannot prove filesystem existence. Plain host execution without a named project retains its existing policy-based directory resolution. Delegate's cwd input and the child drill-in snapshot are real consumers.

spendForOwner adds the prospective owner to existing TurnSpendAdmission wiring without replacing the payer resolver. An active run pin remains ahead of the durable or prospective owner; absent spend wiring stays absent. The four existing delegated/platform callers use this helper and retain their parent checks and ordering.

SessionQueueService validates positional removal and empty recall before mutation, then shares the attachment-carrying snapshot and clear/requeue operation. Surviving steering messages remain ahead of follow-ups; display text and image references stay attached. CLI recall and the web queue remove action are the consumers.

BrainService.stopProject(projectId) is the daemon deletion coordinator's runtime barrier after the Project becomes deleting, before deleting files or ownership rows. SessionTeardownService selects sessions by canonical execution ref and delegated scope, cancels their continuations, workflows and code-mode cells, interrupts turns and drains session locks/publication before disposing runtime. Ordinary conversation histories remain unchanged. Local and runner process sweeps must confirm their stops; runner release must answer released, and a still-live workflow refuses completion. Busy or failed cleanup leaves durable deletion pending, not successful. Background process launch refs are checked separately because a process can outlive its conversation's target selection. The operation uses existing transcript and workflow readers (one metadata and workflow-list read per stored session), not a cwd heuristic. A workflow is selected when its origin or any persisted node targets the deletion set; its other node sessions and descendants join the teardown without stopping an unrelated origin turn or another DAG owned by that origin. The existing workflow control's stopWorkflows({ workflowIds }) stops those exact engines through their captured owner, including background runs, and awaits node startup, late-child stop calls, active turns and summary preparation. It returns every surfaced child session for core's process sweeps and confirmed runner release. A child-stop rejection propagates; an unconfirmed stop never becomes a runtime acknowledgement. The engine retains cancelled-but-draining nodes against history eviction. Cold DAGs have no held engine work; their persisted child sessions still undergo core teardown. A missing workflow owner blocks deletion when a selected durable DAG exists. Selection closes over nested workflows and child relations; its cost is proportional to retained session/workflow metadata and that closure. The durable Project transaction must fence recovery generations and new work first and retain original target/scope rows until this barrier succeeds; this runtime operation is not a replacement for those repository invariants. Interactive PTYs belong to Terminal's awaited Project-removal hook, not the conversation process registry; the coordinator must finish that hook before deleting any files.

ProcessRegistry has no account-wide kill: session kills and predicate sweeps remain, and account teardown uses the daemon/runner IPC contract (killAccountProcesses in src/subagent/runnerHost.ts). BrainDeps is defined in brainDeps.ts. PlatformOrchestrator and DelegatedSessionService take no sandbox dependency, and their live-user checks remain. Stable client bindings keep user-scoped map keys and generation/TTL fences without copying userId into every value. BrainStatusService takes no projectPath dependency.

BrainTurnRunner owns steering, turn scope, admission, settlement and the goal judge; automatic compaction belongs to PI's session factory setup. composeTurnWire is the turn prompt/context composer. Delivery spill chooses a bounded placeholder before PI creates the tool result, while cold-start clearing later retires eligible retained results using its age and cache criteria. Both publish into a managed Project under the account the running turn acts as — cold clearing names currentAccountUserId() rather than the conversation's stored owner, because a synthetic host relay runs the target conversation's turn inside the initiator's scope and the Sandbox provider refuses any other account with actor_mismatch. BrainSessionFactory and the resource loader are used through LiveSessionSpawner in both daemon and forked subagent BrainService instances. Tests may inject a loader through SessionFactoryDeps. Host-owned plan-mode tool admission is documented at toolMetadata.ts and remains paired with capabilities.ts execution clamps, including exec/wait nested calls.

SessionSpec.dynamicCommands and BrainResourceLoaderOptions.dynamicCommands take () => Promise<PluginSlashCommand[]>. LiveSessionSpawner supplies the existing plugin registry's current-writer command reader. The resource loader resolves it once in the asynchronous inputTransformsExtension input handler, clears the previous turn's list before awaiting, and retains only the resolved list for that input. Alias lookup and PI's synchronous getPrompts() read that same list. Before the first input the getter exposes only static templates; repeated getter calls perform no repository reads. Each subsequent input resolves again, so a newly registered command or a revoked plugin grant takes effect on the next turn. Without a dynamic reader the static catalog remains. Provider failures reject the input handler rather than publishing the previous turn's commands. PI runs this input hook before skill and prompt-template expansion; tests/brain/session/cacheRespawn.test.ts exercises the ordering through a real session, including a plugin command added between turns.

LiveRecallOptions.budget returns Promise<LiveRecallBudget>. The context hook awaits the current pass, count and byte limits before deciding whether to search. createLiveRecallWiring forwards the live configured budget and supplies zero limits when that optional dependency is absent; repeated context passes read it again. Budget read failures reject the context hook. Recall retrieval remains asynchronous and nonblocking after that decision.

SessionSpec.onTurnPersisted(assistantMessageId) returns Promise<void>. The persistence projector awaits the callback after the exact terminal assistant entry is durable, and its flush includes that publication. LiveSessionSpawner uses it to publish persistedAssistantMessageId for terminal delivery; an absent callback publishes nothing.

SessionFactoryDeps.instanceRoot, CapabilitySpec.instanceRoot, gateSessionTool(tool, instanceRoot, dreamTools?) and turnModeContext({ instanceRoot, ... }) require the explicit filesystem root independently of the database URL. BrainService forwards workerDatabase.instanceRoot into the session factory; direct capability callers supply the same root. composeSessionTools passes it to the shared plan-write clamp and ExitPlanMode; the factory passes the same root when gating the native ToolSearch definition. Owner and channel mode-context callers pass it through to plan preparation and location. The clamp awaits location authorization before allowing Write or Edit; reminders and tools derive their plan path through the same canonical helpers.

BrainService.create awaits the process boot identity and repository subscriptions before returning the facade. The optional project repository is registered through await projects.onExecutionRebound(listener) before delegation setup; a failed subscription rejects construction. Construction performs no detached database initialization. Core assembly and runner callers must await that factory.

Journal lifecycle composition lives in managedArtifacts.ts. prepareSessionMutation uses the caller's bound SERIALIZABLE repositories to capture raw authority, canonical artifact references and plan intents. Clear, delete and account-removal callers pass that preparation to the DB-only journal operation and await cleanupSessionArtifacts after commit. The captured immutable spill namespace is renamed before reuse, then removed in the background. Pending plan intents retain their own filesystem ownership. drainSessionDeletion awaits bounded batches for a full deletion and starts the logged background drain for a retired sequence range.

queuePlanMigration discovers host copies outside SQL and commits candidates through journal's authority CAS. Its existing managed completion marker and separate local_completed marker queue missing local destinations once without replaying completed managed moves. drainPlanOperations holds the instance's kernel lock, reads committed intents, parses execution targets and performs file/provider IO without an SQL transaction. It acknowledges each result in a separate short transaction. Dependencies follow the actual plan location: execution kind, guest Project and canonical plan slug. Host Projects share the instance plan directory. An unfinished operation blocks later operations touching its source or destination; the blockage propagates forward through transfers. Independent locations, including the host deletion paired with a computer deletion for the same conversation, continue immediately. The journal mutation has already settled server-side conversation state and artifact references before this IO starts. A typed LOCAL_TARGET_OFFLINE retains its intent with settlePlanOperation({status:'deferred'}), clears obsolete failure detail and does not fail the boot/hourly sweep. Authenticated CLI executor attachment invokes CliConnectionDeps.onConnected; daemon composition drains the same durable queue through the current Sandbox seam on every connection. Each drain atomically publishes a generation in locks/brain-plans.request using the shared atomic writer before taking the existing brain-plans.lock. Background acquisition is nonblocking: contention leaves the request for the lock holder, which compares generations after its current pass and coalesces all intervening requests into one further full pass. A second comparison after releasing the lock closes the handoff window; either the releasing owner takes the newer generation or the next lock holder does. There is no timer or periodic retry for contention, and generation changes are the only reason for an additional pass. Session-specific admission retains its awaited lock and original failure contract. Unexpected pass failures remain loud after pending passes finish, with their original operation details. No remote cleanup is abandoned: deleting only the intent could let a later turn rediscover stale computer bytes. Lifecycle stat, bounded version-pinned reads and create-only writes use the shared guest client directly to preserve typed failures even if the computer disconnects between file requests; every call still crosses the core artifact namespace and provider authorization. Other failures still aggregate operation id, session id and original cause for the daemon error log. A session-specific drain still throws the original error, including offline, before plan context can admit stale bytes. planLocation, plan readers and tools take the explicit instance root. BrainService prepares re-keys on the same SERIALIZABLE transaction as their journal mutations. Execution switches use READ COMMITTED and the journal's L protocol: sorted payer-user locks, sorted session identity locks, then session row locks and raw-authority CAS before atomically writing plan intents and execution. Concurrent transcript writes therefore do not invalidate a pinned SERIALIZABLE snapshot; changed authority still refuses the switch. Live rebinding and authorized host renames stay outside the SQL callback.

ChannelSessionService enters the exact session providerPreflight.run handle for prompted, triggered and continuation operations. Owner and channel manual compaction and the cold-start compaction use the same handle and durable provider preparation. Native prompt callbacks record only disposition. Channel completion drains live.persistence.flush() and then live.flushPublications() after every provider operation, including a rejected operation, before its terminal idle receipt or delegated reply can escape. A failed final journal write rejects the turn; a completed workflow node is therefore readable from the settled transcript before a dependent starts. The awaited before-request callback prepares recall and steer SQL on bound repositories, committed in a fresh READ COMMITTED transaction. Recall preparation calls MemoryService.prepareMarkRecalled(userId, ids, repositories) to count on the supplied binding and retain a synchronous invalidation closure. Recall accounting uses a savepoint so an SQL failure can be logged by turn-start recall without aborting admission; delivery tracking remains required and propagates failures. Only committed preparation invokes synchronous publication closures and the owner-facing accepted echo. Dreaming reads the complete transcript and settled request usage after these drains.

preparePartialTurn(snapshot, sessionId) compiles the C5-179 protected snapshot into typed journal operations and exact inbox receipts. It shares native restoration and tool-call projection with ordinary settlePartialTurn; only the supplied fixed-size child-assistant reader may await. The repository captures contextState at branchRows[0].parent_entry_id, not at the active leaf: restoreJournalSnapshot uses it to seed only ancestry before the retained compaction cut. Empty/rooted branches carry no seed state. This is one indexed metadata lookup under the existing owner/session protection, with no journal rewrite. Leaf state can name an assistant already in the retained rows; reusing that ID as an ancestor seed creates a cycle in PI's in-memory index. Boot recovery through DelegatedSettlement uses this same snapshot contract. Open view waits return a non-mutating plan. Native edits continue to hide omitted calls, partial leaves are retired without losing usage, and a result receipt exists only when the new tool-result entry contains that captured result's actual payload and details.resultId. A completed inbox result retains its full captured answer inside the existing [interrupted, result recovered] tool-result framing, so the continuing model knows its Delegate was interrupted and already completed. The same entry retains details.resultId and a success result stays non-error. Generic interrupted/resuming placeholders never acknowledge an inbox row. Delegation applies and validates the plan on its protected transaction.

Spill path resolution in src/shared/paths.ts awaits the process's transcript and delegation repositories. setSpillNamespaceResolver takes (sessionId) => Promise<string>; sessionToolResultSpillNamespace, sessionToolResultSpillDir and the guest twin ownSessionGuestSpillDir all return Promises. The repository owns absent-row handling; only an unwired process uses the session id. setInheritedSpillNamespaceResolver and setAuthorizedWorkflowArtifactResolver take async readonly-array readers; inheritedToolResultSpillDirs and authorizedWorkflowArtifacts await them and grant nothing when unwired. Read failures propagate, rather than becoming namespace fallbacks or empty claims. Host guards, tool-output publishers and guest readers await these boundaries before deriving or comparing paths. The guest verifying reader skips namespace SQL for paths outside the spill tree so ordinary project reads do not depend on artifact availability.

BrainService.forkSession checks ownership before reading the source archive marker through maintenance.journalArchive. It prepares any originals with the standalone readJournalArchive(identity) outside SQL, then passes the exact prepared object or explicit null to journal.forkSession(sourceId, targetId, prepared). Journal atomically fences the marker, restores source originals and copies the branch. An ArchiveUnavailableError triggers a fresh marker read; only a changed session/path/timestamp identity causes fresh file preparation and one retry, using the same target id. Unchanged-marker failures, preparation errors and a second refusal propagate without publication. Normal opening later rehydrates the committed fork.

UploadSessions is the single in-memory owner of resumable and single-request HTTP uploads. Its asynchronous begin(userId, project, open) registers ownership before authorization or destination opening; part and commit reauthorize against current repositories. The brain upload API checks the Project's durable lifecycle even for an administrator writing to a host Project; a deleting Project refuses admission with 409. Membership, destination and conversation checks remain live. Single-request POST /brain/uploads uses this same begin/part/commit path without exposing a session ID.

registerBrainUploadRoutes returns this exact owner for lifecycle composition. After marking a Project deleting, the deletion producer-stop callback must await uploads.cancelProject(projectId) before provider teardown or filesystem removal. The drain cancels idle request readers, then awaits pending authorization, opens, parts, commits and disposal, including entries already cancelled by the client or idle sweep. A dispatched commit can publish before it settles; deletion removes its result only after the drain. Cancellation cannot permit a stale authorization result to restart writes. Failed disposal propagates after every other upload settles and remains owned for an explicit deletion retry; there is no automatic retry or second upload registry. HTTP cancellation still answers immediately for an active part, while the Project drain waits for cleanup. With no open uploads the drain does no filesystem work. This owner covers HTTP uploads; platform attachment producers retain their own awaited lifecycle before using storeProjectUpload. Restart drops sessions, so guest staging TTL and the host attachment sweeper retain their existing restart cleanup responsibility.

PromptComposer.composeRequest awaits each source observation before rendering it. coreTranscriptSource resolves the current identity before filtering private notifications, preserving transcript order and exact principal visibility. PromptComposerRuntime uses the existing deferModelStream boundary to await composition while returning PI's stream synchronously; a failed observation produces the stream's normal error event and cannot reach the provider.

BrainService.mirrorRemoteChildEdge awaits LiveSessionRegistry.setChildRunning and propagates publication failures. Bootstrap hands that Promise to its existing runner-notification tracker, which records failures and drains in-flight notifications at shutdown. Channel delegated-call admission, release and abort-tree clearing await the same registry publication boundary.

recordColdChatEvent writes through the supplied repositories.transcript and repositories.journal, preserving the pending-tail placement rule, then awaits event publication. The caller retains the conversation lock. Plan-file checks and managed-artifact context readers await the current project and account before choosing a host or guest path. turnModeContext receives the same explicit instance root as its owning turn runner or channel.

authorizedChatSelection(selection, userId, authority, warn?) is the asynchronous shared preference-grant boundary. It awaits selectionAllowed before accepting a complete preference and awaits allowedFallbackSelection before returning or reporting a replacement. A denied preference uses the permitted fallback; null rejects an account with no runnable model, and repository failures propagate. Callers await the selection before resolving the runtime route. Without authorization wiring, the existing open-mode selection is preserved.

projectBrainConfig(snapshot, storedProviders, availableOAuthCredentialKeys) is the pure runtime-config projection for bound account-model readers. The daemon captures credential-key presence before opening SQL, using new Set((await brainCredentialStore.list()).map(info => info.providerId)); errors propagate. Inside the transaction it passes the bound config snapshot and stored providers to the projection, then keeps user/settings/grant reads on those same repositories. The projection accepts a required presence set, never OAuth credential objects or tokens, and preserves per-connection OAuth availability, API-key accounts, default model, overrides and wire detections. An empty set excludes every OAuth connection. brainConfigFromElowen delegates to the same projection for ordinary root reads. Availability is a read-time hint; real requests retain live authorized dispatch.

projectTranscriptRows(rawRows) in messageView.ts is the core renderer for transcript's DB-only page/listing rows. It retains the entry identity and timing, renders messages, compaction dividers and message blocks, and excludes internal context, meta messages and discarded attempts. Native checkpoint details remain excluded from display.

LiveRecallOptions.commitDeferred(recall) is required for direct installLiveRecall callers. The session factory supplies it and omits it from caller-facing LiveRecallSpec: it awaits recall.prepareCommit(repositories) inside one db.transaction, then invokes the returned synchronous publication only after that transaction resolves. The live hook first attaches the frozen late turn-start block to its journal anchor and invokes the callback only after attachment succeeds. Failed preparation rolls back accounting and publishes no delivery-cache state; live recall reports the injection failure through its existing best-effort warning path. Direct fixtures provide this callback explicitly, including publication ordering for fixtures that exercise deferred delivery.

Recall and steer results expose prepareCommit(repositories): Promise<() => void>. DeliveredMemories.prepareRecord captures dedup facts through that same bound transcript, then publishes only RAM state. titleTurnConversation writes its provisional title on the supplied repositories and returns the closure that starts the existing background titler after commit. Supplied titles use normalizeConversationTitle in conversationTitler: manual names reject empty normalized text, while dedicated creation falls back to its target; both collapse whitespace and clamp to 120 UTF-16 code units. Its optional announceTitle callback may return a Promise; the background chain awaits it and logs a publication failure without leaking an unhandled rejection. Channel admission and service turn admission share these boundaries.

Service assembly receives database, synchronous prepareEmptyConversation(row) for C5-104's protected empty root rows, and awaited cleanupCommittedSessions(cleanup) after commit. Cleanup retires the captured spill namespace and drains deletion through the existing core helpers. Status receives snapshotReads(repositories); the host supplies the existing config/policy factories through BrainDeps.snapshotReads, so their credentials and policy semantics stay canonical while every SQL read uses the passed binding. BrainService adds bound durable elicitation readers while live questions remain in the one registry. The caller-test fixture tests/brain/postgres.ts exposes useBrainPostgres() and brainDatabaseInputs(instanceRoot) for the socket-only PostgreSQL harness.

Manual compaction

Manual compaction orchestration belongs to src/brain/session/turnSettled.ts. compactTurn opens admission and origin attribution through openTurn, delegates compaction and fresh usage projection to events.ts, and closes the opened turn in finally. Callers pass an already composed TurnOpening and retain their locks, account touch, origin construction and missing-session decisions. BrainService and ChannelSessionService use that same implementation. No-op compaction retains its typed reason; failures propagate and still close the handle. Event mapping does not import turn lifecycle orchestration. Cold-start and native automatic compaction retain their separate owners. The move preserves behavior and adds no runtime option or contribution.

Paid compaction accounting is composed by session/compactionUsage.ts. prepareCompactionUsage captures the immutable journal fact and configured provider identity before opening SQL. persistCompactionUsage appends that fact and awaits onTurnSettled(sessionId, usage, repositories) using the same transaction-bound Repositories; the callback must use those repositories for independent counters. A rollback publishes neither fact nor counters. The session factory awaits the commit, then refreshJournalSession and PI's context refresh. Native PI append mutation and publication never run inside the transaction callback.

The resident journal has one stable SessionManager identity. Its asynchronous refresh reloads retained entries and context ancestry, awaits runtime hydration, and installs the append barrier on the replacement manager before exposing it. A concurrent native append prevents replacement and is reported to explicit continuation callers; compaction's persistence drain defers that refresh to its next captured watermark. Continuation awaits this refresh before rebuilding the PI context.

Internal worker and image boundaries

The catalog worker awaits its targets() callback before forking and sends the resolved endpoint array over IPC. An endpoint-read rejection follows the existing failed-pass reporting path. Stopping while that read is pending prevents a later fork. retainProviderModels awaits the live worker's target authority before pruning the resident snapshot and requesting the next pass; account-change callers await retention. The pass requester handles a rejected background run through the existing catalog failure reporter.

createBackgroundWorkerDatabase supplies export and attachment-sweep children with the same canonical memory vitality, category icons, provider/session vocabulary, host sampler and discovered plugin declarations as core assembly. Each child receives databaseUrl, instanceRoot and the required poolSize over IPC. Core boot sets BrainDeps.workerDatabase.poolSize and the attachment request's poolSize from the already-loaded harness databasePoolRunner, including when the parent is the daemon. The request parser uses the native URL parser to require a PostgreSQL URL, including socket query URLs, and rejects missing or non-positive/non-integer sizes; the worker forwards the supplied value directly to createCoreDatabase, with no local default or harness reread. Each child opens and closes its own worker pool and disables authoritative config preparation and vault-key initialization. Export authorization and rendering share one read-only REPEATABLE READ transaction. No child migrates the database.

forkWorker exposes its reply and giveUp cancellation; the ChildProcess remains private to the fork closure. Cancellation still settles the reply and sends SIGTERM followed by the existing bounded SIGKILL grace. embedBatchThroughWorker is a real consumer.

storeImageByContent accepts a typed base64 string and a served MIME type, decodes with Node's Buffer.from and delegates content-addressed persistence. Unsupported MIME and zero-byte input return null before a write; failed writes return status failed. Node's base64 decoder is lenient and this boundary does not promise syntax-error rejection. externalizeImageBlocks retains the original image block when no reference can be stored. Tool output shaping produces warning for failures/refusals; the shared ToolOutputView danger variant remains available to local-shell presentation.