From 0ed7ea698a24c1fcc40a45dc1d835fbe17417e4b Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 06:17:15 +0000 Subject: [PATCH 1/4] fix(subagent): wait for completion before announce The previous immediate probe (timeoutMs: 0) only caught already-completed runs. Cross-process spawns need to actually wait via agent.wait RPC for the gateway to signal completion, then trigger the announce flow. - Rename probeImmediateCompletion to waitForSubagentCompletion - Use 10 minute wait timeout for agent.wait RPC - Remove leftover debug console.log statements --- src/agents/subagent-registry.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/agents/subagent-registry.ts b/src/agents/subagent-registry.ts index 3f9a9a5c3..14dca3f03 100644 --- a/src/agents/subagent-registry.ts +++ b/src/agents/subagent-registry.ts @@ -68,7 +68,9 @@ function ensureListener() { onAgentEvent((evt) => { if (!evt || evt.stream !== "lifecycle") return; const entry = subagentRuns.get(evt.runId); - if (!entry) return; + if (!entry) { + return; + } const phase = evt.data?.phase; if (phase === "start") { const startedAt = @@ -148,18 +150,23 @@ export function registerSubagentRun(params: { }); ensureListener(); if (archiveAfterMs) startSweeper(); - void probeImmediateCompletion(params.runId); + // Wait for subagent completion via gateway RPC (cross-process). + // The in-process lifecycle listener is a fallback for embedded runs. + void waitForSubagentCompletion(params.runId); } -async function probeImmediateCompletion(runId: string) { +// Default wait timeout: 10 minutes. This covers most subagent runs. +const DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS = 10 * 60 * 1000; + +async function waitForSubagentCompletion(runId: string) { try { const wait = (await callGateway({ method: "agent.wait", params: { runId, - timeoutMs: 0, + timeoutMs: DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS, }, - timeoutMs: 2000, + timeoutMs: DEFAULT_SUBAGENT_WAIT_TIMEOUT_MS + 10_000, })) as { status?: string; startedAt?: number; endedAt?: number }; if (wait?.status !== "ok" && wait?.status !== "error") return; const entry = subagentRuns.get(runId); From 7d6f17d77f9bafaf790b67231e6fda8e282e71b7 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 06:49:27 +0000 Subject: [PATCH 2/4] fix(subagent): make announce prompt more emphatic The previous prompt was too permissive about skipping announcements. Updated to strongly encourage announcing results since the requester is waiting for a response. - Add 'You MUST announce your result' instruction - Clarify ANNOUNCE_SKIP is only for complete failures - Improve guidance on providing useful summaries --- src/agents/subagent-announce.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index c48e6e9d6..235da5d72 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -213,8 +213,11 @@ function buildSubagentAnnouncePrompt(params: { params.subagentReply ? `Sub-agent result: ${params.subagentReply}` : "Sub-agent result: (not available).", - 'Reply exactly "ANNOUNCE_SKIP" to stay silent.', - "Any other reply will be posted to the requester chat provider.", + "", + "**You MUST announce your result.** The requester is waiting for your response.", + "Provide a brief, useful summary of what you accomplished.", + 'Only reply "ANNOUNCE_SKIP" if the task completely failed with no useful output.', + "Your reply will be posted to the requester chat.", ].filter(Boolean); return lines.join("\n"); } From d4e9f23ee9e92f12430f8d1be630dca554040e3b Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 07:06:25 +0000 Subject: [PATCH 3/4] fix(gateway): normalize session key to canonical form before store writes Ensure 'main' alias is always stored as 'agent:main:main' to prevent duplicate entries. Also update loadSessionEntry to check both forms when looking up entries. Fixes duplicate main sessions in session store. --- src/gateway/server-bridge.ts | 38 ++++++++++++++++++++++++----- src/gateway/server-methods/agent.ts | 15 +++++++----- src/gateway/server-methods/chat.ts | 18 ++++++++++++-- src/gateway/session-utils.ts | 12 ++++++++- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/gateway/server-bridge.ts b/src/gateway/server-bridge.ts index 6f676d26d..55f7f1f22 100644 --- a/src/gateway/server-bridge.ts +++ b/src/gateway/server-bridge.ts @@ -24,6 +24,7 @@ import { buildConfigSchema } from "../config/schema.js"; import { loadSessionStore, mergeSessionEntry, + resolveAgentMainSessionKey, resolveMainSessionKeyFromConfig, type SessionEntry, saveSessionStore, @@ -35,7 +36,10 @@ import { } from "../infra/voicewake.js"; import { clearCommandLane } from "../process/command-queue.js"; import { normalizeProviderId } from "../providers/plugins/index.js"; -import { normalizeMainKey } from "../routing/session-key.js"; +import { + normalizeMainKey, + resolveAgentIdFromSessionKey, +} from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; import { abortChatRunById, @@ -918,8 +922,14 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { clientRunId, }); + // Normalize short main key alias to canonical form before store write + const agentId = resolveAgentIdFromSessionKey(p.sessionKey); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); + const rawMainKey = normalizeMainKey(cfg.session?.mainKey); + const storeKey = + p.sessionKey === rawMainKey ? mainSessionKey : p.sessionKey; if (store) { - store[p.sessionKey] = sessionEntry; + store[storeKey] = sessionEntry; if (storePath) { await saveSessionStore(storePath, store); } @@ -1032,12 +1042,18 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { if (text.length > 20_000) return; const sessionKeyRaw = typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; - const mainKey = normalizeMainKey(loadConfig().session?.mainKey); - const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : mainKey; + const cfg = loadConfig(); + const rawMainKey = normalizeMainKey(cfg.session?.mainKey); + const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey; const { storePath, store, entry } = loadSessionEntry(sessionKey); + // Normalize short main key alias to canonical form before store write + const agentId = resolveAgentIdFromSessionKey(sessionKey); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); + const storeKey = + sessionKey === rawMainKey ? mainSessionKey : sessionKey; const now = Date.now(); const sessionId = entry?.sessionId ?? randomUUID(); - store[sessionKey] = { + store[storeKey] = { sessionId, updatedAt: now, thinkingLevel: entry?.thinkingLevel, @@ -1112,9 +1128,19 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : `node-${nodeId}`; const { storePath, store, entry } = loadSessionEntry(sessionKey); + // Normalize short main key alias to canonical form before store write + const nodeCfg = loadConfig(); + const nodeAgentId = resolveAgentIdFromSessionKey(sessionKey); + const nodeMainSessionKey = resolveAgentMainSessionKey({ + cfg: nodeCfg, + agentId: nodeAgentId, + }); + const nodeRawMainKey = normalizeMainKey(nodeCfg.session?.mainKey); + const nodeStoreKey = + sessionKey === nodeRawMainKey ? nodeMainSessionKey : sessionKey; const now = Date.now(); const sessionId = entry?.sessionId ?? randomUUID(); - store[sessionKey] = { + store[nodeStoreKey] = { sessionId, updatedAt: now, thinkingLevel: entry?.thinkingLevel, diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index 386755492..ce4e64b5f 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -189,12 +189,6 @@ export const agentHandlers: GatewayRequestHandlers = { ); return; } - if (store) { - store[requestedSessionKey] = nextEntry; - if (storePath) { - await saveSessionStore(storePath, store); - } - } resolvedSessionId = sessionId; const agentId = resolveAgentIdFromSessionKey(requestedSessionKey); const mainSessionKey = resolveAgentMainSessionKey({ @@ -202,6 +196,15 @@ export const agentHandlers: GatewayRequestHandlers = { agentId, }); const rawMainKey = normalizeMainKey(cfg.session?.mainKey); + // Normalize short main key alias to canonical form before store write + const storeKey = + requestedSessionKey === rawMainKey ? mainSessionKey : requestedSessionKey; + if (store) { + store[storeKey] = nextEntry; + if (storePath) { + await saveSessionStore(storePath, store); + } + } if ( requestedSessionKey === mainSessionKey || requestedSessionKey === rawMainKey diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index ac9647182..3da182034 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -3,7 +3,15 @@ import { randomUUID } from "node:crypto"; import { resolveThinkingDefault } from "../../agents/model-selection.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { agentCommand } from "../../commands/agent.js"; -import { mergeSessionEntry, saveSessionStore } from "../../config/sessions.js"; +import { + mergeSessionEntry, + resolveAgentMainSessionKey, + saveSessionStore, +} from "../../config/sessions.js"; +import { + normalizeMainKey, + resolveAgentIdFromSessionKey, +} from "../../routing/session-key.js"; import { registerAgentRunContext } from "../../infra/agent-events.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; @@ -306,8 +314,14 @@ export const chatHandlers: GatewayRequestHandlers = { clientRunId, }); + // Normalize short main key alias to canonical form before store write + const agentId = resolveAgentIdFromSessionKey(p.sessionKey); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); + const rawMainKey = normalizeMainKey(cfg.session?.mainKey); + const storeKey = + p.sessionKey === rawMainKey ? mainSessionKey : p.sessionKey; if (store) { - store[p.sessionKey] = sessionEntry; + store[storeKey] = sessionEntry; if (storePath) { await saveSessionStore(storePath, store); } diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index 9ddc90b8d..aacd004f0 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -15,6 +15,7 @@ import { buildGroupDisplayName, loadSessionStore, resolveAgentIdFromSessionKey, + resolveAgentMainSessionKey, resolveSessionTranscriptPath, resolveStorePath, type SessionEntry, @@ -172,7 +173,16 @@ export function loadSessionEntry(sessionKey: string) { const store = loadSessionStore(storePath); const parsed = parseAgentSessionKey(sessionKey); const legacyKey = parsed?.rest; - const entry = store[sessionKey] ?? (legacyKey ? store[legacyKey] : undefined); + // Also try the canonical key if sessionKey is the short mainKey alias + const rawMainKey = normalizeMainKey(sessionCfg?.mainKey); + const canonicalKey = + sessionKey === rawMainKey + ? resolveAgentMainSessionKey({ cfg, agentId }) + : undefined; + const entry = + store[sessionKey] ?? + (legacyKey ? store[legacyKey] : undefined) ?? + (canonicalKey ? store[canonicalKey] : undefined); return { cfg, storePath, store, entry }; } From 98777337482160166a5b020d44af54d1eaa24e82 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 12 Jan 2026 01:05:43 +0000 Subject: [PATCH 4/4] fix(gateway): canonicalize main session aliases --- CHANGELOG.md | 1 + src/gateway/server-bridge.ts | 35 ++++------- src/gateway/server-methods/agent.ts | 20 +++--- src/gateway/server-methods/chat.ts | 21 ++----- src/gateway/session-utils.test.ts | 63 +++++++++++++++++++ src/gateway/session-utils.ts | 96 +++++++++++++++++------------ 6 files changed, 145 insertions(+), 91 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7633a36e2..d5dc9eae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Agents: treat message tool errors as failures so fallback replies still send; require `to` + `message` for `action=send`. (#717) — thanks @theglove44. - Agents: route subagent transcripts to the target agent sessions directory and add regression coverage. (#708) — thanks @xMikeMickelson. - Agents/Tools: preserve action enums when flattening tool schemas. (#708) — thanks @xMikeMickelson. +- Gateway/Agents: canonicalize main session aliases for store writes and add regression coverage. (#709) — thanks @xMikeMickelson. - Agents: reset sessions and retry when auto-compaction overflows instead of crashing the gateway. ## 2026.1.10 diff --git a/src/gateway/server-bridge.ts b/src/gateway/server-bridge.ts index 55f7f1f22..fa3d616a5 100644 --- a/src/gateway/server-bridge.ts +++ b/src/gateway/server-bridge.ts @@ -24,7 +24,6 @@ import { buildConfigSchema } from "../config/schema.js"; import { loadSessionStore, mergeSessionEntry, - resolveAgentMainSessionKey, resolveMainSessionKeyFromConfig, type SessionEntry, saveSessionStore, @@ -36,10 +35,7 @@ import { } from "../infra/voicewake.js"; import { clearCommandLane } from "../process/command-queue.js"; import { normalizeProviderId } from "../providers/plugins/index.js"; -import { - normalizeMainKey, - resolveAgentIdFromSessionKey, -} from "../routing/session-key.js"; +import { normalizeMainKey } from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; import { abortChatRunById, @@ -93,6 +89,7 @@ import { readSessionMessages, resolveGatewaySessionStoreTarget, resolveSessionModelRef, + resolveSessionStoreKey, resolveSessionTranscriptCandidates, type SessionsPatchResult, } from "./session-utils.js"; @@ -922,12 +919,10 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { clientRunId, }); - // Normalize short main key alias to canonical form before store write - const agentId = resolveAgentIdFromSessionKey(p.sessionKey); - const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); - const rawMainKey = normalizeMainKey(cfg.session?.mainKey); - const storeKey = - p.sessionKey === rawMainKey ? mainSessionKey : p.sessionKey; + const storeKey = resolveSessionStoreKey({ + cfg, + sessionKey: p.sessionKey, + }); if (store) { store[storeKey] = sessionEntry; if (storePath) { @@ -1044,13 +1039,10 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; const cfg = loadConfig(); const rawMainKey = normalizeMainKey(cfg.session?.mainKey); - const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey; + const sessionKey = + sessionKeyRaw.length > 0 ? sessionKeyRaw : rawMainKey; const { storePath, store, entry } = loadSessionEntry(sessionKey); - // Normalize short main key alias to canonical form before store write - const agentId = resolveAgentIdFromSessionKey(sessionKey); - const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); - const storeKey = - sessionKey === rawMainKey ? mainSessionKey : sessionKey; + const storeKey = resolveSessionStoreKey({ cfg, sessionKey }); const now = Date.now(); const sessionId = entry?.sessionId ?? randomUUID(); store[storeKey] = { @@ -1128,16 +1120,11 @@ export function createBridgeHandlers(ctx: BridgeHandlersContext) { const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : `node-${nodeId}`; const { storePath, store, entry } = loadSessionEntry(sessionKey); - // Normalize short main key alias to canonical form before store write const nodeCfg = loadConfig(); - const nodeAgentId = resolveAgentIdFromSessionKey(sessionKey); - const nodeMainSessionKey = resolveAgentMainSessionKey({ + const nodeStoreKey = resolveSessionStoreKey({ cfg: nodeCfg, - agentId: nodeAgentId, + sessionKey, }); - const nodeRawMainKey = normalizeMainKey(nodeCfg.session?.mainKey); - const nodeStoreKey = - sessionKey === nodeRawMainKey ? nodeMainSessionKey : sessionKey; const now = Date.now(); const sessionId = entry?.sessionId ?? randomUUID(); store[nodeStoreKey] = { diff --git a/src/gateway/server-methods/agent.ts b/src/gateway/server-methods/agent.ts index ce4e64b5f..ba1e633ae 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -11,7 +11,6 @@ import { import { registerAgentRunContext } from "../../infra/agent-events.js"; import { resolveOutboundTarget } from "../../infra/outbound/targets.js"; import { DEFAULT_CHAT_PROVIDER } from "../../providers/registry.js"; -import { normalizeMainKey } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; import { @@ -29,7 +28,7 @@ import { validateAgentParams, validateAgentWaitParams, } from "../protocol/index.js"; -import { loadSessionEntry } from "../session-utils.js"; +import { loadSessionEntry, resolveSessionStoreKey } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; import { waitForAgentJob } from "./agent-job.js"; import type { GatewayRequestHandlers } from "./types.js"; @@ -190,24 +189,21 @@ export const agentHandlers: GatewayRequestHandlers = { return; } resolvedSessionId = sessionId; - const agentId = resolveAgentIdFromSessionKey(requestedSessionKey); - const mainSessionKey = resolveAgentMainSessionKey({ + const canonicalSessionKey = resolveSessionStoreKey({ cfg, - agentId, + sessionKey: requestedSessionKey, }); - const rawMainKey = normalizeMainKey(cfg.session?.mainKey); - // Normalize short main key alias to canonical form before store write - const storeKey = - requestedSessionKey === rawMainKey ? mainSessionKey : requestedSessionKey; + const agentId = resolveAgentIdFromSessionKey(canonicalSessionKey); + const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); if (store) { - store[storeKey] = nextEntry; + store[canonicalSessionKey] = nextEntry; if (storePath) { await saveSessionStore(storePath, store); } } if ( - requestedSessionKey === mainSessionKey || - requestedSessionKey === rawMainKey + canonicalSessionKey === mainSessionKey || + canonicalSessionKey === "global" ) { context.addChatRun(idem, { sessionKey: requestedSessionKey, diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index 3da182034..5f6ef9f6d 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -3,15 +3,7 @@ import { randomUUID } from "node:crypto"; import { resolveThinkingDefault } from "../../agents/model-selection.js"; import { resolveAgentTimeoutMs } from "../../agents/timeout.js"; import { agentCommand } from "../../commands/agent.js"; -import { - mergeSessionEntry, - resolveAgentMainSessionKey, - saveSessionStore, -} from "../../config/sessions.js"; -import { - normalizeMainKey, - resolveAgentIdFromSessionKey, -} from "../../routing/session-key.js"; +import { mergeSessionEntry, saveSessionStore } from "../../config/sessions.js"; import { registerAgentRunContext } from "../../infra/agent-events.js"; import { defaultRuntime } from "../../runtime.js"; import { resolveSendPolicy } from "../../sessions/send-policy.js"; @@ -40,6 +32,7 @@ import { loadSessionEntry, readSessionMessages, resolveSessionModelRef, + resolveSessionStoreKey, } from "../session-utils.js"; import { formatForLog } from "../ws-log.js"; import type { GatewayRequestHandlers } from "./types.js"; @@ -314,12 +307,10 @@ export const chatHandlers: GatewayRequestHandlers = { clientRunId, }); - // Normalize short main key alias to canonical form before store write - const agentId = resolveAgentIdFromSessionKey(p.sessionKey); - const mainSessionKey = resolveAgentMainSessionKey({ cfg, agentId }); - const rawMainKey = normalizeMainKey(cfg.session?.mainKey); - const storeKey = - p.sessionKey === rawMainKey ? mainSessionKey : p.sessionKey; + const storeKey = resolveSessionStoreKey({ + cfg, + sessionKey: p.sessionKey, + }); if (store) { store[storeKey] = sessionEntry; if (storePath) { diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 9fa2cf36c..ce1afe8a6 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -1,9 +1,14 @@ +import os from "node:os"; +import path from "node:path"; import { describe, expect, test } from "vitest"; +import type { ClawdbotConfig } from "../config/config.js"; import type { SessionEntry } from "../config/sessions.js"; import { capArrayByJsonBytes, classifySessionKey, parseGroupKey, + resolveGatewaySessionStoreTarget, + resolveSessionStoreKey, } from "./session-utils.js"; describe("gateway session utils", () => { @@ -31,4 +36,62 @@ describe("gateway session utils", () => { const entry = { chatType: "group" } as SessionEntry; expect(classifySessionKey("main", entry)).toBe("group"); }); + + test("resolveSessionStoreKey maps main aliases to default agent main", () => { + const cfg = { + session: { mainKey: "work" }, + agents: { list: [{ id: "ops", default: true }] }, + } as ClawdbotConfig; + expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe( + "agent:ops:work", + ); + expect(resolveSessionStoreKey({ cfg, sessionKey: "work" })).toBe( + "agent:ops:work", + ); + }); + + test("resolveSessionStoreKey canonicalizes bare keys to default agent", () => { + const cfg = { + session: { mainKey: "main" }, + agents: { list: [{ id: "ops", default: true }] }, + } as ClawdbotConfig; + expect(resolveSessionStoreKey({ cfg, sessionKey: "group:123" })).toBe( + "agent:ops:group:123", + ); + expect( + resolveSessionStoreKey({ cfg, sessionKey: "agent:alpha:main" }), + ).toBe("agent:alpha:main"); + }); + + test("resolveSessionStoreKey honors global scope", () => { + const cfg = { + session: { scope: "global", mainKey: "work" }, + agents: { list: [{ id: "ops", default: true }] }, + } as ClawdbotConfig; + expect(resolveSessionStoreKey({ cfg, sessionKey: "main" })).toBe("global"); + const target = resolveGatewaySessionStoreTarget({ cfg, key: "main" }); + expect(target.canonicalKey).toBe("global"); + expect(target.agentId).toBe("ops"); + }); + + test("resolveGatewaySessionStoreTarget uses canonical key for main alias", () => { + const storeTemplate = path.join( + os.tmpdir(), + "clawdbot-session-utils", + "{agentId}", + "sessions.json", + ); + const cfg = { + session: { mainKey: "main", store: storeTemplate }, + agents: { list: [{ id: "ops", default: true }] }, + } as ClawdbotConfig; + const target = resolveGatewaySessionStoreTarget({ cfg, key: "main" }); + expect(target.canonicalKey).toBe("agent:ops:main"); + expect(target.storeKeys).toEqual( + expect.arrayContaining(["agent:ops:main", "main"]), + ); + expect(target.storePath).toBe( + path.resolve(storeTemplate.replace("{agentId}", "ops")), + ); + }); }); diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index aacd004f0..960ac9021 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -14,8 +14,7 @@ import { resolveStateDir } from "../config/paths.js"; import { buildGroupDisplayName, loadSessionStore, - resolveAgentIdFromSessionKey, - resolveAgentMainSessionKey, + resolveMainSessionKey, resolveSessionTranscriptPath, resolveStorePath, type SessionEntry, @@ -168,22 +167,18 @@ export function capArrayByJsonBytes( export function loadSessionEntry(sessionKey: string) { const cfg = loadConfig(); const sessionCfg = cfg.session; - const agentId = resolveAgentIdFromSessionKey(sessionKey); + const canonicalKey = resolveSessionStoreKey({ cfg, sessionKey }); + const agentId = resolveSessionStoreAgentId(cfg, canonicalKey); const storePath = resolveStorePath(sessionCfg?.store, { agentId }); const store = loadSessionStore(storePath); - const parsed = parseAgentSessionKey(sessionKey); - const legacyKey = parsed?.rest; - // Also try the canonical key if sessionKey is the short mainKey alias - const rawMainKey = normalizeMainKey(sessionCfg?.mainKey); - const canonicalKey = - sessionKey === rawMainKey - ? resolveAgentMainSessionKey({ cfg, agentId }) - : undefined; + const parsed = parseAgentSessionKey(canonicalKey); + const legacyKey = + parsed?.rest ?? parseAgentSessionKey(sessionKey)?.rest ?? undefined; const entry = + store[canonicalKey] ?? store[sessionKey] ?? - (legacyKey ? store[legacyKey] : undefined) ?? - (canonicalKey ? store[canonicalKey] : undefined); - return { cfg, storePath, store, entry }; + (legacyKey ? store[legacyKey] : undefined); + return { cfg, storePath, store, entry, canonicalKey }; } export function classifySessionKey( @@ -293,6 +288,38 @@ function canonicalizeSessionKeyForAgent(agentId: string, key: string): string { return `agent:${normalizeAgentId(agentId)}:${key}`; } +function resolveDefaultStoreAgentId(cfg: ClawdbotConfig): string { + return normalizeAgentId(resolveDefaultAgentId(cfg)); +} + +export function resolveSessionStoreKey(params: { + cfg: ClawdbotConfig; + sessionKey: string; +}): string { + const raw = params.sessionKey.trim(); + if (!raw) return raw; + if (raw === "global" || raw === "unknown") return raw; + const rawMainKey = normalizeMainKey(params.cfg.session?.mainKey); + if (raw === "main" || raw === rawMainKey) { + return resolveMainSessionKey(params.cfg); + } + if (raw.startsWith("agent:")) return raw; + const agentId = resolveDefaultStoreAgentId(params.cfg); + return canonicalizeSessionKeyForAgent(agentId, raw); +} + +function resolveSessionStoreAgentId( + cfg: ClawdbotConfig, + canonicalKey: string, +): string { + if (canonicalKey === "global" || canonicalKey === "unknown") { + return resolveDefaultStoreAgentId(cfg); + } + const parsed = parseAgentSessionKey(canonicalKey); + if (parsed?.agentId) return normalizeAgentId(parsed.agentId); + return resolveDefaultStoreAgentId(cfg); +} + function canonicalizeSpawnedByForAgent( agentId: string, spawnedBy?: string, @@ -314,40 +341,29 @@ export function resolveGatewaySessionStoreTarget(params: { storeKeys: string[]; } { const key = params.key.trim(); - const agentId = resolveAgentIdFromSessionKey(key); + const canonicalKey = resolveSessionStoreKey({ + cfg: params.cfg, + sessionKey: key, + }); + const agentId = resolveSessionStoreAgentId(params.cfg, canonicalKey); const storeConfig = params.cfg.session?.store; const storePath = resolveStorePath(storeConfig, { agentId }); - if (key === "global" || key === "unknown") { - return { agentId, storePath, canonicalKey: key, storeKeys: [key] }; + if (canonicalKey === "global" || canonicalKey === "unknown") { + const storeKeys = key && key !== canonicalKey ? [canonicalKey, key] : [key]; + return { agentId, storePath, canonicalKey, storeKeys }; } - const parsed = parseAgentSessionKey(key); - if (parsed) { - return { - agentId, - storePath, - canonicalKey: key, - storeKeys: [key, parsed.rest], - }; - } - - if (key.startsWith("subagent:")) { - const canonical = canonicalizeSessionKeyForAgent(agentId, key); - return { - agentId, - storePath, - canonicalKey: canonical, - storeKeys: [canonical, key], - }; - } - - const canonical = canonicalizeSessionKeyForAgent(agentId, key); + const parsed = parseAgentSessionKey(canonicalKey); + const storeKeys = new Set(); + storeKeys.add(canonicalKey); + if (parsed?.rest) storeKeys.add(parsed.rest); + if (key && key !== canonicalKey) storeKeys.add(key); return { agentId, storePath, - canonicalKey: canonical, - storeKeys: [canonical, key], + canonicalKey, + storeKeys: Array.from(storeKeys), }; }