From 5479a6272632246b51ddb4ef02603ed37ed8110d Mon Sep 17 00:00:00 2001 From: Jerry-Xin Date: Sat, 14 Mar 2026 21:08:29 +0800 Subject: [PATCH 1/3] fix(gateway): include agent:main:main in sessions_list RPC response Adds regression tests for #45754 to ensure the main agent session is always included in sessions_list results. The tests verify: - Both agent:main:main and channel sessions in same store are returned - agent:main:main is returned when it is the only session - Combined store loading with template paths includes main session - Discovery works when main agent is not in agents.list - Discovery works with no agents.list configured - Bare 'main' key is canonicalized to agent:main:main The core listSessionsFromStore and loadCombinedSessionStoreForGateway functions work correctly as demonstrated by these passing tests. --- src/gateway/session-utils.test.ts | 214 ++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 3c69ce1bcd7..c55202f8f25 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -872,3 +872,217 @@ describe("loadCombinedSessionStoreForGateway includes disk-only agents (#32804)" }); }); }); + +describe("listSessionsFromStore returns agent:main:main (#45754)", () => { + test("both agent:main:main and channel sessions in same store are returned", () => { + const cfg = { + session: { mainKey: "main" }, + agents: { list: [{ id: "main", default: true }] }, + } as OpenClawConfig; + + const store: Record = { + "agent:main:main": { + sessionId: "sess-main", + updatedAt: Date.now(), + } as SessionEntry, + "agent:main:feishu:user123": { + sessionId: "sess-feishu", + updatedAt: Date.now() - 1000, + } as SessionEntry, + }; + + const result = listSessionsFromStore({ + cfg, + storePath: "/tmp/sessions.json", + store, + opts: {}, + }); + + expect(result.sessions).toHaveLength(2); + expect(result.sessions.map((s) => s.key)).toEqual( + expect.arrayContaining(["agent:main:main", "agent:main:feishu:user123"]), + ); + }); + + test("agent:main:main is returned when it is the only session", () => { + const cfg = { + session: { mainKey: "main" }, + agents: { list: [{ id: "main", default: true }] }, + } as OpenClawConfig; + + const store: Record = { + "agent:main:main": { + sessionId: "sess-main", + updatedAt: Date.now(), + } as SessionEntry, + }; + + const result = listSessionsFromStore({ + cfg, + storePath: "/tmp/sessions.json", + store, + opts: {}, + }); + + expect(result.sessions).toHaveLength(1); + expect(result.sessions[0]?.key).toBe("agent:main:main"); + }); + + test("agent:main:main returned via combined store with template path", async () => { + await withStateDirEnv("openclaw-main-main-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + fs.mkdirSync(mainDir, { recursive: true }); + + fs.writeFileSync( + path.join(mainDir, "sessions.json"), + JSON.stringify({ + "agent:main:main": { sessionId: "s-main", updatedAt: 100 }, + "agent:main:feishu:user123": { sessionId: "s-feishu", updatedAt: 200 }, + }), + "utf8", + ); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + agents: { + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + expect(store["agent:main:main"]).toBeDefined(); + expect(store["agent:main:feishu:user123"]).toBeDefined(); + + const result = listSessionsFromStore({ + cfg, + storePath: path.join(mainDir, "sessions.json"), + store, + opts: {}, + }); + + expect(result.sessions).toHaveLength(2); + expect(result.sessions.map((s) => s.key)).toEqual( + expect.arrayContaining(["agent:main:main", "agent:main:feishu:user123"]), + ); + }); + }); + + test("agent:main:main discovered when only other agents in agents.list", async () => { + await withStateDirEnv("openclaw-main-discovered-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + const feishuDir = path.join(agentsDir, "feishu", "sessions"); + fs.mkdirSync(mainDir, { recursive: true }); + fs.mkdirSync(feishuDir, { recursive: true }); + + // Main agent has sessions but is NOT in agents.list + fs.writeFileSync( + path.join(mainDir, "sessions.json"), + JSON.stringify({ + "agent:main:main": { sessionId: "s-main", updatedAt: 100 }, + "agent:main:feishu:user123": { sessionId: "s-feishu", updatedAt: 200 }, + }), + "utf8", + ); + + // Empty store for feishu agent (which IS in agents.list) + fs.writeFileSync(path.join(feishuDir, "sessions.json"), JSON.stringify({}), "utf8"); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + agents: { + // Only feishu is configured - main should be discovered from disk + list: [{ id: "feishu", default: true }], + }, + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + + // Both sessions from the main agent's store should be discovered + expect(store["agent:main:main"]).toBeDefined(); + expect(store["agent:main:feishu:user123"]).toBeDefined(); + }); + }); + + test("agent:main:main discovered with no agents.list configured", async () => { + await withStateDirEnv("openclaw-no-agents-list-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + fs.mkdirSync(mainDir, { recursive: true }); + + fs.writeFileSync( + path.join(mainDir, "sessions.json"), + JSON.stringify({ + "agent:main:main": { sessionId: "s-main", updatedAt: 100 }, + "agent:main:feishu:user123": { sessionId: "s-feishu", updatedAt: 200 }, + }), + "utf8", + ); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + // No agents.list - should use default "main" agent + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + expect(store["agent:main:main"]).toBeDefined(); + expect(store["agent:main:feishu:user123"]).toBeDefined(); + }); + }); + + test("bare 'main' key in store is canonicalized to agent:main:main", async () => { + await withStateDirEnv("openclaw-bare-main-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + fs.mkdirSync(mainDir, { recursive: true }); + + // Store has bare "main" key (legacy format) + fs.writeFileSync( + path.join(mainDir, "sessions.json"), + JSON.stringify({ + main: { sessionId: "s-main", updatedAt: 100 }, + "feishu:user123": { sessionId: "s-feishu", updatedAt: 200 }, + }), + "utf8", + ); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + agents: { + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + + // Bare keys should be canonicalized to agent-prefixed form + expect(store["agent:main:main"]).toBeDefined(); + expect(store["agent:main:feishu:user123"]).toBeDefined(); + + const result = listSessionsFromStore({ + cfg, + storePath: path.join(mainDir, "sessions.json"), + store, + opts: {}, + }); + + expect(result.sessions).toHaveLength(2); + expect(result.sessions.map((s) => s.key)).toEqual( + expect.arrayContaining(["agent:main:main", "agent:main:feishu:user123"]), + ); + }); + }); +}); From 305ccc168aa72743aeca2757ecf6d061fcc8f9d2 Mon Sep 17 00:00:00 2001 From: Jerry-Xin Date: Mon, 16 Mar 2026 18:06:25 +0800 Subject: [PATCH 2/3] fix(gateway): ensure agent:main:main in combined store when sessions.json missing Add ensureMainSessionKey call to the combined-store path so agent:main:main is injected even when the store file does not exist yet. Add test covering the missing-store scenario reported in #45754. --- src/gateway/session-utils.test.ts | 24 ++++++++++++++++++++++++ src/gateway/session-utils.ts | 1 + 2 files changed, 25 insertions(+) diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index c55202f8f25..108a44e8295 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -1085,4 +1085,28 @@ describe("listSessionsFromStore returns agent:main:main (#45754)", () => { ); }); }); + + test("agent:main:main included when sessions.json does not exist yet", async () => { + await withStateDirEnv("openclaw-no-store-file-", async ({ stateDir }) => { + const agentsDir = path.join(stateDir, "agents"); + const mainDir = path.join(agentsDir, "main", "sessions"); + // Create directory structure but do NOT create sessions.json + fs.mkdirSync(mainDir, { recursive: true }); + + const cfg = { + session: { + mainKey: "main", + store: path.join(stateDir, "agents", "{agentId}", "sessions", "sessions.json"), + }, + agents: { + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; + + const { store } = loadCombinedSessionStoreForGateway(cfg); + + // agent:main:main must be present even when store file does not exist + expect(store["agent:main:main"]).toBeDefined(); + }); + }); }); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 00a2cb7747e..f974e8394b3 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -709,6 +709,7 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): { canonicalKey, }); } + ensureMainSessionKey(cfg, combined); return { storePath, store: combined }; } From 799829a8906a04d44ef9a0d6355a8214de5f34ae Mon Sep 17 00:00:00 2001 From: Jerry-Xin Date: Mon, 16 Mar 2026 20:08:05 +0800 Subject: [PATCH 3/3] fix(gateway): ensure main session key in multi-store branch --- src/gateway/session-utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index f974e8394b3..697798a8962 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -731,6 +731,8 @@ export function loadCombinedSessionStoreForGateway(cfg: OpenClawConfig): { } } + ensureMainSessionKey(cfg, combined); + const storePath = typeof storeConfig === "string" && storeConfig.trim() ? storeConfig.trim() : "(multiple)"; return { storePath, store: combined };