Skip to content

Commit 8c8162f

Browse files
committed
perf(gateway): borrow read-only session metadata
1 parent 152f68d commit 8c8162f

7 files changed

Lines changed: 115 additions & 10 deletions

File tree

src/acp/control-plane/manager.core.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export class AcpSessionManager {
197197
const acp = this.deps.readSessionEntry({
198198
cfg: params.cfg,
199199
sessionKey,
200+
clone: false,
200201
})?.acp;
201202
if (acp) {
202203
return {
@@ -2022,6 +2023,8 @@ export class AcpSessionManager {
20222023
await this.writeSessionMeta({
20232024
cfg: params.cfg,
20242025
sessionKey: params.sessionKey,
2026+
skipMaintenance: true,
2027+
takeCacheOwnership: true,
20252028
mutate: (current, entry) => {
20262029
if (!entry) {
20272030
return null;
@@ -2086,12 +2089,16 @@ export class AcpSessionManager {
20862089
entry: SessionEntry | undefined,
20872090
) => SessionAcpMeta | null | undefined;
20882091
failOnError?: boolean;
2092+
skipMaintenance?: boolean;
2093+
takeCacheOwnership?: boolean;
20892094
}): Promise<SessionEntry | null> {
20902095
try {
20912096
return await this.deps.upsertSessionMeta({
20922097
cfg: params.cfg,
20932098
sessionKey: params.sessionKey,
20942099
mutate: params.mutate,
2100+
...(params.skipMaintenance === true ? { skipMaintenance: true } : {}),
2101+
...(params.takeCacheOwnership === true ? { takeCacheOwnership: true } : {}),
20952102
});
20962103
} catch (error) {
20972104
if (params.failOnError) {

src/acp/control-plane/manager.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,38 @@ function extractStatesFromUpserts(): SessionAcpMeta["state"][] {
253253
return states;
254254
}
255255

256+
function extractStateUpsertPersistenceOptions(): Array<{
257+
state: SessionAcpMeta["state"];
258+
skipMaintenance?: boolean;
259+
takeCacheOwnership?: boolean;
260+
}> {
261+
const options: Array<{
262+
state: SessionAcpMeta["state"];
263+
skipMaintenance?: boolean;
264+
takeCacheOwnership?: boolean;
265+
}> = [];
266+
for (const [firstArg] of hoisted.upsertAcpSessionMetaMock.mock.calls) {
267+
const payload = firstArg as {
268+
skipMaintenance?: boolean;
269+
takeCacheOwnership?: boolean;
270+
mutate: (
271+
current: SessionAcpMeta | undefined,
272+
entry: { acp?: SessionAcpMeta } | undefined,
273+
) => SessionAcpMeta | null | undefined;
274+
};
275+
const current = readySessionMeta();
276+
const next = payload.mutate(current, { acp: current });
277+
if (next?.state && payload.skipMaintenance === true && payload.takeCacheOwnership === true) {
278+
options.push({
279+
state: next.state,
280+
...(payload.skipMaintenance === true ? { skipMaintenance: true } : {}),
281+
...(payload.takeCacheOwnership === true ? { takeCacheOwnership: true } : {}),
282+
});
283+
}
284+
}
285+
return options;
286+
}
287+
256288
function extractRuntimeOptionsFromUpserts(): Array<AcpSessionRuntimeOptions | undefined> {
257289
const options: Array<AcpSessionRuntimeOptions | undefined> = [];
258290
for (const [firstArg] of hoisted.upsertAcpSessionMetaMock.mock.calls) {
@@ -314,6 +346,10 @@ describe("AcpSessionManager", () => {
314346
}
315347
expect(resolved.error.code).toBe("ACP_SESSION_INIT_FAILED");
316348
expect(resolved.error.message).toContain("ACP metadata is missing");
349+
expectRecordFields(mockCallArg(hoisted.readAcpSessionEntryMock), {
350+
clone: false,
351+
sessionKey: "agent:codex:acp:session-1",
352+
});
317353
});
318354

319355
it("canonicalizes the main alias before ACP rehydrate after restart", async () => {
@@ -361,6 +397,10 @@ describe("AcpSessionManager", () => {
361397
agent: "main",
362398
sessionKey: "agent:main:main",
363399
});
400+
expect(extractStateUpsertPersistenceOptions()).toEqual([
401+
{ state: "running", skipMaintenance: true, takeCacheOwnership: true },
402+
{ state: "idle", skipMaintenance: true, takeCacheOwnership: true },
403+
]);
364404
});
365405

366406
it("tracks parented direct ACP turns in the task registry", async () => {

src/acp/runtime/session-meta.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function resolveSessionStorePathForAcp(params: {
6565
export function readAcpSessionEntry(params: {
6666
sessionKey: string;
6767
cfg?: OpenClawConfig;
68+
clone?: boolean;
6869
}): AcpSessionStoreEntry | null {
6970
const sessionKey = params.sessionKey.trim();
7071
if (!sessionKey) {
@@ -77,7 +78,7 @@ export function readAcpSessionEntry(params: {
7778
let store: Record<string, SessionEntry>;
7879
let storeReadFailed = false;
7980
try {
80-
store = loadSessionStore(storePath);
81+
store = loadSessionStore(storePath, params.clone === false ? { clone: false } : undefined);
8182
} catch {
8283
storeReadFailed = true;
8384
store = {};
@@ -135,6 +136,8 @@ export async function listAcpSessionEntries(params: {
135136
export async function upsertAcpSessionMeta(params: {
136137
sessionKey: string;
137138
cfg?: OpenClawConfig;
139+
skipMaintenance?: boolean;
140+
takeCacheOwnership?: boolean;
138141
mutate: (
139142
current: SessionAcpMeta | undefined,
140143
entry: SessionEntry | undefined,
@@ -174,6 +177,8 @@ export async function upsertAcpSessionMeta(params: {
174177
{
175178
activeSessionKey: normalizeLowercaseStringOrEmpty(sessionKey),
176179
allowDropAcpMetaSessionKeys: [sessionKey],
180+
...(params.skipMaintenance === true ? { skipMaintenance: true } : {}),
181+
...(params.takeCacheOwnership === true ? { takeCacheOwnership: true } : {}),
177182
},
178183
);
179184
}

src/agents/command/session.resolve-session-key.test.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import type { OpenClawConfig } from "../../config/config.js";
33
import type { SessionEntry } from "../../config/sessions/types.js";
44

55
const hoisted = vi.hoisted(() => ({
6-
loadSessionStoreMock: vi.fn<(storePath: string) => Record<string, SessionEntry>>(),
6+
loadSessionStoreMock:
7+
vi.fn<(storePath: string, opts?: { clone?: boolean }) => Record<string, SessionEntry>>(),
78
listAgentIdsMock: vi.fn<() => string[]>(),
89
}));
910

1011
vi.mock("../../config/sessions/store-load.js", () => ({
11-
loadSessionStore: (storePath: string) => hoisted.loadSessionStoreMock(storePath),
12+
loadSessionStore: (storePath: string, opts?: { clone?: boolean }) =>
13+
hoisted.loadSessionStoreMock(storePath, opts),
1214
}));
1315

1416
vi.mock("../../config/sessions/paths.js", () => ({
@@ -127,4 +129,36 @@ describe("resolveSessionKeyForRequest", () => {
127129
expect(result.storePath).toBe("/stores/embedded-agent.json");
128130
expect(hoisted.loadSessionStoreMock).toHaveBeenCalledTimes(1);
129131
});
132+
133+
it("borrows session stores when requested", () => {
134+
const mainStore = {
135+
"agent:main:main": { sessionId: "sid", updatedAt: 10 },
136+
} satisfies Record<string, SessionEntry>;
137+
const otherStore = {
138+
"agent:other:acp:sid": { sessionId: "sid", updatedAt: 20 },
139+
} satisfies Record<string, SessionEntry>;
140+
mockSessionStores({
141+
"/stores/main.json": mainStore,
142+
"/stores/other.json": otherStore,
143+
});
144+
145+
const result = resolveSessionKeyForRequest({
146+
cfg: {
147+
session: {
148+
store: "/stores/{agentId}.json",
149+
},
150+
} satisfies OpenClawConfig,
151+
sessionId: "sid",
152+
clone: false,
153+
});
154+
155+
expect(result.sessionKey).toBe("agent:other:acp:sid");
156+
expect(result.sessionStore).toBe(otherStore);
157+
expect(hoisted.loadSessionStoreMock).toHaveBeenCalledWith("/stores/main.json", {
158+
clone: false,
159+
});
160+
expect(hoisted.loadSessionStoreMock).toHaveBeenCalledWith("/stores/other.json", {
161+
clone: false,
162+
});
163+
});
130164
});

src/agents/command/session.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ function resolveLegacyMainStoreSessionForDefaultAgent(opts: {
6969
sessionKey?: string;
7070
sessionStore: Record<string, SessionEntry>;
7171
storePath: string;
72+
cloneOnWrite?: boolean;
7273
}): SessionKeyResolution | undefined {
7374
if (opts.defaultAgentId === DEFAULT_AGENT_ID || !opts.sessionKey) {
7475
return undefined;
@@ -92,24 +93,29 @@ function resolveLegacyMainStoreSessionForDefaultAgent(opts: {
9293
for (const legacyKey of legacyKeys) {
9394
const legacyEntry = opts.sessionStore[legacyKey];
9495
if (legacyEntry) {
95-
opts.sessionStore[opts.sessionKey] = { ...legacyEntry };
96+
const sessionStore = opts.cloneOnWrite ? { ...opts.sessionStore } : opts.sessionStore;
97+
sessionStore[opts.sessionKey] = { ...legacyEntry };
9698
return {
9799
sessionKey: opts.sessionKey,
98-
sessionStore: opts.sessionStore,
100+
sessionStore,
99101
storePath: opts.storePath,
100102
};
101103
}
102104
}
103105
return undefined;
104106
}
105-
const legacyStore = loadSessionStore(legacyStorePath);
107+
const legacyStore = loadSessionStore(
108+
legacyStorePath,
109+
opts.cloneOnWrite ? { clone: false } : undefined,
110+
);
106111
for (const legacyKey of legacyKeys) {
107112
const legacyEntry = legacyStore[legacyKey];
108113
if (legacyEntry) {
109-
opts.sessionStore[opts.sessionKey] = { ...legacyEntry };
114+
const sessionStore = opts.cloneOnWrite ? { ...opts.sessionStore } : opts.sessionStore;
115+
sessionStore[opts.sessionKey] = { ...legacyEntry };
110116
return {
111117
sessionKey: opts.sessionKey,
112-
sessionStore: opts.sessionStore,
118+
sessionStore,
113119
storePath: opts.storePath,
114120
};
115121
}
@@ -124,6 +130,7 @@ function collectSessionIdMatchesForRequest(opts: {
124130
storeAgentId?: string;
125131
sessionId: string;
126132
searchOtherAgentStores: boolean;
133+
clone?: boolean;
127134
}): SessionIdMatchSet {
128135
const matches: Array<[string, SessionEntry]> = [];
129136
const primaryStoreMatches: Array<[string, SessionEntry]> = [];
@@ -160,7 +167,10 @@ function collectSessionIdMatchesForRequest(opts: {
160167
continue;
161168
}
162169
const candidateStorePath = resolveStorePath(opts.cfg.session?.store, { agentId });
163-
addMatches(loadSessionStore(candidateStorePath), candidateStorePath);
170+
addMatches(
171+
loadSessionStore(candidateStorePath, opts.clone === false ? { clone: false } : undefined),
172+
candidateStorePath,
173+
);
164174
}
165175

166176
return { matches, primaryStoreMatches, storeByKey };
@@ -203,6 +213,7 @@ export function resolveSessionKeyForRequest(opts: {
203213
sessionId?: string;
204214
sessionKey?: string;
205215
agentId?: string;
216+
clone?: boolean;
206217
}): SessionKeyResolution {
207218
const sessionCfg = opts.cfg.session;
208219
const scope = sessionCfg?.scope ?? "per-sender";
@@ -226,7 +237,8 @@ export function resolveSessionKeyForRequest(opts: {
226237
const storePath = resolveStorePath(sessionCfg?.store, {
227238
agentId: storeAgentId,
228239
});
229-
const sessionStore = loadSessionStore(storePath);
240+
const loadOptions = opts.clone === false ? { clone: false as const } : undefined;
241+
const sessionStore = loadSessionStore(storePath, loadOptions);
230242

231243
const ctx: MsgContext | undefined = opts.to?.trim() ? { From: opts.to } : undefined;
232244
let sessionKey: string | undefined =
@@ -240,6 +252,7 @@ export function resolveSessionKeyForRequest(opts: {
240252
sessionKey,
241253
sessionStore,
242254
storePath,
255+
cloneOnWrite: opts.clone === false,
243256
});
244257
if (legacyMainSession) {
245258
return legacyMainSession;
@@ -262,6 +275,7 @@ export function resolveSessionKeyForRequest(opts: {
262275
storeAgentId,
263276
sessionId: requestedSessionId,
264277
searchOtherAgentStores: requestedAgentId === undefined,
278+
...(opts.clone === false ? { clone: false } : {}),
265279
});
266280
const preferredSelection = resolveSessionIdMatchSelection(matches, requestedSessionId);
267281
const currentStoreSelection =
@@ -293,6 +307,7 @@ export function resolveSession(opts: {
293307
sessionId?: string;
294308
sessionKey?: string;
295309
agentId?: string;
310+
clone?: boolean;
296311
}): SessionResolution {
297312
const sessionCfg = opts.cfg.session;
298313
const { sessionKey, sessionStore, storePath } = resolveSessionKeyForRequest({
@@ -301,6 +316,7 @@ export function resolveSession(opts: {
301316
sessionId: opts.sessionId,
302317
sessionKey: opts.sessionKey,
303318
agentId: opts.agentId,
319+
...(opts.clone === false ? { clone: false } : {}),
304320
});
305321
const now = Date.now();
306322

src/agents/pi-embedded-runner.e2e.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ describe("runEmbeddedPiAgent", () => {
455455
cfg,
456456
sessionId: "resume-123",
457457
agentId: undefined,
458+
clone: false,
458459
});
459460
expect(firstRunEmbeddedAttemptParams().sessionKey).toBe("agent:test:resolved");
460461
});
@@ -495,6 +496,7 @@ describe("runEmbeddedPiAgent", () => {
495496
cfg,
496497
sessionId: "resume-124",
497498
agentId: undefined,
499+
clone: false,
498500
});
499501
expect(firstRunEmbeddedAttemptParams().sessionKey).toBeUndefined();
500502
});

src/agents/pi-embedded-runner/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ function backfillSessionKey(params: {
379379
: resolveSessionKeyForRequest({
380380
cfg: params.config,
381381
sessionId: params.sessionId,
382+
clone: false,
382383
});
383384
return normalizeOptionalString(resolved.sessionKey);
384385
} catch (err) {

0 commit comments

Comments
 (0)