diff --git a/src/agents/tools/sessions-send-tool.ts b/src/agents/tools/sessions-send-tool.ts index b2873e5cd1f..eb3bb738bba 100644 --- a/src/agents/tools/sessions-send-tool.ts +++ b/src/agents/tools/sessions-send-tool.ts @@ -217,7 +217,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(); @@ -321,15 +321,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 ce849e45d07..15bf86427a7 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: { @@ -399,6 +404,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 () => { @@ -449,4 +455,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(); + }); });