diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index c8904ef8a2c..3dad0fd8ab7 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -fce3cbf24274016e01324082ad8ffe81fe2fb41a6e6314aa6efcdbe6689fd628 config-baseline.json +1f705ff2d4e35e5d958d1cb6ddd7cc7decf7bc208f8ff0663c6429895d3c6ca0 config-baseline.json fb6f0ef881fb591d2791d2adca43c7e88d48f8b562457683092ab6e767aece78 config-baseline.core.json 3bb312dc9c39a374ca92613abf21606c25dc571287a3941dac71ff57b2b5c519 config-baseline.channel.json -6c19997f1fb2aff4315f2cb9c7d9e299b403fbc0f9e78e3412cc7fe1c655f222 config-baseline.plugin.json +aa4b1d3d04ed9f9feea73c8fca36c48a54749853e07fadfca54773171b2ef4ff config-baseline.plugin.json diff --git a/extensions/msteams/src/attachments/graph.test.ts b/extensions/msteams/src/attachments/graph.test.ts index 021cb9c586c..6e2ad08a108 100644 --- a/extensions/msteams/src/attachments/graph.test.ts +++ b/extensions/msteams/src/attachments/graph.test.ts @@ -347,18 +347,18 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = // test is the only one that fires. return guardedFetchResult(params, mockFetchResponse({ value: [] })); }); - const log = { debug: vi.fn() }; + const logger = { warn: vi.fn() }; const result = await downloadMSTeamsGraphMedia({ messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-err", tokenProvider: { getAccessToken: vi.fn(async () => "test-token") }, maxBytes: 10 * 1024 * 1024, - log, + logger, }); expect(result.media).toHaveLength(0); - expect(log.debug).toHaveBeenCalledWith( - "graph media message fetch failed", + expect(logger.warn).toHaveBeenCalledWith( + "msteams graph message fetch failed", expect.objectContaining({ error: "network boom" }), ); }); @@ -394,7 +394,7 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: GuardedFetchParams) => guardedFetchResult(params, mockFetchResponse({})), ); - const log = { debug: vi.fn() }; + const logger = { warn: vi.fn() }; const result = await downloadMSTeamsGraphMedia({ messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-token", @@ -404,12 +404,12 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () = }), }, maxBytes: 10 * 1024 * 1024, - log, + logger, }); expect(result.tokenError).toBe(true); - expect(log.debug).toHaveBeenCalledWith( - "graph media token acquisition failed", + expect(logger.warn).toHaveBeenCalledWith( + "msteams graph token acquisition failed", expect.objectContaining({ error: "token expired" }), ); }); diff --git a/extensions/msteams/src/monitor-handler/inbound-media.test.ts b/extensions/msteams/src/monitor-handler/inbound-media.test.ts index 8e8a2239597..ea00e944128 100644 --- a/extensions/msteams/src/monitor-handler/inbound-media.test.ts +++ b/extensions/msteams/src/monitor-handler/inbound-media.test.ts @@ -136,7 +136,7 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => { const call = vi.mocked(downloadMSTeamsGraphMedia).mock.calls[0]?.[0]; // The monitor handler's logger is forwarded so graph.ts can report // message fetch failures instead of swallowing them (#51749). - expect(call?.log).toBe(log); + expect(call?.logger).toBe(log); expect(log.debug).toHaveBeenCalledWith( "graph media fetch empty", expect.objectContaining({ attachmentIdCount: 1 }), diff --git a/extensions/voice-call/src/webhook.test.ts b/extensions/voice-call/src/webhook.test.ts index 3d927faee7c..12b146cce67 100644 --- a/extensions/voice-call/src/webhook.test.ts +++ b/extensions/voice-call/src/webhook.test.ts @@ -49,6 +49,16 @@ const provider: VoiceCallProvider = { getCallStatus: async () => ({ status: "in-progress", isTerminal: false }), }; +type TwilioProviderTestDouble = VoiceCallProvider & + Pick< + TwilioProvider, + | "isValidStreamToken" + | "registerCallStream" + | "unregisterCallStream" + | "hasRegisteredStream" + | "clearTtsQueue" + >; + const createConfig = (overrides: Partial = {}): VoiceCallConfig => { const base = VoiceCallConfigSchema.parse({}); base.serve.port = 0; @@ -115,20 +125,8 @@ function expectWebhookUrl(url: string, expectedPath: string) { expect(parsed.port).not.toBe("0"); } -type TwilioTestProvider = VoiceCallProvider & - Partial< - Pick< - TwilioProvider, - | "clearTtsQueue" - | "hasRegisteredStream" - | "isValidStreamToken" - | "registerCallStream" - | "unregisterCallStream" - > - >; - function createTwilioVerificationProvider( - overrides: Partial = {}, + overrides: Partial = {}, ): VoiceCallProvider { return { ...provider, @@ -139,8 +137,8 @@ function createTwilioVerificationProvider( } function createTwilioStreamingProvider( - overrides: Partial = {}, -): TwilioTestProvider { + overrides: Partial = {}, +): TwilioProviderTestDouble { return { ...createTwilioVerificationProvider({ parseWebhookEvent: () => ({ events: [] }), @@ -773,11 +771,7 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => { }, }, }); - const server = new VoiceCallWebhookServer( - config, - manager, - twilioProvider as unknown as VoiceCallProvider, - ); + const server = new VoiceCallWebhookServer(config, manager, twilioProvider); await server.start(); const mediaHandler = server.getMediaStreamHandler() as unknown as { @@ -805,9 +799,11 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => { }); describe("VoiceCallWebhookServer barge-in suppression during initial message", () => { - const createTwilioProvider = (clearTtsQueue: ReturnType) => + const createTwilioProvider = ( + clearTtsQueue: ReturnType>, + ) => createTwilioStreamingProvider({ - clearTtsQueue: clearTtsQueue as TwilioTestProvider["clearTtsQueue"], + clearTtsQueue, }); const getMediaCallbacks = (server: VoiceCallWebhookServer) => @@ -829,7 +825,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", ( initialMessage: "Hi, this is OpenClaw.", }; - const clearTtsQueue = vi.fn(); + const clearTtsQueue = vi.fn(); const processEvent = vi.fn((event: NormalizedEvent) => { if (event.type === "call.speech") { // Mirrors manager behavior: call.speech transitions to listening. @@ -858,11 +854,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", ( }, }, }); - const server = new VoiceCallWebhookServer( - config, - manager, - createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider, - ); + const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue)); await server.start(); const handleInboundResponse = vi.fn(async () => {}); ( @@ -913,7 +905,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", ( initialMessage: "Hello from inbound greeting.", }; - const clearTtsQueue = vi.fn(); + const clearTtsQueue = vi.fn(); const manager = { getActiveCalls: () => [call], getCallByProviderCallId: (providerCallId: string) => @@ -936,11 +928,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", ( }, }, }); - const server = new VoiceCallWebhookServer( - config, - manager, - createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider, - ); + const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue)); await server.start(); try { diff --git a/scripts/check-web-fetch-provider-boundaries.mjs b/scripts/check-web-fetch-provider-boundaries.mjs index d53a25dff1e..fe61519e99f 100644 --- a/scripts/check-web-fetch-provider-boundaries.mjs +++ b/scripts/check-web-fetch-provider-boundaries.mjs @@ -41,7 +41,11 @@ export async function collectWebFetchProviderBoundaryViolations() { ignoredDirNames, }); for (const { relativeFile, content } of files) { - if (allowedFiles.has(relativeFile) || relativeFile.includes(".test.")) { + if ( + allowedFiles.has(relativeFile) || + relativeFile.includes(".test.") || + relativeFile.includes("test-support") + ) { continue; } const lines = content.split(/\r?\n/); diff --git a/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts b/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts index d42c9fd6944..b66ab02d404 100644 --- a/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts +++ b/src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts @@ -63,12 +63,12 @@ describe("sanitizeSessionHistory openai tool id preservation", () => { { name: "strips fc ids when replayable reasoning metadata is missing", withReasoning: false, - expectedToolId: "call_123", + expectedToolId: "call123", }, { name: "keeps canonical call_id|fc_id pairings when replayable reasoning is present", withReasoning: true, - expectedToolId: "call_123|fc_123", + expectedToolId: "call123fc123", }, ])("$name", async ({ withReasoning, expectedToolId }) => { const result = await sanitizeSessionHistory({ @@ -87,4 +87,45 @@ describe("sanitizeSessionHistory openai tool id preservation", () => { const toolResult = result[1] as { toolCallId?: string }; expect(toolResult.toolCallId).toBe(expectedToolId); }); + + it("repairs displaced tool results before downgrading openai pairing ids", async () => { + const result = await sanitizeSessionHistory({ + messages: [ + castAgentMessage({ + role: "assistant", + content: [{ type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {} }], + }), + castAgentMessage({ + role: "user", + content: [{ type: "text", text: "still waiting" }], + }), + castAgentMessage({ + role: "toolResult", + toolCallId: "call_123|fc_123", + toolName: "noop", + content: [{ type: "text", text: "ok" }], + isError: false, + }), + ], + modelApi: "openai-responses", + provider: "openai", + modelId: "gpt-5.4", + sessionManager: makeSessionManager(), + sessionId: "test-session", + }); + + const toolResult = result[1] as { + role?: string; + toolCallId?: string; + content?: Array<{ type?: string; text?: string }>; + isError?: boolean; + }; + expect(toolResult.role).toBe("toolResult"); + expect(toolResult.toolCallId).toBe("call123"); + expect(toolResult.content?.[0]?.text).toBe("ok"); + expect(toolResult.isError).toBe(false); + + const userMessage = result[2] as { role?: string }; + expect(userMessage.role).toBe("user"); + }); }); diff --git a/src/agents/pi-embedded-runner/replay-history.ts b/src/agents/pi-embedded-runner/replay-history.ts index fff61ac7cd8..a96541ce92c 100644 --- a/src/agents/pi-embedded-runner/replay-history.ts +++ b/src/agents/pi-embedded-runner/replay-history.ts @@ -433,24 +433,38 @@ export async function sanitizeSessionHistory(params: { allowedToolNames: params.allowedToolNames, allowProviderOwnedThinkingReplay, }); + // OpenAI's fc_* pairing downgrade needs the raw call_id|fc_id separator intact, + // but displaced tool results must first be repaired back next to their + // assistant turn so the downgrade can rewrite both sides consistently. + const openAIRepairedToolCalls = + isOpenAIResponsesApi && policy.repairToolUseResultPairing + ? sanitizeToolUseResultPairing(sanitizedToolCalls, { + erroredAssistantResultPolicy: "drop", + }) + : sanitizedToolCalls; + const openAISafeToolCalls = isOpenAIResponsesApi + ? downgradeOpenAIFunctionCallReasoningPairs( + downgradeOpenAIReasoningBlocks(openAIRepairedToolCalls), + ) + : sanitizedToolCalls; const sanitizedToolIds = - policy.sanitizeToolCallIds && policy.toolCallIdMode && !isOpenAIResponsesApi - ? sanitizeToolCallIdsForCloudCodeAssist(sanitizedToolCalls, policy.toolCallIdMode, { + policy.sanitizeToolCallIds && policy.toolCallIdMode + ? sanitizeToolCallIdsForCloudCodeAssist(openAISafeToolCalls, policy.toolCallIdMode, { preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds, preserveReplaySafeThinkingToolCallIds: allowProviderOwnedThinkingReplay, allowedToolNames: params.allowedToolNames, }) - : sanitizedToolCalls; - const repairedTools = policy.repairToolUseResultPairing - ? sanitizeToolUseResultPairing(sanitizedToolIds, { - erroredAssistantResultPolicy: "drop", - }) - : sanitizedToolIds; + : openAISafeToolCalls; + const repairedTools = + !isOpenAIResponsesApi && policy.repairToolUseResultPairing + ? sanitizeToolUseResultPairing(sanitizedToolIds, { + erroredAssistantResultPolicy: "drop", + }) + : sanitizedToolIds; const sanitizedToolResults = stripToolResultDetails(repairedTools); const sanitizedCompactionUsage = ensureAssistantUsageSnapshots( stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults), ); - const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId); const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null; const modelChanged = priorSnapshot @@ -461,11 +475,6 @@ export async function sanitizeSessionHistory(params: { modelId: params.modelId, }) : false; - const sanitizedOpenAI = isOpenAIResponsesApi - ? downgradeOpenAIFunctionCallReasoningPairs( - downgradeOpenAIReasoningBlocks(sanitizedCompactionUsage), - ) - : sanitizedCompactionUsage; const provider = params.provider?.trim(); const providerSanitized = provider && provider.length > 0 @@ -483,13 +492,13 @@ export async function sanitizeSessionHistory(params: { modelApi: params.modelApi, model: params.model, sessionId: params.sessionId, - messages: sanitizedOpenAI, + messages: sanitizedCompactionUsage, allowedToolNames: params.allowedToolNames, sessionState: createProviderReplaySessionState(params.sessionManager), }, }) : undefined; - const sanitizedWithProvider = providerSanitized ?? sanitizedOpenAI; + const sanitizedWithProvider = providerSanitized ?? sanitizedCompactionUsage; if (hasSnapshot && (!priorSnapshot || modelChanged)) { appendModelSnapshot(params.sessionManager, { diff --git a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts b/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts index 80406b8a72a..999b9c2c007 100644 --- a/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts +++ b/src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts @@ -488,14 +488,6 @@ export async function loadRunOverflowCompactionHarness(): Promise<{ runPostCompactionSideEffects: mockedRunPostCompactionSideEffects, })); - vi.doMock("./compact.js", () => ({ - runPostCompactionSideEffects: mockedRunPostCompactionSideEffects, - })); - - vi.doMock("./compaction-hooks.js", () => ({ - runPostCompactionSideEffects: mockedRunPostCompactionSideEffects, - })); - vi.doMock("./utils.js", () => ({ describeUnknownError: vi.fn((err: unknown) => { if (err instanceof Error) { diff --git a/src/agents/pi-embedded-runner/run/auth-controller.test.ts b/src/agents/pi-embedded-runner/run/auth-controller.test.ts index b2e03525e34..748cbeb07b8 100644 --- a/src/agents/pi-embedded-runner/run/auth-controller.test.ts +++ b/src/agents/pi-embedded-runner/run/auth-controller.test.ts @@ -148,7 +148,7 @@ describe("createEmbeddedRunAuthController", () => { it("applies runtime request overrides on the first auth exchange", async () => { const harness = createMutableAuthControllerHarness(); - const setRuntimeApiKey = vi.fn(); + const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>(); mocks.getApiKeyForModel.mockResolvedValue({ apiKey: "source-api-key", @@ -218,7 +218,9 @@ describe("createEmbeddedRunAuthController", () => { version: 1, profiles: {}, } as AuthProfileStore, - authStorage: { setRuntimeApiKey: vi.fn() }, + authStorage: { + setRuntimeApiKey: vi.fn<(provider: string, apiKey: string) => void>(), + }, profileCandidates: ["default"], initialThinkLevel: "medium", attemptedThinking: new Set(), @@ -259,7 +261,7 @@ describe("createEmbeddedRunAuthController", () => { vi.useFakeTimers(); try { const harness = createMutableAuthControllerHarness(); - const setRuntimeApiKey = vi.fn(); + const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>(); const staleRefresh = createDeferred<{ apiKey: string; baseUrl: string;