# Background Orchestration Background orchestration is the default orchestration model for oh-my-opencode-slim. It assumes native OpenCode background subagents are available and changes the orchestrator from a primary worker into a scheduler. The old model was: ```text orchestrator works directly → delegates when useful → waits for result ``` The default background-orchestration model is: ```text orchestrator plans → dispatches background specialists → monitors → reconciles → verifies ``` This is a clean rebuild, not a compatibility layer over the old blocking model. ## Inspecting unsuccessful children `task_status` with `diagnostic: true` returns a read-only, bounded, non-final excerpt for a tracked child that ended in error, cancelled, or stopped. Running children are refused (`result: true` is for completed children), and an owned but untracked handle returns ordinary status plus a notice instead of an excerpt. Unavailable or ambiguous evidence is reported, never invented: attribution is timestamp-based because most runs lack recorded message identities, tool inputs and successful tool outputs are excluded, and host compaction is omitted and labelled. The rendered response is capped at 6,144 UTF-8 bytes, including headers and truncation markers. A diagnostic read is evidence, not consumption or acceptance: it never reconciles or resumes the child. `last_activity_source` explains the activity timestamp (`observed-activity` may lag, `host-busy` is coarse, `launch-time` means no evidence beyond launch, `unknown` means none). See the [tool reference](tools.md#background-task-control) for the full contract. --- ## Runtime Requirement Background orchestration needs an OpenCode release with native background subagents and hook-driven completion, started with background subagents enabled: ```bash OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode ``` The tools, their v1/v2 differences, and the `task()`/`subagent()` vocabulary are listed in [Tools](tools.md#background-task-control). Without the environment variable the default background workflow is unavailable: configure it through the installer (see [Installation](installation.md)) or use the one-shot export above, and confirm the release with `opencode --version`. Setting `backgroundJobs.mode: "off"` (or the legacy `backgroundJobs.enabled: false`) turns this managed layer off entirely: the `task_*` control tools and `wait_for_user` are not registered, and no job board, retention, terminal gate, watchdog, wake scheduler, or startup recovery runs. Native `task`/`subagent` delegation and host completion delivery keep working exactly as OpenCode provides them, so council seat assembly and ordinary background delegation still function — only the OMOS bookkeeping and control surface disappear. Session presets are a separate subsystem (they bind delegated children to the frozen inference snapshot and back `/preset`), so they keep applying in every mode; they are not board state. See [Configuration](configuration.md). `backgroundJobs.mode: "native"` keeps the managed layer off but registers the two thin host-read controls, because the native delegation surface has no model-facing status or cancel: `task_status` reads a child session's host state plus its trailing result (the re-fetch channel after a lost completion notice), and `task_cancel` interrupts a stuck child through the host's own interruption while retaining it for continuation (the host owns cleanup and the completion notice; an idle interrupt is its no-op). Both verify `parentID` from a bounded `session.get`; neither touches a board, lease, or ledger. There is no automatic legacy fallback: missing background-subagent support is an environment or version problem, not an intentional v1 mode. --- ## Core Principle The orchestrator is not the default implementation worker. Its job is to: - understand the user request, - break work into dependent and independent units, - choose the right specialist for each unit, - schedule background work, - track task IDs and states, - avoid conflicting writes, - integrate specialist results, - run or route final verification, - communicate concise progress and outcomes to the user. Specialists do the work. The orchestrator manages the work. ### Children moved to worktrees Moving a child session to another directory does not move its task ownership. The delegating parent's plugin instance still observes that tracked child's lifecycle, so completion and revived-run notifications do not require polling `task_status`. The destination directory handles its own permissions and runtime profiles; unrelated sessions remain excluded from the parent's event handling. ### Unattributed sessions and restart scope A child observed before it can be attributed to a task launch is retained as a provisional placeholder. It remains resolvable by task ID or alias for `task_status`, but is not treated as delegated work by the plugin. A placeholder does not trigger stopped-job recovery or completed/error publication wakes for its parent, including after repeated executions: it is not yet known to be delegated work. This lets another plugin own a child session's notifications. Attribution through the existing task launch path promotes it to an ordinary listed task, including recovery and publication wakes. This distinction uses explicit provenance, not agent names or description text. Aliases and reusable-session history are process-local and do not survive process restarts as a reusable board. Post-restart recovery is partial and best-effort; it does not guarantee restoration of those aliases or the complete history. Two read-only channels work after a restart: a background launch's marker in the parent's history still resolves its alias when it is the final non-empty line of that tool result (tail-anchored only), and `task_status` reports a read-only observed state for an owned session the board no longer tracks — completed or uncertain running-or-incomplete from transcript evidence (unknown when the live status read fails), never claiming definite running — pointing to a `task_status` result read for completed text; it never re-registers or prompts the task. One recovery channel is explicit: `task_revive` on a raw session ID verifies the session against the host (existence and parent ownership) and re-adopts an untracked child owned by the calling parent, then continues it with the new prompt. Recovery verifies the original agent and latest delivered round before importing a retained terminal row. This cache-only import does not launch or notify. On V2, `session.execution.interrupted` with reason `shutdown` is suspension, not a terminal idle transition. OMO preserves the raw event without synthesizing idle/status events; the host retains execution and first-notification ownership. This does not restore process-local board entries or prove restart recovery. `task_cancel` still cannot abort a run launched before the restart — the board has no live handle to it (tracked in #1387). The production board numbers a parent's children only when the host's creation time shows that parent session was created while the plugin instance runs: the board saw every numbered alias it could have. Any other parent (created before a restart, or with no creation time) gives its new children their exact session ID as alias, and task creation still succeeds. This is deliberate: no cheap host read bounds the numbers issued before a restart. The live child count misses deleted children and a persisted high-water mark can miss a failed write, so either floor could hand an old `fix-2` to a new child while history still pairs `fix-2` with another session. Adopted and rehydrated host children keep their session ID as alias; a restored child keeps its historical alias only when history pairs that alias with it alone and no row of that parent holds it (never once the parent's readable history is cut: a v2 compaction or, on v1, more than 1,000 messages). None of them consumes a new number. A forked session counts as new, but the aliases in its copied history belong to the original parent: refer to the fork's new children by exact session ID. The continuation guidance changes the static orchestrator prompt and board wording. Existing sessions incur a one-time prompt-cache re-warm on their first request with this version; this is not a cache-neutral release. The new guidance remains deterministic and does not rewrite earlier messages. On v2 hosts with `session.prompt` and `session.context`, adoption queues the new instruction in the same session after shared recovery verifies parent ownership, agent evidence, and readable history. Host-proven aliases use the same path; unverified or ambiguous aliases remain refused. On-demand recovery uses the session's host outcome: a terminal round is imported, and a round without a terminal outcome (still running, or awaiting the host's restart resume) is refused except for an explicit queued revive. An assistant created after the host idle means the terminal does not cover the latest transcript activity, so the round stays pending. Startup recovery is unchanged and still classifies persisted transcript idles. An explicit queued revive can adopt identity before a previous round's idle is visible; that adoption is not terminal evidence, so recovery never imports the round from it. The host wakes an idle session or runs the input after the current execution; revival does not wait for idle or interrupt it. The plugin assigns a message ID before admission and only delivers the terminal answer after that exact input. An older execution's result, error, or idle event cannot complete the continuation. Missing input/answer evidence remains pending. Only one continuation is admitted locally at a time. An uncertain admission must not be retried: its lease stays held until acknowledgement or an attributable answer proves completion. Queued adoption does not arm a session-wide timeout abort, because that could kill the preceding execution. Deletion suppresses late delivery without a compensating interrupt. V1 adoption retains its live-status gate; existing tracked revival retains its previous idle-verification behavior. --- ## Execution Loop Every non-trivial request follows this loop: ```text Understand ↓ Plan dependency graph ↓ Dispatch independent specialists in background ↓ Track task IDs and ownership ↓ Continue only independent coordination work ↓ Wait for hook-driven completion ↓ Reconcile results and resolve conflicts ↓ Dispatch follow-up work if needed ↓ Verify ↓ Final response ``` The orchestrator should not act on assumptions from a still-running task. It can continue scheduling independent work, but dependent work waits for terminal task results. --- ## Scheduler Responsibilities ### 1. Build a dependency graph Before dispatching agents, the orchestrator identifies: - which questions must be answered before implementation, - which tasks can run in parallel, - which tasks must be sequential, - which files or subsystems each writer owns, - which outputs are needed for final verification. This does not need to be a long plan. It should be just enough structure to avoid wasted work and conflicting edits. ### 2. Dispatch background specialists Independent work should be launched with background tasks: ```text task( description="Search auth flow", subagent_type="explorer", background=true, prompt="Find the auth entry points, session storage, and login callback paths. Return file paths and a concise map. Do not edit files." ) ``` The orchestrator records the returned task ID and keeps working only on safe, independent coordination. ### 3. Track ownership The scheduler must prevent write conflicts. Rules: - Only one write-capable specialist owns a file at a time. - Do not run two `fixer` tasks against overlapping folders unless ownership is explicit. - UI work that touches shared components should not run beside implementation work that edits the same components. - Review tasks can run in parallel with read-only discovery, but not with edits they are supposed to review. ### 4. Wait, message, cancel, and revive Background tasks are not complete until the host injects their terminal result or a hook-driven completion marks them terminal. Use `task_status` to inspect a task and `task_status` with `result: true` to collect its result; a result read is a read, not a required step before every continuation. - `task_message` (v1 only) sends a non-interrupting, queue-only message (`noReply`, `resume: false`). It never wakes, steers, resumes, or interrupts the child, and acceptance does not mean the child read it. To steer a running child on v2, use the native delegation call (`subagent(sessionID, prompt)`). - `task_cancel` stops a generation and retains its session. Inspect and reconcile partial file changes before launching replacement work. - `task_revive` (v1 only) resumes a retained session after its state is verified safe, in the same child. v2 continues through the native delegation call. No path drops an explicit resume id to spawn another session. Terminal reconciliation proves lifecycle state, not that the output was used: verify that the relevant result was consumed before finalizing. Parent notifications are best-effort with a 10-second transport wait per attempt; a timeout can still duplicate a delivery, and acceptance is not an exactly-once guarantee. A brand-new `task` spawn is refused while the parent still owns an unreconciled terminal job with the same agent and an exactly matching objective; the refusal names the existing task ID and points at `task_status` with `result: true`. Once that result is retrieved, the same objective may be dispatched again. The orchestrator wake scheduler (v1 only; v2 creates none) may prompt an idle parent with incomplete todos after continuous idle time; it does not depend on the job board. After a plugin or OpenCode restart, persisted launch identities (task ID, parent, agent, objective, launch time, delivery marker) and, on v2, the host's own re-delivery of a background subagent outcome restore running and terminal children without replaying ones already delivered. Recovery is idempotent: board presence, generation fences, and the delivery marker prevent a second adoption or duplicate wake, and deletion tombstones prevent resurrecting a child deleted while the plugin was down. A missing or idle child is surfaced as `stopped, unreconciled` after a 5-second grace; timeouts stay unknown, never completion. The recovery and fencing rules are documented in [Background job state](opencode-v2-compatibility.md#background-job-state-rehydrate-probe-and-persistence), and the tools themselves in [Tools](tools.md#background-task-control). Specialist outputs are inputs, not final truth. The orchestrator reconciles them against each other and the original user goal. ### 5. Verify Verification remains orchestrator-owned and should be proportionate to the change. Use focused checks against the final state, broadening them only when risk or uncertainty warrants it. Oracle review is conditional: dispatch it for material semantic or architectural risk, unresolved uncertainty, or another high-cost decision—not automatically. The final response should only happen after relevant background work is terminal, reconciled, and supported by final-state evidence. --- ## Specialist Roles ### Explorer Read-only reconnaissance, codebase mapping, and external research (docs, version-specific API behavior, real-world examples). Usually the first background task for unfamiliar work. ### Fixer Bounded implementation worker. Receives a clear objective, file ownership, constraints, and validation expectations. ### Designer User-facing UI/UX implementation and review. Owns visual polish, responsive layout, interaction quality, and design consistency. ### Oracle Architecture, code review, simplification, risk analysis, and high-stakes debugging. Often used after implementation or before risky refactors. ### Council Multi-model decision support for critical trade-offs. It is not a worker pool; it is for judgment where disagreement is useful. ### Observer Visual/media analysis isolated from the orchestrator context. --- ## Direct Work Boundary Background orchestration removes the orchestrator-as-worker default. The orchestrator may directly: - ask clarifying questions, - read minimal context needed to route work, - create and update todos, - launch and monitor tasks, - synthesize results, - run final checks when that is cheaper than delegating. The orchestrator should delegate: - broad code search, - unfamiliar library research, - implementation, - test creation or updates, - UI polish, - architecture review, - visual/media analysis. This keeps the main context focused on coordination instead of filling it with worker detail. --- ## Task Prompt Contract Every delegated task should be self-contained. Include: - objective, - constraints, - relevant files or search scope, - ownership boundaries, - expected output format, - whether edits are allowed, - validation to run or report, - what not to do. ### Task-fit rejections If a task is outside a specialist's role, it must not attempt partial work. It returns a brief reason to the orchestrator. The orchestrator treats that reason as routing input to reroute or clarify the task and must not retry the unchanged task with the same specialist. Good background task prompt: ```text Investigate src/hooks/task-session-manager for assumptions that a task tool result means the child task has finished. Do not edit files. Return: 1. exact files/functions involved, 2. which assumptions break with background tasks, 3. recommended code changes, 4. tests that should be added. ``` Bad background task prompt: ```text Look into background tasks. ``` --- ## State The Orchestrator Must Track The runtime treats background tasks as a small job board (machine state only — it is never rendered into a prompt): | Field | Meaning | |-------|---------| | task ID | Native OpenCode background task/session ID | | specialist | Agent type assigned | | objective | What the task is responsible for | | state | running; stopped (runtime ended without terminal task output); completed, error, or cancelled (explicit terminal task output); reconciled (terminal result consumed) | | ownership | Files/folders/subsystems the task may edit | | dependencies | Tasks that must complete first | | result | Final task output once terminal | | status certainty | `status uncertain` when the live status map is malformed or unavailable; it never implies completion | Cancelled, errored, and stopped sessions can remain retained for a later `task_revive`. They may be revived immediately once their retained state has been verified safe. Acknowledgement controls parent consumption and reusable-pool display, not same-session revival. Stopped sessions stay out of the ordinary `task()` reuse pool because that generation has no terminal result; after ack they are retained for `task_revive`. The current todo list can represent user-visible work, but task IDs and file ownership need to be explicit in the orchestrator's working context. --- ## Runtime Integration The plugin is aware that a `task` return can mean "background job launched" rather than "work complete". It tracks running task IDs, updates aliases from task results, and keeps multiplexer panes attached while the parent orchestrator continues scheduling. The job board is plugin-internal state: it is never rendered into a prompt (issue #1647); `task_status` is the read surface. ### Orchestrator wake scheduler When an orchestrator parent stays continuously idle, the plugin may send a periodic internal wake prompt so incomplete TODOs are not abandoned. This is **enabled by default** with a **5-minute** interval: ```jsonc { "backgroundJobs": { "orchestratorWake": { "enabled": true, "intervalMs": 300000, "mode": "auto" } } } ``` `intervalMs` must be an integer from `60000` to `2147483647`. `0` is invalid. `mode` selects the v1 wake condition: `"auto"` (default) always uses todo-gating; explicit `"todo"` falls back to children where the todo API is absent, and `"children"` selects children-driven mode. Set `enabled: false` to disable wakes while keeping idle reconciliation and background-job orchestration. This whole section is **v1-only**: on OpenCode v2 hosts the plugin creates no wake scheduler at all — the native background notifier owns terminal delivery (council 2026-10-23) — and every `backgroundJobs.orchestratorWake` key warns as ignored there. Behavior: - Per-session recursive `setTimeout(...).unref()` after continuous parent-idle time (never a global interval). - Only sessions known as the parent `orchestrator` via session metadata. - Host client APIs are authoritative (`session.get`, `todo`, `children`, `status`, `promptAsync` with the nested directory request shape). The local job board is never read or used as a gate. - Wake requires valid host response shapes, parent currently idle, and at least one TODO with status `pending` or `in_progress`. Unknown/malformed status fails closed. **Active children do not suppress a wake.** - Suppress/clear on question/permission input waits, `wait_for_user`, foreground fallback, session busy, session deletion, external user messages, and server disposal. - `session.time.archived` is authoritative when available. Archived sessions do not receive periodic or stopped-job-recovery wakes; archive updates cancel timers and stale evaluations, while an unarchive permits future lifecycle activity. v2 hosts without `session.get()` use observed session updates. - One in-flight evaluation/wake per session. Status/waits/generation are rechecked immediately before `promptAsync`. Cooldown/reservation is recorded before the call so a failed `promptAsync` cannot storm retries. - Default-on safety: the scheduler evaluates a bounded host-progress fingerprint (TODO statuses plus child status/update evidence) to decide whether to keep waking. After **two** successful wakes with an unchanged fingerprint, further wakes stop for that continuous idle spell. A real external user message or host-observed progress re-arms the cap. Busy caused by the wake itself does **not** rearm the cap; unrelated busy/error lifecycle events do. The wake prompt text is static and does **not** include a fingerprint or snapshot. The no-progress caps live in a process-global wake gate that the plugin's dispose hook clears on instance teardown, so a recreated plugin generation (for example after `opencode reload` on v2 hosts) starts with fresh caps instead of inheriting the previous generation's. - Static wake text (internal initiator part via `promptAsync` only — no message transform injection or history rewrite): ```text Finish any incomplete TODOs. Await running agents; if one appears stuck, assess it and cancel/respawn only when justified. Do not respond to this reminder. ``` If the same delta-less wake body was already delivered in the session, the repeat sends a strictly shorter, non-identical marker instead (`Repeat wake #N:`) so identical reminder bytes are not persisted repeatedly; delta-bearing wakes and child-input asks always carry the full template. The scheduler does **not** perform automatic cancellation and does not rely on the local job board. When no incomplete TODOs remain, it ends the current idle spell and stops polling until new activity. **v2 hosts: no scheduler.** On OpenCode v2 the plugin creates no wake scheduler at all (council 2026-10-23): the native background notifier owns terminal delivery, restart recovery re-delivers anything lost, and the parent discovers open child asks via `task_status` pull. #### v2 structural defaults On v2 the host's native background notifier already delivers each run's first terminal result (completed, error, cancelled): an idle parent gets a new turn, a busy parent gets a steer. Two plugin channels would duplicate that, so they are structurally off, not configurable: - the job board is never injected into a prompt (true on v1 too; the render was removed in #1647, and `backgroundJobs.boardInjection` no longer exists); - no wake scheduler is created: every `backgroundJobs.orchestratorWake` key is v1-only, and an explicit value is ignored with a load-time warning. On v1, event-driven wakes still run (stopped-job recovery, later terminal publications of the same run, child-input asks), with SDK-error retries on the idle timer; failed publication retries survive parent busy/idle transitions. Later-result wakes carry `task_status { sessionID, result: true }` pointers in a tagged volatile message, so the parent never has to guess from delegation history. Child-input wakes wait 250 ms, and per-kind policy applies: by default only question asks wake the parent (#1504). Permission asks that stay parked are visible through `task_status`; set `backgroundJobs.childInputWakeKinds.permission = true` or rely on `wallClockTimeoutMs` when no UI or responder will answer them. On v2 no child-input wake is sent: asks are pull-only via `task_status`. Terminal retirement is observation-based on both hosts: the plugin retires the exact run (generation- and revision-fenced) when it observes the delivery that reported it — the native `` delivery on v2, its own tagged `` push on v1, or the revived-run notification. A completed, error, or cancelled record also retires once the parent demonstrably moves past the terminal report. Stopped jobs are not retired that way (a stop has no native notification): they stay stopped and unreconciled, the stopped-job recovery wake reports them, and `task_revive` acknowledges them. Reopen corrections tell the parent once not to rely on the earlier result. For external manual work, the orchestrator gives concrete steps and then calls `wait_for_user` as its final tool action; the wait is cleared only by a distinct real external user message or genuine session deletion. Synthetic messages, replayed user messages, fallback teardown, session errors, and idle/busy events do not clear it. Immediate choices and clarifications use the `question` tool. `wait_for_user` is v1-only: on v2 the orchestrator always uses the native `question` tool. ### No Job Board In The Prompt The job board is never rendered into a prompt — on any host. It stays a machine-side projection: dedupe by objective, alias authority, stall tracking, the terminal gate, session adoption, and generations keep working, but no prompt surface lists them. There is no switch to turn the render back on; the `backgroundJobs.boardInjection` key was removed with the render. Terminal facts travel as one-shot pushes instead: - The host's native notifier delivers a run's FIRST terminal publication (completed, error) to the parent on both host versions. The plugin publishes nothing beside it. - A later publication of the same run (rev>1: a child that self-continued, or a direct prompt to the child session) has no native notifier. On v1 the plugin pushes ONE self-contained `` delta (alias, task, state, result) through the same bounded delta queue the stopped-job recovery uses, so the result survives a busy parent. The push honors the per-parent publication spacing (`publicationWakeMinIntervalMs`) by deferring, not dropping: results inside the window stay queued and one wake delivers them together when it elapses. The wake that carries it is a bare resume pointer: the delta holds the facts. On v2 the plugin sends no wake for it: a later run relaunched through the `subagent` tool is delivered natively, and any other later result is pulled with `task_status`. - Overflowed pushes keep a durable pointer to `task_status` instead of listing the coalesced facts inline. ### Runtime status and session eviction Liveness is read from the host, never inferred: a session that `session.status` reports as busy or retrying is live, and an unreadable or timed-out read stays **unknown** — never "finished". Startup listing, session-info, and transcript waits are each bounded at 5 seconds; a stalled list falls back to persisted identities and never blocks later children. `pruneEvictedSessions` (v1 only — v2 exposes no live status map, so every prune is skipped there) may delete the host child of an evicted record. It only considers background children this plugin launched, and re-checks immediately before deleting: a bounded `session.get` must report the same `parentID`, a bounded `session.status` must report idle, and the board must not track the task again. Parked child-input waits, adopted or restored sessions, and explicit `clearParent`/`drop` evictions are never pruned. The delete itself has the same deadline, and every read plus the delete is bounded, so a revive waiting on an in-flight prune always settles. Tombstones are consumed at most once and fence startup adoption. ### Admission and supervision Three opt-in knobs shape background execution; keys, defaults, and the exact rules live in [Configuration](configuration.md#background-job-management). - **Concurrency** caps running native background tasks (`0` = unlimited; the most specific cap wins: model > provider > default). Nested managed tasks are exempt, and admission has no timeout, so pair it with the watchdog. - **Same-provider conversion** (`sameProviderPolicy`) turns an explicit background call into the foreground path when parent and child share the configured local backend, avoiding KV-cache thrash. - **Wall-clock watchdog** (`wallClockTimeoutMs`) aborts a supervised child at the deadline and lets the host publish the real terminal. There is no confirmation timeout (`abortGraceMs` was removed). ## Example Flow User asks: ```text Make background subagents first-class in this plugin. ``` The orchestrator should do something like: 1. Create todos for discovery, design, implementation, docs, tests, and verification. 2. Launch Explorer in background to map task-session hooks and task lifecycle. 3. Launch Oracle in background only if the change has material semantic or architectural risk or unresolved uncertainty. 4. Continue by preparing the dependency graph and file ownership plan. 5. Wait for the launched specialists via hook-driven completion. 6. Dispatch Fixer to implement prompt/config/hook changes with clear ownership. 7. Dispatch a second Fixer for tests if file ownership is separate. 8. Wait for implementation results. 9. Reconcile the implementation and dispatch Oracle only if remaining risk or uncertainty makes independent review worthwhile. 10. Run proportionate checks against the final state. 11. Report final state. At no point does the orchestrator become the main implementer. --- ## Success Criteria Background orchestration is working when: - the orchestrator launches independent specialists in background by default, - task IDs are tracked until terminal state, - dependent work waits for real task results, - file ownership prevents concurrent write conflicts, - final responses only happen after reconciliation and verification, - users see faster progress on multi-step work, - the orchestrator context stays focused on decisions instead of worker detail. Background orchestration is not just "parallel agents." It is a scheduler-centered operating model for OpenCode's native background subagents.