Observed behavior
If a loop() run is terminated ungracefully (Ctrl+C / SIGINT, SIGKILL, or a crash) while a prompt is claimed but not yet completed, the persisted claim is never cleaned up. On any subsequent run the prompt is skipped with Skip (claimed elsewhere) and is never processed again on that run or any future run. There is no error or warning: the resumed run reports normal completion while having silently dropped one (or more) outstanding prompts.
Expected behavior
Resuming after an interrupted run is a documented core feature ("Resumes from saved state if a previous run was interrupted"). A prompt that was claimed but never completed should be treated as outstanding and reprocessed on the next run.
Minimal reproduction
- Configure a loop over several outstanding prompts with a real (non-instant) agent.
- Start the loop. While a prompt is being processed (its claim is held), interrupt the process with Ctrl+C.
- Re-run the same loop (same
name/outputDir, i.e. resume).
Result: the prompt that was in flight at interrupt time is logged as Skip (claimed elsewhere) and is not processed — on this run or any later run.
Root cause
FileLoopState.claim() persists the claim to the state file (await this.save()) when it is first acquired, keyed by the current run's runId.
- A claim is only removed by
complete() (on a terminal result) or by release(), which loop() calls in its finally block.
- A default SIGINT terminates the process without unwinding the
finally, so release() never runs and the claim remains in the persisted state, owned by a now-dead runId.
- On a later run,
claim() sees a claim whose runId differs from the new run's and returns false → Skip (claimed elsewhere). PromptClaim has an expiresAt field, but it is never set and never checked, so the stale claim never expires and the prompt is skipped on every future run.
Impact / severity
Interrupt-then-resume is mainstream, documented usage, and the failure is silent and permanent (the stale claim persists in the state file and suppresses the prompt indefinitely). That silent-wrong-result, beyond-the-initial-call quality is why this is filed S2; it has S1-leaning characteristics and could reasonably be triaged higher. (For calibration: #89 covers a different crash-window state issue rated S4 — this one differs in that the bad state persists and silently drops work on every subsequent run rather than losing progress that would be re-done.)
Possible remedies (non-prescriptive)
- Install a scoped SIGINT/SIGTERM handler for the lifetime of
loop() that releases the run's claims before exiting (covers all interrupt points; needs care as library-level signal handling and must be removed on normal exit).
- Give claims a TTL via the existing
expiresAt field and let claim() reclaim expired claims. The TTL must be safely longer than a prompt's maximum runtime to avoid double-processing the same prompt under concurrent processes.
Note
This was identified while adding the per-prompt cost-estimate summary, which prints a "Press CTRL+C to stop execution" prompt before a deliberate pause. That feature was adjusted so the claim is acquired after the pause (the claim is no longer held during the Ctrl+C window the summary advertises). The underlying issue described here — an interrupt during the agent invoke, where a claim is necessarily held — is independent of that feature and exists on main.
Observed behavior
If a
loop()run is terminated ungracefully (Ctrl+C / SIGINT, SIGKILL, or a crash) while a prompt is claimed but not yet completed, the persisted claim is never cleaned up. On any subsequent run the prompt is skipped withSkip (claimed elsewhere)and is never processed again on that run or any future run. There is no error or warning: the resumed run reports normal completion while having silently dropped one (or more) outstanding prompts.Expected behavior
Resuming after an interrupted run is a documented core feature ("Resumes from saved state if a previous run was interrupted"). A prompt that was claimed but never completed should be treated as outstanding and reprocessed on the next run.
Minimal reproduction
name/outputDir, i.e. resume).Result: the prompt that was in flight at interrupt time is logged as
Skip (claimed elsewhere)and is not processed — on this run or any later run.Root cause
FileLoopState.claim()persists the claim to the state file (await this.save()) when it is first acquired, keyed by the current run'srunId.complete()(on a terminal result) or byrelease(), whichloop()calls in itsfinallyblock.finally, sorelease()never runs and the claim remains in the persisted state, owned by a now-deadrunId.claim()sees a claim whoserunIddiffers from the new run's and returnsfalse→Skip (claimed elsewhere).PromptClaimhas anexpiresAtfield, but it is never set and never checked, so the stale claim never expires and the prompt is skipped on every future run.Impact / severity
Interrupt-then-resume is mainstream, documented usage, and the failure is silent and permanent (the stale claim persists in the state file and suppresses the prompt indefinitely). That silent-wrong-result, beyond-the-initial-call quality is why this is filed S2; it has S1-leaning characteristics and could reasonably be triaged higher. (For calibration: #89 covers a different crash-window state issue rated S4 — this one differs in that the bad state persists and silently drops work on every subsequent run rather than losing progress that would be re-done.)
Possible remedies (non-prescriptive)
loop()that releases the run's claims before exiting (covers all interrupt points; needs care as library-level signal handling and must be removed on normal exit).expiresAtfield and letclaim()reclaim expired claims. The TTL must be safely longer than a prompt's maximum runtime to avoid double-processing the same prompt under concurrent processes.Note
This was identified while adding the per-prompt cost-estimate summary, which prints a "Press CTRL+C to stop execution" prompt before a deliberate pause. That feature was adjusted so the claim is acquired after the pause (the claim is no longer held during the Ctrl+C window the summary advertises). The underlying issue described here — an interrupt during the agent invoke, where a claim is necessarily held — is independent of that feature and exists on
main.