Skip to main content

Module reload

Module reload 

Source
Expand description

synaps daemon reload (PLAN-phase3 §2.8, C3): version gate → drain → Checkpoint{Reload} every session → reload-state → announce Reloading/Bye{Reloading} → execv self (same pid, adopted flock, CLOEXEC listener) → new image rehydrates sessions from reload-state BEFORE accepting.

What reload cannot preserve (§2.8, stated once): in-flight turns (checkpointed = cancelled; partial history + restart marker kept), pending prompts (answered None), PTY/background shells (closed, announced), turn_replay, input ownership (the owner reclaims it on reconnect via was_owner). What it preserves — each session’s Checkpoint{Reload} reply carries a SessionReloadRecord: its journal and id (the rehydrated session continues the same journal), its SessionConfig as created (cwd, system, prompt manifest, compaction policy, auto-compact, …), its keep-warm pin, its lifecycle (a Parked session comes back Parked), the non-persisted runtime knobs (settings_replay: context window, compaction model, retries, timeouts, tool-output cap, worker grants, /system), the CURRENT model/thinking (/model mid-session survives), and the session’s exact tool activations (pins only, re-validated by the new runtime at its first turn start — SessionToolSet::restore_carried).

Windows has no execv, and a byte-range lock cannot be inherited, so step 5 is a hand-off between two processes (see daemon::win): the old image starts the new one with the reload-state and a hand-off event; the new image queues its daemon.lock request behind the old one and sets the event; the old image exits — discarding what an exec would — and its release grants the queued request, so the liveness oracle never reads “free”. The new image does this before booting anything, then proceeds exactly as after an exec. Same guarantees, one difference: the pid changes (clients learn it from the next Welcome).

Structs§

Prepared
Steps 1–4 (§2.8): gate, drain, checkpoint, reload-state, announce. Nothing irreversible has happened when this returns Ok; the caller (the requesting connection) sends its own Bye{Reloading}, flushes, then calls exec.
PrintVersion
<exe> daemon --print-version output.
ReloadRequest
ReloadSession
One recorded session.
ReloadState
registry_dir()/daemon[-P].reload.json body.

Enums§

ReloadError

Constants§

DRAIN_SECS_ENV
Env: default drain budget in seconds.
HANDOFF_EVENT_ENV
Windows: env naming the event the new image sets once its daemon.lock request is queued behind the old image (which then exits).
HANDOFF_PID_ENV
Windows: env carrying the old image’s pid; the new image waits for it to be gone (and its pipe instances with it) before it binds.
LOCK_FD_ENV
Env: inherited daemon.lock fd (mandatory when RELOAD_STATE_ENV is set).
RELOAD_STATE_ENV
Env: path of the reload-state file handed to the new image.
RETRY_AFTER_MS
What clients are told to wait before reconnecting.
SUBAGENT_ID_FLOOR_ENV
Env: the subagent handle counter read at the execv itself, later than ReloadState::next_subagent_id (written after the checkpoints): a worker that ignored its stop could still dispatch in between.

Functions§

adopt_from_env
New-image side (Daemon::start): when RELOAD_STATE_ENV is set, the lock is ADOPTED from LOCK_FD_ENV (mandatory — refuse to start without it, risk §6.8) and the recorded generation is returned.
exec
Step 5: rewrite daemon.json (generation+1, same pid), stop sidecars, hand the flock to the next image and execv. Returns only on failure — then daemon.json/reload-state are restored, extension discovery is re-spawned (the sidecars were stopped for the exec — M4: no zombie daemon without hooks), and the old image keeps serving (sessions checkpointed but alive; clients that got Bye reconnect to THIS image — a reconnect_of.generation mismatch is tolerated, the daemon answers with its own generation).
prepare
probe_version
Run <exe> daemon --print-version (5 s) and parse it.
rehydrate
reload_state_path
resolve_exe
The executable to re-exec: argv[0] canonicalised (NOT /proc/self/exe, which reads “(deleted)” after an in-place rebuild); current_exe only as a fallback when argv[0] is not resolvable (e.g. bare name on PATH).
validate_exe
M8: --exe must be something we would trust to execv in our own pid: an existing regular file (canonicalised), executable, owned by our uid (or root — package-installed binaries), not group/world-writable. Returns the canonical path. Same-uid 0600 socket → not a privilege boundary; this is a footgun guard, not a sandbox.
version_gate
Directional gate (§2.1): new protocol ≥ ours (clients must still be able to reconnect) and new binary ≥ ours (newer OR EQUAL; same-version rebuilds are the common case).