Navigace
ELOWEN · Dokumentace

Poslední aktualizace: 20. října 2026

Procházet dokumentaci · Daemon construction and identity

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

Developer reference

Daemon construction and identity

Initial Project identity

buildBrainCore seeds the configured host Project only during initial account setup, before any user exists. Later boots do not recreate a Project an administrator deleted, even when another Project has reused its slug. The protected daemon checkout is resolved by its configured path, not by a reusable slug or row ID; without a matching row the configured identity still protects that path during Project deletion.

Shared operation services

buildBrainCore calls createCoreServices in src/daemon/services.ts once and returns projectService, accountPresets, microsoftProvisioner, microsoftSso and memoryService. Bootstrap passes those exact objects in the required ServerDeps.services: CoreServices bundle. The brain's Project tool, the plugin host's Microsoft provisioning and HTTP routes share their process-owned instances. createRouteContext reads the bundle and builds only HTTP state, including the single WebPresence; it never constructs these operation services.

The factory reads one fresh configuration snapshot per memory recall-default, score-weight and deduplication callback; each two-field calculation uses one get() instead of two. buildBrainCore similarly reads the three live-recall budget fields from one fresh get() per search. Snapshots remain local to the calculation, never across calls. Its preset callback calls reapplyAccountSettings after commit through a late-bound brain getter, because presets exist before BrainService.create finishes. API fixtures use this same exported factory through makeTestApp or createTestServer. Partial fixtures without users or user settings omit account presets and Microsoft services, preserving the preset routes' 503 response. Without both memory storage and an embedder the optional memory service is absent; CRUD keeps its direct store path and computes vitality from live retention.

The lazy plugin registry reads enabled names once and lets loadPlugins request each discovered plugin's config through configFor. buildBrainCore no longer eagerly hydrates an enabled-name map overridden by configFor. This removes exactly one unused hydration per enabled name, including unavailable plugins, without changing loader capability, consent, secret-projection or registration boundaries.

Managed artifact startup

Daemon bootstrap installs setManagedSandboxResolver from its live plugin registry before any conversation is spawned. Runner composition installs the same resolver against its own registry. Each operation resolves coreSandboxSeam afresh; a missing Sandbox contribution stays unavailable. Session tools receive their explicit resolver without changing this process-wide background seam. Maintenance admits initial retention and physical-deletion passes, including plan, workflow-artifact and attachment sweeps, after required service settlement, recovery dispatch and its follow-up hook. Their existing intervals start at that point. Recovered turns need not complete first. Shutdown prevents late admission and drains already tracked work before pool closure.

The successful HTTP listen callback emits boot milestone: http-bound before maintenance admission. Boot logs then distinguish maintenance-start, claim-complete, platforms-ready and recovery-dispatched. Elapsed milestone times are measured from maintenance start, not HTTP bind. Service starts, alert synchronization and reconcilePluginServices report separate durations, including durations below the slow-step threshold. These overlapping stages must not be summed as independent readiness savings.

Background worker jobs

src/brain/backgroundWorkerProtocol.ts validates IPC requests for the fork owned by forkWorker. Its one-shot jobs are catalog refresh (pass), provider probe (probe), embeddings (embeddings), attachment sweep (attachments), transcript export (export) and plugin data summary (plugin-data-summary). Bound plugin-computation jobs retain one child for sequential requests. Credentials cross IPC rather than command arguments. The fork starts the shared event-loop lag monitor and watchdog under its own logger; shutdown releases their timers.

backgroundWorkerFork.ts resolves the child entry beside its own module. Compiled installs use the sibling .js entry and inherit Node flags. Source callers use the .ts entry and preserve every inherited process.execArgv entry, including preloads and memory/debugger flags. The fork adds the repository's scripts/registerTsResolve.mjs NodeNext resolver and Node's type transform only when absent; both --import value and --import=value retain an existing resolver URL or absolute path without duplication. This lets API-only callers fork the same worker without a running daemon or an inherited TypeScript loader; tests/plugins/pluginDataSummary.test.ts exercises the default fork against missing and populated data directories.

The admin plugin-detail route is the data-summary consumer. Core resolves a confined plugin data path, forks one request and awaits a validated PluginDataSummary; the JSON response is unchanged. The worker skips symlinks and stops after 50,000 entries or two seconds, reporting partial totals and unreadable entries. A missing directory reports exists: false. Fork, IPC, timeout and malformed-result failures remain errors with no request-thread fallback. The parent always calls giveUp after settlement.

readLogFile is asynchronous and reads a snapshot of at most 64 MiB in 256 KiB blocks. It retains and decodes only the requested tail, but counts newline bytes across the snapshot to preserve exact totalLines. File opening retains O_NOFOLLOW; truncation during the read fails explicitly. Chat attachment routes use asynchronous file reads after the existing ownership checks.

Database health diagnostics

The daemon supplies ServerDeps.databaseHealth: () => DatabaseHealth from its process-owned elowen-db instance, for example databaseHealth: () => database.health(). The dependency is required. createServer publishes its current snapshot as GET /health's database section; it does not acquire a connection, execute SQL, check server connectivity or read the statement map. An idle pool and a disconnected database therefore remain observable through their recorded figures, rather than making this liveness probe wait for database I/O. The old writerLock section is removed.

The snapshot contains total, idle and waiting pool counts; acquisition count, cumulative, maximum and most recent wait time in milliseconds; query count and total duration; the ten slowest queries among the latest 100; connection error count, last timestamp and SQLSTATE or transport code; and statementMapFailed. Query timings carry only fingerprint, calling function, duration and failure state. The database module collects these figures during ordinary work. Reading health only copies bounded in-memory diagnostics. Event-loop figures, licence state, platform readiness, runner-pool state and the health-only Server-Timing header retain their existing behavior. The runner-pool provider must likewise read in-memory state, including the current pool cap and enabled flag; it must not fetch configuration from a repository while serving health.

Queries above 250 ms emit structured slow-query events through the logger supplied when creating the database. Their fingerprint is the first 16 hexadecimal characters of the SHA-256 of exact SQL. New slow fingerprints asynchronously append SQL to the local statement map, with no bound values. The module creates its parent directory with mode 0700 and a new file with mode 0600. SQL can contain literals, so the map stays local and never appears in HTTP responses or telemetry. A map-write failure emits statement-map-error, sets statementMapFailed and rejects the eventual close flush. Connection errors expose only their code, never the original error text or connection URL.

GET /setup separately awaits users.count() before deciding whether onboarding is needed. Unlike health, setup intentionally reads the account repository. With no account repository, needsSetup remains false. src/cli/doctor.ts stays an HTTP consumer: its bounded public health probe precedes authenticated GET /system/readiness; it never opens a database or imports a store. The HTTP error handler imports AccountProjectError, UnreadableSettingsError and UnreadableUserSettingError from the public elowen-db entry point, preserving repository error identity and the existing 409 and 503 responses across the package boundary.

Shutdown construction

Bootstrap owns problem-reporting shutdown through the maintenance beforePause callback. After aborting any running memory dreaming fork, it stops account usage, awaits ProblemService.stop() and pauses the privileged worker. Separately, its awaited beforeCheckpoint callback shuts down call, dictation and ring input producers before the brain seals persistence, so a final owner-call transcript and in-flight admission cannot be lost to the pause. Maintenance stops only its own timers, background worker, attachment sweep worker, OAuth refresh loop and tracked asynchronous work; repeated stop calls share the same settlement promise. Its shutdown wrapper starts beforePause and maintenance cleanup together, and the shutdown handler waits for cleanup before closing plugin dependencies. An absent beforePause contributes no additional cleanup. This is daemon boot wiring, not a plugin shutdown registration API; the existing checkpoint guard bounds asynchronous cleanup. The real consumer is buildApp in src/daemon/bootstrap.ts.

BrainPauseRecovery.pauseForRestart checkpoints running turns and seals each resumable session through LiveBrain.persistence.pause() before reporting the checkpoint complete. The same handle stops native journal capture and terminal event reduction synchronously, drains the captured journal and provider-request queues, and exposes a process-lifetime paused promise. Provider and tool boundaries await that promise on later dispatch or completion, so a late provider abort cannot write another transcript, settle the parked turn, or reach accepted-send failure handling while bootstrap closes the pool. Boot constructs a fresh session and silently continues the committed tail; pause never aborts PI or waits for the model or tool turn to finish. Unparkable turns retain their existing bounded settle window and seal afterwards. The final resident drain also covers idle owner and channel publications; all drains settle before their failures are surfaced together as AggregateError.

Paired-computer RPCs stay pinned to their original executor connection. Bootstrap closes those sockets after the pause checkpoint; connection loss can reject an in-flight RPC with LOCAL_EXECUTION_LOST and an unknown mutation outcome. Its old session remains sealed. Top-level owner recovery supplies synthetic interrupted results and silently continues on the same computer. An offline computer leaves the park and queued input durable until an authenticated executor attach calls its onConnected(identity) hook, which hands the connection to ComputerPrerequisiteCoordinator.wakeComputer; core then dispatches BrainService.resumeComputerConversation, resumeComputerDelegatedCall or resumeComputerWorkflowNode, plus the plan-operation continuation, without holding attachment for a model turn. The conversation continuation shares ParkedConversationRecovery with boot, serializes concurrent discoveries through the durable claim, and rechecks that claim and the live connection before continuation. Opening a parked conversation cannot select a server fallback. Project removal atomically fails the waiting owner turn before ordinary target rebinding. Local delegated sub-agent and workflow recovery remain terminal. An unavailable park claim does not identify who consumed the marker.

Activity-log persistence is admitted asynchronously through the events repository. installEventRecording defers admission until synchronous bus producers return, preserves submission order, reports failed writes, and returns a drain function without unsubscribing the process-lifetime recorder. Bootstrap forwards it through maintenance to shutdown's beforeExit callback, after unparkable settlement, the restart notice and plugin service shutdown. The drain includes writes admitted while it waits, and the existing pause guard remains armed until the final drain finishes and the process exits. Forced second signals and an expired guard still exit immediately. Transient events do not write database state; admin audit writes are awaited by their caller.

Database and retention maintenance

PostgreSQL owns WAL and checkpoints. Each data-owning process holds one small pool through transaction-pooling PgBouncer; only daemon startup migrates. Pool shutdown awaits database.close(), including local statement-map writes. See database persistence for transaction boundaries.

createMaintenanceLoops in src/daemon/maintenance.ts calls repositories.maintenance.maintain(operation, input, now) directly on the daemon's PostgreSQL pool. The owning implementation is packages/db/src/maintenance/index.ts; each domain retains its transaction bounds, locks and policy.

Dashboard digest retention shares the existing event-retention callback and hourly tracked housekeeping owner after recovery dispatch; shutdown prevents late admission and drains admitted passes. Its UTC day cutoff and retained claim counters are specified in Persistence.

Scheduled event, token, alert, recall and usage retention cap each pass at 128 bounded batches. Each iteration awaits its PostgreSQL queries and commit. Origin redaction and each expiry tier have separate transactions, while each redaction fold remains atomic. Tokens and inherited-prefix reclamation retain their owners' SERIALIZABLE transactions. The daemon tracks admitted passes and drains them before pool shutdown.

Recall expiry first runs after boot recovery dispatch and then hourly, separately from daily memory eviction. Its 128-transaction / 128-row budget carries up to 16,384 expired rows per hourly pass. Other retention keeps its existing cadence. The typed operations, archive preparation and transaction boundaries are documented in Persistence.

Attachment maintenance

createMaintenanceLoops dispatches one attachments request through forkWorker after boot recovery dispatch and daily. The typed IPC request carries protected database configuration, the explicit instance root, image directory, pending image names and the clock captured at dispatch. runAttachmentSweep reads references asynchronously through its worker-owned pool, closes that pool before replying, uses the canonical attachment reference collectors and filesystem sweeps, then returns only removed image/file counts. The one-hour grace is measured from the captured clock, so new admissions remain protected while previously queued images are retained explicitly. No directory means no worker. One flight per maintenance loop prevents overlap; a 60-second timeout, malformed reply or failure stops the child and logs the existing attachment-sweep error. Maintenance shutdown cancels its worker and awaits the tracked promise. No journal writes, new scan authority or persistent job state are added. The work still scales with referenced history and stored files, but runs outside the daemon event loop.

Database latency is observed through the process-owned pool diagnostics and statement map described above. Distinguish asynchronous query or pool-acquisition waits from event-loop stalls using the existing lag monitor and watchdog. PostgreSQL repository tests use the ephemeral cluster harness under packages/db/testing/; attachment work still crosses the typed background-worker boundary. CPU profiles and any extracted conversation bodies remain private instance data and must not be committed.

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 awaited fakeDeps() call creates an isolated PostgreSQL fixture and its own session state through tests/brain/postgres.ts. 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 exposes the same count result and uses independent instances per server context for password/Microsoft SSO/password-proof attempts and OTP attempts: ten per origin in five minutes each, with Retry-After on refusal. Only final authentication clears the first-step budget; returning a TOTP challenge does not. The OTP origin counter is not cleared by password or factor success; the repository independently owns the durable shared per-account OTP budget. 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, authSecurity.ts for the existing Account security surface and administrator reset, selfSettings.ts for personal preferences, avatars.ts for uploads and signed image links, usersDefaults.ts and usersDefaultPlugins.ts for the instance account defaults, users.ts for account administration and optional project assignments, and impersonation.ts for sign-in-as and proof-bound return/cancel. Registration order is part of the contract: usersDefaults.ts registers before users.ts so /users/defaults is not captured by /users/:id. The 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.
  • Account authentication delegates transitions to UsersRepository.accountSecurity, never to a route-level verify-then-mint sequence. /auth/login returns the frozen authenticated envelope or HTTP 202 challenge; the challenge authorizes only public completion/cancellation, never bearer access. Authenticated security routes require fullHumanLogin (including a non-impersonated credential) and the repository rechecks live authority inside the write transaction. Refusals map to 401 publicly, 403 for authenticated proof refusals, 409 for stale state, 404 for foreign/missing session rows and 429 with retry seconds. Enrollment issuer uses resolveBrand and the active theme, not Host. Every final issuance, including SSO and impersonation, includes repository-authoritative absolute sessionExpiresAt. The live catalog shares accountSecurityAccess with the credential gate, additionally requires a real fullHumanLogin, and hides administrator reset from ordinary accounts.
  • createRouteContext installs one accountSecurity.onSessionsRevoked callback per HTTP app. It publishes only {type:'auth-security',userId} after commit through the existing bus; the transaction already owns the durable audit. createServer returns dispose(); bootstrap calls it in the existing shutdown beforeExit after producers stop and before the recorder drain/database close. It unsubscribes the HTTP callback, and open mode installs nothing. The event is owner-scoped in /events. /events uses the existing shared browserStreamGuard with a forced credential refresh before delivery and heartbeat, closes revoked streams without cancelling admitted work, and serializes private voice delivery through the same check. The guard's synchronous dirty/revision invalidation prevents an in-flight validity read from overtaking committed revocation; API/paired credentials keep their separate authority.
  • Full-mode startDaemon mounts attachPluginWebSocketRoutes exactly once on the Node server returned by listen; locked mode never mounts it. It composes the existing app.dispose() so dispatcher close() first detaches the upgrade listener and security subscriber and closes plugin sockets, then disposes the HTTP context before the recorder/database shutdown. No second connection registry or timer is created.
  • The dispatcher synchronously invalidates browser ticket admission and plugin sockets, then returns a promise that settles both browser media engines, including when either fails. Cleanup rejection reaches the existing committed-security error boundary instead of a log-only success.
  • EventBus.publish(auth-security) invokes subscribers synchronously so browser admission generations move immediately, then awaits every returned cleanup promise; ordinary event publication stays synchronous. A failed committed-security subscriber throws safe AuthSecurityDeliveryError only after all subscribers settle. The HTTP response is 503 {error:'auth_security_cleanup_failed',committed:true}: the factor/session mutation and atomic audit already committed, so clients must refresh authoritative state and never retry the mutation automatically. Typed vault unavailability/corruption returns safe 503 auth_unavailable; trusted database connection/SQLSTATE availability failures return database_unavailable, with no credential-bearing exception prose. Wrong codes are ordinary refusals, not incidents.
  • 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.
  • The /events, /brain/conversations and /brain/stream handlers call waitUnlessAborted(30_000, signal) from elowen-plugin-shared/waitUnlessAborted, combining request cancellation with Hono response-stream cancellation. Disconnecting releases the timer and subscriber immediately. 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. The shared wait owns no authentication, filters or presence policy. See the author helper contract.
  • 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 repositories.config.userSettings, owned by packages/db/src/config/. 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 the asynchronous setup(), which opens a fresh PostgreSQL fixture through tests/brain/postgres.ts and creates 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 asynchronous repository projection, repositories.transcript.transcriptPageRows, declared in packages/db/src/repositories/transcript.ts and used by the private BrainStatusService.historyPage in src/brain/service/statusService.ts 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:82) 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. Both reset the conversation's activity through the shared resetConversationActivity in src/brain/session/conversationActivity.ts, which Stop and interrupted-turn settlement also consume. 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 deletes an unspoken runtime identity through the same transcript.unspokenSessionIds authority as conversation listings. BrainSessionFactory must create that identity before PI rehydration for ownership, delegation parents and execution binding. Model/thinking initialization alone never makes it a listed conversation. SessionTeardownService calls dropIfUnspoken after disposing a runtime on client close or idle eviction. A stable client's successful conversation switch calls releaseUnspokenSession for its previous binding: it releases only an unspoken, unclaimed, idle identity under the session lock, without interrupting accepted work, questions or goals. Another attached terminal or pending start protects the identity even while it has no user message. Named roots and deliberately cleared conversations survive. The existing fresh-selection cleanup reuses the repository predicate for already abandoned shells; no separate timer or client-side filter exists. Eligibility reads scan the user's session headers with indexed user-message existence checks; no transcript bodies are decoded. Deletion rechecks durable eligibility in the existing serializable artifact-deletion transaction. After asynchronous artifact preparation, deleteSessionWithArtifacts.beforeDelete synchronously rechecks client/live occupancy and marks disposal while holding the existing bare session lock. A claim that arrived during preparation vetoes cleanup; a later start observes disposal without a remaining runtime, waits behind that same lock, and re-authorizes instead of recreating a deleted explicit id without its execution binding. The marker survives COMMIT and artifact cleanup and is cleared in finally. Goals in any status, custom-message user actions, queued checkpointed sends, durable view waits and parked state share listing visibility and deletion protection. An active pointer is cleared only when its row was deleted. SessionTeardownService.descendantSessionIds (private, src/brain/service/sessionTeardown.ts:681) 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:487) 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 progress once per publication for active children plus the watched children touched by that event; turnControl, canonical sparedChildSessionIds and child delegation projections share that transient input. The existing asynchronous repositories.delegation.getSubagentRuns(parent, childIds) applies child selection in SQL before decoding JSON. repositories.delegation.getWorkflowRuns(parent, childIds) keeps running DAGs plus settled DAGs containing selected children, so a watched child still receives its terminal type/status and a live DAG between nodes remains visible. Empty child selection retains running workflows; omitting selection preserves complete history reads. applySubagentUpdate uses the same selected subagent reader for type enrichment and persist-first readback, including continuation calls on that child. No transcript reader, cache, setting or retention limit is added. Work scales with selected progress payloads rather than archived payloads copied onto the JS heap; The delegation repository decodes stored workflow JSON to select matching nodes; SQL does not decode byte-sensitive payloads. 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:108: 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:192, created by src/brain/service/spawner.ts:1202) and the exported appendReplayBrainEvent array appender (liveEventReplay.ts:157), which the CLI headless reconciler calls (src/cli/chat/headlessReconciler.ts:101). The brain-stream route uses SerializedEventBuffer (src/brain/session/serializedEventBuffer.ts), which calls neither function. 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 instruction activation follows step context. 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.
  • packages/db/ owns PostgreSQL pools, migrations, diagnostics and typed asynchronous repositories for brain sessions, configuration, accounts, Projects, usage, memory, plugin state, provider requests, events and related durable data. Runtime callers use elowen-db.
  • 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. packages/plugin-shared/xml.mjs owns XML 1.0 control-character sanitation and separate text/attribute escaping, and src/shared/xml.ts re-exports it. Use xmlEscapeText for element content and xmlEscape for quoted attributes. displayFormat.ts owns the shared formatters the CLI and web consume through its byte-pinned mirror web/lib/displayFormat.ts — formatCompactCount for dense token labels, formatSpeed and cacheHitPct for generation telemetry, and contextMeter, whose raw mode keeps a reported percentage for hero and footer labels while rails keep the clamped one; elapsed run times come from elowen-plugin-shared/duration (see the packages/plugin-shared/ bullet below). providerAccounts.ts owns stripProvider and upsertProvider, which CLI setup saves in merge mode and web provider settings in replace mode.

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 threshold 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; 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:209) 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:68). 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; that edit is the shared upsertProvider(providers, entry, 'merge') from src/shared/providerAccounts.ts. 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. A rejected slash-command refresh publishes the shared packages/plugin-shared/errors.mjs::errorText(error) result in the existing notice slot, preserving the exact Error message, including an empty one, without replacing the loaded command definitions or editor draft. Error normalization adds no I/O or retry.

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.

The managed MCP transport rejects frames above 16 MiB before JSON parsing, matching pi-mcp's stdio default, and reports McpResultTooLargeError with code mcp_result_too_large. The bridge bounds the serialized tool result, including structured content and embedded resources, to the same size before mapping it. Inline images are checked against 10 MiB of decoded bytes before allocation or prepareInlineImage; oversized data fails explicitly. Host stdio and HTTP parsing remain owned by pi-mcp.

Within the bundled MCP plugin, plugins/mcp/lib/managedSession.mjs (lease heartbeat every 5 s, lines 72-76) 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:281-335) 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. An untap that reaches the runner before its tap has attached is remembered by tap id, so the late tap detaches itself on completion and never answers.

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 their typed report. 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.

Owner clients explicitly identify surface: 'web' | 'cli' on POST /brain/start. The CLI client and web client already send it; missing or invalid surfaces fail HTTP validation before start. A stable /brain/stream?client=... also requires surface, and internal attachment/start calls refuse missing surface before creating a binding. Only explicit CLI bindings pin idle runtime, and only an explicit CLI start resolves a bare cwd to a conversation. Anonymous observers and internal starts without a client or cwd need no surface and create no stable binding. Generation-bound send and control requests carry the same explicit surface in BoundClientRequest, including detached binding reconstruction after restart. CLI requestBinding, web mode switches, and bundled Terminal/Subagent owner controls pass it through; bound requests without it are refused. Stop checks every remaining stable viewer, while idle-runtime pinning filters explicitly to CLI. Generation fencing protects new-send admission and pending-start occupancy. Conversation switches and superseded starts never cancel questions, pause goals or dispose accepted work; SessionTeardownService owns idle runtime cleanup through its shared quiescence predicate. No additional I/O or retry is added.

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.

src/shared/sse.ts owns incremental SSE framing and web/lib/sse.ts is its byte-pinned importless browser mirror. parseSse(buffer, maxBufferLength?) returns event, optional id, multiline data and the incomplete rest; append the next decoded chunk to rest. CRLF normalization preserves split reads, heartbeat-only blocks yield no frame, and absent event names default to message. The CLI still dispatches by JSON BrainEvent.type and restores replay cursors from id; Desktop dispatches named events and supplies its existing 256 KiB reassembly ceiling. Payload validation and reconnect/cancellation policies stay with each consumer. The parser does not reconnect or poll and only scans the accumulated text.

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, iproute2 then iptables order (iptables provides the iptables and ip6tables commands the machine firewall unit requires; stock cloud images lack it), 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.

Plugin host wiring and registry availability

PluginRegistry.contextFor(name, options) delegates scoped accessor assembly to buildPluginContext in src/plugins/pluginContext.ts. The assembler receives a ContextRegistry contribution contract and PluginContextOptions from the type-only src/plugins/pluginContextTypes.ts, which also owns host wiring and embedder declarations. It receives bound operations for tool ownership, skill pinning, live-config callbacks and change validation. The registry owns those private records, merge rules and service lifecycle. Accessors continue resolving turn identity, sibling controls and host state at call time; each context keeps its own config application closure and interval resources. The plugin loader is the consumer. This extraction introduces no public plugin hook, persistence or boot work.

PluginHostExternalUsers.resolvePlatformUser(platform, platformUserId, verifiedEmail?) is required whenever an external-account host is supplied. createPluginHost delegates to the same resolver used by the brain and returns a PluginUserView or null for an unlinked sender. The optional email remains a verified input from the channel; an absent account is never inferred from a provider binding. Hosts without external-account services still refuse the capability accessor. Teams account linking is a real consumer.

PluginHostDeps derives desktopCommandRpc from PluginHostWiring rather than daemon construction options. PluginRegistryUnavailableError is owned by src/plugins/registryErrors.ts, independently of registry loading. PluginRegistryProvider throws that same class; ProjectService and the HTTP boundary consume it without importing the provider graph. The error message, cause and HTTP 503 behavior are unchanged.

Session identity read model

User drill-in is read-only at every depth. BrainService.preflightSend (src/brain/brainService.ts:2746) rejects owned child targets with SubagentReadOnlyError (src/brain/sessionErrors.ts:60) 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:115-122) 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:2168) 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.

sessionIdentity in 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

brainCore's policyForProjects delegates to resolveProjectPolicy({userProjects,projects}, ids, accountUserId): linked accounts use bounded authority reads, unlinked platform metadata uses getMany without account-less managed/computer execution. Account-backed descriptors consume Policy.resolveAccess for the current evaluation, never a turn-wide cache. currentIdentity checks captured privileged bits before live host authority, so nonprivileged identities need no SQL.

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.

CLI frame and interaction ownership

ShellInputDeps is owned by src/cli/chat/chatCapabilities.ts; composition and key actions consume the same input contract without a reverse dependency on composition. ChatApplication supplies the existing thinking-level, help, theme/model/session picker and task-action callbacks. The type adds no runtime option or contribution and changes no input behavior. Manual compaction orchestration belongs to the turn lifecycle owner.

FrameScheduler is the zero-delay dirty-reason coalescer used by RenderShell. Call schedule(reason) or scheduleForced(reason); its only option is the injected now clock. One pending timer merges reasons, retains the earliest requestedAt and carries the forced flag. PI alone throttles physical frames. Pause drops pending work, resume waits for a new request, and stop permanently rejects scheduling. An idle scheduler owns no timer; it adds no frame interval or priority policy.

ChatViewport.beginScrollbarDrag(row) only remembers the pointer's grab offset and returns void. It performs no history indexing and does nothing at zero viewport height. updateScrollbarDrag and continueScrollbarDrag return whether another bounded indexing batch is needed; InputRouter schedules those continuations until the target is reachable, even if the pointer stops. CardPanel.desiredRows() takes no width because its natural row count is independent of terminal width; chatComposition uses that count in its existing layout budget.

openCenteredModal owns one centered overlay handle, focus restoration and idempotent close. Supply tui, editor and makeComponent(close), plus either longest/minWidth/pad/maxHeight for the existing adaptive width or geometry containing the exact PI overlay options. It returns close and optionally calls onClose once before hiding and restoring focus; WorkflowModal uses that hook to dispose its animation timer. Explicit geometry is not given the adaptive 90% clamp. Stats and keybinds retain adaptive sizing, Picker retains its external close handle, and text input keeps its percentage/minimum geometry. An empty picker creates no overlay and restores editor focus directly. The helper creates no animation timer or live geometry owner; PI/OverlayController retain reflow.

borrowEditorSlot in components.ts supplies show(component, focus) and restore() for approval and Ask docks. Each operation replaces the editor container's child, focuses the supplied target and requests a forced render; restore returns the original editor. Ask's Other input supplies a separate focus component, while read-only docks keep their existing restrictions. The helper stores no answer and owns no transport. Without a flow, the normal editor stays mounted.

createChatKeyActions in chatKeyActions.ts is the internal dispatcher for direct chords and resolved leader actions. Composition supplies the current state, client/editor/stash/lifetime resources, stream, live keymap getter, render port and telemetry callback. Rewind's in-flight state and existing session checks stay with its action; background operations use the application lifetime. Telemetry geometry, Esc escalation and queue recall remain composition-owned. The dispatcher starts no idle work and adds no command or persistence contract.

countFilledCells(done, total, cells) is the card/workflow count-meter policy: zero total fills no cells, completion fills all, and incomplete nonzero progress lights at least one cell while reserving the last. Card meters keep at most five cells; the workflow summary keeps ten and counts both done and error as finished. Empty workflows retain their empty frame. Context and pressure percent meters keep their separate tracks and colors.

formatSpeed(tps, locale, fractionDigits, 'compact') in src/shared/displayFormat.ts returns the unlocalized whole-number t/s label from 1 t/s upward and an empty string below that threshold or when absent. Subagent rail/transcript rows and workflow projections select that compact mode; status and stats keep the standard tok/s mode. workflowCanvas.workflowNodeMeta(node, seconds) combines existing token, compact speed and optional duration fields without reading a clock. workflowNodeGlyph(node, spinner) uses the supplied running spinner or the shared settled-status glyph. Canvas and narrow modal lists share these pure projections; the detail dock keeps its richer fields and split colors. These helpers add no timers or I/O.

toolBlocks exports the single SHOWN_OUTPUT_CONNECTOR and requires tool identity in both rendering entry points: framedDiffBlock(diff, width, title, expanded, lang) and toolOutputBlock(view, width, expanded, heading). TurnRenderer supplies the title/heading. Collapsed diffs retain eighteen logical rows, expanded diffs retain all rows and add the collapse affordance when needed. Each diff is sanitized once before wrapping and painting. There is no raw diffBlock renderer, sixty-row default, alternate connector or unlabelled/category-heading fallback. Labels use terminalInlineText, while multiline bodies keep terminalPlainText. Empty bodies add no body row unless command/status/notes supply context. Rendering is synchronous visible-row work and performs no I/O.

ELOWEN_TUI_PERF=1 is the sole opt-in diagnostics switch; ELOWEN_TUI_LOG chooses the JSONL file and an absent path uses the OS temporary directory. TuiDiagnostics exposes enabled, record and close, without a public path or lifecycle detail field. Scheduler records, content-free physical-frame timing, summaries and sink-failure containment remain unchanged. Disabled diagnostics do no recording or traversal. TerminalLifecycle retains beforeStop for composition disposal and no separate unused dispose hook; scheduler/tui/alternate-buffer cleanup order remains unchanged.

editorCommand returns a nonempty argv tuple after the existing VISUAL/EDITOR/vi resolution and quote-aware tokenizer, so the external editor caller destructures its command without another fallback. Child cancellation and temporary-file cleanup stay unchanged. createOptimisticGoal reads the current clock internally and retains its default session argument; durable goal updates still pass through ChatState.setGoal. ChatState starts with no goal; the fenced metadata read or streamed goal event installs the current goal through setGoal, rather than a constructor seed.