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.
|
- **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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user