mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
Telegram: keep verbose tool results separate from final answers (#80294)
* Telegram: keep tool progress separate from final answers * Telegram: separate verbose tool results from final answers * fix: separate Telegram verbose tool drafts
This commit is contained in:
@@ -294,6 +294,7 @@ Docs: https://docs.openclaw.ai
|
||||
- ACPX/Windows: hide the MCP proxy target child process window on Windows so ACP-backed agents do not flash or fail because of terminal window handling. Fixes #60672. (#60678) Thanks @KChow-ctrl.
|
||||
- Agents: abort generic repeated no-progress tool loops at the critical threshold when identical calls keep returning identical outcomes. (#80668) Thanks @frankekn.
|
||||
- Exec approvals: omit generated command highlights for non-POSIX Windows and shell-wrapper approval commands until those command languages have native highlighting support. (#80566) Thanks @jesse-merhi.
|
||||
- Telegram: keep verbose tool progress and result drafts separate from the final assistant answer so tool output no longer blends into the final Telegram message. (#80294) Thanks @jalehman.
|
||||
|
||||
## 2026.5.9
|
||||
|
||||
|
||||
@@ -1118,6 +1118,49 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(deliverReplies).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rotates a tool-progress-only answer draft before streaming the final answer", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
async ({ dispatcherOptions, replyOptions }) => {
|
||||
await replyOptions?.onToolStart?.({ name: "exec", phase: "start" });
|
||||
await dispatcherOptions.deliver({ text: "Branch is up to date" }, { kind: "final" });
|
||||
return { queuedFinal: true };
|
||||
},
|
||||
);
|
||||
|
||||
await dispatchWithContext({ context: createContext() });
|
||||
|
||||
expect(answerDraftStream.update).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.stringMatching(/`🛠️ Exec`$/),
|
||||
);
|
||||
expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Branch is up to date");
|
||||
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(answerDraftStream.clear).not.toHaveBeenCalled();
|
||||
const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0];
|
||||
const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1];
|
||||
expect(rotationOrder).toBeLessThan(finalUpdateOrder);
|
||||
});
|
||||
|
||||
it("rotates a verbose tool result draft before streaming the final answer", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
||||
await dispatcherOptions.deliver({ text: "🛠️ Exec: pnpm test" }, { kind: "tool" });
|
||||
await dispatcherOptions.deliver({ text: "Tests passed" }, { kind: "final" });
|
||||
return { queuedFinal: true };
|
||||
});
|
||||
|
||||
await dispatchWithContext({ context: createContext() });
|
||||
|
||||
expect(answerDraftStream.update).toHaveBeenNthCalledWith(1, "🛠️ Exec: pnpm test");
|
||||
expect(answerDraftStream.update).toHaveBeenNthCalledWith(2, "Tests passed");
|
||||
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(answerDraftStream.clear).not.toHaveBeenCalled();
|
||||
const rotationOrder = answerDraftStream.forceNewMessage.mock.invocationCallOrder[0];
|
||||
const finalUpdateOrder = answerDraftStream.update.mock.invocationCallOrder[1];
|
||||
expect(rotationOrder).toBeLessThan(finalUpdateOrder);
|
||||
});
|
||||
|
||||
it("keeps progress updates in a draft and sends the final answer normally", async () => {
|
||||
const { answerDraftStream } = setupDraftStreams({ answerMessageId: 2001 });
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
@@ -1143,7 +1186,8 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
"Cracking...\n`🛠️ Exec`\n`🛠️ git rev-parse --abbrev-ref HEAD`",
|
||||
);
|
||||
expect(answerDraftStream.update).not.toHaveBeenCalledWith("Branch is up to date");
|
||||
expect(answerDraftStream.clear).toHaveBeenCalledTimes(1);
|
||||
expect(answerDraftStream.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(answerDraftStream.clear).not.toHaveBeenCalled();
|
||||
expectDeliveredReply(0, { text: "Branch is up to date" });
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -1264,9 +1308,9 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(draftStream.update).toHaveBeenCalledWith(
|
||||
"Shelling\n`🔎 Web Search: docs lookup`\n• `tests passed`",
|
||||
);
|
||||
expect(draftStream.forceNewMessage).not.toHaveBeenCalled();
|
||||
expect(draftStream.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(draftStream.materialize).not.toHaveBeenCalled();
|
||||
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
||||
expect(draftStream.clear).not.toHaveBeenCalled();
|
||||
expectDeliveredReply(0, { text: "Final after tool" });
|
||||
expect(editMessageTelegram).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -588,6 +588,19 @@ export const dispatchTelegramMessage = async ({
|
||||
let streamToolProgressSuppressed = false;
|
||||
let streamToolProgressLines: string[] = [];
|
||||
let lastAnswerPartialText = "";
|
||||
let activeAnswerDraftIsToolProgressOnly = false;
|
||||
function resetAnswerToolProgressDraft() {
|
||||
activeAnswerDraftIsToolProgressOnly = false;
|
||||
}
|
||||
async function prepareAnswerLaneForToolProgress() {
|
||||
if (activeAnswerDraftIsToolProgressOnly) {
|
||||
return;
|
||||
}
|
||||
if (answerLane.hasStreamedMessage) {
|
||||
await rotateLaneForNewMessage(answerLane);
|
||||
}
|
||||
activeAnswerDraftIsToolProgressOnly = true;
|
||||
}
|
||||
const renderProgressDraft = async (options?: { flush?: boolean }) => {
|
||||
if (!answerLane.stream || streamMode !== "progress") {
|
||||
return;
|
||||
@@ -601,6 +614,7 @@ export const dispatchTelegramMessage = async ({
|
||||
if (!streamText || streamText === answerLane.lastPartialText) {
|
||||
return;
|
||||
}
|
||||
await prepareAnswerLaneForToolProgress();
|
||||
answerLane.lastPartialText = streamText;
|
||||
answerLane.hasStreamedMessage = true;
|
||||
answerLane.finalized = false;
|
||||
@@ -640,6 +654,7 @@ export const dispatchTelegramMessage = async ({
|
||||
seed: progressSeed,
|
||||
formatLine: formatProgressAsMarkdownCode,
|
||||
});
|
||||
await prepareAnswerLaneForToolProgress();
|
||||
answerLane.lastPartialText = streamText;
|
||||
answerLane.hasStreamedMessage = true;
|
||||
answerLane.finalized = false;
|
||||
@@ -732,6 +747,9 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
lane.hasStreamedMessage = false;
|
||||
lane.finalized = false;
|
||||
if (lane === answerLane) {
|
||||
resetAnswerToolProgressDraft();
|
||||
}
|
||||
};
|
||||
const rotateLaneForNewMessage = async (lane: DraftLaneState) => {
|
||||
if (!lane.hasStreamedMessage && typeof lane.stream?.messageId() !== "number") {
|
||||
@@ -742,7 +760,21 @@ export const dispatchTelegramMessage = async ({
|
||||
lane.stream?.forceNewMessage();
|
||||
resetDraftLaneState(lane);
|
||||
};
|
||||
const rotateAnswerLaneAfterToolProgress = async () => {
|
||||
if (!activeAnswerDraftIsToolProgressOnly) {
|
||||
return false;
|
||||
}
|
||||
await answerLane.stream?.stop();
|
||||
answerLane.stream?.forceNewMessage();
|
||||
resetDraftLaneState(answerLane);
|
||||
streamToolProgressSuppressed = true;
|
||||
streamToolProgressLines = [];
|
||||
return true;
|
||||
};
|
||||
const prepareAnswerLaneForText = async () => {
|
||||
if (await rotateAnswerLaneAfterToolProgress()) {
|
||||
return;
|
||||
}
|
||||
if (!answerLane.finalized) {
|
||||
return;
|
||||
}
|
||||
@@ -762,6 +794,7 @@ export const dispatchTelegramMessage = async ({
|
||||
if (streamMode === "progress") {
|
||||
return;
|
||||
}
|
||||
resetAnswerToolProgressDraft();
|
||||
streamToolProgressSuppressed = true;
|
||||
streamToolProgressLines = [];
|
||||
}
|
||||
@@ -1092,8 +1125,12 @@ export const dispatchTelegramMessage = async ({
|
||||
payload: ReplyPayload,
|
||||
text: string,
|
||||
): Promise<LaneDeliveryResult> => {
|
||||
await answerLane.stream?.clear();
|
||||
resetDraftLaneState(answerLane);
|
||||
if (activeAnswerDraftIsToolProgressOnly) {
|
||||
await rotateAnswerLaneAfterToolProgress();
|
||||
} else {
|
||||
await answerLane.stream?.clear();
|
||||
resetDraftLaneState(answerLane);
|
||||
}
|
||||
const delivered = await sendPayload(applyTextToPayload(payload, text), { durable: true });
|
||||
answerLane.finalized = true;
|
||||
return delivered ? { kind: "sent" } : { kind: "skipped" };
|
||||
@@ -1216,6 +1253,24 @@ export const dispatchTelegramMessage = async ({
|
||||
const segments = split.segments;
|
||||
const reply = resolveSendableOutboundReplyParts(effectivePayload);
|
||||
|
||||
const deliverFinalAnswerText = async (
|
||||
answerPayload: ReplyPayload,
|
||||
text: string,
|
||||
buttons?: TelegramInlineButtons,
|
||||
) => {
|
||||
if (streamMode === "progress") {
|
||||
return deliverProgressModeFinalAnswer(answerPayload, text);
|
||||
}
|
||||
await rotateAnswerLaneAfterToolProgress();
|
||||
return deliverLaneText({
|
||||
laneName: "answer",
|
||||
text,
|
||||
payload: answerPayload,
|
||||
infoKind: "final",
|
||||
buttons,
|
||||
});
|
||||
};
|
||||
|
||||
const flushBufferedFinalAnswer = async () => {
|
||||
const buffered =
|
||||
reasoningStepState.takeBufferedFinalAnswer(replyFenceGeneration);
|
||||
@@ -1227,13 +1282,11 @@ export const dispatchTelegramMessage = async ({
|
||||
| { buttons?: TelegramInlineButtons }
|
||||
| undefined
|
||||
)?.buttons;
|
||||
await deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: buffered.text,
|
||||
payload: buffered.payload,
|
||||
infoKind: "final",
|
||||
buttons: bufferedButtons,
|
||||
});
|
||||
await deliverFinalAnswerText(
|
||||
buffered.payload,
|
||||
buffered.text,
|
||||
bufferedButtons,
|
||||
);
|
||||
reasoningStepState.resetForNextStep();
|
||||
};
|
||||
|
||||
@@ -1254,13 +1307,15 @@ export const dispatchTelegramMessage = async ({
|
||||
if (segment.lane === "reasoning") {
|
||||
reasoningStepState.noteReasoningHint();
|
||||
}
|
||||
if (segment.lane === "answer" && info.kind === "tool") {
|
||||
await prepareAnswerLaneForToolProgress();
|
||||
}
|
||||
const result =
|
||||
streamMode === "progress" &&
|
||||
segment.lane === "answer" &&
|
||||
info.kind === "final"
|
||||
? await deliverProgressModeFinalAnswer(
|
||||
segment.lane === "answer" && info.kind === "final"
|
||||
? await deliverFinalAnswerText(
|
||||
effectivePayload,
|
||||
segment.update.text,
|
||||
telegramButtons,
|
||||
)
|
||||
: await deliverLaneText({
|
||||
laneName: segment.lane,
|
||||
@@ -1315,6 +1370,7 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
|
||||
if (info.kind === "final") {
|
||||
await rotateAnswerLaneAfterToolProgress();
|
||||
await answerLane.stream?.stop();
|
||||
await reasoningLane.stream?.stop();
|
||||
reasoningStepState.resetForNextStep();
|
||||
|
||||
Reference in New Issue
Block a user