From 52929c0600fda988788ca126af1f49b6e258a2f5 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 05:52:33 +0000 Subject: [PATCH 1/4] fix(agent): use session key agentId for transcript path Cross-agent subagent spawns wrote transcripts to the spawner's agent directory instead of the target agent's directory. For example, when main spawned a codex subagent with session key agent:codex:subagent:..., the transcript went to agents/main/sessions/ instead of agents/codex/sessions/. Pass sessionAgentId to resolveSessionFilePath so transcripts are written to the correct agent's session directory. --- src/commands/agent.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/agent.ts b/src/commands/agent.ts index b7d2ea448..77fb4d942 100644 --- a/src/commands/agent.ts +++ b/src/commands/agent.ts @@ -416,7 +416,9 @@ export async function agentCommand( catalog: catalogForThinking, }); } - const sessionFile = resolveSessionFilePath(sessionId, sessionEntry); + const sessionFile = resolveSessionFilePath(sessionId, sessionEntry, { + agentId: sessionAgentId, + }); const startedAt = Date.now(); let lifecycleEnded = false; From 587a556d6b8fb93cb65f6378797948302b681931 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 06:17:15 +0000 Subject: [PATCH 2/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 f34d7e0fe057dd960d85266747446fb7ccd05940 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 06:49:27 +0000 Subject: [PATCH 3/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 029db064772dcb3820937f16502cb378bcf6e063 Mon Sep 17 00:00:00 2001 From: user Date: Sun, 11 Jan 2026 07:06:25 +0000 Subject: [PATCH 4/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 87ac5a955..a17a15b27 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, @@ -34,7 +35,10 @@ import { setVoiceWakeTriggers, } from "../infra/voicewake.js"; import { clearCommandLane } from "../process/command-queue.js"; -import { normalizeMainKey } from "../routing/session-key.js"; +import { + normalizeMainKey, + resolveAgentIdFromSessionKey, +} from "../routing/session-key.js"; import { defaultRuntime } from "../runtime.js"; import { abortChatRunById, @@ -917,8 +921,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); } @@ -1031,12 +1041,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, @@ -1118,9 +1134,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 3184539be..a6a5c311c 100644 --- a/src/gateway/server-methods/agent.ts +++ b/src/gateway/server-methods/agent.ts @@ -192,12 +192,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({ @@ -205,6 +199,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 c3afb65d4..1aebc68b0 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"; @@ -305,8 +313,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 }; }