fix(tui): close resume handoff races

This commit is contained in:
NI0317
2026-07-24 12:58:53 +08:00
committed by ZiyaZhang
parent 2ae9f4fdf3
commit 54d986ed87
20 changed files with 375 additions and 76 deletions
@@ -14,9 +14,9 @@ The original `/resume` printed shell commands. It did not let a keyboard user in
`session-query.readSession()` supplies a detached complete log validated by the same core replay boundary used by resume. The TUI folds title and goal state from that log. A candidate load failure is local to that row; selecting a candidate repeats the load, cwd, occupancy, and route checks so a stale listing cannot bypass preflight. A missing adapter reports an intact session with an unavailable route. Running agents are never switched or cancelled implicitly.
First-party persistence backends implement a cross-process live lease under the shared coordinator. JSONL uses an owner-only lock record; SQLite uses a `live_session_leases` row. Both retain PID plus an exec-stable nonce, reject another live process, reclaim a dead PID, and release only after the exact session lifecycle drains. `AgentLoop.resume()` claims before load, closing the preflight/start race.
First-party persistence backends implement a cross-process live lease under the shared coordinator. JSONL uses an owner-only lock record; SQLite uses a `live_session_leases` row. Both retain PID plus an exec-stable nonce, reject another live process, reclaim a dead PID or a same-PID different-incarnation owner, and release only after the exact session lifecycle drains. A final process-local release excludes reacquisition until the physical lease settles. `AgentLoop.resume()` claims before load, closing the preflight/start race.
After preflight, the TUI flushes the current session and stops the terminal before calling `TuiRuntime.handoffResume`. The shipped `dsh` host disposes the root app and uses `process.execve` with a normalized `--resume` argument, atomically replacing the process rather than spawning a second terminal owner. The resumed app publishes the same `SessionId`; ordinary replay restores transcript, title, todos, and durable goal state. Goal activation is intentionally disarmed, and the TUI asks for human confirmation or `/goal resume`.
After preflight, the TUI claims the target's exec-stable live lease before flushing the current session. A lost claim race remains in the current TUI; any later recoverable failure releases the reservation. The TUI then stops the terminal before calling `TuiRuntime.handoffResume`. The shipped `dsh` host disposes the root app and uses `process.execve` with a normalized `--resume` argument, atomically replacing the process while retaining the target reservation rather than spawning a second terminal owner. The resumed app publishes the same `SessionId`; ordinary replay restores transcript, title, todos, and durable goal state. Goal activation is intentionally disarmed, and the TUI asks for human confirmation or `/goal resume`.
`resumeCommand` remains an exit and no-host fallback. The TUI substitutes `{session}` only for display and never executes arbitrary shell text. The exit hint still appears only after the current session is durable.