mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-05 15:03:42 +00:00
fix(telegram): keep the streaming preview on screen >=4s before deleting
The streaming "gerund" progress box was deleteMessage'd immediately at teardown, so on fast turns it flashed and vanished before it could be read, and the delete could race a just-persisted message (intermittently dropping the first verbose commentary). Add MIN_PREVIEW_DWELL_MS (4000ms) and schedule the delete DETACHED via setTimeout for max(0, 4000 - timeVisible), measured from when the box first appeared. The delete never awaits, so turn teardown is never stalled. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Ayaan Zaidi
parent
f5f3a2a571
commit
b95fe7f00f
@@ -313,18 +313,27 @@ describe("createTelegramDraftStream", () => {
|
||||
});
|
||||
|
||||
it("deletes message preview on clear after finalization", async () => {
|
||||
const api = createMockDraftApi();
|
||||
const stream = createThreadedDraftStream(api, { id: 42, scope: "dm" });
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const api = createMockDraftApi();
|
||||
const stream = createThreadedDraftStream(api, { id: 42, scope: "dm" });
|
||||
|
||||
stream.update("Hello");
|
||||
await stream.flush();
|
||||
stream.update("Hello again");
|
||||
await stream.stop();
|
||||
await stream.clear();
|
||||
stream.update("Hello");
|
||||
await stream.flush();
|
||||
stream.update("Hello again");
|
||||
await stream.stop();
|
||||
await stream.clear();
|
||||
|
||||
expectPreviewSend(api, "Hello", { message_thread_id: 42 });
|
||||
expectPreviewEdit(api, "Hello again");
|
||||
expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);
|
||||
expectPreviewSend(api, "Hello", { message_thread_id: 42 });
|
||||
expectPreviewEdit(api, "Hello again");
|
||||
// The delete is deferred until the preview has been on screen for the
|
||||
// dwell window; advance past it to trigger the detached cleanup.
|
||||
expect(api.deleteMessage).not.toHaveBeenCalled();
|
||||
await vi.advanceTimersByTimeAsync(4_000);
|
||||
expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("creates new message after forceNewMessage is called", async () => {
|
||||
@@ -351,20 +360,50 @@ describe("createTelegramDraftStream", () => {
|
||||
});
|
||||
|
||||
it("creates new message after cleanup and forceNewMessage", async () => {
|
||||
const { api, stream } = createForceNewMessageHarness();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const { api, stream } = createForceNewMessageHarness();
|
||||
|
||||
stream.update("Stale preview");
|
||||
await stream.flush();
|
||||
stream.update("Stale preview");
|
||||
await stream.flush();
|
||||
|
||||
await stream.clear();
|
||||
expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);
|
||||
await stream.clear();
|
||||
// Delete is deferred past the dwell window; advance to trigger it.
|
||||
await vi.advanceTimersByTimeAsync(4_000);
|
||||
expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);
|
||||
|
||||
stream.forceNewMessage();
|
||||
stream.update("Next preview");
|
||||
await stream.flush();
|
||||
stream.forceNewMessage();
|
||||
stream.update("Next preview");
|
||||
await stream.flush();
|
||||
|
||||
expect(api.sendMessage).toHaveBeenCalledTimes(2);
|
||||
expectNthPreviewSend(api, 2, "Next preview");
|
||||
expect(api.sendMessage).toHaveBeenCalledTimes(2);
|
||||
expectNthPreviewSend(api, 2, "Next preview");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the streaming preview on screen for the dwell window before deleting", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const api = createMockDraftApi();
|
||||
const stream = createDraftStream(api);
|
||||
|
||||
stream.update("Working");
|
||||
await stream.flush();
|
||||
// Fast turn: the preview has only been visible ~1s when the turn tears down.
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
await stream.clear();
|
||||
|
||||
// Delete is deferred, not synchronous, and does not fire before the 4s dwell.
|
||||
await vi.advanceTimersByTimeAsync(2_000);
|
||||
expect(api.deleteMessage).not.toHaveBeenCalled();
|
||||
// At the dwell boundary (~4s after first appearing) the detached delete runs.
|
||||
await vi.advanceTimersByTimeAsync(1_000);
|
||||
expect(api.deleteMessage).toHaveBeenCalledWith(123, 17);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("sends first update immediately after forceNewMessage within throttle window", async () => {
|
||||
|
||||
@@ -37,6 +37,13 @@ const MAX_CONSECUTIVE_PREVIEW_FAILURES = 3;
|
||||
// Flood waits beyond this freeze the preview longer than it is useful; clamp so
|
||||
// a large retry_after cannot park the suspension past the run's lifetime.
|
||||
const MAX_PREVIEW_FLOOD_SUSPEND_MS = 60_000;
|
||||
// Minimum time the streaming preview ("gerund" box) stays on screen before it
|
||||
// is deleted at teardown, measured from when it first became visible. On fast
|
||||
// turns the box otherwise flashed and vanished before it could be read, and the
|
||||
// immediate delete could race a just-persisted message (intermittently dropping
|
||||
// the first verbose commentary). The delete is scheduled DETACHED so the turn is
|
||||
// never stalled waiting on the dwell.
|
||||
const MIN_PREVIEW_DWELL_MS = 4_000;
|
||||
|
||||
export type TelegramDraftStream = {
|
||||
update: (text: string) => void;
|
||||
@@ -533,6 +540,8 @@ export function createTelegramDraftStream(params: {
|
||||
};
|
||||
|
||||
const clear = async () => {
|
||||
// Capture before the stop; takeMessageIdAfterStop resets streamVisibleSinceMs.
|
||||
const visibleSince = streamVisibleSinceMs;
|
||||
const messageId = await takeMessageIdAfterStop({
|
||||
stopForClear,
|
||||
readMessageId: () => streamMessageId,
|
||||
@@ -541,11 +550,26 @@ export function createTelegramDraftStream(params: {
|
||||
},
|
||||
});
|
||||
if (typeof messageId === "number" && Number.isFinite(messageId)) {
|
||||
try {
|
||||
await params.api.deleteMessage(chatId, messageId);
|
||||
params.log?.(`telegram stream preview deleted (chat=${chatId}, message=${messageId})`);
|
||||
} catch (err) {
|
||||
params.warn?.(`telegram stream preview cleanup failed: ${formatErrorMessage(err)}`);
|
||||
const runDelete = async () => {
|
||||
try {
|
||||
await params.api.deleteMessage(chatId, messageId);
|
||||
params.log?.(`telegram stream preview deleted (chat=${chatId}, message=${messageId})`);
|
||||
} catch (err) {
|
||||
params.warn?.(`telegram stream preview cleanup failed: ${formatErrorMessage(err)}`);
|
||||
}
|
||||
};
|
||||
// Keep the preview on screen for at least MIN_PREVIEW_DWELL_MS from when it
|
||||
// first appeared, then delete DETACHED (scheduled, not awaited) so teardown
|
||||
// is never stalled waiting for the dwell.
|
||||
const elapsedMs =
|
||||
typeof visibleSince === "number" ? Date.now() - visibleSince : MIN_PREVIEW_DWELL_MS;
|
||||
const remainingMs = Math.max(0, MIN_PREVIEW_DWELL_MS - elapsedMs);
|
||||
if (remainingMs <= 0) {
|
||||
void runDelete();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
void runDelete();
|
||||
}, remainingMs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user