You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I searched existing issues and found related but not exact matches.
I am using the latest version of oh-my-openagent (4.5.1).
I read the documentation (docs/guide/orchestration.md) and couldn't find the answer.
Bug Description
A sync subagent delegation returns to its parent before the background descendants it launched have quiesced.
Case: main delegates task(subagent_type="plan", run_in_background=false). plan runs its mandatory protocol and launches explore / librarian via call_omo_agent(..., run_in_background=true). If plan reaches a terminal turn before those finish, the parent's sync poller declares plan complete and returns immediately — so the parent gets a plan that never synthesized the explore/librarian findings.
The notification path is not the problem: parent-wake (parent-wake-notifier.ts, recently refined) does deliver completion wakes to the plan session. It is an ordering bug — the main-side sync poller returns the premature result before parent-wake can resume plan.
This is the runtime layer of #2124 (addressed only at the prompt layer by #2328, which covered Sisyphus but not plan), and the same quiescence gap as #4163.
Steps to Reproduce
Delegate task(subagent_type="plan", run_in_background=false) from a main session.
Let plan launch explore / librarian via call_omo_agent(..., run_in_background=true).
Let plan reach a terminal turn before they finish.
Parent receives the plan result while explore/librarian are still running; their findings are not synthesized.
Expected Behavior
While a sync subagent owns direct child background tasks in pending/running, the parent delegation should stay non-quiescent until they finish and the subagent emits a terminal turn after collecting them. This should apply to all sync subagents (not just plan), and stay bounded since descendants carry their own timeouts.
Actual Behavior
The sync poller decides completion from the subagent's terminal assistant finish alone, never consulting the BackgroundManager for in-flight descendants, so it returns early.
Two Code Paths (why notifications work but the parent returns early)
Background completion is handled by two independent paths that never meet:
Path
Trigger
Knows about background descendants?
Effect on the parent delegation
Parent-wake (notification)
A background task reaches a terminal state, gated on validateSessionHasOutput (real response arrived) + no incomplete todos
Yes — driven by task completion
Async-injects a system-reminder into the parent session so it could resume. Recently refined (defer during active turns, unblock stale wakes, align activity windows).
Sync completion (poller)
The polled subagent emits a terminal assistant turn
No — purely session-local, no output/descendant cross-check
Breaks the poll loop and returns the subagent result to its caller
The asymmetry is the core issue: the background path waits for real output before notifying, while the sync path returns on terminal finish alone. Because the two are decoupled, the sync poller fires first on plan's initial terminal turn and returns the premature result; the parent-wake that would have resumed plan arrives afterward, against a delegation that has already concluded. Improving parent-wake therefore cannot fix this — the completion decision has to consult descendant state.
Root Cause Analysis
The sync completion check is session-local and is never given a BackgroundManager reference; the parent-wake path is async and not consulted by the completion decision.
Evidence:
sync-session-poller.ts — isSessionComplete() returns true from the session's own terminal finish / pending tool parts only; the poll loop breaks immediately, with no descendant check.
sync-task.ts — executeSyncTask() holds executorCtx.manager but never passes it to pollSyncSession(...); after the poll it fetches the result and returns with no continuation.
completion-poller.ts — waitForCompletion() has the same gap on the call_omo_agent sync path (polls only target session status/messages).
parent-wake-notifier.ts — delivers wakes asynchronously and never checks subagentSessions / syncSubagentSessions, so it cannot hold the sync poller open.
manager.ts — the needed predicate already exists: getTasksByParentSession() / getAllDescendantTasks(), and updateBackgroundTaskMarker() already treats running || pending as active; the background path's own completion is gated by validateSessionHasOutput().
Sync completion check (deterministic): make it descendant-aware. sync-session-poller.ts should not treat the session as complete while its direct child background tasks are pending/running (resetting the inactivity timer during that wait); sync-task.ts should pass executorCtx.manager to the poller; apply the same to completion-poller.ts for the call_omo_agent sync path. Bound the wait and limit it to direct children (not a recursive descendant block, per [Bug] todo-continuation can resume parent session before descendant background tasks finish #4163).
The runtime gate keeps the parent from returning early (giving parent-wake time to resume plan); the prompt ensures the resumed plan actually collects and synthesizes. Parent-wake itself needs no change.
Prerequisites
Bug Description
A sync subagent delegation returns to its parent before the background descendants it launched have quiesced.
Case: main delegates
task(subagent_type="plan", run_in_background=false).planruns its mandatory protocol and launchesexplore/librarianviacall_omo_agent(..., run_in_background=true). Ifplanreaches a terminal turn before those finish, the parent's sync poller declaresplancomplete and returns immediately — so the parent gets a plan that never synthesized the explore/librarian findings.The notification path is not the problem: parent-wake (
parent-wake-notifier.ts, recently refined) does deliver completion wakes to theplansession. It is an ordering bug — the main-side sync poller returns the premature result before parent-wake can resumeplan.This is the runtime layer of #2124 (addressed only at the prompt layer by #2328, which covered Sisyphus but not
plan), and the same quiescence gap as #4163.Steps to Reproduce
task(subagent_type="plan", run_in_background=false)from a main session.planlaunchexplore/librarianviacall_omo_agent(..., run_in_background=true).planreach a terminal turn before they finish.Expected Behavior
While a sync subagent owns direct child background tasks in
pending/running, the parent delegation should stay non-quiescent until they finish and the subagent emits a terminal turn after collecting them. This should apply to all sync subagents (not justplan), and stay bounded since descendants carry their own timeouts.Actual Behavior
The sync poller decides completion from the subagent's terminal assistant finish alone, never consulting the BackgroundManager for in-flight descendants, so it returns early.
Two Code Paths (why notifications work but the parent returns early)
Background completion is handled by two independent paths that never meet:
validateSessionHasOutput(real response arrived) + no incomplete todosThe asymmetry is the core issue: the background path waits for real output before notifying, while the sync path returns on terminal finish alone. Because the two are decoupled, the sync poller fires first on
plan's initial terminal turn and returns the premature result; the parent-wake that would have resumedplanarrives afterward, against a delegation that has already concluded. Improving parent-wake therefore cannot fix this — the completion decision has to consult descendant state.Root Cause Analysis
The sync completion check is session-local and is never given a BackgroundManager reference; the parent-wake path is async and not consulted by the completion decision.
Evidence:
sync-session-poller.ts—isSessionComplete()returns true from the session's own terminal finish / pending tool parts only; the poll loop breaks immediately, with no descendant check.sync-task.ts—executeSyncTask()holdsexecutorCtx.managerbut never passes it topollSyncSession(...); after the poll it fetches the result and returns with no continuation.completion-poller.ts—waitForCompletion()has the same gap on thecall_omo_agentsync path (polls only target session status/messages).parent-wake-notifier.ts— delivers wakes asynchronously and never checkssubagentSessions/syncSubagentSessions, so it cannot hold the sync poller open.manager.ts— the needed predicate already exists:getTasksByParentSession()/getAllDescendantTasks(), andupdateBackgroundTaskMarker()already treatsrunning || pendingas active; the background path's own completion is gated byvalidateSessionHasOutput().constants.ts—PLAN_AGENT_SYSTEM_PREPEND_STATIC_BEFORE_SKILLStellsplanto launch the agents but not to wait for and collect their results (unlike Sisyphus after fix(agents): prevent orchestrator from rushing ahead of background agents #2328).Suggested Fix Surface
Two layers, mirroring the runtime/prompt split:
sync-session-poller.tsshould not treat the session as complete while its direct child background tasks arepending/running(resetting the inactivity timer during that wait);sync-task.tsshould passexecutorCtx.managerto the poller; apply the same tocompletion-poller.tsfor thecall_omo_agentsync path. Bound the wait and limit it to direct children (not a recursive descendant block, per [Bug] todo-continuation can resume parent session before descendant background tasks finish #4163).planprompt (reinforcement): instructplanto wait for and collect explore/librarian results viabackground_outputbefore producing the plan, as fix(agents): prevent orchestrator from rushing ahead of background agents #2328 did for Sisyphus.The runtime gate keeps the parent from returning early (giving parent-wake time to resume
plan); the prompt ensures the resumedplanactually collects and synthesizes. Parent-wake itself needs no change.Related Issues
todo-continuation-enforcer.call_omo_agentwaiter (the non-idle completion paths that skip the output gate).plan.Additional Context
Based on behavior directly observed while using omo, combined with source analysis of the
devbranch.