loop(): an interrupted run leaves a non-expiring claim, so the prompt is silently skipped forever on resume
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- typescript
- Domain
- backend
Research direction
Start by tracing FileLoopState.claim(), complete(), and release(), then follow loop()'s finally block and the persisted PromptClaim fields, especially expiresAt. Reproduce an interrupted run and inspect the saved state; done means a claimed-but-uncompleted prompt is safely eligible for processing after resume without breaking normal completion or concurrent claims.
Written by the indexing model from the issue text.
Description
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'srunId.- A claim is only removed by
complete()(on a terminal result) or byrelease(), whichloop()calls in itsfinallyblock. - A default SIGINT terminates the process without unwinding the
finally, sorelease()never runs and the claim remains in the persisted state, owned by a now-deadrunId. - On a later run,
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)
- 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
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.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from joewalker/loop-the-loop
-
bug S4
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
joewalker/loop-the-loop#88 ·
-
Git exec(): child killed by a signal rejects with new Error('') and loses the signal information Openbug S3
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
joewalker/loop-the-loop#84 ·
-
bug S3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
joewalker/loop-the-loop#83 ·
-
bug S3
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
joewalker/loop-the-loop#82 ·
-
bug S4
Difficulty 1/5 Under an hour Newbie friendliness 90/100
joewalker/loop-the-loop#79 ·
All issues in joewalker/loop-the-loop
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Eynzof/Hermes-CN-Desktop#610 ·
-
bug clawsweeper:linked-pr-open clawsweeper:needs-live-repro clawsweeper:no-new-fix-pr impact:message-loss issue-rating: 🐚 platinum hermit P2 regression
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100
danielmiessler/LifeOS#2218 ·