@@ -54,6 +54,52 @@ function applyToolDisplay(
5454 if ( displayTitle ) toolCall . displayTitle = displayTitle
5555}
5656
57+ /**
58+ * Upsert the durable `async_tool_calls` row before the authoritative tool-call
59+ * SSE frame is forwarded to the client, so `/api/copilot/confirm` can never
60+ * race ahead of the row that identifies the call. This is the sole
61+ * persistence point for client-executable tools; gating mirrors the
62+ * client-wait branch in `dispatchToolExecution`.
63+ */
64+ export async function prePersistClientExecutableToolCall (
65+ event : StreamEvent ,
66+ context : StreamingContext
67+ ) : Promise < void > {
68+ if ( event . type !== 'tool' ) return
69+ if ( ! isToolCallStreamEvent ( event ) ) return
70+
71+ const data = event . payload
72+ const isGenerating = data . status === TOOL_CALL_STATUS . generating
73+ const isPartial = data . partial === true || isGenerating
74+ if ( isPartial ) return
75+
76+ const ui = getToolCallUI ( data )
77+ if ( ! ui . clientExecutable ) return
78+
79+ const catalogEntry = getToolEntry ( data . toolName )
80+ const isInternal = ui . internal === true || catalogEntry ?. internal === true
81+ if ( isInternal ) return
82+
83+ const delegateWorkflowRunToClient = isWorkflowToolName ( data . toolName )
84+ if ( isSimExecuted ( data . toolName ) && ! delegateWorkflowRunToClient ) return
85+
86+ if ( ! context . runId ) return
87+
88+ await upsertAsyncToolCall ( {
89+ runId : context . runId ,
90+ toolCallId : data . toolCallId ,
91+ toolName : data . toolName ,
92+ args : data . arguments ,
93+ status : MothershipStreamV1AsyncToolRecordStatus . running ,
94+ } ) . catch ( ( err ) => {
95+ logger . warn ( 'Failed to pre-persist async tool row before forwarding call frame' , {
96+ toolCallId : data . toolCallId ,
97+ toolName : data . toolName ,
98+ error : err instanceof Error ? err . message : String ( err ) ,
99+ } )
100+ } )
101+ }
102+
57103/**
58104 * Unified tool event handler for both main and subagent scopes.
59105 *
@@ -365,11 +411,6 @@ async function dispatchToolExecution(
365411 }
366412 } else {
367413 toolCall . status = 'executing'
368- // Span covers the entire "wait for browser/client to execute this
369- // tool and report back" window — typically the single largest
370- // non-LLM latency contributor for mothership requests that use
371- // client-side tools. Before this, the wait was uninstrumented and
372- // only visible as a gap in the waterfall.
373414 const pendingPromise = withCopilotSpan (
374415 TraceSpan . CopilotToolWaitForClientResult ,
375416 {
@@ -379,22 +420,6 @@ async function dispatchToolExecution(
379420 ...( context . runId ? { [ TraceAttr . RunId ] : context . runId } : { } ) ,
380421 } ,
381422 async ( span ) => {
382- await upsertAsyncToolCall ( {
383- runId : context . runId ,
384- toolCallId,
385- toolName,
386- args,
387- status : MothershipStreamV1AsyncToolRecordStatus . running ,
388- } ) . catch ( ( err ) => {
389- logger . warn (
390- `Failed to persist async tool row for client-executable ${ scopeLabel } tool` ,
391- {
392- toolCallId,
393- toolName,
394- error : err instanceof Error ? err . message : String ( err ) ,
395- }
396- )
397- } )
398423 const completion = await waitForToolCompletion (
399424 toolCallId ,
400425 options . timeout || STREAM_TIMEOUT_MS ,
0 commit comments