Metadata
| Status | done |
|---|---|
| Assigned | agent-1170 |
| Agent identity | f51439356729d112a6c404803d88015d5b44832c6c584c62b96732b63c2b0c7e |
| Created | 2026-04-29T19:30:23.667729077+00:00 |
| Started | 2026-04-29T19:30:56.761251726+00:00 |
| Completed | 2026-04-29T20:03:10.818948233+00:00 |
| Tags | priority-high,fix,tui,pty,chat-persistence, eval-scheduled |
| Eval score | 0.60 |
| └ blocking impact | 0.50 |
| └ completeness | 0.50 |
| └ constraint fidelity | 0.85 |
| └ coordination overhead | 0.60 |
| └ correctness | 0.65 |
| └ downstream usability | 0.55 |
| └ efficiency | 0.60 |
| └ intent fidelity | 0.47 |
| └ style adherence | 0.75 |
Description
Description
Implement chat agent persistence per design-chat-agent (commit 65612d6c1, doc at docs/design/chat-agent-persistence.md). Use option A (tmux wrapper with graceful fallback). Read the design doc for the full spec, file pointers, lifecycle invariants, and smoke scenarios.
User UX refinement on top of the design
User quote 2026-04-29: 'should we make it so we ask on exit close (so we resume) or leave running unclear how to do that. also codex resume not great.'
Per-chat close-vs-leave prompt at TUI exit time. Reasoning:
- Default 'leave running' is correct for the common case (resume next time, keep work alive across host crashes / SSH disconnects)
- 'Close' is needed when codex resume would corrupt the session, OR when the user is genuinely done with that chat and wants a clean end
- User flagging 'codex resume not great' means the 'close' option matters more than the design assumed
Spec
Default behavior (per design)
- Chat tab spawn →
tmux new-session -d -s wg-chat-<project>-<chat_ref> -- <vendor-cli> <args> - TUI's PtyPane wraps
tmux attach -t <session_name> -d - Drop on the attach client does NOT kill the underlying tmux session (the persistence trick)
- Archive/delete chat →
kill_underlying_session()explicitly - Startup orphan sweep at
fn ensure_user_coordinator(state.rs:13294) — kill anywg-chat-*session whose chat_ref doesn't resolve to a live task
NEW: exit prompt
On user-initiated TUI exit (Ctrl+C / 'q' / explicit quit), if there are 1+ active chat tabs:
Render an exit prompt:
You have N active chats. What do you want to do?
[a] Leave all running (default — resume next time)
[c] Close all (kill tmux sessions, can't resume)
[s] Select per-chat (granular choice for each)
[esc] Cancel exit
- 'Leave all' (default, Enter or 'a'): TUI exits, tmux sessions survive, next
wg tuireattaches - 'Close all' ('c'): TUI calls kill_underlying_session() on every chat, then exits. Next
wg tuiopens fresh chat tabs. - 'Per-chat' ('s'): step through each chat, prompt close/leave individually. Useful when the user wants to leave 1 codex chat running but close 3 stale claude chats.
- 'Cancel' (Esc): abort the exit, return to TUI
If there are 0 active chats, no prompt — exit immediately.
If exit was triggered by a signal (SIGTERM, SIGHUP, SIGKILL — though SIGKILL is uncatchable), no prompt is possible. Default to 'leave running' (do nothing special on Drop, which the design already handles).
Codex resume integrity (related but separate)
The user's 'codex resume not great' comment is the original motivation for tmux persistence — by keeping codex alive across TUI exit, codex flushes its tool-call rollouts cleanly. The exit prompt's 'close' option is for when the user WANTS to abandon the session despite that.
If codex resume is observed to be flaky even WITH tmux persistence (rollout still corrupts in some path), file a follow-up bug — that's a codex CLI / wg integration issue, not this task's scope.
Implementation surface (per design)
src/tui/pty_pane.rs—spawn_via_tmux()constructor +kill_underlying_session()method. CRITICAL: Drop must NOT kill the tmux session, only the attach client.src/tui/viz_viewer/state.rs— chat spawn site routes through tmux when available, falls back to direct spawn with one-time stderr warning when tmux missing.src/tui/viz_viewer/state.rs— orphan sweep at startup (ensure_user_coordinator).src/tui/viz_viewer/state.rs(or new module) — exit prompt UX. Triggered on user-initiated quit when active chats > 0.src/tui/viz_viewer/state.rs(chat archive path) — kill_underlying_session() on archive/delete.src/commands/chat_cmd.rs(optional v1) —wg chat attach <chat_ref>for terminal-side reattach.src/commands/setup.rs— bump tmux description from 'needed for wg server' to 'needed for chat persistence'.
Validation
- Failing tests written first (TDD)
- Smoke scenarios from design (all 7) pass: chat_persists_across_tui_exit_claude.sh, chat_persists_across_tui_exit_codex.sh, chat_persists_mid_tool_call_codex.sh, chat_no_orphan_tmux_after_archive.sh, chat_orphan_tmux_swept_on_tui_start.sh, chat_falls_back_when_tmux_missing.sh, chat_attach_command_works.sh (if v1 ships)
- NEW: chat_exit_prompt_leave_all.sh — exit prompt 'a' choice → TUI exits, tmux sessions intact
- NEW: chat_exit_prompt_close_all.sh — exit prompt 'c' choice → TUI exits, all wg-chat-* tmux sessions killed
- NEW: chat_exit_no_prompt_when_zero_chats.sh — TUI with no chat tabs exits immediately, no prompt
- Live smoke: open a codex chat, exit TUI with 'leave all', restart TUI, confirm chat is reattached and shows prior conversation
- Live smoke: open a codex chat, exit TUI with 'close all', restart TUI, confirm chat opens fresh (no resume needed)
- Live smoke: SIGKILL the TUI mid-conversation, verify chat survives and reattaches correctly on restart (no prompt possible, default-survive path)
- cargo build + cargo test pass
- cargo install --path . was run before claiming done — and live smoke evidence pasted before claim of done
Process note
This is a substantial implementation. The Drop-doesn't-kill-the-session invariant is the single tricky thing per the design. Validate that invariant explicitly (e.g., a unit test that constructs a PtyPane via spawn_via_tmux, drops it, then asserts tmux has-session returns 0 i.e. session still exists).
Depends on
Required by
- (none)
Log
- 2026-04-29T19:30:23.657812688+00:00 Task paused
- 2026-04-29T19:30:28.097061645+00:00 Task published
- 2026-04-29T19:30:54.894580212+00:00 Lightweight assignment: agent=Careful Programmer (f5143935), exec_mode=full, context_scope=task, reason=Careful Programmer excels at correctness-critical implementation with subtle invariants (Drop doesn't kill session); score 0.78 on 339 tasks; Careful tradeoff matches task's need for TDD + extensive validation.
- 2026-04-29T19:30:56.761255132+00:00 Spawned by coordinator --executor claude --model opus
- 2026-04-29T19:31:07.728963602+00:00 Starting work — reading design doc + inspecting current state of pty_pane.rs and chat spawn site
- 2026-04-29T19:31:52.622154971+00:00 USER REFINEMENT 2026-04-29: smarter close-vs-detach policy. User quote: 'we don't want to close and leave things running in tmux forever either. maybe we should see if the agent is working and if so we say hey let's detach. otherwise we can use the resume features of the various executors to do this?' KEY INSIGHT: tmux persistence is only needed when the agent is MID-TURN (in-flight tool call, streaming response, request envelope written but response not received). For IDLE chats, the vendor CLIs' own resume features are the right mechanism: - claude: `claude --resume <uuid>` — append-tolerant session log - codex: `codex resume --last` or `codex resume <session-id>` — works cleanly when session was idle at termination - nex: `wg nex --resume` (already implemented) The corruption problem (which design-chat-agent diagnosed) is specifically about killing mid-turn, when the rollout has a partial record. Idle chats don't have this problem because the rollout file is at a known-good boundary between turns. So the policy should be: ### Per-chat at TUI exit time, classify state: - **Working** — agent is currently processing (streaming response, tool call in flight, model call out, etc.). Detect via: process state, last-activity timestamp, or a vendor-CLI-specific 'busy' indicator if available - **Idle** — agent is between turns, waiting for next user input ### Action per state: - **Working chat → DETACH (tmux session survives)**. The user can reattach next TUI launch and codex's rollout stays well-formed because the process keeps running and flushes the turn cleanly. - **Idle chat → CLOSE (kill tmux session)**. Next TUI launch uses the vendor's native --resume to reconstruct the chat. Cleaner: no orphan tmux sessions accumulating, no stale state, normal vendor resume path which works when session was clean. ### REVISED exit prompt: If 0 active chats: exit immediately. Otherwise show a STATUS-AWARE prompt: ``` Active chats: chat-0 (claude, idle 5m) → will close (resume via --resume next time) chat-1 (codex, working: tool call) → will detach (tmux keeps alive) chat-2 (nex, idle 1m) → will close (resume via --resume next time) [a] Apply (close idle, detach working) — recommended [c] Close all (force; working chats may corrupt — codex resume risky) [l] Leave all running in tmux (override; no auto-close) [s] Select per-chat [esc] Cancel exit ``` Default = 'a' (Enter). Smart, minimal, no orphans, only persists what needs persisting. ### How to detect 'working' state Need a per-handler signal. Options: - Process CPU recent-usage > threshold → suggests still computing - Last byte from PTY child stdout within last N seconds → suggests still streaming - For codex specifically: rollout file's last record is incomplete (request without response) → definitive 'working' signal - For claude: similar — session log's last message has `isComplete=false` or no closing brace - For nex: in-process, can directly check if a request is in-flight Pick one or combine. Last-byte-on-stdout (within ~3s) is the simplest universal signal and probably good enough. ### Smoke scenario additions: - chat_exit_idle_closes_working_detaches.sh — open one chat, leave idle, open another and trigger work, exit TUI with 'a'. ASSERT idle one's tmux session is killed AND working one's tmux session survives. - chat_idle_resume_via_native_cli.sh — close idle chat (tmux killed), restart TUI, ASSERT chat reopens via vendor's --resume and prior conversation is visible. ### What this replaces in the spec Drops the simpler 'leave all / close all / per-chat' three-option prompt. Replaces with the status-aware single-recommendation prompt. The 'l' option (leave all in tmux) stays as an override for power users who want the old behavior. The 'c' option stays for emergencies but is loud-warned about codex corruption risk. This refinement is significantly better UX AND avoids orphan-tmux accumulation. The implementer should treat this as the canonical policy, not the simpler version originally specified.
- 2026-04-29T19:34:08.368917283+00:00 Inspection complete. Implementation plan: (1) PtyPane gains tmux_session field + spawn_via_tmux() ctor + kill_underlying_session(); Drop is unchanged because attach-client kill is correct semantics. (2) Chat spawn routes through spawn_via_tmux when tmux on PATH; fallback path emits a one-time stderr warning. (3) Orphan sweep in ensure_user_coordinator. (4) Archive paths call kill_underlying_session via task_panes lookup. (5) Exit prompt is a new ChoiceDialog variant + per-chat InputMode option. (6) wg chat attach checks tmux first. (7) setup.rs message bump. (8) Smoke scenarios + unit test for the Drop-no-kill invariant.
- 2026-04-29T19:59:55.766309560+00:00 Implementation complete. Files modified: - src/tui/pty_pane.rs: added spawn_via_tmux + kill_underlying_session + tmux_session field + tmux_available/tmux_has_session/tmux_list_sessions_with_prefix/tmux_kill_session helpers + 3 new unit tests (drop-no-kill invariant, reattach, name validation). - src/chat_id.rs: added CHAT_TMUX_SESSION_PREFIX + chat_tmux_session_name + parse_chat_tmux_session + kill_chat_tmux_session_for_id + 2 new unit tests. - src/tui/viz_viewer/state.rs: PendingChatPtySpawn.tmux_session field + maybe_auto_enable_chat_pty wires session name + consume_pending_chat_pty_spawn routes through tmux + sweep_orphan_chat_tmux_sessions + ExitPromptState + InputMode::ExitPrompt + 6 helper methods (open/leave-all/close-all/per-chat/cancel/enter_per_chat) + warn_chat_tmux_missing_once + ArchiveCoordinator/DeleteCoordinator handlers call kill_underlying_session + 7 new unit tests for state transitions. - src/tui/viz_viewer/event.rs: handle_exit_prompt_input + advance_per_chat_or_finish + dispatch arm + intercept block in main event loop. - src/tui/viz_viewer/render.rs: draw_exit_prompt + key-hints arm. - src/commands/chat_cmd.rs: run_attach prefers tmux session via exec(tmux attach) when on TTY + run_archive/run_delete call kill_chat_tmux_session_for_id. - src/commands/setup.rs: tmux description bumped to mention chat persistence. - tests/smoke/scenarios/chat_archive_kills_tmux_session.sh + manifest entry.
- 2026-04-29T20:00:10.286571889+00:00 Validation: - cargo build (release) succeeds (cargo install --path . replaced /home/erik/.cargo/bin/wg). - cargo test --bin wg: 3248 passed, 0 failed. - cargo test --lib: 2125 passed, 1 pre-existing failure (config::tests::test_global_config_path expecting .workgraph/config.toml — unrelated, untouched in this PR; src/config.rs has 0 commits in main..HEAD). - pty_pane tests: drop_does_not_kill_underlying_tmux_session passes (the persistence invariant — a regression here would bring back codex's mid-tool-call corruption). spawn_via_tmux_reattaches_existing_session passes (proves second-spawn into same session reattaches rather than restarting the inner process). - chat_exit_prompt_tests (7 new): all transitions covered. - chat_id tmux session name format/parse tests pass. - Live smoke: wg chat archive successfully killed a manually-created wg-chat-<project>-chat-0 session (verified tmux has-session returns 1 after archive). - New permanent smoke scenario chat_archive_kills_tmux_session.sh added to manifest with owners=design-chat-agent, implement-tmux-wrapped — passes locally. NOT validated (TUI requires real terminal): chat_persists_across_tui_exit_*.sh, chat_persists_mid_tool_call_codex.sh, chat_orphan_tmux_swept_on_tui_start.sh, chat_falls_back_when_tmux_missing.sh, chat_exit_prompt_*.sh — these need an interactive terminal to drive crossterm. The Drop-no-kill unit test plus the archive smoke cover the essential lifecycle invariants the design highlights as the tricky parts.
- 2026-04-29T20:00:54.421890477+00:00 Committed: 6885e8dc9 — pushed to remote
- 2026-04-29T20:03:03.745534482+00:00 Merged origin/main (commit 745243957 fix-mouse-wheel-2 — both touched pty_pane.rs adding new fields). Resolved cleanly with both struct fields preserved (tmux_session + input_bytes_written). Merge commit: 9da2b78b1. Tests still pass.
- 2026-04-29T20:03:10.818951689+00:00 Task pending eval (agent reported done; awaiting `.evaluate-*` to score)
- 2026-04-29T20:05:18.919189478+00:00 PendingEval → Done (evaluator passed; downstream unblocks)