Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bcdfe44
docs: add implementation plan for opencode-daemon-death-recovery
danshapiro Sep 21, 2026
e5024b6
docs: apply load-bearing corrections to the daemon-death recovery plan
danshapiro Sep 22, 2026
7bf6f28
docs: apply plan-review round 1 fixes to the daemon-death recovery plan
danshapiro Sep 22, 2026
385da61
docs: apply plan-review round 2 fixes (fence binding, re-warm test sh…
danshapiro Sep 22, 2026
d07e016
docs: apply plan-review round 3 fixes (ownership-gated revival, commi…
danshapiro Sep 22, 2026
8ba3acc
fix(opencode): compact and config timeouts never kill the shared serv…
danshapiro Sep 22, 2026
4fed3f2
feat(opencode): structured log for shared-daemon discards
danshapiro Sep 22, 2026
900742d
feat(opencode): daemon exit watcher, loss signal, and backoff re-warm
danshapiro Sep 22, 2026
cc9c5f8
feat(freshopencode): daemon-loss self-heal edge, respawn revival, and…
danshapiro Sep 22, 2026
24ef078
fix(fresh-agent): recover freshopencode panes from snapshot 409 via f…
danshapiro Sep 22, 2026
4fad5dd
fix(fresh-agent): monotonic refusal fence, canonical fold key, reveal…
danshapiro Sep 22, 2026
a6305c8
test(e2e): freshopencode snapshot-409 recovery runs cloud-legal end t…
danshapiro Sep 22, 2026
3872ef7
test(e2e): real-daemon death self-heal recovery runs end to end (loca…
danshapiro Sep 22, 2026
6735ea3
docs: mark all plan task steps complete (Tasks 1-8 executed and revie…
danshapiro Sep 22, 2026
fe14378
fix(opencode): stale request timeouts never discard a replacement daemon
danshapiro Sep 22, 2026
515aad5
docs: correct the daemon re-warm backoff reset semantics in the plan …
danshapiro Sep 22, 2026
e71f33b
fix(freshopencode): revival restarts bridges still bound to a lost da…
danshapiro Sep 22, 2026
71c0d0f
fix(opencode): daemon-loss cleanup never sweeps a successor daemon's …
danshapiro Sep 22, 2026
d449a46
fix(opencode): a lost daemon's events cannot reach the successor era
danshapiro Sep 22, 2026
564732a
fix(freshopencode): transitional sessions recover from daemon loss
danshapiro Sep 22, 2026
b9f7572
fix(opencode): era-verify daemon events atomically at dispatch
danshapiro Sep 22, 2026
9fc0ad4
fix(freshopencode): rescue rides the guarded restart helper, never re…
danshapiro Sep 22, 2026
3b63b91
fix(freshopencode): handoff commit re-checks the bridge generation an…
danshapiro Sep 22, 2026
100391d
fix(freshopencode): rescue verifies the committed session instance an…
danshapiro Sep 22, 2026
0288605
Merge remote-tracking branch 'origin/main' into the-usual/opencode-da…
danshapiro Sep 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ stderr note). Provisioning, rotation, and revocation live in

**Rename scope contract:** pane/tab labels are layout-local; only an explicit session rename writes a durable session title (terminal renames are terminal-scoped). The four name scopes and the reset-to-provider-title flow live in [docs/development/rename-scope-contract.md](docs/development/rename-scope-contract.md).

**Agent Status Indicators:** Blue/busy status is derived from provider activity slices through `resolvePaneActivity`; green/needs-attention and the idle sound flow through `recordTurnComplete` and `useTurnCompletionNotifications`. Turn-complete (green/sound) is server-authoritative everywhere: terminal CLIs via `terminal.turn.complete`, and fresh-agent panes (freshclaude/kilroy/freshcodex/freshopencode) via a discrete `freshAgent.turn.complete` edge emitted only on a positive completion — freshclaude/kilroy on the SDK `result` with `subtype === 'success'`, freshopencode on the success-only `emitStatus(idle)` path, and freshcodex on `turn/completed` only when `params.turn.status === 'completed'` (the notification also fires on interrupt). The client folds it in via `applyFreshAgentCompletion` using the `at`-monotonic dedupe regime (wall-clock `at`, no per-session counter, so a resumed durable session can't swallow completions across a server restart). The waiting-for-approval edge is ALSO server-authoritative: the Claude/kilroy `SdkBridge` emits a discrete `freshAgent.turn.waiting` edge on the 0→≥1 pending permission/question transition (only Claude/kilroy raise approvals/questions), and the client folds it in via `applyFreshAgentWaiting` under a distinct `${provider}:${sessionId}#waiting` dedupe namespace so it can never poison (or be poisoned by) the turn-complete bucket. The fragile client-side busy→idle derivation AND the client-side waiting-edge hook (`useAgentSessionTurnCompletion`) were both removed — all green/sound edges are now server-emitted. freshcodex additionally self-heals a crashed/disconnected codex sidecar by consuming the runtime `onExit` hook in `subscribe()`, emitting `sdk.status:'exited'` to clear BLUE (no chime — a crash is not a positive completion). freshcodex also runs a wedged-sidecar deadman: after a bounded quiet window (default 10 min, env `FRESHELL_FRESHCODEX_QUIET_WINDOW_MS`) with a turn in flight and no sidecar events, the server stops asserting busy and marks the pane `stuck`, and the client shows an amber "Agent appears stuck" card (`role="alert"`) with "Restart sidecar" (kill + resume re-mint) and "Start new conversation" actions; the deadman never fabricates a turn-complete (no green/chime). `freshopencode` still runs on a shared long-lived `opencode serve` sidecar and uses server-pushed `session.idle`/`session.status` events to drive busy. Gemini and Kimi terminal modes are status-in... [truncated] Separately, the sidebar shows cross-device remote status rings around a session row's icon: a green ring means the session is open on another device, a blue ring means it is busy on another device (blue wins over green), and rings are suppressed entirely when the session is open on this device (derived from `tabs.sync` registry snapshots — producing clients stamp pane payloads with `sessionKeys`/`busySessionKeys`, consumers re-query remote snapshots on a 30s interval, and the server partitions same-device records into `sameDeviceOpen`, which never produces rings).
**Agent Status Indicators:** Blue/busy status is derived from provider activity slices through `resolvePaneActivity`; green/needs-attention and the idle sound flow through `recordTurnComplete` and `useTurnCompletionNotifications`. Turn-complete (green/sound) is server-authoritative everywhere: terminal CLIs via `terminal.turn.complete`, and fresh-agent panes (freshclaude/kilroy/freshcodex/freshopencode) via a discrete `freshAgent.turn.complete` edge emitted only on a positive completion — freshclaude/kilroy on the SDK `result` with `subtype === 'success'`, freshopencode on the success-only `emitStatus(idle)` path, and freshcodex on `turn/completed` only when `params.turn.status === 'completed'` (the notification also fires on interrupt). The client folds it in via `applyFreshAgentCompletion` using the `at`-monotonic dedupe regime (wall-clock `at`, no per-session counter, so a resumed durable session can't swallow completions across a server restart). The waiting-for-approval edge is ALSO server-authoritative: the Claude/kilroy `SdkBridge` emits a discrete `freshAgent.turn.waiting` edge on the 0→≥1 pending permission/question transition (only Claude/kilroy raise approvals/questions), and the client folds it in via `applyFreshAgentWaiting` under a distinct `${provider}:${sessionId}#waiting` dedupe namespace so it can never poison (or be poisoned by) the turn-complete bucket. The fragile client-side busy→idle derivation AND the client-side waiting-edge hook (`useAgentSessionTurnCompletion`) were both removed — all green/sound edges are now server-emitted. freshcodex additionally self-heals a crashed/disconnected codex sidecar by consuming the runtime `onExit` hook in `subscribe()`, emitting `sdk.status:'exited'` to clear BLUE (no chime — a crash is not a positive completion). freshcodex also runs a wedged-sidecar deadman: after a bounded quiet window (default 10 min, env `FRESHELL_FRESHCODEX_QUIET_WINDOW_MS`) with a turn in flight and no sidecar events, the server stops asserting busy and marks the pane `stuck`, and the client shows an amber "Agent appears stuck" card (`role="alert"`) with "Restart sidecar" (kill + resume re-mint) and "Start new conversation" actions; the deadman never fabricates a turn-complete (no green/chime). `freshopencode` still runs on a shared long-lived `opencode serve` sidecar and uses server-pushed `session.idle`/`session.status` events to drive busy, and the runtime self-heals a shared-daemon death (the 2026-09-20 incident class): daemon loss fans a typed `freshAgent.error{code:"OPENCODE_DAEMON_LOST"}` edge out to exactly one frame per materialized session (the client's generic `sessionError` banner + busy-clear; NO chime — a crash is never a positive completion), the manager respawns the daemon on a backoff ladder (fresh-incident reset, crash-loop escalation), every successful cold start drives a level-triggered revival pass that restarts dead session bridges and pushes `freshAgent.session.snapshot{status:"idle"}` (the client's transcript-refetch trigger) while respecting the ownership coordinator (retired, transitioned, or terminal-owned sessions are never revived), and a generation-fenced `freshAgent.attach` is itself a recovery verb that respawns the daemon before re-bridging. Client-side, a snapshot GET that still answers the typed 409 `RESTORE_UNAVAILABLE` for the pane's own stale fresh-agent claim (e.g. the pane loaded while the server was restarting) does not dead-end on the dismiss-only banner: the pane drives the documented recovery ONCE per pane identity — it refreshes the observed owner fence from the refusal's own `ownerGeneration` (preserving the record's epoch), sends one generation-fenced `freshAgent.attach`, and refetches through the reveal path when reveal-dirty (so the "Refreshing conversation" overlay can clear) or via `manual` otherwise; a suppressed attach restores the once-guard, and repeated 409s fall through to the honest error banner — terminal-owned refusals stay out of this path (their recovery door is the session-directory handoff). Gemini and Kimi terminal modes are status-in... [truncated] Separately, the sidebar shows cross-device remote status rings around a session row's icon: a green ring means the session is open on another device, a blue ring means it is busy on another device (blue wins over green), and rings are suppressed entirely when the session is open on this device (derived from `tabs.sync` registry snapshots — producing clients stamp pane payloads with `sessionKeys`/`busySessionKeys`, consumers re-query remote snapshots on a 30s interval, and the server partitions same-device records into `sameDeviceOpen`, which never produces rings).

**Fresh-Agent Orchestration:** The Rust REST agent API (`/api/tabs`, `/api/panes/:id/split`, `/api/panes/:id/send-keys`, `/api/panes/:id/capture`, `/api/panes/:id/wait-for`) and the standalone Node MCP client accept `agent`/`model`/`effort` parameters where the Rust contract supports them. The Rust orchestration layer dispatches to the registered fresh-agent runtimes. On MCP `new-tab`, resume sugar (`resume`/`resumeSessionId`) is honored for `agent: "opencode"`; terminal-mode resume uses an explicit provider-matched `sessionRef` (raw Codex resume IDs are rejected because they are not sufficient restore identity). Unsupported legacy actions return a deterministic unavailable result instead of contacting a removed backend route.

Expand Down
Loading
Loading