diff --git a/src/resources/extensions/remote-questions/manager.ts b/src/resources/extensions/remote-questions/manager.ts index e9dd53cb7..a1e1d10f7 100644 --- a/src/resources/extensions/remote-questions/manager.ts +++ b/src/resources/extensions/remote-questions/manager.ts @@ -80,11 +80,9 @@ export async function tryRemoteQuestions( markPromptAnswered(prompt.id, answer); // Best-effort acknowledgement gives remote users a visible receipt signal. - if (dispatch.ref) { - try { - await adapter.acknowledgeAnswer?.(dispatch.ref); - } catch { /* best-effort */ } - } + try { + await adapter.acknowledgeAnswer?.(dispatch.ref); + } catch { /* best-effort */ } return { content: [{ type: "text", text: JSON.stringify({ answers: formatForTool(answer) }) }], diff --git a/src/resources/extensions/remote-questions/store.ts b/src/resources/extensions/remote-questions/store.ts index 226ac8996..83c80dcd0 100644 --- a/src/resources/extensions/remote-questions/store.ts +++ b/src/resources/extensions/remote-questions/store.ts @@ -51,11 +51,15 @@ export function updatePromptRecord( ): RemotePromptRecord | null { const current = readPromptRecord(id); if (!current) return null; - const next: RemotePromptRecord = { + const merged = { ...current, ...updates, updatedAt: Date.now(), }; + // After spreading, the merged object satisfies one of the union members + // but TypeScript can't prove it statically. The invariant is maintained + // by callers: once `ref` is set via markPromptDispatched it is never removed. + const next = merged as RemotePromptRecord; writePromptRecord(next); return next; } diff --git a/src/resources/extensions/remote-questions/types.ts b/src/resources/extensions/remote-questions/types.ts index dfa4752b2..6f13876fd 100644 --- a/src/resources/extensions/remote-questions/types.ts +++ b/src/resources/extensions/remote-questions/types.ts @@ -44,17 +44,16 @@ export interface RemoteAnswer { export type RemotePromptStatus = "pending" | "answered" | "timed_out" | "failed" | "cancelled"; -export interface RemotePromptRecord { +/** Shared fields present on every prompt record regardless of dispatch state. */ +interface RemotePromptRecordBase { version: 1; id: string; createdAt: number; updatedAt: number; - status: RemotePromptStatus; channel: RemoteChannel; timeoutAt: number; pollIntervalMs: number; questions: RemoteQuestion[]; - ref?: RemotePromptRef; response?: RemoteAnswer; lastPollAt?: number; lastError?: string; @@ -63,6 +62,30 @@ export interface RemotePromptRecord { }; } +/** Record before the prompt has been dispatched to a channel. */ +export interface PendingPromptRecord extends RemotePromptRecordBase { + status: "pending"; + ref?: undefined; +} + +/** Record after the prompt has been dispatched (ref is always present). */ +export interface DispatchedPromptRecord extends RemotePromptRecordBase { + status: RemotePromptStatus; + ref: RemotePromptRef; +} + +/** + * A prompt record is either pre-dispatch (no ref) or post-dispatch (ref required). + * + * Narrow via `record.ref`: + * ```ts + * if (record.ref) { + * // DispatchedPromptRecord — ref is RemotePromptRef + * } + * ``` + */ +export type RemotePromptRecord = PendingPromptRecord | DispatchedPromptRecord; + export interface RemoteDispatchResult { ref: RemotePromptRef; }