Navigácia
ELOWEN · Dokumentácia

Posledná aktualizácia: 20. októbra 2026

Prechádzať dokumentáciu · Restart and recovery

Tento článok zatiaľ nie je preložený. Je dostupný v angličtine.

Developer reference

Restart and recovery

Restart and recovery

SIGTERM and SIGINT trigger a bounded pause. The daemon stops admitting new ordinary turns, checkpoints queued messages and resumable live work, records what cannot be resumed, and exits. A requested restart uses the same pause path and lets its supervisor start a new daemon process.

Boot recovery has two phases. Before platform traffic starts, recovery providers await their claims of durable work left by the previous process. After platform startup, the coordinator resumes claimed work in dependency order and waves. This prevents inbound traffic from observing stale running rows and lets recovery turns use the normal channel and brain path.

BrainBootRecovery in src/brain/recovery/boot.ts implements BootRecoveryHost and owns the shared claim state: claimed runs and calls, owner result parents, and workflows still owed to the engine. It composes three internal recovery owners from the same live dependency object. The type-only recovery/deps.ts owns BootRecoveryDeps for that shared object, keeping each owner independent of the boot orchestrator. BrainPauseRecovery in pause.ts owns PauseSummary, the awaited persistence checkpoint, the single bounded unparkable wait, and paused-queue replay. WorkflowResumeRecovery in workflowResume.ts owns the existing attempt cap, current-authority journal check, output persistence and durable completion, including terminalization when the workflow control is absent or declines recovery. ParkedConversationRecovery in parkedConversations.ts owns result wakes and silent owner continuations, their marker/activity compare-and-set and the failure budget for the original result IDs. An owner continuation injects no generic restart message; an already-settled transcript needs no provider call. An owner result wake drains the pending batch through the settled-journal delivery authority described in Brain turns. Ordinary results are acknowledged once journaled, even when the parent has not answered. Recovery notices remain pending until consumed by real user admission or a delivered user steer, and release the recovery item. A pending notice removed from live context by compaction is restored before user admission even when its earlier delivery is settled in the journal; user-steer consumption is checked against settled active-branch history. Original rows still pending after the drain consume their existing bounded wake budget. If results were delivered without an answer, claimParkedResultContinuation checks that a current park still stands before the existing subagent-result-resume message continues the parent. A cleared park or a result-only wake is released without a continuation or failed attempt. A failed continuation reports recovery.result_continuation_failed locally and keeps the park marker: the next boot silently resumes the transcript tail through continueInterrupted, with the generic parked-turn budget. It never re-pends acknowledged results. When an earlier drain has already emptied the original inbox before recovery starts, a parked item takes that same generic path, which returns nothing on a settled answer; a non-parked item is released. Recovery joins any continuation-triggered drain outside the turn locks. No additional flag or storage is introduced. Undelivered original rows still consume their own bounded budget; late arrivals do not. Result wake and park resume failures log WARN before the third attempt and ERROR at exhaustion; timed result delivery uses the same rule with its five-attempt budget. Its outer finally replays checkpointed user input last, including on failed or released recovery. These internal boundaries add no storage, provider calls, retries or timeout budgets. BrainService remains the real consumer through its unchanged public verbs, and createBootRecovery is still handed the brain. Claims are awaited before platforms start, resumes run afterwards in coordinator waves, and publishToSession remains the brain's injected fan-out callback.

Before returning the owner-conversation claim worklist, boot drains pending inert conversation appends only for cold idle owners outside that worklist, paused queues and computer prerequisites. Parked owners retain their native tail through interrupted-tool repair and silent continuation. Queued replay drains accepted appends before its real user admission; otherwise the parked owner drains only after native claim/activity settlement and park clearance commit, under the existing conversation locks with a fresh generation check. This preserves reconnect's completion evidence and queued activity fence while keeping voice transcripts durable and readable without starting another turn. The shared append drain also checks the current park/prerequisite for public cold appends and replay-finally calls; neither absence of a runtime nor a finished replay answer releases recovery's native completion evidence. Only real user pre-admission bypasses that check to place earlier accepted input before its own entry. A cold Stop cancels the native continuation first, then repairs the tail and drains accepted appends under the existing conversation locks before returning, without spawning or calling the model. See the append seam and ordering.

Native computer conversation recovery

Top-level owner turns on a paired computer use the same silent continuation. preparePartialTurn gives interrupted calls mutationOutcome:'unknown' and a verify-before-repeat result; no executor operation is replayed. Offline admission retains the native wait, park and accepted queue without charging an attempt or selecting a host target. Authenticated attach reaches the existing ComputerPrerequisiteCoordinator; recovery selects the exact native conversation reference and continuation_generation, not a broad scan of parked sessions.

BrainBootRecovery.resumeComputerConversation handles a socket replacement during its initial session read or native claim assertion before handing off to the conversation/platform owner. A replacement's attach scan may already have skipped the claimed row. The stale callback uses reparkComputerPrerequisite with the SAME held claim and captured Project/reason, then awaits wakeProject only if that repark committed. A claim invalidated by Stop or a new generation returns false and schedules no wake; claim/repark selection spends no provider attempt. This is an early handoff fence, not a blanket post-owner finally retry: later owner settlement, failure handling, execution pins and accepted-input order stay unchanged, and no prompt or executor operation is replayed.

The first actual provider preflight calls beginConversationRecoveryAttempt with the exact generation and either the held claim or expected attempt. Later preflights and retained-input handoff reassert that SAME claim and current connection without another admission/attempt. Scope construction and dispatch retain the captured computer reference and fresh execution binding. New owner or channel-root admission, Stop and explicit target selection advance the native generation; accepted steers, retained handoff, result arrival, repark, reboot and settlement of the original answer do not. An earlier Stop or generation change wins dispatch. There is no automatic offline-computer-to-host move.

When continueInterrupted returns nothing for a claimed local owner because the final answer is already durable, ParkedConversationRecovery defers activity settlement to its final native transaction. That outcome describes only the old continuation: the finally block may still dispatch retained accepted input. Genuine no-dispatch completion retains the original working turn/activity fence. If queued replay completes, BrainPauseRecovery.replayPausedQueue returns its last completed turn's onTurnCompleted activity receipt, captured under the runner's send/admission lock after opened.begin() and before execution; that receipt replaces the original fence only for this deferred settlement path. Without a completed replay, no replacement is supplied, including on refusal.

The bound claim assertion retains the existing native owner/session/Project and prerequisite locks. Recovery rechecks the generation, current park and exact current working turn ID/activity sequence against the captured fence, refusing retained input, pending result delivery, a platform envelope or an unanswered view. Existing withoutTrailingChatEvents and finishedAnswerOf must prove a surviving settled final assistant answer: provider stop, complete usage, nonempty text and no tool calls.

That transaction settles the exact wait, compare-and-sets the captured activity to done, then clears the park. A failed protected activity CAS throws; it or park-clear failure rolls all three writes back. Activity notification follows commit. Stop, a changed generation, stale/reclaimed claim, changed activity, new pins or a discarded answer refuse completion. Settlement fabricates no provider admission and resets no budget; genuine no-dispatch completion leaves dispatch_admitted and park_attempts unchanged, including zero for a pre-dispatch wait. Actual queued dispatch uses the existing admission/budget owner. The generic pre-dispatch activity fence stays intact.

Parking commits the native wait, Project/generation, accepted-input checkpoint and park together before the loss abort. Input refused before admission is not silently parked. Recovery retains the durable/live origin pin and schedules its post-commit wake only after the existing send/session lock snapshots settle; it does not join wake execution under those locks or permanently seal publication.

The turn execution boundary carries proof of that committed, same-generation wait through the rejected accepted-send completion. Its internal waiting outcome suppresses only the otherwise terminal accepted-failure event, not continuation or Stop semantics. Publication uses the captured outcome rather than a later park lookup, including after successful resume or explicit Stop. Paused-queue replay retains its existing failed-batch return and durable rows; a proven wait changes only its local-only log classification, not replay order or the recovery budget.

Detached coordinator-wake failures retain their native owner's declared software code: recovery.computer_wake_failed for conversation/platform recovery, subagent.computer_wake_failed for delegated calls/results and workflow.computer_wake_failed for workflow nodes. The original error/cause reaches the existing logger/reporting boundary while the committed wait remains durable. Offline, unavailable and stale connections normally return without throwing; disconnection itself is not a software report. These codes do not replace recovery.result_wake_failed, whose separate bounded result-delivery series owns its own failure budget. No dispatch, retry budget, prerequisite identity or schema changes. See the producer and audience inventories.

replayPausedQueue reads only the current generation in native order and passes the same daemon-local wake as the third turnRunner.send argument. Each accepted row remains until its native user journal receipt commits. createSessionPersistenceProjector in src/brain/persistence.ts appends that PI entry and transfers the exact paused row in one transaction; journalEntryId is separate from queue/admission identity and PI IDs are unchanged. A refused send stops replay without consuming or reordering remaining input. If connection loss follows append, recovery reparks the held claim even when the row has already transferred; native history resumes it without a duplicate prompt. Computer Project deletion terminalizes its waiting activity and queue atomically, so a stale worklist cannot move that work to the host.

Delegated channel admission shares the same native preflight boundary, but the sub-agent and workflow owners still own their separate continuation/publication implementations. These conversation and host-routing contracts do not establish missing-child dispatch, native workflow publication or waiting-parent result drain. See turn composition and Sub-agent runtime.

The shared slowStep seam measures awaited boot phases and turn-start work. slowSync measures completed synchronous work, logging local INFO from 250 ms to below one second and local-only WARN at one second and above, and retaining the last 32 slow intervals in memory. recordSlowSync(label, startedAt, endedAt) feeds already-timed synchronous intervals into that history with the same fixed 250 ms threshold. Intervals from 50 ms up to that threshold enter a second silent ring of 64 (recentFineSyncSteps). PostgreSQL connection acquisition and query latency are measured separately by packages/db, so an awaited database operation is not recorded as synchronous event-loop blockage.

startDaemon (src/daemon/main.ts) times licence evaluation, app construction and bind. startLoops starts the maintenance loops from createMaintenanceLoops (src/daemon/maintenance.ts), which awaits the recovery claim pass and times plugin/platform startup and synchronous recovery resume dispatch. The background resumeAll promise includes respawned child turns and is not an awaited boot step: readiness and the boot announcement precede it, so it has no stuck-boot watchdog. Dependency waves, item ordering and provider outcome summaries still await each provider's own resume work; a delegation's resumed count means its continuation completed and queued the result, not merely that dispatch began. Slow-step histories do not survive process exit.

Recovery report ownership

Retryable platform delivery/resume and parked conversation wake/continuation failures report once at the third durable attempt. Earlier failures keep their local severity and diagnostics. Successful acknowledgement or clearing the park marker ends that sequence; another result starts its own counter. Timed delegated-result delivery reports its third failure and its terminal exhaustion once at attempt five. A sub-agent parent without that retry timer reports an unsuccessful delivery immediately. Outer boot orchestration, malformed state, failed authority and terminal give-up remain immediate. In particular, the delegated recovery callback's outer catch still owns unexpected orchestration/store errors; ordinary delivery failures are handled and counted inside their durable retry owners.

Loop-lag attribution and the stall profiler

The loop watchdog (watchLoopLag in src/shared/eventLoopLag.ts) warns when p99 crosses 150 ms or severe stalls exceed 2 s, with severe warnings limited to once per reading window. Its blocked by: part names recorded synchronous steps overlapping the window, then frames sampled inside each of the up to three longest severe stalls (LoopLag.stalls, clipped intervals on the performance.now() timeline). If neither names anything, it uses sub-threshold steps from the fine ring; otherwise it says unattributed. Measured origin: a 2.7 s stall on 2026-10-13 that no slowSync covered.

The same watchdog also sums the retained slow and fine steps intersecting the last 60 seconds. More than 1,000 ms adds sync step aggregate: <total>ms in 60s; top labels: <label> <total>ms / <count> to one WARN, limited to once per 60 seconds. The five labels with most recorded time lead the summary. This uses runtime.event_loop_lag; aggregation alone remains reporting: local-only. A severe-stall warning explained by measured scheduling gaps carries a HostConditionError from elowen-plugin-shared/errors instead of explicit local-only metadata, and ProblemService (src/problems/service.ts) reports that condition only once the same problem occurs three times within five minutes: it contributes those three together, then each later occurrence while it keeps recurring, and a lone occurrence stays in the local log. A simultaneous ordinary lag warning carries the summary on its existing line. Individual step thresholds are unchanged. All aggregation runs at watchdog cadence, with no additional work on fast calls. Histories remain capped at 32 slow and 64 fine steps, so counts describe retained evidence, not a complete activity census. Nested synchronous intervals may overlap and are deliberately counted by label; their aggregate is not elapsed blocked time.

startStallProfiler in src/shared/stallProfiler.ts asynchronously starts stallProfilerWorker.ts. The worker connects an inspector session to the daemon's main thread, samples every 2 ms, and serializes periodic rotation, attribution and stop requests. Profile decoding, self/inclusive frame aggregation, JSON serialization and filesystem retention stay in the worker; only the three leading self frames and three inclusive frames cross back. V8's main-thread sampling/code-map work still costs time, so this is not a promise of zero rotation latency. Rotation remains every five minutes with two segments retained, and attribution rotates the current segment before reading it. The watchdog awaits one attribution at a time and emits nothing after it is stopped.

Each attribution retains the monitored process role, PID and optional thread ID, its wall-clock end, and the original self/inclusive accounting. Missing samples remain explicit. A sample is credited with at most two sampling intervals of the gap before it (4 ms at the 2 ms interval; ordinary jitter measured in production stays below that, median 2.06 ms, p99 3.5 ms). The rest of a longer gap is time in which the thread was not observed running, such as host swapping or CPU starvation, and is credited to no frame: both the sampled tail and the unscheduled prefix are intersected with the requested stall before summing, so time outside that stall cannot suppress a CPU warning. The unscheduled intersection is reported as unscheduledMs, with the largest overlapping gap as largestGapMs, and the warning says process not scheduled for <N>ms (largest gap <M>ms) before self:. Measured origin: single samples carrying 303 ms to 3,705 ms on one VM were reported as the self time of whichever frame the next sample hit. Covering segments are persisted once as stall-<time>-<role>-<pid>[-thread-<id>].cpuprofile beside the daily logs; the newest ten files are retained. Chrome DevTools can open the profile. A write failure stays a local warning until a write succeeds.

A local synthetic profile comparison at about 2.04 MB measured the largest main-loop gap falling from 74.2 ms to 30.9 ms. Total asynchronous attribution time was 74.1 ms before and 85.2 ms after. This demonstrates reduced loop blocking, not faster end-to-end attribution or a measurement on the affected deployment.

startDaemon starts a profiler and hands it through BuildOpts.stallProfiler to ServerDeps.stallProfiler. Each sub-agent runner starts the same profiler and a watchdog with the daemon's thresholds and 60-second monitor, in addition to its short-window heartbeat monitor. Profiles retain the runner role and PID, the same two in-memory segments and ten-file retention. A runner's exitRunner waits for database ownership, awaits the brain's existing pauseForRestart() to drain captured persistence and publications, then closes its process-owned pool and stops the inspector worker before exiting; the existing forced-exit deadline still applies. /health.eventLoop remains the daemon's reading. Test apps without a profiler name timed steps only. Inspector initialization or rotation failure disables sampling with a local warning. Worker failure settles outstanding attribution requests with no sampled result. The watchdog retains its timed-step evidence when sampling is unavailable. /health.eventLoop does not carry stall intervals.

PostgreSQL diagnostics live in packages/db/src/diagnostics.ts. Each query records its caller, a 16-character SHA-256 fingerprint, duration and failure state. Queries taking more than 250 ms emit a local slow-query event without SQL or parameters. The first slow occurrence of a fingerprint in each process queues an append to <instance root>/logs/database-statements.jsonl, as configured by buildBrainCore. Each line is JSON { "fingerprint": "<16 hex sha256>", "sql": "<source>" }; it contains SQL source literals and comments, never bound parameters. The file is created with mode 0600, independently of an overridden ELOWEN_LOG_DIR. It is append-only, and mappings may repeat across processes or restarts. A failed append emits statement-map-error, sets statementMapFailed and causes the diagnostic flush during database close to reject. Abrupt exit can lose queued appends.

Database.health() exposes pool totals, idle and waiting counts, acquisition timings, query totals, the ten slowest of the latest 100 query timings, connection-error counts and the statement-map failure flag. /health.database returns this bounded in-memory snapshot without acquiring a connection or running a probe query. These PostgreSQL timings complement the loop watchdog; they are not sampled synchronous statement or lock-hold intervals.

Synchronous journal append diffing and active-entry parsing in src/brain/journalWriter.ts, and export parsing in src/brain/persistence.ts, still use slowSync. Only their completed synchronous intervals feed the loop-attribution rings; database awaits remain in the separate PostgreSQL diagnostics.

After the claim pass and plugin registry reconciliation, adapter connects run concurrently with plugin boot reconciles. Each adapter has a 30-second start deadline and each plugin reconcile has a 20-second budget; an expired task continues in the background and late failures are logged. Plugin services start once both startup waits have settled, then the boot announcement and recovery resume follow. onPlatformsStarted and /health.platformsReady mean that the platform startup phase has settled or exhausted its deadlines, not that every gateway is connected; a timed-out gateway can connect later. The sub-agent runner still starts only its own adapter and no plugin services. A process with no adapters resolves immediately.

The durable platform turn envelope and the authority re-derived from it are pure functions in src/brain/platformTurnEnvelope.ts (PlatformTurnResumeEnvelope, normalizePlatformTurnEnvelope, provePlatformSenderBinding, resolvePlatformTurnAuthority, and the serializer and history seed that share its bytes). ChannelSessionService.send writes the envelope; platformTurnRecovery.ts reads it back. The serialized bytes are persisted and compared on resume, so they stay identical.

Platform recovery uses the affected adapter's application readiness, not socket connection or a startup deadline as success. An already-computed answer is considered for delivery before computer/model admission. An unready adapter retains the park, resume envelope, delivery obligation and attempt zero; it does not block independent items or start provider work. After awaits, the owner re-reads durable state and authority. PlatformResumeContinuation.acknowledge receives the SAME transaction-bound Repositories as native answer promotion; Stop or generation replacement before promotion prevents its ACK and post. Only actual provider dispatch crosses native recovery admission. Existing adapter retry/readiness ownership remains separate from this recovery consumer.

The reconcile budget also releases only the host's wait, not a plugin's work. Sandbox coalesces overlapping reconcile calls onto one sweep. Its automatic recovery re-reads the environment inside the existing store transaction before both exhaustion and enqueue writes, matching the inspected generation and lifecycle epoch and rejecting a stopped/deleted intent or active operation. A late observation cannot restore an obsolete generation or overwrite a newer lifecycle decision. Cronjob's boot reconcile is synchronous and has no post-budget continuation.

Local calls that were in flight when the daemon stopped are settled with an interrupted tool result saying that the effect may or may not have happened and must be verified before repeating it. Unanswered delegation calls are handled differently: the child is recovered and its result is delivered, so the parent must not delegate the same task again. A child whose call was waiting on its own background jobs lost them with the process; the child row's jobs_awaited_at marker says so, and the recovered call wakes the child with a hidden reminder that a daemon restart ended them before its reply can become the result (see the delegated-call settlement in Brain turns). A partial assistant stream that is not provably final is removed so the model can regenerate that step. A final assistant answer is retained.

Recovery is fail-closed for malformed or unverifiable durable envelopes. It does not invent authority or replay an unknown side effect. Completed child results remain durable until their parent delivery is acknowledged.

Interrupted cron and scheduled runs are terminalized without another model turn. Boot recovery attempts their owner notice once through PlatformOrchestrator.notify; a rejected delivery is logged as platform.notice_delivery_failed and reported with its redacted sample, and the scheduled work remains terminalized.

A plugin change is a separate lifecycle operation: the daemon restarts and rebuilds the plugin registry generation from disk, while PostgreSQL state and conversation history remain. All requested restarts use ShutdownControl.requestRestart: a 2.5-second quiet window measured from the last request joins a burst into one restart. It does not wait for parkable turns. The existing pause checkpoints resumable work immediately after the window and gives only unparkable turns their unchanged, single 20-second settleUnparkable budget. Joined HTTP callers retain bounded response-flush protection. Signals bypass the request window and still use the same pause. The /system/restart route (the web System panel and elowen restart), a platform /restart slash and plugin change requests all reach this one path through applyChange; there is no per-caller bypass. The restart request reports its quietMs window to the caller.

Explicit Resume journal handoff

WorkflowResume prepares fresh access/pins/cwd and prospective unfinished-node state without changing the terminal engine. Its existing per-workflow in-memory preparation lock serializes duplicate staging; it is not native authority. An exact old-generation terminal snapshot must succeed before the COMPLETE next journal is durably written to the existing file, BEFORE advanceWorkflowGeneration. Both staging and accepted-metadata handoff use the existing writeJsonAtomic(..., { durable: true }): file fsync before rename and parent-directory fsync after rename, under the writer's canonical platform rules. Ordinary journal writes keep their existing policy. Only native advance performs terminal-to-running, generation/budget reset and obsolete-wait cancellation. Accepted native metadata is installed and journaled before the new DAG starts.

If reservation refuses, throws or loses its acknowledgement, staging is removed ONLY after a successful old-generation terminal snapshot proves native advance did not commit. A stale/failed proof keeps the complete journal for boot, which uses native-provided recovery metadata rather than trusting its old journal epoch. Old callbacks cannot overwrite the staged next journal; late old children are stopped and retention cannot evict an engine during handoff. The preparation lock is released before driving the new DAG. This publication ordering covers a committed advance with lost ACK, but is NOT an atomic DB/filesystem transaction. See engine waiting/publication.

Required shutdown lifecycle

installGracefulShutdown accepts either an absent brain or the required notify, pauseForRestart, settleUnparkable and shutdownPluginServices lifecycle methods. Pass the real BrainService; test doubles must implement all four methods. Signals and requested restarts first await the optional beforeCheckpoint input-producer callback, then await resumable-work persistence, wait once for unparkable turns when present, and bound notification and plugin-service teardown before exiting. The callback shares the existing five-second checkpoint guard and never waits for model turns; bootstrap uses it for voice call, dictation and ring shutdown. The existing beforePause maintenance cleanup remains concurrent, and an absent callback adds no wait. pauseForRestart stops admission, flushes each live session's captured persistence, checkpoints queued messages, awaits park markers and drains persistence/publication queues, including idle sessions. The pool stays open throughout this work. The daemon's final beforeExit hook stops CLI connections, drains pending runner notifications and event recording, then awaits db.close(). Database close ends the process-owned pool and flushes its queued statement-map appends. Without a brain, the handler uses an empty pause summary and still performs supplied pre-pause cleanup. The seam does not poll running turns or make individual lifecycle methods optional. createMaintenanceLoops is the daemon consumer that installs it after boot wiring.

BrainService.shutdownPluginServices coalesces repeated calls. It awaits platforms.stopAll() before closing registry WebSockets and then tears down started services, collecting failures rather than losing later cleanup. Starting plugin services after shutdown is refused. The daemon's existing three-second plugin shutdown budget is unchanged; this ordering is not a promise that every adapter finishes before that outer deadline.

Every stop that goes through the pause posts one restarting notice through BrainService.notify, after the checkpoint, with the turn and sub-agent counts. The notice is bounded to 1.5 seconds and is best-effort, so a chat API failure never blocks the exit. installGracefulShutdown skips it when it is called with notify: false. The next boot posts backOnline when it was requested, and otherwise backOnlineVersion. announceBoot in src/daemon/shutdown.ts skips an unrequested announcement that follows another one within 60 seconds.

Owning code, minimal use and limits

Owning code:

  • src/daemon/shutdown.ts: signal pause, restart quiet window (RESTART_QUIET_MS, 2.5 s), checkpoint guard (PAUSE_CHECKPOINT_GUARD_MS, 5 s).
  • src/daemon/maintenance.ts: createMaintenanceLoops orders the boot phases (claim, platform startup, resume dispatch) and installs installGracefulShutdown.
  • src/daemon/applyChange.ts: the one restart and reload path (createApplyChange).
  • src/brain/recovery/: boot.ts (BrainBootRecovery), deps.ts, pause.ts (unparkable wait, 20 s), workflowResume.ts, parkedConversations.ts, providers.ts (createBootRecovery).
  • src/brain/platformTurnEnvelope.ts and src/brain/platformTurnRecovery.ts: the durable platform turn envelope and its resume.
  • src/shared/slowStep.ts: slowSync (logs at 250 ms, keeps the last 32 slow intervals, plus the silent 50 ms ring of 64) and slowStep (warns after 1.5 s and every 30 s while a step is still pending).
  • src/shared/eventLoopLag.ts: the loop-lag monitor and watchdog; src/shared/stallProfiler.ts: the sampling stall profiler behind blocked by: (startStallProfiler, describeAttribution), started in src/daemon/main.ts and src/subagent/runner.ts.
  • src/brain/persistence.ts and src/brain/recovery/pause.ts: synchronous journal capture with awaited persistence and publication drains.
  • packages/db/src/database.ts and packages/db/src/diagnostics.ts: process-owned PostgreSQL pool, acquisition/query timings, health snapshot and statement-map flush.
  • The CLI verb elowen restart <daemon|web|all> is in src/cli/lifecycleCommands.ts.
  • Memory dreaming's boot sweep and pause abort are in src/daemon/bootstrap.ts: memoryDreaming?.interruptRunning(...) beside memoryMaintenance.recoverInterruptedRuns(), and memoryDreaming?.beforePause() as the first thing in the pre-pause hook (see Memory dreaming).

Minimal use, from the system route:

const { request } = await applyChange({ mode: 'restart', byUserId, reason: '/system/restart', flushed });

Limits and absence:

  • A process without an applyChange queues no restart. The plugin marketplace route returns no request in that case (src/api/routes/plugins/index.ts).
  • The pause arms a 5-second guard before awaiting its persistence checkpoint. Unparkable turns get one 20-second wait, with a 24-second outer guard covering that wait, notification, service teardown and final persistence close. A guard expiry exits with daemon.pause_timeout; a checkpoint refusal logs daemon.checkpoint_failed. A plugin service's own stop keeps its 30-second grace (STOP_GRACE_MS in src/plugins/serviceRunner.ts); the pause's three-second budget, not that grace, bounds the teardown.
  • Platform adapter startup is capped at 30 s and each plugin reconcile at 20 s; expired work continues in the background.
  • Slow-step history is process-local. Stall profiles retain the newest 10 files in the logs dir; the local SQL statement map is append-only.
  • A dream child is never parked or resumed. beforePause aborts running dream children and awaits their persisted interrupted state as part of pre-pause cleanup; a crash is covered by the awaited boot sweep. Its lock keeps the dead boot as holder and goes stale on the next check (see Memory dreaming).

Real consumers: BrainService.settleUnparkable (src/brain/brainService.ts), the /system/restart route (src/api/routes/system.ts) and the platform /restart command (src/brain/platforms.ts).