1# Server updates
2
3A [stable launcher](../../apps/server/src/serviceLauncher.ts) owns the runtime
4selected by systemd or launchd. It is the only runtime writer of durable service
5state. Server children request updates over inherited IPC; they never rewrite
6their service definition or select their own replacement. Local service commands
7may replace the launcher and state while the service is stopped. Foreground CLI
8processes do not self-update.
9
10Exact-version installs keep restarts independent of npm cache eviction or a moving
11release tag. Installation and preflight happen in staging before publishing an
12immutable runtime. Preflight checks the launcher protocol because a target that
13needs new rollback guarantees cannot safely run under an older launcher. Upgrading
14that launcher requires a local service update.
15
16## Commit boundary
17
18The launcher durably records the pending update before acknowledging it, then
19stops the old child and starts the target as a trial. Service-state writes use
20same-directory replacement with file and directory fsync. Invalid state stops
21startup rather than guessing which runtime to boot.
22
23The trial must finish migrations, acquire dependencies, bind HTTP, and park every
24long-running root at the activation gate before reporting `prepared`. The launcher
25then commits the target version durably and replies `committed`. Only then may the
26child release its gates, accept commands, and publish ready. Keep fallible startup
27acquisitions before this boundary. A listener alone does not prove the runtime is
28ready to commit.
29
30A failed or timed-out trial returns to the old version. After commit, the target
31is authoritative and the service manager's ordinary restart policy applies.
32
33The prepared deadline is long because migrations and SQLite's open-time optimize
34scale with the database and block the trial's main thread, so it cannot report
35progress. A missed deadline restores the snapshot, and the retry pays the same
36cost again. A crashed trial is caught when it exits; the deadline bounds only a
37stuck one. Client update waits must outlast the deadline, or a slow update shows
38as failed.
39
40## Database rollback
41
42Before stopping the old child, the launcher refuses an update whose snapshot will
43not fit on disk, so a full disk does not cause an outage. After the old child
44exits, the launcher snapshots SQLite's main file, WAL, and shared-memory file. This makes trial migrations reversible without down
45migrations. The snapshot is made once per update and survives launcher restarts;
46replacing it during a retry could capture changes from the failed trial.
47
48Rollback stops the trial before restoring. A durable restore marker makes an
49interrupted restore finish before either version boots. Keep the snapshot until
50commit, or until both restoration and the terminal rollback state are durable.
51Attachments and other files outside SQLite are outside this rollback boundary.
52
53## Client acknowledgement
54
55An accepted update is still pending. Clients correlate the launcher's update ID
56with the ready event after reconnecting, then check the outcome and target version.
57A reconnect alone cannot distinguish successful replacement from rollback. Older
58servers without an update ID retain version-only correlation.
59
60Desktop updates have a separate two-phase handoff because installing the app stops
61its bundled backend. Preparation returns a token while the connection is alive;
62the client commits that token only after receiving it. Otherwise backend shutdown
63could lose the only successful RPC result. The client must then observe the
64prepared version after reconnecting. If installation fails, desktop restarts the
65stopped backends and replays the failure for the same token.
66
67## Recovering interrupted threads
68
69Restart continuation is an environment-owned preference, on by default. A
70`settings.json` that fails to decode turns it off, since the file may hold an
71opt-out. The
72[v2 recovery service](../../apps/server/src/orchestration-v2/ProviderRuntimeRecoveryService.ts)
73requires matching durable run, provider thread, session, and native resume identity.
74Queued runs never started, so recovery holds them and continues the run they wait
75behind. A settled run qualifies only when the restart cancels work the agent waits on
76(a monitor, subagent, or background task) and its session was live, so stale records
77from an older crash never wake it. Its continuation prompt is the note of what will not
78report back. A shell alone does not wake a settled run: most are dev servers nobody
79waits on, so that note waits for the next user message.
80
81Recovery retires effects tied to the lost process and records continuation intent
82in the durable outbox. That intent survives another restart before provider startup.
83Continuation effects wait for activation; a slow provider must not delay the server's
84readiness or the launcher's commit boundary. Graceful shutdown captures intent before
85closing providers, then reconciles after ingestion has stopped so a late completion
86cannot be overwritten by a stale cancellation.
87
88The [continuation handler](../../apps/server/src/orchestration-v2/RestartContinuation.ts)
89rechecks the preference, provider selection, newer user work, a stop the user requested,
90and maintenance turns such as `/compact` before dispatching. Intent and delivery both
91refuse a thread that is archived, settled, or still snoozed, a cut turn whose provider
92turn already ended, and a run waiting on the user: an approval, question, or secret that was pending,
93or that recovery closed on the cut turn. Message-capable questions stay answerable, so
94they do not block. Delivery also stops after three consecutive continuations in one
95`restartContinuationOfRunId` chain, so a turn that crashes the server cannot loop. Stable
96command and message IDs prevent duplicate submissions after an outbox retry. Codex
97resumes without adding provider prompt text unless the turn lost background work;
98other adapters receive the continuation message through their normal turn path.
99
100Delegated tasks (`delegate_task` child threads) are reconciled as their own threads,
101never as the parent's background work. The orchestrator settles child results and
102completion deliveries in a startup pass after reconciliation, because the terminal-run
103listener ignores reconciliation's cancellations. A cancelled child whose restart
104continuation is still pending in the outbox is not a result yet; the continuation's run
105settles it, or the handler settles it when it declines to continue. Schedulers wait for
106activation so they cannot start runs that reconciliation would then cancel.