Commit Graph

4777 Commits

Author SHA1 Message Date
YeonGyu-Kim c1ccf8d096 refactor(background-agent): introduce ParentWakeNotifier module
Extracts the parent-wake coalescing logic (pending/dispatched wake maps,
timers, notification reply assembly) from manager.ts into a standalone
ParentWakeNotifier class. Takes dependency-injected client, directory,
and an enqueueNotificationForParent callback, so the manager can delegate
parent-wake state to a narrow API.

This commit only introduces the new module; wiring manager.ts to use it
is a follow-up commit so the refactor stays atomic (HIGH-9 step 1 of 2).

Closes HIGH-9 (step 1: extraction)
Refs HIGH-9 (step 2: manager.ts integration deferred until verification)

Co-authored-by: manager-extract (deep / gpt-5.3-codex high)
2026-05-16 00:49:50 +09:00
YeonGyu-Kim 8c4cc09de7 test(prompt-async-route-audit): migrate to TypeScript AST walker
Replaces the previous regex-based audit (6 line-prefix patterns) with a
TypeScript Compiler API AST walker that detects raw client.session.prompt
and client.session.promptAsync access in any access shape:
- direct call (existing): client.session.promptAsync(...)
- property access reference: const x = client.session.promptAsync
- bracket access: client['session']['promptAsync']
- optional chaining: client.session?.promptAsync
- type cast aliasing: (client.session as { promptAsync }).promptAsync
- destructuring: const { promptAsync } = client.session

RAW_PROMPT_ALLOWLIST captures two legitimate callers that route through
the gate but reference promptAsync as a property value:
- src/plugin/event.ts wires a client facade for team-idle-wake-hint
- src/hooks/session-recovery/recover-unavailable-tool.ts guards capability
before dispatching through promptAsyncAfterSessionIdle.

Each allowlist entry carries a justification string so future contributors
understand why the exception exists.

Closes HIGH-5

Co-authored-by: audit-ast (deep / gpt-5.3-codex high)
2026-05-16 00:49:50 +09:00
YeonGyu-Kim f93d7297c8 test(prompt-async-gate): cover dispatch timeout and post-dispatch error hold
Adds regression coverage for BLOCKER-1 (dispatch timeout releases
reservation for next caller after stalled upstream) and BLOCKER-2
(post-dispatch error preserves the post-dispatch hold so an immediate
second caller observes the reservation and is gated).

Both tests subscribe-first on the promptAsync call count and assert
status transitions without sleep-based synchronization. dispatchTimeoutMs
is the system under test, so passing it explicitly as 1ms in those tests
is the SUT, not a sleep-as-synchronization (per test-discipline.md).

Closes BLOCKER-3 (dispatch timeout + post-dispatch coverage)

Co-authored-by: gate-tests (deep / gpt-5.3-codex high)
2026-05-16 00:49:28 +09:00
YeonGyu-Kim b333a52800 fix(prompt-async-gate): add dispatch timeout, shared runner, harden prefix release
BLOCKER-1 (dispatch deadlock): wrap session.promptAsync / session.prompt in
withDispatchTimeout() that uses Promise.race with a default 30s timeout.
Stalled upstream responses no longer hold the reservation forever.

BLOCKER-2 (post-dispatch failure released too early): collapse the
holdReservationAfterDispatch flag into a dispatchAttempted state so the
post-dispatch hold runs in the finally block regardless of whether
promptAsync resolved or threw. AGENTS.md's documented race window where
promptAsync 'returns before durably accepted, later failures arrive as
session.error' is now covered.

HIGH-6 (sync/async protocol duplicated): extract dispatchAfterSessionIdle
internal runner. promptAsyncAfterSessionIdle and promptAfterSessionIdle
become thin wrappers passing client.session.promptAsync vs prompt as
the dispatch callback. Future reservation semantics fixes apply once.

HIGH-7 (releasePromptAsyncReservation prefix foot-gun, partial): tighten
reservationSourceMatches to require prefix strings to end in ':' so
release cannot accidentally free reservations whose source merely starts
with the same identifier characters. Symbol token verification is still
internal-only as the audit invariant prevents external callers from
bypassing the gate.

Closes BLOCKER-1, BLOCKER-2, HIGH-6
Refs HIGH-7 (prefix hardened; token-required release deferred to follow-up)

Co-authored-by: gate-correctness (deep / gpt-5.3-codex high)
2026-05-16 00:49:28 +09:00
YeonGyu-Kim c067b0fc06 refactor(plugin-entry): move createPluginModule to testing module
createPluginModule and PluginModuleDeps were exposed at package entry as
a test seam. Their export creates accidental public TS API obligations
for internal manager/tool/hook constructor types. Move to
src/testing/create-plugin-module.ts so only tests reach them.

Closes HIGH-8

Co-authored-by: api-surface (deep / gpt-5.3-codex high)
2026-05-16 00:49:03 +09:00
YeonGyu-Kim e63c5b9a22 fix(tmux): require explicit active isolated window 2026-05-16 00:26:44 +09:00
YeonGyu-Kim 1ff59f44ce test(tmux): align pane-state runner format 2026-05-16 00:22:52 +09:00
YeonGyu-Kim d02cca4422 test(tmux): align pane replace placeholder expectations 2026-05-16 00:18:33 +09:00
YeonGyu-Kim 54a7256a71 test(tmux): align placeholder command expectations 2026-05-16 00:14:06 +09:00
Disaster-Terminator 91f1cf5fbc fix(tmux): pin pane commands to /bin/sh 2026-05-16 00:08:51 +09:00
Disaster-Terminator 8c5ca73634 fix(tmux): sweep suffixed stale isolated sessions 2026-05-16 00:07:57 +09:00
Disaster-Terminator 0c8e546c57 fix(tmux): support manager-scoped isolated session names 2026-05-16 00:06:58 +09:00
Disaster-Terminator 4e3684eb2a fix(tmux): gate isolated pane activation on visible client focus 2026-05-16 00:05:02 +09:00
Disaster-Terminator c63108d55b fix(tmux): track placeholder panes before attach readiness 2026-05-16 00:02:02 +09:00
Disaster-Terminator 688bb551b2 fix(tmux): defer subagent attach until pane focus 2026-05-15 23:57:07 +09:00
Enoch d11613999a fix tool execute after hook boundary 2026-05-15 23:37:45 +09:00
YeonGyu-Kim ad10450b5c Merge pull request #3841 from Momentum96/fix/background-manager-tmux-ordering 2026-05-15 23:35:29 +09:00
YeonGyu-Kim be25109f6d fix(continuation): skip internal user turns 2026-05-15 23:16:23 +09:00
YeonGyu-Kim c580b8f2ce fix(session): ignore internal synthetic turns 2026-05-15 23:16:05 +09:00
YeonGyu-Kim e8de8b79a8 fix(team-mode): skip pending mailbox reinjection 2026-05-15 23:15:46 +09:00
Momentum96 e90ff8058e fix(tmux): treat busy sessions as attachable 2026-05-15 23:12:56 +09:00
Momentum96 efb862ce9f fix(tmux): prefer real tmux when session env exists 2026-05-15 23:12:56 +09:00
Momentum96 5238dd484e fix(background-agent): start promptAsync before blocking tmux callback
- Fixes BackgroundManager to launch promptAsync before invoking the blocking tmux callback.
- Adds regression test to ensure promptAsync is called before tmux callback.
2026-05-15 23:11:07 +09:00
YeonGyu-Kim a9a003250c Merge pull request #3497 from Disaster-Terminator/fix/reminder-hooks-preserve-state-across-compaction 2026-05-15 23:08:48 +09:00
YeonGyu-Kim 291b1f7b3c test(reminder-hooks): clean up compaction regressions 2026-05-15 22:59:58 +09:00
YeonGyu-Kim 672f5d6e9b fix(keyword-detector): skip synthetic turns 2026-05-15 22:49:17 +09:00
YeonGyu-Kim 196f6512ae fix(team-mode): defer live mailbox acks 2026-05-15 22:49:10 +09:00
Disaster-Terminator 392c20e53a test(reminder-hooks): make delete reset regression diagnostic 2026-05-15 22:43:32 +09:00
Disaster-Terminator 29e7e97d8f test(reminder-hooks): cover delegated sessions across compaction 2026-05-15 22:42:49 +09:00
Disaster-Terminator 3db1da1e5b fix(reminder-hooks): preserve suppression state across compaction 2026-05-15 22:42:49 +09:00
YeonGyu-Kim 3e9b125fd5 test(runtime-fallback): cover localized provider errors 2026-05-15 22:25:20 +09:00
wjiuxing c206b16865 feat: add Chinese error patterns to model-error-classifier 2026-05-15 22:23:06 +09:00
wjiuxing 149a83d703 feat: add Chinese quota patterns to classifyErrorType 2026-05-15 22:22:21 +09:00
wjiuxing adfa8befbb feat: add Chinese error patterns to RETRYABLE_ERROR_PATTERNS 2026-05-15 22:21:06 +09:00
Ravi Tharuma d9033d73ae fix(runtime-fallback): keep variant in equivalence
Treat parsed variant as part of runtime-fallback model equivalence so variant-only fallback hops remain distinct while preserving the existing Claude-family alias handling.

Constraint: Oracle verification flagged unresolved PR #3322 review concerns about variant equivalence and remote state
Rejected: Preserve provider identity in equivalence | contradicted the original live-loop fix for equivalent Claude aliases
Confidence: medium
Scope-risk: narrow
Directive: Any future equivalence broadening must prove both live retry-loop behavior and variant/provider semantics with targeted tests before merging
Tested: bun run typecheck
Tested: bun test src/hooks/runtime-fallback/index.test.ts src/hooks/runtime-fallback/error-classifier.test.ts src/plugin/event.model-fallback.test.ts
Not-tested: Full live end-to-end repro across all provider redundancy policies
2026-05-15 22:05:27 +09:00
Ravi Tharuma f501c47c49 fix(runtime-fallback): skip equivalent claude aliases
Prevent runtime fallback from cycling through provider aliases that resolve to the same underlying Claude family model. This keeps retry handling moving toward a genuinely distinct fallback model instead of appearing to fallback while staying on the same effective model.

Constraint: Live retry/fallback bug is in /Users/ravi/Code/personal/oh-my-opencode, while oh-my-openagent contribution work remains isolated to /Users/ravi/Code/forks/oh-my-openagent
Rejected: Change fallback chain precedence (category vs agent) first | lower-confidence root cause than equivalent-model retry
Confidence: high
Scope-risk: narrow
Directive: Keep alias-equivalence logic limited to model families that are intentionally interchangeable for runtime failover, and expand with targeted tests before broadening provider-family collapsing
Tested: bun run typecheck
Tested: bun test src/hooks/runtime-fallback/index.test.ts src/hooks/runtime-fallback/error-classifier.test.ts src/plugin/event.model-fallback.test.ts
Not-tested: Full live end-to-end session repro against external provider outages
2026-05-15 22:05:00 +09:00
YeonGyu-Kim f54888b2fe Merge pull request #3576 from Disaster-Terminator/fix/background-busy-stall-detection 2026-05-15 21:56:31 +09:00
YeonGyu-Kim 27788b4ab0 fix(session-recovery): audit raw prompt aliases 2026-05-15 21:53:05 +09:00
YeonGyu-Kim 4a1c260d8b test(todo-continuation): cover peer-message reservation holds 2026-05-15 21:52:53 +09:00
YeonGyu-Kim 39fef2042b fix(background-agent): resolve parent wake agent aliases 2026-05-15 21:52:40 +09:00
YeonGyu-Kim 2bd4944bad fix(prompt-gate): scope reservation releases 2026-05-15 21:52:29 +09:00
Disaster-Terminator b6caa5d3e0 fix(background-agent): correct stall timeout guidance 2026-05-15 21:44:29 +09:00
Disaster-Terminator 189af23e96 fix(background-agent): detect stalled active sessions 2026-05-15 21:44:29 +09:00
YeonGyu-Kim 65c1283338 fix(todo-continuation): clean up idle event diagnostics 2026-05-15 21:29:21 +09:00
Evgeny Zotov 047ca069a2 test: rename test to reflect todo-only stagnation check
Address review feedback: test name no longer references 'tool activity'
since activity tracking was removed.
2026-05-15 21:11:08 +09:00
Evgeny Zotov 68e9d54fa5 fix(todo-continuation): remove activity-based stagnation bypass
Activity signals (tool calls like compress, grep, bash) were treated as
'progress' by the stagnation detector, resetting the stagnation counter
every cycle. This prevented MAX_STAGNATION_COUNT from being reached,
causing infinite continuation loops when models degrade to minimal
responses in long sessions (e.g. GLM-5.1 at ~100K tokens).

Stagnation now only tracks actual todo state changes: incomplete count
decrease, completed count increase, or todo snapshot change. Tool-level
activity no longer resets the stagnation counter.
2026-05-15 21:11:08 +09:00
rshks b504fb1deb fix(tool-pair-validator): emit schema-compatible synthetic tool results 2026-05-15 21:04:03 +09:00
Ivan Smetanin 3199bd3d90 fix(runtime-fallback): broaden watchdog progress detection + harden test timing
Addresses two issues identified by cubic on PR #3952.

1. Watchdog cancellation was too narrow — only `text`/`reasoning` parts
   counted as progress, so a subagent that immediately ran tools
   (Read/Bash/Edit) emitted `tool`/`tool_use`/`tool_result`/`tool-call`/
   `step-start` parts that the watchdog ignored, risking a false fire
   on actively-working subagents. Broaden to: any assistant part of any
   known type counts as progress (the model has started responding,
   whether or not visible text has arrived yet). `info.error` and
   `info.finish` continue to cancel.

2. Test timing margins were tight (15ms pre-cancel against a 40ms
   timer), risking CI flakiness on loaded runners. Bumped to a 100ms
   threshold with a 40ms pre-cancel window and a 250ms post-fire wait,
   giving a 60ms margin before the timer fires and ~2.5x the threshold
   after — robust against scheduler delay.

Refactor for testability: extracted the OpenCode-event→watchdog-signal
translation out of `hook.ts` into an exported `observeEventForWatchdog`
helper on the watchdog module. This let me add direct unit tests for
every part-type case (text, reasoning, tool, tool_use, tool_result,
tool-call, step-start, file) plus the error/finish/empty-parts branches
without spinning up the full hook. Net diff: hook.ts shrinks, watchdog
module gains a small pure function with parametrised coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:47:03 +09:00
Ivan Smetanin a130fa70d1 fix(runtime-fallback): add first-prompt watchdog for stuck subagents
When a subagent is dispatched to a provider and the underlying SDK
enters a silent internal retry loop on a 429/quota error, no error
event is ever emitted back to OpenCode. The runtime-fallback hook —
which is fully reactive (listens to message.updated/session.error/
session.status) — has nothing to react to and never dispatches the
configured fallback. The subagent sits in `retry` status until the
parent's 30-minute poll timeout (DEFAULT_POLL_TIMEOUT_MS) gives up,
during which the parent's pending task tool call shows "waiting for
subagent" with no indication of failure.

This change adds a first-prompt watchdog that synthesises the missing
error-event trigger:

  - Armed when a user message lands in a subagent session
    (membership check via `subagentSessions`).
  - Cancelled on the first sign of progress: any assistant message
    with text/reasoning content, finish field, or an error field (any
    of which is something the existing handlers will deal with).
  - Cancelled on session terminal events (idle/stop/deleted/error).
  - On fire (90s default): aborts the in-flight request and routes
    into the existing dispatchFallbackRetry path — the same code that
    runs when a session.error arrives. No new fallback mechanism.

Design choices:

  - Dispatch fallback, do not abort the subagent outright. Network
    loss looks identical to a stuck retry from the hook's vantage
    point; with fallback-dispatch behaviour, network loss degrades
    to today's baseline (both attempts fail, 30-min outer timeout
    still ends things) rather than destructively aborting work.
  - Scope strictly to subagents. Parent/user sessions can legitimately
    take 90s+ to produce the first token; subagent dispatches in
    practice produce first content much faster, so a 90s ceiling is
    safe.
  - Threshold is tunable via the third arg to createFirstPromptWatchdog;
    DEFAULT_FIRST_PROMPT_WATCHDOG_MS = 90_000 in constants.ts.

Also adds a diagnostic log in session-status-handler when a
`session.status: retry` event arrives whose message does not match
RETRYABLE_ERROR_PATTERNS. This is the hook's other silent-return
spot for retry events; logging the raw retry message will let us
extend the patterns next time we hit a provider whose phrasing
we don't yet match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:45:46 +09:00
YeonGyu-Kim bda0452b2a Merge pull request #4029 from sandikodev/fix/json-error-recovery-exclude-todowrite
fix(json-error-recovery): add todowrite and todoread to exclude list
2026-05-15 20:27:42 +09:00