Status: Diagnosis only — no code change yet.
Date: 2026-07-19
Related PR: #818 (fix/preset-tui-slash-command) — same root class as the original /preset fix.
Suspected sibling bug reported by user: During superpowers / brainstorm skill conversations, when the orchestrator asks for confirmation or work is interrupted (subagent completes, background task finishes), a build agent turn sometimes appears with an empty user input.
The build agent turn with empty input is the same class of bug as the original /preset issue fixed in #818: a plugin hook calls sessionSdk.promptAsync({ body: { parts: [createInternalAgentTextPart(...)] } }) without specifying an agent field. opencode then resolves the agent via agents.defaultInfo(), which falls back to the built-in build agent whenever default_agent is unset, user-overridden, or not effectively applied. The synthetic: true flag hides the injected text from the TUI, so the user perceives the build turn as having "empty input."
Update (Issue #854): the incomplete-todo continuation path now passes agent: 'orchestrator', is an opt-in beta via backgroundJobs.continueOnIdle: true, and uses a process-local one-attempt gate. Remaining agent-less promptAsync call sites are interview/smartfetch (below).
Orchestrator enters input-wait. After emitting a confirmation question (skill flow), the assistant turn finishes. opencode's per-session Runner transitions to Idle (packages/opencode/src/effect/runner.ts:115-138, packages/opencode/src/session/run-state.ts:60-63). The session is no longer "busy" from the Runner's perspective.
Plugin hook fires promptAsync with a synthetic part and historically no agent field. Remaining agent-less sites:
src/interview/service.ts:622, 871, 933, 1007 — interview/skill flow injections (matches "brainstorm skill flow").continuation-evaluator.ts) previously matched "subagent completes" / "background task finishes"; it now sets agent: 'orchestrator' and only runs when continueOnIdle is enabled.opencode does not guard promptAsync against busy/input-wait state. The HTTP handler at packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts:311-329 does not call assertNotBusy and does not consult the Question service. It proceeds straight to promptSvc.prompt. The Runner, being Idle, immediately startRuns the new turn (runner.ts:131-134). No queue, no reject, no cancellation of the pending question.
Agent resolves to build. packages/opencode/src/session/prompt.ts:636-637:
const agentName = input.agent
const ag = agentName ? yield* agents.get(agentName) : yield* agents.defaultInfo()
When input.agent is omitted, opencode uses agents.defaultInfo() (packages/opencode/src/agent/agent.ts:328-340), which returns the first visible mode: "primary" agent — build (declared first in the agent registry, agent.ts:141-155). omos attempts to set default_agent = "orchestrator" via its config hook (src/index.ts:546-551), but only when default_agent is absent. build is selected whenever:
config.setDefaultAgent === false (plugin config disables it)opencode.json already sets a different default_agentconfig hook didn't run or didn't apply (SDK/runtime version skew: plugin built against @opencode-ai/sdk v1.4.3, installed runtime v1.18.3)"Empty input" is the synthetic flag's visual effect. synthetic: true only controls TUI visibility. packages/opencode/src/session/message-v2.ts:206 still includes synthetic text parts in model messages (the only filters are !part.ignored and part.text !== ""; there is no synthetic filter when building model messages). The user sees the build agent respond to a turn with no visible user message — perceived as "empty input." (createInternalAgentTextPart appends a \n<!-- SLIM_INTERNAL_INITIATOR --> marker, so the text is non-empty from the LLM's perspective.)
Session agent is durably corrupted. packages/opencode/src/session/prompt.ts:672-689 compares current.agent !== info.agent and, if they differ, calls sessions.setAgentModel({ agent: info.agent, ... }). A single agent-less promptAsync that resolves to build permanently rewrites the session's agent to build — every subsequent turn also routes to build until explicitly reset.
| File:line | Trigger | Body omits agent? |
Gate |
|---|---|---|---|
src/hooks/task-session-manager/continuation-evaluator.ts (promptAsync) |
session.idle / session.status(idle) on orchestrator session with incomplete todos when the opt-in beta backgroundJobs.continueOnIdle is true |
No (agent: 'orchestrator') |
continueOnIdle, process-local one-attempt gate (reserve→commit), hasInputWait, isCurrentContinuation, isFallbackInProgress, backgroundJobBoard.hasTerminalUnreconciled, malformed/active SDK short-circuits |
src/interview/service.ts:622 |
User submits interview dashboard input | Yes | sessionBusy lock, interview active state |
src/interview/service.ts:871 |
User submits interview chat | Yes | same |
src/interview/service.ts:933 |
User submits interview answer | Yes | same |
src/interview/service.ts:1007 |
User submits interview comment | Yes | same |
src/interview/service.ts:504 |
Interview URL notification | Yes (but noReply: true, non-synthetic text) |
none |
src/tools/smartfetch/secondary-model.ts:252 |
Smartfetch secondary model query | Yes | none |
src/hooks/foreground-fallback/index.ts:635-639 explicitly includes the agent:
const promptBody = {
parts: lastUser.parts,
model: ref,
...(agentName ? { agent: agentName } : {}),
};
This is the pattern every promptAsync caller in omos should follow.
hasInputWait gate in task-session-manager is not sufficientThe gate exists and works in the common case (see continuation-evaluator.ts and
task-session-manager/index.test.ts continuation cases). Notes:
Continuation is opt-in beta. backgroundJobs.continueOnIdle defaults
to false; set it to true to enable continuation SDK calls. Idle
reconciliation remains active either way. When enabled, a process-local
reserve/commit gate allows at most one promptAsync per session epoch
between real user messages.
Documented race window (when enabled). IDLE_RECONCILE_DELAY_MS = 2_000.
The idle-reconciliation comment admits late completions can still race the
window. If session.idle fires and the timer expires before
question.asked is delivered, and the SDK liveness reads resolve before the
wait is tracked, a nudge can still fire.
Input-wait is not the only trigger. The interview/skill path
(src/interview/service.ts) does not consult hasInputWait at all — it
injects on user dashboard actions, which can happen while the orchestrator is
mid-question.
Continuation path sets agent: 'orchestrator'. The historical missing-
agent misroute on the continuation nudge is fixed in
continuation-evaluator.ts. Remaining agent-less promptAsync call sites
are outside that path (interview/smartfetch below).
/preset fix#818's original bug: /preset used createInternalAgentTextPart() to trigger an LLM turn that was invisible in the TUI (synthetic: true). The fix moved /preset to pure TUI dialogs (src/tui-preset.ts uses only api.ui.dialog / DialogSelect / DialogPrompt / DialogConfirm — no promptAsync).
This bug: other hooks still use the same createInternalAgentTextPart + promptAsync pattern, and additionally omit the agent field, so the invisible turn routes to build instead of the orchestrator. Same shape: a synthetic part starting an invisible turn. Different symptom: build agent instead of orchestrator.
Continuation already passes agent: 'orchestrator'. Add the same to remaining
agent-less promptAsync bodies:
src/interview/service.ts (dashboard/chat/answer/comment injectors)This ensures interview injections always route to the orchestrator regardless of
opencode's default_agent resolution, eliminating the path to build.
hasInputWait (or an equivalent signal) before injecting in src/interview/service.ts. Do not inject while the orchestrator is waiting for user input.promptAsync, assert current.agent was not changed out from under the orchestrator; if it was, restore it via setAgentModel.default_agent application reliability on opencode v1.18.x. The plugin was built against @opencode-ai/sdk v1.4.3; the installed runtime is v1.18.3. The config hook's default_agent = 'orchestrator' mutation may not be applied reliably under this skew. (Note: #799 tracks the package upgrade.)IDLE_RECONCILE_DELAY_MS race for sessions that have a pending question.asked / permission.asked.src/hooks/task-session-manager/continuation-evaluator.ts, continuation-attempt-gate.ts, backgroundJobs.continueOnIdle in src/config/schema.tsagent field (skill flow): src/interview/service.ts:622, 871, 933, 1007src/hooks/foreground-fallback/index.ts:635-639default_agent only when absent: src/index.ts:546-551createInternalAgentTextPart produces synthetic: true: src/utils/internal-initiator.ts:9-21CONTINUATION_NUDGE is non-empty: src/hooks/task-session-manager/continuation-evaluator.tshasInputWait / continuation gates: continuation-evaluator.ts, input-wait-tracker.tsIDLE_RECONCILE_DELAY_MS race window: src/hooks/task-session-manager/index.ts (IDLE_RECONCILE_DELAY_MS)disableDefaultAgents preserves build and plan: src/cli/config-io.ts:564-600anomalyco/opencode @ dev)promptAsync HTTP handler (no busy/input-wait guard): packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts:311-329prompt HTTP handler (no guard): same file, :295-309prompt always starts a new turn: packages/opencode/src/session/prompt.ts:1052-1071input.agent ?? defaultInfo(): packages/opencode/src/session/prompt.ts:636-637defaultInfo() → default_agent or first visible primary: packages/opencode/src/agent/agent.ts:328-340build agent definition (default primary, first in registry): packages/opencode/src/agent/agent.ts:141-155setAgentModel overwrite on agent change: packages/opencode/src/session/prompt.ts:672-689packages/opencode/src/session/message-v2.ts:206ensureRunning (Idle = run now, no queue): packages/opencode/src/effect/runner.ts:115-138packages/opencode/src/session/run-state.ts:60-63assertNotBusy (NOT used by prompt/promptAsync): packages/opencode/src/session/run-state.ts:71-75default_agent doc: "Falls back to 'build' if not set or invalid": node_modules/@opencode-ai/sdk/dist/v2/gen/types.gen.d.ts:1255-1257SessionPromptAsyncData.body.agent? is optional: node_modules/@opencode-ai/sdk/dist/v2/gen/types.gen.d.ts:3241-3269build is a built-in agent: node_modules/@opencode-ai/sdk/dist/v2/gen/types.gen.d.ts:1273-1279MERGEABLE / CLEAN / CI green; amending widens its scope and may delay merge.)default_agent application (fix direction #3) as part of this work, or track it separately under #799?This report is diagnostic only. No code was changed. The fix awaits the user's decision on scope and PR strategy.