mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
clawdbot-403: preserve lifecycle patch row keys
This commit is contained in:
@@ -52,6 +52,88 @@ describe("session entry lifecycle seam", () => {
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves an existing raw row key while exposing normalized context", async () => {
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify({
|
||||
GLOBAL: {
|
||||
sessionId: "session-raw",
|
||||
updatedAt: 10,
|
||||
},
|
||||
}),
|
||||
);
|
||||
let contextKey: string | undefined;
|
||||
|
||||
await patchSessionLifecycleEntry(
|
||||
{ sessionKey: "GLOBAL", storePath },
|
||||
(entry, context) => {
|
||||
contextKey = context.sessionKey;
|
||||
entry.fastMode = true;
|
||||
return entry;
|
||||
},
|
||||
{ replaceEntry: true, skipMaintenance: true },
|
||||
);
|
||||
|
||||
const store = loadSessionStore(storePath, { skipCache: true });
|
||||
expect(contextKey).toBe("global");
|
||||
expect(store.GLOBAL?.fastMode).toBe(true);
|
||||
expect(store.global).toBeUndefined();
|
||||
});
|
||||
|
||||
it("collapses legacy aliases when a usable canonical row exists", async () => {
|
||||
fs.writeFileSync(
|
||||
storePath,
|
||||
JSON.stringify({
|
||||
"agent:main:main": {
|
||||
sessionId: "session-canonical",
|
||||
updatedAt: 10,
|
||||
fastMode: false,
|
||||
},
|
||||
"Agent:Main:Main": {
|
||||
sessionId: "session-legacy",
|
||||
updatedAt: 20,
|
||||
fastMode: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await patchSessionLifecycleEntry(
|
||||
{ sessionKey: "Agent:Main:Main", storePath },
|
||||
(entry) => {
|
||||
entry.fastMode = true;
|
||||
return entry;
|
||||
},
|
||||
{ replaceEntry: true, skipMaintenance: true },
|
||||
);
|
||||
|
||||
const store = loadSessionStore(storePath, { skipCache: true });
|
||||
expect(store["agent:main:main"]?.sessionId).toBe("session-legacy");
|
||||
expect(store["agent:main:main"]?.fastMode).toBe(true);
|
||||
expect(store["Agent:Main:Main"]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("preserves a fallback raw row key", async () => {
|
||||
await patchSessionLifecycleEntry(
|
||||
{ sessionKey: "UNKNOWN", storePath },
|
||||
(entry) => {
|
||||
entry.fastMode = true;
|
||||
return entry;
|
||||
},
|
||||
{
|
||||
fallbackEntry: {
|
||||
sessionId: "session-fallback",
|
||||
updatedAt: 10,
|
||||
},
|
||||
replaceEntry: true,
|
||||
skipMaintenance: true,
|
||||
},
|
||||
);
|
||||
|
||||
const store = loadSessionStore(storePath, { skipCache: true });
|
||||
expect(store.UNKNOWN?.fastMode).toBe(true);
|
||||
expect(store.unknown).toBeUndefined();
|
||||
});
|
||||
|
||||
it("patches multiple entries without exposing a mutable store", async () => {
|
||||
await upsertSessionEntry(
|
||||
{ sessionKey: "agent:main:one", storePath },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getRuntimeConfig } from "../io.js";
|
||||
import { resolveStorePath } from "./paths.js";
|
||||
import { hasMismatchedCaseSensitiveDeliveryProof } from "./store-entry.js";
|
||||
import {
|
||||
archiveRemovedSessionTranscripts,
|
||||
loadSessionStore,
|
||||
@@ -63,6 +64,11 @@ export async function patchSessionLifecycleEntry(
|
||||
if (!existing) {
|
||||
return { changed: false, entry: null };
|
||||
}
|
||||
const storageKey = resolveLifecyclePatchStorageKey({
|
||||
store,
|
||||
resolved,
|
||||
requestedKey: scope.sessionKey,
|
||||
});
|
||||
const patch = await update(structuredClone(existing), {
|
||||
existingEntry: resolved.existing ? structuredClone(resolved.existing) : undefined,
|
||||
sessionKey: resolved.normalizedKey,
|
||||
@@ -78,8 +84,11 @@ export async function patchSessionLifecycleEntry(
|
||||
: options.preserveActivity
|
||||
? mergeSessionEntryPreserveActivity(existing, patch)
|
||||
: mergeSessionEntry(existing, patch);
|
||||
store[resolved.normalizedKey] = next;
|
||||
store[storageKey] = next;
|
||||
for (const legacyKey of resolved.legacyKeys) {
|
||||
if (legacyKey === storageKey) {
|
||||
continue;
|
||||
}
|
||||
delete store[legacyKey];
|
||||
}
|
||||
return { changed: true, entry: next };
|
||||
@@ -219,6 +228,35 @@ function collectReferencedSessionIds(store: Record<string, SessionEntry>): Set<s
|
||||
);
|
||||
}
|
||||
|
||||
// Preserve the concrete row key for behavior-neutral file-backed updates. Callers
|
||||
// still receive the normalized key in context for canonical decisions.
|
||||
function resolveLifecyclePatchStorageKey(params: {
|
||||
store: Record<string, SessionEntry>;
|
||||
requestedKey: string;
|
||||
resolved: ReturnType<typeof resolveSessionStoreEntry>;
|
||||
}): string {
|
||||
if (
|
||||
params.resolved.existing &&
|
||||
params.store[params.resolved.normalizedKey] === params.resolved.existing
|
||||
) {
|
||||
return params.resolved.normalizedKey;
|
||||
}
|
||||
const canonicalEntry = params.store[params.resolved.normalizedKey];
|
||||
if (
|
||||
canonicalEntry &&
|
||||
!hasMismatchedCaseSensitiveDeliveryProof(canonicalEntry, params.resolved.normalizedKey)
|
||||
) {
|
||||
return params.resolved.normalizedKey;
|
||||
}
|
||||
const existingLegacyKey = params.resolved.legacyKeys.find(
|
||||
(legacyKey) => params.store[legacyKey] === params.resolved.existing,
|
||||
);
|
||||
if (existingLegacyKey) {
|
||||
return existingLegacyKey;
|
||||
}
|
||||
return params.requestedKey.trim() || params.resolved.normalizedKey;
|
||||
}
|
||||
|
||||
function rememberRemovedSessionFile(
|
||||
removedSessionFiles: Map<string, string | undefined>,
|
||||
entry: SessionEntry,
|
||||
|
||||
Reference in New Issue
Block a user