Runtime dead-session sweep: end manual runs whose tmux session is gone (generalize readopt's rule to a periodic tick) #93
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/coding-lab#93
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Decision from the 2026-07-10 grill on #62's design (full decision record on #62). Dead tmux sessions end runs in exactly two places today:
reconcile.readoptat startup (EndRun(death, "session gone at startup")) and the AFK reaper at runtime (ActiveAFKRunsonly — AFK kinds). A manual run (store.RunKindManual) whose agent process dies at runtime is never reconciled: the row stays active forever, the instance list showslive=falseagainst an active row, and the chat tailer polls the corpse until the next lab restart.Under #62's liveness design this graduates from cosmetic to correctness: the chosen backstop rule is downgrades only on hard evidence (no staleness timeouts, no transcript inference — they risk spurious "done" flips and, later, spurious notifications). A dead session is the hard evidence, and this sweep is where it gets enforced. Without it, a crash mid-turn pins
workingforever on both adapters — claude-code's future working-edge marker is never cleared by a Stop hook that will never fire, and codex's rollout already ends ontask_startedin exactly that case.The sweep also becomes the single chokepoint a future notification layer hangs a one-shot "run died" event off.
Design
internal/reconcile— it already owns the death rule (readopt) and the start-race discipline. The periodic tick joins the existing reaper-driven sweep hook (afk.Servicealready tickss.sweeponSettingSweepIntervalMinutes) or gets its own ~30–45s ticker in the same family — implementer's choice, but the rule lives in reconcile.kind = manual). AFK kinds stay exclusively the AFK reaper's: it classifies with tracker context (session gone + PR present =done, notdeath), and a generic session-gone rule racing it would misclassify finished AFK runs.EndRun(death, "session gone")(runtime sibling of readopt's pinned startup reason) + publish run-changed, so the chat view flips to ended immediately, the tailer disarms on its next sync, and the spool is GC'd by the existing keep-set sweep.Snapshot()STRICTLY BEFORESessionRunner.List(the same discipline readopt and Pass A/B document), so a run between row-insert and tmux-create is never reaped.Acceptance
death, reason"session gone"), run-changed is published, the chat view shows ended, the tailer is disarmed, and the run's spool/settings files are GC'd on the next tailer sync.done).readoptstill owns the at-startup pass with its own reason string).Out of scope
Sequencing
Independently landable now; useful on its own (fixes the zombie-row gap). #62's implementation phase is blocked on this (it is the hard-evidence half of the backstop).
Landing audit — PR #95 (
afk/93) → PASSVerification signal relied on: Forgejo Actions CI
ci / native (pull_request)— success (4m54s, run #100). Not re-run.Checks:
fix(reconcile): …).afk/93, body carries a validCloses #93.origin/mainis an ancestor of the branch — no conflict.sweepDeadSessionsreaps onlystore.RunKindManualactive runs whose tmux session is gone →EndRun(death, "session gone")+publishRunChanged; AFK kinds skipped, leaving done-vs-death classification to the AFK reaper.ActiveRuns(store) →guard.Snapshot()→runner.List(tmux strictly last); union of starting-set + live protects a run mid-Start.Snapshot()returns a fresh slice, soappend(starting, live...)is a safe copy.DeadSessionLoop(30sdeadSessionTick), nil-guarded incmd/lab/main.go; distinct reason string from readopt's startup pass. AListerror reaps nothing.TestSweepDeadSessions_*covering dead-reap/live-kept, AFK-untouched, startguard race, list-failure-safety, run-changed publish — map 1:1 to the issue's acceptance criteria.Verdict: PASS. Awaiting free-text merge confirmation.