PR3 v0.3.0 - real intervention + stuck-timeout + intervention cap - #3
Open
bettercallzaal wants to merge 1 commit into
Open
PR3 v0.3.0 - real intervention + stuck-timeout + intervention cap#3bettercallzaal wants to merge 1 commit into
bettercallzaal wants to merge 1 commit into
Conversation
PR2 logged supervisor verdicts but did not act. PR3 wires them through. What lands: - HermesRunner captures session_id from the first stream-json system line, then on intervene() kills the live claude process and respawns `claude --resume <session_id> --print --output-format stream-json ... <message>` preserving the same systemPrompt + tool whitelist. The orchestrator's stream loop transparently continues - the new process appends to the same output buffer. - Orchestrator races iter.next() against a stuck-timeout (default 60s, opt-out with stuckTimeoutMs: 0). On stuck, records an intervened event and calls runner.intervene with a "are you stuck?" nudge. - maxInterventions cap (default 3). After N interventions, the next intervene-or-stuck escalates to kill + aborted outcome. Prevents unbounded back-and-forth on a hopelessly off-track session. - All intervened events now record acted: true (was false in PR2). - supervisor.ts clears its loop window after firing a loop verdict so the orchestrator does not double-fire on the next identical message. Surface area: - types.ts: OrchestrateOptions += stuckTimeoutMs, maxInterventions - adapters/hermes-runner.ts: session_id capture, intervene() implementation, swapping flag so the close handler does not finalize the run mid-swap - orchestrator.ts: stuck-timeout race in drain loop, intervene call, intervention cap with kill escalation - supervisor.ts: reset state.recentAssistantMessages after fire - tests/orchestrator.test.ts: +3 PR3 tests - calls runner.intervene on loop verdict with acted=true - stuck-timeout fires and caps escalate to kill - max-interventions cap escalates verdict-driven intervene to kill Tests: 26/26 pass (+3 orchestrator). Known limit (PR4): supervisor still uses static loopThreshold per call. Learning from past intervened outcomes lives in PR4 when learner.retrieve returns hits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR teaches
PR2 watched the stream and logged supervisor verdicts. PR3 wires them through. The first PR where, in production, a stuck or off-track Hermes session gets pulled back on course mid-run instead of after-the-fact.
The hard part wasn't the supervisor logic - it was the runner.
claude --printis one-shot; you cannot inject mid-run input. The fix: capture the session_id from the first stream-json system line, then on intervene() kill the live process and respawnclaude --resume <id> ... <message>with the same systemPrompt + tool whitelist. The orchestrator's stream loop transparently continues - the new process appends to the same output buffer.Mechanics
wireProcess()in HermesRunner, regex on stdoutintervene()kills old, spawnsclaude --resume ...Promise.race(iter.next(), sleep)stuckTimeoutMs: 60_000maxInterventions: 3state.recentAssistantMessages = []after loop fireThe
swappingflag on ActiveRun is the load-bearing detail. Without it, the SIGTERM on the old process triggersclosewhich setsclosed = true, the stream loop exits, and the orchestrator never sees the resumed events. With it, the close handler skips finalisation during the swap window.Limits left for later PRs
loopThresholdper call. PR4 wireslearner.retrieveagainst pastintervenedoutcomes for this pattern; the supervisor becomes adaptive (e.g. drop loopThreshold to 2 if this pattern historically loops at 3). Dormant until Bonfire admin labeling unlocks - the code path lands now.research-doc,meeting-capture. Today onlyhermes-bug-fixexists.Verification
New orchestrator tests:
calls runner.intervene on loop-detected verdict with acted=truestuck-timeout fires when the stream goes quietmaxInterventions cap escalates a verdict-driven intervene to killBuild-in-public note
This is the PR I would have wanted three weeks ago when a research subagent burnt 40 minutes spinning on
WebFetchagainst a Reddit URL that always returned 403. With PR3 in place: supervisor flags off-track on tool call #2, orchestrator nudges with "use/fetchskill instead - reddit blocks WebFetch",claude --resumepicks up the redirect, agent recovers. Without it: 40 minutes of stuck-loop billed silently.Lands on merge
v0.3.0after PR2 → PR3 cascadeacted: trueflag is now real - downstream consumers can trust it🤖 Generated with Claude Code