Continuation injector previously called normalizeAgentForPromptKey,
which collapsed agent inputs like "Sisyphus - Ultraworker" or
"sisyphus" down to the lowercase config key (e.g. "sisyphus").
OpenCode's promptAsync rejects that with "Agent not found", so the
ralph-loop continue prompt silently failed to dispatch on parent
sessions whose inherited message used a known display name.
Switch to normalizeAgentForPrompt and add a small wrapper that:
- preserves any agent string already in canonical " - " display form
verbatim (covers ZWSP-prefixed inheritance and user-defined custom
agents)
- otherwise normalizes config keys / legacy parenthesized names to the
registered display name OpenCode expects.
Update the existing regression tests so that ZWSP-prefixed and clean
inherited agents both assert the registered display name reaches
promptAsync.
Keep prompt reservations briefly after successful dispatch so rapid idle/message/error transitions cannot inject the same follow-up twice.
Route all production session prompt calls through the shared gate, restore skipped background resume state, release holds after abort/recovery paths, and preserve Ralph/ULW loop state when a dispatch is deferred.
Add regression coverage for session routing, static prompt route auditing, team-mode live messaging, model suggestion retries, call-omo-agent reuse, background parent wakes, runtime fallback, compaction recovery, Atlas, and Ralph/ULW loops.
Guard the tool-pair validator when transformed assistant messages belong to a tracked subagent session, while keeping normal main-session orphaned tool_use repair intact.
Constraint: code-yeongyu/oh-my-openagent#3996 reports background Oracle sessions hanging after synthetic placeholder tool_result insertion.
Rejected: disable tool-pair-validator globally | would regress main-session compaction/orphaned tool_use repair.
Confidence: high
Scope-risk: narrow
Directive: Keep subagent skip coverage and normal repair coverage together when changing tool-pair validation.
Tested: bun test src/hooks/tool-pair-validator/hook.test.ts src/plugin/messages-transform.test.ts
Tested: bun run typecheck
Not-tested: live Oracle background task e2e; no remote push or PR comment performed.
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.
When runtime-fallback aborts an in-flight request to swap in a fallback
model, opencode emits session.error{isAbort:true} as a consequence. The
existing event handler treated that as a user cancellation and called
resetRetryState — wiping attemptCount. Every subsequent provider
auto-retry signal then started over at attempt:1, never reaching
max_fallback_attempts, producing an infinite retry loop firing a new
fallback every ~2 seconds.
The bug only surfaces when the configured fallback target itself
silently fails (e.g. github-copilot quota exhausted): the original
model keeps re-emitting retry signals, our handler keeps "fixing"
them, the counter never advances. Reproducible on upstream/dev HEAD
(5ffbe0e24e).
Fix:
- New `internallyAbortedSessions: Set<string>` on HookDeps tracks
sessions whose abort we triggered ourselves.
- abortSessionRequest in auto-retry.ts adds the session to the set
when called with one of our internal sources:
"session.status.retry-signal", "message.updated.retry-signal",
"session.timeout". The "session.stop" source (user-initiated) is
intentionally NOT marked — that path must still wipe state.
- handleSessionError in event-handler.ts checks the set before the
cancellation branch. If the session is marked, consume the flag
(delete it so a later user-abort still gets the reset) and skip
resetRetryState. The state's attemptCount is preserved, so the
next iteration progresses 1→2→3→... until max_fallback_attempts.
- dispose() clears the new set alongside the other per-session maps.
Tests: 3 new event-handler integration tests cover the fix
(internal-abort preserves state, external-abort still resets,
consecutive internal-abort cycles advance attemptCount). Existing
tests pass: 7/7 on event-handler. Pre-existing 2 dispose-test flakes
on the full runtime-fallback suite were verified to exist on
upstream/dev without this patch — unrelated.
bun run build: pass. bun run typecheck: pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>