Repository navigation
Goal rounds repeatedly wake an idle orchestrator while its subagents are still running, burning rounds and tokens on no-op turns #9607
Replies: 2 comments
|
Thanks — the root cause is exactly as you traced it. There is a second half worth making explicit, because it decides where the fix belongs. The driver already refuses a paused goal.
Two delegation shapes behave differently, and only one is invisible:
The job half is already covered by a shipped plugin. Its README names the remaining case as a boundary: "Waiting for something that is not a job (an approval, a subagent reporting through another channel) is not covered. The same pause/resume shape extends to it with a different trigger." Continuable children are that case, and I am extending the plugin with a subagent arm — pairing On your suggested fix, three notes:
One channel note: this repository has issues and PRs disabled — Discussions is the only upstream route. An out-of-tree plugin published to npm (which you have already built) is how a fix like this reaches users; mounting it from a profile is the same |
|
Following up on this thread with a packaged fix, since the gap here is exactly the one a plugin can close from outside the harness. The driver's gate is already right —
Mount it in the profile; both dimensions default on: - insert:
- id: goal-progress-guard
name: '@argszero/cordis-plugin-goal-progress-guard'
config:
deferWhileJobsPending: true
deferWhileSubagentsPending: true
maxDeferMs: 1800000 # optional: release a deferral held this longTwo design notes that may be useful here independent of the plugin:
Verified: 47 tests, plus 22 mutation checks ( Source: this discussion (#9607), building on the progress-guard plugin already written for #7198 (with #1831 / #4664). |
Uh oh!
There was an error while loading. Please reload this page.
Summary
While a goal is active and the agent has delegated work to running subagents (or background jobs), the goal-round driver keeps admitting new rounds for the orchestrator as soon as it goes idle. The orchestrator has nothing actionable to do, so each round degenerates into a no-op turn: model tokens are spent, the
max_goal_roundsbudget is consumed, and the goal can eventually be blocked withround-limiteven though the delegated work never finished.Steps to reproduce
create_goal), so the goal is armed and rounds fire automatically.subagent) so they keep running for several minutes.Observed (DSH
0.2.1-alpha.2, commitd743267388): each time the orchestrator returns to idle, a new<goal_round>continuation message is queued and a fresh model turn runs. The orchestrator cannot make progress — the only pending work lives in the subagents — so it produces filler output (e.g. restating that it is waiting) and ends the turn, after which the cycle repeats for as long as the subagents run.Expected: while the only outstanding work is delegated/background work, the goal-round driver should hold the next round instead of waking the orchestrator into an empty turn.
Root cause (upstream code)
packages/goal/goal-round-driver/src/index.tsdecides drivability purely from the agent's own state:readyToDrive()(line 103) checksagent.status === 'idle'and the absence of competing queued prompts — nothing else.drive()then reserves the next round (round = goal.roundsStarted + 1, line 174) and queues it viaagent.followup(message)(line 192).The driver has no awareness of in-flight descendants or background jobs: there is no reference to subagent or job state anywhere in the package. An idle orchestrator with live delegated work therefore looks exactly like an idle orchestrator with nothing pending.
Impact:
roundsStarted(and thusmax_goal_rounds) is consumed by no-op rounds; a long delegation can push the goal intoround-limitblocking before any real round happens.Suggested fix
Add an in-flight gate in
drive(), before reserving the next round:subagentsservice) and background jobs (jobsservice) viactx.get(...)so both stay optional dependencies.runningor any job isrunning/stopping, defer the round: reserve nothing, spend no round budget, and re-check after a short configurable poll interval.Configfields rather than hardcoded constants, per repo conventions.To be clear about intent: this is not asking goal rounds to wait for subagent results or block on them. The gate only skips rounds that provably have nothing to do; as soon as the agent has its own pending work, or the delegated work finishes (or the deferral cap expires), rounds resume exactly as before.
I have implemented and tested this in an out-of-tree fork of
dsh-goal-round-driver(drop-in replacement plugin) and am happy to contribute it upstream in whatever form is preferred:inFlightWork()/deferRound()/ themaxDeferMsexpiry check indrive().Environment
d743267388), packages/goal/goal-round-driverAll reactions