Compare commits
3 Commits
main
...
fix/telegr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28a5b16bbe | ||
|
|
48f2924d30 | ||
|
|
4f7bd48458 |
@ -188,6 +188,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/failover: detect Amazon Bedrock `Too many tokens per day` quota errors as rate limits across fallback, cron retry, and memory embeddings while keeping context-window `too many tokens per request` errors out of the rate-limit lane. (#39377) Thanks @gambletan.
|
||||
- Mattermost replies: keep `root_id` pinned to the existing thread root when an agent replies inside a thread, while still using reply-target threading for top-level posts. (#27744) thanks @hnykda.
|
||||
- Telegram/DM partial streaming: keep DM preview lanes on real message edits instead of native draft materialization so final replies no longer flash a second duplicate copy before collapsing back to one.
|
||||
- Telegram/DM fallback preview cleanup: delete orphaned answer previews after fallback final sends and clear the consumed preview state so later assistant-message boundaries do not archive deleted preview ids or force duplicate fallback sends. (#39455) Thanks @eveiljuice.
|
||||
- macOS overlays: fix VoiceWake, Talk, and Notify overlay exclusivity crashes by removing shared `inout` visibility mutation from `OverlayPanelFactory.present`, and add a repeated Talk overlay smoke test. (#39275, #39321) Thanks @fellanH.
|
||||
- macOS Talk Mode: set the speech recognition request `taskHint` to `.dictation` for mic capture, and add regression coverage for the request defaults. (#38445) Thanks @dmiv.
|
||||
- macOS release packaging: default `scripts/package-mac-app.sh` to universal binaries for `BUILD_CONFIG=release`, and clarify that `scripts/package-mac-dist.sh` already produces the release zip + DMG. (#33891) Thanks @cgdusek.
|
||||
|
||||
@ -171,6 +171,13 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
||||
params.activePreviewLifecycleByLane[laneName] = "complete";
|
||||
params.retainPreviewOnCleanupByLane[laneName] = true;
|
||||
};
|
||||
const clearActivePreviewState = (laneName: LaneName, lane: DraftLaneState) => {
|
||||
lane.stream?.forceNewMessage?.();
|
||||
lane.lastPartialText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
params.activePreviewLifecycleByLane[laneName] = "transient";
|
||||
params.retainPreviewOnCleanupByLane[laneName] = false;
|
||||
};
|
||||
const isDraftPreviewLane = (lane: DraftLaneState) => lane.stream?.previewMode?.() === "draft";
|
||||
const canMaterializeDraftFinal = (
|
||||
lane: DraftLaneState,
|
||||
@ -529,8 +536,26 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
||||
`telegram: preview final too long for edit (${text.length} > ${params.draftMaxChars}); falling back to standard send`,
|
||||
);
|
||||
}
|
||||
const shouldCleanupTransientPreview =
|
||||
params.activePreviewLifecycleByLane[laneName] === "transient";
|
||||
const previewMessageIdBeforeFallback = lane.stream?.messageId();
|
||||
await params.stopDraftLane(lane);
|
||||
const previewMessageIdAfterStop = previewMessageIdBeforeFallback ?? lane.stream?.messageId();
|
||||
const delivered = await params.sendPayload(params.applyTextToPayload(payload, text));
|
||||
if (
|
||||
delivered &&
|
||||
shouldCleanupTransientPreview &&
|
||||
typeof previewMessageIdAfterStop === "number"
|
||||
) {
|
||||
try {
|
||||
await params.deletePreviewMessage(previewMessageIdAfterStop);
|
||||
clearActivePreviewState(laneName, lane);
|
||||
} catch (err) {
|
||||
params.log(
|
||||
`telegram: ${laneName} fallback send orphaned preview cleanup failed (${previewMessageIdAfterStop}): ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return delivered ? "sent" : "skipped";
|
||||
}
|
||||
|
||||
|
||||
@ -42,8 +42,8 @@ function createHarness(params?: {
|
||||
const deletePreviewMessage = vi.fn().mockResolvedValue(undefined);
|
||||
const log = vi.fn();
|
||||
const markDelivered = vi.fn();
|
||||
const activePreviewLifecycleByLane = { answer: "transient", reasoning: "transient" } as const;
|
||||
const retainPreviewOnCleanupByLane = { answer: false, reasoning: false } as const;
|
||||
const activePreviewLifecycleByLane = { answer: "transient", reasoning: "transient" };
|
||||
const retainPreviewOnCleanupByLane = { answer: false, reasoning: false };
|
||||
const archivedAnswerPreviews: Array<{
|
||||
messageId: number;
|
||||
textSnapshot: string;
|
||||
@ -53,8 +53,8 @@ function createHarness(params?: {
|
||||
const deliverLaneText = createLaneTextDeliverer({
|
||||
lanes,
|
||||
archivedAnswerPreviews,
|
||||
activePreviewLifecycleByLane: { ...activePreviewLifecycleByLane },
|
||||
retainPreviewOnCleanupByLane: { ...retainPreviewOnCleanupByLane },
|
||||
activePreviewLifecycleByLane,
|
||||
retainPreviewOnCleanupByLane,
|
||||
draftMaxChars: params?.draftMaxChars ?? 4_096,
|
||||
applyTextToPayload: (payload: ReplyPayload, text: string) => ({ ...payload, text }),
|
||||
sendPayload,
|
||||
@ -81,6 +81,8 @@ function createHarness(params?: {
|
||||
log,
|
||||
markDelivered,
|
||||
archivedAnswerPreviews,
|
||||
activePreviewLifecycleByLane,
|
||||
retainPreviewOnCleanupByLane,
|
||||
};
|
||||
}
|
||||
|
||||
@ -434,7 +436,6 @@ describe("createLaneTextDeliverer", () => {
|
||||
|
||||
it("retains preview on ambiguous API error during final", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
// Plain Error with no error_code → ambiguous, prefer incomplete over duplicate
|
||||
harness.editPreview.mockRejectedValue(new Error("500: Internal Server Error"));
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
@ -473,6 +474,78 @@ describe("createLaneTextDeliverer", () => {
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
});
|
||||
|
||||
it("clears active preview state after successful fallback orphan cleanup", async () => {
|
||||
const harness = createHarness({
|
||||
answerMessageId: 5555,
|
||||
answerHasStreamedMessage: true,
|
||||
answerLastPartialText: "Partial streaming...",
|
||||
});
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Final with media",
|
||||
payload: { text: "Final with media", mediaUrl: "file:///tmp/example.png" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(5555);
|
||||
expect(harness.answer.stream?.forceNewMessage).toHaveBeenCalledTimes(1);
|
||||
expect(harness.lanes.answer.hasStreamedMessage).toBe(false);
|
||||
expect(harness.lanes.answer.lastPartialText).toBe("");
|
||||
expect(harness.activePreviewLifecycleByLane.answer).toBe("transient");
|
||||
expect(harness.retainPreviewOnCleanupByLane.answer).toBe(false);
|
||||
});
|
||||
|
||||
it("does not delete orphaned preview when fallback send fails", async () => {
|
||||
const harness = createHarness({ answerMessageId: 555 });
|
||||
harness.sendPayload.mockResolvedValue(false);
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Final with image",
|
||||
payload: { text: "Final with image", mediaUrl: "file:///tmp/photo.png" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("skipped");
|
||||
expect(harness.sendPayload).toHaveBeenCalledTimes(1);
|
||||
expect(harness.deletePreviewMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs error but does not throw when orphaned preview cleanup fails", async () => {
|
||||
const harness = createHarness({ answerMessageId: 555 });
|
||||
harness.deletePreviewMessage.mockRejectedValue(new Error("404: message not found"));
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Final with image",
|
||||
payload: { text: "Final with image", mediaUrl: "file:///tmp/photo.png" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(555);
|
||||
expect(harness.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("orphaned preview cleanup failed"),
|
||||
);
|
||||
});
|
||||
|
||||
it("deletes orphaned preview created by stop() during fallback", async () => {
|
||||
const harness = createHarness({ answerMessageIdAfterStop: 888 });
|
||||
|
||||
const result = await harness.deliverLaneText({
|
||||
laneName: "answer",
|
||||
text: "Final with image",
|
||||
payload: { text: "Final with image", mediaUrl: "file:///tmp/photo.png" },
|
||||
infoKind: "final",
|
||||
});
|
||||
|
||||
expect(result).toBe("sent");
|
||||
expect(harness.sendPayload).toHaveBeenCalledTimes(1);
|
||||
expect(harness.deletePreviewMessage).toHaveBeenCalledWith(888);
|
||||
});
|
||||
|
||||
it("keeps the active preview when an archived final edit target is missing", async () => {
|
||||
const harness = createHarness({ answerMessageId: 999 });
|
||||
harness.archivedAnswerPreviews.push({
|
||||
@ -557,14 +630,12 @@ describe("createLaneTextDeliverer", () => {
|
||||
});
|
||||
|
||||
it("retains when sendMayHaveLanded is true and a prior preview was visible", async () => {
|
||||
// Stream has a messageId (visible preview) but loses it after stop
|
||||
const stream = createTestDraftStream({ messageId: 999 });
|
||||
stream.sendMayHaveLanded.mockReturnValue(true);
|
||||
const harness = createHarness({
|
||||
answerStream: stream,
|
||||
answerHasStreamedMessage: true,
|
||||
});
|
||||
// Simulate messageId lost after stop (e.g. forceNewMessage or timeout)
|
||||
harness.stopDraftLane.mockImplementation(async (lane: DraftLaneState) => {
|
||||
stream.setMessageId(undefined);
|
||||
await lane.stream?.stop();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user