Skip to content

fix(hive): stop workers from getting silently stuck (#1811) - #1812

Open
konard wants to merge 10 commits into
mainfrom
issue-1811-7f33adedb747
Open

fix(hive): stop workers from getting silently stuck (#1811)#1812
konard wants to merge 10 commits into
mainfrom
issue-1811-7f33adedb747

Conversation

@konard

@konard konard commented May 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1811 — hive workers occasionally stalled inside verifyResults with no progress, no failure, and no recovery. The root cause was a hung gh api user call (the GitHub CLI has no default HTTP timeout). The hive parent had no way to notice "child is alive but quiet," so the stuck worker would block its slot indefinitely.

This PR adds two complementary safety nets and the supporting test + documentation work.

1. gh call-level timeout (src/github-rate-limit.lib.mjs)

  • New GhTimeoutError and callWithTimeout(fn, ms, signal) helper that races a promise against a setTimeout and aborts the underlying AbortController when the timer wins (the loser's rejection is silenced so we don't leak unhandled rejections).
  • ghWithRateLimitRetry / wrapDollarWithGhRetry now accept a per-call timeoutMs and a wrapper-level defaultTimeoutMs.
  • wrapDollarWithGhRetry can opt in (supportsOptionsForm: true) to the dual-form $({ signal }) API exposed by command-stream, so a timed-out call actually SIGTERMs the spawned gh child instead of just rejecting the JS promise. solve.results.lib.mjs opts in; tests with naive $ fakes do not, preserving backward-compatible single-call semantics.
  • Defaults: GH_TIMEOUT_SECONDS env / --gh-timeout-seconds CLI flag, 90s default. Set to 0 to disable.

2. Parent-side worker inactivity watchdog (src/hive-worker-watchdog.lib.mjs)

  • Extracted into a standalone, injectable-clock helper so the warn → heartbeat → SIGTERM → SIGKILL state machine can be unit-tested without spawning real processes.
  • hive.mjs wires it into every spawned worker; stdout/stderr lines call watchdog.markActivity(line).
  • New CLI options on hive:
    • --worker-inactivity-warn-seconds (default 300) — log a warning when a worker emits nothing for N seconds.
    • --worker-inactivity-kill-seconds (default 0, disabled) — SIGTERM the worker, then SIGKILL after a 10s grace period.
  • Verbose mode adds a "still alive but quiet" heartbeat between 60s and warnMs.

3. Case study + upstream report

Full root-cause walkthrough, the actual 90k-line stuck-hive log, timeline, and a draft upstream report for the gh CLI live under docs/case-studies/issue-1811/.

Files

  • src/github-rate-limit.lib.mjsGhTimeoutError, callWithTimeout, timeoutMs / defaultTimeoutMs / supportsOptionsForm plumbing.
  • src/hive-worker-watchdog.lib.mjs (new) — pure helper, injectable now clock, onEvent callback (kind ∈ {warn, heartbeat, sigterm, sigkill}).
  • src/hive.mjs — uses the watchdog for each spawned worker; pipes events into the existing log() channel.
  • src/hive.config.lib.mjs — registers the new --worker-inactivity-* flags and adds them to HIVE_ONLY_OPTION_NAMES.
  • src/solve.results.lib.mjs — opts in to the dual-form $ API so verifyResults can actually be timed out.
  • src/config.lib.mjsgh-timeout-seconds flag + GH_TIMEOUT_SECONDS env wiring.
  • tests/github-rate-limit.test.mjs — 12 new tests (39 total) covering GhTimeoutError, callWithTimeout, per-call timeouts, default timeouts, and the opt-in options form.
  • tests/test-hive-worker-watchdog-1811.mjs (new) — 8 tests for the watchdog state machine using a virtual clock and a fake child.
  • .changeset/issue-1811-stuck-task-watchdog.md — minor bump to 1.73.0.
  • docs/case-studies/issue-1811/** — README, requirements, timeline, root causes, solutions, upstream draft, and the raw hive log.

Test plan

  • node tests/github-rate-limit.test.mjs39 passed, 0 failed.
  • node tests/test-hive-worker-watchdog-1811.mjs8 passed, 0 failed.
  • node --check for every modified .mjs.
  • CI on this PR.
  • Manual smoke test: run a hive with --worker-inactivity-warn-seconds 10 --worker-inactivity-kill-seconds 30 against a worker that gets kill -STOP-ed; confirm warning then SIGTERM/SIGKILL fire.

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

Issue: #1811
@konard konard self-assigned this May 16, 2026
konard added 6 commits May 16, 2026 22:39
Add a deep-dive case study at docs/case-studies/issue-1811/
documenting the silent stall observed in a 5-issue hive run:

- README.md (TL;DR + index)
- timeline.md (reconstructed event sequence with log-line citations)
- requirements.md (R1-R5 + non-goals)
- root-causes.md (RC1-RC5: gh has no default network timeout,
  wrapDollarWithGhRetry has no timeout layer, hive worker has no
  inactivity watchdog, verbose mode does not log shell commands)
- solutions.md (implementation map, components considered, test plan)
- upstream-issue-draft.md (cli/cli issue draft with reproduction)
- logs/hive-issue-1811.txt (8.4MB failing-run log; .txt extension
  to bypass repo .gitignore that excludes *.log)

The case study is the prerequisite analysis for the code changes
that follow on this branch (PR #1812).
The `.gitignore` rule `*.log` was silently excluding the reproduction log
from git tracking. Rename to `hive-issue-1811.txt` so the log ships with
the case study, and update the three references in requirements.md,
root-causes.md and timeline.md accordingly.

Issue #1811.
Closes the silent-stall reported in #1811. Three layered fixes:

* RC2: `wrapDollarWithGhRetry` now accepts a per-call `timeoutMs`
  (default `timeouts.ghApiMs`, 15s, env
  `HIVE_MIND_GH_API_TIMEOUT_MS`). Each `gh` invocation is raced against
  the timeout; on expiry the spawned child is SIGTERMed via
  command-stream's `signal` option (with safe fallback when an older
  command-stream rejects the options form). The timeout is exposed as a
  typed `GhTimeoutError` that feeds the existing transient-error budget
  so legitimately slow calls still retry.
* RC1: `verifyResults` now calls `gh api user` (and `gh pr list`, `gh
  search prs`) under that timeout and degrades gracefully — if user
  lookup times out, the branch-only PR search continues with
  `currentUser = null`. Per-call shell tracing is emitted under
  `--verbose` so future hangs are diagnosable from the log alone.
* RC3: `hive.mjs:worker()` gains a parent-side inactivity watchdog.
  Every stdout/stderr line marks the child as alive; a 1s interval
  warns when `--worker-inactivity-warn-seconds` is exceeded (default
  300s) and SIGTERMs (then SIGKILLs after a 10s grace) when
  `--worker-inactivity-kill-seconds` is set. Verbose mode emits an
  earlier heartbeat between 60s and the warn threshold.

New config:
* `timeouts.ghApiMs` (env `HIVE_MIND_GH_API_TIMEOUT_MS`, default 15000)
* `workers.inactivityWarnMs`, `workers.inactivityKillMs`,
  `workers.killGraceMs` (env-overridable)

New CLI flags:
* `--worker-inactivity-warn-seconds` (default 300)
* `--worker-inactivity-kill-seconds` (default 0 = disabled)

See `docs/case-studies/issue-1811/` for the full root-cause analysis.
…outError

Adds 12 new unit tests for the issue #1811 timeout machinery in
github-rate-limit.lib.mjs and tightens the production wrapper:

- callWithTimeout: silence the loser of the Promise.race so its
  rejection never surfaces as an unhandled rejection; remove the
  unref() on the inner timer (it caused Node to exit before the
  timeout could fire in synthetic tests).
- wrapDollarWithGhRetry: make the options-form invocation
  (`dollar({ signal })`) opt-in via `supportsOptionsForm: true`. This
  keeps backward-compat for naive `$` test fakes that don't distinguish
  the two calling conventions, while solve.results.lib.mjs opts in so
  command-stream's signal-driven cancellation still propagates to the
  spawned `gh` child.

New tests:
- callWithTimeout (3): resolves/rejects/disabled.
- ghWithRateLimitRetry with timeoutMs (2): retry budget, retryOnTimeout=false.
- wrapDollarWithGhRetry options form (7): per-call timeoutMs, wrapper
  defaultTimeoutMs, defaultTimeoutMs=0, verboseLog, $.gh({...}),
  supportsOptionsForm on/off.

All 39 tests pass.
Move the inline 70-line watchdog from hive.mjs into a standalone
src/hive-worker-watchdog.lib.mjs with an injectable clock and onEvent
callback, so the warn/heartbeat/SIGTERM/SIGKILL state machine can be
unit-tested without spawning real child processes.

Covered by 8 new tests in tests/test-hive-worker-watchdog-1811.mjs.
Refs #1811.
Document the gh timeout + worker inactivity watchdog as a minor release
and bump the version accordingly.
@konard konard changed the title [WIP] Some tasks in /hive command just stuck, and didn't finish without clear fail or stop fix(hive): stop workers from getting silently stuck (#1811) May 16, 2026
@konard
konard marked this pull request as ready for review May 16, 2026 23:03
konard added 2 commits May 16, 2026 23:08
CI feedback on #1811:
- check-file-line-limits failed: hive.mjs was 1537 lines (limit 1500).
  Move the per-worker watchdog wiring into wireHiveWorkerWatchdog()
  inside hive-worker-watchdog.lib.mjs and auto-attach the stdout/stderr/
  close/error listeners there. hive.mjs is now 1500 lines, net +2 vs main.
- Check for Manual Version Changes failed: bumps are managed by the
  release workflow via the changeset, so revert package.json 1.73.0 -> 1.72.0.
- Prettier format check failed on two case-study docs and the new
  watchdog helper; re-format.
The opts-form `$({ cwd: tempDir })\`git ...\`` invocations in
solve.results.lib.mjs#cleanupClaudeFile were silently dropping `cwd`
because the wrapper introduced in #1811 treated every key in the
per-call options object as a gh-retry option. Git commands then ran in
the wrong directory, breaking test-issue-1791-gitkeep-cleanup test #3
on this branch.

Now split per-call options into wrapper-owned keys (timeoutMs,
retryOnTimeout, label, log, ...) and dollar-owned keys (cwd, env,
stdin, signal, ...), and forward the latter to the underlying dollar
via its own options-form when it supports it. Falls back to the bare
tagged-template invocation when dollar doesn't accept the options form.

Adds 4 regression tests covering: cwd forwarding for non-gh tagged
templates, cwd forwarding for gh tagged templates without timeout, cwd
merged with signal for gh with supportsOptionsForm + timeoutMs, and
that wrapper-only options (timeoutMs) don't leak into dollar options.
@konard

konard commented May 16, 2026

Copy link
Copy Markdown
Contributor Author

Working session summary

PR is open, ready, mergeable. CI in progress. The background task will notify on completion.


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

@konard

konard commented May 16, 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: $24.245940

📊 Context and tokens usage:

Claude Opus 4.7: (8 sub-sessions)

  1. 118.4K / 1M (12%) input tokens, 9.1K / 128K (7%) output tokens
  2. 117.3K / 1M (12%) input tokens, 13.5K / 128K (11%) output tokens
  3. 115.2K / 1M (12%) input tokens, 30.7K / 128K (24%) output tokens
  4. 116.2K / 1M (12%) input tokens, 20.1K / 128K (16%) output tokens
  5. 113.5K / 1M (11%) input tokens, 39.4K / 128K (31%) output tokens
  6. 116.5K / 1M (12%) input tokens, 27.2K / 128K (21%) output tokens
  7. 116.8K / 1M (12%) input tokens, 22.9K / 128K (18%) output tokens
  8. 36.8K / 1M (4%) input tokens, 1.2K / 128K (1%) output tokens

Total: (28.0K new + 742.0K cache writes + 27.7M cache reads) input tokens, 223.9K output tokens, $24.245940 cost

🤖 Models used:

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

📎 Log file uploaded as Gist (11816KB)


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

@konard

konard commented May 16, 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.

Some tasks in /hive command just stuck, and didn't finish without clear fail or stop

1 participant