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:
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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<PromptAsyncGateResult>
|
||||
|
||||
export function promptAfterSessionIdle(
|
||||
options: PromptAfterSessionIdleOptions,
|
||||
): Promise<PromptAsyncGateResult>
|
||||
export function dispatchInternalPrompt(
|
||||
options: InternalPromptDispatchArgs,
|
||||
): Promise<InternalPromptDispatchResult>
|
||||
```
|
||||
|
||||
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<TInput>(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.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const RAW_PROMPT_ALLOWLIST = new Map<string, string>([
|
||||
],
|
||||
[
|
||||
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",
|
||||
],
|
||||
])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user