fix(codex): converge retained raw binding rows

(cherry picked from commit b4db960616)
This commit is contained in:
Vincent Koc
2026-07-11 15:14:41 -07:00
parent 78edf1cec1
commit 39fac06f48
3 changed files with 219 additions and 6 deletions
@@ -353,6 +353,156 @@ describe("codex doctor contract", () => {
await fs.rm(fixture.stateDir, { recursive: true, force: true });
});
it("normalizes a partial raw conversation import before copying the session row", async () => {
const threadId = "thread-partial-import";
const sessionId = "partial-import";
const sessionKey = "agent:main:partial-import";
const emptyConversation: StoredCodexAppServerBinding = {
version: 1,
state: "active",
binding: {
threadId,
cwd: "",
dynamicToolsFingerprint: "",
},
};
const rawFingerprint = "x".repeat(
65_535 - Buffer.byteLength(JSON.stringify(emptyConversation)),
);
const rawConversation: StoredCodexAppServerBinding = {
...emptyConversation,
binding: {
...emptyConversation.binding,
dynamicToolsFingerprint: rawFingerprint,
},
};
const rawSession = { ...rawConversation, sessionId };
expect(Buffer.byteLength(JSON.stringify(rawConversation))).toBe(65_535);
expect(Buffer.byteLength(JSON.stringify(rawSession))).toBeGreaterThan(65_536);
const fixture = await createBindingMigrationFixture({
name: sessionId,
sessionIndex: {
[sessionKey]: {
sessionId,
sessionFile: `${sessionId}.jsonl`,
},
},
threadId,
binding: { dynamicToolsFingerprint: rawFingerprint },
});
const conversationKey = bindingStoreKey({
kind: "conversation",
bindingId: legacyCodexConversationBindingId(fixture.transcriptPath),
});
const sessionBindingKey = bindingStoreKey({
kind: "session",
agentId: "main",
sessionId,
sessionKey,
});
const store = openBindingStore(fixture.env);
await store.register(conversationKey, rawConversation);
await expect(fixture.migration.migrateLegacyState(fixture.params)).resolves.toEqual({
changes: [
"Migrated 1 Codex app-server binding sidecar(s) to plugin state and archived the legacy sources",
],
warnings: [],
});
const expectedFingerprint = hashCodexAppServerBindingFingerprint(rawFingerprint);
await expect(store.lookup(conversationKey)).resolves.toMatchObject({
state: "active",
binding: { dynamicToolsFingerprint: expectedFingerprint },
});
await expect(store.lookup(sessionBindingKey)).resolves.toMatchObject({
state: "active",
sessionId,
binding: { dynamicToolsFingerprint: expectedFingerprint },
});
await expect(fs.access(fixture.sidecarPath)).rejects.toThrow();
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined();
await expect(fixture.migration.migrateLegacyState(fixture.params)).resolves.toEqual({
changes: [],
warnings: [],
});
await fs.rm(fixture.stateDir, { recursive: true, force: true });
});
it("normalizes retained raw conversation and session rows before comparison", async () => {
const threadId = "thread-retained-import";
const sessionId = "retained-import";
const sessionKey = "agent:main:retained-import";
const rawFingerprint = "x".repeat(60_000);
const rawConversation: StoredCodexAppServerBinding = {
version: 1,
state: "active",
binding: {
threadId,
cwd: "",
dynamicToolsFingerprint: rawFingerprint,
},
};
const rawSession: StoredCodexAppServerBinding = {
...rawConversation,
sessionId,
};
expect(Buffer.byteLength(JSON.stringify(rawSession))).toBeLessThan(65_536);
const fixture = await createBindingMigrationFixture({
name: sessionId,
sessionIndex: {
[sessionKey]: {
sessionId,
sessionFile: `${sessionId}.jsonl`,
},
},
threadId,
binding: { dynamicToolsFingerprint: rawFingerprint },
});
const conversationKey = bindingStoreKey({
kind: "conversation",
bindingId: legacyCodexConversationBindingId(fixture.transcriptPath),
});
const sessionBindingKey = bindingStoreKey({
kind: "session",
agentId: "main",
sessionId,
sessionKey,
});
const store = openBindingStore(fixture.env);
await store.register(conversationKey, rawConversation);
await store.register(sessionBindingKey, rawSession);
await expect(fixture.migration.migrateLegacyState(fixture.params)).resolves.toEqual({
changes: [
"Migrated 1 Codex app-server binding sidecar(s) to plugin state and archived the legacy sources",
],
warnings: [],
});
const expectedFingerprint = hashCodexAppServerBindingFingerprint(rawFingerprint);
await expect(store.lookup(conversationKey)).resolves.toMatchObject({
state: "active",
binding: { dynamicToolsFingerprint: expectedFingerprint },
});
await expect(store.lookup(sessionBindingKey)).resolves.toMatchObject({
state: "active",
sessionId,
binding: { dynamicToolsFingerprint: expectedFingerprint },
});
await expect(fs.access(fixture.sidecarPath)).rejects.toThrow();
await expect(fs.access(`${fixture.sidecarPath}.migrated`)).resolves.toBeUndefined();
await expect(fixture.migration.migrateLegacyState(fixture.params)).resolves.toEqual({
changes: [],
warnings: [],
});
await fs.rm(fixture.stateDir, { recursive: true, force: true });
});
it("matches an owner through the contained fallback for a stale session file locator", async () => {
const sessionKey = "agent:main:stale-locator";
const fixture = await createBindingMigrationFixture({
@@ -280,6 +280,21 @@ function normalizeLegacyBindingFingerprints(
return normalized;
}
export function normalizeStoredCodexAppServerBindingFingerprints(
value: unknown,
): StoredCodexAppServerBinding | undefined {
const stored = readStoredCodexAppServerBinding(value);
if (!stored || stored.state !== "active") {
return stored;
}
const binding = normalizeLegacyBindingFingerprints(
stored.binding as unknown as Record<string, unknown>,
);
return binding === stored.binding
? stored
: readStoredCodexAppServerBinding({ ...stored, binding });
}
/** Encodes a migrated sidecar binding as one canonical plugin-state row. */
export function createStoredCodexAppServerBinding(
value: unknown,
@@ -424,7 +424,12 @@ async function migrateSource(
]);
const raw = JSON.parse(contents) as Record<string, unknown>;
const [
{ bindingStoreKey, createStoredCodexAppServerBinding, readStoredCodexAppServerBinding },
{
bindingStoreKey,
createStoredCodexAppServerBinding,
normalizeStoredCodexAppServerBindingFingerprints,
readStoredCodexAppServerBinding,
},
{ legacyCodexConversationBindingId },
] = await Promise.all([
import("../app-server/session-binding.js"),
@@ -467,17 +472,53 @@ async function migrateSource(
bindingId: legacyCodexConversationBindingId(sessionFile),
}),
);
const normalizeStoredRow = async (
key: string,
current: MigratedBindingRow,
): Promise<{ value?: MigratedBindingRow; warning?: string }> => {
const parsed = readStoredCodexAppServerBinding(current);
if (!parsed) {
return { warning: `canonical plugin state is invalid at ${key}` };
}
const normalized = normalizeStoredCodexAppServerBindingFingerprints(parsed);
if (!normalized) {
return { warning: `canonical plugin state is invalid at ${key}` };
}
if (isDeepStrictEqual(parsed, normalized)) {
return { value: parsed };
}
if (parsed.lease && parsed.lease.expiresAt > Date.now()) {
return { warning: `canonical plugin state is leased at ${key}` };
}
const update = store.update;
if (!update) {
return { warning: `canonical plugin state could not be normalized at ${key}` };
}
await update(key, (candidate) => {
const candidateParsed = readStoredCodexAppServerBinding(candidate);
if (!candidateParsed || !isDeepStrictEqual(candidateParsed, parsed)) {
return undefined;
}
return normalized;
});
const persisted = readStoredCodexAppServerBinding(await store.lookup(key));
if (!persisted || !isDeepStrictEqual(persisted, normalized)) {
return { warning: `canonical plugin state changed at ${key}` };
}
importedKeys++;
return { value: normalized };
};
let currentConversation: MigratedBindingRow | undefined;
for (const key of conversationKeys) {
const current = await store.lookup(key);
if (current === undefined) {
continue;
}
const parsed = readStoredCodexAppServerBinding(current);
if (!parsed) {
return retain(`canonical plugin state is invalid at ${key}`);
const result = await normalizeStoredRow(key, current);
if (result.warning || !result.value) {
return retain(result.warning ?? `canonical plugin state is invalid at ${key}`);
}
currentConversation ??= parsed;
currentConversation ??= result.value;
}
const stored = currentConversation ?? baseStored;
const sessionKey = owner
@@ -509,7 +550,14 @@ async function migrateSource(
};
for (const entry of entries) {
const current = await store.lookup(entry.key);
if (current !== undefined && !hasExpected(current, entry.value)) {
if (current === undefined) {
continue;
}
const result = await normalizeStoredRow(entry.key, current);
if (result.warning || !result.value) {
return retain(result.warning ?? `canonical plugin state is invalid at ${entry.key}`);
}
if (!hasExpected(result.value, entry.value)) {
return retain(`canonical plugin state changed at ${entry.key}`);
}
}