Skip to content

Commit 3051748

Browse files
committed
fix(codex): hash dynamic tool fingerprints
1 parent 976a7bd commit 3051748

2 files changed

Lines changed: 74 additions & 9 deletions

File tree

extensions/codex/src/app-server/thread-lifecycle.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import {
1717
buildTurnStartParams,
1818
buildThreadResumeParams,
1919
buildThreadStartParams,
20+
areCodexDynamicToolFingerprintsCompatible,
2021
codexDynamicToolsFingerprint,
22+
codexLegacyDynamicToolsFingerprint,
2123
formatCodexThreadLifecycleTimingSummary,
2224
resolveCodexAppServerThreadModelSelection,
2325
resolveReasoningEffort,
@@ -363,6 +365,36 @@ describe("Codex app-server native code mode config", () => {
363365
expect(searchableFingerprint).not.toBe(directFingerprint);
364366
});
365367

368+
it("keeps hashed dynamic tool fingerprints compatible with legacy JSON bindings", () => {
369+
const tools = [
370+
{
371+
type: "function" as const,
372+
name: "message",
373+
description: "Send a visible message",
374+
inputSchema: {
375+
type: "object",
376+
additionalProperties: false,
377+
properties: {
378+
text: { type: "string" },
379+
},
380+
required: ["text"],
381+
},
382+
},
383+
];
384+
const hashed = codexDynamicToolsFingerprint(tools);
385+
const legacy = codexLegacyDynamicToolsFingerprint(tools);
386+
387+
expect(hashed).toMatch(/^sha256:/);
388+
expect(legacy).toContain('"name":"message"');
389+
expect(
390+
areCodexDynamicToolFingerprintsCompatible({
391+
previous: legacy,
392+
next: hashed,
393+
nextLegacy: legacy,
394+
}),
395+
).toBe(true);
396+
});
397+
366398
it("keeps OpenClaw skill catalogs out of developer instructions", () => {
367399
const params = createAttemptParams({ provider: "openai" });
368400
params.skillsSnapshot = {

extensions/codex/src/app-server/thread-lifecycle.ts

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
// Codex plugin module implements thread lifecycle behavior.
2+
import * as crypto from "node:crypto";
23
import {
34
buildSkillWorkshopPromptSection,
45
embeddedAgentLog,
@@ -349,8 +350,12 @@ export async function startOrResumeThread(params: {
349350
...params.timing,
350351
enabled: params.timing?.enabled ?? isCodexAppServerProfilerEnabled(params.params.config),
351352
});
353+
const legacyDynamicToolsFingerprint = lifecycleTiming.measureSync(
354+
"legacy-dynamic-tools-fingerprint",
355+
() => legacyFingerprintDynamicTools(params.dynamicTools),
356+
);
352357
const dynamicToolsFingerprint = lifecycleTiming.measureSync("dynamic-tools-fingerprint", () =>
353-
fingerprintDynamicTools(params.dynamicTools),
358+
hashDynamicToolFingerprint(legacyDynamicToolsFingerprint),
354359
);
355360
const dynamicToolsContainDeferred = flattenCodexDynamicToolFunctions(params.dynamicTools).some(
356361
(tool) => tool.deferLoading === true,
@@ -674,11 +679,12 @@ export async function startOrResumeThread(params: {
674679
!areDynamicToolFingerprintsCompatible(
675680
binding.dynamicToolsFingerprint,
676681
dynamicToolsFingerprint,
682+
legacyDynamicToolsFingerprint,
677683
)
678684
) {
679685
preserveExistingBinding = shouldStartTransientNoToolThread({
680686
previous: binding.dynamicToolsFingerprint,
681-
next: dynamicToolsFingerprint,
687+
nextHasDynamicTools: params.dynamicTools.length > 0,
682688
});
683689
if (preserveExistingBinding) {
684690
embeddedAgentLog.debug(
@@ -1663,19 +1669,32 @@ export function codexDynamicToolsFingerprint(dynamicTools: CodexDynamicToolSpec[
16631669
return fingerprintDynamicTools(dynamicTools);
16641670
}
16651671

1672+
export function codexLegacyDynamicToolsFingerprint(dynamicTools: CodexDynamicToolSpec[]): string {
1673+
return legacyFingerprintDynamicTools(dynamicTools);
1674+
}
1675+
16661676
export function areCodexDynamicToolFingerprintsCompatible(params: {
16671677
previous?: string;
16681678
next: string;
1679+
nextLegacy?: string;
16691680
}): boolean {
1670-
return areDynamicToolFingerprintsCompatible(params.previous, params.next);
1681+
return areDynamicToolFingerprintsCompatible(params.previous, params.next, params.nextLegacy);
16711682
}
16721683

16731684
function fingerprintDynamicTools(dynamicTools: CodexDynamicToolSpec[]): string {
1685+
return hashDynamicToolFingerprint(legacyFingerprintDynamicTools(dynamicTools));
1686+
}
1687+
1688+
function legacyFingerprintDynamicTools(dynamicTools: CodexDynamicToolSpec[]): string {
16741689
return JSON.stringify(
16751690
dynamicTools.map(fingerprintDynamicToolSpec).toSorted(compareJsonFingerprint),
16761691
);
16771692
}
16781693

1694+
function hashDynamicToolFingerprint(canonical: string): string {
1695+
return "sha256:" + crypto.createHash("sha256").update(canonical).digest("hex");
1696+
}
1697+
16791698
function fingerprintUserMcpServersConfigPatch(
16801699
configPatch: JsonObject | undefined,
16811700
): string | undefined {
@@ -1740,20 +1759,34 @@ function readActiveCodexTurnIds(thread: unknown): string[] {
17401759
.filter((turnId) => turnId.trim().length > 0);
17411760
}
17421761

1743-
const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = JSON.stringify([]);
1762+
const LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT = legacyFingerprintDynamicTools([]);
1763+
const EMPTY_DYNAMIC_TOOLS_FINGERPRINT = hashDynamicToolFingerprint(
1764+
LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT,
1765+
);
17441766

1745-
function areDynamicToolFingerprintsCompatible(previous: string | undefined, next: string): boolean {
1746-
return !previous || previous === next;
1767+
function areDynamicToolFingerprintsCompatible(
1768+
previous: string | undefined,
1769+
next: string,
1770+
nextLegacy?: string,
1771+
): boolean {
1772+
return !previous || previous === next || previous === nextLegacy;
17471773
}
17481774

17491775
function shouldStartTransientNoToolThread(params: {
17501776
previous: string | undefined;
1751-
next: string;
1777+
nextHasDynamicTools: boolean;
17521778
}): boolean {
17531779
return Boolean(
17541780
params.previous &&
1755-
params.previous !== EMPTY_DYNAMIC_TOOLS_FINGERPRINT &&
1756-
params.next === EMPTY_DYNAMIC_TOOLS_FINGERPRINT,
1781+
!isEmptyDynamicToolsFingerprint(params.previous) &&
1782+
!params.nextHasDynamicTools,
1783+
);
1784+
}
1785+
1786+
function isEmptyDynamicToolsFingerprint(fingerprint: string): boolean {
1787+
return (
1788+
fingerprint === EMPTY_DYNAMIC_TOOLS_FINGERPRINT ||
1789+
fingerprint === LEGACY_EMPTY_DYNAMIC_TOOLS_FINGERPRINT
17571790
);
17581791
}
17591792

0 commit comments

Comments
 (0)