refactor(prompt-async-gate): collapse dispatch into mode-based entrypoint

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>
This commit is contained in:
YeonGyu-Kim
2026-05-17 16:37:59 +09:00
parent b5d24619c8
commit a42f894f88
+99 -63
View File
@@ -38,6 +38,30 @@ type PromptClient<TInput> = {
} }
} }
type InternalPromptDispatchClient<TInput> = {
session?: {
status?: () => Promise<unknown>
messages?: (input: { path: { id: string }; query: PromptMessagesQuery }) => Promise<unknown>
promptAsync?: (input: TInput) => Promise<unknown>
prompt?: (input: TInput) => Promise<unknown>
}
}
export type InternalPromptDispatchMode = "async" | "sync"
export type InternalPromptDispatchArgs<TInput = PromptAsyncInput> = {
mode: InternalPromptDispatchMode
client: InternalPromptDispatchClient<TInput>
sessionID: string
input: TInput
source: string
settleMs?: number
postDispatchHoldMs?: number
dispatchTimeoutMs?: number
checkStatus?: boolean
checkToolState?: boolean
}
type PromptAsyncReservation = { type PromptAsyncReservation = {
source: string source: string
reservedAt: number reservedAt: number
@@ -45,18 +69,20 @@ type PromptAsyncReservation = {
expiresAt?: number expiresAt?: number
} }
declare function setTimeout(callback: () => void, delay?: number): ReturnType<typeof globalThis.setTimeout> declare function setTimeout(callback: () => void, delay?: number): unknown
declare function clearTimeout(timeout: ReturnType<typeof globalThis.setTimeout>): void declare function clearTimeout(timeout: unknown): void
let promptGateMessagesFetchTimeoutMsForTesting: number | undefined let promptGateMessagesFetchTimeoutMsForTesting: number | undefined
export type PromptAsyncGateResult = export type InternalPromptDispatchResult =
| { status: "dispatched"; response: unknown } | { status: "dispatched"; response: unknown }
| { status: "active" } | { status: "active" }
| { status: "reserved"; reservedBy: string } | { status: "reserved"; reservedBy: string }
| { status: "unavailable" } | { status: "unavailable" }
| { status: "failed"; error: unknown } | { status: "failed"; error: unknown }
export type PromptAsyncGateResult = InternalPromptDispatchResult
type PromptAsyncReservationReleaseOptions = { type PromptAsyncReservationReleaseOptions = {
reservedBy?: string | readonly string[] reservedBy?: string | readonly string[]
reservedByPrefix?: string | readonly string[] reservedByPrefix?: string | readonly string[]
@@ -121,7 +147,7 @@ async function withDispatchTimeout<T>(
return operation return operation
} }
let timeoutID: ReturnType<typeof globalThis.setTimeout> | undefined let timeoutID: unknown
const timeoutPromise = new Promise<never>((_, reject) => { const timeoutPromise = new Promise<never>((_, reject) => {
timeoutID = setTimeout(() => { timeoutID = setTimeout(() => {
reject(new Error(`${operationName} timed out after ${dispatchTimeoutMs}ms`)) reject(new Error(`${operationName} timed out after ${dispatchTimeoutMs}ms`))
@@ -260,7 +286,7 @@ async function dispatchAfterSessionIdle<TInput>(args: {
checkStatus: boolean checkStatus: boolean
checkToolState: boolean checkToolState: boolean
dispatch: (input: TInput) => Promise<unknown> dispatch: (input: TInput) => Promise<unknown>
}): Promise<PromptAsyncGateResult> { }): Promise<InternalPromptDispatchResult> {
const { const {
sessionName, sessionName,
client, client,
@@ -360,6 +386,68 @@ async function dispatchAfterSessionIdle<TInput>(args: {
} }
} }
function getInternalPromptDispatcher<TInput>(
mode: InternalPromptDispatchMode,
session: InternalPromptDispatchClient<TInput>["session"],
): {
sessionName: "promptAsync" | "prompt"
dispatch?: (input: TInput) => Promise<unknown>
} {
if (mode === "async") {
if (typeof session?.promptAsync !== "function") {
return { sessionName: "promptAsync" }
}
const dispatchPromptAsync = session.promptAsync.bind(session)
return {
sessionName: "promptAsync",
dispatch: (input) => dispatchPromptAsync(input),
}
}
if (typeof session?.prompt !== "function") {
return { sessionName: "prompt" }
}
const dispatchPrompt = session.prompt.bind(session)
return {
sessionName: "prompt",
dispatch: (input) => dispatchPrompt(input),
}
}
export async function dispatchInternalPrompt<TInput = PromptAsyncInput>(
args: InternalPromptDispatchArgs<TInput>,
): Promise<InternalPromptDispatchResult> {
const {
client,
sessionID,
input,
source,
settleMs = DEFAULT_SESSION_IDLE_SETTLE_MS,
} = args
const postDispatchHoldMs = args.postDispatchHoldMs ?? DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS
const dispatchTimeoutMs = args.dispatchTimeoutMs ?? DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS
const { sessionName, dispatch } = getInternalPromptDispatcher(args.mode, client.session)
if (!dispatch) {
log(`[prompt-async-gate] ${sessionName} unavailable`, { sessionID, source })
return { status: "unavailable" }
}
return dispatchAfterSessionIdle({
sessionName,
client,
sessionID,
input,
source,
settleMs,
postDispatchHoldMs,
dispatchTimeoutMs,
checkStatus: args.checkStatus !== false,
checkToolState: args.checkToolState !== false,
dispatch,
})
}
export async function promptAsyncAfterSessionIdle<TInput = PromptAsyncInput>(args: { export async function promptAsyncAfterSessionIdle<TInput = PromptAsyncInput>(args: {
client: PromptAsyncClient<TInput> client: PromptAsyncClient<TInput>
sessionID: string sessionID: string
@@ -371,35 +459,9 @@ export async function promptAsyncAfterSessionIdle<TInput = PromptAsyncInput>(arg
checkStatus?: boolean checkStatus?: boolean
checkToolState?: boolean checkToolState?: boolean
}): Promise<PromptAsyncGateResult> { }): Promise<PromptAsyncGateResult> {
const { return dispatchInternalPrompt({
client, ...args,
sessionID, mode: "async",
input,
source,
settleMs = DEFAULT_SESSION_IDLE_SETTLE_MS,
} = args
const postDispatchHoldMs = args.postDispatchHoldMs ?? DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS
const dispatchTimeoutMs = args.dispatchTimeoutMs ?? DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS
const session = client.session
if (typeof session?.promptAsync !== "function") {
log("[prompt-async-gate] promptAsync unavailable", { sessionID, source })
return { status: "unavailable" }
}
const dispatchPromptAsync = session.promptAsync.bind(session)
return dispatchAfterSessionIdle({
sessionName: "promptAsync",
client,
sessionID,
input,
source,
settleMs,
postDispatchHoldMs,
dispatchTimeoutMs,
checkStatus: args.checkStatus !== false,
checkToolState: args.checkToolState !== false,
dispatch: (dispatchInput) => dispatchPromptAsync(dispatchInput),
}) })
} }
@@ -414,35 +476,9 @@ export async function promptAfterSessionIdle<TInput = PromptAsyncInput>(args: {
checkStatus?: boolean checkStatus?: boolean
checkToolState?: boolean checkToolState?: boolean
}): Promise<PromptAsyncGateResult> { }): Promise<PromptAsyncGateResult> {
const { return dispatchInternalPrompt({
client, ...args,
sessionID, mode: "sync",
input,
source,
settleMs = DEFAULT_SESSION_IDLE_SETTLE_MS,
} = args
const postDispatchHoldMs = args.postDispatchHoldMs ?? DEFAULT_PROMPT_ASYNC_POST_DISPATCH_HOLD_MS
const dispatchTimeoutMs = args.dispatchTimeoutMs ?? DEFAULT_PROMPT_DISPATCH_TIMEOUT_MS
const session = client.session
if (typeof session?.prompt !== "function") {
log("[prompt-async-gate] prompt unavailable", { sessionID, source })
return { status: "unavailable" }
}
const dispatchPrompt = session.prompt.bind(session)
return dispatchAfterSessionIdle({
sessionName: "prompt",
client,
sessionID,
input,
source,
settleMs,
postDispatchHoldMs,
dispatchTimeoutMs,
checkStatus: args.checkStatus !== false,
checkToolState: args.checkToolState !== false,
dispatch: (dispatchInput) => dispatchPrompt(dispatchInput),
}) })
} }