mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
fix(auto-reply): suppress room-event followup notices
This commit is contained in:
@@ -843,6 +843,40 @@ describe("createFollowupRunner reply-lane admission", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses preflight compaction failure notices for queued room events", async () => {
|
||||
runPreflightCompactionIfNeededMock.mockRejectedValueOnce(
|
||||
new Error("Preflight compaction required but failed: auth profile mismatch"),
|
||||
);
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
sessionKey: "main",
|
||||
defaultModel: "anthropic/claude",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
currentInboundEventKind: "room_event",
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
originatingAccountId: "acct-1",
|
||||
originatingThreadId: "thread-1",
|
||||
originatingChatType: "group",
|
||||
run: {
|
||||
messageProvider: "discord",
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
verboseLevel: "off",
|
||||
sessionKey: "main",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(runEmbeddedAgentMock).not.toHaveBeenCalled();
|
||||
expect(routeReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("preserves non-compaction preflight failures for queued followup runs", async () => {
|
||||
runPreflightCompactionIfNeededMock.mockRejectedValueOnce(new Error("session load failed"));
|
||||
const runner = createFollowupRunner({
|
||||
@@ -1424,6 +1458,7 @@ describe("createFollowupRunner runtime config", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
expect(runCliAgentMock).toHaveBeenCalledOnce();
|
||||
expect(routeReplyMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
payload: { text: "persisted CLI followup" },
|
||||
@@ -2353,6 +2388,179 @@ describe("createFollowupRunner progress forwarding", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps queued room-event verbose tool summaries suppressed", async () => {
|
||||
const queued = createQueuedRun({
|
||||
currentInboundEventKind: "room_event",
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
originatingAccountId: "acct-1",
|
||||
originatingThreadId: "thread-1",
|
||||
run: {
|
||||
messageProvider: "discord",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
verboseLevel: "on",
|
||||
},
|
||||
});
|
||||
|
||||
runEmbeddedAgentMock.mockImplementationOnce(
|
||||
async (args: {
|
||||
onToolResult?: (payload: { text: string }) => Promise<void>;
|
||||
shouldEmitToolResult?: () => boolean;
|
||||
}) => {
|
||||
expect(args.shouldEmitToolResult?.()).toBe(true);
|
||||
await args.onToolResult?.({ text: "🛠️ Exec: echo ambient-progress" });
|
||||
return { payloads: [], meta: { agentMeta: {} } };
|
||||
},
|
||||
);
|
||||
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
defaultModel: "claude",
|
||||
});
|
||||
|
||||
await runner(queued);
|
||||
|
||||
expect(routeReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delivers queued fast auto progress for non-room-event message-tool-only turns", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1_000);
|
||||
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
|
||||
"../../infra/agent-events.js",
|
||||
);
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"claude-cli": { command: "claude" },
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
runCliAgentMock.mockImplementationOnce((params: { runId?: string }) => {
|
||||
realAgentEvents.emitAgentEvent({
|
||||
runId: params.runId ?? "run-fast-followup",
|
||||
stream: "tool",
|
||||
data: { phase: "start", name: "bash", toolCallId: "call-1" },
|
||||
});
|
||||
vi.setSystemTime(7_100);
|
||||
realAgentEvents.emitAgentEvent({
|
||||
runId: params.runId ?? "run-fast-followup",
|
||||
stream: "tool",
|
||||
data: { phase: "result", name: "bash", toolCallId: "call-1" },
|
||||
});
|
||||
return { payloads: [], meta: { agentMeta: {} } };
|
||||
});
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
defaultModel: "anthropic/claude-opus-4-7",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
currentInboundEventKind: "user_request",
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
originatingAccountId: "acct-1",
|
||||
originatingThreadId: "thread-1",
|
||||
run: {
|
||||
config: runtimeConfig,
|
||||
messageProvider: "discord",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
fastMode: "auto",
|
||||
fastModeOverride: true,
|
||||
fastModeAutoOnSeconds: 5,
|
||||
fastModeAutoOnSecondsOverride: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(routeReplyMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "discord",
|
||||
to: "channel:C1",
|
||||
accountId: "acct-1",
|
||||
threadId: "thread-1",
|
||||
mirror: false,
|
||||
replyKind: "tool",
|
||||
payload: expect.objectContaining({
|
||||
text: "💨Fast: auto-off(6s>=5s)",
|
||||
channelData: { openclawProgressKind: "fast-mode-auto" },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("suppresses queued fast auto progress for room-event message-tool-only turns", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(1_000);
|
||||
const realAgentEvents = await vi.importActual<typeof import("../../infra/agent-events.js")>(
|
||||
"../../infra/agent-events.js",
|
||||
);
|
||||
const runtimeConfig: OpenClawConfig = {
|
||||
agents: {
|
||||
defaults: {
|
||||
cliBackends: {
|
||||
"claude-cli": { command: "claude" },
|
||||
},
|
||||
models: {
|
||||
"anthropic/claude-opus-4-7": { agentRuntime: { id: "claude-cli" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
runCliAgentMock.mockImplementationOnce((params: { runId?: string }) => {
|
||||
realAgentEvents.emitAgentEvent({
|
||||
runId: params.runId ?? "run-fast-followup",
|
||||
stream: "tool",
|
||||
data: { phase: "start", name: "bash", toolCallId: "call-1" },
|
||||
});
|
||||
vi.setSystemTime(7_100);
|
||||
realAgentEvents.emitAgentEvent({
|
||||
runId: params.runId ?? "run-fast-followup",
|
||||
stream: "tool",
|
||||
data: { phase: "result", name: "bash", toolCallId: "call-1" },
|
||||
});
|
||||
return { payloads: [], meta: { agentMeta: {} } };
|
||||
});
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
defaultModel: "anthropic/claude-opus-4-7",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
currentInboundEventKind: "room_event",
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
originatingAccountId: "acct-1",
|
||||
originatingThreadId: "thread-1",
|
||||
run: {
|
||||
config: runtimeConfig,
|
||||
messageProvider: "discord",
|
||||
provider: "anthropic",
|
||||
model: "claude-opus-4-7",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
fastMode: "auto",
|
||||
fastModeOverride: true,
|
||||
fastModeAutoOnSeconds: 5,
|
||||
fastModeAutoOnSecondsOverride: true,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(routeReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("drains fire-and-forget queued tool progress before final delivery", async () => {
|
||||
const queued = createQueuedRun({
|
||||
originatingChannel: "discord",
|
||||
@@ -4091,6 +4299,24 @@ describe("createFollowupRunner messaging delivery and dedupe", () => {
|
||||
expectNoBlockReplyText(onBlockReply, "second payload");
|
||||
});
|
||||
|
||||
it("suppresses cross-channel route-failure notices for room events", async () => {
|
||||
routeReplyMock.mockResolvedValue({
|
||||
ok: false,
|
||||
error: "forced route failure",
|
||||
});
|
||||
const queued = baseQueuedRun("webchat");
|
||||
queued.currentInboundEventKind = "room_event";
|
||||
queued.originatingChannel = "discord";
|
||||
queued.originatingTo = "channel:C1";
|
||||
const { onBlockReply } = await runMessagingCase({
|
||||
agentResult: { payloads: [{ text: "hello world!" }, { text: "second payload" }] },
|
||||
queued,
|
||||
});
|
||||
|
||||
expect(routeReplyMock).toHaveBeenCalledTimes(2);
|
||||
expect(onBlockReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not emit cross-channel route-failure notice when a later payload routes", async () => {
|
||||
routeReplyMock
|
||||
.mockResolvedValueOnce({
|
||||
@@ -4433,6 +4659,47 @@ describe("createFollowupRunner messaging delivery and dedupe", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("suppresses queued compaction notices for room events", async () => {
|
||||
runPreflightCompactionIfNeededMock.mockImplementationOnce(
|
||||
async (params: {
|
||||
onCompactionNotice?: (phase: "start" | "end") => Promise<void> | void;
|
||||
sessionEntry?: SessionEntry;
|
||||
}) => {
|
||||
await params.onCompactionNotice?.("start");
|
||||
await params.onCompactionNotice?.("end");
|
||||
return params.sessionEntry;
|
||||
},
|
||||
);
|
||||
runEmbeddedAgentMock.mockResolvedValueOnce({
|
||||
payloads: [],
|
||||
meta: {},
|
||||
});
|
||||
const runner = createFollowupRunner({
|
||||
typing: createMockTypingController(),
|
||||
typingMode: "instant",
|
||||
defaultModel: "openai/gpt-5.5",
|
||||
});
|
||||
|
||||
await runner(
|
||||
createQueuedRun({
|
||||
currentInboundEventKind: "room_event",
|
||||
originatingChannel: "discord",
|
||||
originatingTo: "channel:C1",
|
||||
messageId: "current-msg-1",
|
||||
run: {
|
||||
config: {
|
||||
channels: { discord: { replyToMode: "all" } },
|
||||
agents: { defaults: { compaction: { notifyUser: true } } },
|
||||
},
|
||||
messageProvider: "discord",
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(routeReplyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("routes queued compaction hook messages alongside notifyUser notices (#90185)", async () => {
|
||||
runEmbeddedAgentMock.mockImplementationOnce(
|
||||
async (args: {
|
||||
|
||||
@@ -482,6 +482,10 @@ export function createFollowupRunner(params: {
|
||||
!routedAnyCrossChannelPayloadToOrigin &&
|
||||
opts?.onBlockReply
|
||||
) {
|
||||
if (queued.currentInboundEventKind === "room_event") {
|
||||
logVerbose("followup queue: cross-channel failure notice suppressed for room_event");
|
||||
return;
|
||||
}
|
||||
await sendDispatcherPayload({
|
||||
text:
|
||||
"Follow-up completed, but OpenClaw could not deliver it to the originating " +
|
||||
@@ -560,6 +564,7 @@ export function createFollowupRunner(params: {
|
||||
shouldEmitVerboseProgress() && !shouldSuppressDefaultToolProgressMessages();
|
||||
const shouldEmitToolOutputProgress = () =>
|
||||
resolveCurrentVerboseLevel() === "full" && !shouldSuppressDefaultToolProgressMessages();
|
||||
const isRoomEventFollowup = () => queued.currentInboundEventKind === "room_event";
|
||||
let observedVisibleToolErrorProgress = false;
|
||||
const markVisibleToolErrorProgress = () => {
|
||||
if (resolveCurrentVerboseLevel() === "on" && shouldEmitToolResultProgress()) {
|
||||
@@ -653,6 +658,10 @@ export function createFollowupRunner(params: {
|
||||
modelId: fallbackModel,
|
||||
},
|
||||
) => {
|
||||
if (isRoomEventFollowup()) {
|
||||
logVerbose("followup queue: compaction notice suppressed for room_event");
|
||||
return;
|
||||
}
|
||||
const noticePayloads = resolveFollowupDeliveryPayloads({
|
||||
cfg: runtimeConfig,
|
||||
payloads: [payload],
|
||||
@@ -719,6 +728,12 @@ export function createFollowupRunner(params: {
|
||||
includeDetails: run.verboseLevel === "on" || run.verboseLevel === "full",
|
||||
});
|
||||
if (preflightCompactionFailureText) {
|
||||
if (isRoomEventFollowup()) {
|
||||
logVerbose(
|
||||
"followup queue: preflight compaction failure notice suppressed for room_event",
|
||||
);
|
||||
return;
|
||||
}
|
||||
await sendFollowupPayloads(
|
||||
[
|
||||
markReplyPayloadForSourceSuppressionDelivery({
|
||||
@@ -932,6 +947,11 @@ export function createFollowupRunner(params: {
|
||||
// summary tracker so both runners deliver identical durable summaries.
|
||||
const deliverFollowupToolSummary = (payload: ReplyPayload) =>
|
||||
enqueueProgressDelivery(async () => {
|
||||
// room_event turns are ambient; only an explicit message tool call
|
||||
// may post back into the source chat.
|
||||
if (isRoomEventFollowup()) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
run.sourceReplyDeliveryMode === "message_tool_only" &&
|
||||
!shouldEmitToolResultProgress()
|
||||
@@ -1013,6 +1033,11 @@ export function createFollowupRunner(params: {
|
||||
: undefined,
|
||||
onFastModeAutoProgress: async (payload) => {
|
||||
await enqueueProgressDelivery(async () => {
|
||||
// Mirrors direct dispatch progress suppression: ambient
|
||||
// room events never get automatic fast-mode notices.
|
||||
if (isRoomEventFollowup()) {
|
||||
return;
|
||||
}
|
||||
await sendFollowupPayloads(
|
||||
[payload],
|
||||
effectiveQueued,
|
||||
|
||||
Reference in New Issue
Block a user