# AUTOPILOT-129 — Stage 0: a re-dispatch is told where the work is, and the enforced deadline is the recorded one

Two findings, one change each. Part 1 is the load-bearing one (a re-dispatch
that restarts instead of resuming is the difference between a tool and a demo).
Part 2 is the budget question answered from evidence rather than by raising a
default.

## Part 1 — the prior attempt's work now reaches EVERY worker lane

`auto-dispatch.ts` has named a re-dispatch's preserved branch/tip since
AUTOPILOT-60 — but only inside `workerTask()`, and only when the worktree
journal happened to hold a pointer. Two gaps survived that:

1. **The manual lane never called it at all.** `queue_dispatch` builds its task
   from `params.task` alone, so a human re-dispatch of a `failed` item — the
   exact move the failure note tells the operator to make — handed the worker a
   scope with NO statement about prior work. This is how AUTOPILOT-126 lost
   three attempts (the fix was committed on
   `pi-subagents/main-0d539f0-be42-s0-t0` and the operator ended up
   hand-writing the branch into the item's scope) and how AUTOPILOT-127 lost
   two hours.
2. **Absence was silent.** `reviewPointersFor` returning nothing produced an
   empty string, so a worker could not tell "no prior work is recorded" from
   "the framework forgot to tell me", and went looking anyway.

A third gap made the first two worse, and it is the one that explains the
roadmap's own observation:

3. **The journal the pointers came FROM is empty in the live profile.**
   Verified on this machine: `~/.local/state/orchestrator/personal/` has no
   `handoffs.jsonl`, and `orchestrator-autopilot` had zero
   `refs/orchestrator/keep/*` refs at the time of the check (the single one it
   has now is the ref the reproduction below journals), while the incident
   runs' worktrees
   (`pi-worktree-<full-runId>-s0-0` for `0d539f05…`, `9048d6d0…`, `d42f28fb…`)
   are still on disk. `preserveRunWorktree` works when it is called against a
   live worktree (reproduced: it journals `pi-subagents/main-9048d6d-ef14-s0-t0`
   @ `93b0352` from the run's own worktree), so the framework simply never ran
   it for these runs — and a journal-only resolver therefore named NOTHING for
   AUTOPILOT-126.

The fix:

- `priorAttemptContext` is now the ONE place a worker brief learns where prior
  work is, and it answers both ways: a pointer is named with the branch, the
  tip, whether it is already reachable from main, and the imperative
  **"IMPORT, DO NOT REBUILD"**; no pointer on a re-dispatch says
  **"NO PRESERVED WORK IS RECORDED — do not go looking for one"**.
- The pointers come from TWO sources, unioned (`priorAttemptPointers`): the
  worktree journal when it has them, PLUS the item's own recorded prior-run
  identity — `runWorktree.branch` (guarded by the AUTOPILOT-85 ownership rule)
  and the branches `parallelBranches` resolves for the prior run id by matching
  the FULL run id embedded in the worktree directory path. Without the second
  source the fix would still have named nothing in this profile.
- A new `isRedispatch(item)` predicate draws the line: `lastRunId`, `runId`,
  `attempts`, `recoveries`, a `failed` status or a recorded `failCause` all
  mean the item has run before. A never-run `approved` item gets no block at
  all — silence stays silence where there is genuinely nothing to say.
- `queue_dispatch` now prepends the same block, built by the same function the
  harness lanes use.

The wording is deliberately "the RECORD, not proof": the journal can be empty
for reasons other than "no work" (AUTOPILOT-77's silently-empty journal is the
precedent), so the brief tells the worker to stop hunting for a pointer while
still importing anything it happens to find.

## Part 2 — which deadline actually fires (determination, not a budget bump)

**Finding: the deadline that fires is the CHILD's `timeoutMs`. The async run
around it has no deadline of its own.** The pi backend wraps every worker in a
one-step workflow script (`backends/pi.ts` `spawn`) and launches it with
`{ workflowScript, context }` — no `timeoutMs` — so the workflow wrapper is an
unbounded composite; only the `runs.run("main", { …, timeoutMs })` child
carries a deadline. What that child is told is whatever the spawn SITE passes
as `timeoutMs`, so "does the enforced deadline equal the item's recorded
budget?" is a question about the LANE, not about the backend. On the
**item-dispatch lanes** — manual `queue_dispatch`, `autoDispatchEligible`,
review-FAIL `autoRedispatch`, auto-recovery, and `queue_review`/autoReview —
the number comes from `resolveDispatchBudget`, so **the enforced deadline
equals the budget recorded on the item** (or the explicit request, when one was
passed — `queue_dispatch`'s receipt NAMES it as `enforcedTimeoutMs` /
`effectiveDeadline`), and when no budget was recorded the applied default is
written back onto the item in the same store patch. The claim is scoped to
those lanes on purpose; two spawn sites do NOT satisfy it, and naming them is
part of the finding:

- **The spec-pass recon lane** (`dispatchPendingSpecPass`) runs under its own
  short `SPEC_PASS_TIMEOUT_MS` (15 min) and deliberately does NOT persist it
  onto the item — overwriting `timeoutMs` there would silently change the
  budget the item's real dispatch later runs under. That deadline is therefore
  neither the item's recorded value nor surfaced on any receipt, and it is the
  one place where "the effective deadline is recorded" is honestly false.
- **An explicit request against a missing record** (`queue_review`'s
  `params.timeoutMs` on an item with no recorded budget) runs under the
  REQUEST; because the item had no record to default, nothing is written back,
  and the review receipt does not name the deadline either.

Evidence, from the live async-run records
(`$TMPDIR/pi-subagents-uid-<uid>/async-subagent-runs/*/status.json`), not from
the docs:

| wrapper run | state | wrapper `timeoutMs` | child run | child `timeoutMs` | child outcome |
| --- | --- | --- | --- | --- | --- |
| `97c080e6…` (AUTOPILOT-126) | failed | `None` | `d42f28fb…` | `3600000` | `timedOut: true`, "Subagent timed out after 3600000ms"; ran 3 592 217 ms |
| `8b20619d…` (AUTOPILOT-127) | failed | `None` | `9f01bbd5…` | `3600000` | `timedOut: true`; ran ≈ 1 h |
| `dc90959f…` (AUTOPILOT-127) | failed | `None` | `9048d6d0…` | `7200000` | `timedOut: true`; ran 7 186 291 ms (≈ 2 h) |
| `66625140…` (AUTOPILOT-126 recovery) | failed | `None` | `0d539f05…` | `5400000` | `paused` — "Paused after interrupt", NOT a timeout |
| `acadc02d…` (AUTOPILOT-127 current) | running | `None` | `37093c00…` | `10800000` | running |

So the "5.4M recorded but killed at ~1h" reading was a **timing artifact**: the
1 h deaths happened while the item recorded `3600000` (the failure note written
at `15:50:55` says exactly that), auto-recovery then bumped the record to
`5400000` (`15:51:33`), and the next run *did* get `5400000` — it died to a
STEER interrupt, which is the separate Stage 0 item about a correction channel
that kills the run it means to help. There was no phantom cap.

The change made: no budget was raised. The dispatch receipt now NAMES the
effective deadline (duration **and** clock time) and reports
`enforcedTimeoutMs` / `effectiveDeadline` in `details`, and the budget-capped
failure note says the recorded budget is "the deadline the child ran under"
instead of the stale "(runtime default)" fallback (dead since AUTOPILOT-53 made
every lane record a budget). The determination itself is pinned by
`test/framework/redispatch-prior-work.test.ts` and by the existing
`test/framework/timeout-plumbing.test.ts` lane matrix.

## Tests

- `test/framework/redispatch-prior-work.test.ts` (new) — a manual re-dispatch
  names the journaled branch + tip and says "IMPORT, DO NOT REBUILD"; with no
  journaled work it says "NO PRESERVED WORK IS RECORDED"; a first dispatch gets
  no block; the spawn `timeoutMs`, `details.enforcedTimeoutMs` and the item's
  recorded `timeoutMs` are the SAME number.
- `test/framework/auto-dispatch.test.ts` — the shared builder's other half (a
  re-dispatch with no journaled pointer states it; a first dispatch stays
  silent), plus the EMPTY-journal case: the prior run's branch is still named
  from the item's run id alone.
