Prerequisites
Bug Description
When using opencode run in CLI mode, the process exits prematurely if the main agent dispatches background subtasks (run_in_background=true) but has no incomplete todos. The main session goes idle, pollForCompletion() detects all completion conditions are met, and the process exits — even though background tasks are still running.
PR #217 (commit 8d9b68d8) attempted to fix this by checking for running background tasks in the todo-continuation-enforcer, but the fix has two gaps:
Root Cause Analysis
Gap 1: todo-continuation-enforcer only injects continuations when todos exist
In src/hooks/todo-continuation-enforcer/idle-event.ts, the handleSessionIdle() function checks for running background tasks (line 73-79), but this check is bypassed when there are no todos (line 110-114):
if (!todos || todos.length === 0) {
return // ← returns early, never injects continuation to keep session alive
}
This means: if the main agent dispatches background tasks without creating todos (a common pattern), the enforcer does nothing and the session goes idle.
Gap 2: Background task status check ignores "pending" tasks
The existing BG task check only looks for status === "running":
backgroundManager.getTasksByParentSession(sessionID)
.some((task) => task.status === "running")
Tasks queued for concurrency have status === "pending" — they have no session created yet, making them invisible to both the enforcer and areAllChildrenIdle().
Gap 3: checkCompletionConditions() has no direct BackgroundManager integration
src/cli/run/completion.ts relies solely on session.children() + session.status() to detect child activity. This can't see:
- Tasks in "pending" state (no session created yet)
- Tasks whose sessions have momentarily idle status
- Tasks whose sessions are missing from the status response (treated as idle per commit 0d888df)
Steps to Reproduce
- Run
opencode run with a prompt that triggers background subtask dispatch
- The main agent dispatches background task(s) with run_in_background=true
- The main agent's turn ends (session goes idle) before background tasks complete
- The CLI process exits prematurely with exit code 0
- Background tasks are orphaned/aborted during shutdown
Expected Behavior
opencode run should wait until ALL background tasks (including pending/queued ones) complete before exiting. The main agent should receive completion notifications and provide a final summary.
Actual Behavior
The process exits as soon as the main session goes idle, regardless of background task state. OPENCODE_BG_SHUTDOWN_WAIT=1 only delays the abort during shutdown — it doesn't prevent pollForCompletion() from returning prematurely.
Doctor Output
oh-my-opencode version: 3.17.3
OpenCode: 1.4.5
OS: Linux
Error Logs
Configuration
Additional Context
Related
Proposed Fix
Use the existing run-continuation-state marker system (same as boulder/ralph-loop) to signal active background tasks across the process boundary:
- BackgroundManager writes a "background-task" continuation marker on launch() and clears it when all tasks complete/cancel/fail
- CLI checkCompletionConditions() reads this marker and blocks exit while active
- Fix BG task status checks to include "pending" alongside "running"
Operating System
Linux
OpenCode Version
1.4.5
Prerequisites
Bug Description
When using
opencode runin CLI mode, the process exits prematurely if the main agent dispatches background subtasks (run_in_background=true) but has no incomplete todos. The main session goes idle,pollForCompletion()detects all completion conditions are met, and the process exits — even though background tasks are still running.PR #217 (commit
8d9b68d8) attempted to fix this by checking for running background tasks in thetodo-continuation-enforcer, but the fix has two gaps:Root Cause Analysis
Gap 1: todo-continuation-enforcer only injects continuations when todos exist
In
src/hooks/todo-continuation-enforcer/idle-event.ts, thehandleSessionIdle()function checks for running background tasks (line 73-79), but this check is bypassed when there are no todos (line 110-114):This means: if the main agent dispatches background tasks without creating todos (a common pattern), the enforcer does nothing and the session goes idle.
Gap 2: Background task status check ignores "pending" tasks
The existing BG task check only looks for status === "running":
backgroundManager.getTasksByParentSession(sessionID)
.some((task) => task.status === "running")
Tasks queued for concurrency have status === "pending" — they have no session created yet, making them invisible to both the enforcer and areAllChildrenIdle().
Gap 3: checkCompletionConditions() has no direct BackgroundManager integration
src/cli/run/completion.ts relies solely on session.children() + session.status() to detect child activity. This can't see:
Steps to Reproduce
opencode runwith a prompt that triggers background subtask dispatchExpected Behavior
opencode runshould wait until ALL background tasks (including pending/queued ones) complete before exiting. The main agent should receive completion notifications and provide a final summary.Actual Behavior
The process exits as soon as the main session goes idle, regardless of background task state. OPENCODE_BG_SHUTDOWN_WAIT=1 only delays the abort during shutdown — it doesn't prevent pollForCompletion() from returning prematurely.
Doctor Output
Error Logs
Configuration
Additional Context
Related
Proposed Fix
Use the existing run-continuation-state marker system (same as boulder/ralph-loop) to signal active background tasks across the process boundary:
Operating System
Linux
OpenCode Version
1.4.5