diff --git a/docs/.generated/config-baseline.sha256 b/docs/.generated/config-baseline.sha256 index fb3a8eddf13..c71fd66d5b9 100644 --- a/docs/.generated/config-baseline.sha256 +++ b/docs/.generated/config-baseline.sha256 @@ -1,4 +1,4 @@ -747a24d0acf12f95ec75feabb47dad8f03ff0e3a7173b4d277c648f75d956ce5 config-baseline.json -cbb9a6ee1cb69068d5eb63f00f95512ba19778415ea5b2eabe056aaea38978b5 config-baseline.core.json -e239cc20f20f8d0172812bc0ad3ee6df52da88e2e2702e3d03a47e01561132ae config-baseline.channel.json -b695cb31b4c0cf1d31f842f2892e99cc3ff8d84263ae72b72977cae844b81d6e config-baseline.plugin.json +12639f02b5283d35fb01136164f68301c22b8ac6786e69662142ec484924bdd8 config-baseline.json +ada810ee23e23ad739f1b652891adffb34988d455638d5e4ea3cb8ac15197304 config-baseline.core.json +99bb34fcf83ba6bb50a3fc11f170bd379bee5728b0938707fc39ebd7638e12eb config-baseline.channel.json +5f5d4e850df6e9854a85b5d008236854ce185c707fdbb566efcf00f8c08b36e3 config-baseline.plugin.json diff --git a/src/auto-reply/reply/agent-runner-execution.test.ts b/src/auto-reply/reply/agent-runner-execution.test.ts index c7e11f74516..ed1e46c86c5 100644 --- a/src/auto-reply/reply/agent-runner-execution.test.ts +++ b/src/auto-reply/reply/agent-runner-execution.test.ts @@ -931,6 +931,131 @@ describe("runAgentTurnWithFallback", () => { ); }); + it("prefers onCompactionEnd callback over default notice when notifyUser is enabled", async () => { + const onBlockReply = vi.fn(); + const onCompactionEnd = vi.fn(); + state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); + await params.onAgentEvent?.({ + stream: "compaction", + data: { phase: "end", completed: true }, + }); + return { payloads: [{ text: "final" }], meta: {} }; + }); + + const followupRun = createFollowupRun(); + followupRun.run.config = { + agents: { + defaults: { + compaction: { + notifyUser: true, + }, + }, + }, + }; + + const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); + const result = await runAgentTurnWithFallback({ + commandBody: "hello", + followupRun, + sessionCtx: { + Provider: "whatsapp", + MessageSid: "msg", + } as unknown as TemplateContext, + opts: { onBlockReply, onCompactionEnd }, + typingSignals: createMockTypingSignaler(), + blockReplyPipeline: null, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + applyReplyToMode: (payload) => payload, + shouldEmitToolResult: () => true, + shouldEmitToolOutput: () => false, + pendingToolTasks: new Set(), + resetSessionAfterCompactionFailure: async () => false, + resetSessionAfterRoleOrderingConflict: async () => false, + isHeartbeat: false, + sessionKey: "main", + getActiveSessionEntry: () => undefined, + resolvedVerboseLevel: "off", + }); + + expect(result.kind).toBe("success"); + expect(onCompactionEnd).toHaveBeenCalledTimes(1); + // The start notice still fires (no onCompactionStart callback provided), + // but the completion notice is suppressed in favor of the callback. + expect(onBlockReply).toHaveBeenCalledTimes(1); + expect(onBlockReply).toHaveBeenCalledWith( + expect.objectContaining({ + text: "🧹 Compacting context...", + isCompactionNotice: true, + }), + ); + }); + + it("emits an incomplete compaction notice when compaction ends without completing", async () => { + const onBlockReply = vi.fn(); + state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: EmbeddedAgentParams) => { + await params.onAgentEvent?.({ stream: "compaction", data: { phase: "start" } }); + await params.onAgentEvent?.({ + stream: "compaction", + data: { phase: "end", completed: false }, + }); + return { payloads: [{ text: "final" }], meta: {} }; + }); + + const followupRun = createFollowupRun(); + followupRun.run.config = { + agents: { + defaults: { + compaction: { + notifyUser: true, + }, + }, + }, + }; + + const runAgentTurnWithFallback = await getRunAgentTurnWithFallback(); + const result = await runAgentTurnWithFallback({ + commandBody: "hello", + followupRun, + sessionCtx: { + Provider: "whatsapp", + MessageSid: "msg", + } as unknown as TemplateContext, + opts: { onBlockReply }, + typingSignals: createMockTypingSignaler(), + blockReplyPipeline: null, + blockStreamingEnabled: false, + resolvedBlockStreamingBreak: "message_end", + applyReplyToMode: (payload) => payload, + shouldEmitToolResult: () => true, + shouldEmitToolOutput: () => false, + pendingToolTasks: new Set(), + resetSessionAfterCompactionFailure: async () => false, + resetSessionAfterRoleOrderingConflict: async () => false, + isHeartbeat: false, + sessionKey: "main", + getActiveSessionEntry: () => undefined, + resolvedVerboseLevel: "off", + }); + + expect(result.kind).toBe("success"); + expect(onBlockReply).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + text: "🧹 Compacting context...", + isCompactionNotice: true, + }), + ); + expect(onBlockReply).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + text: "🧹 Compaction incomplete", + isCompactionNotice: true, + }), + ); + }); + it("does not show a rate-limit countdown for mixed-cause fallback exhaustion", async () => { state.runWithModelFallbackMock.mockRejectedValueOnce( Object.assign( diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index 28f1387bd4a..2f9f6aa34bf 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -626,12 +626,18 @@ export async function runAgentTurnWithFallback(params: { const currentMessageId = params.sessionCtx.MessageSidFull ?? params.sessionCtx.MessageSid; const shouldNotifyUserAboutCompaction = runtimeConfig?.agents?.defaults?.compaction?.notifyUser === true; - const sendCompactionNotice = async (phase: "start" | "end") => { + const sendCompactionNotice = async (phase: "start" | "end" | "incomplete") => { if (!params.opts?.onBlockReply) { return; } + const text = + phase === "start" + ? "🧹 Compacting context..." + : phase === "end" + ? "🧹 Compaction complete" + : "🧹 Compaction incomplete"; const noticePayload = params.applyReplyToMode({ - text: phase === "start" ? "🧹 Compacting context..." : "🧹 Compaction complete", + text, replyToId: currentMessageId, replyToCurrent: true, isCompactionNotice: true, @@ -1172,13 +1178,17 @@ export async function runAgentTurnWithFallback(params: { await sendCompactionNotice("start"); } } - const completed = evt.data?.completed === true; - if (phase === "end" && completed) { - attemptCompactionCount += 1; - if (params.opts?.onCompactionEnd) { - await params.opts.onCompactionEnd(); + if (phase === "end") { + const completed = evt.data?.completed === true; + if (completed) { + attemptCompactionCount += 1; + if (params.opts?.onCompactionEnd) { + await params.opts.onCompactionEnd(); + } else if (shouldNotifyUserAboutCompaction) { + await sendCompactionNotice("end"); + } } else if (shouldNotifyUserAboutCompaction) { - await sendCompactionNotice("end"); + await sendCompactionNotice("incomplete"); } } }