feat(dev): lab reaps AFK runs on PR success, death, or timeout #73

Merged
dominik.polakovics merged 1 commit from afk/63 into main 2026-06-01 22:30:35 +02:00

Closes #63 — Slice 2 of #61 (lab owns the AFK-run lifecycle).

claude --remote-control never self-exits — an AFK run opens its PR and then idles, holding its tmux session and instance slot forever — so lab can't use "the session ended" as the done signal. This adds a single long-lived watcher goroutine (lab's first server-side periodic worker) that sweeps live AFK runs on a ~30s tick and reaps each terminal one, freeing its instance slot.

Behavior

  • Success — an open or merged afk/<N> PR exists: stop the session, remove the worktree; the branch + PR survive (the PR's Closes #N closes the issue on merge). A present PR is success regardless of session liveness (a run that opened its PR then died still succeeded).
  • Failure (death) — session gone, no PR: keep the worktree for inspection.
  • Failure (timeout) — alive, no PR, past the 45-min budget: stop, keep the worktree.
  • In progress — alive, no PR, under budget: left alone.
  • A closed-and-unmerged afk/<N> PR is treated as no PR (so the run fails on death/timeout rather than being falsely reaped as success).
  • Manual Stop is now neutral: keeps the worktree, leaves the issue in-progress for requeue — supersedes Slice 1's interim "Stop removes the worktree".

Design (per ADR-0007)

  • New Tracker.ListPulls seam (tea pulls list, head + state matched client-side), mirroring the existing ReadyIssues seam and substitutable by the test fake.
  • Pure classifyAFKRun(prPresent, sessionAlive, age, budget) is the unit-tested core — no tmux, tea, or clock.
  • All terminal outcomes route through one reap chokepoint (reapAFKRun) — the seam #65 will extend to count consecutive failures. This slice reads/writes no failure counter.
  • The per-run reap decision is remade under afkRunsMu from fresh liveness and claims the run atomically, so a manual Stop racing a sweep is never reaped as a failure.
  • Watcher interval (~30s) and run budget (45 min) are named constants defined together. The budget clock is in-memory, keyed by session name, reset on a lab restart (accepted by ADR-0007).

Tests

go test ./... green under -race; go vet / go build clean. Covers the done/failure/in-progress classification (including PR-present-but-session-dead = success and closed-unmerged = not-success), each reap path over a real tmux, the neutral manual Stop, and the stop-vs-reap concurrency.

lab's Go tests don't run in the repo pre-commit hook (eval-only), so go test/vet/build were run locally.

Closes #63 — Slice 2 of #61 (lab owns the AFK-run lifecycle). `claude --remote-control` never self-exits — an AFK run opens its PR and then idles, holding its tmux session and instance slot forever — so lab can't use "the session ended" as the done signal. This adds a single long-lived watcher goroutine (lab's first server-side periodic worker) that sweeps live AFK runs on a ~30s tick and reaps each terminal one, freeing its instance slot. ## Behavior - **Success** — an open or merged `afk/<N>` PR exists: stop the session, remove the worktree; the branch + PR survive (the PR's `Closes #N` closes the issue on merge). A present PR is success **regardless of session liveness** (a run that opened its PR then died still succeeded). - **Failure (death)** — session gone, no PR: keep the worktree for inspection. - **Failure (timeout)** — alive, no PR, past the 45-min budget: stop, keep the worktree. - **In progress** — alive, no PR, under budget: left alone. - A **closed-and-unmerged** `afk/<N>` PR is treated as no PR (so the run fails on death/timeout rather than being falsely reaped as success). - **Manual Stop is now neutral**: keeps the worktree, leaves the issue in-progress for requeue — supersedes Slice 1's interim "Stop removes the worktree". ## Design (per ADR-0007) - New `Tracker.ListPulls` seam (`tea pulls list`, head + state matched client-side), mirroring the existing `ReadyIssues` seam and substitutable by the test fake. - Pure `classifyAFKRun(prPresent, sessionAlive, age, budget)` is the unit-tested core — no tmux, tea, or clock. - All terminal outcomes route through **one** reap chokepoint (`reapAFKRun`) — the seam #65 will extend to count consecutive failures. This slice reads/writes **no** failure counter. - The per-run reap decision is remade under `afkRunsMu` from fresh liveness and claims the run atomically, so a manual Stop racing a sweep is never reaped as a failure. - Watcher interval (~30s) and run budget (45 min) are named constants defined together. The budget clock is in-memory, keyed by session name, reset on a lab restart (accepted by ADR-0007). ## Tests `go test ./...` green under `-race`; `go vet` / `go build` clean. Covers the done/failure/in-progress classification (including **PR-present-but-session-dead = success** and **closed-unmerged = not-success**), each reap path over a real tmux, the neutral manual Stop, and the stop-vs-reap concurrency. > lab's Go tests don't run in the repo pre-commit hook (eval-only), so `go test`/`vet`/`build` were run locally.
claude --remote-control never self-exits: an AFK run opens its PR and then
idles, holding its tmux session and instance slot forever. Add a single
long-lived watcher goroutine (lab's first server-side periodic worker) that
sweeps live AFK runs on a ~30s tick and takes one terminal action each:

- success: an open or merged afk/<N> PR exists -> stop the session and
  remove the worktree; the branch and PR survive (Closes #N closes the
  issue on merge). A present PR is success regardless of liveness.
- failure (death): the session is gone with no PR -> keep the worktree.
- failure (timeout): alive, no PR, past a 45-min budget -> stop, keep it.
- in progress: alive, no PR, under budget -> leave it alone.

A closed-and-unmerged afk/<N> PR counts as no PR. Manual Stop becomes
neutral (keeps the worktree, leaves the issue in-progress for requeue),
superseding Slice 1's interim "Stop removes the worktree".

All terminal outcomes route through one reap chokepoint (the seam #65 will
extend to count consecutive failures); this slice adds no failure counter.
The per-run reap decision is remade under afkRunsMu from fresh liveness and
claims the run atomically, so a manual Stop racing a sweep is never reaped
as a failure.

Adds the Tracker.ListPulls seam (tea pulls list, head + state matched
client-side), an in-memory per-run budget clock keyed by session name, and
unit + integration tests covering the classification, the success/death/
timeout/in-progress paths, and the stop-vs-reap concurrency under -race.

Closes #63
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/nixos!73
No description provided.