From cf7c6cb94b75b4ec34b63d1d14ae8ca61de1f32b Mon Sep 17 00:00:00 2001 From: Stephen Schoettler Date: Thu, 5 Mar 2026 07:20:30 -0800 Subject: [PATCH] fix(sessions-send): increase default timeout to 90s and deliver reply on timeout via A2A flow Co-Authored-By: Claude Opus 4.6 --- src/agents/tools/sessions-send-tool.ts | 10 ++++- src/agents/tools/sessions.test.ts | 56 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index 82eff0adf7a..e5be7b33b1d 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -191,7 +191,7 @@ export function createSessionsSendTool(opts?: { const timeoutSeconds = typeof params.timeoutSeconds === "number" && Number.isFinite(params.timeoutSeconds) ? Math.max(0, Math.floor(params.timeoutSeconds)) - : 30; + : 90; const timeoutMs = timeoutSeconds * 1000; const announceTimeoutMs = timeoutSeconds === 0 ? 30_000 : timeoutMs; const idempotencyKey = crypto.randomUUID(); @@ -315,15 +315,21 @@ export function createSessionsSendTool(opts?: { } catch (err) { const messageText = err instanceof Error ? err.message : typeof err === "string" ? err : "error"; + const isTimeout = messageText.includes("gateway timeout"); + if (isTimeout) { + // Same late-reply fix: deliver via A2A when gateway-level timeout fires + startA2AFlow(undefined, runId); + } return jsonResult({ runId, - status: messageText.includes("gateway timeout") ? "timeout" : "error", + status: isTimeout ? "timeout" : "error", error: messageText, sessionKey: displayKey, }); } if (waitStatus === "timeout") { + startA2AFlow(undefined, runId); return jsonResult({ runId, status: "timeout", diff --git a/src/agents/tools/sessions.test.ts b/src/agents/tools/sessions.test.ts index aa831027f68..447d1261a3d 100644 --- a/src/agents/tools/sessions.test.ts +++ b/src/agents/tools/sessions.test.ts @@ -9,6 +9,11 @@ vi.mock("../../gateway/call.js", () => ({ callGateway: (opts: unknown) => callGatewayMock(opts), })); +const runSessionsSendA2AFlowMock = vi.fn(); +vi.mock("./sessions-send-tool.a2a.js", () => ({ + runSessionsSendA2AFlow: (opts: unknown) => runSessionsSendA2AFlowMock(opts), +})); + type SessionsToolTestConfig = { session: { scope: "per-sender"; mainKey: string }; tools: { @@ -389,6 +394,7 @@ describe("sessions_list transcriptPath resolution", () => { describe("sessions_send gating", () => { beforeEach(() => { callGatewayMock.mockClear(); + runSessionsSendA2AFlowMock.mockClear(); }); it("returns an error when neither sessionKey nor label is provided", async () => { @@ -439,4 +445,54 @@ describe("sessions_send gating", () => { expect(callGatewayMock.mock.calls[0]?.[0]).toMatchObject({ method: "sessions.list" }); expect(result.details).toMatchObject({ status: "forbidden" }); }); + + it("sessions_send delivers reply via A2A when wait times out", async () => { + loadConfigMock.mockReturnValue({ + session: { scope: "per-sender", mainKey: "main" }, + tools: { + agentToAgent: { enabled: true }, + sessions: { visibility: "all" }, + }, + }); + + const targetKey = "agent:main:discord:dm:user1"; + const fakeRunId = "run-timeout-123"; + + // sessions.list for visibility check + callGatewayMock.mockImplementation((opts: { method: string }) => { + if (opts.method === "sessions.list") { + return Promise.resolve({ + path: "/tmp/sessions.json", + sessions: [ + { key: targetKey, kind: "direct" }, + { key: MAIN_AGENT_SESSION_KEY, kind: "direct" }, + ], + }); + } + if (opts.method === "agent") { + return Promise.resolve({ runId: fakeRunId }); + } + if (opts.method === "agent.wait") { + return Promise.resolve({ status: "timeout", error: "timed out" }); + } + return Promise.resolve({}); + }); + + const tool = createMainSessionsSendTool(); + const result = await tool.execute("call-timeout", { + sessionKey: targetKey, + message: "ping", + timeoutSeconds: 5, + }); + + expect(result.details).toMatchObject({ + status: "timeout", + runId: fakeRunId, + }); + + expect(runSessionsSendA2AFlowMock).toHaveBeenCalledTimes(1); + const a2aArgs = runSessionsSendA2AFlowMock.mock.calls[0]?.[0] as Record; + expect(a2aArgs.waitRunId).toBe(fakeRunId); + expect(a2aArgs.roundOneReply).toBeUndefined(); + }); });