The OpenCode TUI renders background subagent session entries using
`props.metadata.sessionId` as the navigation target. When the wait-loop
in delegate-task and background-task tools exits before the session is
assigned, but the session is created moments later (before metadata
publish), the published metadata had `sessionId: undefined`, leaving
the TUI entry stuck spinning with no clickable target.
Add a single late-fallback `manager.getTask(task.id)?.sessionId` check
between the wait-loop exit and metadata publish in both paths. This
closes the narrow race window that produced the symptom in #4252.
Regression test: `late-session-id-capture.test.ts` mocks the exact race
(launch returns no sessionId; getTask returns it after the wait loop).
Verified:
- npm run build: PASS
- npm test: exit code 0 (one unrelated pre-existing failure in
sisyphus-task > browserProvider propagation re: agent-browser skill)
default-mode (system-transform):
- e5463e2db introduced auto-activation of ultrawork+ralph-loop, and
dc2e082ac then skipped the ultrawork system prompt whenever ralph_loop
was also enabled. Net effect: the keyword-detector still showed
'Default ultrawork mode enabled' to the user, but the first turn had
none of the ultrawork behavior. Loop continuation kept the ultrawork
prefix, so the contract was honored only on later iterations.
- Drop the skip so the initial turn matches what the toast advertises.
New matrix test pins all four (ultrawork, ralph_loop) combinations.
multimodal-looker:
- Prompt claimed 'read' and 'call_omo_agent' were available, but the
look_at invocation runtime explicitly disables both via READ_ENABLED
and createAgentToolAllowlist([]). Small VL models trusted the prompt
and looped on rejected tool calls (#4116).
- Rewrite the agent prompt to describe direct-attachment analysis and
forbid tool/agent calls. Add a consistency test that extracts the
prompt's 'available tools' claim and compares it against the
configured allowlist.
delegate-task (skill-resolver):
- 088693697 filtered per-agent restricted skills at the skill tool and
builtin agent prompt layers, but delegate-task itself happily injected
whatever skill name a caller passed. A target agent could be force-fed
a skill marked agent: oracle just by listing it in load_skills.
- Thread the target agent through resolveSkills and silently filter
skills whose definition.agent does not include it. Public skills with
no restriction are unaffected. Regression test pins the bypass.
- prometheus-prompt.test.ts: close missing }) on the OpenSpec expanded
commands describe block (introduced by d66b6bcbf, parse error).
- agent-sort-shim/agent-config-integration/continuation-injection/
unstable-agent-babysitter/subagent-resolver/sync-executor/
resolve-caller-team-lead tests: expect 'Sisyphus - ultraworker'
(lowercase) to match production after cd39f8858, which lowercased the
display name to dodge a TUI ZWSP rendering glitch. Legacy uppercase
inputs that exercise the normalization path are preserved.
- sync-executor.ts + resolve-caller-team-lead.ts: route legacy display
name inputs through normalizeAgentForPrompt so prompt agent names and
caller team lead lookups produce the canonical lowercase form.
Maintainer feedback (#4071 review): the original guard rejected
sisyphus and atlas as subagent targets even from team-mode where
resolveMember() intentionally calls resolveSubagentExecution with
allowPrimaryAgentDelegation: true. Per AGENT_ELIGIBILITY_REGISTRY
(src/features/team-mode/types.ts), only prometheus is hard-reject;
sisyphus and atlas are explicitly verdict: 'eligible' for team
membership.
Shrink COORDINATOR_AGENT_NAMES to ['prometheus'] so the guard
aligns with the registry's authoritative classification, document
the scoping rule in a comment, and add regression tests covering:
- sisyphus is NOT blocked by the coordinator guard (registry eligible)
- atlas is NOT blocked by the coordinator guard (registry eligible)
- prometheus IS blocked even when allowPrimaryAgentDelegation: true
(registry hard-reject is authoritative)
Fixes the 5 zauc-mocks resolver tests that were locking in the
wrong rejection set (including 'allows delegating to a primary
agent when allowPrimaryAgentDelegation is enabled'). The one test
asserting the literal primary-agent error string for Prometheus
display-name was loosened to a regex that accepts either guard's
message, since prometheus is now caught by the coordinator path
which fires before the primary-agent lookup.
Agents could select coordinator/meta agents (Prometheus, Atlas,
Sisyphus/Ultraworker) as subagent targets via task() / delegation,
producing duplicate orchestration loops and conflicting team state.
This is the inverse of #3987 / #4065 — symmetric guard on the
delegation TARGET side, using the same AGENT_ELIGIBILITY_REGISTRY
classification.
Add a runtime guard at the delegation entry point that rejects
task() calls whose subagent_type resolves to an agent marked as
hard-reject / coordinator-only in the eligibility registry, with
an actionable error naming the agent. Regression test asserts a
prometheus-targeted delegation is rejected before any subagent
session spawns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two blockers from the maintainer's Oracle review on PR #4121:
Blocker 1 — load_skills=null should still throw, omitted should default
The previous PR collapsed both `loadSkills === undefined` and
`loadSkills === null` into a silent default of `[]`. The closing
rationale of PR #1663 (which reverted PR #1493) and the maintainer's
review both call out the importance of preserving the distinct
"omitted -> default, explicit invalid -> throw" contract. `null`
strongly signals "I tried to pass something and it was wrong";
silently coercing it hides bugs upstream.
Restored the split: `undefined` -> default `[]` + log,
`null` -> throw with the historical error string.
Blocker 2 — task_id continuation test rewritten, not deleted
The original PR removed the `task_id without run_in_background ->
throws` test entirely. The behavior IS preserved (default false ->
`isExplicitSyncRun` true -> `executeSyncContinuation`), but with the
test gone the new contract was unprotected.
Added a regression test that asserts the new contract: when
`task_id` is present and `run_in_background` is omitted,
`tool.execute` must route through sync continuation without throwing
the legacy required-parameter error. Mocks include `session.abort`
because the sync poller calls it during shutdown.
Also flipped the existing `load_skills=null` regression test from
"normalizes to []" back to "throws with the legacy error string" to
match the restored contract.
Tests:
- bun test src/tools/delegate-task/tools.test.ts -> 132/132 pass
- bun test src/tools/delegate-task/ -> 406/406 pass
- bun run typecheck -> clean
Sisyphus and other delegators occasionally invoke the task() tool without
an explicit run_in_background or load_skills argument. The runtime
validators in tool-argument-preparation.ts threw a hard Error in that
case, which short-circuited tool.execute() entirely. Because OpenCode's
tool.execute.after hook only runs on returned results, the
delegate-task-retry hook never had a chance to attach corrective
guidance — so the model saw a raw failure and either burned several
retries or fell back to a synchronous Explore call, silently losing
parallel execution.
Behavior change:
- run_in_background omitted -> defaults to false (sync delegation), with
a log entry for observability.
- load_skills omitted or null -> normalized to [] with a log entry on
the explicit-null path.
- The Zod schema entries are now .optional() and their .describe()
strings declare the defaults honestly; the markdown tool description
was updated to match (no more 'REQUIRED' lie).
The orthogonal validation 'Must provide either category or
subagent_type.' is unchanged and still surfaces as a returned error.
Tests:
- The five throw-on-missing tests in tools.test.ts are rewritten to
assert the new default-and-proceed contract.
- The 'no category, no subagent_type' test now asserts the
missing-target error remains intact.
Refs the workaround the reporter validated in the original issue body;
matches the design from PR #2375 which was previously reverted by
566031f4.
After the 4.2.0 unified-dispatch refactor (a42f894f / df198d8b / fee515c5 / 989ab717 / dd3fecaf / 1bbe065c / 12bd6580), at least one caller in the new prompt-async-gate path forwards a FallbackModelObject (or some other non-string shape) into parsers that statically claim 'model: string'. The downstream .trim() call then throws 'model.trim is not a function', which rejects the session.processor promise and surfaces as 'Aborted process' + UI 'interrupted'. The issue (#4145) reports this aborts 90% of subagent dispatches across every provider on 4.2.0 + opencode 1.15.4.
This patch adds a 'typeof x !== "string"' runtime guard at the four parser entrypoints called from the dispatch path:
- src/shared/fallback-chain-from-models.ts :: parseVariantFromModel, parseFallbackModelEntry
- src/tools/delegate-task/model-string-parser.ts :: parseVariantFromModelID, parseModelString
- src/shared/model-string-parser.ts (duplicate file with same API) :: parseVariantFromModelID, parseModelString
- src/features/claude-code-agent-loader/claude-model-mapper.ts :: mapClaudeModelString
Each parser now returns undefined / { modelID: "" } for non-string input instead of throwing. This unblocks subagent dispatch and leaves the underlying caller bug for a follow-up.
Regression coverage: three new tests in src/shared/fallback-chain-from-models.test.ts pin the non-string behavior (object, null/undefined, number). Existing 38 tests still pass. Total: 41/41 green, typecheck clean.
Two call sites built the sync delegate tool gate independently:
sync-prompt-sender's prompt body construction and sync-task's bootstrap
registration. Drift between them would let bootstrap claim one tool set
while the actual prompt sent a different one. Extract buildSyncPromptTools
and route both call sites through it so the registered bootstrap and the
dispatched prompt always agree.
Preserve delegated child prompt/bootstrap metadata for early runtime fallback before OpenCode has persisted the first user turn. Bind prompt gate calls to the SDK session receiver and keep completed background task lookup visible across plugin manager instances.
mergeWithClaudeCodeAgents deduplicated by raw agent.name.toLowerCase() while
matchesRequestedAgent strips invisible characters, the numeric sort prefix,
and wrapper characters via stripAgentListSortPrefix. A project or user agent
named with a zero-width prefix, quote wrappers, or a sort prefix survived as
a visible duplicate of the hidden native build or demoted plan agent and
matched subagent_type="build" or "plan", which let an OMO orchestrator reach
the hidden execution agent the previous filter was meant to block.
Apply the same canonicalization to the dedup key so visible aliases of hidden
server agents collapse onto the hidden entry instead of bypassing the filter.
Adds three regression tests covering ZWSP, quote-wrapper, and sort-prefix
bypass paths.
bun.lock: refresh platform optionalDependencies to 4.1.1 so frozen-lockfile
install succeeds in CI.
OpenCode injects native execution agents like build (and a demoted plan in OMO mode) as { mode: 'subagent', hidden: true }. The dynamic agent discovery in subagent-discovery.ts only filtered by mode, so a hidden agent still resolved as a callable target via task(). This created a boundary leak: an OMO orchestrator (sisyphus, prometheus, etc.) could delegate work into the hidden native build/plan path instead of the OMO category/skill pipeline.
Add hidden?: boolean to AgentInfo, plumb it through mergeWithClaudeCodeAgents, and skip hidden agents in both findCallableAgentMatch and listCallableAgentNames so hidden natives are neither matched nor advertised in 'Available agents' error messages. The OpenCode SDK Agent type already exposes hidden?: boolean, so no schema work is required.
Verified by adding three regression tests in zauc-mocks-subagent-resolver/subagent-resolver.test.ts: hidden 'build' is rejected, hidden 'plan' is rejected, and hidden agents are excluded from the Available agents list. Full delegate-task suite (395 tests) and call-omo-agent suite (57 tests) pass; bun run typecheck is clean.
resolveModelForDelegateTask returned input.userModel immediately without
availability checking when it was set, silently ignoring the user's
configured fallback_models. Effect: a team-mode category member
(e.g. hyperplan's 'artistry' category) configured with
{ model: "opencode/gemini-3.1-pro", fallback_models: [...] }
spawned with the unreachable primary even though a listed fallback was
reachable, leading to a dropped/broken team member instead of graceful
degradation.
Now, when the provider-models cache is warm AND userFallbackModels is
non-empty, the function verifies userModel is reachable via
fuzzyMatchModel. If not, it iterates userFallbackModels and promotes the
first reachable entry. Cold-cache (first-run) behavior is preserved -
the userModel is returned as-is when availability data is unavailable,
matching the existing 'trust the user' contract covered by the
pre-cache test fixtures.
Adds three regression tests covering: (1) unreachable primary + reachable
fallback -> fallback promoted, (2) reachable primary + reachable fallback
-> primary kept (fast path), (3) unreachable primary + unreachable
fallback -> legacy trust-user behavior preserved.
Sync the PR branch with the newest dev branch and resolve the new import-level conflicts in background-agent manager and runtime-fallback tests. Preserve both the delegated bootstrap coverage from this branch and the newer upstream test utilities and runtime wiring changes, then re-verify the affected delegated fallback suites and typecheck.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sync the PR branch with the latest dev branch and resolve the remaining conflict in sync-task.test.ts while preserving both the new upstream poll-recovery coverage and this branch's delegated bootstrap cleanup and isolation coverage. Re-verified the affected delegated fallback suites and typecheck after the merge resolution.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>