Anthropic made the 1M context window GA for Opus 4.6, Sonnet 4.6, and 4.7 models on March 13, 2026 — no beta header required. The resolver was still falling back to 200K when no modelContextLimitsCache entry existed, causing premature compaction and over-truncation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Address Oracle review feedback: refactor 4 aliased mutations via argsObject
in plugin/tool-execute-before.ts and 1 via toolOutput in atlas/tool-execute-before.ts.
Strengthen audit test regex to catch Output.args mutations regardless of the
variable name prefix (toolOutput, argsObject aliases).
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Add replace-tool-args.audit.test.ts that scans src/**/*.ts for direct
output.args property assignments and Object.assign(output.args, ...) outside
the helper. Also fix the 9th mutation site discovered by the audit in
compaction-todo-preserver/hook.ts.
Add replace-tool-args.test.ts with 12 regression tests covering both mutable
and Object.freeze'd output.args scenarios for all hook patterns.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
opencode >=1.14 freezes output.args via Immer before plugin hooks run.
Direct property assignment or Object.assign on a frozen object throws
TypeError. This helper replaces output.args with a shallow clone
containing the patch, avoiding mutation of the frozen original.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Refs #3772 (the rotation half — EPIPE shutdown-noise suppression
remains a separate follow-up).
`src/shared/logger.ts` appends every entry to `os.tmpdir()/oh-my-opencode.log`
via `fs.appendFileSync` with no size cap. On long-running or busy projects
the file grows into the multi-GB range — a real-world reproduction on one
machine showed a 4.5 GB `oh-my-opencode.log.1` accumulated from per-shutdown
noise across many sessions. Eats `%TEMP%` on Windows and `/tmp` on Unix.
Add size-based rotation inside the existing batched `flush()` path:
oh-my-opencode.log → oh-my-opencode.log.1
oh-my-opencode.log.1 → oh-my-opencode.log.2 (oldest dropped)
Cap is 50 MB per file; worst-case on-disk footprint is therefore ~150 MB.
The check runs only inside `flush()`, so the cost is amortized over
`BUFFER_SIZE_LIMIT` (50 entries) or the 500 ms flush timer. All filesystem
ops stay wrapped in try/catch — logging must never throw — and a failed
rotation leaves existing on-disk state intact rather than crashing the
agent. Pattern mirrors `src/openclaw/reply-listener-log.ts`, but with two
backup slots instead of one to keep a usable history window for debugging.
No config knobs in this iteration. The issue proposes `logs.max_size_mb`
/ `logs.max_files`, but the defaults are reasonable and adding schema is
more surface area than the bug warrants. Easy to promote later (the
existing test seams already let callers override the cap).
Tests:
- `src/shared/logger.test.ts` (new): under-threshold no-rotate, over-
threshold rotates to `.1`, repeated rotation evicts oldest, rotation-
failure-doesn't-throw, default path lives under `os.tmpdir()`. Uses a
`mock.module(...)` substring marker so `script/run-ci-tests.ts` routes
the file to its own bun process — the logger module's singleton state
otherwise gets contaminated by sibling tests that mock `./shared`.
Out of scope: suppressing specific shutdown-noise messages (EPIPE,
`unhandledRejection received during shutdown cleanup`). The rotation
cap bounds the disk impact regardless of which noise pattern is
generating volume; per-message suppression can stand on its own
merits in a follow-up.
Use one dispatchInternalPrompt surface with mode: async | sync so source, settle, hold, timeout, status checks, reservations, and release semantics stay in one runner. Keep the old helper names temporarily so caller migration can land atomically in follow-up commits.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Root cause: `getContextWindowUsage` caches the *promise* of
`fetchContextWindowUsage` in a per-session WeakMap keyed by client. When
`ctx.client.session.messages({ path: { id: sessionID } })` never settles
(observed once `service=session.processor ... error=Aborted process`
takes hold), the cached pending promise wedges every concurrent and
later caller in the same session. The five hooks that share one
`createDynamicTruncator(ctx)` -- directory-agents-injector,
directory-readme-injector, rules-injector, tool-output-truncator, plus
indirect callers -- all await that same poisoned promise on every Read,
so the user-facing tool chain hangs forever and ESC cannot break it.
Reporters in #4086 land on this path consistently when reading AGENTS.md
files (which trigger directory-agents-injector via the directory walk).
Fix: race the underlying `session.messages` call against a 5s timeout
through a new `withFetchTimeout` helper. On timeout the catch block logs
the failure and returns `null`, which `dynamicTruncate` already treats
as the "context usage unavailable" signal and falls back to the static
truncation budget. Successful responses still cache as before. The
`message.updated finish=true` invalidation hook still clears poisoned
caches on the next completed turn so retries are clean.
Tests:
- Add a never-settling `session.messages` mock with a 50 ms override via
the new `_setContextWindowUsageFetchTimeoutMsForTesting` hook (matches
the established `_setXxxForTesting` pattern in `opencode-http-api.ts`
and `prompt-async-gate.ts`).
- Three new BDD cases pin the fix: (1) single caller returns null fast,
(2) parallel concurrent callers all unblock on the same cached promise
instead of hanging, (3) invalidate + retry rehydrates cleanly.
- All 8 pre-existing tests in the file still pass (happy paths, cache
reuse, invalidation, env/model fallback).
Verification:
- `bun test src/shared/dynamic-truncator.test.ts` -- 11 pass.
- `bun test src/shared/prompt-async-route-audit.test.ts` -- 6 pass
(added log import, no raw prompt route added).
- `bun test` (full suite) -- 7009 pass, 1 skip, 1 pre-existing flake in
`closeTmuxPane` mock.module test (reproduces on dev without this
change; isolated run passes).
- `bun run typecheck` -- clean.
- `bun run build` -- clean (esm bundle + tsc + schema).
- Manual harness `.debugging/manual-qa.ts` (uncommitted) drives the same
shape as the real hook chain and resolves the hang scenario in 51 ms.
- Wrap isSessionActive in withDispatchTimeout (capped at 5s) so a
stuck OpenCode SDK status() call cannot block internal prompts forever.
- Catch the timeout and treat session as inactive so the prompt can
proceed rather than hanging indefinitely.
- Add regression test: session.status that never resolves now times out
and allows dispatch instead of hanging the test (and production).
Refs: AGENTS.md internal-message-injection safety note
Red: a43215f24 introduced plugin/build-team-idle-wake-hint-client.ts which
accesses session.promptAsync for method binding. The audit test flagged it
as a raw prompt route offender, breaking CI on dev.
Green: Add the narrow client facade to RAW_PROMPT_ALLOWLIST with the same
justification pattern used for event.ts and recover-unavailable-tool.ts.
The facade binds SDK methods back to the Session instance and performs
no direct dispatch itself; all downstream calls flow through the shared
prompt-async gate.
Verification: bun test src/shared/prompt-async-route-audit.test.ts passes
(6 pass, 0 fail, offenders list empty).
Add optional system and tools fields to DelegatedChildSessionBootstrap
so callers can stash the original delegated context alongside retry
parts. Backward compatible - existing callers stay unchanged.
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.
New AST-based audit walks all *.test.ts files under src/ and asserts every mock.module(...) call is paired with cleanup. Existing offenders are documented in MOCK_MODULE_LIFECYCLE_ALLOWLIST with TODO references.
Closes HIGH-10
The promptWithModelSuggestionRetry async variant did not release the
post-dispatch reservation when the wrapped promptAsync threw. Callers
that immediately retry (such as sendSyncPrompt error toast paths) hit
the gate as reserved and surfaced 'promptAsync skipped by gate: reserved'
instead of the underlying error.
Mirrors the existing sync variant fix from ff1b15d53.
Closes regression introduced by BLOCKER-2 hardening
Walk all test files, parse with TypeScript Compiler API, assert every
mock.module(path, factory) invocation has a paired afterEach/afterAll
cleanup. Existing offenders are allowlisted with TODOs for v4.2.1 work.
Closes H10
After BLOCKER-2's post-dispatch hold landed (the gate now keeps the
reservation through the hold window regardless of whether the dispatch
threw), the synchronous retry path inside promptSyncWithModelSuggestionRetry
hit 'reserved' on its own second attempt because the first attempt's
post-dispatch hold was still active.
The first attempt's failure is ProviderModelNotFoundError, which is a
synchronous SDK rejection - the prompt never reached the server, so
there is no durable session state worth protecting from a duplicate
injection. Release the post-dispatch reservation hold explicitly before
the suggested-model retry so the second attempt can dispatch immediately.
Fixes test regression introduced by the gate hardening (BLOCKER-2 fix).
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)
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)