Download src/agents/cli-runner/execute-tool-tracking.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 27.9 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/cli-runner/execute-tool-tracking.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/agents/cli-runner/execute-tool-tracking.ts
-
curl -L -o execute-tool-tracking.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/agents/cli-runner/execute-tool-tracking.ts
27.9 kB
| import { isDeepStrictEqual } from "node:util"; | |
| import { | |
| beginMcpLoopbackToolCallCapture, | |
| clearMcpLoopbackToolCallCapture, | |
| type McpLoopbackToolCallStart, | |
| waitForMcpLoopbackToolCallCaptureIdle, | |
| } from "../../gateway/mcp-http.loopback-runtime.js"; | |
| import { shouldUseInternalSourceReplySink } from "../../infra/outbound/internal-source-reply.js"; | |
| import { | |
| normalizeAcceptedSessionSpawnResult, | |
| type AcceptedSessionSpawn, | |
| } from "../accepted-session-spawn.js"; | |
| import type { CliOutput, CliToolUseStartDelta } from "../cli-output-contracts.js"; | |
| import { readEmbeddedMessageDeliveryFact } from "../embedded-agent-message-delivery.js"; | |
| import { | |
| isDeliveredMessageToolOnlySourceReplyResult, | |
| isDeliveredMessagingToolResult, | |
| resolveMessageToolSourceReplyFinal, | |
| } from "../embedded-agent-message-tool-source-reply.js"; | |
| import { | |
| extractMessagingToolSendResult, | |
| extractMessagingToolSourceReplyPayload, | |
| isDeliveredMessagingToolSendToCurrentSource, | |
| } from "../embedded-agent-messaging-extraction.js"; | |
| import { | |
| isMessagingTool, | |
| isMessagingToolDeliveryAction, | |
| isMessagingToolSendAction, | |
| isPluginNativeMessagingTool, | |
| } from "../embedded-agent-messaging.js"; | |
| import type { | |
| MessagingToolSend, | |
| MessagingToolSourceReplyPayload, | |
| } from "../embedded-agent-messaging.types.js"; | |
| import { | |
| extractToolResultMediaArtifact, | |
| filterToolResultMediaUrls, | |
| } from "../embedded-agent-tool-media.js"; | |
| import { readToolResultDetails } from "../tool-result-error.js"; | |
| import { closeCliLiveSession } from "./cli-live-session-registry.js"; | |
| import { attachCliMessagingDeliveryEvidence } from "./delivery-evidence.js"; | |
| import * as Deadline from "./execute-ask-user-deadline.js"; | |
| import { | |
| appendUniqueCliMessagingEvidence, | |
| buildMessagingToolSendEvidenceKey, | |
| CLI_MESSAGING_EVIDENCE_MAX_CALLS, | |
| extractCliMessagingContent, | |
| extractCliMessagingTarget, | |
| normalizeCliMessagingToolName, | |
| } from "./execute-messaging.js"; | |
| import type { PreparedCliRunContext } from "./types.js"; | |
| const CLI_LOOPBACK_CORRELATION_MAX_CALLS = 64; | |
| const CLI_MCP_DELIVERY_DRAIN_GRACE_MS = 5_000; | |
| const CLI_MCP_REQUEST_ADMISSION_GRACE_MS = 250; | |
| type ActiveCliTool = Deadline.ActiveCliTool; | |
| type CliLoopbackCall = Deadline.CliLoopbackCall; | |
| export function createCliToolTracking(context: PreparedCliRunContext) { | |
| let gatewayCaptureKey: string | undefined; | |
| let yielded = false; | |
| let yieldAcknowledgment: string | undefined; | |
| let didSendViaMessagingTool = false; | |
| let didDeliverSourceReplyViaMessageTool = false; | |
| let sourceReplyDelivered: true | undefined; | |
| let inFlightUnclassifiedMcpRequests = 0; | |
| let inFlightMessagingToolCalls = 0; | |
| const inFlightPreparedMessagingCalls = new Set<McpLoopbackToolCallStart>(); | |
| const pendingMessagingCalls = new Map< | |
| string, | |
| { toolName: string; args: Record<string, unknown>; target?: MessagingToolSend } | |
| >(); | |
| const cliLoopbackCalls: CliLoopbackCall[] = []; | |
| const activeCliTools = new Map<string, ActiveCliTool>(); | |
| let cliLoopbackCorrelationOverflowed = false; | |
| const askUserDeadlines = Deadline.createAskUserDeadlineTracking( | |
| activeCliTools, | |
| () => cliLoopbackCorrelationOverflowed, | |
| ); | |
| const messagingToolSentTexts: string[] = []; | |
| const messagingToolSentTextKeys = new Set<string>(); | |
| const messagingToolSentMediaUrls: string[] = []; | |
| const messagingToolSentMediaUrlKeys = new Set<string>(); | |
| const messagingToolSentTargets: MessagingToolSend[] = []; | |
| const messagingToolSentTargetKeys = new Set<string>(); | |
| const messagingToolSourceReplyPayloads: MessagingToolSourceReplyPayload[] = []; | |
| const toolMediaUrls: string[] = []; | |
| const toolMediaUrlKeys = new Set<string>(); | |
| let toolAudioAsVoice = false; | |
| let toolTrustedLocalMedia = false; | |
| const acceptedSessionSpawns: AcceptedSessionSpawn[] = []; | |
| const matchesCliLoopbackCall = ( | |
| toolName: string, | |
| toolArgs: Record<string, unknown>, | |
| call: McpLoopbackToolCallStart, | |
| ) => | |
| normalizeCliMessagingToolName(toolName) === call.toolName && | |
| isDeepStrictEqual(toolArgs, call.args); | |
| const markCliLoopbackCallsAmbiguous = ( | |
| calls: CliLoopbackCall[], | |
| activeEntries = Array.from(activeCliTools.entries()).filter( | |
| ([, activeTool]) => | |
| activeTool.loopbackCall !== undefined && calls.includes(activeTool.loopbackCall), | |
| ), | |
| ) => { | |
| const groups = new Set<Deadline.CliLoopbackAmbiguityGroup>(); | |
| for (const call of calls) { | |
| if (call.ambiguityGroup) { | |
| groups.add(call.ambiguityGroup); | |
| } | |
| } | |
| for (const [, activeTool] of activeEntries) { | |
| if (activeTool.ambiguityGroup) { | |
| groups.add(activeTool.ambiguityGroup); | |
| } | |
| } | |
| const group = groups.values().next().value ?? { | |
| calls: new Set<CliLoopbackCall>(), | |
| activeToolCallIds: new Set<string>(), | |
| }; | |
| for (const existing of groups) { | |
| if (existing === group) { | |
| continue; | |
| } | |
| for (const call of existing.calls) { | |
| call.ambiguityGroup = group; | |
| group.calls.add(call); | |
| } | |
| for (const toolCallId of existing.activeToolCallIds) { | |
| const activeTool = activeCliTools.get(toolCallId); | |
| if (activeTool) { | |
| activeTool.ambiguityGroup = group; | |
| group.activeToolCallIds.add(toolCallId); | |
| } | |
| } | |
| existing.calls.clear(); | |
| existing.activeToolCallIds.clear(); | |
| } | |
| for (const call of calls) { | |
| call.ambiguous = true; | |
| call.ambiguityGroup = group; | |
| group.calls.add(call); | |
| } | |
| for (const [toolCallId, activeTool] of activeEntries) { | |
| activeTool.loopbackAmbiguous = true; | |
| activeTool.ambiguityGroup = group; | |
| group.activeToolCallIds.add(toolCallId); | |
| } | |
| askUserDeadlines.refresh(); | |
| }; | |
| const matchingActiveCliTools = (call: McpLoopbackToolCallStart): Array<[string, ActiveCliTool]> => | |
| Array.from(activeCliTools.entries()).filter(([, activeTool]) => | |
| matchesCliLoopbackCall(activeTool.toolName, activeTool.args, call), | |
| ); | |
| const markCliLoopbackSignatureAmbiguous = (call: McpLoopbackToolCallStart) => { | |
| const calls = cliLoopbackCalls.filter((candidate) => | |
| matchesCliLoopbackCall(call.toolName, call.args, candidate.admitted), | |
| ); | |
| markCliLoopbackCallsAmbiguous(calls, matchingActiveCliTools(call)); | |
| }; | |
| const retainCliLoopbackCall = (call: McpLoopbackToolCallStart) => { | |
| if (cliLoopbackCalls.length >= CLI_LOOPBACK_CORRELATION_MAX_CALLS) { | |
| cliLoopbackCorrelationOverflowed = true; | |
| for (const activeTool of activeCliTools.values()) { | |
| if (activeTool.loopbackCall || activeTool.toolName.startsWith("mcp__")) { | |
| activeTool.loopbackAmbiguous = true; | |
| } | |
| } | |
| cliLoopbackCalls.length = 0; | |
| askUserDeadlines.refresh(); | |
| return undefined; | |
| } | |
| const retained: CliLoopbackCall = { admitted: call, current: call, ambiguous: false }; | |
| cliLoopbackCalls.push(retained); | |
| return retained; | |
| }; | |
| const bindCliLoopbackCall = ( | |
| call: CliLoopbackCall, | |
| toolCallId: string, | |
| activeTool: ActiveCliTool, | |
| ) => { | |
| call.boundToolCallId = toolCallId; | |
| activeTool.loopbackCall = call; | |
| activeTool.loopbackAmbiguous ||= call.ambiguous; | |
| if (call.ambiguityGroup) { | |
| activeTool.ambiguityGroup = call.ambiguityGroup; | |
| call.ambiguityGroup.activeToolCallIds.add(toolCallId); | |
| } | |
| askUserDeadlines.refresh(); | |
| }; | |
| const removeCliLoopbackCall = (call: CliLoopbackCall | undefined) => { | |
| if (!call) { | |
| return; | |
| } | |
| const index = cliLoopbackCalls.indexOf(call); | |
| if (index >= 0) { | |
| cliLoopbackCalls.splice(index, 1); | |
| } | |
| }; | |
| const retireCliLoopbackCorrelation = ( | |
| toolCallId: string, | |
| activeTool: ActiveCliTool | undefined, | |
| ) => { | |
| removeCliLoopbackCall(activeTool?.loopbackCall); | |
| const group = activeTool?.ambiguityGroup; | |
| if (!group) { | |
| return; | |
| } | |
| group.activeToolCallIds.delete(toolCallId); | |
| const hasUnboundCall = Array.from(group.calls).some( | |
| (call) => call.boundToolCallId === undefined && cliLoopbackCalls.includes(call), | |
| ); | |
| if (group.activeToolCallIds.size > 0 || hasUnboundCall) { | |
| return; | |
| } | |
| // An ambiguous group owns unbound captures too. Retire the whole group | |
| // once its parsed tools finish so stale calls cannot poison later tools. | |
| for (const call of group.calls) { | |
| removeCliLoopbackCall(call); | |
| } | |
| group.calls.clear(); | |
| }; | |
| const commitMessagingToolResult = (params: { | |
| toolName: string; | |
| target?: MessagingToolSend; | |
| args?: Record<string, unknown>; | |
| result?: unknown; | |
| isError?: boolean; | |
| }) => { | |
| const deliveryFact = readEmbeddedMessageDeliveryFact( | |
| readToolResultDetails(params.result)?.messageDelivery, | |
| ); | |
| const delivered = deliveryFact | |
| ? deliveryFact.status === "settled" && | |
| (params.isError !== true || deliveryFact.partialDelivery) | |
| : isPluginNativeMessagingTool(params.toolName) && isDeliveredMessagingToolResult(params); | |
| if (!delivered) { | |
| return; | |
| } | |
| didSendViaMessagingTool = true; | |
| // Implicit source replies can settle without an argument-derived target. | |
| if (deliveryFact?.sourceReplyDelivered === true) { | |
| sourceReplyDelivered = true; | |
| } | |
| const toolArgs = params.args ?? {}; | |
| const isMessagingSend = isMessagingToolSendAction(params.toolName, toolArgs); | |
| const content = isMessagingSend ? extractCliMessagingContent(toolArgs, params.result) : {}; | |
| const confirmedTarget = | |
| params.target && extractMessagingToolSendResult(params.target, params.result); | |
| const deliveredCurrentSourceReply = isDeliveredMessageToolOnlySourceReplyResult({ | |
| sourceReplyDeliveryMode: context.params.sourceReplyDeliveryMode, | |
| toolName: params.toolName, | |
| args: params.args, | |
| result: params.result, | |
| isError: params.isError, | |
| allowExplicitSourceRoute: isDeliveredMessagingToolSendToCurrentSource({ | |
| send: confirmedTarget, | |
| config: context.params.config, | |
| currentProvider: context.params.messageChannel ?? context.params.messageProvider, | |
| currentAccountId: context.params.agentAccountId, | |
| currentChannelId: context.params.currentChannelId, | |
| currentThreadId: context.params.currentThreadTs, | |
| sessionKey: context.params.sessionKey, | |
| deliveredPayload: params.result, | |
| }), | |
| deliveryConfirmed: true, | |
| }); | |
| const sourceReplyFinal = deliveredCurrentSourceReply | |
| ? resolveMessageToolSourceReplyFinal(toolArgs) | |
| : undefined; | |
| if (isMessagingSend) { | |
| appendUniqueCliMessagingEvidence( | |
| messagingToolSentTexts, | |
| messagingToolSentTextKeys, | |
| content.text ? [content.text] : [], | |
| ); | |
| appendUniqueCliMessagingEvidence( | |
| messagingToolSentMediaUrls, | |
| messagingToolSentMediaUrlKeys, | |
| content.mediaUrls ?? [], | |
| ); | |
| } | |
| if (deliveredCurrentSourceReply) { | |
| didDeliverSourceReplyViaMessageTool = true; | |
| const payload = extractMessagingToolSourceReplyPayload(params.result); | |
| if (payload) { | |
| if (messagingToolSourceReplyPayloads.length >= CLI_MESSAGING_EVIDENCE_MAX_CALLS) { | |
| messagingToolSourceReplyPayloads.shift(); | |
| } | |
| // Each internal source-reply send is a distinct delivery, even when | |
| // two intentional sends have identical text or media. | |
| messagingToolSourceReplyPayloads.push({ | |
| ...payload, | |
| ...(sourceReplyFinal !== undefined ? { sourceReplyFinal } : {}), | |
| }); | |
| } | |
| } | |
| if (!confirmedTarget) { | |
| return; | |
| } | |
| const targetWithContent = { | |
| ...confirmedTarget, | |
| ...content, | |
| ...(sourceReplyFinal !== undefined ? { sourceReplyFinal } : {}), | |
| }; | |
| const evidenceKey = buildMessagingToolSendEvidenceKey(targetWithContent); | |
| if (messagingToolSentTargetKeys.has(evidenceKey)) { | |
| return; | |
| } | |
| if (messagingToolSentTargets.length >= CLI_MESSAGING_EVIDENCE_MAX_CALLS) { | |
| const removed = messagingToolSentTargets.shift(); | |
| if (removed) { | |
| messagingToolSentTargetKeys.delete(buildMessagingToolSendEvidenceKey(removed)); | |
| } | |
| } | |
| messagingToolSentTargets.push(targetWithContent); | |
| messagingToolSentTargetKeys.add(evidenceKey); | |
| }; | |
| const isPreparedInternalSourceReply = async (call: McpLoopbackToolCallStart) => { | |
| if ( | |
| context.params.sourceReplyDeliveryMode !== "message_tool_only" || | |
| normalizeCliMessagingToolName(call.toolName) !== "message" || | |
| call.args.action !== "send" || | |
| !context.params.config | |
| ) { | |
| return false; | |
| } | |
| return await shouldUseInternalSourceReplySink( | |
| { | |
| cfg: context.params.config, | |
| action: "send", | |
| sessionKey: context.params.sessionKey, | |
| sourceReplyDeliveryMode: context.params.sourceReplyDeliveryMode, | |
| toolContext: { | |
| currentChannelProvider: context.params.messageChannel ?? context.params.messageProvider, | |
| currentChannelId: context.params.currentChannelId, | |
| currentThreadTs: context.params.currentThreadTs, | |
| currentMessageId: context.params.currentMessageId, | |
| replyToMode: context.params.replyToMode, | |
| }, | |
| }, | |
| call.args, | |
| ); | |
| }; | |
| const beginGatewayCapture = (captureKey: string | undefined, assertCurrent: () => void) => { | |
| if (!captureKey || gatewayCaptureKey === captureKey) { | |
| return; | |
| } | |
| if (gatewayCaptureKey) { | |
| throw new Error("CLI MCP capture key changed during an active attempt"); | |
| } | |
| context.preparedBackend.mcpClientGrantCapture?.activate(captureKey, assertCurrent); | |
| gatewayCaptureKey = captureKey; | |
| const isPotentialDelivery = (toolName: string) => | |
| isMessagingTool(normalizeCliMessagingToolName(toolName)); | |
| const isPreparedDelivery = (toolName: string, toolArgs: Record<string, unknown>) => | |
| toolArgs.dryRun !== true && | |
| isMessagingToolDeliveryAction(normalizeCliMessagingToolName(toolName), toolArgs); | |
| beginMcpLoopbackToolCallCapture({ | |
| captureKey, | |
| onYield: (_message, acknowledgment) => { | |
| yielded = true; | |
| yieldAcknowledgment = acknowledgment; | |
| }, | |
| onRequestStart: () => { | |
| inFlightUnclassifiedMcpRequests += 1; | |
| }, | |
| onRequestClassified: () => { | |
| inFlightUnclassifiedMcpRequests = Math.max(0, inFlightUnclassifiedMcpRequests - 1); | |
| }, | |
| onToolCallStart: (call) => { | |
| const retained = retainCliLoopbackCall(call); | |
| const candidates = matchingActiveCliTools(call); | |
| // Parallel same-name calls can reach the loopback out of stream order. | |
| // Bind only a unique name+arguments match; ambiguity is safer than a wrong outcome. | |
| let matched = | |
| retained && | |
| candidates.length === 1 && | |
| !candidates[0]?.[1].loopbackCall && | |
| !candidates[0]?.[1].loopbackAmbiguous | |
| ? candidates[0] | |
| : undefined; | |
| if (retained && matched) { | |
| bindCliLoopbackCall(retained, matched[0], matched[1]); | |
| } else if (retained && candidates.length > 0) { | |
| markCliLoopbackSignatureAmbiguous(call); | |
| matched = candidates.find(([, activeTool]) => !activeTool.loopbackCall); | |
| if (matched) { | |
| bindCliLoopbackCall(retained, matched[0], matched[1]); | |
| } | |
| } | |
| if (isPotentialDelivery(call.toolName)) { | |
| inFlightMessagingToolCalls += 1; | |
| } | |
| return matched?.[0]; | |
| }, | |
| onToolCallUpdate: ({ previous, current }) => { | |
| const candidates = cliLoopbackCalls.filter((candidate) => | |
| matchesCliLoopbackCall(previous.toolName, previous.args, candidate.current), | |
| ); | |
| const candidate = candidates.at(0); | |
| if (candidates.length === 1 && candidate && !candidate.ambiguous) { | |
| candidate.current = current; | |
| const toolName = normalizeCliMessagingToolName(current.toolName); | |
| askUserDeadlines.update(candidate, toolName, current.args); | |
| } else if (candidates.length > 0) { | |
| markCliLoopbackCallsAmbiguous(candidates); | |
| } | |
| inFlightPreparedMessagingCalls.delete(previous); | |
| const wasDelivery = isPotentialDelivery(previous.toolName); | |
| const isDelivery = isPreparedDelivery(current.toolName, current.args); | |
| if (wasDelivery !== isDelivery) { | |
| inFlightMessagingToolCalls = Math.max( | |
| 0, | |
| inFlightMessagingToolCalls + (isDelivery ? 1 : -1), | |
| ); | |
| } | |
| if (isDelivery) { | |
| inFlightPreparedMessagingCalls.add(current); | |
| } | |
| }, | |
| onToolCallFinish: (call, { prepared }) => { | |
| const isDelivery = prepared | |
| ? isPreparedDelivery(call.toolName, call.args) | |
| : isPotentialDelivery(call.toolName); | |
| if (isDelivery) { | |
| inFlightMessagingToolCalls = Math.max(0, inFlightMessagingToolCalls - 1); | |
| } | |
| inFlightPreparedMessagingCalls.delete(call); | |
| }, | |
| onToolCallResult: (call) => { | |
| const terminalOutcome: Deadline.CliToolTerminalOutcome = | |
| call.outcome === "blocked" | |
| ? { outcome: call.outcome, deniedReason: call.deniedReason } | |
| : { outcome: call.outcome }; | |
| const correlated = call.correlationId | |
| ? cliLoopbackCalls.find((candidate) => candidate.boundToolCallId === call.correlationId) | |
| : undefined; | |
| const candidates = correlated | |
| ? [correlated] | |
| : cliLoopbackCalls.filter((candidate) => | |
| matchesCliLoopbackCall(call.toolName, call.args, candidate.current), | |
| ); | |
| if (candidates.length === 1 && candidates[0]) { | |
| candidates[0].outcome = terminalOutcome; | |
| } else if (candidates.length > 1) { | |
| markCliLoopbackCallsAmbiguous(candidates); | |
| } | |
| const toolName = normalizeCliMessagingToolName(call.toolName); | |
| const acceptedSessionSpawn = | |
| toolName === "sessions_spawn" && call.outcome === "completed" && "result" in call | |
| ? normalizeAcceptedSessionSpawnResult(call.result) | |
| : null; | |
| if ( | |
| acceptedSessionSpawn && | |
| acceptedSessionSpawns.length < CLI_LOOPBACK_CORRELATION_MAX_CALLS | |
| ) { | |
| acceptedSessionSpawns.push(acceptedSessionSpawn); | |
| } | |
| if (isMessagingToolDeliveryAction(toolName, call.args)) { | |
| commitMessagingToolResult({ | |
| toolName, | |
| target: extractCliMessagingTarget(context, toolName, call.args), | |
| args: call.args, | |
| result: "result" in call ? call.result : undefined, | |
| isError: call.outcome !== "completed", | |
| }); | |
| } else if (call.outcome === "completed" && "result" in call) { | |
| const artifact = extractToolResultMediaArtifact(call.result); | |
| const mediaUrls = artifact | |
| ? filterToolResultMediaUrls(toolName, artifact.mediaUrls, call.result) | |
| : []; | |
| appendUniqueCliMessagingEvidence(toolMediaUrls, toolMediaUrlKeys, mediaUrls); | |
| if (mediaUrls.length > 0) { | |
| toolAudioAsVoice ||= artifact?.audioAsVoice === true; | |
| toolTrustedLocalMedia ||= artifact?.trustedLocalMedia === true; | |
| } | |
| } | |
| }, | |
| }); | |
| }; | |
| const handleCliToolUseStart = (event: CliToolUseStartDelta) => { | |
| if (event.kind !== "server_tool_use") { | |
| const activeTool: ActiveCliTool = { | |
| toolName: event.name, | |
| args: event.args, | |
| loopbackAmbiguous: cliLoopbackCorrelationOverflowed && event.name.startsWith("mcp__"), | |
| }; | |
| activeCliTools.set(event.toolCallId, activeTool); | |
| const admittedCall = { | |
| toolName: normalizeCliMessagingToolName(event.name), | |
| args: event.args, | |
| }; | |
| const pendingCandidates = cliLoopbackCalls.filter( | |
| (candidate) => | |
| candidate.boundToolCallId === undefined && | |
| matchesCliLoopbackCall(event.name, event.args, candidate.admitted), | |
| ); | |
| const hasAssociatedPeer = matchingActiveCliTools(admittedCall).some( | |
| ([toolCallId, peer]) => | |
| toolCallId !== event.toolCallId && | |
| (peer.loopbackCall !== undefined || peer.loopbackAmbiguous), | |
| ); | |
| const pending = pendingCandidates[0]; | |
| if (hasAssociatedPeer || pendingCandidates.length > 1 || pending?.ambiguous) { | |
| markCliLoopbackSignatureAmbiguous(admittedCall); | |
| if (pending) { | |
| bindCliLoopbackCall(pending, event.toolCallId, activeTool); | |
| } | |
| } else if (pendingCandidates.length === 1 && pending) { | |
| bindCliLoopbackCall(pending, event.toolCallId, activeTool); | |
| } | |
| } | |
| const toolName = normalizeCliMessagingToolName(event.name); | |
| if ( | |
| event.kind === "server_tool_use" || | |
| gatewayCaptureKey || | |
| event.args.dryRun === true || | |
| !isMessagingToolDeliveryAction(toolName, event.args) | |
| ) { | |
| return; | |
| } | |
| if (pendingMessagingCalls.size >= CLI_MESSAGING_EVIDENCE_MAX_CALLS) { | |
| const oldestToolCallId = pendingMessagingCalls.keys().next().value; | |
| if (oldestToolCallId !== undefined) { | |
| pendingMessagingCalls.delete(oldestToolCallId); | |
| // Once an unresolved send is evicted, its later result cannot be correlated. | |
| // Fail closed so a failed turn cannot duplicate it. | |
| didSendViaMessagingTool = true; | |
| } | |
| } | |
| pendingMessagingCalls.set(event.toolCallId, { | |
| toolName, | |
| args: event.args, | |
| target: extractCliMessagingTarget(context, toolName, event.args), | |
| }); | |
| }; | |
| const handleCliToolResult = (event: { | |
| toolCallId: string; | |
| name: string; | |
| isError: boolean; | |
| result?: unknown; | |
| }) => { | |
| const activeTool = activeCliTools.get(event.toolCallId); | |
| if (activeTool?.loopbackCall) { | |
| askUserDeadlines.clear(activeTool.loopbackCall); | |
| } | |
| activeCliTools.delete(event.toolCallId); | |
| retireCliLoopbackCorrelation(event.toolCallId, activeTool); | |
| const pending = pendingMessagingCalls.get(event.toolCallId); | |
| if (pending) { | |
| pendingMessagingCalls.delete(event.toolCallId); | |
| commitMessagingToolResult({ | |
| toolName: pending.toolName, | |
| target: pending.target, | |
| args: pending.args, | |
| result: event.result, | |
| isError: event.isError, | |
| }); | |
| } | |
| }; | |
| const resolveCliLoopbackTerminalOutcome = (toolCallId: string) => { | |
| const activeTool = activeCliTools.get(toolCallId); | |
| if (activeTool?.loopbackAmbiguous) { | |
| return { outcome: "unknown" } as const; | |
| } | |
| return activeTool?.loopbackCall?.outcome; | |
| }; | |
| const finishDeliveryTracking = async (params: { | |
| useManagedClaudeLiveSession: boolean; | |
| recordRunError: (error: unknown) => void; | |
| }) => { | |
| try { | |
| if (!gatewayCaptureKey && pendingMessagingCalls.size > 0) { | |
| const calls = Array.from(pendingMessagingCalls.values()); | |
| const internalStates = await Promise.all(calls.map(isPreparedInternalSourceReply)); | |
| if (internalStates.some((internal) => !internal)) { | |
| didSendViaMessagingTool = true; | |
| params.recordRunError( | |
| new Error("CLI JSONL message tool call remained unresolved after exit"), | |
| ); | |
| } else { | |
| params.recordRunError( | |
| new Error("CLI JSONL source reply call remained unresolved after exit"), | |
| ); | |
| } | |
| } | |
| if (!gatewayCaptureKey) { | |
| return; | |
| } | |
| const captureBecameIdle = await waitForMcpLoopbackToolCallCaptureIdle(gatewayCaptureKey, { | |
| timeoutMs: CLI_MCP_DELIVERY_DRAIN_GRACE_MS, | |
| admissionGraceMs: CLI_MCP_REQUEST_ADMISSION_GRACE_MS, | |
| }); | |
| if (captureBecameIdle) { | |
| return; | |
| } | |
| if (params.useManagedClaudeLiveSession) { | |
| // The child still holds the process-env capture key. If drain cannot | |
| // prove idle, kill it so a stale key cannot admit later sends. | |
| await closeCliLiveSession(context, "mcp-capture-rotation"); | |
| } | |
| const internalStates = await Promise.all( | |
| Array.from(inFlightPreparedMessagingCalls).map(isPreparedInternalSourceReply), | |
| ); | |
| const internalCount = internalStates.filter(Boolean).length; | |
| const hasPotentialVisibleSend = inFlightMessagingToolCalls > internalCount; | |
| if (inFlightUnclassifiedMcpRequests > 0 || hasPotentialVisibleSend) { | |
| didSendViaMessagingTool = true; | |
| params.recordRunError(new Error("CLI message tool call remained in flight after exit")); | |
| } else if (inFlightMessagingToolCalls > 0) { | |
| params.recordRunError(new Error("CLI source reply call remained in flight after exit")); | |
| } | |
| } catch (error) { | |
| if ( | |
| pendingMessagingCalls.size > 0 || | |
| inFlightUnclassifiedMcpRequests > 0 || | |
| inFlightMessagingToolCalls > 0 | |
| ) { | |
| didSendViaMessagingTool = true; | |
| } | |
| params.recordRunError(error); | |
| } | |
| }; | |
| const finalizeCapture = (finalizeParsedTools: () => void) => { | |
| // Captured MCP calls may settle after the attempt returns. Drain first so | |
| // finalization can use their trusted terminal outcomes. | |
| try { | |
| finalizeParsedTools(); | |
| } finally { | |
| if (gatewayCaptureKey) { | |
| // Fence this exact grant generation before clearing observers; otherwise | |
| // a late request escapes accounting. | |
| try { | |
| context.preparedBackend.mcpClientGrantCapture?.deactivate(gatewayCaptureKey); | |
| } finally { | |
| clearMcpLoopbackToolCallCapture(gatewayCaptureKey); | |
| } | |
| } | |
| } | |
| }; | |
| const evidence = () => ({ | |
| didSendViaMessagingTool, | |
| didDeliverSourceReplyViaMessageTool, | |
| sourceReplyDelivered, | |
| messagingToolSentTexts, | |
| messagingToolSentMediaUrls, | |
| messagingToolSentTargets, | |
| messagingToolSourceReplyPayloads, | |
| toolMediaUrls, | |
| toolAudioAsVoice, | |
| toolTrustedLocalMedia, | |
| acceptedSessionSpawns, | |
| }); | |
| return { | |
| beginGatewayCapture, | |
| getActiveLoopbackAskUserDeadline: askUserDeadlines.get, | |
| onActiveLoopbackAskUserDeadlineChange: askUserDeadlines.onChange, | |
| handleCliToolUseStart, | |
| handleCliToolResult, | |
| resolveCliLoopbackTerminalOutcome, | |
| finishDeliveryTracking, | |
| finalizeCapture, | |
| withExecutionEvidence(output: CliOutput): CliOutput { | |
| const current = evidence(); | |
| return { | |
| ...output, | |
| ...(yielded ? { yielded: true as const } : {}), | |
| ...(yieldAcknowledgment ? { yieldAcknowledgment } : {}), | |
| ...(current.didSendViaMessagingTool ? { didSendViaMessagingTool: true } : {}), | |
| ...(current.didDeliverSourceReplyViaMessageTool | |
| ? { didDeliverSourceReplyViaMessageTool: true } | |
| : {}), | |
| ...(current.sourceReplyDelivered ? { sourceReplyDelivered: true as const } : {}), | |
| ...(current.messagingToolSentTexts.length > 0 | |
| ? { messagingToolSentTexts: current.messagingToolSentTexts.slice() } | |
| : {}), | |
| ...(current.messagingToolSentMediaUrls.length > 0 | |
| ? { messagingToolSentMediaUrls: current.messagingToolSentMediaUrls.slice() } | |
| : {}), | |
| ...(current.messagingToolSentTargets.length > 0 | |
| ? { messagingToolSentTargets: current.messagingToolSentTargets.slice() } | |
| : {}), | |
| ...(current.messagingToolSourceReplyPayloads.length > 0 | |
| ? { messagingToolSourceReplyPayloads: current.messagingToolSourceReplyPayloads.slice() } | |
| : {}), | |
| ...(current.toolMediaUrls.length > 0 | |
| ? { toolMediaUrls: current.toolMediaUrls.slice() } | |
| : {}), | |
| ...(current.toolAudioAsVoice ? { toolAudioAsVoice: true } : {}), | |
| ...(current.toolTrustedLocalMedia ? { toolTrustedLocalMedia: true } : {}), | |
| ...(current.acceptedSessionSpawns.length > 0 | |
| ? { acceptedSessionSpawns: current.acceptedSessionSpawns.slice() } | |
| : {}), | |
| }; | |
| }, | |
| attachDeliveryEvidence(error: unknown) { | |
| return attachCliMessagingDeliveryEvidence(error, evidence()); | |
| }, | |
| }; | |
| } | |
| export type CliToolTracking = ReturnType<typeof createCliToolTracking>; | |