
































@@ -71,6 +71,108 @@ vi.mock("../agents/subagent-registry-read.js", () => subagentRegistryReadMock);
71717272import { loadGatewaySessionRow } from "./session-utils.js";
737374+const MAIN_AGENT_ID = "main";
75+const TEST_MODEL = "openai/gpt-5.4";
76+77+type SingleRowCacheContext = {
78+now: number;
79+storePath: string;
80+};
81+82+type MovingChildFixture = {
83+oldParent: string;
84+newParent: string;
85+child: string;
86+store: Record<string, SessionEntry>;
87+};
88+89+async function withSingleRowCacheStore(
90+statePrefix: string,
91+workspace: string,
92+run: (context: SingleRowCacheContext) => Promise<void>,
93+): Promise<void> {
94+await withStateDirEnv(statePrefix, async () => {
95+const cfg: OpenClawConfig = {
96+agents: {
97+list: [
98+{
99+id: MAIN_AGENT_ID,
100+default: true,
101+ workspace,
102+},
103+],
104+defaults: { model: { primary: TEST_MODEL } },
105+},
106+} as OpenClawConfig;
107+setRuntimeConfigSnapshot(cfg, cfg);
108+await run({
109+now: Math.floor(Date.now() / 1_000) * 1_000 + 100,
110+storePath: resolveStorePath(cfg.session?.store, { agentId: MAIN_AGENT_ID }),
111+});
112+});
113+}
114+115+function parentSession(sessionId: string, now: number): SessionEntry {
116+return {
117+ sessionId,
118+updatedAt: now,
119+};
120+}
121+122+function runningChildSession(
123+sessionId: string,
124+parentSessionKey: string,
125+now: number,
126+): SessionEntry {
127+return {
128+ sessionId,
129+ parentSessionKey,
130+updatedAt: now,
131+status: "running",
132+};
133+}
134+135+function setSubagentControllerRun(
136+childSessionKey: string,
137+controllerSessionKey: string,
138+createdAt: number,
139+): void {
140+subagentRegistryReadMock.setSubagentRunsForTest([
141+{
142+ childSessionKey,
143+ controllerSessionKey,
144+requesterSessionKey: controllerSessionKey,
145+ createdAt,
146+},
147+]);
148+}
149+150+function createMovingChildFixture(now: number): MovingChildFixture {
151+const oldParent = "agent:main:subagent:parent-old";
152+const newParent = "agent:main:subagent:parent-new";
153+const child = "agent:main:subagent:child";
154+return {
155+ oldParent,
156+ newParent,
157+ child,
158+store: {
159+[oldParent]: parentSession("parent-old", now),
160+[newParent]: parentSession("parent-new", now),
161+[child]: runningChildSession("child", oldParent, now),
162+},
163+};
164+}
165+166+function expectChildMovedToNewParent(fixture: MovingChildFixture, now: number): void {
167+expect(
168+loadGatewaySessionRow(fixture.oldParent, { now: now + 50 })?.childSessions,
169+).toBeUndefined();
170+expect(loadGatewaySessionRow(fixture.newParent, { now: now + 50 })?.childSessions).toEqual([
171+fixture.child,
172+]);
173+expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
174+}
175+74176describe("single gateway session row child-session cache", () => {
75177afterEach(() => {
76178resetConfigRuntimeState();
@@ -80,173 +182,84 @@ describe("single gateway session row child-session cache", () => {
80182});
8118382184test("shares the child-session index across repeated single-row loads for the same store", async () => {
83-await withStateDirEnv("openclaw-single-row-cache-", async () => {
84-const cfg: OpenClawConfig = {
85-agents: {
86-list: [
87-{
88-id: "main",
89-default: true,
90-workspace: "/tmp/openclaw-single-row-cache",
91-},
92-],
93-defaults: { model: { primary: "openai/gpt-5.4" } },
94-},
95-} as OpenClawConfig;
96-setRuntimeConfigSnapshot(cfg, cfg);
97-const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
98-const store: Record<string, SessionEntry> = {
99-"agent:main:subagent:parent-a": {
100-sessionId: "parent-a",
101-updatedAt: now,
102-},
103-"agent:main:subagent:child-a": {
104-sessionId: "child-a",
105-parentSessionKey: "agent:main:subagent:parent-a",
106-updatedAt: now,
107-status: "running",
108-},
109-"agent:main:subagent:parent-b": {
110-sessionId: "parent-b",
111-updatedAt: now,
112-},
113-"agent:main:subagent:child-b": {
114-sessionId: "child-b",
115-parentSessionKey: "agent:main:subagent:parent-b",
116-updatedAt: now,
117-status: "running",
118-},
119-};
120-await saveSessionStore(resolveStorePath(cfg.session?.store, { agentId: "main" }), store);
121-122-const rowA = loadGatewaySessionRow("agent:main:subagent:parent-a", { now });
123-const rowB = loadGatewaySessionRow("agent:main:subagent:parent-b", { now: now + 50 });
124-const rowAAfterWindow = loadGatewaySessionRow("agent:main:subagent:parent-a", {
125-now: now + 1_500,
126-});
127-128-expect(rowA?.childSessions).toEqual(["agent:main:subagent:child-a"]);
129-expect(rowB?.childSessions).toEqual(["agent:main:subagent:child-b"]);
130-expect(rowAAfterWindow?.childSessions).toEqual(["agent:main:subagent:child-a"]);
131-expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
132-});
185+await withSingleRowCacheStore(
186+"openclaw-single-row-cache-",
187+"/tmp/openclaw-single-row-cache",
188+async ({ now, storePath }) => {
189+const store: Record<string, SessionEntry> = {
190+"agent:main:subagent:parent-a": parentSession("parent-a", now),
191+"agent:main:subagent:child-a": runningChildSession(
192+"child-a",
193+"agent:main:subagent:parent-a",
194+now,
195+),
196+"agent:main:subagent:parent-b": parentSession("parent-b", now),
197+"agent:main:subagent:child-b": runningChildSession(
198+"child-b",
199+"agent:main:subagent:parent-b",
200+now,
201+),
202+};
203+await saveSessionStore(storePath, store);
204+205+const rowA = loadGatewaySessionRow("agent:main:subagent:parent-a", { now });
206+const rowB = loadGatewaySessionRow("agent:main:subagent:parent-b", { now: now + 50 });
207+const rowAAfterWindow = loadGatewaySessionRow("agent:main:subagent:parent-a", {
208+now: now + 1_500,
209+});
210+211+expect(rowA?.childSessions).toEqual(["agent:main:subagent:child-a"]);
212+expect(rowB?.childSessions).toEqual(["agent:main:subagent:child-b"]);
213+expect(rowAAfterWindow?.childSessions).toEqual(["agent:main:subagent:child-a"]);
214+expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
215+},
216+);
133217});
134218135219test("refreshes subagent registry state while reusing store child candidates", async () => {
136-await withStateDirEnv("openclaw-single-row-cache-fresh-registry-", async () => {
137-const cfg: OpenClawConfig = {
138-agents: {
139-list: [
140-{
141-id: "main",
142-default: true,
143-workspace: "/tmp/openclaw-single-row-cache-fresh-registry",
144-},
145-],
146-defaults: { model: { primary: "openai/gpt-5.4" } },
147-},
148-} as OpenClawConfig;
149-setRuntimeConfigSnapshot(cfg, cfg);
150-const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
151-const oldParent = "agent:main:subagent:parent-old";
152-const newParent = "agent:main:subagent:parent-new";
153-const child = "agent:main:subagent:child";
154-const store: Record<string, SessionEntry> = {
155-[oldParent]: {
156-sessionId: "parent-old",
157-updatedAt: now,
158-},
159-[newParent]: {
160-sessionId: "parent-new",
161-updatedAt: now,
162-},
163-[child]: {
164-sessionId: "child",
165-parentSessionKey: oldParent,
166-updatedAt: now,
167-status: "running",
168-},
169-};
170-await saveSessionStore(resolveStorePath(cfg.session?.store, { agentId: "main" }), store);
171-172-subagentRegistryReadMock.setSubagentRunsForTest([
173-{
174-childSessionKey: child,
175-controllerSessionKey: oldParent,
176-requesterSessionKey: oldParent,
177-createdAt: now,
178-},
179-]);
180-expect(loadGatewaySessionRow(oldParent, { now })?.childSessions).toEqual([child]);
181-182-subagentRegistryReadMock.setSubagentRunsForTest([
183-{
184-childSessionKey: child,
185-controllerSessionKey: newParent,
186-requesterSessionKey: newParent,
187-createdAt: now + 25,
188-},
189-]);
190-expect(loadGatewaySessionRow(oldParent, { now: now + 50 })?.childSessions).toBeUndefined();
191-expect(loadGatewaySessionRow(newParent, { now: now + 50 })?.childSessions).toEqual([child]);
192-expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
193-});
220+await withSingleRowCacheStore(
221+"openclaw-single-row-cache-fresh-registry-",
222+"/tmp/openclaw-single-row-cache-fresh-registry",
223+async ({ now, storePath }) => {
224+const fixture = createMovingChildFixture(now);
225+await saveSessionStore(storePath, fixture.store);
226+227+setSubagentControllerRun(fixture.child, fixture.oldParent, now);
228+expect(loadGatewaySessionRow(fixture.oldParent, { now })?.childSessions).toEqual([
229+fixture.child,
230+]);
231+232+setSubagentControllerRun(fixture.child, fixture.newParent, now + 25);
233+expectChildMovedToNewParent(fixture, now);
234+},
235+);
194236});
195237196238test("rebuilds store child candidates after same-object session store writes", async () => {
197-await withStateDirEnv("openclaw-single-row-cache-write-version-", async () => {
198-const cfg: OpenClawConfig = {
199-agents: {
200-list: [
201-{
202-id: "main",
203-default: true,
204-workspace: "/tmp/openclaw-single-row-cache-write-version",
205-},
206-],
207-defaults: { model: { primary: "openai/gpt-5.4" } },
208-},
209-} as OpenClawConfig;
210-setRuntimeConfigSnapshot(cfg, cfg);
211-const now = Math.floor(Date.now() / 1_000) * 1_000 + 100;
212-const oldParent = "agent:main:subagent:parent-old";
213-const newParent = "agent:main:subagent:parent-new";
214-const child = "agent:main:subagent:child";
215-const storePath = resolveStorePath(cfg.session?.store, { agentId: "main" });
216-const store: Record<string, SessionEntry> = {
217-[oldParent]: {
218-sessionId: "parent-old",
219-updatedAt: now,
220-},
221-[newParent]: {
222-sessionId: "parent-new",
223-updatedAt: now,
224-},
225-[child]: {
226-sessionId: "child",
227-parentSessionKey: oldParent,
228-updatedAt: now,
229-status: "running",
230-},
231-};
232-await saveSessionStore(storePath, store);
233-234-expect(loadGatewaySessionRow(oldParent, { now })?.childSessions).toEqual([child]);
235-await updateSessionStore(
236-storePath,
237-(cachedStore) => {
238-const childEntry = cachedStore[child];
239-if (childEntry) {
240-childEntry.parentSessionKey = newParent;
241-childEntry.updatedAt = now + 25;
242-}
243-},
244-{ skipMaintenance: true, takeCacheOwnership: true },
245-);
239+await withSingleRowCacheStore(
240+"openclaw-single-row-cache-write-version-",
241+"/tmp/openclaw-single-row-cache-write-version",
242+async ({ now, storePath }) => {
243+const fixture = createMovingChildFixture(now);
244+await saveSessionStore(storePath, fixture.store);
246245247-expect(loadGatewaySessionRow(oldParent, { now: now + 50 })?.childSessions).toBeUndefined();
248-expect(loadGatewaySessionRow(newParent, { now: now + 50 })?.childSessions).toEqual([child]);
249-expect(subagentRegistryReadMock.buildSubagentRunReadIndex).not.toHaveBeenCalled();
250-});
246+expect(loadGatewaySessionRow(fixture.oldParent, { now })?.childSessions).toEqual([
247+fixture.child,
248+]);
249+await updateSessionStore(
250+storePath,
251+(cachedStore) => {
252+const childEntry = cachedStore[fixture.child];
253+if (childEntry) {
254+childEntry.parentSessionKey = fixture.newParent;
255+childEntry.updatedAt = now + 25;
256+}
257+},
258+{ skipMaintenance: true, takeCacheOwnership: true },
259+);
260+261+expectChildMovedToNewParent(fixture, now);
262+},
263+);
251264});
252265});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。