mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-06 23:55:12 +00:00
fix: mark suppressed source replies in diagnostics
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Release validation: install the cross-OS TypeScript harness through Windows-safe Node/npm shims so native Windows package checks reach the OpenClaw smoke suites instead of exiting before artifact capture. Thanks @vincentkoc.
|
||||
- Release validation: let Windows packaged-upgrade checks continue after the shipped 2026.5.2 updater hits its native-module swap cleanup fallback, verifying the fallback-installed candidate through package metadata and downstream smoke instead of crashing on the immediate update-status probe. Thanks @vincentkoc.
|
||||
- Doctor/plugins: skip channel-derived official plugin installs when another configured plugin is the effective owner for the same channel, so `doctor --repair` does not reinstall `feishu` while `openclaw-lark` handles `channels.feishu`. Fixes #76623. Thanks @fuyizheng3120.
|
||||
- Channels/delivery: mark message-tool-only turns with generated-but-suppressed visible output as `source-reply-delivery-suppressed` in diagnostics, including rich presentation replies, so successful-but-private channel runs are easier to identify. Fixes #76980.
|
||||
- Agents/bootstrap: keep pending `BOOTSTRAP.md` and bootstrap truncation notices in system-prompt Project Context instead of copying setup text or raw warning diagnostics into WebChat user/runtime context. Fixes #76946.
|
||||
- Channels/WhatsApp: allow `@whiskeysockets/libsignal-node` in `onlyBuiltDependencies` so pnpm v9+ `blockExoticSubdeps` no longer rejects the baileys git-tarball subdep and silences all inbound agent replies. Fixes #76539. Thanks @ottodeng and @vincentkoc.
|
||||
- Gateway/install: keep `.env`-managed values in the macOS LaunchAgent env file while still tracking `OPENCLAW_SERVICE_MANAGED_ENV_KEYS`, so regenerated services do not boot without managed auth/provider keys. Fixes #75374.
|
||||
|
||||
@@ -82,6 +82,7 @@ function createMessageEndContext(
|
||||
finalizeAssistantTexts?: ReturnType<typeof vi.fn>;
|
||||
consumeReplyDirectives?: ReturnType<typeof vi.fn>;
|
||||
warn?: ReturnType<typeof vi.fn>;
|
||||
debug?: ReturnType<typeof vi.fn>;
|
||||
builtinToolNames?: ReadonlySet<string>;
|
||||
state?: Record<string, unknown>;
|
||||
} = {},
|
||||
@@ -125,7 +126,7 @@ function createMessageEndContext(
|
||||
noteLastAssistant: vi.fn(),
|
||||
recordAssistantUsage: vi.fn(),
|
||||
commitAssistantUsage: vi.fn(),
|
||||
log: { debug: vi.fn(), warn: params.warn ?? vi.fn() },
|
||||
log: { debug: params.debug ?? vi.fn(), warn: params.warn ?? vi.fn() },
|
||||
builtinToolNames: params.builtinToolNames,
|
||||
stripBlockTags: (text: string) => text,
|
||||
finalizeAssistantTexts: params.finalizeAssistantTexts ?? vi.fn(),
|
||||
@@ -756,11 +757,13 @@ describe("handleMessageEnd", () => {
|
||||
});
|
||||
|
||||
it("does not duplicate block reply for text_end channels even when stripping differs", () => {
|
||||
const debug = vi.fn();
|
||||
const onBlockReply = vi.fn();
|
||||
const emitBlockReply = vi.fn();
|
||||
// Same pattern: directive accumulator returns null for empty final flush
|
||||
const consumeReplyDirectives = vi.fn((text: string) => (text ? { text } : null));
|
||||
const ctx = createMessageEndContext({
|
||||
debug,
|
||||
onBlockReply,
|
||||
emitBlockReply,
|
||||
consumeReplyDirectives,
|
||||
@@ -789,6 +792,9 @@ describe("handleMessageEnd", () => {
|
||||
// send should NOT fire for text_end channels. The only consumeReplyDirectives
|
||||
// call is the final empty flush which returns null.
|
||||
expect(emitBlockReply).not.toHaveBeenCalled();
|
||||
expect(debug).toHaveBeenCalledWith(
|
||||
"Skipping message_end safety send for text_end channel - content already emitted via text_end",
|
||||
);
|
||||
});
|
||||
|
||||
it("emits a replacement final assistant event when final_answer appears only at message_end", () => {
|
||||
|
||||
@@ -856,7 +856,7 @@ export function handleMessageEnd(
|
||||
// lastBlockReplyText is still null and message_end must deliver.
|
||||
if (ctx.state.blockReplyBreak === "text_end" && ctx.state.lastBlockReplyText != null) {
|
||||
ctx.log.debug(
|
||||
`Skipping message_end safety send for text_end channel - content already delivered via text_end`,
|
||||
`Skipping message_end safety send for text_end channel - content already emitted via text_end`,
|
||||
);
|
||||
} else {
|
||||
// Check for duplicates before emitting (same logic as emitBlockChunk).
|
||||
|
||||
@@ -4587,6 +4587,50 @@ describe("sendPolicy deny — suppress delivery, not processing (#53328)", () =>
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("marks diagnostics when message-tool-only suppresses generated visible output", async () => {
|
||||
setNoAbort();
|
||||
const cfg = { diagnostics: { enabled: true } } as OpenClawConfig;
|
||||
const dispatcher = createDispatcher();
|
||||
const richPayload = {
|
||||
presentation: {
|
||||
blocks: [
|
||||
{
|
||||
type: "buttons",
|
||||
buttons: [{ label: "Open", url: "https://example.test/result" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies ReplyPayload;
|
||||
const replyResolver = vi.fn(async (_ctx: MsgContext, opts?: GetReplyOptions) => {
|
||||
await opts?.onBlockReply?.(richPayload);
|
||||
return richPayload;
|
||||
});
|
||||
|
||||
const result = await dispatchReplyFromConfig({
|
||||
ctx: buildTestCtx({
|
||||
ChatType: "channel",
|
||||
Provider: "discord",
|
||||
Surface: "discord",
|
||||
SessionKey: "test:discord:channel:C1",
|
||||
}),
|
||||
cfg,
|
||||
dispatcher,
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(result.queuedFinal).toBe(false);
|
||||
expect(dispatcher.sendBlockReply).not.toHaveBeenCalled();
|
||||
expect(dispatcher.sendFinalReply).not.toHaveBeenCalled();
|
||||
expect(diagnosticMocks.logMessageProcessed).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
outcome: "completed",
|
||||
reason: "source-reply-delivery-suppressed",
|
||||
sessionKey: "test:discord:channel:C1",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not auto-route same-provider group/channel final replies in message-tool-only mode", async () => {
|
||||
setNoAbort();
|
||||
mocks.routeReply.mockClear();
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
import { isDiagnosticsEnabled } from "../../infra/diagnostic-events.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { getSessionBindingService } from "../../infra/outbound/session-binding-service.js";
|
||||
import { hasReplyPayloadContent } from "../../interactive/payload.js";
|
||||
import {
|
||||
logMessageProcessed,
|
||||
logMessageQueued,
|
||||
@@ -760,6 +761,21 @@ export async function dispatchReplyFromConfig(
|
||||
sourceReplyDeliveryMode === "message_tool_only"
|
||||
? { ...result, sourceReplyDeliveryMode }
|
||||
: result;
|
||||
let suppressedSourceReplyContent = false;
|
||||
const markSuppressedSourceReplyContent = (payload: ReplyPayload) => {
|
||||
if (!suppressDelivery || payload.isReasoning === true) {
|
||||
return;
|
||||
}
|
||||
if (!hasReplyPayloadContent(payload, { extraContent: payload.audioAsVoice })) {
|
||||
return;
|
||||
}
|
||||
if (!suppressedSourceReplyContent) {
|
||||
logVerbose(
|
||||
`automatic source reply content suppressed by ${deliverySuppressionReason || "delivery policy"}; visible output requires the message tool or automatic visible replies`,
|
||||
);
|
||||
}
|
||||
suppressedSourceReplyContent = true;
|
||||
};
|
||||
|
||||
let pluginFallbackReason:
|
||||
| "plugin-bound-fallback-missing-plugin"
|
||||
@@ -1352,6 +1368,7 @@ export async function dispatchReplyFromConfig(
|
||||
markInboundDedupeReplayUnsafe();
|
||||
}
|
||||
if (suppressDelivery) {
|
||||
markSuppressedSourceReplyContent(payload);
|
||||
return;
|
||||
}
|
||||
// Suppress reasoning payloads — channels using this generic dispatch
|
||||
@@ -1540,6 +1557,10 @@ export async function dispatchReplyFromConfig(
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const reply of replies) {
|
||||
markSuppressedSourceReplyContent(reply);
|
||||
}
|
||||
}
|
||||
|
||||
const counts = dispatcher.getQueuedCounts();
|
||||
@@ -1547,7 +1568,11 @@ export async function dispatchReplyFromConfig(
|
||||
commitInboundDedupeIfClaimed();
|
||||
recordProcessed(
|
||||
"completed",
|
||||
pluginFallbackReason ? { reason: pluginFallbackReason } : undefined,
|
||||
pluginFallbackReason
|
||||
? { reason: pluginFallbackReason }
|
||||
: suppressedSourceReplyContent
|
||||
? { reason: "source-reply-delivery-suppressed" }
|
||||
: undefined,
|
||||
);
|
||||
markIdle("message_completed");
|
||||
return attachSourceReplyDeliveryMode({ queuedFinal, counts });
|
||||
|
||||
Reference in New Issue
Block a user