From c92265a51b72988cdee2af383a7dd0afda288dd5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 17 Jan 2026 07:41:06 +0000 Subject: [PATCH] refactor: canonicalize gateway session store keys --- src/gateway/server-session-key.ts | 5 +- ...erver.agent.gateway-server-agent-a.test.ts | 309 +++++++----------- ...erver.agent.gateway-server-agent-b.test.ts | 113 +++---- .../server.agent.gateway-server-agent.test.ts | 23 +- .../server.chat.gateway-server-chat-b.test.ts | 106 +++--- .../server.chat.gateway-server-chat-c.test.ts | 63 ++-- .../server.chat.gateway-server-chat.test.ts | 127 +++---- ...ridge.gateway-server-node-bridge-b.test.ts | 81 ++--- ...ridge.gateway-server-node-bridge-c.test.ts | 65 ++-- ...sessions.gateway-server-sessions-a.test.ts | 85 +++-- ...r.sessions.gateway-server-sessions.test.ts | 74 ++--- src/gateway/test-helpers.server.ts | 29 +- src/routing/session-key.ts | 19 ++ 13 files changed, 449 insertions(+), 650 deletions(-) diff --git a/src/gateway/server-session-key.ts b/src/gateway/server-session-key.ts index 08d4389a0..b1931c4bb 100644 --- a/src/gateway/server-session-key.ts +++ b/src/gateway/server-session-key.ts @@ -1,7 +1,7 @@ import { loadConfig } from "../config/config.js"; import { loadSessionStore, resolveStorePath } from "../config/sessions.js"; import { getAgentRunContext, registerAgentRunContext } from "../infra/agent-events.js"; -import { parseAgentSessionKey } from "../routing/session-key.js"; +import { toAgentRequestSessionKey } from "../routing/session-key.js"; export function resolveSessionKeyForRun(runId: string) { const cached = getAgentRunContext(runId)?.sessionKey; @@ -12,8 +12,7 @@ export function resolveSessionKeyForRun(runId: string) { const found = Object.entries(store).find(([, entry]) => entry?.sessionId === runId); const storeKey = found?.[0]; if (storeKey) { - const parsed = parseAgentSessionKey(storeKey); - const sessionKey = parsed?.rest ?? storeKey; + const sessionKey = toAgentRequestSessionKey(storeKey) ?? storeKey; registerAgentRunContext(runId, { sessionKey }); return sessionKey; } diff --git a/src/gateway/server.agent.gateway-server-agent-a.test.ts b/src/gateway/server.agent.gateway-server-agent-a.test.ts index 576d50fe0..71fe1ee18 100644 --- a/src/gateway/server.agent.gateway-server-agent-a.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-a.test.ts @@ -9,6 +9,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -26,22 +27,16 @@ describe("gateway server agent", () => { testState.allowFrom = ["+436769770569"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-stale", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-stale", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -70,20 +65,14 @@ describe("gateway server agent", () => { test("agent forwards sessionKey to agentCommand", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:subagent:abc": { - sessionId: "sess-sub", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + "agent:main:subagent:abc": { + sessionId: "sess-sub", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -111,23 +100,17 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-account", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "default", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-account", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "default", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -156,23 +139,17 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-explicit", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "legacy", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-explicit", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "legacy", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -201,23 +178,17 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-explicit-account", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "legacy", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-explicit-account", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "legacy", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -247,23 +218,17 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-implicit", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - lastAccountId: "kev", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-implicit", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "kev", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -290,20 +255,14 @@ describe("gateway server agent", () => { test("agent forwards image attachments as images[]", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-images", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-images", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -343,20 +302,14 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-missing-provider", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-missing-provider", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -384,22 +337,16 @@ describe("gateway server agent", () => { test("agent routes main last-channel whatsapp", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-whatsapp", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-whatsapp", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -429,22 +376,16 @@ describe("gateway server agent", () => { test("agent routes main last-channel telegram", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - lastChannel: "telegram", - lastTo: "123", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + lastChannel: "telegram", + lastTo: "123", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -473,22 +414,16 @@ describe("gateway server agent", () => { test("agent routes main last-channel discord", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-discord", - updatedAt: Date.now(), - lastChannel: "discord", - lastTo: "channel:discord-123", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-discord", + updatedAt: Date.now(), + lastChannel: "discord", + lastTo: "channel:discord-123", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -517,22 +452,16 @@ describe("gateway server agent", () => { test("agent routes main last-channel slack", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-slack", - updatedAt: Date.now(), - lastChannel: "slack", - lastTo: "channel:slack-123", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-slack", + updatedAt: Date.now(), + lastChannel: "slack", + lastTo: "channel:slack-123", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -561,22 +490,16 @@ describe("gateway server agent", () => { test("agent routes main last-channel signal", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-signal", - updatedAt: Date.now(), - lastChannel: "signal", - lastTo: "+15551234567", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-signal", + updatedAt: Date.now(), + lastChannel: "signal", + lastTo: "+15551234567", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/server.agent.gateway-server-agent-b.test.ts b/src/gateway/server.agent.gateway-server-agent-b.test.ts index 983a8473e..fe4cf485e 100644 --- a/src/gateway/server.agent.gateway-server-agent-b.test.ts +++ b/src/gateway/server.agent.gateway-server-agent-b.test.ts @@ -18,6 +18,7 @@ import { startGatewayServer, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -80,22 +81,16 @@ describe("gateway server agent", () => { setActivePluginRegistry(registry); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-teams", - updatedAt: Date.now(), - lastChannel: "msteams", - lastTo: "conversation:teams-123", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-teams", + updatedAt: Date.now(), + lastChannel: "msteams", + lastTo: "conversation:teams-123", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -133,22 +128,16 @@ describe("gateway server agent", () => { setActivePluginRegistry(registry); const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-alias", - updatedAt: Date.now(), - lastChannel: "imessage", - lastTo: "chat_id:123", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-alias", + updatedAt: Date.now(), + lastChannel: "imessage", + lastTo: "chat_id:123", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -206,22 +195,16 @@ describe("gateway server agent", () => { testState.allowFrom = ["+1555"]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main-webchat", - updatedAt: Date.now(), - lastChannel: "webchat", - lastTo: "+1555", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main-webchat", + updatedAt: Date.now(), + lastChannel: "webchat", + lastTo: "+1555", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -247,25 +230,19 @@ describe("gateway server agent", () => { await server.close(); }); - test("agent uses webchat for internal runs when last provider is webchat", async () => { - const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); - testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { + test("agent uses webchat for internal runs when last provider is webchat", async () => { + const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); + testState.sessionStorePath = path.join(dir, "sessions.json"); + await writeSessionStore({ + entries: { + main: { sessionId: "sess-main-webchat-internal", updatedAt: Date.now(), lastChannel: "webchat", lastTo: "+1555", }, }, - null, - 2, - ), - "utf-8", - ); + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -408,20 +385,14 @@ describe("gateway server agent", () => { test("agent events stream to webchat clients when run context is registered", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws, { diff --git a/src/gateway/server.agent.gateway-server-agent.test.ts b/src/gateway/server.agent.gateway-server-agent.test.ts index cbad29398..8fde4ad7c 100644 --- a/src/gateway/server.agent.gateway-server-agent.test.ts +++ b/src/gateway/server.agent.gateway-server-agent.test.ts @@ -11,6 +11,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -66,21 +67,15 @@ describe("gateway server agent", () => { test("suppresses tool stream events when verbose is off", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - verboseLevel: "off", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + verboseLevel: "off", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws, { diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index 2f2bebb16..7f510b286 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -11,6 +11,7 @@ import { sessionStoreSaveDelayMs, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -28,20 +29,14 @@ describe("gateway server chat", () => { test("chat.history caps payload bytes", { timeout: 15_000 }, async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -78,22 +73,16 @@ describe("gateway server chat", () => { test("chat.send does not overwrite last delivery route", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -105,11 +94,12 @@ describe("gateway server chat", () => { }); expect(res.ok).toBe(true); - const stored = JSON.parse(await fs.readFile(testState.sessionStorePath, "utf-8")) as { - main?: { lastChannel?: string; lastTo?: string }; - }; - expect(stored.main?.lastChannel).toBe("whatsapp"); - expect(stored.main?.lastTo).toBe("+1555"); + const stored = JSON.parse(await fs.readFile(testState.sessionStorePath, "utf-8")) as Record< + string, + { lastChannel?: string; lastTo?: string } | undefined + >; + expect(stored["agent:main:main"]?.lastChannel).toBe("whatsapp"); + expect(stored["agent:main:main"]?.lastTo).toBe("+1555"); ws.close(); await server.close(); @@ -118,20 +108,14 @@ describe("gateway server chat", () => { test("chat.abort cancels an in-flight chat.send", { timeout: 15000 }, async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); let inFlight: Promise | undefined; @@ -210,20 +194,14 @@ describe("gateway server chat", () => { test("chat.abort cancels while saving the session store", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); sessionStoreSaveDelayMs.value = 120; @@ -288,11 +266,11 @@ describe("gateway server chat", () => { test("chat.send treats /stop as an out-of-band abort", { timeout: 15000 }, async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify({ main: { sessionId: "sess-main", updatedAt: Date.now() } }, null, 2), - "utf-8", - ); + await writeSessionStore({ + entries: { + main: { sessionId: "sess-main", updatedAt: Date.now() }, + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/server.chat.gateway-server-chat-c.test.ts b/src/gateway/server.chat.gateway-server-chat-c.test.ts index 516290c21..1a55a2488 100644 --- a/src/gateway/server.chat.gateway-server-chat-c.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-c.test.ts @@ -11,6 +11,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -96,7 +97,7 @@ describe("gateway server chat", () => { test("chat.abort returns aborted=false for unknown runId", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile(testState.sessionStorePath, JSON.stringify({}, null, 2), "utf-8"); + await writeSessionStore({ entries: {} }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -116,20 +117,14 @@ describe("gateway server chat", () => { test("chat.abort rejects mismatched sessionKey", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -189,20 +184,14 @@ describe("gateway server chat", () => { test("chat.abort is a no-op after chat.send completes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -259,20 +248,14 @@ describe("gateway server chat", () => { test("chat.send preserves run ordering for queued runs", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/server.chat.gateway-server-chat.test.ts b/src/gateway/server.chat.gateway-server-chat.test.ts index a36a10e20..3e21ced55 100644 --- a/src/gateway/server.chat.gateway-server-chat.test.ts +++ b/src/gateway/server.chat.gateway-server-chat.test.ts @@ -12,6 +12,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -106,22 +107,16 @@ describe("gateway server chat", () => { }, }; - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "discord:group:dev": { - sessionId: "sess-discord", - updatedAt: Date.now(), - chatType: "group", - channel: "discord", - }, + await writeSessionStore({ + entries: { + "discord:group:dev": { + sessionId: "sess-discord", + updatedAt: Date.now(), + chatType: "group", + channel: "discord", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -148,20 +143,14 @@ describe("gateway server chat", () => { }, }; - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "cron:job-1": { - sessionId: "sess-cron", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + "cron:job-1": { + sessionId: "sess-cron", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -236,20 +225,14 @@ describe("gateway server chat", () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const lines: string[] = []; for (let i = 0; i < 300; i += 1) { @@ -349,21 +332,15 @@ describe("gateway server chat", () => { "utf-8", ); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - sessionFile: forkedPath, - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + sessionFile: forkedPath, + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -397,20 +374,14 @@ describe("gateway server chat", () => { "utf-8", ); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws); @@ -447,20 +418,14 @@ describe("gateway server chat", () => { ]; const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); await fs.writeFile( path.join(dir, "sess-main.jsonl"), JSON.stringify({ diff --git a/src/gateway/server.node-bridge.gateway-server-node-bridge-b.test.ts b/src/gateway/server.node-bridge.gateway-server-node-bridge-b.test.ts index e936c2dd2..c06c23d61 100644 --- a/src/gateway/server.node-bridge.gateway-server-node-bridge-b.test.ts +++ b/src/gateway/server.node-bridge.gateway-server-node-bridge-b.test.ts @@ -16,6 +16,7 @@ import { startGatewayServer, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; const _decodeWsData = (data: unknown): string => { @@ -217,20 +218,14 @@ describe("gateway server node/bridge", () => { test("bridge RPC chat.history returns session messages", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); await fs.writeFile( path.join(dir, "sess-main.jsonl"), @@ -274,20 +269,14 @@ describe("gateway server node/bridge", () => { test("bridge RPC sessions.list returns session rows", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const port = await getFreePort(); const server = await startGatewayServer(port); @@ -331,20 +320,14 @@ describe("gateway server node/bridge", () => { test("bridge chat events are pushed to subscribed nodes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const port = await getFreePort(); const server = await startGatewayServer(port); @@ -408,20 +391,14 @@ describe("gateway server node/bridge", () => { test("bridge chat.send forwards image attachments to agentCommand", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const port = await getFreePort(); const server = await startGatewayServer(port); diff --git a/src/gateway/server.node-bridge.gateway-server-node-bridge-c.test.ts b/src/gateway/server.node-bridge.gateway-server-node-bridge-c.test.ts index e8cd80afb..d4c87a727 100644 --- a/src/gateway/server.node-bridge.gateway-server-node-bridge-c.test.ts +++ b/src/gateway/server.node-bridge.gateway-server-node-bridge-c.test.ts @@ -14,6 +14,7 @@ import { startGatewayServer, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; const decodeWsData = (data: unknown): string => { @@ -42,22 +43,16 @@ describe("gateway server node/bridge", () => { test("bridge voice transcript defaults to main session", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - lastChannel: "whatsapp", - lastTo: "+1555", - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + lastChannel: "whatsapp", + lastTo: "+1555", }, - null, - 2, - ), - "utf-8", - ); + }, + }); const port = await getFreePort(); const server = await startGatewayServer(port); @@ -92,20 +87,14 @@ describe("gateway server node/bridge", () => { test("bridge voice transcript triggers chat events for webchat clients", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { server, ws } = await startServerWithClient(); await connectOk(ws, { @@ -183,20 +172,14 @@ describe("gateway server node/bridge", () => { test("bridge chat.abort cancels while saving the session store", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-gw-")); testState.sessionStorePath = path.join(dir, "sessions.json"); - await fs.writeFile( - testState.sessionStorePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); sessionStoreSaveDelayMs.value = 120; diff --git a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts index b780b1499..1bf47c850 100644 --- a/src/gateway/server.sessions.gateway-server-sessions-a.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions-a.test.ts @@ -10,6 +10,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; import { DEFAULT_PROVIDER } from "../agents/defaults.js"; @@ -65,41 +66,35 @@ describe("gateway server sessions", () => { "utf-8", ); - await fs.writeFile( - storePath, - JSON.stringify( - { - "agent:main:main": { - sessionId: "sess-main", - updatedAt: now - 30_000, - inputTokens: 10, - outputTokens: 20, - thinkingLevel: "low", - verboseLevel: "on", - lastProvider: "whatsapp", - lastTo: "+1555", - lastAccountId: "work", - }, - "agent:main:discord:group:dev": { - sessionId: "sess-group", - updatedAt: now - 120_000, - totalTokens: 50, - }, - "agent:main:subagent:one": { - sessionId: "sess-subagent", - updatedAt: now - 120_000, - spawnedBy: "agent:main:main", - }, - global: { - sessionId: "sess-global", - updatedAt: now - 10_000, - }, + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: now - 30_000, + inputTokens: 10, + outputTokens: 20, + thinkingLevel: "low", + verboseLevel: "on", + lastChannel: "whatsapp", + lastTo: "+1555", + lastAccountId: "work", }, - null, - 2, - ), - "utf-8", - ); + "discord:group:dev": { + sessionId: "sess-group", + updatedAt: now - 120_000, + totalTokens: 50, + }, + "agent:main:subagent:one": { + sessionId: "sess-subagent", + updatedAt: now - 120_000, + spawnedBy: "agent:main:main", + }, + global: { + sessionId: "sess-global", + updatedAt: now - 10_000, + }, + }, + }); const { server, ws } = await startServerWithClient(); const hello = await connectOk(ws); @@ -355,21 +350,15 @@ describe("gateway server sessions", () => { "utf-8", ); - await fs.writeFile( - storePath, - JSON.stringify( - { - "agent:main:main": { sessionId: "sess-main", updatedAt: Date.now() }, - "agent:main:discord:group:dev": { - sessionId: "sess-active", - updatedAt: Date.now(), - }, + await writeSessionStore({ + entries: { + main: { sessionId: "sess-main", updatedAt: Date.now() }, + "discord:group:dev": { + sessionId: "sess-active", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); embeddedRunMock.activeIds.add("sess-active"); embeddedRunMock.waitResults.set("sess-active", true); diff --git a/src/gateway/server.sessions.gateway-server-sessions.test.ts b/src/gateway/server.sessions.gateway-server-sessions.test.ts index 82d0b02dd..14bf82803 100644 --- a/src/gateway/server.sessions.gateway-server-sessions.test.ts +++ b/src/gateway/server.sessions.gateway-server-sessions.test.ts @@ -8,6 +8,7 @@ import { rpcReq, startServerWithClient, testState, + writeSessionStore, } from "./test-helpers.js"; installGatewayTestHooks(); @@ -25,38 +26,30 @@ describe("gateway server sessions", () => { const workDir = path.join(dir, "work"); await fs.mkdir(homeDir, { recursive: true }); await fs.mkdir(workDir, { recursive: true }); - await fs.writeFile( - path.join(homeDir, "sessions.json"), - JSON.stringify( - { - "agent:home:main": { - sessionId: "sess-home-main", - updatedAt: Date.now(), - }, - "agent:home:discord:group:dev": { - sessionId: "sess-home-group", - updatedAt: Date.now() - 1000, - }, + await writeSessionStore({ + storePath: path.join(homeDir, "sessions.json"), + agentId: "home", + entries: { + main: { + sessionId: "sess-home-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); - await fs.writeFile( - path.join(workDir, "sessions.json"), - JSON.stringify( - { - "agent:work:main": { - sessionId: "sess-work-main", - updatedAt: Date.now(), - }, + "discord:group:dev": { + sessionId: "sess-home-group", + updatedAt: Date.now() - 1000, }, - null, - 2, - ), - "utf-8", - ); + }, + }); + await writeSessionStore({ + storePath: path.join(workDir, "sessions.json"), + agentId: "work", + entries: { + main: { + sessionId: "sess-work-main", + updatedAt: Date.now(), + }, + }, + }); const { ws } = await startServerWithClient(); await connectOk(ws); @@ -92,20 +85,17 @@ describe("gateway server sessions", () => { testState.agentsConfig = { list: [{ id: "ops", default: true }] }; testState.sessionConfig = { mainKey: "work" }; - await fs.writeFile( + await writeSessionStore({ storePath, - JSON.stringify( - { - "agent:ops:work": { - sessionId: "sess-ops-main", - updatedAt: Date.now(), - }, + agentId: "ops", + mainKey: "work", + entries: { + main: { + sessionId: "sess-ops-main", + updatedAt: Date.now(), }, - null, - 2, - ), - "utf-8", - ); + }, + }); const { ws } = await startServerWithClient(); await connectOk(ws); diff --git a/src/gateway/test-helpers.server.ts b/src/gateway/test-helpers.server.ts index e274bb4ff..fe40ad4f9 100644 --- a/src/gateway/test-helpers.server.ts +++ b/src/gateway/test-helpers.server.ts @@ -6,11 +6,12 @@ import path from "node:path"; import { afterEach, beforeEach, expect } from "vitest"; import { WebSocket } from "ws"; -import { resolveMainSessionKeyFromConfig } from "../config/sessions.js"; +import { resolveMainSessionKeyFromConfig, type SessionEntry } from "../config/sessions.js"; import { resetAgentRunContextForTest } from "../infra/agent-events.js"; import { drainSystemEvents, peekSystemEvents } from "../infra/system-events.js"; import { rawDataToString } from "../infra/ws.js"; import { resetLogger, setLoggerOverride } from "../logging.js"; +import { DEFAULT_AGENT_ID, toAgentStoreSessionKey } from "../routing/session-key.js"; import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js"; import { PROTOCOL_VERSION } from "./protocol/index.js"; @@ -31,6 +32,32 @@ let previousHome: string | undefined; let tempHome: string | undefined; let tempConfigRoot: string | undefined; +export async function writeSessionStore(params: { + entries: Record>; + storePath?: string; + agentId?: string; + mainKey?: string; +}): Promise { + const storePath = params.storePath ?? testState.sessionStorePath; + if (!storePath) throw new Error("writeSessionStore requires testState.sessionStorePath"); + const agentId = params.agentId ?? DEFAULT_AGENT_ID; + const store: Record> = {}; + for (const [requestKey, entry] of Object.entries(params.entries)) { + const rawKey = requestKey.trim(); + const storeKey = + rawKey === "global" || rawKey === "unknown" + ? rawKey + : toAgentStoreSessionKey({ + agentId, + requestKey, + mainKey: params.mainKey, + }); + store[storeKey] = entry; + } + await fs.mkdir(path.dirname(storePath), { recursive: true }); + await fs.writeFile(storePath, JSON.stringify(store, null, 2), "utf-8"); +} + export function installGatewayTestHooks() { beforeEach(async () => { setLoggerOverride({ level: "silent", consoleLevel: "silent" }); diff --git a/src/routing/session-key.ts b/src/routing/session-key.ts index 1821b7ad5..98c63c3e6 100644 --- a/src/routing/session-key.ts +++ b/src/routing/session-key.ts @@ -16,6 +16,25 @@ export type ParsedAgentSessionKey = { rest: string; }; +export function toAgentRequestSessionKey(storeKey: string | undefined | null): string | undefined { + const raw = (storeKey ?? "").trim(); + if (!raw) return undefined; + return parseAgentSessionKey(raw)?.rest ?? raw; +} + +export function toAgentStoreSessionKey(params: { + agentId: string; + requestKey: string | undefined | null; + mainKey?: string | undefined; +}): string { + const raw = (params.requestKey ?? "").trim(); + if (!raw || raw === DEFAULT_MAIN_KEY) { + return buildAgentMainSessionKey({ agentId: params.agentId, mainKey: params.mainKey }); + } + if (raw.startsWith("agent:")) return raw; + return `agent:${normalizeAgentId(params.agentId)}:${raw}`; +} + export function resolveAgentIdFromSessionKey(sessionKey: string | undefined | null): string { const parsed = parseAgentSessionKey(sessionKey); return normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);