Repository navigation
feat: recover interrupted dispatches in pickup and handoff - #20
Conversation
PDN-0004 slice S5: - passdown-pickup reads the attempt store read-only (list --unresolved, claims, probe) in every plan repository. It classifies each unresolved attempt C1-C11 with its ownership risk and one proposed action, to be carried out later through dispatch Reconcile, and reports a handoff attempt missing from the store. It never writes a receipt, claim, plan or log, and never proposes retrying an uncertain attempt. - Its completion check follows the dispatch rule: a task with an unresolved attempt is never accepted, the latest Dispatched: line decides, and a legacy line counts only for a task with no receipts. - passdown-handoff lists unresolved attempts in an optional open_attempts frontmatter key and names them in Caveats / traps. It never resolves an attempt and never ticks a task that has one. - Both skills report a missing jq as "attempt store not readable" instead of guessing. The example workspace adds a handoff with an open attempt, SMOKE_TEST covers both skills against a live attempt, and tests/skills.sh and tests/docs.sh lint the new rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reference pickup reads open_attempts from a handoff log's frontmatter (F16 now writes a real log) and reports an open attempt that has no receipt as inconsistent. New scenario PICKUP, the S5 acceptance check: - the class names used by the helper's CLASS_JQ and the reference pickup must equal the rows of the pickup skill's class table and of dispatch Reconcile; - every verdict the reference pickup prints is a term the skill defines; - with a live writer and a handoff naming a missing attempt, pickup leaves every file under docs/, src/ and the attempt store byte-identical. Dropping a class row from either skill, or letting pickup write, fails it. The design records the S5 implementation notes; CHANGELOG the slice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vunm-dev
left a comment
There was a problem hiding this comment.
Two recovery issues need adjustment before merging. Validation: 142 skill checks, 32 documentation checks, and interruption scenarios PICKUP and F16 passed. The interruption scenarios required execution outside the sandbox to inspect processes correctly.
| written: run `passdown-attempt --json list --unresolved` (the helper in | ||
| this skill's `scripts/` directory, with `--store <attempt_dir>` when that | ||
| key is set) in each repository that holds a plan this session touched, | ||
| and list every ID it prints. Omit the key when there are none. The next |
There was a problem hiding this comment.
[P2] Preserve unresolved handoff IDs when their store is missing
The helper returns an empty array successfully when the store does not exist, so rebuilding open_attempts solely from list --unresolved and omitting the key on an empty result loses previously known attempts. For example, pickup reports an ID from the previous handoff as missing with ownership risk, but the next handoff drops that ID even though nobody has confirmed the worker stopped. Subsequent pickup uses the latest handoff and loses that recovery signal. Carry forward previously recorded IDs whose ownership remains unresolved, including missing receipts, and distinguish an absent/unreadable store from a verified empty store. Please cover pickup followed by handoff with a missing store.
There was a problem hiding this comment.
Fixed in 3029311 (skill and helper) and 32b0ff6 (harness).
passdown-attempt listnow warns "absent, not empty" when the store directory does not exist (it still prints[]), so the two cases can be told apart.- Handoff builds
open_attemptsfrom two sources: the store's unresolved attempts, plus every ID from the previous handoff whose receipt does not show it resolved. An ID with no receipt stays listed as# receipt missinguntil the owner confirms no worker remains. An absent store is never evidence that nothing is open. Whenjqis missing, the previous IDs are carried forward unchanged. - Pickup treats the handoff's IDs as missing when the store is absent.
- Covered by the new scenario
HANDOFF:- With a store: the live attempt is listed, the receipt-less ID is carried forward, and the resolved ID is dropped.
- With no store at all, pickup and then handoff: pickup reports the missing attempt and sees the absent store, handoff keeps the ID, and neither one creates the store.
tests/attempt.shchecks the warning.HANDOFFfails when the carry-forward of receipt-less IDs is removed.
| | C6 verdict, no projection | `accepted`, not projected | No | Re-check the task digest, the checks and the artifact digest; then finish the projection. | | ||
| | C7 stale | the task changed since `new` | Per observation | Resolve ownership risk first, then reject `stale`; new work is a new attempt. | | ||
| | C8 cancel unconfirmed | cancel requested, `running`/`unknown` | **Yes** | `probe`; escalate the cancel per the card; stop only with safe evidence. | | ||
| | C9 orphaned | another session's attempt, older than its dispatch budget (use one hour when none is recorded), not named in the latest handoff's `open_attempts` | Per observation | Surface it prominently with age and location, then classify it as C1–C8. | |
There was a problem hiding this comment.
[P2] Base C9 on inactivity rather than creation age
Section 13.2 requires no update for longer than the budget, but this rule uses the attempt's age, and the helper's age_seconds is measured from created_at. An attempt created two hours ago and updated moments ago by another session is therefore classified as orphaned under the one-hour fallback if it is absent from the latest handoff. Use the last recorded update (for example, the latest receipt transition) for the inactivity threshold and add a case with an old creation time and a recent transition; the existing F16 only covers an old attempt without a subsequent update.
There was a problem hiding this comment.
Fixed in 3029311 / 32b0ff6.
listnow reportsidle_seconds, measured from the receipt's last transition, next toage_seconds. The pickup C9 row and the reference pickup use it.tests/attempt.shcreates an attempt, waits 2 s, arms it, and requiresage_seconds >= 2andidle_seconds <= 1.- New scenario F16b: another session's attempt is created, and 3 s later that session updates it (
observe unknown). With budget 2 it is not C9. After 3 more idle seconds it is C9. F16b fails with the old age-based rule.
…vity Review round 1 on #20: - An absent attempt store is not an empty one. `passdown-attempt list` warns "absent, not empty" when the store directory does not exist, and still prints []. Handoff builds open_attempts from the store's unresolved attempts plus every ID from the previous handoff whose receipt does not show it resolved. An ID with no receipt stays listed, marked `# receipt missing`, until the owner confirms no worker remains. Pickup treats the handoff's IDs as missing when the store is absent. - C9 follows inactivity, as §13.2 says ("no update for longer than the budget"). `list` reports idle_seconds, measured from the receipt's last transition, next to age_seconds, and pickup uses it. tests/attempt.sh covers both helper changes; tests/skills.sh lints the skill rules; the design records them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ref-host gains `handoff`, the reference for the open_attempts rule, and measures C9 by idle_seconds. Its pickup and handoff no longer create the store. - HANDOFF: with a store, a live attempt is listed, a receipt-less ID is carried forward and a resolved one dropped. With no store at all, pickup reports the missing attempt and sees the absent store, handoff keeps the ID, and neither creates the store. - F16b: an attempt created long ago but updated recently by its own session is not orphaned; once idle past the budget it is C9. Both scenarios fail against the old rules (age-based C9; dropping receipt-less IDs). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vity Review round 1 on #20: - An absent attempt store is not an empty one. `passdown-attempt list` warns "absent, not empty" when the store directory does not exist, and still prints []. Handoff builds open_attempts from the store's unresolved attempts plus every ID from the previous handoff whose receipt does not show it resolved. An ID with no receipt stays listed, marked `# receipt missing`, until the owner confirms no worker remains. Pickup treats the handoff's IDs as missing when the store is absent. - C9 follows inactivity, as §13.2 says ("no update for longer than the budget"). `list` reports idle_seconds, measured from the receipt's last transition, next to age_seconds, and pickup uses it. tests/attempt.sh covers both helper changes; tests/skills.sh lints the skill rules; the design records them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDN-0004, slice S5 (pickup and handoff recovery). Design:
docs/design/PDN-0004-v05-acceptance-recovery.md§13, §21–§23, §26.2 S5. Issue #12. It builds on S4 (#18): classes are carried out through dispatch Reconcile.What changes
passdown-pickup:list --unresolved,claims, andprobefor C2/C3/C8.open_attempts.Dispatched:line decides as a v0.5, host or legacy line (legacy only without receipts).passdown-handoff:open_attemptsfrontmatter key, and every open attempt named in Caveats / traps with class, location and ownership risk.docs/SMOKE_TEST.mdgains items for pickup and handoff against a live attempt. README, CHANGELOG and the S5 implementation notes are updated.Acceptance check (§26.2: "pickup classification uses the same class names the skill defines")
New harness scenario
tests/interrupt.sh PICKUP:CLASS_JQplus the reference pickup's C9 must equal the rows of the pickup skill's table and of dispatch Reconcile.accepted,unaccepted,unconfirmed,inconsistent) must be defined in the skill.docs/,src/and the attempt store byte-identical.Mutation-checked by hand: dropping
| C7from the pickup skill, dropping| C10from Reconcile, or letting pickup append to the plan each fails the scenario.ref-host pickupnow readsopen_attemptsfrom a real handoff log's frontmatter (F16 was updated to write one).Review focus
examples/basic-workspaceversus keeping the example attempt-free.Tests (local, macOS, bash 3.2)
tests/interrupt.sh: 35/35 (PICKUP added).tests/skills.sh: 142.tests/docs.sh: 32.tests/openspec-apply.sh: 6.tests/install.sh: 7.tests/version.sh: 4.shellcheck,sync-bundled --checkandvalidate-pluginare clean.🤖 Generated with Claude Code