diff --git a/AGENTS.md b/AGENTS.md index c4bcc7518..51836dcf5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -175,7 +175,7 @@ Schema autocomplete: `"$schema": "https://raw.githubusercontent.com/code-yeongyu - **OpenClaw bidirectional:** Outbound dispatchers fire on session events; inbound daemon polls Discord/Telegram and `send-keys` replies into the tracked tmux pane. - **Internal message injection is dangerous:** OpenCode의 stupid한 설계로 플러그인이 `session.prompt` / `session.promptAsync` 같은 메인 세션 메시지 API를 통해 메인 시스템을 망가뜨릴 수 있다. - Root cause to remember: OpenCode `promptAsync` returns before the prompt is durably accepted, and later failures can arrive as `session.error`. Multiple OMO hooks/tools can observe the same idle/error/completion edge and inject the same internal message into a live parent session. - - Treat every `session.prompt` / `session.promptAsync` call as a write to shared session state. Production code may call them only inside `src/shared/prompt-async-gate.ts`; all other routes must use `promptAsyncAfterSessionIdle`, `promptAfterSessionIdle`, or a proven equivalent gate. +- Treat every `session.prompt` / `session.promptAsync` call as a write to shared session state. Production code may call them only inside `src/shared/prompt-async-gate.ts`; all other routes must use `dispatchInternalPrompt({ mode: "async" | "sync", ... })` or a proven equivalent gate. - Required gate semantics: reserve per session before dispatch, check active session state, keep a short post-dispatch hold, release only on intentional abort/recovery paths, and restore optimistic task/loop state when dispatch is skipped or fails later. - Forbidden patterns: raw prompt calls outside the shared gate, `postDispatchHoldMs: 0`, no-session fallback to raw prompt, and new internal message routes without duplicate-injection regression tests. - Tests must pin both the shared invariant and the route behavior: update the static raw-prompt audit, then add route-specific tests proving concurrent/live/idle/error triggers collapse to one dispatch. Cover background completion wakes, fallback retries, team mailbox live delivery, recovery continuations, CLI run resumes, Claude Code hook injections, and sync/background subagent prompts. diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e957d713..c5e83d6e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `createPluginModule` test seam moved out of public API surface to `src/testing/create-plugin-module.ts`. New public exports for the prompt-async-gate primitives: `promptAsyncAfterSessionIdle`, `promptAfterSessionIdle`, `releasePromptAsyncReservation`, `DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS`, `DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS`. +- `createPluginModule` test seam moved out of public API surface to `src/testing/create-plugin-module.ts`. New public exports for the prompt-async-gate primitives: `dispatchInternalPrompt`, `releasePromptAsyncReservation`, `DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS`, `DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS`. - `ParentWakeNotifier` module (`src/features/background-agent/parent-wake-notifier.ts`) extracted from `BackgroundManager`. Background-agent parent-wake state now lives in its own narrow class with dependency-injected client, directory, and notification enqueue callback. ### Changed diff --git a/docs/reference/prompt-async-gate-rfc.md b/docs/reference/prompt-async-gate-rfc.md index 799a2cb80..f72caa292 100644 --- a/docs/reference/prompt-async-gate-rfc.md +++ b/docs/reference/prompt-async-gate-rfc.md @@ -63,16 +63,12 @@ The root `AGENTS.md` now records the governing invariant in the section Create `src/shared/prompt-async-gate.ts` as the single production owner of raw OpenCode prompt dispatch. -The gate exposes the public wrappers that production callers must use: +The gate exposes one public dispatcher that production callers must use: ```ts -export function promptAsyncAfterSessionIdle( - options: PromptAsyncAfterSessionIdleOptions, -): Promise - -export function promptAfterSessionIdle( - options: PromptAfterSessionIdleOptions, -): Promise +export function dispatchInternalPrompt( + options: InternalPromptDispatchArgs, +): Promise ``` The gate coordinates callers with a module-global reservation map: @@ -125,16 +121,16 @@ export const DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS = 30_000 `session.prompt` call with `Promise.race`. A hung OpenCode API call must fail closed instead of holding a reservation forever. -Both public gate helpers delegate to one internal runner: +The public dispatcher delegates to one internal runner: ```ts dispatchAfterSessionIdle(args) ``` -`promptAsyncAfterSessionIdle` passes a `session.promptAsync` dispatcher. -`promptAfterSessionIdle` passes a `session.prompt` dispatcher. Sharing the -runner keeps reservation, hold, timeout, logging, and active-session behavior -identical for async and sync prompt routes. +`dispatchInternalPrompt({ mode: "async", ... })` binds `session.promptAsync`. +`dispatchInternalPrompt({ mode: "sync", ... })` binds `session.prompt`. +Sharing the runner keeps reservation, hold, timeout, logging, and active-session +behavior identical for async and sync prompt routes. The public gate result is a discriminated union. Callers must treat `active` and `reserved` as successful suppression, not automatic retry signals. A route @@ -198,7 +194,7 @@ optional chaining, and aliased or cast access patterns. ### Migration Existing `session.prompt` and `session.promptAsync` callers must route through -`promptAfterSessionIdle` or `promptAsyncAfterSessionIdle`. +`dispatchInternalPrompt` with the matching dispatch mode. Existing production callers were wired through the introduction PR #4034. diff --git a/src/shared/prompt-async-route-audit.test.ts b/src/shared/prompt-async-route-audit.test.ts index 19649efe6..a90e9cb73 100644 --- a/src/shared/prompt-async-route-audit.test.ts +++ b/src/shared/prompt-async-route-audit.test.ts @@ -16,7 +16,7 @@ const RAW_PROMPT_ALLOWLIST = new Map([ ], [ path.join(SOURCE_ROOT, "hooks", "session-recovery", "recover-unavailable-tool.ts"), - "runtime type guard checks promptAsync presence before gate-routed promptAsyncAfterSessionIdle", + "runtime type guard checks promptAsync presence before gate-routed dispatchInternalPrompt", ], ])