fix(feishu): suppress stale replay typing indicators (#30709) (thanks @arkyu2077)

This commit is contained in:
Peter Steinberger
2026-03-02 03:52:56 +00:00
parent 7fbc40f821
commit 02b1958760
3 changed files with 67 additions and 2 deletions

View File

@@ -116,6 +116,59 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("skips typing indicator for stale replayed messages", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Date.now() - 3 * 60_000,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("treats second-based timestamps as stale for typing suppression", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Math.floor((Date.now() - 3 * 60_000) / 1000),
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).not.toHaveBeenCalled();
});
it("keeps typing indicator for fresh messages", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,
agentId: "agent",
runtime: {} as never,
chatId: "oc_chat",
replyToMessageId: "om_parent",
messageCreateTimeMs: Date.now() - 30_000,
});
const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0];
await options.onReplyStart?.();
expect(addTypingIndicatorMock).toHaveBeenCalledTimes(1);
expect(addTypingIndicatorMock).toHaveBeenCalledWith(
expect.objectContaining({
messageId: "om_parent",
}),
);
});
it("keeps auto mode plain text on non-streaming send path", async () => {
createFeishuReplyDispatcher({
cfg: {} as never,

View File

@@ -25,6 +25,16 @@ function shouldUseCard(text: string): boolean {
/** Maximum age (ms) for a message to receive a typing indicator reaction.
* Messages older than this are likely replays after context compaction (#30418). */
const TYPING_INDICATOR_MAX_AGE_MS = 2 * 60_000;
const MS_EPOCH_MIN = 1_000_000_000_000;
function normalizeEpochMs(timestamp: number | undefined): number | undefined {
if (!Number.isFinite(timestamp) || timestamp === undefined || timestamp <= 0) {
return undefined;
}
// Defensive normalization: some payloads use seconds, others milliseconds.
// Values below 1e12 are treated as epoch-seconds.
return timestamp < MS_EPOCH_MIN ? timestamp * 1000 : timestamp;
}
export type CreateFeishuReplyDispatcherParams = {
cfg: ClawdbotConfig;
@@ -72,9 +82,10 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
}
// Skip typing indicator for old messages — likely replays after context
// compaction that would flood users with stale notifications (#30418).
const messageCreateTimeMs = normalizeEpochMs(params.messageCreateTimeMs);
if (
params.messageCreateTimeMs &&
Date.now() - params.messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
messageCreateTimeMs !== undefined &&
Date.now() - messageCreateTimeMs > TYPING_INDICATOR_MAX_AGE_MS
) {
return;
}