Skip to content

Commit 202dca3

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 8e24ee8 + 383c854 commit 202dca3

17 files changed

Lines changed: 489 additions & 84 deletions
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
fce3cbf24274016e01324082ad8ffe81fe2fb41a6e6314aa6efcdbe6689fd628 config-baseline.json
1+
1f705ff2d4e35e5d958d1cb6ddd7cc7decf7bc208f8ff0663c6429895d3c6ca0 config-baseline.json
22
fb6f0ef881fb591d2791d2adca43c7e88d48f8b562457683092ab6e767aece78 config-baseline.core.json
33
3bb312dc9c39a374ca92613abf21606c25dc571287a3941dac71ff57b2b5c519 config-baseline.channel.json
4-
6c19997f1fb2aff4315f2cb9c7d9e299b403fbc0f9e78e3412cc7fe1c655f222 config-baseline.plugin.json
4+
aa4b1d3d04ed9f9feea73c8fca36c48a54749853e07fadfca54773171b2ef4ff config-baseline.plugin.json

extensions/msteams/src/attachments/graph.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -347,18 +347,18 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
347347
// test is the only one that fires.
348348
return guardedFetchResult(params, mockFetchResponse({ value: [] }));
349349
});
350-
const log = { debug: vi.fn() };
350+
const logger = { warn: vi.fn() };
351351

352352
const result = await downloadMSTeamsGraphMedia({
353353
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-err",
354354
tokenProvider: { getAccessToken: vi.fn(async () => "test-token") },
355355
maxBytes: 10 * 1024 * 1024,
356-
log,
356+
logger,
357357
});
358358

359359
expect(result.media).toHaveLength(0);
360-
expect(log.debug).toHaveBeenCalledWith(
361-
"graph media message fetch failed",
360+
expect(logger.warn).toHaveBeenCalledWith(
361+
"msteams graph message fetch failed",
362362
expect.objectContaining({ error: "network boom" }),
363363
);
364364
});
@@ -394,7 +394,7 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
394394
vi.mocked(fetchWithSsrFGuard).mockImplementation(async (params: GuardedFetchParams) =>
395395
guardedFetchResult(params, mockFetchResponse({})),
396396
);
397-
const log = { debug: vi.fn() };
397+
const logger = { warn: vi.fn() };
398398

399399
const result = await downloadMSTeamsGraphMedia({
400400
messageUrl: "https://graph.microsoft.com/v1.0/chats/c/messages/msg-token",
@@ -404,12 +404,12 @@ describe("downloadMSTeamsGraphMedia attachment sourcing and error logging", () =
404404
}),
405405
},
406406
maxBytes: 10 * 1024 * 1024,
407-
log,
407+
logger,
408408
});
409409

410410
expect(result.tokenError).toBe(true);
411-
expect(log.debug).toHaveBeenCalledWith(
412-
"graph media token acquisition failed",
411+
expect(logger.warn).toHaveBeenCalledWith(
412+
"msteams graph token acquisition failed",
413413
expect.objectContaining({ error: "token expired" }),
414414
);
415415
});

extensions/msteams/src/monitor-handler/inbound-media.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => {
136136
const call = vi.mocked(downloadMSTeamsGraphMedia).mock.calls[0]?.[0];
137137
// The monitor handler's logger is forwarded so graph.ts can report
138138
// message fetch failures instead of swallowing them (#51749).
139-
expect(call?.log).toBe(log);
139+
expect(call?.logger).toBe(log);
140140
expect(log.debug).toHaveBeenCalledWith(
141141
"graph media fetch empty",
142142
expect.objectContaining({ attachmentIdCount: 1 }),

extensions/voice-call/src/webhook.test.ts

Lines changed: 22 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ const provider: VoiceCallProvider = {
4949
getCallStatus: async () => ({ status: "in-progress", isTerminal: false }),
5050
};
5151

52+
type TwilioProviderTestDouble = VoiceCallProvider &
53+
Pick<
54+
TwilioProvider,
55+
| "isValidStreamToken"
56+
| "registerCallStream"
57+
| "unregisterCallStream"
58+
| "hasRegisteredStream"
59+
| "clearTtsQueue"
60+
>;
61+
5262
const createConfig = (overrides: Partial<VoiceCallConfig> = {}): VoiceCallConfig => {
5363
const base = VoiceCallConfigSchema.parse({});
5464
base.serve.port = 0;
@@ -115,20 +125,8 @@ function expectWebhookUrl(url: string, expectedPath: string) {
115125
expect(parsed.port).not.toBe("0");
116126
}
117127

118-
type TwilioTestProvider = VoiceCallProvider &
119-
Partial<
120-
Pick<
121-
TwilioProvider,
122-
| "clearTtsQueue"
123-
| "hasRegisteredStream"
124-
| "isValidStreamToken"
125-
| "registerCallStream"
126-
| "unregisterCallStream"
127-
>
128-
>;
129-
130128
function createTwilioVerificationProvider(
131-
overrides: Partial<TwilioTestProvider> = {},
129+
overrides: Partial<TwilioProviderTestDouble> = {},
132130
): VoiceCallProvider {
133131
return {
134132
...provider,
@@ -139,8 +137,8 @@ function createTwilioVerificationProvider(
139137
}
140138

141139
function createTwilioStreamingProvider(
142-
overrides: Partial<TwilioTestProvider> = {},
143-
): TwilioTestProvider {
140+
overrides: Partial<TwilioProviderTestDouble> = {},
141+
): TwilioProviderTestDouble {
144142
return {
145143
...createTwilioVerificationProvider({
146144
parseWebhookEvent: () => ({ events: [] }),
@@ -773,11 +771,7 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => {
773771
},
774772
},
775773
});
776-
const server = new VoiceCallWebhookServer(
777-
config,
778-
manager,
779-
twilioProvider as unknown as VoiceCallProvider,
780-
);
774+
const server = new VoiceCallWebhookServer(config, manager, twilioProvider);
781775
await server.start();
782776

783777
const mediaHandler = server.getMediaStreamHandler() as unknown as {
@@ -805,9 +799,11 @@ describe("VoiceCallWebhookServer stream disconnect grace", () => {
805799
});
806800

807801
describe("VoiceCallWebhookServer barge-in suppression during initial message", () => {
808-
const createTwilioProvider = (clearTtsQueue: ReturnType<typeof vi.fn>) =>
802+
const createTwilioProvider = (
803+
clearTtsQueue: ReturnType<typeof vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>>,
804+
) =>
809805
createTwilioStreamingProvider({
810-
clearTtsQueue: clearTtsQueue as TwilioTestProvider["clearTtsQueue"],
806+
clearTtsQueue,
811807
});
812808

813809
const getMediaCallbacks = (server: VoiceCallWebhookServer) =>
@@ -829,7 +825,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
829825
initialMessage: "Hi, this is OpenClaw.",
830826
};
831827

832-
const clearTtsQueue = vi.fn();
828+
const clearTtsQueue = vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>();
833829
const processEvent = vi.fn((event: NormalizedEvent) => {
834830
if (event.type === "call.speech") {
835831
// Mirrors manager behavior: call.speech transitions to listening.
@@ -858,11 +854,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
858854
},
859855
},
860856
});
861-
const server = new VoiceCallWebhookServer(
862-
config,
863-
manager,
864-
createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider,
865-
);
857+
const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue));
866858
await server.start();
867859
const handleInboundResponse = vi.fn(async () => {});
868860
(
@@ -913,7 +905,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
913905
initialMessage: "Hello from inbound greeting.",
914906
};
915907

916-
const clearTtsQueue = vi.fn();
908+
const clearTtsQueue = vi.fn<TwilioProviderTestDouble["clearTtsQueue"]>();
917909
const manager = {
918910
getActiveCalls: () => [call],
919911
getCallByProviderCallId: (providerCallId: string) =>
@@ -936,11 +928,7 @@ describe("VoiceCallWebhookServer barge-in suppression during initial message", (
936928
},
937929
},
938930
});
939-
const server = new VoiceCallWebhookServer(
940-
config,
941-
manager,
942-
createTwilioProvider(clearTtsQueue) as unknown as VoiceCallProvider,
943-
);
931+
const server = new VoiceCallWebhookServer(config, manager, createTwilioProvider(clearTtsQueue));
944932
await server.start();
945933

946934
try {

scripts/check-web-fetch-provider-boundaries.mjs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export async function collectWebFetchProviderBoundaryViolations() {
4141
ignoredDirNames,
4242
});
4343
for (const { relativeFile, content } of files) {
44-
if (allowedFiles.has(relativeFile) || relativeFile.includes(".test.")) {
44+
if (
45+
allowedFiles.has(relativeFile) ||
46+
relativeFile.includes(".test.") ||
47+
relativeFile.includes("test-support")
48+
) {
4549
continue;
4650
}
4751
const lines = content.split(/\r?\n/);

src/agents/pi-embedded-runner.openai-tool-id-preservation.test.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,12 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
6363
{
6464
name: "strips fc ids when replayable reasoning metadata is missing",
6565
withReasoning: false,
66-
expectedToolId: "call_123",
66+
expectedToolId: "call123",
6767
},
6868
{
6969
name: "keeps canonical call_id|fc_id pairings when replayable reasoning is present",
7070
withReasoning: true,
71-
expectedToolId: "call_123|fc_123",
71+
expectedToolId: "call123fc123",
7272
},
7373
])("$name", async ({ withReasoning, expectedToolId }) => {
7474
const result = await sanitizeSessionHistory({
@@ -87,4 +87,45 @@ describe("sanitizeSessionHistory openai tool id preservation", () => {
8787
const toolResult = result[1] as { toolCallId?: string };
8888
expect(toolResult.toolCallId).toBe(expectedToolId);
8989
});
90+
91+
it("repairs displaced tool results before downgrading openai pairing ids", async () => {
92+
const result = await sanitizeSessionHistory({
93+
messages: [
94+
castAgentMessage({
95+
role: "assistant",
96+
content: [{ type: "toolCall", id: "call_123|fc_123", name: "noop", arguments: {} }],
97+
}),
98+
castAgentMessage({
99+
role: "user",
100+
content: [{ type: "text", text: "still waiting" }],
101+
}),
102+
castAgentMessage({
103+
role: "toolResult",
104+
toolCallId: "call_123|fc_123",
105+
toolName: "noop",
106+
content: [{ type: "text", text: "ok" }],
107+
isError: false,
108+
}),
109+
],
110+
modelApi: "openai-responses",
111+
provider: "openai",
112+
modelId: "gpt-5.4",
113+
sessionManager: makeSessionManager(),
114+
sessionId: "test-session",
115+
});
116+
117+
const toolResult = result[1] as {
118+
role?: string;
119+
toolCallId?: string;
120+
content?: Array<{ type?: string; text?: string }>;
121+
isError?: boolean;
122+
};
123+
expect(toolResult.role).toBe("toolResult");
124+
expect(toolResult.toolCallId).toBe("call123");
125+
expect(toolResult.content?.[0]?.text).toBe("ok");
126+
expect(toolResult.isError).toBe(false);
127+
128+
const userMessage = result[2] as { role?: string };
129+
expect(userMessage.role).toBe("user");
130+
});
90131
});

src/agents/pi-embedded-runner/replay-history.ts

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -433,24 +433,38 @@ export async function sanitizeSessionHistory(params: {
433433
allowedToolNames: params.allowedToolNames,
434434
allowProviderOwnedThinkingReplay,
435435
});
436+
// OpenAI's fc_* pairing downgrade needs the raw call_id|fc_id separator intact,
437+
// but displaced tool results must first be repaired back next to their
438+
// assistant turn so the downgrade can rewrite both sides consistently.
439+
const openAIRepairedToolCalls =
440+
isOpenAIResponsesApi && policy.repairToolUseResultPairing
441+
? sanitizeToolUseResultPairing(sanitizedToolCalls, {
442+
erroredAssistantResultPolicy: "drop",
443+
})
444+
: sanitizedToolCalls;
445+
const openAISafeToolCalls = isOpenAIResponsesApi
446+
? downgradeOpenAIFunctionCallReasoningPairs(
447+
downgradeOpenAIReasoningBlocks(openAIRepairedToolCalls),
448+
)
449+
: sanitizedToolCalls;
436450
const sanitizedToolIds =
437-
policy.sanitizeToolCallIds && policy.toolCallIdMode && !isOpenAIResponsesApi
438-
? sanitizeToolCallIdsForCloudCodeAssist(sanitizedToolCalls, policy.toolCallIdMode, {
451+
policy.sanitizeToolCallIds && policy.toolCallIdMode
452+
? sanitizeToolCallIdsForCloudCodeAssist(openAISafeToolCalls, policy.toolCallIdMode, {
439453
preserveNativeAnthropicToolUseIds: policy.preserveNativeAnthropicToolUseIds,
440454
preserveReplaySafeThinkingToolCallIds: allowProviderOwnedThinkingReplay,
441455
allowedToolNames: params.allowedToolNames,
442456
})
443-
: sanitizedToolCalls;
444-
const repairedTools = policy.repairToolUseResultPairing
445-
? sanitizeToolUseResultPairing(sanitizedToolIds, {
446-
erroredAssistantResultPolicy: "drop",
447-
})
448-
: sanitizedToolIds;
457+
: openAISafeToolCalls;
458+
const repairedTools =
459+
!isOpenAIResponsesApi && policy.repairToolUseResultPairing
460+
? sanitizeToolUseResultPairing(sanitizedToolIds, {
461+
erroredAssistantResultPolicy: "drop",
462+
})
463+
: sanitizedToolIds;
449464
const sanitizedToolResults = stripToolResultDetails(repairedTools);
450465
const sanitizedCompactionUsage = ensureAssistantUsageSnapshots(
451466
stripStaleAssistantUsageBeforeLatestCompaction(sanitizedToolResults),
452467
);
453-
454468
const hasSnapshot = Boolean(params.provider || params.modelApi || params.modelId);
455469
const priorSnapshot = hasSnapshot ? readLastModelSnapshot(params.sessionManager) : null;
456470
const modelChanged = priorSnapshot
@@ -461,11 +475,6 @@ export async function sanitizeSessionHistory(params: {
461475
modelId: params.modelId,
462476
})
463477
: false;
464-
const sanitizedOpenAI = isOpenAIResponsesApi
465-
? downgradeOpenAIFunctionCallReasoningPairs(
466-
downgradeOpenAIReasoningBlocks(sanitizedCompactionUsage),
467-
)
468-
: sanitizedCompactionUsage;
469478
const provider = params.provider?.trim();
470479
const providerSanitized =
471480
provider && provider.length > 0
@@ -483,13 +492,13 @@ export async function sanitizeSessionHistory(params: {
483492
modelApi: params.modelApi,
484493
model: params.model,
485494
sessionId: params.sessionId,
486-
messages: sanitizedOpenAI,
495+
messages: sanitizedCompactionUsage,
487496
allowedToolNames: params.allowedToolNames,
488497
sessionState: createProviderReplaySessionState(params.sessionManager),
489498
},
490499
})
491500
: undefined;
492-
const sanitizedWithProvider = providerSanitized ?? sanitizedOpenAI;
501+
const sanitizedWithProvider = providerSanitized ?? sanitizedCompactionUsage;
493502

494503
if (hasSnapshot && (!priorSnapshot || modelChanged)) {
495504
appendModelSnapshot(params.sessionManager, {

src/agents/pi-embedded-runner/run.overflow-compaction.harness.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -488,14 +488,6 @@ export async function loadRunOverflowCompactionHarness(): Promise<{
488488
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
489489
}));
490490

491-
vi.doMock("./compact.js", () => ({
492-
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
493-
}));
494-
495-
vi.doMock("./compaction-hooks.js", () => ({
496-
runPostCompactionSideEffects: mockedRunPostCompactionSideEffects,
497-
}));
498-
499491
vi.doMock("./utils.js", () => ({
500492
describeUnknownError: vi.fn((err: unknown) => {
501493
if (err instanceof Error) {

src/agents/pi-embedded-runner/run/auth-controller.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe("createEmbeddedRunAuthController", () => {
148148

149149
it("applies runtime request overrides on the first auth exchange", async () => {
150150
const harness = createMutableAuthControllerHarness();
151-
const setRuntimeApiKey = vi.fn();
151+
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
152152

153153
mocks.getApiKeyForModel.mockResolvedValue({
154154
apiKey: "source-api-key",
@@ -218,7 +218,9 @@ describe("createEmbeddedRunAuthController", () => {
218218
version: 1,
219219
profiles: {},
220220
} as AuthProfileStore,
221-
authStorage: { setRuntimeApiKey: vi.fn() },
221+
authStorage: {
222+
setRuntimeApiKey: vi.fn<(provider: string, apiKey: string) => void>(),
223+
},
222224
profileCandidates: ["default"],
223225
initialThinkLevel: "medium",
224226
attemptedThinking: new Set(),
@@ -259,7 +261,7 @@ describe("createEmbeddedRunAuthController", () => {
259261
vi.useFakeTimers();
260262
try {
261263
const harness = createMutableAuthControllerHarness();
262-
const setRuntimeApiKey = vi.fn();
264+
const setRuntimeApiKey = vi.fn<(provider: string, apiKey: string) => void>();
263265
const staleRefresh = createDeferred<{
264266
apiKey: string;
265267
baseUrl: string;

0 commit comments

Comments
 (0)