When many background tasks complete in rapid succession while the parent
session is idle, each completion fired its own promptAsync call, stacking
N consecutive `<system-reminder>` user messages with no assistant turn
between. Hyperplan + many parallel explore subagents made this very
visible to the user.
Route the idle-path through the existing pendingParentWakes queue with a
100ms debounce window. Notifications arriving during the debounce join
the same batch, the 150ms settle window also coalesces newcomers, and a
single batched prompt fires to the parent. Busy-path semantics are
unchanged (still 1s retry).
Prior attempts (1c05c60dc, ea55c385b, a337635e3) all coalesced only the
busy-defer path, leaving the idle-immediate-send path uncoalesced.
Reframe the skill's Lifecycle section so the lead treats teams as
ephemeral, one-per-phase units. The moment a phase ends or the shape
no longer fits, call team_delete and spawn a fresh team. Restructure
through delete-then-create, never in place.
Also fixes a misframing in old step 5: team_shutdown_request is a
per-session self-shutdown signal, not a 'wind down the team' command.
team_delete is what tears the whole team down.
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.
Prevent delayed loop-start message counts from overwriting active Ralph Loop state after the loop has already advanced, so ULW completion can still enter Oracle verification instead of iterating forever.
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 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>
The circuit breaker in manager.ts uses recordToolCall to detect when a subagent gets stuck repeating identical tool_use blocks. It passed partInfo.state?.input as the tool-input signature. When a model (Kimi K2.6 in the reporter's case) emits duplicate tool_use parts faster than the tool actually starts running, state.input is still null, so loop-detector falls back to the bare 'tool::__unknown-input__' signature. As soon as one part has state.input populated (next event), the signature flips to 'tool::{actual-args}' and the consecutive counter resets to 1, repeatedly. The breaker never reaches its 20-call threshold.
Add a top-level input?: Record<string, unknown> field to the local MessagePartInfo interface and prefer state.input when present, falling back to the part's own input when state is still pre-running. The OpenCode part payload carries the tool input as soon as the tool_use block is generated, so this fallback restores signature stability across the model's repeated emissions.
Verification: added 2 regression tests in manager-circuit-breaker.test.ts. Test 1 (reproduce) emits 20 part.updated events with only top-level input and asserts the task is cancelled by the breaker — fails before the fix, passes after. Test 2 confirms that when state.input IS present, it still wins over the top-level input (precedence preserved). All 10 manager-circuit-breaker tests pass, all 20 loop-detector tests pass, typecheck clean.
Cubic AI reviewer flagged the use of the untrusted Host header as the
URL base in startCallbackServer. The server only binds to 127.0.0.1,
so hardcoding "http://127.0.0.1" as the URL base is robust against
malformed or manipulated Host values and matches upstream behavior
prior to the node:http refactor.
Existing test only checked `globalThis.Bun` top-level destructures
and `__require` calls. Add two new test cases:
1. Raw Bun runtime API scanner: scans dist/index.js for any
`Bun.<anyMethod>(` or `Bun.<anyMethod>.` call outside shim-safe
patterns (runtime.Bun, globalThis.Bun, typeof Bun, and the
"Bun is not defined" error-message string). Uses a negative
lookbehind so shim indirection (`runtime.Bun.foo`) passes.
2. Node smoke test: imports dist/index.js under
`node --input-type=module` and asserts stderr contains no
`ReferenceError` and no `Bun is not defined`. The existing
case 3 only checked exit code, which masked lazy-evaluation
crashes that fire after import. Reading exports forces lazy
module-level evaluation paths to run.
bun-install.ts streamToText() was reachable from the plugin bundle via
the cli/config-manager barrel re-export. Replace with the WHATWG
standard `new Response(stream).text()` pattern which works
identically in Bun and Node and avoids the last raw Bun.* runtime
call in dist/index.js.
The OAuth callback server was using Bun.serve, which would crash if
mcp-oauth code paths ever entered the plugin bundle. Switch to
node:http.createServer with the same WHATWG behavior:
- Binds to 127.0.0.1, preserves 200/400/404 status codes
- Translates fetch(Request)→Response to (req, res) callback style
- Clears OAuth timeout on success/error/missing-param paths
- Replaces server.stop(true) with server.close() for shutdown
Functional behavior and response bodies unchanged.
isPortAvailable() previously bound a one-shot Bun.serve and stopped it.
That call was reachable from the plugin bundle through
src/shared/index.ts barrel re-export and crashed on Electron.
Switch to node:net.createServer().listen(port, host), which Bun fully
implements as well. Adds a 2s safety timeout and removes both
"error" and "listening" handlers on resolution to prevent listener
leaks. Behavior is bit-equivalent: returns true iff a server can bind
to (host, port) right now.
Test file is fully rewritten away from stale Bun.serve mocking. New
tests exercise: free-port detection via port 0, EADDRINUSE handling
via a real net.createServer blocker, findAvailablePort range
exhaustion, getAvailableServerPort auto-selection, 127.0.0.1 default
hostname binding, and probe-server resource cleanup.
Eliminates 19 unguarded `Bun.*` runtime call sites in the plugin bundle
that crashed with `ReferenceError: Bun is not defined` under Electron.
Per-tool-call hot paths (executed on every Read/Edit):
- src/tools/hashline-edit/hash-computation.ts: Bun.hash.xxHash32 → bunHashXxh32
- src/tools/hashline-edit/hashline-edit-executor.ts: 8 sites via bunFile/bunWrite
- src/hooks/hashline-read-enhancer/hook.ts: Bun.file → bunFile
- src/hooks/hashline-edit-diff-enhancer/hook.ts: 2 sites via bunFile
Plugin-load paths:
- src/hooks/claude-code-hooks/config.ts and config-loader.ts: Bun.file → bunFile
- src/features/claude-code-mcp-loader/loader.ts: Bun.file → bunFile
- src/features/claude-code-plugin-loader/mcp-server-loader.ts: Bun.file → bunFile
- src/features/team-mode/deps.ts: Bun.spawn → spawn shim
- src/hooks/session-notification-utils.ts: Bun.which → bunWhich, also drops
the bare `declare const Bun` ambient declaration
- src/shared/binary-downloader.ts: Bun.write → bunWrite
Pure mechanical API swaps. No control-flow or signature changes.