Skip to content

Detect orphaned run_in_background watchers (Issue #1739)#1740

Open
konard wants to merge 3 commits into
mainfrom
issue-1739-8c3fcb9986ca
Open

Detect orphaned run_in_background watchers (Issue #1739)#1740
konard wants to merge 3 commits into
mainfrom
issue-1739-8c3fcb9986ca

Conversation

@konard

@konard konard commented May 1, 2026

Copy link
Copy Markdown
Contributor

Closes #1739.

Summary

Issue #1739 captured a Claude session that appeared "complete" while a background watcher kept polling forever. The model emitted

until [ "$(gh run view RUNID --json status -q .status)" = "completed" ]; do sleep 20; done

with run_in_background: true, then ended its turn (stop_reason: end_turn, terminal_reason: completed). The hive-mind harness had no awareness of live background tasks, so it accepted the result as a clean session — the watcher was orphaned and the run hung silently for 2 h 54 min until manual CTRL+C. Total wasted spend: $12.87 across 242 turns. See docs/case-studies/issue-1739/README.md for the full forensic write-up.

This PR addresses the root cause on three fronts:

  • Detect it (S-1) — every JSONL event flows through a new BackgroundTaskTracker (src/claude.background-tasks.lib.mjs) that maps system.task_started / system.task_completed. At the result event the harness now logs how many background tasks survived end-of-turn (🔎 Background tasks: clean (0 alive at result event) for healthy sessions) and emits 🛑 STUCK-WATCH DETECTED when a passive final assistant message ("I'll wait for…", "Once it completes…") coincides with surviving tasks.
  • Prevent it (S-2) — the Claude system prompt (src/claude.prompts.lib.mjs) now explicitly forbids hand-rolled until ... do sleep N; done and while ... do sleep N; done watchers and steers the model toward gh run watch <run-id> --exit-status and timeout T ... as finite-timeout alternatives.
  • Document it — a complete case study lands in docs/case-studies/issue-1739/ (timeline, requirements, root causes, solution plan, external research, draft upstream issue body for anthropics/claude-code) along with the original 4.4 MB forensic log, preserved via a narrow .gitignore exception.

The classifier is diagnostic-only in this PR — no behavior change beyond log lines. A follow-up can gate --abort-on-stuck-watch on the same signal.

Files

Area File What
New lib src/claude.background-tasks.lib.mjs BackgroundTaskTracker, looksLikePassiveWaitText, classifyStuckWatch, plus thin createStuckWatchSessionState / observeSessionEvent / reportStuckWatchAtResult helpers
Wire-up src/claude.lib.mjs Two added lines: observeSessionEvent(...) per event and reportStuckWatchAtResult(...) at the result event
Prompt src/claude.prompts.lib.mjs New "wait for external event" guidance that forbids unbounded sleep loops
Tests tests/test-stuck-watch-detection-1739.mjs Replays the exact issue-1739 events; covers passive-text edge cases and formatForLog output
Docs docs/case-studies/issue-1739/ README, timeline, requirements, root causes, solution plan, external research, plus logs/original.log (forensic artifact)
Release .changeset/stuck-watch-detection-1739.md Patch release note

Test plan

  • node tests/test-stuck-watch-detection-1739.mjs — passes; replays the exact issue-1739 task IDs / passive text and asserts classifyStuckWatch returns { stuck: true }
  • npm run lint — clean across the whole repo (claude.lib.mjs stays under the 1500-line cap by extracting helpers into the new lib)
  • npx prettier --check on all touched files — clean
  • node scripts/run-tests.mjs --suite default — 47 passed, 3 pre-existing failures in test-issue-1706-sub-session-size.mjs (unrelated; reproduce on origin/main)
  • Manual repro: run a long workflow + check that 🔎 Background tasks: clean appears in --verbose logs of any normal session, and 🛑 STUCK-WATCH DETECTED appears when the antipattern is provoked
  • Follow-up: file upstream issue at anthropics/claude-code (draft ready in docs/case-studies/issue-1739/external-research.md)

Mapping to issue requirements

The issue body lists 12 explicit asks. Status table is in docs/case-studies/issue-1739/requirements.md. In short:

  • ✅ Root cause (RC-1..RC-5 in root-causes.md)
  • ✅ Logs/data committed to docs/case-studies/issue-1739/
  • ✅ Reproducible example (fixture replay recipe in external-research.md)
  • ✅ Workarounds documented (S-2 prompt change applied; user-side guidance in case study)
  • ✅ Suggestions for upstream fix (drafted, ready to file post-review per Dockerize solve.mjs #12)
  • ✅ Verbose tracing for next iteration (S-4 — 🔎 Background tasks: … log line)
  • 🟡 Cumulative cost ceiling on auto-restart (S-3, deferred to follow-up)

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #1739
@konard konard self-assigned this May 1, 2026
…unbounded poll loops (#1739)

Issue #1739 reported a Claude session that emitted `until [ "$(gh run view ...)" = "completed" ]; do sleep 20; done` with `run_in_background: true`, then ended its turn while the watcher kept polling. The harness had no awareness of live background tasks, so it accepted `stop_reason: end_turn` / `terminal_reason: completed` as a clean session — the watcher was orphaned and the run silently hung for 2 h 54 min before manual CTRL+C.

Changes:
- src/claude.background-tasks.lib.mjs: BackgroundTaskTracker that maps `system.task_started` / `system.task_completed` events; `looksLikePassiveWaitText` heuristic; `classifyStuckWatch` combines them; thin `createStuckWatchSessionState`/`observeSessionEvent`/`reportStuckWatchAtResult` helpers keep the call site in claude.lib.mjs to two added lines.
- src/claude.lib.mjs: wire the tracker into the JSONL parser; at the result event, log `🔎 Background tasks: ...` (clean or live) and `🛑 STUCK-WATCH DETECTED` when a passive final message coincides with surviving tasks.
- src/claude.prompts.lib.mjs: forbid hand-rolled `until/while ... do sleep N; done` watchers in the system prompt; recommend `gh run watch <run-id> --exit-status` and `timeout T ...` instead.
- tests/test-stuck-watch-detection-1739.mjs: replays the exact issue-1739 event sequence; covers passive-text heuristic edge cases and `formatForLog` output.
- docs/case-studies/issue-1739/: full case study (README, timeline, requirements, root causes, solution plan, external research) plus the original 4.4 MB forensic log preserved via a narrow `.gitignore` exception.
- .changeset/: patch release note.

The classifier is diagnostic-only in this PR — no behavior change beyond log lines. A follow-up can gate `--abort-on-stuck-watch` on the same signal.
@konard konard changed the title [WIP] claude tool task solution is stuck Detect orphaned run_in_background watchers (Issue #1739) May 1, 2026
@konard
konard marked this pull request as ready for review May 1, 2026 14:53
@konard

konard commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

CI is queued for the new commit. Done with the immediate work — let me confirm the PR contents are visible end-to-end.

PR #1740 is ready at #1740

What landed:

  • New diagnostic library src/claude.background-tasks.lib.mjs tracks Claude Code's run_in_background Bash tasks via system.task_started/task_completed events; classifier flags sessions that end with passive "I'll wait…" text while a watcher is still alive.
  • Two-line wire-up in src/claude.lib.mjs logs 🔎 Background tasks: … at every result event and 🛑 STUCK-WATCH DETECTED when the issue-1739 shape recurs.
  • System prompt update in src/claude.prompts.lib.mjs forbids unbounded until/while … sleep watchers and steers the model to gh run watch --exit-status / timeout T ….
  • Full case study in docs/case-studies/issue-1739/ (README, timeline, requirements, root causes, solution plan, external research) plus the original 4.4 MB forensic log via a narrow .gitignore exception.
  • Unit tests (tests/test-stuck-watch-detection-1739.mjs) replay the exact event sequence; lint and prettier clean across the repo.
  • Patch changeset added.

Deferred to follow-ups (documented in case study): cost ceilings on auto-restart (S-3), --abort-on-stuck-watch flag (S-1b), and filing the drafted upstream issue at anthropics/claude-code (S-5).

CI is queued on commit df0b1322; pre-existing failures in test-issue-1706-sub-session-size.mjs reproduce on origin/main and are unrelated.

Want me to /schedule an agent in 1 week to file the upstream anthropics/claude-code issue once this PR has been reviewed?


This summary was automatically extracted from the AI working session output.

@konard

konard commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $8.124249

📊 Context and tokens usage:

Claude Opus 4.7: (3 sub-sessions)

  1. 125.8K / 1M (13%) input tokens, 4.5K / 128K (3%) output tokens
  2. 114.2K / 1M (11%) input tokens, 35.0K / 128K (27%) output tokens
  3. 75.3K / 1M (8%) input tokens, 15.8K / 128K (12%) output tokens

Total: (3.8K new + 287.4K cache writes + 9.1M cache reads) input tokens, 69.6K output tokens, $8.124249 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus
  • Model: Claude Opus 4.7 (claude-opus-4-7)

📎 Log file uploaded as Gist (4528KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented May 1, 2026

Copy link
Copy Markdown
Contributor Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claude tool task solution is stuck

1 participant