# Resume attachable session recovery (reconciled plan)

## Goal

Resume, rather than recreate, the interrupted work for recovered tasks #41, #148, and #164. Preserve checkpoint commit `6403f794` (`Checkpoint attachable session WIP`) and plan checkpoint `25e86c54` (`Checkpoint attachable session plan`), keep `specs/b9892096_attachable-session-recovery.md` unchanged as the earlier request record, merge the authoritative current `origin/main`, and finish two user-visible outcomes:

1. eligible top-level non-human agent launches are tmux-hosted, ledger-visible, capturable, and attachable even when their caller has no TTY, while a headless caller still blocks and receives the launched command's exact exit status; and
2. a conservatively identified predecessor's Claude tasks are adopted into the current session's native task directory, so Ctrl+T/TaskList sees them rather than recovery being only startup text.

Do all work with fixture-owned state. Never inspect, attach, kill, sweep, copy, or mutate an owner session, owner tmux server, live transcript, real `~/.claude/tasks` directory, or unrelated worktree.

## Starting state and constraints

- The isolated build checkout is detached at `25e86c54`; its parent is WIP checkpoint `6403f794`, based on merge `151ef46a`.
- `6403f794` intentionally changes:
  - `modules/workstation/claude/bin/_agent-session-tmux`
  - `modules/workstation/claude/bin/_tmpjail-shim.sh`
  - `modules/workstation/claude/tests/agent-session-ledger.test.sh`
- The checkpoint correctly moves the TTY/terminfo decision from the shim to `_agent-session-tmux`, starts dedicated tmux hosting for headless top-level launches, and waits for a pane status file. It is incomplete: its pane trap is not installed before `cd`, its receipt is not atomic, its wait can poll forever after pane/server loss, interactive runs can leave status handoff residue, and the new test does not yet prove status propagation, pane capture, or caller-loss survival.
- Native task rendering already exists in `modules/workstation/claude/hooks/compact-context-restore.mjs`; unique fallback currently only renders a predecessor as `additionalContext`. It does not create `~/.claude/tasks/<current-session-id>`, so Ctrl+T/TaskList remains unaware of recovered work.
- Authoritative behavior documents are `docs/agent-session-ledger.md` and `docs/specs/2026-08-05-compact-proof-todo-injection.md`.
- Repo instructions require `docs/specs/2026-07-30-harness-reliability-lessons-and-plan.md` for caller-completion/recovery behavior. Preserve its foreground, synchronous completion rule and do not add a daemon, watchdog, coordinator relaunch, or automatic retry.
- Planning confirmed the build broker has fetch refspec `+refs/heads/*:refs/remotes/origin/*`, but both `git rev-parse origin/main` and `git ls-remote origin refs/heads/main` currently yield no main ref. Do not infer current main from `refs/rb/in`, `151ef46a`, the shared checkout, or another session's worktree. The builder/orchestrator must refresh this isolated checkout from the authoritative repository until `origin/main` resolves.
- `.rb-origin` (tracked-modified) and `.factory/` (untracked) are orchestrator metadata. Do not discard them and do not include them in product commits.

## Implementation plan

### 1. Preserve both checkpoints and reconcile authoritative main

1. Before changing code, record `git status --short --branch`, `git show --stat 6403f794`, `git show --stat 25e86c54`, and `git diff 151ef46a..6403f794 --` for the three WIP files. This is the durable recovery baseline.
2. Attach a normal task branch to the current detached `25e86c54` in this isolated checkout. Do not reset, rebase away, cherry-pick over, or reconstruct the checkpoint commits; both hashes must remain ancestors of the final branch.
3. Use the orchestrator's supported broker/worktree refresh to obtain current `origin/main`, require `git rev-parse --verify origin/main` to succeed, and record its commit. Never operate in `/home/user/Projects/overdeck` or use an owner's worktree as a source ref.
4. Merge current `origin/main` into the checkpoint branch. Resolve only real conflicts after re-reading the upstream versions of all files named in this plan. Preserve the intent of the WIP, but do not blindly choose “ours” where main has hardened the launch, confinement, ledger, task, deployment, or test contracts.
5. After the merge, inspect all upstream changes since `151ef46a` that touch the shim, tmux host, ledger reader/CLI/collector contract, compact recovery hook, tests, manifest, and deployment scripts. The existing `agent-sessions`/collector tmux-coordinate path should not need a parallel observability implementation; modify a consumer only if a focused fixture proves current main no longer consumes the populated coordinates.
6. Keep the original `specs/b9892096_attachable-session-recovery.md` byte-for-byte unchanged. The new `_v2` plan is additive. Keep `.factory/`, `.rb-origin`, live state, and unrelated changes out of every diff and commit.

### 2. Finish headless, viewable, attachable tmux hosting (#41/#148)

Primary files:

- `modules/workstation/claude/bin/_tmpjail-shim.sh`
- `modules/workstation/claude/bin/_agent-session-tmux`
- `modules/workstation/claude/tests/agent-session-ledger.test.sh`
- `docs/agent-session-ledger.md`

Related regression suites, changed only if reconciliation requires it:

- `modules/workstation/claude/tests/agent-session-passthrough.test.sh`
- `modules/workstation/claude/tests/agent-session-cap.test.sh`

#### Launch behavior

1. Preserve all existing eligibility exclusions: do not dedicated-wrap human-routed sessions, nested `TMPJAIL_ACTIVE` launches, processes already inside `TMUX`, disabled `AGENT_LEDGER_MUX=0` launches, births without a ledger ID, or hosts lacking tmux/systemd/user-manager prerequisites. Preserve `_agent-session-admission`, `agent.slice`, per-session scope, and exact argv/environment forwarding.
2. For every other top-level non-human launch, request the dedicated systemd-owned tmux server regardless of whether the immediate caller has a TTY. The shim must no longer use terminal presence as a hosting gate.
3. In `_agent-session-tmux`, compute whether an attaching client is possible separately: attach only when stdin and stdout are TTYs and `TERM`/terminfo is drivable. Interactive behavior remains an `exec tmux ... attach-session`; document that the client cannot report the pane command's eventual exit code.
4. When no client can attach, retain foreground caller semantics: the wrapper waits without an overall runtime deadline, because the agent may legitimately run indefinitely, and returns the exact pane command status when it completes. This is a blocking foreground launch, not supervision or relaunch.

#### Safe pane handoff and completion receipt

5. Replace the checkpoint's shared-looking `<ledgerId>.status` handling with a unique mode-0700 per-launch handoff under the ledger's mode-0700 `spawn/` directory. Put the generated pane script and, for headless mode, its receipt inside that owned handoff. Use `umask 077`; scripts/directories must not expose the forwarded environment.
6. Generate the pane script so an `EXIT` trap is the first fallible runtime behavior after the shebang—before exported-environment commands, `cd`, unlinking, or admission. Capture `$?` immediately. The trap must clean its own spawn script and atomically publish a numeric receipt (write a private temporary file, then rename) for every controlled headless exit, including failed `cd`, missing admission helper, command-not-found, signal-derived shell exit, and normal success. Interactive mode must clean its handoff without leaving an unread status file no caller will consume.
7. Do not use `exec` for the headless admission command before the trap can run. Preserve exact shell exit values in the range the shell can return; malformed, empty, out-of-range, or unreadable receipts return nonzero with a concise diagnostic.
8. After `tmux new-session` has accepted the pane, ownership has transferred. Only then update `mux`, `tmuxSession`, and `tmuxSocket` on the ledger entry. Failures before acceptance may stop the fixture-owned server and execute the existing unwrapped fallback exactly once; failures after acceptance must never launch the command a second time.
9. In the headless wait loop, check the atomic receipt first, then verify the exact owned tmux session/server still exists. If it disappears without a receipt, stop waiting and return nonzero with the ledger/session identity. Do not add an automatic restart. Remove only this launch's handoff after consuming the receipt or detecting terminal failure.
10. Do not install a caller-exit trap that stops the accepted tmux server. If the headless caller is killed, the systemd-owned server and pane must remain alive and the ledger coordinates must remain usable.

#### Fixture coverage

11. Extend `agent-session-ledger.test.sh` using only its temporary HOME, ledger root, copied shim, stub runtime binaries, unique ledger IDs, sockets, and units. Never signal by a broad process pattern when a recorded fixture PID/unit is available. Add bounded assertions for:
    - a no-TTY long-running stub gets `mux.kind=tmux`, `tmuxSession`, and `tmuxSocket` only after a real session exists;
    - `agent-sessions` prints `agent-sessions attach <ledgerId>` and `tmux capture-pane` over the recorded fixture coordinates returns a fixture marker while no client is attached;
    - killing only the waiting fixture caller leaves the fixture runtime, tmux session, pane capture, and reopen command alive;
    - short headless stubs returning 0 and a nonzero value (for example 7) make the original caller return exactly 0 and 7;
    - a generated-pane `cd` failure and an early admission/command failure produce a receipt/nonzero result within a bounded timeout rather than hanging;
    - loss of the fixture tmux session before a receipt returns nonzero rather than polling forever;
    - no spawn/receipt residue remains after normal interactive/headless completion; cleanup stops only fixture units/sockets/processes.
12. Keep all existing ledger tests green: interactive terminal-loss survival, DETACHED-ALIVE classification, cgroup placement, nested launch attribution/no double wrap, broken-ledger fail-open launch, multiplexer discovery, retention, and rescue behavior.
13. Keep `agent-session-passthrough.test.sh` green so Ctrl-B, Ctrl-Space, arrows, output, and resize still cross the interactive tmux layer unchanged. Keep `agent-session-cap.test.sh` green so moving the hosting decision does not bypass admission or session caps.
14. Update `docs/agent-session-ledger.md` to say that eligible top-level non-human shim launches—not only interactive launches—receive dedicated tmux hosting. Explain headless synchronous status receipts, capture/reopen behavior, interactive exit-status limitation, no double wrapping, kill switch/prerequisite fallback, and the irreducible fact that a process which bypassed the shim cannot be retrofitted into tmux after launch. Remove obsolete statements that non-TTY implies `mux.kind: null` or that scripted launches are unaffected because only interactive launches are hosted.

### 3. Adopt uniquely recovered tasks into the native current-session store (#164)

Files:

- `modules/workstation/claude/hooks/compact-context-restore.mjs`
- `modules/workstation/claude/hooks/test-compact-context-restore.sh`
- `docs/specs/2026-08-05-compact-proof-todo-injection.md`

#### Selection and materialization

1. Keep the existing conservative lookup order. An existing readable current-session directory is exact and authoritative even when empty, closed-only, or partly malformed; never replace it and never fall back around it. If exact is absent, candidates still come only from older sibling non-`.live.jsonl` transcripts in the supplied transcript directory, and exactly one candidate with at least one valid open task is required. Zero, multiple, newer-only, or malformed-only candidates remain a hint/no-op, never a guess or merge.
2. Validate current and candidate session IDs as safe single path components before joining them to `CCR_TASKS_ROOT`/`~/.claude/tasks`; reject empty, absolute, separator-containing, dot-segment, traversal, control-character, or overlong IDs. Use `lstat`/regular-file checks so numeric symlinks, directories, devices, and malformed JSON are never adopted.
3. For the one proven predecessor, read and validate its numeric task files without mutating it. Stage raw validated JSON bytes in a private mode-0700 temporary directory under the task root, with files mode 0600. Preserve valid task IDs, status, `blocks`, `blockedBy`, descriptions, unknown schema fields, and source mtimes needed by the existing render ordering. Copy all valid numeric task records, including closed records, once the source is eligible; malformed/non-file records are skipped. Require filename/task-ID consistency rather than publishing a misleading native file.
4. Publish the staged directory to `<tasks-root>/<current-session-id>` with an atomic **no-replace** directory rename on this Linux platform (for example GNU `mv -T --no-clobber`, checking both its exit status and whether the staged directory was consumed). Plain `fs.rename` over an existing empty directory is forbidden because it can replace an exact empty store. If another writer creates the exact directory first, delete only the hook's private staging directory and re-read that exact winner; never copy into it, replace it, or merge files.
5. Keep SessionStart fail-open: bad input or an unexpected error exits 0. If staging/publication fails and no exact winner exists, emit only the existing safe hint where the source contract calls for it; do not use a “recovered from” header and do not claim native recovery. Do not introduce a database, predecessor pointer, task schema mutation, daemon, or arbitrary cross-session search.
6. After a successful own publication, re-read tasks from the new exact current-session directory before rendering. The first invocation may retain the existing `recovered from <predecessor>` provenance header, but its rendered bytes must come from the published current store. If another writer won, render/silence according to that exact store without claiming the predecessor. Every later startup/resume/compact invocation resolves exact and never consults the predecessor again.
7. Preserve the current rendering and journal-recovery contracts: only open tasks render; in-progress/pending selection and numeric output order; mtime priority; normalized controls/whitespace; UTF-8-safe subject/description limits; 40-task selection; complete 4096-byte budget with exact overflow count; compact prompts/decisions/TodoWrite/journal pointer; valid-empty silence; fallback hint; and `clear` silence.

#### Regression coverage

8. Extend `test-compact-context-restore.sh` under fresh `CCR_TASKS_ROOT` fixtures to assert:
    - one eligible predecessor atomically creates `<current-session-id>` and its numeric JSON files are readable there with IDs/status/subject/description/`blocks`/`blockedBy` intact;
    - source files and directory remain present and byte-identical, and source mtimes/order are preserved in the adopted copy;
    - the first output names recovery but is rendered from current files, while a second invocation is idempotent, uses the exact header/store, and does not create another staging directory;
    - pre-existing empty, closed-only, populated, malformed, and concurrently-created exact directories are never replaced or merged;
    - two concurrent hook invocations result in one complete native store, no partial set, no overwrite, and no private-temp residue;
    - ambiguous, newer, malformed-only, unsafe current/source ID, traversal, numeric-symlink, filename/ID mismatch, and publication-failure cases create no current task store and do not claim recovery;
    - malformed siblings are skipped when a valid predecessor remains eligible, while valid open and closed records are copied according to the adoption contract;
    - all pre-existing compact/startup/resume rendering, 4096-byte, normalization, completed/deleted filtering, exact-empty, and `clear` tests remain green.
9. Amend `docs/specs/2026-08-05-compact-proof-todo-injection.md` as the design truth: unique fallback now atomically adopts valid predecessor files into the native current-session store because Ctrl+T/TaskList reads that store; exact stores are no-replace; races re-read the winner; source stores are immutable; ambiguous sessions are not merged. Reconcile its old “render fallback only” and two-file implementation boundary with this follow-up without erasing the historical intent.

### 4. Verify locally and on a real declared e2e host

Judge every command by exit status. Do not infer success from output text, and do not run any provider-backed agent or use a real session/task store as a test fixture.

1. Static checks:
   - `bash -n modules/workstation/claude/bin/_tmpjail-shim.sh modules/workstation/claude/bin/_agent-session-tmux modules/workstation/claude/tests/agent-session-ledger.test.sh modules/workstation/claude/tests/agent-session-passthrough.test.sh modules/workstation/claude/tests/agent-session-cap.test.sh modules/workstation/claude/hooks/test-compact-context-restore.sh`
   - `node --check modules/workstation/claude/hooks/compact-context-restore.mjs`
   - `git diff --check`
2. Native recovery suite:
   - `bash modules/workstation/claude/hooks/test-compact-context-restore.sh`
3. Run user-manager/tmux suites outside the tmp jail so fixture panes are observable:
   - `systemd-run --user --pipe --wait --collect --same-dir --setenv=TERM="${TERM:-xterm-256color}" -- env -u TMPJAIL_ACTIVE bash modules/workstation/claude/tests/agent-session-ledger.test.sh`
   - run `agent-session-passthrough.test.sh` the same way.
   - `bash modules/workstation/claude/tests/agent-session-cap.test.sh` (or its current-main documented invocation).
4. If current main supplies a safe filtered workstation aggregate, run only the filters covering these suites. Never run an exhaustion-class or unfiltered `modules/workstation/claude/tests/run-all.sh` selection on the workstation; obey the dangerlab gate.
5. Read `modules/workstation/claude/buildbox-hosts.json` at execution time and select the first host in `orders.e2e` whose registry state is `reachable` (currently ordered debian1, debian2, debian3). Use the repository's hardened SSH option construction/config, stage into a unique `/home/user/builds/...` fixture, and run the compact suite plus ledger/passthrough suites through that host's user manager. If transport to the first declared host fails, record the exit/evidence and try the next declared reachable e2e host; never hardcode an undeclared host.
6. A green skip is insufficient for the remote acceptance proof. Explicitly require tmux and the user manager, then use a fixture-owned headless marker stub long enough to assert `has-session`, ledger coordinates, detached `capture-pane`, and the reopen command. Kill only the waiting fixture caller and prove the pane remains; then terminate the recorded fixture process/unit and verify cleanup. Also run a short nonzero fixture and assert exact status 7. For recovery, use remote `CCR_TASKS_ROOT` and assert current-session native files contain the predecessor IDs/fields.
7. Clean only unique local/remote fixture paths, sockets, processes, and units. Record commands, hosts, and exit codes; report any unavailable environment honestly rather than substituting a real owner session.

## 5. Review, land, deploy, and prove installed behavior

1. Review `git status`, `git diff --check`, the merge diff, and the full `151ef46a..HEAD`/checkpoint-to-final diff. Confirm both checkpoint commits remain ancestors and only intended code, tests, docs, and the additive `_v2` plan are product changes. Never commit `.factory/`, `.rb-origin`, credentials, sockets, receipts, transcripts, task fixtures, or unrelated WIP.
2. Commit the implementation with an imperative subject describing headless attachable sessions and native task adoption. Immediately before landing, refresh and merge the latest `origin/main` again; rerun focused suites after any touched-file conflict or relevant upstream change.
3. Land only through `.claude/scripts/ship.sh` and the orchestrator's guarded land queue. Do not hand-type the merge/push cascade and do not use direct land unless the orchestrator explicitly authorizes the documented escape hatch. Preserve `6403f794` and `25e86c54` in landed ancestry.
4. Ensure normal post-land `packaging/deploy-local.sh` completes from the pristine deploy clone. Deployment must use landed `origin/main`, not this build checkout. Record the landed and deployed commit receipts.
5. Prove installed files, not just source files:
   - confirm `~/.claude/bin`, `~/.claude/hooks`, and `~/.claude/lib` resolve through the deploy-managed manifest and that the deploy clone HEAD equals landed `origin/main`;
   - compare hashes/symlink targets for installed `_tmpjail-shim.sh`, `_agent-session-tmux`, and `compact-context-restore.mjs` against the landed files;
   - run the compact test against the installed hook with a fresh temporary `CCR_TASKS_ROOT` and verify native current-session files;
   - build a temporary fake HOME/PATH around the installed shim/host plus a harmless copied stub binary and temporary `AGENT_SESSIONS_DIR`; from a no-TTY `systemd-run --user` caller prove live pane capture/reopen and exact nonzero status without invoking Claude/Codex/Cursor or touching the real ledger;
   - run the deployed `shim-drift-check` and retain `packaging/deploy-local.sh` service/health receipts.
6. Report landed commit, deployed commit, local/remote/installed-path commands and exit codes, pane/native-task evidence, and exact fixture cleanup. Explicitly list anything not verified.

## Acceptance criteria

- `6403f794` and `25e86c54` remain in final ancestry, the original plan spec remains unchanged, and the WIP is merged with authoritative current `origin/main` rather than discarded or rebuilt from memory.
- Every eligible top-level non-human shim launch is hosted in a dedicated systemd-owned tmux server even with no TTY; its accepted coordinates make it capturable and attachable through existing ledger consumers.
- A headless caller blocks and receives the exact command exit status. Controlled early pane failure or server loss cannot leave it polling forever, and killing only that caller does not kill the accepted pane.
- Unique predecessor recovery atomically creates the current session's native task files, enabling Ctrl+T/TaskList. Existing exact stores are never replaced (including empty stores), races never merge/partially publish, source stores stay byte-identical, and ambiguous/unsafe recovery is rejected.
- Focused local and real declared-e2e-host tests pass using only owned fixtures, with no provider agent invocation and no owner-session contact.
- Guarded landing and deployment complete, and installed-path probes demonstrate both headless attachability and native task adoption from the landed commit.
