From bd16093909a78c341e4b863d77a6726379310dd3 Mon Sep 17 00:00:00 2001 From: Zoz Date: Sun, 27 Sep 2026 20:14:22 +0200 Subject: [PATCH 1/2] feat: show a persistent "waiting for your answer" marker on sessions with a pending ask_user A session blocked on an ask_user question was indistinguishable in the sidebar from any other session with unread activity: the amber unread dot disappears as soon as the session is opened, even though the agent is still waiting for an answer. Only tool confirmations had a dedicated marker. - server: ask.ts tracks per-session awaiting state and notifies listeners on transitions (question asked / answered / cancelled) - server: broadcasts `session.awaiting_answer` to all clients, and exposes `awaitingAnswer` on session summaries (/api/sessions, /api/sessions/home) so the marker survives a reload - web: sidebar shows a pulsing amber marker "Waiting for your answer" that takes precedence over running/unread; homepage status dot treats a pending question like a pending confirmation --- src/server/index.ts | 18 ++++- src/server/tools/ask.test.ts | 46 +++++++++++ src/server/tools/ask.ts | 79 ++++++++++++++----- src/shared/protocol.ts | 5 ++ src/shared/types.ts | 1 + web/src/components/HomePage.tsx | 21 +++-- web/src/components/layout/Sidebar.test.tsx | 16 ++++ web/src/components/layout/Sidebar.tsx | 10 +++ web/src/stores/session/messageHandler.test.ts | 25 ++++++ web/src/stores/session/messageHandler.ts | 7 ++ 10 files changed, 194 insertions(+), 34 deletions(-) diff --git a/src/server/index.ts b/src/server/index.ts index 72bec1967..01757d786 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -13,7 +13,12 @@ import { initDatabase } from './db/index.js' import { getProject, deleteProject } from './db/projects.js' import { initEventStore, getEventStore, combineEventsWithSnapshot } from './events/index.js' import { buildMessagesFromStoredEvents } from './events/folding.js' -import { provideAnswer, getPendingQuestionsForSession } from './tools/ask.js' +import { + provideAnswer, + getPendingQuestionsForSession, + isSessionAwaitingAnswer, + onAwaitingAnswerChange, +} from './tools/ask.js' import { providePathConfirmation, getPendingConfirmationsBySession } from './tools/path-security.js' import './llm/proxy.js' import { detectModel, getLlmStatus, getBackendDisplayName } from './llm/index.js' @@ -810,6 +815,7 @@ export async function createServerHandle(config: Config): Promise const sessionsWithPrompts = sessions.map((session) => ({ ...session, + awaitingAnswer: isSessionAwaitingAnswer(session.id), recentUserPrompts: getRecentUserPromptsForSession(session.id, 10), })) @@ -842,7 +848,11 @@ export async function createServerHandle(config: Config): Promise * Registered before /api/sessions/:id so 'home' is not treated as an id. */ app.get('/api/sessions/home', (_req, res) => { - res.json({ sessions: sessionManager.listHomeSessions() }) + res.json({ + sessions: sessionManager + .listHomeSessions() + .map((session) => ({ ...session, awaitingAnswer: isSessionAwaitingAnswer(session.id) })), + }) }) /** @@ -3628,6 +3638,10 @@ export async function createServerHandle(config: Config): Promise ) const wss = wssExports.wss + onAwaitingAnswerChange((sessionId, awaitingAnswer) => { + wssExports.broadcastAll({ type: 'session.awaiting_answer', sessionId, payload: { awaitingAnswer } }) + }) + // Point the plugin host at the live WebSocket broadcaster now that it exists. pluginHost.setBroadcaster((message) => wssExports.broadcastAll(message)) diff --git a/src/server/tools/ask.test.ts b/src/server/tools/ask.test.ts index 3927fc6d0..48f2b86b3 100644 --- a/src/server/tools/ask.test.ts +++ b/src/server/tools/ask.test.ts @@ -7,8 +7,54 @@ import { hasPendingQuestion, provideAnswer, getPendingQuestionsForSession, + isSessionAwaitingAnswer, + onAwaitingAnswerChange, } from './ask.js' +async function ask(sessionId: string, toolCallId: string): Promise { + try { + await askUserTool.execute( + { question: 'Q?' }, + { workdir: '/tmp/project', sessionId, sessionManager: {} as never, toolCallId }, + ) + } catch { + // AskUserInterrupt expected + } +} + +describe('awaiting-answer notifications', () => { + it('notifies only on transitions of a session awaiting state', async () => { + const changes: Array<[string, boolean]> = [] + const unsubscribe = onAwaitingAnswerChange((sessionId, awaiting) => changes.push([sessionId, awaiting])) + + await ask('s-await', 'c-await-1') + await ask('s-await', 'c-await-2') + expect(isSessionAwaitingAnswer('s-await')).toBe(true) + provideAnswer('c-await-1', 'x') + expect(isSessionAwaitingAnswer('s-await')).toBe(true) + cancelQuestion('c-await-2', 'stop') + expect(isSessionAwaitingAnswer('s-await')).toBe(false) + + await ask('s-await', 'c-await-3') + cancelQuestionsForSession('s-await', 'deleted') + + unsubscribe() + await ask('s-await', 'c-await-4') + provideAnswer('c-await-4', 'x') + + expect(changes).toEqual([ + ['s-await', true], + ['s-await', false], + ['s-await', true], + ['s-await', false], + ]) + }) + + it('reports false for a session with no pending question', () => { + expect(isSessionAwaitingAnswer('never-asked')).toBe(false) + }) +}) + describe('ask_user tool', () => { it('throws an AskUserInterrupt and tracks the pending question', async () => { let interrupt: AskUserInterrupt | null = null diff --git a/src/server/tools/ask.ts b/src/server/tools/ask.ts index f61bad900..df3007313 100644 --- a/src/server/tools/ask.ts +++ b/src/server/tools/ask.ts @@ -18,6 +18,39 @@ const pendingQuestions = new Map< } >() +type AwaitingAnswerListener = (sessionId: string, awaiting: boolean) => void +const awaitingAnswerListeners = new Set() + +export function isSessionAwaitingAnswer(sessionId: string): boolean { + for (const pending of pendingQuestions.values()) { + if (pending.sessionId === sessionId) return true + } + return false +} + +export function onAwaitingAnswerChange(listener: AwaitingAnswerListener): () => void { + awaitingAnswerListeners.add(listener) + return () => { + awaitingAnswerListeners.delete(listener) + } +} + +function trackAwaitingTransition(sessionId: string, mutate: () => T): T { + const before = isSessionAwaitingAnswer(sessionId) + const result = mutate() + const after = isSessionAwaitingAnswer(sessionId) + if (before !== after) { + for (const listener of awaitingAnswerListeners) { + try { + listener(sessionId, after) + } catch { + // a failing listener must not break question handling + } + } + } + return result +} + export const askUserTool: Tool = { name: 'ask_user', definition: { @@ -68,15 +101,17 @@ export const askUserTool: Tool = { const deferred = createDeferred() void deferred.promise.catch(() => {}) - pendingQuestions.set(callId, { - promise: deferred.promise, - resolve: deferred.resolve, - reject: deferred.reject, - sessionId: context.sessionId, - question, - type, - options, - }) + trackAwaitingTransition(context.sessionId, () => + pendingQuestions.set(callId, { + promise: deferred.promise, + resolve: deferred.resolve, + reject: deferred.reject, + sessionId: context.sessionId, + question, + type, + options, + }), + ) throw new AskUserInterrupt(callId, question, type, options) }, @@ -101,7 +136,7 @@ export function provideAnswer(callId: string, answer: string, skip?: boolean): b } pending.resolve(skip ? '[user skipped]' : answer) - pendingQuestions.delete(callId) + trackAwaitingTransition(pending.sessionId, () => pendingQuestions.delete(callId)) return true } @@ -112,24 +147,26 @@ export function cancelQuestion(callId: string, reason: string): boolean { } pending.reject(new Error(reason)) - pendingQuestions.delete(callId) + trackAwaitingTransition(pending.sessionId, () => pendingQuestions.delete(callId)) return true } export function cancelQuestionsForSession(sessionId: string, reason: string): number { - let cancelledCount = 0 + return trackAwaitingTransition(sessionId, () => { + let cancelledCount = 0 - for (const [callId, pending] of pendingQuestions.entries()) { - if (pending.sessionId !== sessionId) { - continue - } + for (const [callId, pending] of pendingQuestions.entries()) { + if (pending.sessionId !== sessionId) { + continue + } - pending.reject(new Error(reason)) - pendingQuestions.delete(callId) - cancelledCount += 1 - } + pending.reject(new Error(reason)) + pendingQuestions.delete(callId) + cancelledCount += 1 + } - return cancelledCount + return cancelledCount + }) } export function hasPendingQuestion(callId: string): boolean { diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index 762ee0894..67dc5c678 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -89,6 +89,7 @@ export type ServerMessageType = | 'session.deleted' | 'session.deletedAll' | 'session.running' // Real-time running state change + | 'session.awaiting_answer' // An ask_user question is pending (or resolved) in this session (broadcast to all) | 'session.pause' // Cooperative pause state change (none/pending/paused/resuming) | 'session.name_generated' // Session name was auto-generated | 'session.confirmation_pending' // Path confirmation waiting in another session (broadcast to all) @@ -242,6 +243,10 @@ export interface SessionRunningPayload { isRunning: boolean } +export interface SessionAwaitingAnswerPayload { + awaitingAnswer: boolean +} + export interface SessionPausePayload { pauseState: import('./types.js').PauseState } diff --git a/src/shared/types.ts b/src/shared/types.ts index f64005f45..1428c46d4 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -163,6 +163,7 @@ export interface SessionSummary { mode: SessionMode phase: SessionPhase // Current workflow phase isRunning: boolean + awaitingAnswer?: boolean // An ask_user question is pending an answer from the user isFavorite: boolean // Whether the session is favorited for pinning providerId?: string | null // Per-session provider override providerModel?: string | null // Per-session model override diff --git a/web/src/components/HomePage.tsx b/web/src/components/HomePage.tsx index 7cf414ddd..ee0e865ca 100644 --- a/web/src/components/HomePage.tsx +++ b/web/src/components/HomePage.tsx @@ -116,26 +116,25 @@ function SessionProjectMenu({ /** Color-coded activity dot for a session row on the homepage list. */ function SessionStatusDot({ session, waiting }: { session: SessionSummary; waiting: boolean }) { const t = useT() - if (session.isRunning) { + if (waiting) { return ( ) } - if (waiting || session.phase === 'blocked') { + if (session.isRunning) { return ( ) } + if (session.phase === 'blocked') { + return + } return } @@ -375,7 +374,7 @@ export function HomePage() { const project = projectById.get(session.projectId) const displayTitle = session.title ?? session.id.slice(0, 8) const matchType = matchTypes?.get(session.id) - const waiting = sessionsWithPendingConfirmations.includes(session.id) + const waiting = sessionsWithPendingConfirmations.includes(session.id) || session.awaitingAnswer === true const rowClass = 'flex items-center gap-3 px-3 md:px-4 py-2.5 transition-colors' + (project ? ' hover:bg-bg-tertiary/50 cursor-pointer' : ' cursor-default') diff --git a/web/src/components/layout/Sidebar.test.tsx b/web/src/components/layout/Sidebar.test.tsx index 04a3dcb4e..cb2250b06 100644 --- a/web/src/components/layout/Sidebar.test.tsx +++ b/web/src/components/layout/Sidebar.test.tsx @@ -143,6 +143,22 @@ describe('Sidebar', () => { expect(html).not.toContain('bg-black/50') }) + it('shows a waiting-for-answer marker on a background session awaiting an answer, above the unread dot', () => { + sessionStoreStateRef.current = { + ...sessionStoreState, + sessions: sessionStoreState.sessions.map((s) => (s.id === 'session-3' ? { ...s, awaitingAnswer: true } : s)), + } + const html = renderToStaticMarkup() + expect(html).toContain('Waiting for your answer') + expect(html).not.toContain('Unread activity') + }) + + it('shows the waiting-for-answer marker on the active session while its question is pending', () => { + sessionStoreStateRef.current = { ...sessionStoreState, pendingQuestions: [{ callId: 'c1' }] } as never + const html = renderToStaticMarkup() + expect(html.match(/Waiting for your answer/g)?.length).toBe(2) + }) + it('collapses the inline sidebar to zero width when closed', () => { const html = renderToStaticMarkup() expect(html).toContain('w-0') diff --git a/web/src/components/layout/Sidebar.tsx b/web/src/components/layout/Sidebar.tsx index 15b6c1c31..68ede6a45 100644 --- a/web/src/components/layout/Sidebar.tsx +++ b/web/src/components/layout/Sidebar.tsx @@ -67,6 +67,7 @@ export function Sidebar({ projectId, isOpen = true, overlay = false, onClose }: const sessionsPaginationLoading = useSessionStore((state) => state.sessionsPaginationLoading) const sessionsWithPendingConfirmations = useSessionStore((state) => state.sessionsWithPendingConfirmations) const pendingPathConfirmations = useSessionStore((state) => state.pendingPathConfirmations) + const currentPendingQuestionsCount = useSessionStore((state) => state.pendingQuestions?.length ?? 0) const toggleFavorite = useSessionStore((state) => state.toggleFavorite) const currentProject = useCurrentProject() @@ -492,6 +493,7 @@ export function Sidebar({ projectId, isOpen = true, overlay = false, onClose }: projectId, sessionsWithPendingConfirmations, pendingPathConfirmations, + currentPendingQuestionsCount, searchQuery, focusedIndex, t, @@ -546,6 +548,7 @@ function renderSessionList( projectId: string, sessionsWithPendingConfirmations: string[], pendingPathConfirmations: PendingPathConfirmation[], + currentPendingQuestionsCount: number, searchQuery: string, focusedIndex: number, t: ( @@ -564,6 +567,7 @@ function renderSessionList( const isFavorite = session.isFavorite const hasPendingConfirmation = sessionsWithPendingConfirmations.includes(session.id) || (isActive && pendingPathConfirmations.length > 0) + const isAwaitingAnswer = isActive ? currentPendingQuestionsCount > 0 : session.awaitingAnswer === true return (
+ ) : isAwaitingAnswer ? ( + ) : isRunning ? ( ) : hasUnread && !isActive ? ( diff --git a/web/src/stores/session/messageHandler.test.ts b/web/src/stores/session/messageHandler.test.ts index a8c433f2c..62cf42751 100644 --- a/web/src/stores/session/messageHandler.test.ts +++ b/web/src/stores/session/messageHandler.test.ts @@ -692,6 +692,31 @@ describe('chat.stats handler', () => { expect(useSessionStore.getState().liveTurnStats).toBeNull() }) + it('tracks awaitingAnswer on a background session summary', async () => { + const useSessionStore = await loadSessionStore() + + useSessionStore.setState((state) => ({ + ...state, + sessions: [{ id: 'bg-1', isRunning: false } as any, { id: 'bg-2', isRunning: false } as any], + })) + + useSessionStore.getState().handleServerMessage({ + type: 'session.awaiting_answer', + sessionId: 'bg-1', + payload: { awaitingAnswer: true }, + }) + const afterTrue = useSessionStore.getState().sessions + expect(afterTrue.find((s) => s.id === 'bg-1')?.awaitingAnswer).toBe(true) + expect(afterTrue.find((s) => s.id === 'bg-2')?.awaitingAnswer).toBeUndefined() + + useSessionStore.getState().handleServerMessage({ + type: 'session.awaiting_answer', + sessionId: 'bg-1', + payload: { awaitingAnswer: false }, + }) + expect(useSessionStore.getState().sessions.find((s) => s.id === 'bg-1')?.awaitingAnswer).toBe(false) + }) + it('clears stale live turn stats when a new turn starts running', async () => { const useSessionStore = await loadSessionStore() diff --git a/web/src/stores/session/messageHandler.ts b/web/src/stores/session/messageHandler.ts index d05da18f2..f75c156c3 100644 --- a/web/src/stores/session/messageHandler.ts +++ b/web/src/stores/session/messageHandler.ts @@ -5,6 +5,7 @@ import type { GitDiffFile, SessionListPayload, SessionRunningPayload, + SessionAwaitingAnswerPayload, SessionPausePayload, ChatAskUserPayload, ChatDeltaPayload, @@ -473,6 +474,12 @@ export function handleServerMessage( break } + case 'session.awaiting_answer': { + const payload = message.payload as SessionAwaitingAnswerPayload + updateSessionField(message, set, get, (s) => ({ ...s, awaitingAnswer: payload.awaitingAnswer })) + break + } + case 'session.pause': { const payload = message.payload as SessionPausePayload const eventSessionId = message.sessionId From 8540ceca42f9516bdf29a780b2afb5d2bb1ce380 Mon Sep 17 00:00:00 2001 From: Zoz Date: Wed, 30 Sep 2026 23:02:39 +0200 Subject: [PATCH 2/2] refactor(ask): O(1) awaiting lookup and cancel pending questions on session deletion Addresses review on #385. - ask.ts: keep a per-session count of pending questions next to pendingQuestions; isSessionAwaitingAnswer() is now a Map lookup instead of a scan of every pending question. Both maps are only mutated through addPendingQuestion/removePendingQuestion, and re-registering the same call id does not inflate the count. - SessionManager.deleteSession() now cancels the session's pending ask_user questions and path confirmations. This covers deleteAllSessions() (DELETE /api/projects/:projectId/sessions), which previously left them in memory with promises that never settle. Project deletion (REST and MCP) bypasses SessionManager through the ON DELETE CASCADE; that path is handled separately in #398. --- src/server/session/manager.test.ts | 40 ++++++++++++++++++++++ src/server/session/manager.ts | 5 +++ src/server/tools/ask.test.ts | 18 ++++++++++ src/server/tools/ask.ts | 53 +++++++++++++++++++----------- 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/src/server/session/manager.test.ts b/src/server/session/manager.test.ts index 2a8222957..d4e2a8988 100644 --- a/src/server/session/manager.test.ts +++ b/src/server/session/manager.test.ts @@ -205,6 +205,46 @@ describe('SessionManager', () => { expect(getSessionDisabledServers(session.id)).toEqual([]) }) + it('cancels pending ask_user questions when a session is deleted', async () => { + const { askUserTool, getPendingQuestionsForSession, isSessionAwaitingAnswer } = await import('../tools/ask.js') + const session = manager.createSession(projectId) + const other = manager.createSession(projectId) + for (const [id, callId] of [ + [session.id, 'del-q-1'], + [other.id, 'del-q-2'], + ] as const) { + await askUserTool + .execute({ question: 'Q?' }, { workdir, sessionId: id, sessionManager: {} as never, toolCallId: callId }) + .catch(() => {}) + } + + manager.deleteSession(session.id) + + expect(getPendingQuestionsForSession(session.id)).toEqual([]) + expect(isSessionAwaitingAnswer(session.id)).toBe(false) + expect(isSessionAwaitingAnswer(other.id)).toBe(true) + }) + + it('cancels pending ask_user questions of every session when all project sessions are deleted', async () => { + const { askUserTool, getPendingQuestionsForSession, isSessionAwaitingAnswer } = await import('../tools/ask.js') + const sessions = [manager.createSession(projectId), manager.createSession(projectId)] + for (const [index, s] of sessions.entries()) { + await askUserTool + .execute( + { question: 'Q?' }, + { workdir, sessionId: s.id, sessionManager: {} as never, toolCallId: `del-all-q-${index}` }, + ) + .catch(() => {}) + } + + manager.deleteAllSessions(projectId, workdir) + + for (const s of sessions) { + expect(getPendingQuestionsForSession(s.id)).toEqual([]) + expect(isSessionAwaitingAnswer(s.id)).toBe(false) + } + }) + it('uses database is_running as source of truth for session state', () => { const session = manager.createSession(projectId) diff --git a/src/server/session/manager.ts b/src/server/session/manager.ts index 8bf059b23..72028aab7 100644 --- a/src/server/session/manager.ts +++ b/src/server/session/manager.ts @@ -72,6 +72,8 @@ import { logger } from '../utils/logger.js' import { EventEmitter, type Unsubscribe } from '../utils/async.js' import { getLspManager as getOrCreateLspManager, shutdownLspManager, type LspManager } from '../lsp/index.js' import { devServerManager } from '../dev-server/manager.js' +import { cancelQuestionsForSession } from '../tools/ask.js' +import { cancelPathConfirmationsForSession } from '../tools/path-security.js' import { resolveLLMClientForAgent, getAgentModelOverride } from '../agents/model-overrides.js' import { parseDefaultModelSelection } from '../provider-manager.js' import { getEventStore } from '../events/store.js' @@ -750,6 +752,9 @@ export class SessionManager { // Clear message queue to prevent memory leak this.messageQueues.delete(id) + cancelQuestionsForSession(id, 'Session deleted') + cancelPathConfirmationsForSession(id, 'Session deleted') + // Release any blocked pause gate and drop the pause state this.clearPauseState(id) diff --git a/src/server/tools/ask.test.ts b/src/server/tools/ask.test.ts index 48f2b86b3..e2d9b9dfd 100644 --- a/src/server/tools/ask.test.ts +++ b/src/server/tools/ask.test.ts @@ -53,6 +53,24 @@ describe('awaiting-answer notifications', () => { it('reports false for a session with no pending question', () => { expect(isSessionAwaitingAnswer('never-asked')).toBe(false) }) + + it('keeps the per-session count exact when a call id is asked twice', async () => { + await ask('s-dup', 'c-dup-1') + await ask('s-dup', 'c-dup-1') + expect(getPendingQuestionsForSession('s-dup')).toHaveLength(1) + provideAnswer('c-dup-1', 'x') + expect(isSessionAwaitingAnswer('s-dup')).toBe(false) + }) + + it('is independent across sessions', async () => { + await ask('s-a', 'c-a-1') + await ask('s-b', 'c-b-1') + cancelQuestionsForSession('s-a', 'deleted') + expect(isSessionAwaitingAnswer('s-a')).toBe(false) + expect(isSessionAwaitingAnswer('s-b')).toBe(true) + provideAnswer('c-b-1', 'x') + expect(isSessionAwaitingAnswer('s-b')).toBe(false) + }) }) describe('ask_user tool', () => { diff --git a/src/server/tools/ask.ts b/src/server/tools/ask.ts index df3007313..9d17cad57 100644 --- a/src/server/tools/ask.ts +++ b/src/server/tools/ask.ts @@ -4,28 +4,43 @@ import type { PendingQuestionPayload, ChoiceOption } from '../../shared/protocol import { normalizeAskOptions } from '../../shared/ask-options.js' import { createDeferred } from '../utils/async.js' +interface PendingQuestion { + promise: Promise + resolve: (answer: string) => void + reject: (error: Error) => void + sessionId: string + question: string + type: 'text' | 'confirm' | 'choice' + options: ChoiceOption[] | undefined +} + // Store pending questions by call ID -const pendingQuestions = new Map< - string, - { - promise: Promise - resolve: (answer: string) => void - reject: (error: Error) => void - sessionId: string - question: string - type: 'text' | 'confirm' | 'choice' - options: ChoiceOption[] | undefined +const pendingQuestions = new Map() +const pendingCountBySession = new Map() + +function addPendingQuestion(callId: string, pending: PendingQuestion): void { + removePendingQuestion(callId) + pendingQuestions.set(callId, pending) + pendingCountBySession.set(pending.sessionId, (pendingCountBySession.get(pending.sessionId) ?? 0) + 1) +} + +function removePendingQuestion(callId: string): void { + const pending = pendingQuestions.get(callId) + if (!pending) return + pendingQuestions.delete(callId) + const remaining = (pendingCountBySession.get(pending.sessionId) ?? 1) - 1 + if (remaining > 0) { + pendingCountBySession.set(pending.sessionId, remaining) + } else { + pendingCountBySession.delete(pending.sessionId) } ->() +} type AwaitingAnswerListener = (sessionId: string, awaiting: boolean) => void const awaitingAnswerListeners = new Set() export function isSessionAwaitingAnswer(sessionId: string): boolean { - for (const pending of pendingQuestions.values()) { - if (pending.sessionId === sessionId) return true - } - return false + return pendingCountBySession.has(sessionId) } export function onAwaitingAnswerChange(listener: AwaitingAnswerListener): () => void { @@ -102,7 +117,7 @@ export const askUserTool: Tool = { void deferred.promise.catch(() => {}) trackAwaitingTransition(context.sessionId, () => - pendingQuestions.set(callId, { + addPendingQuestion(callId, { promise: deferred.promise, resolve: deferred.resolve, reject: deferred.reject, @@ -136,7 +151,7 @@ export function provideAnswer(callId: string, answer: string, skip?: boolean): b } pending.resolve(skip ? '[user skipped]' : answer) - trackAwaitingTransition(pending.sessionId, () => pendingQuestions.delete(callId)) + trackAwaitingTransition(pending.sessionId, () => removePendingQuestion(callId)) return true } @@ -147,7 +162,7 @@ export function cancelQuestion(callId: string, reason: string): boolean { } pending.reject(new Error(reason)) - trackAwaitingTransition(pending.sessionId, () => pendingQuestions.delete(callId)) + trackAwaitingTransition(pending.sessionId, () => removePendingQuestion(callId)) return true } @@ -161,7 +176,7 @@ export function cancelQuestionsForSession(sessionId: string, reason: string): nu } pending.reject(new Error(reason)) - pendingQuestions.delete(callId) + removePendingQuestion(callId) cancelledCount += 1 }