diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 8cb5d2e..fb3cf69 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -18,10 +18,14 @@ wizard, and cleans up — only the unified `maestro` binary ends up persisted in **Updating is the same command.** Re-running `./install.sh` detects an existing systemd unit, refreshes it, and runs `systemctl --user restart maestro` instead -of `start`. No `--update` flag needed. **The restart keeps your sessions:** the unit sets +of `start`. No `--update` flag needed. **The restart keeps your terminal sessions:** the unit sets `KillMode=process`, so systemd signals only the daemon — the tmux server (and -every session) survives, and the new daemon re-adopts them on start. A unit -written before this default lacks the line; re-run `./install.sh` to refresh it. +every tmux-backed session) survives, and the new daemon re-adopts them on +start. Structured sessions (an agent run as a daemon child over ACP) are not +tmux panes, so shutdown ends their active turn; the transcript survives and +the next daemon attempts one resume (`docs/RESTART.md#structured-sessions`). A +unit written before this default lacks the line; re-run `./install.sh` to +refresh it. Inside the script, `cargo install --git https://git.naps.pt/naps62/maestro …` uses your already-configured git credentials for this host (HTTPS helper or @@ -94,7 +98,9 @@ running a local daemon, for a host that only talks to another machine's. The daemon unit carries the same `KillMode=process` as the wizard-written one, so `systemctl --user restart maestro-daemon` leaves the tmux server and every -session alive. +terminal session alive. Structured sessions end their active turn on +shutdown and resume once, from their event log, on the next daemon +(`docs/RESTART.md#structured-sessions`). ## Thin builds for single-role hosts diff --git a/docs/RESTART.md b/docs/RESTART.md index 9da18a9..1ca2bc6 100644 --- a/docs/RESTART.md +++ b/docs/RESTART.md @@ -86,10 +86,14 @@ a tmux pane: finish it, and no tmux pane keeping it alive underneath. - The **transcript survives**, in the per-session event log (`docs/STRUCTURED-VIEW.md#persistence-recovery-and-dead-sessions`) — a - graceful shutdown marks the session for one resume attempt, and the next - daemon replays that log into a fresh adapter connection via `session/load`. - An ungraceful death (a crash, `kill -9`) leaves no resume marker, and the - session stays dead permanently. + graceful shutdown marks the session for one resume attempt. Recovery is + two separate operations, not one: the next daemon calls `session/load` so + the adapter restores its own ACP session, but whatever history the + adapter emits during that call is discarded, not replayed into anything — + the daemon already has that history on disk. Separately, the daemon + rebuilds its own projection by folding the persisted event log back + through it. An ungraceful death (a crash, `kill -9`) leaves no resume + marker, and the session stays dead permanently. - **Pending approvals and their nonces do not survive.** Both live only in the old daemon's memory; any ACP permission request still mid-flight when the daemon exits is answered `Cancelled` on the way down, and a resumed diff --git a/docs/STRUCTURED-VIEW.md b/docs/STRUCTURED-VIEW.md index ce1a777..c843207 100644 --- a/docs/STRUCTURED-VIEW.md +++ b/docs/STRUCTURED-VIEW.md @@ -188,9 +188,10 @@ read it: a `ReducedState` covering the whole log as it stands, then streams `ProjectionUpdate { transcript, state }` for everything that happens next. There is no client-supplied resume cursor on this socket: every connection - gets the full snapshot, not a gap-filled continuation. A client that needs - history from before it was connected uses the replay endpoint below, not - this socket. + gets the full snapshot, not a gap-filled continuation, so a client never + needs the replay endpoint just to obtain the current transcript. Use + replay instead for cursor-bounded pagination through the durable log — + history from before this connection, fetched a page at a time. - **`GET /api/sessions/:id/structured/replay`** — cursor-bounded pagination over the durable log, returning one `StructuredReplayEvent` (`cursor`, `transcript`, `state`) per matching line. `?since=` returns the first @@ -200,9 +201,15 @@ read it: still in ascending order — passing both is a `400`. `?limit` defaults to 100 and is clamped to 500. Each call re-folds the entire log from scratch to compute the state at every returned cursor, - so a page reflects exactly the state *at* its cursor, not an - approximation — at the cost of redoing the whole fold per request. A - session that is not a live structured session (terminal, or a dead + so a page reflects exactly the *durable* state at its cursor, not an + approximation of it — at the cost of redoing the whole fold per request. + That durable state is not always identical to what a live client saw at + that moment, though: turn lifecycle transitions are broadcast live without + a durable event backing them (see [Prompt and cancel](#prompt-and-cancel-bounded-liveness)), + and a restored `Approval` row carries no pending nonce, because the nonce + never reached disk (see [Approvals](#approvals)) — replay is exact and + reconstructible, not a perfect record of everything the live view showed. + A session that is not a live structured session (terminal, or a dead adapter) answers `409 not_structured` from either route rather than a coherent-looking but stale reply. @@ -286,8 +293,8 @@ allowed to claim the daemon's idea of "the active turn." ## Persistence, recovery, and dead sessions -A structured session's durable state is three files next to its sidecar -(`.json`) in the sidecar directory: +A structured session's durable state is three files in the sidecar +directory: the sidecar itself (`.json`), plus two next to it: - **`.events.jsonl`** — the unified event log. Each line is either a raw ACP `SessionNotification` (kept in its original JSON so old logs stay