Skip to content

Commit 440284f

Browse files
committed
fix: rekey ACP metadata during session key migration
1 parent 784e864 commit 440284f

4 files changed

Lines changed: 113 additions & 2 deletions

File tree

src/acp/runtime/session-meta.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db
88
import { withTempDir } from "../../test-helpers/temp-dir.js";
99
import {
1010
listAcpSessionEntries,
11+
moveAcpSessionMetaForMigration,
1112
readAcpSessionEntry,
13+
readAcpSessionMetaForEntry,
1214
upsertAcpSessionMeta,
1315
writeAcpSessionMetaForMigration,
1416
} from "./session-meta.js";
@@ -213,6 +215,57 @@ describe("ACP session metadata SQLite store", () => {
213215
});
214216
});
215217

218+
it("moves ACP metadata rows when session-store keys are canonicalized", async () => {
219+
await withTempDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
220+
const storePath = path.join(dir, "sessions.json");
221+
const databasePath = path.join(dir, "state", "openclaw.sqlite");
222+
const cfg = { session: { store: storePath } } as OpenClawConfig;
223+
const legacyKey = "agent:CODEX:acp:legacy-runtime";
224+
const canonicalKey = "agent:codex:acp:legacy-runtime";
225+
await writeSessionStoreForTestAsync(storePath, {
226+
[canonicalKey]: {
227+
sessionId: "sess-acp",
228+
updatedAt: 100,
229+
},
230+
});
231+
writeAcpSessionMetaForMigration({
232+
databasePath,
233+
sessionKey: legacyKey,
234+
sessionId: "sess-acp",
235+
meta: {
236+
backend: "acpx",
237+
agent: "codex",
238+
runtimeSessionName: legacyKey,
239+
mode: "persistent",
240+
state: "idle",
241+
lastActivityAt: 123,
242+
},
243+
});
244+
245+
expect(
246+
moveAcpSessionMetaForMigration({
247+
databasePath,
248+
fromSessionKey: legacyKey,
249+
toSessionKey: canonicalKey,
250+
entry: { sessionId: "sess-acp" },
251+
now: () => 200,
252+
}),
253+
).toBe(true);
254+
255+
expect(
256+
readAcpSessionMetaForEntry({
257+
databasePath,
258+
sessionKey: legacyKey,
259+
entry: { sessionId: "sess-acp" },
260+
}),
261+
).toBeUndefined();
262+
expect(
263+
readAcpSessionEntry({ cfg, databasePath, sessionKey: canonicalKey })?.acp
264+
?.runtimeSessionName,
265+
).toBe(legacyKey);
266+
});
267+
});
268+
216269
it("lists SQLite ACP rows while joining current session-store entries", async () => {
217270
await withTempDir({ prefix: "openclaw-acp-meta-" }, async (dir) => {
218271
const storePath = path.join(dir, "sessions", "codex.json");

src/acp/runtime/session-meta.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ function selectAcpSessionRow(db: DatabaseSync, sessionKey: string): AcpSessionRo
157157
);
158158
}
159159

160-
function acpSessionRowMatchesEntry(row: AcpSessionRow, entry: SessionEntry | undefined): boolean {
160+
function acpSessionRowMatchesEntry(
161+
row: AcpSessionRow,
162+
entry: Pick<SessionEntry, "sessionId"> | undefined,
163+
): boolean {
161164
// Rows tied to a specific sessionId are stale after the JSON session entry rotates.
162165
return row.session_id == null || row.session_id === entry?.sessionId;
163166
}
@@ -191,7 +194,7 @@ export function readAcpSessionMeta(params: {
191194

192195
export function readAcpSessionMetaForEntry(params: {
193196
sessionKey: string;
194-
entry: SessionEntry | undefined;
197+
entry: Pick<SessionEntry, "sessionId"> | undefined;
195198
env?: NodeJS.ProcessEnv;
196199
databasePath?: string;
197200
}): SessionAcpMeta | undefined {
@@ -248,6 +251,45 @@ export function writeAcpSessionMetaForMigration(params: {
248251
);
249252
}
250253

254+
export function moveAcpSessionMetaForMigration(params: {
255+
fromSessionKey: string;
256+
toSessionKey: string;
257+
entry?: Pick<SessionEntry, "sessionId">;
258+
env?: NodeJS.ProcessEnv;
259+
databasePath?: string;
260+
now?: () => number;
261+
}): boolean {
262+
const fromSessionKey = params.fromSessionKey.trim();
263+
const toSessionKey = params.toSessionKey.trim();
264+
if (!fromSessionKey || !toSessionKey || fromSessionKey === toSessionKey) {
265+
return false;
266+
}
267+
268+
let moved = false;
269+
runOpenClawStateWriteTransaction(
270+
(database) => {
271+
const row = selectAcpSessionRow(database.db, fromSessionKey);
272+
if (!row || !acpSessionRowMatchesEntry(row, params.entry)) {
273+
return;
274+
}
275+
upsertAcpSessionMetaRow(database.db, {
276+
...row,
277+
session_key: toSessionKey,
278+
updated_at: params.now?.() ?? Date.now(),
279+
});
280+
executeSqliteQuerySync(
281+
database.db,
282+
getAcpSessionKysely(database.db)
283+
.deleteFrom("acp_sessions")
284+
.where("session_key", "=", fromSessionKey),
285+
);
286+
moved = true;
287+
},
288+
{ env: params.env, path: params.databasePath },
289+
);
290+
return moved;
291+
}
292+
251293
function upsertAcpSessionMetaRow(db: DatabaseSync, row: Insertable<AcpSessionsTable>): void {
252294
executeSqliteQuerySync(
253295
db,

src/gateway/sessions-resolve-store.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ describe("resolveSessionKeyFromResolveParams store canonicalization", () => {
295295
p: { key: acpKey },
296296
}),
297297
).resolves.toEqual({ ok: true, key: acpKey });
298+
299+
await expect(
300+
resolveSessionKeyFromResolveParams({
301+
cfg,
302+
p: { key: acpKey },
303+
}),
304+
).resolves.toEqual({ ok: true, key: acpKey });
298305
});
299306
});
300307

src/gateway/sessions-resolve.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import {
77
errorShape,
88
type SessionsResolveParams,
99
} from "../../packages/gateway-protocol/src/index.js";
10+
import { moveAcpSessionMetaForMigration } from "../acp/runtime/session-meta.js";
1011
import { loadSessionStore, updateSessionStore, type SessionEntry } from "../config/sessions.js";
1112
import type { OpenClawConfig } from "../config/types.openclaw.js";
1213
import { resolveSessionIdMatchSelection } from "../sessions/session-id-resolution.js";
14+
import { isAcpSessionKey } from "../sessions/session-key-utils.js";
1315
import { parseSessionLabel } from "../sessions/session-label.js";
1416
import {
1517
filterAndSortSessionEntries,
@@ -161,6 +163,13 @@ export async function resolveSessionKeyFromResolveParams(params: {
161163
}
162164
});
163165
const migratedStore = loadSessionStore(target.storePath);
166+
if (isAcpSessionKey(target.canonicalKey) || isAcpSessionKey(legacyKey)) {
167+
moveAcpSessionMetaForMigration({
168+
fromSessionKey: legacyKey,
169+
toSessionKey: target.canonicalKey,
170+
entry: migratedStore[target.canonicalKey],
171+
});
172+
}
164173
if (
165174
!isResolvedSessionKeyVisible({
166175
cfg,

0 commit comments

Comments
 (0)