refactor: canonicalize gateway session store keys

This commit is contained in:
Peter Steinberger
2026-01-17 07:41:06 +00:00
parent d5fdda8e28
commit c92265a51b
13 changed files with 449 additions and 650 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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, {

View File

@@ -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, {

View File

@@ -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<unknown> | 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);

View File

@@ -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);

View File

@@ -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({

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<string, Partial<SessionEntry>>;
storePath?: string;
agentId?: string;
mainKey?: string;
}): Promise<void> {
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<string, Partial<SessionEntry>> = {};
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" });

View File

@@ -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);