mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:28:38 +00:00
fix(telegram): clear stale retain before transient final fallback (#41763)
Merged via squash.
Prepared head SHA: c0940838bc
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -101,6 +101,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
- Security/plugin runtime: stop unauthenticated plugin HTTP routes from inheriting synthetic admin gateway scopes when they call `runtime.subagent.*`, so admin-only methods like `sessions.delete` stay blocked without gateway auth.
|
- Security/plugin runtime: stop unauthenticated plugin HTTP routes from inheriting synthetic admin gateway scopes when they call `runtime.subagent.*`, so admin-only methods like `sessions.delete` stay blocked without gateway auth.
|
||||||
- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`.
|
- Security/session_status: enforce sandbox session-tree visibility and shared agent-to-agent access guards before reading or mutating target session state, so sandboxed subagents can no longer inspect parent session metadata or write parent model overrides via `session_status`.
|
||||||
- Security/nodes: treat the `nodes` agent tool as owner-only fallback policy so non-owner senders cannot reach paired-node approval or invoke paths through the shared tool set.
|
- Security/nodes: treat the `nodes` agent tool as owner-only fallback policy so non-owner senders cannot reach paired-node approval or invoke paths through the shared tool set.
|
||||||
|
- Telegram/final preview cleanup follow-up: clear stale cleanup-retain state only for transient preview finals so archived-preview retains no longer leave a stale partial bubble beside a later fallback-sent final. (#41763) Thanks @obviyus.
|
||||||
|
|
||||||
## 2026.3.8
|
## 2026.3.8
|
||||||
|
|
||||||
|
|||||||
@@ -1031,6 +1031,81 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
|||||||
expect(deliverReplies).not.toHaveBeenCalled();
|
expect(deliverReplies).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears the active preview when a later final falls back after archived retain", async () => {
|
||||||
|
let answerMessageId: number | undefined;
|
||||||
|
let answerDraftParams:
|
||||||
|
| {
|
||||||
|
onSupersededPreview?: (preview: { messageId: number; textSnapshot: string }) => void;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
const answerDraftStream = {
|
||||||
|
update: vi.fn().mockImplementation((text: string) => {
|
||||||
|
if (text.includes("Message B")) {
|
||||||
|
answerMessageId = 1002;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
flush: vi.fn().mockResolvedValue(undefined),
|
||||||
|
messageId: vi.fn().mockImplementation(() => answerMessageId),
|
||||||
|
clear: vi.fn().mockResolvedValue(undefined),
|
||||||
|
stop: vi.fn().mockResolvedValue(undefined),
|
||||||
|
forceNewMessage: vi.fn().mockImplementation(() => {
|
||||||
|
answerMessageId = undefined;
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const reasoningDraftStream = createDraftStream();
|
||||||
|
createTelegramDraftStream
|
||||||
|
.mockImplementationOnce((params) => {
|
||||||
|
answerDraftParams = params as typeof answerDraftParams;
|
||||||
|
return answerDraftStream;
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => reasoningDraftStream);
|
||||||
|
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||||
|
async ({ dispatcherOptions, replyOptions }) => {
|
||||||
|
await replyOptions?.onPartialReply?.({ text: "Message A partial" });
|
||||||
|
await replyOptions?.onAssistantMessageStart?.();
|
||||||
|
await replyOptions?.onPartialReply?.({ text: "Message B partial" });
|
||||||
|
answerDraftParams?.onSupersededPreview?.({
|
||||||
|
messageId: 1001,
|
||||||
|
textSnapshot: "Message A partial",
|
||||||
|
});
|
||||||
|
|
||||||
|
await dispatcherOptions.deliver({ text: "Message A final" }, { kind: "final" });
|
||||||
|
await dispatcherOptions.deliver({ text: "Message B final" }, { kind: "final" });
|
||||||
|
return { queuedFinal: true };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
deliverReplies.mockResolvedValue({ delivered: true });
|
||||||
|
const preConnectErr = new Error("connect ECONNREFUSED 149.154.167.220:443");
|
||||||
|
(preConnectErr as NodeJS.ErrnoException).code = "ECONNREFUSED";
|
||||||
|
editMessageTelegram
|
||||||
|
.mockRejectedValueOnce(new Error("400: Bad Request: message to edit not found"))
|
||||||
|
.mockRejectedValueOnce(preConnectErr);
|
||||||
|
|
||||||
|
await dispatchWithContext({ context: createContext(), streamMode: "partial" });
|
||||||
|
|
||||||
|
expect(editMessageTelegram).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
123,
|
||||||
|
1001,
|
||||||
|
"Message A final",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
expect(editMessageTelegram).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
123,
|
||||||
|
1002,
|
||||||
|
"Message B final",
|
||||||
|
expect.any(Object),
|
||||||
|
);
|
||||||
|
const finalTextSentViaDeliverReplies = deliverReplies.mock.calls.some((call: unknown[]) =>
|
||||||
|
(call[0] as { replies?: Array<{ text?: string }> })?.replies?.some(
|
||||||
|
(r: { text?: string }) => r.text === "Message B final",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(finalTextSentViaDeliverReplies).toBe(true);
|
||||||
|
expect(answerDraftStream.clear).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it.each(["partial", "block"] as const)(
|
it.each(["partial", "block"] as const)(
|
||||||
"keeps finalized text preview when the next assistant message is media-only (%s mode)",
|
"keeps finalized text preview when the next assistant message is media-only (%s mode)",
|
||||||
async (streamMode) => {
|
async (streamMode) => {
|
||||||
|
|||||||
@@ -464,6 +464,12 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
|||||||
!hasMedia && text.length > 0 && text.length <= params.draftMaxChars && !payload.isError;
|
!hasMedia && text.length > 0 && text.length <= params.draftMaxChars && !payload.isError;
|
||||||
|
|
||||||
if (infoKind === "final") {
|
if (infoKind === "final") {
|
||||||
|
// Transient previews must decide cleanup retention per final attempt.
|
||||||
|
// Completed previews intentionally stay retained so later extra payloads
|
||||||
|
// do not clear the already-finalized message.
|
||||||
|
if (params.activePreviewLifecycleByLane[laneName] === "transient") {
|
||||||
|
params.retainPreviewOnCleanupByLane[laneName] = false;
|
||||||
|
}
|
||||||
if (laneName === "answer") {
|
if (laneName === "answer") {
|
||||||
const archivedResult = await consumeArchivedAnswerPreviewForFinal({
|
const archivedResult = await consumeArchivedAnswerPreviewForFinal({
|
||||||
lane,
|
lane,
|
||||||
|
|||||||
Reference in New Issue
Block a user