diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ddc5bb6d93..d440c967f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Agents/sessions: keep delayed `sessions_send` A2A replies alive after soft wait-window timeouts, while preserving terminal run timeouts and avoiding stale target replies in requester sessions. Fixes #76443. Thanks @ryswork1993 and @vincentkoc. +- CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc. - Config/doctor: cap `.clobbered.*` forensic snapshots per config path and serialize snapshot writes so repeated `doctor --fix` recovery loops cannot flood the config directory. Fixes #76454; carries forward #65649. Thanks @JUSTICEESSIELP, @rsnow, and @vincentkoc. - Feishu: suppress duplicate text when replies send native voice media while preserving captions for ordinary audio files and falling back to text plus attachment links when voice uploads fail. - Feishu: keep packaged Feishu startup from bundling the Lark SDK's ESM `__dirname` path by loading the SDK as a plugin-local runtime dependency. Fixes #76291 and #76494. (#76392) Thanks @zqchris. diff --git a/src/agents/command/delivery.ts b/src/agents/command/delivery.ts index dffc20a2fc0..9a1f3c06524 100644 --- a/src/agents/command/delivery.ts +++ b/src/agents/command/delivery.ts @@ -350,7 +350,6 @@ export async function deliverAgentCommandResult(params: { } if (!payloads || payloads.length === 0) { - runtime.log("No reply from agent."); return { payloads: [], meta: resultMeta }; } diff --git a/src/commands/agent-via-gateway.test.ts b/src/commands/agent-via-gateway.test.ts index cdbaccecaba..cb574074db0 100644 --- a/src/commands/agent-via-gateway.test.ts +++ b/src/commands/agent-via-gateway.test.ts @@ -147,6 +147,42 @@ describe("agentCliCommand", () => { }); }); + it("stays silent when the gateway returns an intentional empty reply", async () => { + await withTempStore(async () => { + callGateway.mockResolvedValue({ + runId: "idem-1", + status: "ok", + summary: "completed", + result: { + payloads: [], + meta: { stub: true }, + }, + }); + + await agentCliCommand({ message: "hi", to: "+1555" }, runtime); + + expect(runtime.log).not.toHaveBeenCalled(); + }); + }); + + it("logs non-ok gateway summaries when payloads are empty", async () => { + await withTempStore(async () => { + callGateway.mockResolvedValue({ + runId: "idem-1", + status: "timeout", + summary: "aborted", + result: { + payloads: [], + meta: { aborted: true }, + }, + }); + + await agentCliCommand({ message: "hi", to: "+1555" }, runtime); + + expect(runtime.log).toHaveBeenCalledWith("aborted"); + }); + }); + it("passes model overrides through gateway requests", async () => { await withTempStore(async () => { mockGatewaySuccessReply(); diff --git a/src/commands/agent-via-gateway.ts b/src/commands/agent-via-gateway.ts index 2f117147568..23965faa43b 100644 --- a/src/commands/agent-via-gateway.ts +++ b/src/commands/agent-via-gateway.ts @@ -201,7 +201,9 @@ async function agentViaGatewayCommand(opts: AgentCliOpts, runtime: RuntimeEnv) { const payloads = result?.payloads ?? []; if (payloads.length === 0) { - runtime.log(response?.summary ? response.summary : "No reply from agent."); + if (response?.status !== "ok") { + runtime.log(response?.summary ? response.summary : "No reply from agent."); + } return response; } diff --git a/src/commands/agent.acp.test.ts b/src/commands/agent.acp.test.ts index 9953fae20b4..ff2148797e7 100644 --- a/src/commands/agent.acp.test.ts +++ b/src/commands/agent.acp.test.ts @@ -378,6 +378,16 @@ describe("agentCommand ACP runtime routing", () => { }); }); + it("keeps no-reply ACP turns silent", async () => { + await withAcpSessionEnv(async () => { + const { assistantEvents, logLines } = await runAcpTurnWithAssistantEvents(["NO_REPLY"]); + + expect(assistantEvents.map((event) => event.text).filter(Boolean)).toEqual([]); + expect(logLines.some((line) => line.includes("NO_REPLY"))).toBe(false); + expect(logLines).toEqual([]); + }); + }); + it("fails closed for ACP-shaped session keys missing ACP metadata", async () => { await withTempHome(async (home) => { const storePath = path.join(home, "sessions.json"); diff --git a/src/commands/agent.delivery.test.ts b/src/commands/agent.delivery.test.ts index 5772a91b4a7..56042cf7b62 100644 --- a/src/commands/agent.delivery.test.ts +++ b/src/commands/agent.delivery.test.ts @@ -200,6 +200,21 @@ describe("deliverAgentCommandResult", () => { ); }); + it("stays silent for intentional empty payloads", async () => { + const runtime = createRuntime(); + + await runDelivery({ + opts: { + message: "hello", + }, + runtime, + payloads: [], + }); + + expect(runtime.log).not.toHaveBeenCalled(); + expect(mocks.deliverOutboundPayloads).not.toHaveBeenCalled(); + }); + it("uses runContext turn source over stale session last route", async () => { await runDelivery({ opts: {