NAVIGATION
ELOWEN DOCUMENTATION

Last updated: 10 October 2026

Browse documentation · Daemon construction and identity
Developer reference

Daemon construction and identity

Core daemon

BrainService integration coverage lives in tests/brain/brainService/, grouped by the production owner exercised: admission and queueing, lifecycle and clients, prompt/mode/context, memory, channels and origins, status/history, delegation delivery/control, and each boot-recovery substrate. Voice provenance and origin recovery retain their own suites. Run the directory with npx vitest run tests/brain/brainService/ --maxWorkers=2 inside the isolated test harness. fixtures.ts owns only the shared PI session/database fake, wire/state helpers, boot coordinator wiring, tool-registration fixtures and per-suite scratch-directory cleanup. Its beforeAll initializes a separate model runtime in each isolated test file; every fakeDeps() call still creates its own in-memory database and session state. Suite-specific seed helpers and assertions stay with their tests; no fixture or runtime is shared across Vitest workers. The split preserves complete test bodies and names and adds no production boundary.

  • src/daemon/ boots the application, installs shutdown handling, starts maintenance, coordinates recovery, and owns the daemon-only process topology.
  • src/api/ defines the Hono HTTP server, authentication middleware, route registration, schemas, SSE, and plugin WebSocket upgrades. src/api/validation.ts owns shared request parsers; route query integers use queryInt(), whose default sends empty values to the fallback, while emptyAsZero preserves routes where a present empty filter has historically meant zero. For example, debug limits pass emptyAsZero: true; ordinary list limits keep the fallback behavior.
  • src/shared/windowRateLimit.ts owns process-local fixed-window request counting for login, search and API tokens. createWindowRateLimiter(windowMs) provides count(key, now, max) and clear(key); keys are strings or numbers, and callers supply their already-validated positive budget on every counted attempt. The first attempt starts the window. Every attempt, including a refused one, increments the counter before comparison, and changing the budget retains the count and reset time. The result contains limited, the millisecond resetAt, and retryAfterSeconds, rounded up to whole seconds with a minimum of one. A request at the exact reset time starts a fresh window. Calls sweep expired entries when the map holds more than 5,000 keys; ordinary calls use one map lookup and a sweep scans the current map. There are no timers or background work when nothing calls the counter. The helper writes no database state, and a new instance or daemon restart starts with empty counters. loginRateLimit.ts uses one instance per server context for ten password/Microsoft SSO attempts per IP in five minutes, cleared after successful password login. Search registration uses independent rank and ask instances for thirty and ten requests per account per minute; absent embedding/model configuration still consumes an attempt before its existing 503 response. Login uses the server clock and search uses Date.now(). ApiTokenStore.countRequest supplies each token's live requests-per-minute limit and uses the returned retry seconds for refusal. An unlimited token bypasses the window counter while retaining the store's durable request accounting; revocation clears that token's window.
  • src/api/routes/auth.ts is the account-route registrar. With no user store it registers nothing; otherwise it mounts authSession.ts for login/session/profile/password, selfSettings.ts for personal preferences, avatars.ts for uploads and signed image links, users.ts for account administration and optional project assignments, and impersonation.ts for sign-in-as and proof-bound return/cancel. The original route order, authentication middleware, response bodies, status codes and store/effect ordering stay intact. Its strict account-admin predicate is passed to administration and impersonation; it requires a current administrator even without a project-assignment store. Only directory reads and account creation relax that gate while the account store is empty. Project assignment writes retain RouteContext.projectService authorization, and assignment routes register only when userProjects exists. Registration creates only the existing closures, with no extra persistence or background work. The first-setup wizard and the Users panel are real consumers. Personal alert validation uses AlertStore's exported ALERT_PUSH_MIN domain, shared with stored preference sanitization.
  • src/api/routes/config.ts registers the config route family and owns config reads/writes, MCP and push subscriptions. toolDeferral.ts owns the live catalog and draft-policy preview; publicTheme.ts owns the public brand/assets cache and coalesced live brand reapplication; configPluginConsentGate.ts owns the added-plugin consent check and the running-plugin snapshot before removal. system.ts owns readiness, update operations, restart and log routes; events.ts owns instance-event filtering, voice routing and full-human web presence. Registration and setup-tolerant versus strict admin gates retain their existing order. Without an active theme the public theme route serves the built-in brand; without optional services the existing unavailable/absent behavior remains.
  • src/api/sseHeartbeat.ts owns waitForSseHeartbeat(signal) and its fixed 30_000 ms interval: one timeout and one abort listener per active wait, both removed at completion. It resolves true after 30 seconds and false on cancellation, including an already-aborted signal without allocating a timer or listener. The /events, /brain/conversations and /brain/stream handlers combine request cancellation with Hono response-stream cancellation before waiting, so disconnecting releases the timer and subscriber immediately. The helper owns no authentication, event filters or presence policy: those stay in their handlers. All three keep the 30-second ping comment; only the opted-in brain stream emits the named heartbeat, and only /events refreshes web presence after a successful ping. Callers supply only the cancellation signal; the interval is not configurable and no timer runs outside an active wait.
  • src/api/routes/cliSettingsPatch.ts owns the unknown-input boundary for PATCH /auth/me/cli-settings. parseCliSettingsPatch(body, user, deps) requires a JSON object before inspecting keys, returns either a typed patch plus platform links and optional revision or a validation error, and validates complete model selections and changed project pins against the caller's grants. It reads configured providers and, when pins are supplied, the existing personal pins; it never writes or restarts a session. The self-settings route in src/api/routes/selfSettings.ts (PATCH /auth/me/cli-settings) maps invalid input to 400 and a stale revision to 409, then retains revision-checked persistence and background reapplication. Omitted fields leave stored settings intact; malformed replacement Fast lists or project maps reject the whole request. Platform identity normalization remains in UserSettingStore. The account settings editor is a real consumer.
  • src/api/schemas/memory.ts owns embedding and categorization patch schemas shared by PUT /config and PUT /memory/embedding|categorization. Embedding dimensions are a positive integer or null; omitted dimensions preserve the stored value. All block fields remain optional and unknown keys are stripped. The memory settings editor can send just { dimensions: 1536 } to the embedding endpoint, with no provider request or reindex performed by validation. Memory and category PATCH schemas derive from their create schemas through partial(); only memory patches add status, preserving trimming and field error messages.
  • src/api/routes/plugins/index.ts composes installed-plugin listing, detail, data and lifecycle routes. configValues.ts owns the instance config writer plus the shared validation, secret masking and patch rules consumed by userConfig.ts; both use the live account model gate and canonical shared/pluginNumber.ts alignment. consent.ts keeps installed enable decisions and approved marketplace candidate decisions distinct, including empty grants versus no new grants, while sharing stored-consent reads and acknowledgement comparison. marketplace.ts owns literal marketplace routes, control dependency resolution and its HTTP error mapper. Registration stays before /plugins/:name, and the parent supplies the existing post-persistence restart action to the config writer. Missing services retain their existing unavailable responses; no extra registry or store is introduced.
  • src/api/routes/brainOauth.ts registers the setup-tolerant admin OAuth endpoints from the brain family. brainConversationLinks.ts owns the authorized conversation roots, independent sub-agent/cron statuses and validated link projection. It reads the current registry's cron control owner for both the plugin grant and encoded /p/<owner>?job=<id> destination. Missing controls or grants are unavailable, read failures are errors, and an available empty contribution is an empty list. The history register consumes one read per listing, with the existing 100 explicitly requested-root limit and unchanged transcript ownership checks.
  • Brain-route API characterization keeps core conversation reads and lifecycle tests in tests/api/brainRoutes/admission.test.ts; tests/api/brainRoutes/ separates attachments, chat actions, debug reads, model selection, plugin-owned operations, processes, provider routes, send admission/contributions, streaming, and telemetry by their route owners. fixtures.ts owns the shared fake brain, provider contributions, authenticated request builders, and setup(), which creates fresh in-memory stores and call-recording state for each invocation. Importing the fixture registers the existing per-test log-sink reset and temporary-root cleanup; suites that create disk fixtures add their roots to pluginRoots. For example, telemetry.test.ts supplies an MCP or LSP provider to setup({ plugins }); without a provider, the server receives no plugin registry and the tests retain the route's absent-contribution assertions. This fixture is test-only and performs no provider requests of its own. Complete test statements and original describe/test names remain unchanged across the split.
  • src/brain/ contains the brain service and its domain subsystems: session lifecycle, turn admission and execution, prompts, model and provider routing, tools, memory, channels, delegation, recovery, context management, usage, cards, artifacts, and conversation views.
  • Interactive history has one bounded store projection, BrainStore.transcriptPageRows (src/store/brainStore.ts:1750), used by the private BrainStatusService.historyPage (src/brain/service/statusService.ts:658) for HTTP pages and SSE snapshots. A stable exclusive entry-id cursor walks the active parent chain by the existing (session_id, entry_id) index, stops before the old history, and projects/shapes only that window. The public BrainMessagePage wire contract is shared by web and CLI; no-limit reads default to CHAT_HISTORY_PAGE_SIZE instead of the retired eager array response. Live replay echoes are excluded before page counting, and the parked-restart exception examines only provisional current-run rows. Full journal reads remain explicitly for model replay and export. See docs/WEB.md, Interactive transcript history, for client scroll and resync semantics.
  • src/brain/service/ contains orchestration for spawning live sessions, resolving working directories, building turn context, running turns, activity, permissions, compaction, and session teardown.
  • ConversationLifecycle (src/brain/service/lifecycle.ts:63) retains conversation addressing and delegates in-place clear/rewind to src/brain/service/conversationReset.ts and temporary image-model respawns to src/brain/service/visionHop.ts. Both use the same live registry, spawner and durable journal; they add no stored state. Clear and rewind retain distinct refusal text, check before waiting and again inside the send lock followed by the session lock, and share same-id respawn only after their own journal rewrite. Rewind preserves detached work whose originating call remains in history; clear also refuses pending child results. Cold clear rewrites storage without spawning, while cold rewind publishes the existing history-refetch notification. Vision hopping keeps the id, client attachments and working directory, marks only a reached fallback route, restores the previous model/reasoning profile on the next text turn and pauses goal continuation on a failed respawn.
  • src/brain/service/emptyConversations.ts owns the synchronous unspoken-shell deletion predicate shared by fresh starts and SessionTeardownService.stopSession. It retains live or client-claimed conversations, spoken history, explicitly named rows and deliberately cleared rows, and clears an active pointer only when deleting its row. The scan costs one pass over the user's stored conversations; dropping one shell uses only existing store and attachment reads. SessionTeardownService.descendantSessionIds (private, src/brain/service/sessionTeardown.ts:588) is the single breadth-first durable run traversal for process cleanup, deletion and retention, with repeated child ids visited once. Cleanup collects the whole tree before its first local/runner process stop; every stop must be confirmed before any session row is erased. No live child claim is required to discover a durable descendant.
  • Current conversation indicators have one projection: BrainStatusService.turnControl(live, sessionId) (src/brain/service/statusService.ts:382) returns the shared BrainStreamControl. Status, atomic snapshots, owned/admin lists and presence consume it; lifecycle, elicitation and child/workflow changes publish it through the existing replaceable control replay event. Its programStatus uses public PI ProgramStatus.state/kind: active turns are working, actual approvals/questions are blocked, an undecided settled plan is blocked/question, and durable failed maps to error here only. streaming remains the foreground control edge, including admitted retry/compaction gaps and PI's public isCompacting during manual compaction. Both compaction start and end publish control; a recorded plan dismissal fans out through the existing activity callback to chat control and owner-list invalidation for live and cold sessions, while stale decisions and failed writes publish no success; backgroundWorking reports canonical spared children and background workflows independently. Unconfigured MCP alone does not block a conversation. No observer means no extra work beyond the existing bounded replay publication; there is no status table, polling, prompt injection or database migration. Durable activity, seq/readSeq, unread and automation remain the result/read authority. CLI consumers send the viewed lane's projection to public ProcessTerminal.setProgramStatus, clear it on teardown, and use setTitle for native title encoding while retaining process.title for process-based tabs. Platform chat adapters classify control as explicitly unrendered in the shared live-message reducer: owner-composer and terminal chrome have no room equivalent, while the matching ask events still render the actual question or approval.
  • Progress publication uses the existing BrainService.publishToSession seam. Mutation publishers mark recorded subagent/workflow progress with progressChanged; replaying a display frame does not re-project parent control. publishControl reads nothing when there is neither a live replay journal nor an attached parent stream. For watched parents and children, BrainStatusService.progressRows loads the parent delegation/workflow rows once per publication; turnControl, canonical sparedChildSessionIds and child delegation projections share that transient input. The input is never retained across publications, and live/boot workflow checks still apply. Resumed workflow publication is a real consumer: two watched nodes share one read of each parent row set. Status before the first conversation passes an explicit null session to turnControl and gets idle Build control; an explicit invalid durable session still fails through the existing ownership/store contract.
  • Tool timing uses the existing native journal and ToolTrace seam. Direct tool_execution_start supplies one epoch startedAt; every direct completion carries PI's durationMs, including silent completion, diffs and media. ToolTraceSink.call is the single nested-call measurement boundary because Code Mode calls do not pass through PI's direct executor: it records epoch start/reason and a monotonic performance.now() duration in the same bounded trace and durable settlement. Consumers never estimate stored durations from assistant timestamps. Optional timing means an older journal genuinely has no measurement, so it displays none. Explicit pending state settles only the matching call ID and can outlive a yielding exec/wait parent. Image deduplication suppresses only the repeated picture: every image-producing call still publishes its matching settlement and native duration. Existing trace count/character budgets remain; grouped rows retain their latest detail/count and the actual pending member.
  • src/brain/session/ contains session-scoped prompt, capability, tool-search, code-mode, compaction, steering, runtime-frame, and live-session components. liveEventReplay.ts classifies replay merges only (module-private replayMergeKind, liveEventReplay.ts:104: adjacent text/reasoning deltas coalesce, keyed snapshot state replaces, everything else — steps, user/tool events, mismatched keys — is kept); cursor stamping, char budgets and eviction stay in the buffers. Two code paths use that classification: the live LiveEventReplay journal (liveEventReplay.ts:182, created by src/brain/service/spawner.ts:700) and the exported appendReplayBrainEvent array appender (liveEventReplay.ts:132), which the CLI headless reconciler calls (src/cli/chat/headlessReconciler.ts:94). The brain-stream route uses SerializedEventBuffer (src/brain/session/serializedEventBuffer.ts), which calls neither function; the comment at liveEventReplay.ts:126 still describes route-local buffers as mirroring these rules. modelStream.ts is the shared session-local ModelRuntime.streamSimple wrapper: it binds unchanged runtime methods and preserves custom values while nesting interceptors in factory order. Add wrappers with wrapModelStream(runtime, stream => (...args) => stream(...args)); a runtime with no wrappers stays untouched. The composer uses its property override only for its private marker, while recorder, watchdog, malformed-call recovery, persistence refusal, Responses chaining and Fast mode use the same wrapper. tests/brain/modelStream.test.ts pins the ordering and property behavior.
  • session/effectiveTiming.ts owns provider-generation timing independently of optional request capture: ProviderRequestRecorder forwards PI lifecycle events, request initiation, chat stream events and synthetic failures to one session-local EffectiveTiming. The owner resets turn/model identity at the existing boundaries, measures with a monotonic clock from stream start to successful terminal, and stamps the unchanged version-3 fields on the terminal message before journal persistence. Failed attempts, backoff, tools and compaction contribute no speed sample; streaming stays unknown until the first valid terminal. The weak session map exposes a copied settled aggregate to events.ts, whose sessionUsageSnapshot supplies the same rate to CLI and web and reads persisted v3 fields when no live state exists. Disabled capture still measures normally; a capture correlation failure discards the current attempt markers while retaining prior settled samples, exactly as before. Timing performs constant work per event without database reads, timers or streamed-character estimates.
  • BrainSessionFactory owns store preparation, journal rehydration, runtime composition and persistence subscriptions. session/compactionThreshold.ts owns the unchanged reserve arithmetic, emergency/hard-ceiling limits, fixed-cost and rendered-prefill estimates, minimal retained tail and configured-provider/model percentage lookup. The factory samples the live render, re-applies thresholds after measured usage or compaction, and passes the same budget to the circuit breaker and cold-start assessment; without a per-model override the global percentage applies. BrainService and LiveSessionSpawner use the same percentage lookup.
  • session/resourceLoader.ts owns BrainResourceLoaderOptions and defaultResourceLoaderFactory(options), used by the factory to carry supplied prompt chunks, plugin skills/templates and project instructions into PI. All disk discovery stays disabled. Inline handlers retain their registration order: the compaction breaker precedes summarization, live recall precedes step context, and run-boundary handling follows both. Missing optional contributions install no handler or preview work. The factory supplies journal-append callbacks and reloads the loader before creating PI's session. Assembly performs no provider request; its cost is loader reload plus the contributed handlers' existing work.
  • Fork cache diagnostics use ForkCacheVerdict.prefixMiss from session/forkPrefix.ts to decide whether forkPrefixDiff.ts reads captured parent/child requests. Display reasons and log text remain independent of that typed decision; different models, absent cache accounting, unknown parent prefixes and failed first requests perform no comparison. Capture disabled or unavailable keeps the optional first-difference phrase absent. The session factory's first-response subscription is the real consumer; only actual counter-based misses read captured-request history.
  • src/subagent/ dispatches and supervises optional out-of-process delegated runners. src/brain/delegatedTurn.ts and src/brain/service/delegatedSession.ts connect the runner boundary to durable brain sessions.
  • src/store/ opens SQLite and contains domain stores for brain sessions and messages, configuration, accounts, Projects, usage, memory, embeddings, plugin state, provider requests, events, and related durable data.
  • src/plugins/ loads manifests, builds registry generations, exposes plugin controls, runs services and hooks, and enforces plugin paths, policy, grants, and lifecycle.
  • src/shared/ contains contracts and cross-cutting rules such as identity, Project execution references, paths, wire formats, time, logging, and shared web mirrors. wireContract.ts is a type-only browser boundary with no module import declarations or runtime exports. Its only external type dependency is the erased public PI ProgramStatus query for BrainStreamControl.programStatus; there is no daemon-path dependency or PI runtime in the web bundle. The TypeScript syntax guard rejects every other type query, import, runtime load and re-export. It owns wire shapes such as the ActivityEvent store base, brain cards, and REST/SSE rows; web imports these types only through web/lib/types.ts. Brain cards require a plain-text item form understood by every client, while structured labels, stable ids, owners, unresolved blockers, timestamps and measured durations are additive. Card ids replace their prior snapshots, and empty cards remove them. src/plugins/manifest.ts owns plugin config field types and the single renderable-field list used by its validator; web consumes that declaration type-only. The team-feed route enriches ActivityEvent per response as ActivityFeedEvent (ranked tools, cleared target) and nothing else — absent enrichment the row ships as stored, and the dashboard ActivityTile renders the route view. src/brain/events.ts re-exports the shared card types for terminal consumers. xml.ts owns XML 1.0 control-character sanitation and separate text/attribute escaping; use xmlEscapeText for element content and xmlEscape for quoted attributes. displayFormat.ts owns the shared formatters both CLIs consume — formatCompactCount for dense token labels, formatSpeed and cacheHitPct for generation telemetry; elapsed run times come from elowen-plugin-shared/duration (see the packages/plugin-shared/ bullet below).

wireContract.ts also owns SkinCatalogueEntry/SkinCatalogue, stored PermissionSettings and NoninteractivePermissionBoundary, and the complete TerminalSettings/TerminalPalette shape. Existing store, permission and skin modules re-export their consumed type paths; web imports only these erased types. Stored permissions have no HTTP revision; PermissionSettingsSnapshot adds the existing optional client generation, while TerminalSettingsSnapshot adds its required generation. showThoughtsCli is required, matching the store's sanitized output. This changes no persisted JSON or route validation and requires no migration. Terminal font size and scrollback use the existing numeric clamp with their original bounds and defaults; model capability families reuse ladderToMap for the same low/medium/high ladder.

modelGrantRule.ts owns ALL_MODELS_GRANT and grantsAllModels(list); execs.ts retains its consumed exports and uses this predicate for execution grants. Only ['*'] grants all models, an empty list grants none, and mixed or repeated stars do not grant all. Its importless browser copy web/lib/modelGrant.ts is byte-pinned, as are pluginNumber.ts and nameGrammar.ts. The grammar is consumed by browser asset editors and problem reporting, and by published Sites gateway slug validation in core. These rules do constant-time or existing input-size work, perform no I/O and add no alternative permission or resource-name policy. Plugin editor number conversion lives in web/modules/settings/pluginConfigNumber.ts. It reuses the pluginNumber alignment predicate for slider eligibility, retaining display divisors, bounds and decimal step rounding before draft writes. Sandbox's CPU defaults consume it; without display metadata, canonical units stay unchanged.

  • src/embeddings/semanticTypes.ts owns the shared document and vector-cache contracts and the default semantic relevance floor used by memory and site search. Memory recall may apply its operator-configured override; consumers should not copy the defaults.
  • src/auth/, src/embeddings/, src/git/, src/integrations/, src/mcp/, src/privileged/, src/prompts/, src/push/, and src/search/ provide focused boundaries used by the core. src/projects/ holds the one Project service the HTTP routes and the agent's Project tool share (see "Project management" under Data flow of a turn).
  • src/cli/ implements the terminal client. It uses the daemon API and does not own durable brain state. Chat composition sends native program status only when its state, application, blocked kind or message changes; PI still owns capability detection, terminal restart replay and shutdown clear. Empty-chat notice overflow is measured once during frame preparation and reused by frame geometry and root rendering; event-time layout checks remain fresh. A work-mode toggle before the authoritative conversation status arrives shows localized unavailable feedback and leaves the mode unset.

CLI transcript tool blocks have one rendering owner, src/cli/chat/toolBlocks.ts. turnRenderer.ts imports framedDiffBlock for file edits and toolOutputBlock for daemon-projected output directly; rail panels, approvals, user rows and spinner frames remain in components.ts, with no compatibility reexports. Each diff is sanitized once, then logical-row preview limits are applied before ANSI-aware wrapping and painting. A preview longer than 18 logical rows exposes expansion; empty output contributes no body rows unless command, status or notes supply context. The module performs no I/O and keeps the existing bounded preview and synchronous visible-row work. codeHighlight.ts owns lazy grammar loading and token caches shared with Markdown: an unloaded grammar keeps plain diff rows until a load notifies the registered render hooks. Each composition keeps the function returned by setCodeHighlightListener(callback) and calls it on disposal; unregistering one hook leaves other compositions subscribed.

Headless reconnect output is owned by src/cli/chat/headlessReconciler.ts. runHeadless supplies its event sink, feeds live events and transport snapshots, and loads durable history pages at idle. The reconciler uses the shared replay normalizer and durable row IDs to emit only missing bytes; initial history is a silent baseline. It retains only invocation-local replay/identity state and owns no requests, timeout or terminal exit policy.

Workflow terminal geometry is owned by workflowCanvas.ts::layoutCircuit: one CircuitLayout returns card placements and routed dependency edges from the shared layout engine. WorkflowModal.render reuses that result for selection, viewport dimensions and drawCircuit, while narrow mode renders the same placements as a wave list. Drawing never recomputes layout; each non-empty frame performs one DAG layout and cell painting, with no stored layout cache. Navigation computes fresh placements for the current snapshot separately. Empty or absent workflows keep their message frame without a layout calculation.

Opt-in ELOWEN_TUI_PERF=1 physical-frame diagnostics (src/cli/chat/tuiDiagnostics.ts:163; ELOWEN_TUI_DEBUG=1 enables the same writer, and an unset ELOWEN_TUI_LOG falls back to a file in the OS temp directory) write content-free JSONL to ELOWEN_TUI_LOG. Each frame snapshots animationTimers from the existing AnimationController.timerCount after PI finishes preparing the root and overlays. The goal and long tmux gates wait for a frame with zero live animation handles before measuring idle frame counts, so legitimate rail-collapse and mascot motion cannot race the idle assertion, and a leaked timer cannot pass during a quiet gap. Disabled diagnostics perform no traversal or recording; this counter neither changes animation scheduling nor adds timers. The aggregate tmux evidence contract requires both the widened 46-column rail and the restored 36-column rail checkpoints. Read-only child checkpoints require the visible locked-composer narrative instead of an editable-composer caret; all other frame and raw-evidence checks remain in force. analyzeFrameDiagnostics retains work for every frame containing animation:mascot in the broad mascot summary, including ticks coalesced with input or geometry. Its onlyFrames and onlyMaxRenderedTurns cover frames whose sole reason is that animation. Both the long scenario and raw-evidence aggregate require non-empty pure-animation evidence with zero settled-turn work; mixed-frame timing and scroll budgets still apply unchanged. Raw frame validation requires animationTimers to be a non-negative integer.

The session-bound src/cli/chat/brainClient.ts owns REST requests, endpoint-specific decoding and the committed conversation/client generation. Its private requestBinding supplies fenced parent mutation bodies for send, interrupt, background controls and commands; explicit child targets omit the parent attachment, and quit escalation with afterStop omits the released generation. Before start there is no fence; a server rebind retains the committed generation. Stop remains separate: it fences the highest issued start generation, including a still-pending switch. The private checkResponse preserves Unauthorized identity and each endpoint's error prefix/path; provider 403, stale-plan 409, rename and POST server messages remain endpoint-owned. It performs no decoding or extra requests.

BrainClient.stream delegates to src/cli/chat/brainStream.ts, which also owns the shared BrainStreamFrame type used directly by the client, coordinator and headless consumer, through a typed port for fetch, headers, current binding, rebind and the Unauthorized constructor, without a runtime import back to the client. The transport reads binding on every reconnect, starts onOpen on the first body byte, and rearms its 75-second silence watchdog on all bytes, including pings. Snapshot and live event cursors remain non-enumerable replay metadata. Explicit child streams neither claim nor rebind the parent attachment. StreamCoordinator and headless chat use the client method; without an explicit session it follows the bound conversation. Parser work is linear in received frames, with one attempt watchdog and one abort-aware reconnect delay; there are no added requests, mutation retries or durable state.

src/cli/ui/fieldEdit.ts owns the pure printable-input decoder, FieldState, newFieldState, editField and inputWindow. Text and password prompts in ui/prompts.ts share its UTF-16 caret state, untouched-prefill replacement, grapheme navigation/deletion and terminal-column scroll window. Password rendering uses one bullet per grapheme without changing the stored value. Chat picker filters and rename input in chat/picker.ts import printableInput directly for ordinary, Kitty and bracketed-paste input. Submit/cancel and validation stay with the caller; non-editing input returns null from editField. The module does no terminal I/O, scans only the current input/value and shares one grapheme segmenter; prompts do not re-export it.

CLI startup (src/cli/chat/brainClient.ts:162) bounds the one /brain/start attempt to 5 s and exits with an explicit daemon-unresponsive error on timeout; it never retries a mutation. First-paint metadata reads have 3 s deadlines and keep their existing offline defaults. Transcript hydration has its own 10 s cancellation and retention lane; live SSE uses a separate silence watchdog, not these short read deadlines. Login has a 10 s deadline (src/cli/chat/token.ts:50). These bounds cover asynchronous waits, not a daemon blocking its own event loop synchronously.

CLI setup provider writes share upsertBrainProvider(ctx, entry, providers) in src/cli/setup/steps/shared.ts. Both wizard AI/memory steps and headless setup pass the public provider list read from GET /config; PublicProvider derives from ElowenConfig. The helper strips only apiKeySet, merges explicitly supplied fields into the matching id, preserves list order and every other public setting, and sends one PUT /config. Omitting apiKey leaves the stored secret intact. With no match it appends the new entry; the list transformation is linear and adds no reads or retries.

Memory setup uses openRouterEntry(providers, apiKey) to complete the first matching OpenAI-type OpenRouter endpoint or allocate an unused id through the existing unique-slug helper. Pass the returned entry and the same public list to upsertBrainProvider to preserve the endpoint's settings, models, label and position. The builder is pure and scans the list; it does not fetch, save or validate credentials. runMemoryStep first reuses an already-keyed matching endpoint without prompting or writing. Headless setup calls the builder when a memory key is supplied, including rotation; without a key it only reuses an already-keyed entry. Skipped memory setup never calls this builder or changes providers.

setInstanceDefaultModel(ctx, providerId, model) sends one PUT /config containing only brain.defaultModel: { providerId, model } and returns whether the save succeeded. Both runAiStep and runHeadlessSetup call it after choosing a provider and a non-empty model; when no model is chosen they skip this explicit default write. Provider-save initialization remains owned by the config API, which validates an explicit default pair against the offered conversation catalog before persistence. A refused save leaves the stored pair unchanged and produces the existing setup warning while the provider smoke test continues. The helper adds no reads or retries and writes no retired executor configuration.

CLI text and password fields use stripControlChars from src/shared/text.ts through printableInput in src/cli/ui/fieldEdit.ts. The shared helper removes C0, DEL and C1 characters from raw printable input and bracketed paste. Kitty printable decoding still returns its decoded text directly; paste framing and unmatched-escape handling remain in printableInput, not in the shared sanitizer.

Interactive login checks both input and output TTY availability before prompting; Esc reports cancellation and neither logs in nor opens chat.

CLI suggestion state lives in src/cli/chat/suggestionController.ts. Its createSuggestionController consumer in chatComposition.ts wires the editor's change callback and the existing InputRouter actions/getters. The controller owns slash, model-argument and file-mention overlays, their stable handles and the lazy application-wide model catalogue cache. OverlayController still owns native overlay creation, reflow and teardown; composition supplies a geometry callback evaluated from the current prepared editor/queue/attachment/telemetry budget. No active suggestion means no suggestion reflow on budget changes. Slash filtering preserves selection for unchanged text, mentions reuse the existing bounded project index and frecency, and model scoring displays at most twelve candidates. The first model argument starts one application-lifetime catalogue request; late replies update only a still-active argument overlay, remain useful across conversation switches and cannot publish after application shutdown. A rejected catalogue keeps the existing empty cached result without a retry. Disposal releases local overlay references after native teardown.

src/shared/chatPresentation.ts directly owns composing-label precedence, locale types and marker thresholds for CLI consumers such as TurnRenderer and chatComposition. The former CLI reexport module is removed; these pure rules add no I/O and preserve the existing Czech, Slovak and English labels.

StartScreen and startScreenInputTop share one private row allocation in src/cli/chat/startScreen.ts; OverlayController uses the exported input position to anchor suggestions. The status row remains pinned, compact screens reserve input then notices, model and hints, and render model/hints before notices. Without notices their reservation is zero; without a mascot its spacer also disappears. Allocation is constant work and preserves the cursor-aware input viewport. SuggestionOverlay formats each label/description once and uses the shared color.selected truecolor foreground, so bold-intensity terminal settings cannot turn selected black text into bright black.

The MCP runner declaration contract is src/plugins/mcpSnapshot.ts: each connected instance server contributes tool name, optional title and description, and verbatim optional object inputSchema and outputSchema. The parser applies the same object boundary to both schemas; malformed required entries reject the snapshot. The bundled MCP bridge's registerBridgedTool consumes all five fields, including structured Code Mode results. An empty snapshot declares no inherited tools; an absent snapshot uses normal boot discovery. Snapshot registration opens no MCP connection and costs one linear descriptor copy; first execution resolves the client lazily. Personal and project-bound definitions continue to come from their authorized persisted cache. Offline descriptor parity uses parityTools(many) exported by scripts/tests/subagent-parity/mock-mcp-server.mjs, the same list served by the mock; importing it starts no server. Prompt baselines remain the fields captured by run.mjs, without a second descriptor copy.

Within the bundled MCP plugin, plugins/mcp/lib/managedSession.mjs (lease heartbeat every 5 s, line 70-75) owns live binding authorization and createManagedSession returns the operation-scoped withProjectClient used by tool calls, resource calls and management verification. The bridge injects its native client factory, transport authority, binding validation and typed failure reporter; the session module has no import back to the bridge. Without Sandbox or a linked binding actor it fails closed. Each operation acquires a fresh guest lease, checks the execution target and verified cancellation, renews every five seconds, stops on abort or heartbeat failure, awaits cancellation and child closure, then releases the lease. Cleanup failure remains an error. CLI and web consume the plugin's existing plugins/mcp/serverPresentation.mjs for reconnect eligibility, reconnect-all selection and returned failure text; sign-in stays a separate action.

The subagent runner's observational tap (src/subagent/runnerHost.ts:251-296) waits at most 2 s for IPC acknowledgment, then detaches its message/exit listeners, sends untap and lets BrainService.tapSessionSnapshot return the durable local snapshot. An absent runner follows the same fallback. The tap does not start a runner; a timed-out runner may still finish its own work independently.

The shared update contract's runtime companion, src/shared/updateStatus.ts, owns pluginUpdateRecords(status, name, requestId?). Web progress and the setup LSP waiter use its queue/journal/report selection by plugin name and request id; the web reaches it through the byte-pinned mirror web/lib/updateRecords.ts, because web code never imports daemon runtime. Missing records remain undefined; selection is three bounded list scans with no I/O or completion verdict. Setup still accepts only a matching report listing its request, with a committed or noUpdate run and an applied plugin outcome. It retains queue precedence, waits for retained terminal journals without a report, and bounds polling and reconnects to 120 seconds.

src/brain/sessionErrors.ts::isTurnAdmissionRefusal(error) owns the common local-only logging classification for POST /brain/send and POST /v1/agent/runs after their account refusal handling. It recognizes unknown, forbidden, read-only, not-running and aborted sessions; unexpected failures keep code-only reporting. The own-run endpoint handles busy before classification; ordinary send never requests busy refusal. Stop-session keeps its narrower teardown classification. The predicate performs only typed error checks and does not change HTTP responses or isAccountRefusal.

SessionSpec and BrainResourceLoaderOptions carry no native skill list. The resource loader always sets noSkills and returns an empty skillsOverride. Registered skills are announced through the live turn catalog and loaded through SkillLoad or plugin input transforms under current authority. Optional loader inputTransforms is { transforms, userId }; userId is required when the object exists. The factory uses ownerUserId as the fallback, while currentContributionUserId is resolved at input time so a linked room writer retains their own scope. sessionId remains the durable transform identity. Without inputTransforms no input handler is installed. The bundled Skills input transform is a real consumer; no skill bodies enter the cached native catalog.

LiveSessionRegistry.anyLive(sessionId) reads the personal-session bucket first, then the channel bucket only for a channel session id; absent records return undefined. Status, stream publication, delegated identity and restart checkpoint consumers use it without changing independent existence or running-state checks. withConversationLock(sessionId, operation) takes the send lock followed by the bare session lock. ConversationReset.clearConversation and rewindConversation, hidden turn dispatch and plan dismissal consume it. It is not reentrant: a send holding the outer lock calls ensureLive, which takes only the inner lock. The helper adds no runtime state beyond the existing lock queue.

DelegatedSessionService.sendDelegated consumes the same toolAuthorityForUser resolver as owner and channel turns. First spawn and every continuation, result drain and recovery use delegatedAuthorityUserId from delegatedScope: captured settingsUserId, then contributionUserId, then the durable row owner when neither exists. A shared-room child's grant belongs to its sender, independently of the room owner; delegationBelongsTo still owns principal matching. BrainService supplies users and the current PluginRegistryProvider. Each idle continuation, result drain or recovery dispatch intersects the captured allow list with the current account grant and adds the current ungranted-plugin tool denies. No plugin provider adds no plugin-specific denies; the finite account grant still applies. This can only narrow captured authority and does not rewrite it. Steering an already-running child retains that turn's existing authority. DelegateContinue is a real consumer.

ClientRequestStaleError in brain/sessionErrors.ts identifies a closed client start generation or a claim lost while waiting for the session writer. ConversationLifecycle.start throws new ClientRequestStaleError() at those two refusal sites; POST /brain/start recognizes the class and returns HTTP 409 with { error: 'client request is no longer current' }. Without a stale refusal, start behavior is unchanged. Unrelated failures remain HTTP 500 even when their message matches. The type does not change claim ordering, persistence or cancellation and adds no retry. A CLI start reordered after its stop consumes this refusal.

Account administration uses RouteContext.notAccountAdmin from createAccessHelpers. Minimal use is to return HTTP 403 when notAccountAdmin(request) is true. It reads the current users.isAdmin bit and denies a missing account or users store, including when project tenancy is absent. Daemon-wide notAdmin and first-run notAdminUnlessSetup retain their separate open-mode rules. GET/POST /users apply their explicit first-account setup exception before this gate; user administration and impersonation consume the strict helper directly. The check is synchronous and performs one account-admin lookup.

Config enablement reuses consentFor({ config }, name, manifest, undefined) from src/api/routes/plugins/consent.ts. This internal decision reads stored consent and compares the installed manifest's required grants; it does not seed, persist or restart anything. createConfigPluginConsentGate, consumed by PUT /config, checks only newly added plugin names and refuses unknown or undiscoverable manifests before invoking it. Without an acknowledgement, absent or incomplete stored consent refuses the whole required grant set; an unreadable set is logged by storedConsents and refused. Covered grants permit enablement; manifests with no consent-required grants need no stored-consent read. Unchanged enabled lists, removals and patches without an enabled list skip the decision. Cost is the gate's existing discovery scan and one consent-store read per judged plugin with required grants. Config writes cannot record new consent; the installed-plugin enable route remains the real consumer that accepts and persists acknowledgements.

The CLI stream parser in src/cli/chat/brainStream.ts returns only data and optional id plus the unconsumed buffer tail: call parseSse(buffer), process frames and retain rest for the next chunk. SSE event names are ignored without discarding their data; the BrainEvent type comes from the JSON body. Multiline data and comment-only heartbeat handling are unchanged. streamBrainEvents consumes this framing and restores replay cursors from id and snapshot eventCursors.

HeadlessSnapshotReconciler in src/cli/chat/headlessReconciler.ts receives live events through live(event) and reconnect snapshots through snapshot(frame). runHeadless is its consumer. The current daemon can return durable waiting views without cursors when no live replay exists, and can append those views after a cursor-stamped replay. The reconciler retains the normalized local tail, suppresses repeated identical cursorless views and emits only missing text suffixes for coalesced replay entries. Without waiting views there is no additional output. This adds no I/O or public API; comparison scans the existing local tail for each cursorless snapshot event and does not replace durable history reconciliation.

The standalone operations library in scripts/elowen-site-gateway.mjs keeps one private NSPAWN_REQUIRED_PACKAGES list for nspawn status and provisioning. A minimal request is { domain: 'nspawn', op: 'status' }; an operator root request also names the service user. Status reports missing packages without installing them. Provisioning installs only missing packages, in systemd-container then iproute2 order, on supported Debian and Ubuntu hosts. Installation failures abort provisioning. The bundled Environments runtime consumes these readiness results. This is an internal constant, not a plugin contribution point. The unused defaultReadDescriptorOwner export and readDescriptorOwner test fixture key are removed; live descriptor identity and ownership checks continue to use the existing fstatSync paths.

Session identity read model

User drill-in is read-only at every depth. BrainService.preflightSend (src/brain/brainService.ts:2326) rejects owned child targets with SubagentReadOnlyError (src/brain/sessionErrors.ts:18) and conceals unknown/foreign ids. Both send and startSend use that same admission seam for explicit targets, so the generic send endpoint cannot bypass the restriction. ConversationLifecycle.ownedUserSession remains ordinary-session authorization only: non-send status and headless-run opening continue to conceal child targets with UnknownSessionError. The Subagent plugin's POST /brain/subagent/send (plugins/subagent/index.mjs:117-123) calls the payload-free host.subagentOwnerApi().preflightSend(): never and returns 409 before reading or validating any body, including malformed JSON. Core's BrainService.preflightSubagentSend (src/brain/brainService.ts:1806) supplies the same typed refusal; no provider turn, origin pin, queue entry or session lookup is opened. There are no human send or pinOrigin operations. The parent agent still uses the existing delegated continuation/confirmed-steer seam with its durable ancestry, principal and scope checks. CLI composition displays only the viewed session's input state: children have no user editor or parent queue, while the bound parent's draft/queue stay available on return. Web child views use the existing read-only provider state and null plugin session id.

src/brain/session/sessionIdentity.ts projects BrainSessionIdentity from one selected session for both BrainStatusService.status and streamSnapshot. Delegation progress uses the same projection through delegatedChildIdentity, storing its model and effort subset for the agents table, telemetry rows and terminal panels. A child's snapshot owns the CLI footer and web FAST tag; ancestor projections remain only for delegation names, elapsed call time and workflow topology. Exiting drill-in rehydrates the parent's own status.

Live PI state supplies the effective model and post-clamp effort. A remote runner's tap invokes the same status service inside that runner, where its channel live record exists. Daemon-only oversight, a timed-out tap and settled children have no such record: the projection reads the durable session selection, delegated settings account and requested effort, and the local provider registry/capability catalog. It reuses PI's effort clamp, resolveFastModeRoute and fastModeAccount, which is also the request-account resolver used by the spawner. Ordinary rooms without a verified turn writer cannot borrow their host's Fast preference. No configured route means no Fast availability; removed provider ids remain readable with an empty label. Cold parent effort comes from PI's restored journal context. No new persistence or network request is introduced; cold reads reconstruct the local catalog and restore journal effort only for a reasoning-capable model without explicit scope/dispatch effort. Live reads avoid journal/catalog reconstruction.

The owner child-model switch uses the existing runner release and session-lock seams to dispose idle runtime before publishing the new selection. It returns the same identity to the picker and broadcasts resync: model-switched; web and CLI child taps reopen that same child, not the parent. Runner transcript taps retain a daemon fixed-session control tap for these resyncs; its combined disposer releases both taps, and ordinary progress is never duplicated. Model permissions and owner ancestry are revalidated after IPC; running children still refuse switches, and a runner whose idle child could not release its resources refuses with a retry message instead of the running-turn one. Fast commands remain unavailable in child focus.

Parent subagent/workflow publications project child delegation sidecars only for attached session listeners, including transports on hidden tabs. A live runtime alone is not a listener. BrainStatusService.delegationsFor verifies each requested child's durable parent, then reads active children, delegation rows and parsed workflow nodes once per publication. delegationFor uses that same projection for authoritative reconnect snapshots; nothing is cached across publications or copied into the transcript. Parent frames and watched child frames keep their synchronous order.

Live conversation authority

resolvePolicy in src/plugins/policy.ts captures a session ceiling once, then exposes allowedProjectIds as a live read of current account authority intersected with that ceiling. Demotion removes unrestricted host access immediately, retaining only current project assignments; a scoped session never widens on promotion or later grants. Deleted/deleting projects are excluded. allowedPaths, canAccessProject and canExecuteHost consume that same effective decision. Keep the policy object live rather than spreading or caching its effective marker. File containment, working-directory validation, named host targets and authenticated identity projection already read it. Delegated admin policies also recheck live host authority on each read during an admitted dispatch. currentIdentity intersects the turn's captured admin/owner bits with live host authority, so a demoted account cannot keep direct shell access through an identity minted before demotion. Queued host Write/Edit mutations recheck their anchored path after waiting, before any file read/write. Managed file calls retain the filesystem provider's per-operation live access check. Static policies without an account resolver retain their explicit supplied scope. Reads cost local account/project lookups only; no session rebuild, network request or persistence is added.