In-capsule dirty-exit design rationale
Records the failure model and recovery design for preserving capsule state after an agent exits with uncommitted changes.
Research state: Reference
Summary
Dirty agent exits must preserve recoverable workspace state and surface an explicit operator recovery path.
This rationale explains the current dirty-exit contract; Runtime restore and image architecture owns delivery work.
Research question
A host-side dirty-exit confirmation can prompt only after the container exits, so resuming requires a rebuild or restart. The decision must occur before teardown while the session is still warm.
Decision — move the confirmation inside the capsule
The confirmation runs inside the capsule (the in-container multiplexer), not on the host after the container exits. A host-side post-exit dialog cannot protect the dirty case because the interactive session has already ended.
Why the decision belongs in the capsule:
- Ask before teardown. In-capsule, the container is still warm; starting a new agent is instant instead of requiring a rebuild/restart cycle.
- Decision at the place of work. The operator is already inside the container; the capsule already owns the TUI and a full set of dialogs. No new surface, no second binary.
- Less resource churn. No stop -> ask -> recreate cycle.
The split of responsibility: the capsule detects dirty/unpushed state and asks the operator before any teardown; the host still executes the filesystem keep/discard, because it owns the isolated worktrees. The capsule decides; the host carries it out.
Architecture — share the dirty-detection logic
The detection logic had to be shared, not duplicated, so the capsule and the host can never disagree on what "dirty" means.
The path-based assessment lives in jackin-core and returns Clean | Dirty | Unpushed, plus the per-file change list the Inspect view needs. Both the host's assess_cleanup wrapper and the capsule's last-session-close path use that function, so dirty-state meaning cannot diverge across the boundary.
Target workflow — in-capsule dirty-exit
The hinge is the last-session-close branch in the capsule daemon:
last live session exits (agent /exit, Ctrl-D, or crash)
│ run the shared dirty assessment on the workspace mount(s) in-container
├─ clean / pushed ─────────────► drain_and_exit (silent exit)
└─ dirty / unpushed ───────────► push in-capsule modal; container stays alive
├─ Start a new agent ─────► open the New-tab agent picker (verbatim) → spawn → back to work
├─ Inspect changes ───────► read-only diff view (Esc back to modal)
├─ Exit & keep changes ───► drain_and_exit → host preserves the instance (resumable)
└─ Exit & discard changes ► drain_and_exit → host discards the dirty work"Start a new agent" reuses the capsule's existing new_agent_picker (PickerIntent::NewTab) — the exact dialog "New tab" already opens. The confirmation reuses the capsule's existing dialog / modal-stack infrastructure; no new chrome.
Alternatives considered for the host signal
Three options for how the capsule tells the host which action the operator picked:
- A file the capsule writes before draining (chosen): the capsule writes
/jackin/state/exit-action.json({ "action": "keep" | "discard" }) beforedrain_and_exit; the host finalize reads it and executes that action. Survives the exit and is inspectable. - An exit-code convention: rejected — range-limited, and conflates the signal with real process-failure exit codes.
- A control frame over the existing socket: rejected — more plumbing than a state file for the same one-shot signal.
Additional decisions:
Ctrl+Qfrom the exit modal maps toExit & keep— the safe default that never loses work.- Discard takes no extra sub-confirm: selecting
Exit & discard changesis itself the explicit destructive approval (the operator chose the split-Exit shape). No second Yes/No.
UI contract
The modal renders in the capsule's existing chrome (row 0 jackin❯ brand pill + tab strip, row 1 ━ underline) as a centered choice list, reusing the capsule's existing dialog and modal-stack components.
The exit modal, Unsaved work — exit?, shows one summary line per dirty repo (<repo> N changed · N unpushed) above four choices:
jackin❯ [the-architect]
━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Unsaved work — exit? ───────────────┐
│ jackin 2 changed · 1 unpushed │
│ holla-apt 3 changed │
│ │
│ ▸ Start a new agent │
│ Inspect changes │
│ Exit & keep changes │
│ Exit & discard changes │
└───────────────────────────────────────┘
↑↓ · ↵ select · Ctrl-Q quitDesign decisions:
- Default focus is the first row,
Start a new agent.Escis ignored — the modal is a forced explicit choice; there is no ambiguous cancel. Start a new agentopens the exact New-tab agent picker (Dialog::new_agent_picker(agents, PickerIntent::NewTab)), rendered identically: titleNew tab, full-width── agents ──/── shells ──section dashes, agent display names, type-to-filter, and theShellrow. It is the New-tab flow verbatim, not a lookalike.Escin the picker walks back one step to the exit modal (modal stack).Inspect changesopens a read-only changed-file list inside the capsule, grouped by repo.Escreturns to the exit modal. A side-by-side per-file diff pane is outside the current contract.- Keep vs discard is decided in-capsule: the two
Exit & …rows are the decision. The host only executes the chosen action — it never prompts. - Multiple dirty repos are listed one line each, with full per-file detail behind
Inspect.
The exact row wording is presentation detail. Cross-screen double-Ctrl+C handling is a capsule-global concern; this modal consumes Ctrl+C like any other key.
Current code ownership
jackin-core::worktree_dirtyownsassess_worktree,WorktreeState, changed-file parsing, and the shared fail-closed detection semantics.jackin-capsuleowns last-session assessment, the dirty-exit and inspect dialogs, the new-agent path, and writing/jackin/state/exit-action.jsonbefore drain.jackin-runtimeowns final inspection and filesystem action.keepmaps to preservation,discardmaps to terminal cleanup, and an absent action file maps to preservation so a crash cannot silently lose work.
Related work
- Roadmap tracking item: Runtime Restore and Image Architecture
-
crates/jackin-core/src/worktree_dirty.rs -
crates/jackin-capsule/src/daemon.rs -
crates/jackin-capsule/src/tui/components/dialog.rs -
crates/jackin-isolation/src/finalize.rs -
crates/jackin-runtime/src/runtime/launch.rs -
crates/jackin-runtime/src/runtime/attach.rs