Fix ask_user_question in DeepSeek Harness goal rounds
During an autonomous goal round, when the model calls ask_user_question it does not receive your answer but an empty-looking return value, so it keeps spinning for dozens of rounds. The cause is two independent facts combining: goal rounds share the same runtime root as the user (the existing interaction guard only covers subagents, not autonomous rounds), and in Code Mode a nested tool answer only re-enters model context if the program logs or returns it. The right fix is an agent-scoped ctx.tools.guard() that denies ask_user_question for the lifetime of that round so the model records the need through update_goal(blocked); the upstream in-tree fix is ready and waiting for someone to open the pull request, with a behaviour-equivalent community plugin available in the meantime.
DeepSeek Harness goal-round spin as it happens
The symptom reads like "the model ignores the human", but it is really "the model never received the human". The recorded incident:
- Nested ask plus two answers, neither taking effect: during an autonomous goal run (Code Mode /
run_code, same-session goal with a 256-round cap) the model called, insiderun_code,await tools.ask_user_question({...}); return "asked";. The user answered twice, and both answers do appear in the session log as innertool/code-dispatchresults, but the outertool/resultthe model sees was only"asked"both times (#6074). - Then a long tail of spinning: the model went on to run 30+ goal rounds, each executing verifier slices and each ending with "Awaiting your two decisions" — waiting for an answer that would never arrive.
- A pending question stalls the driver: a pending question declares no timeout budget, so the driver waits forever. The first answer took about 8.5 minutes to arrive in the recorded case, during which the whole autonomous flow was stalled.
- Both causes carry half the blame: (1) autonomous rounds have no blocking mechanism for interactive asks, and (2) Code Mode's return-value semantics silently drop nested answers. Fixing only one of them only relieves half the problem.
- The decisive debugging check: do not judge by "I asked and the model says it is waiting", but align three layers — whether the inner
tool/code-dispatchreceipt carries the answer, whether the outertool/resultcarries it, and whether the program wrote it back into context at all.
DeepSeek Harness mechanism: goal rounds share the user's runtime root
This crack comes from two precise source facts, and neither is "the model disobeying". Layer by layer:
- The interaction guard only covers subagents:
dsh-user-questionsonly rejects live subagents (DELEGATED_CALLER), while a goal round runs as the same runtime root, so nothing stops it from asking interactively mid-autonomy. - A pending question has no budget: the question declares no timeout, so the driver waits indefinitely, and "no one will answer" is never translated into "this should be blocked" — the round simply hangs.
- Code Mode only re-feeds logs and returns: in Code Mode only the program's own logs and returns re-enter model context; a tool return value nested inside
run_codemust be written back explicitly by the program, or a successful execution is equivalent to nothing happening. ctx.tools.guard()is an existing core primitive:packages/core/tools/src/index.ts:704definestype ToolGuard = (execution) => string | undefined, where returning a string denies that execution;:1100registersguard(guard: ToolGuard): () => void, whose comment states that it registers after the extensibletools/pre-executewaterfall and that "a plain-context guard applies globally; one registered throughagent.ctxapplies only to that agent". That is exactly the scoped semantics "deny only during autonomous rounds" needs.- Why not
restrict():tools.restrict()(:1061) also requires a scoped context and:1064throws "requires a scoped context (agent.ctx)". More importantly, it hides the tool from the menu, whereas a guard denial is a catchableToolCallError: the model can plainly see that the tool is disabled for the current turn and switch toupdate_goal(blocked)or keep pushing, which is precisely what an autonomous round needs. - A plugin can mount on the same seam: the
agent/inbox/claimedevent (packages/core/agent-loop/src/inbox.ts:114, fired when a message is claimed) together withsession/event'suser/message(the admit path) andturn/end/turn/startlets a plugin reproduce the same semantics without touching driver source.
DSH plugin recipe: deny interactive asks with ctx.tools.guard()
The recipe is: detect the goal window, install an agent-scoped guard, and dispose it when the window closes. The upstream fix and the community plugin share the same primitive. Specifically:
- Window detection: identical to goal-round-driver — only
source.kind === 'goal'withround > 0counts as an autonomous round, so a user's legitimate question in a non-goal round is never affected; everything outside the window passes through. - Install site: register an agent-scoped
ctx.tools.guard()onagent/createdplusagent/inbox/claimed, returning a denial reason forask_user_question. The in-tree version implements this insidepackages/goal/goal-round-driver/(addingDriverState.askGuardDispose,GOAL_ROUND_ASK_DENIAL, and theinstallAskGuard/clearAskGuardhelpers), and the core of it is short:
/** Deny interactive human questions while an admitted goal round owns the turn. */
const denyAskDuringRound: ToolGuard = execution =>
execution.name === 'ask_user_question' ? GOAL_ROUND_ASK_DENIAL : undefined
- The clear sites must be complete: clearing only on
turn/endis not enough — if a claimed goal round is discarded or superseded and the guard is only cleared at turn end, it lingers and denies a legitimate non-goal ask for the remainder of that turn. The full set also includesagent/session-start,agent/inbox/inserted(competing input),agent/inbox/discarded(superseded), andagent/disposed; on a multi-agent host theturn/endclose must additionally be routed per agent (viasession.id → agent), or agent A's turn end clears agent B's window early. - The denial text must point somewhere: saying only "this round will continue on its own" is not enough. Use the same in-tree
GOAL_ROUND_ASK_DENIALtext so the model is directed to record the need withupdate_goal(blocked)(including a concreteblocked_reason) rather than silently retrying. - Use the community plugin in the interim:
@argszero/cordis-plugin-goal-ask-guardreproduces the same semantics with the primitive above at theagent/createdseam; v0.1.1 added per-agentturn/endrouting, the extra clear sites, listener disposal, and the shared denial copy (13/13 tests passing):
npm install @argszero/cordis-plugin-goal-ask-guard
- insert:
- id: goal-ask-guard
name: '@argszero/cordis-plugin-goal-ask-guard'
- Install plugins through the market: add and update guard plugins like this from DSH Plugin Hub under Settings, Plugin Market, which is safer than hand-writing mount lines into the profile because a failure rolls the manifest back. If you are also debugging other guard-type backstops (repeat calls, runaway arguments), see runaway tool-call argument troubleshooting.
DSH plugin troubleshooting notes
Do not blame the model alone — line up all three layers to see whether the answer ever reached the model's context; that is the key discriminator here. Six points to keep in mind when a DeepSeek Harness plugin hits an autonomous goal round that asks a question:
- Do not blame the model alone: inspect the inner receipt, the outer
tool/result, and whether the program wrote the value back into context, as three separate layers. DELEGATED_CALLERcannot reach autonomous rounds: a goal round shares the user's root, so it is not restricted like a subagent.- Do not swap the guard for
restrict(): hiding the tool leaves the model unable to tell why, whereas a catchable denial lets it move toupdate_goal(blocked). - Err on the side of more clear sites: besides
turn/endthere are session-start, competing input, superseded, and disposed. - Mind scoping on multi-agent hosts: the window is per agent, so avoid a global
turn/endlistener. - In Code Mode the answer must be written back explicitly: that is the other half of the root cause, independent of the guard, since tool return values are not re-fed automatically.

Sources: Discussion #6074, cordis-plugin-goal-ask-guard, npm @argszero/cordis-plugin-goal-ask-guard.
FAQ
In a DeepSeek Harness goal round the model never receives your answer because two independent facts combine. (1) A goal round shares the same runtime root as the user, and dsh-user-questions only rejects live subagents (DELEGATED_CALLER), so nothing stops an autonomous round from asking interactively; (2) in Code Mode only the program's own logs and returns re-enter model context, so an answer nested inside run_code is silently discarded. In the recorded incident the outer tool/result returned only "asked" both times, after which the model ran 30+ goal rounds, each ending with "Awaiting your two decisions" (source: Discussion #6074).
In DeepSeek Harness a pending question declares no timeout budget, so the driver waits indefinitely for an answer that may never come. In the recorded incident the first answer took about 8.5 minutes to arrive, and the driver stalled for the whole interval. Nothing cancels it automatically, and nothing tells the model that no one is going to reply, so the autonomous round simply hangs (source: Discussion #6074).
In a DeepSeek Harness goal round the right landing point is an agent-scoped ctx.tools.guard(): the guard returns a string to deny that execution, and a guard registered through agent.ctx applies only to that agent, which matches the "deny only during autonomous rounds" semantics exactly. It is preferable to restrict()-hiding because a guard denial is a **catchable ToolCallError**: the model can see that the tool is disabled for this turn and switch to recording the need through update_goal(blocked) or push on, instead of facing a tool that silently vanished (source: Discussion #6074).
DeepSeek Harness has a fix ready as an in-tree branch (packages/goal/goal-round-driver/ gains DriverState.askGuardDispose, GOAL_ROUND_ASK_DENIAL, and installAskGuard / clearAskGuard; 88 insertions across 4 files; the package suite passes 57/57), but its author lacks pull-request permission on the repository, so a maintainer has to open it from the branch. Meanwhile the community plugin @argszero/cordis-plugin-goal-ask-guard (v0.1.1, 13/13 tests) reproduces the same semantics with the same existing primitive without touching driver source (source: Discussion #6074).
Related Terms
- goal round
- One round of execution driven by continuation under autonomous goal mode, identified by message source `source.kind === 'goal'` with `round > 0`. It shares the same runtime root as user interaction, so it is not restricted the way a subagent is.— https://github.com/deepseek-ai/deepseek-harness/discussions/6074
- ctx.tools.guard()
- An existing core tool-guard primitive that registers a monotonic guard after the extensible tools/pre-execute waterfall; returning a string denies that execution. Registered through agent.ctx it applies only to that agent, and the denial is not hidden from the tool menu but surfaced as a catchable ToolCallError.— https://github.com/deepseek-ai/deepseek-harness/discussions/6074
- Code Mode return-value semantics
- In Code Mode the model only sees the program's own logs and returns, so a tool's return value must be written back explicitly by the program; otherwise a successful execution never re-enters model context.— https://github.com/deepseek-ai/deepseek-harness/discussions/6074
Sources
- deepseek-harness Discussion #6074: ask_user_question usable during autonomous goal rounds (with the in-tree diff and the community plugin)· deepseek-ai (GitHub Discussions)
- cordis-plugin-goal-ask-guard: registers an agent-scoped guard over ask_user_question during autonomous goal rounds· GitHub (argszero)
- npm: @argszero/cordis-plugin-goal-ask-guard (v0.1.1, 13/13 tests passing)· npm