docs(prompt-gate): document unified dispatch invariant

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-17 17:16:18 +09:00
parent 6768decddb
commit 98df0a43e3
4 changed files with 13 additions and 17 deletions
+1 -1
View File
@@ -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. - **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를 통해 메인 시스템을 망가뜨릴 수 있다. - **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. - 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. - 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. - 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. - 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.
+1 -1
View File
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### 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. - `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 ### Changed
+10 -14
View File
@@ -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 Create `src/shared/prompt-async-gate.ts` as the single production owner of raw
OpenCode prompt dispatch. 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 ```ts
export function promptAsyncAfterSessionIdle( export function dispatchInternalPrompt(
options: PromptAsyncAfterSessionIdleOptions, options: InternalPromptDispatchArgs,
): Promise<PromptAsyncGateResult> ): Promise<InternalPromptDispatchResult>
export function promptAfterSessionIdle(
options: PromptAfterSessionIdleOptions,
): Promise<PromptAsyncGateResult>
``` ```
The gate coordinates callers with a module-global reservation map: 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 `session.prompt` call with `Promise.race`. A hung OpenCode API call must fail
closed instead of holding a reservation forever. 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 ```ts
dispatchAfterSessionIdle<TInput>(args) dispatchAfterSessionIdle<TInput>(args)
``` ```
`promptAsyncAfterSessionIdle` passes a `session.promptAsync` dispatcher. `dispatchInternalPrompt({ mode: "async", ... })` binds `session.promptAsync`.
`promptAfterSessionIdle` passes a `session.prompt` dispatcher. Sharing the `dispatchInternalPrompt({ mode: "sync", ... })` binds `session.prompt`.
runner keeps reservation, hold, timeout, logging, and active-session behavior Sharing the runner keeps reservation, hold, timeout, logging, and active-session
identical for async and sync prompt routes. behavior identical for async and sync prompt routes.
The public gate result is a discriminated union. Callers must treat `active` The public gate result is a discriminated union. Callers must treat `active`
and `reserved` as successful suppression, not automatic retry signals. A route and `reserved` as successful suppression, not automatic retry signals. A route
@@ -198,7 +194,7 @@ optional chaining, and aliased or cast access patterns.
### Migration ### Migration
Existing `session.prompt` and `session.promptAsync` callers must route through 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. Existing production callers were wired through the introduction PR #4034.
+1 -1
View File
@@ -16,7 +16,7 @@ const RAW_PROMPT_ALLOWLIST = new Map<string, string>([
], ],
[ [
path.join(SOURCE_ROOT, "hooks", "session-recovery", "recover-unavailable-tool.ts"), 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",
], ],
]) ])