loop(): an interrupted run leaves a non-expiring claim, so the prompt is silently skipped forever on resume

Open
#94 0 comments 0 reactions 0 assignees View on GitHub

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

bug S2

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

  1. Configure a loop over several outstanding prompts with a real (non-instant) agent.
  2. Start the loop. While a prompt is being processed (its claim is held), interrupt the process with Ctrl+C.
  3. 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 falseSkip (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.

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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from joewalker/loop-the-loop

All issues in joewalker/loop-the-loop

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.