mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-15 17:30:45 +00:00
fix(telegram): preserve tool-only duplicate suppression
This commit is contained in:
@@ -308,6 +308,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Memory Wiki: skip empty and whitespace-only source pages when refreshing generated Related blocks, preventing blank pages from being rewritten into Related-only stubs. Fixes #78121. Thanks @amknight.
|
||||
- LINE: reject `dmPolicy: "open"` configs without wildcard `allowFrom` so webhook DMs fail validation instead of being acknowledged and silently blocked before inbound processing. Fixes #78316.
|
||||
- Telegram/Codex: keep message-tool-only progress drafts visible and render native Codex tool progress once per tool instead of duplicating item/tool draft lines. Fixes #75641. (#77949) Thanks @keshavbotagent.
|
||||
- Telegram: keep duplicate message-tool-only Codex turns from posting generic silent-reply fallback text, so private finals stay private after inbound dedupe. Thanks @rubencu.
|
||||
- Telegram/sessions: gap-fill delivered embedded final replies into the session JSONL even when the runner trace is missing, so Telegram answers after tool calls do not vanish from the durable transcript. Fixes #77814. (#78426) Thanks @obviyus, @ChushulSuri, and @DougButdorf.
|
||||
- Providers/xAI: stop sending OpenAI-style reasoning effort controls to native Grok Responses models, so `xai/grok-4.3` no longer fails live Docker/Gateway runs with `Invalid reasoning effort`.
|
||||
- Providers/xAI: clamp the bundled xAI thinking profile to `off` so live Gateway runs cannot send unsupported reasoning levels to native Grok Responses models.
|
||||
|
||||
@@ -1162,6 +1162,41 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add silent fallback when source delivery is message-tool-only", async () => {
|
||||
setupDraftStreams({ answerMessageId: 2001, reasoningMessageId: 3001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({
|
||||
queuedFinal: false,
|
||||
counts: { block: 0, final: 0, tool: 0 },
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
});
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext({
|
||||
ctxPayload: {
|
||||
SessionKey: "agent:main:telegram:direct:123",
|
||||
} as unknown as TelegramMessageContext["ctxPayload"],
|
||||
}),
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
silentReply: {
|
||||
direct: "disallow",
|
||||
group: "allow",
|
||||
internal: "allow",
|
||||
},
|
||||
silentReplyRewrite: {
|
||||
direct: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(deliverReplies).not.toHaveBeenCalled();
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
expect(sendMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows compacting reaction during auto-compaction and resumes thinking", async () => {
|
||||
const statusReactionController = {
|
||||
setThinking: vi.fn(async () => {}),
|
||||
|
||||
@@ -2727,6 +2727,68 @@ describe("dispatchReplyFromConfig", () => {
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps message-tool-only delivery mode on duplicate inbound returns", async () => {
|
||||
setNoAbort();
|
||||
const cfg = emptyConfig;
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "channel",
|
||||
To: "telegram:chat:123",
|
||||
MessageSid: "msg-tool-only-duplicate",
|
||||
SessionKey: "agent:main:telegram:channel:123",
|
||||
});
|
||||
const replyResolver = vi.fn(async () => ({ text: "hi" }) as ReplyPayload);
|
||||
|
||||
const first = await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
const duplicate = await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(first.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
expect(duplicate.sourceReplyDeliveryMode).toBe("message_tool_only");
|
||||
});
|
||||
|
||||
it("does not mark duplicate inbound returns as tool-only when message is unavailable", async () => {
|
||||
setNoAbort();
|
||||
const cfg = { tools: { allow: ["read"] } } as OpenClawConfig;
|
||||
const ctx = buildTestCtx({
|
||||
Provider: "telegram",
|
||||
Surface: "telegram",
|
||||
ChatType: "channel",
|
||||
To: "telegram:chat:123",
|
||||
MessageSid: "msg-tool-unavailable-duplicate",
|
||||
SessionKey: "agent:main:telegram:channel:123",
|
||||
});
|
||||
const replyResolver = vi.fn(async () => ({ text: "visible fallback" }) as ReplyPayload);
|
||||
|
||||
const first = await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
const duplicate = await dispatchReplyFromConfig({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher: createDispatcher(),
|
||||
replyResolver,
|
||||
});
|
||||
|
||||
expect(replyResolver).toHaveBeenCalledTimes(1);
|
||||
expect(first.sourceReplyDeliveryMode).toBeUndefined();
|
||||
expect(duplicate.sourceReplyDeliveryMode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps local discord exec approval tool prompts when the native runtime is inactive", async () => {
|
||||
setNoAbort();
|
||||
const cfg = {
|
||||
|
||||
@@ -430,20 +430,10 @@ export async function dispatchReplyFromConfig(
|
||||
});
|
||||
};
|
||||
|
||||
const inboundDedupeClaim = claimInboundDedupe(ctx);
|
||||
if (inboundDedupeClaim.status === "duplicate" || inboundDedupeClaim.status === "inflight") {
|
||||
recordProcessed("skipped", { reason: "duplicate" });
|
||||
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
|
||||
}
|
||||
let inboundDedupeReplayUnsafe = false;
|
||||
const markInboundDedupeReplayUnsafe = () => {
|
||||
inboundDedupeReplayUnsafe = true;
|
||||
};
|
||||
const commitInboundDedupeIfClaimed = () => {
|
||||
if (inboundDedupeClaim.status === "claimed") {
|
||||
commitInboundDedupe(inboundDedupeClaim.key);
|
||||
}
|
||||
};
|
||||
|
||||
const initialSessionStoreEntry = resolveSessionStoreLookup(ctx, cfg);
|
||||
const boundAcpDispatchSessionKey = resolveBoundAcpDispatchSessionKey({ ctx, cfg });
|
||||
@@ -807,6 +797,20 @@ export async function dispatchReplyFromConfig(
|
||||
? { ...result, sourceReplyDeliveryMode }
|
||||
: result;
|
||||
|
||||
const inboundDedupeClaim = claimInboundDedupe(ctx);
|
||||
if (inboundDedupeClaim.status === "duplicate" || inboundDedupeClaim.status === "inflight") {
|
||||
recordProcessed("skipped", { reason: "duplicate" });
|
||||
return attachSourceReplyDeliveryMode({
|
||||
queuedFinal: false,
|
||||
counts: dispatcher.getQueuedCounts(),
|
||||
});
|
||||
}
|
||||
const commitInboundDedupeIfClaimed = () => {
|
||||
if (inboundDedupeClaim.status === "claimed") {
|
||||
commitInboundDedupe(inboundDedupeClaim.key);
|
||||
}
|
||||
};
|
||||
|
||||
let pluginFallbackReason:
|
||||
| "plugin-bound-fallback-missing-plugin"
|
||||
| "plugin-bound-fallback-no-handler"
|
||||
|
||||
Reference in New Issue
Block a user