diff --git a/src/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.ts b/src/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.ts index 5fdcbe87c..be7580137 100644 --- a/src/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.ts +++ b/src/hooks/anthropic-context-window-limit-recovery/aggressive-truncation-strategy.ts @@ -17,7 +17,7 @@ import { findNearestMessageWithFields, findNearestMessageWithFieldsFromSDK, } from "../../features/hook-message-injector" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" export async function runAggressiveTruncationStrategy(params: { sessionID: string @@ -88,7 +88,8 @@ export async function runAggressiveTruncationStrategy(params: { const launchVariant = previousMessage?.model?.variant const inheritedTools = resolveInheritedPromptTools(params.sessionID, previousMessage?.tools) - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: params.client, sessionID: params.sessionID, source: "auto-compact", diff --git a/src/hooks/atlas/boulder-continuation-injector.ts b/src/hooks/atlas/boulder-continuation-injector.ts index 4535b2470..4451903ae 100644 --- a/src/hooks/atlas/boulder-continuation-injector.ts +++ b/src/hooks/atlas/boulder-continuation-injector.ts @@ -6,7 +6,7 @@ import { import { stripAgentListSortPrefix } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { createInternalAgentContinuationTextPart, resolveInheritedPromptTools } from "../../shared" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" import { HOOK_NAME } from "./hook-name" import { BOULDER_CONTINUATION_PROMPT } from "./system-reminder-templates" import { resolveRecentPromptContextForSession } from "./recent-model-resolver" @@ -92,21 +92,22 @@ export async function injectBoulderContinuation(input: { : undefined const launchVariant = promptContext.model?.variant - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: HOOK_NAME, settleMs: idleSettleMs, input: { - path: { id: sessionID }, - body: { - agent: continuationAgent, - ...(launchModel ? { model: launchModel } : {}), - ...(launchVariant ? { variant: launchVariant } : {}), - ...(inheritedTools ? { tools: inheritedTools } : {}), - parts: [createInternalAgentContinuationTextPart(prompt)], - }, - query: { directory: ctx.directory }, + path: { id: sessionID }, + body: { + agent: continuationAgent, + ...(launchModel ? { model: launchModel } : {}), + ...(launchVariant ? { variant: launchVariant } : {}), + ...(inheritedTools ? { tools: inheritedTools } : {}), + parts: [createInternalAgentContinuationTextPart(prompt)], + }, + query: { directory: ctx.directory }, }, }) if (promptResult.status === "failed") { diff --git a/src/hooks/atlas/idle-event.ts b/src/hooks/atlas/idle-event.ts index 4dce6ab4b..973b079ec 100644 --- a/src/hooks/atlas/idle-event.ts +++ b/src/hooks/atlas/idle-event.ts @@ -20,7 +20,7 @@ import { createInternalAgentContinuationTextPart } from "../../shared" import { getAgentConfigKey } from "../../shared/agent-display-names" import { log } from "../../shared/logger" import { shouldPromptAfterSessionIdle } from "../shared/session-idle-settle" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" import { injectBoulderContinuation } from "./boulder-continuation-injector" import { HOOK_NAME } from "./hook-name" import { resolveActiveBoulderSession } from "./resolve-active-boulder-session" @@ -291,7 +291,8 @@ export async function handleAtlasSessionIdle(input: { return } - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: HOOK_NAME, diff --git a/src/hooks/claude-code-hooks/handlers/session-event-handler.ts b/src/hooks/claude-code-hooks/handlers/session-event-handler.ts index 8326fdb5f..a9ed6deb8 100644 --- a/src/hooks/claude-code-hooks/handlers/session-event-handler.ts +++ b/src/hooks/claude-code-hooks/handlers/session-event-handler.ts @@ -8,7 +8,7 @@ import { clearToolInputCache, stopToolInputCacheCleanup } from "../tool-input-ca import type { PluginConfig } from "../types" import { createInternalAgentTextPart, isHookDisabled, log } from "../../../shared" import { resolveSessionEventID } from "../../../shared/event-session-id" -import { promptAfterSessionIdle } from "../../../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../../../shared/prompt-async-gate" import { clearAllSessionHookState, clearSessionHookState, @@ -109,7 +109,8 @@ export function createSessionEventHandler( }) } else if (stopResult.block && stopResult.injectPrompt) { log("Stop hook returned block with inject_prompt", { sessionID }) - const promptResult = await promptAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "sync", client: ctx.client, sessionID, source: "claude-code-stop-hook:inject-prompt", diff --git a/src/hooks/compaction-context-injector/recovery.ts b/src/hooks/compaction-context-injector/recovery.ts index 91713d7e2..e0cb37c12 100644 --- a/src/hooks/compaction-context-injector/recovery.ts +++ b/src/hooks/compaction-context-injector/recovery.ts @@ -21,7 +21,7 @@ import { import { AGENT_RECOVERY_PROMPT, NO_TEXT_TAIL_THRESHOLD, RECOVERY_COOLDOWN_MS, RECENT_COMPACTION_WINDOW_MS } from "./constants" import type { CompactionContextClient } from "./types" import type { TailMonitorState } from "./tail-monitor" -import { promptAsyncAfterSessionIdle, releasePromptAsyncReservation } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt, releasePromptAsyncReservation } from "../shared/prompt-async-gate" export function createRecoveryLogic( ctx: CompactionContextClient | undefined, @@ -82,7 +82,8 @@ export function createRecoveryLogic( } try { - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: "compaction-context-injector", diff --git a/src/hooks/ralph-loop/continuation-prompt-injector.ts b/src/hooks/ralph-loop/continuation-prompt-injector.ts index fd9c133e2..30b12f8ca 100644 --- a/src/hooks/ralph-loop/continuation-prompt-injector.ts +++ b/src/hooks/ralph-loop/continuation-prompt-injector.ts @@ -10,7 +10,7 @@ import { resolveInheritedPromptTools, } from "../../shared" import { normalizeAgentForPrompt, stripAgentListSortPrefix } from "../../shared/agent-display-names" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" type MessageInfo = { agent?: string @@ -139,7 +139,8 @@ export async function injectContinuationPrompt( let response: unknown try { - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID: options.sessionID, source: "ralph-loop", diff --git a/src/hooks/runtime-fallback/auto-retry.ts b/src/hooks/runtime-fallback/auto-retry.ts index 74d7932b5..5e907b270 100644 --- a/src/hooks/runtime-fallback/auto-retry.ts +++ b/src/hooks/runtime-fallback/auto-retry.ts @@ -12,7 +12,7 @@ import { getLastUserRetryPayload } from "./last-user-retry-parts" import { extractSessionMessages } from "./session-messages" import { resolveRegisteredAgentName } from "../../features/claude-code-session-state" import { - promptAsyncAfterSessionIdle, + dispatchInternalPrompt, releasePromptAsyncReservation, } from "../shared/prompt-async-gate" @@ -157,7 +157,8 @@ export function createAutoRetryHelpers(deps: HookDeps) { sessionAwaitingFallbackResult.add(sessionID) scheduleSessionFallbackTimeout(sessionID, retryAgent) - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: `runtime-fallback:${source}`, diff --git a/src/hooks/session-recovery/recover-tool-result-missing.ts b/src/hooks/session-recovery/recover-tool-result-missing.ts index ec504067e..2f7e30b0f 100644 --- a/src/hooks/session-recovery/recover-tool-result-missing.ts +++ b/src/hooks/session-recovery/recover-tool-result-missing.ts @@ -3,7 +3,7 @@ import type { MessageData, ResumeConfig } from "./types" import { readParts } from "./storage/parts-reader" import { isSqliteBackend } from "../../shared/opencode-storage-detection" import { normalizeSDKResponse } from "../../shared" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" type Client = ReturnType type ToolResultContent = { type: "text"; text: string } @@ -169,7 +169,8 @@ export async function recoverToolResultMissing( return false } - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client, sessionID, source: options?.source ?? "session-recovery-tool-result-missing", diff --git a/src/hooks/session-recovery/recover-unavailable-tool.ts b/src/hooks/session-recovery/recover-unavailable-tool.ts index 2b8cf0702..4d703eeeb 100644 --- a/src/hooks/session-recovery/recover-unavailable-tool.ts +++ b/src/hooks/session-recovery/recover-unavailable-tool.ts @@ -4,7 +4,7 @@ import { readParts } from "./storage" import type { MessageData } from "./types" import { normalizeSDKResponse } from "../../shared" import { isSqliteBackend } from "../../shared/opencode-storage-detection" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" type Client = ReturnType @@ -119,7 +119,8 @@ export async function recoverUnavailableTool( return false } - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client, sessionID, source: "session-recovery-unavailable-tool", diff --git a/src/hooks/session-recovery/resume.ts b/src/hooks/session-recovery/resume.ts index 24fbd05b0..2dd641644 100644 --- a/src/hooks/session-recovery/resume.ts +++ b/src/hooks/session-recovery/resume.ts @@ -4,7 +4,7 @@ import { isRealUserMessage, resolveInheritedPromptTools, } from "../../shared" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" import type { MessageData, ResumeConfig } from "./types" const RECOVERY_RESUME_TEXT = "[session recovered - continuing previous task]" @@ -38,7 +38,8 @@ export async function resumeSession(client: Client, config: ResumeConfig): Promi : undefined const launchVariant = config.model?.variant - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client, sessionID: config.sessionID, source: "session-recovery", diff --git a/src/hooks/team-session-events/team-idle-wake-hint.ts b/src/hooks/team-session-events/team-idle-wake-hint.ts index c9f8fbd95..6c654dd18 100644 --- a/src/hooks/team-session-events/team-idle-wake-hint.ts +++ b/src/hooks/team-session-events/team-idle-wake-hint.ts @@ -9,7 +9,7 @@ import { listUnreadMessages } from "../../features/team-mode/team-mailbox/inbox" import { loadRuntimeState, transitionRuntimeState } from "../../features/team-mode/team-state-store/store" import { resolveSessionEventID } from "../../shared/event-session-id" import { log } from "../../shared/logger" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" type PromptAsyncInput = { path: { id: string } @@ -111,7 +111,8 @@ export function createTeamIdleWakeHint(ctx: TeamIdleWakeHintContext, config: Tea } applyMemberSessionRouting(sessionID, memberEntry) - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: "team-idle-wake-hint", diff --git a/src/hooks/todo-continuation-enforcer/continuation-injection.ts b/src/hooks/todo-continuation-enforcer/continuation-injection.ts index d5a988bf8..8b121d6ca 100644 --- a/src/hooks/todo-continuation-enforcer/continuation-injection.ts +++ b/src/hooks/todo-continuation-enforcer/continuation-injection.ts @@ -22,7 +22,7 @@ import { normalizeAgentForPromptKey, stripAgentListSortPrefix, } from "../../shared/agent-display-names" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" import { CONTINUATION_PROMPT, @@ -187,7 +187,8 @@ ${todoList}` : undefined const launchVariant = model?.variant - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID, source: HOOK_NAME, diff --git a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts index 30cb01cca..3d39dee93 100644 --- a/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts +++ b/src/hooks/unstable-agent-babysitter/unstable-agent-babysitter-hook.ts @@ -12,7 +12,7 @@ import { isUnstableTask, THINKING_SUMMARY_MAX_CHARS, } from "./task-message-analyzer" -import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate" +import { dispatchInternalPrompt } from "../shared/prompt-async-gate" const HOOK_NAME = "unstable-agent-babysitter" const DEFAULT_TIMEOUT_MS = 120000 @@ -216,7 +216,8 @@ export function createUnstableAgentBabysitterHook(ctx: BabysitterContext, option ? { providerID: model.providerID, modelID: model.modelID } : undefined const launchVariant = model?.variant - const promptResult = await promptAsyncAfterSessionIdle({ + const promptResult = await dispatchInternalPrompt({ + mode: "async", client: ctx.client, sessionID: mainSessionID, source: HOOK_NAME, diff --git a/src/shared/prompt-async-gate.ts b/src/shared/prompt-async-gate.ts index df49a5ed1..033fbaff2 100644 --- a/src/shared/prompt-async-gate.ts +++ b/src/shared/prompt-async-gate.ts @@ -38,20 +38,9 @@ type PromptClient = { } } -type InternalPromptDispatchClient = { - session?: { - status?: () => Promise - messages?: (input: { path: { id: string }; query: PromptMessagesQuery }) => Promise - promptAsync?: (input: TInput) => Promise - prompt?: (input: TInput) => Promise - } -} - export type InternalPromptDispatchMode = "async" | "sync" -export type InternalPromptDispatchArgs = { - mode: InternalPromptDispatchMode - client: InternalPromptDispatchClient +type InternalPromptDispatchCommonArgs = { sessionID: string input: TInput source: string @@ -62,6 +51,11 @@ export type InternalPromptDispatchArgs = { checkToolState?: boolean } +export type InternalPromptDispatchArgs = InternalPromptDispatchCommonArgs & ( + | { mode: "async"; client: PromptAsyncClient } + | { mode: "sync"; client: PromptClient } +) + type PromptAsyncReservation = { source: string reservedAt: number @@ -386,34 +380,6 @@ async function dispatchAfterSessionIdle(args: { } } -function getInternalPromptDispatcher( - mode: InternalPromptDispatchMode, - session: InternalPromptDispatchClient["session"], -): { - sessionName: "promptAsync" | "prompt" - dispatch?: (input: TInput) => Promise -} { - 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( args: InternalPromptDispatchArgs, ): Promise { @@ -426,7 +392,24 @@ export async function dispatchInternalPrompt( } = 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) + const sessionName = args.mode === "async" ? "promptAsync" : "prompt" + const dispatch = (() => { + if (args.mode === "async") { + const session = args.client.session + if (typeof session?.promptAsync !== "function") { + return undefined + } + const dispatchPromptAsync = session.promptAsync.bind(session) + return (dispatchInput: TInput) => dispatchPromptAsync(dispatchInput) + } + + const session = args.client.session + if (typeof session?.prompt !== "function") { + return undefined + } + const dispatchPrompt = session.prompt.bind(session) + return (dispatchInput: TInput) => dispatchPrompt(dispatchInput) + })() if (!dispatch) { log(`[prompt-async-gate] ${sessionName} unavailable`, { sessionID, source })