mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(codex): converge retained raw binding rows
(cherry picked from commit b4db960616)
This commit is contained in:
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user