Skip to content

[Bug]: wait_for_user loops repeatedly while background tasks are unreconciled #924

Description

@major

What happened, and what did you expect?

What happened: While the orchestrator had 2+ background task(..., background: true) lanes running (via @explorer/@librarian) and had called wait_for_user to pause for them, it got re-invoked and called wait_for_user again roughly every 4-9 seconds, repeatedly, for 30-40+ seconds per background job — 13 separate LLM turns in one incident — even though nothing new had happened and no external user message arrived in most of those cycles. Each of these is a full, separately-billed LLM completion.

Expected: Once wait_for_user is called, the orchestrator should stay quiesced until either (a) a background job actually needs to report a new completion the model hasn't seen yet, or (b) a real external user message arrives — not be re-invoked repeatedly for the same already-delivered completion.

Root cause (from plugin logs + session DB)

Reconstructed from ~/.local/share/opencode/log/oh-my-opencode-slim.*.log for the affected session. One representative burst:

19:39:01.501  job A (background task) completes → marked terminalUnreconciled:true
19:39:01.502  "terminal jobs injected for reconciliation" [job A] → orchestrator turn → calls wait_for_user
19:39:09.568  "terminal jobs injected for reconciliation" [job A] → orchestrator turn → calls wait_for_user  (same job, again)
19:39:14.587  "terminal jobs injected for reconciliation" [job A] → ... again
19:39:18.577  "terminal jobs injected for reconciliation" [job A] → ... again
19:39:25.490  "terminal jobs injected for reconciliation" [job A] → ... again
19:39:31.377  "terminal jobs injected for reconciliation" [job A] → ... again
19:39:35.xxx  "terminal jobs injected for reconciliation" [job A] → ... again
19:39:38.589  session finally goes idle → job A actually reconciled, reminder cleared

This is a feedback loop across three files in src/hooks/task-session-manager/:

  1. board-injection.ts (injectCheckpointBoard / rememberInjectedTerminalJobs, called from the experimental.chat.messages.transform hook on every LLM request) re-attaches the "Background Job Board" reminder to every request as long as any job is terminalUnreconciled. It doesn't track whether the orchestrator has already been shown this specific still-unreconciled completion in a prior step.
  2. idle-reconciliation.ts only clears terminalUnreconciled (via reconcileInjectedTerminalJobs) inside a timer that's scheduled on a genuine session.idle event.
  3. event-router.ts's busy handler unconditionally calls idleReconciler.clearIdleTimers(sessionId) on every busy status pulse — and the logs show the session stayed continuously busy → busy → busy … with no idle event logged in between, for the entire 30-40s stretch.

Net effect: the completed job's reminder keeps getting reinjected on every step. The orchestrator model (reasonably, given what it's shown each time) reacts by calling wait_for_user again. That reaction is itself another busy pulse, which cancels the only timer that would have cleared the reminder. The loop only breaks once two busy pulses happen to land further apart than IDLE_RECONCILE_DELAY_MS (2000ms in index.ts), letting a queued idle-reconcile timer survive long enough to actually fire.

This isn't the model malfunctioning — it's being shown the same "you have an unreconciled completed background task" state repeatedly, with no way to have that state cleared before its next turn.

Suggested fix direction

Track per-job "already surfaced to the model at least once" state (e.g. in terminalJobsInjectedByParent, which already exists) and skip re-injecting/re-triggering a continuation for a job that's been injected but not yet reconciled — i.e., inject once, then wait for genuine idle or new user input, rather than re-injecting on every subsequent request. Alternatively, reconcile a job immediately at injection time instead of deferring reconciliation to an idle timer that competes with the busy pulses caused by the injection itself.

Steps to reproduce

  1. From an orchestrator session, dispatch 2 background tasks: task(..., background: true) for two different subagents (e.g. one explorer, one librarian) doing multi-step work (multiple internal tool calls each, so they take at least 20-30s and don't finish at the same instant).
  2. Call wait_for_user to pause for both.
  3. Watch ~/.local/share/opencode/log/oh-my-opencode-slim.*.log for the session: once the first background task completes, observe repeated "terminal jobs injected for reconciliation" log lines for the same task ID every few seconds, each accompanied by another orchestrator turn/wait_for_user call, until the session finally reaches a clean idle gap.

oh-my-opencode.json

Default config, no custom idleReconcileDelayMs or task-session-manager overrides.

OpenCode version

1.18.10

oh-my-opencode-slim version

2.2.8

Operating system

Linux (Fedora-based)

Logs, screenshots, or extra context

Relevant source:

  • src/hooks/task-session-manager/board-injection.ts (injectCheckpointBoard, rememberInjectedTerminalJobs)
  • src/hooks/task-session-manager/idle-reconciliation.ts (scheduleIdleReconciliation)
  • src/hooks/task-session-manager/event-router.ts (busy handler clearing idle timers unconditionally on every busy pulse)
  • src/tools/wait-for-user.ts (the tool itself; its beginUserWait gate correctly blocks new idle-reconcile scheduling, but doesn't stop an in-flight injection/continuation cycle already underway)

Log excerpt (session/task identifiers redacted):

[task-session-manager] background job status updated {"taskID":"<redacted>","state":"completed","terminalUnreconciled":true}
[task-session-manager] processed injected background completion {"taskID":"<redacted>","state":"completed"}
[task-session-manager] terminal jobs injected for reconciliation {"parentSessionID":"<redacted>","taskIDs":["<redacted>"]}
[task-session-manager] busy/status busy observed {"sessionID":"<redacted>","managesSession":true}
... (repeats ~6x over 30s for the same taskID, no idle event logged in between) ...
[task-session-manager] idle/status idle observed {"sessionID":"<redacted>","terminalJobsPending":0}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions