From 96303d2d19f7d8bae9b71bf28db608e53079c23b Mon Sep 17 00:00:00 2001 From: Cypherm <28184436+Cypherm@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:33:09 +0800 Subject: [PATCH] feat: show status reaction during context compaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When context auto-compaction runs, the bot appears frozen for 10-30s with no feedback. This adds a ✍ (writing) status reaction during compaction so users know the bot is still working. - Add `compacting` state to StatusReactionController with ✍ emoji - Add `onCompactionStart`/`onCompactionEnd` callbacks to GetReplyOptions - Wire existing compaction events to the status reaction system - Apply to both Telegram and Discord channels Co-Authored-By: Claude Opus 4.6 --- src/auto-reply/reply/agent-runner-execution.ts | 6 +++++- src/auto-reply/types.ts | 4 ++++ src/channels/status-reactions.ts | 9 +++++++++ src/discord/monitor/message-handler.process.ts | 12 ++++++++++++ src/telegram/bot-message-dispatch.ts | 6 ++++++ src/telegram/status-reaction-variants.ts | 3 +++ 6 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/auto-reply/reply/agent-runner-execution.ts b/src/auto-reply/reply/agent-runner-execution.ts index bdbd68ac2e4..ff3838a1936 100644 --- a/src/auto-reply/reply/agent-runner-execution.ts +++ b/src/auto-reply/reply/agent-runner-execution.ts @@ -393,11 +393,15 @@ export async function runAgentTurnWithFallback(params: { await params.opts?.onToolStart?.({ name, phase }); } } - // Track auto-compaction completion + // Track auto-compaction completion and notify UI layer if (evt.stream === "compaction") { const phase = typeof evt.data.phase === "string" ? evt.data.phase : ""; + if (phase === "start") { + await params.opts?.onCompactionStart?.(); + } if (phase === "end") { autoCompactionCompleted = true; + await params.opts?.onCompactionEnd?.(); } } }, diff --git a/src/auto-reply/types.ts b/src/auto-reply/types.ts index 4692d442ea5..be32e3635e1 100644 --- a/src/auto-reply/types.ts +++ b/src/auto-reply/types.ts @@ -54,6 +54,10 @@ export type GetReplyOptions = { onToolResult?: (payload: ReplyPayload) => Promise | void; /** Called when a tool phase starts/updates, before summary payloads are emitted. */ onToolStart?: (payload: { name?: string; phase?: string }) => Promise | void; + /** Called when context auto-compaction starts (allows UX feedback during the pause). */ + onCompactionStart?: () => Promise | void; + /** Called when context auto-compaction completes. */ + onCompactionEnd?: () => Promise | void; /** Called when the actual model is selected (including after fallback). * Use this to get model/provider/thinkLevel for responsePrefix template interpolation. */ onModelSelected?: (ctx: ModelSelectedContext) => void; diff --git a/src/channels/status-reactions.ts b/src/channels/status-reactions.ts index 4b0651232c8..4dd29a36c7c 100644 --- a/src/channels/status-reactions.ts +++ b/src/channels/status-reactions.ts @@ -24,6 +24,7 @@ export type StatusReactionEmojis = { error?: string; // Default: "❌" stallSoft?: string; // Default: "⏳" stallHard?: string; // Default: "⚠️" + compacting?: string; // Default: "✍" }; export type StatusReactionTiming = { @@ -38,6 +39,7 @@ export type StatusReactionController = { setQueued: () => Promise | void; setThinking: () => Promise | void; setTool: (toolName?: string) => Promise | void; + setCompacting: () => Promise | void; setDone: () => Promise; setError: () => Promise; clear: () => Promise; @@ -58,6 +60,7 @@ export const DEFAULT_EMOJIS: Required = { error: "😱", stallSoft: "🥱", stallHard: "😨", + compacting: "✍", }; export const DEFAULT_TIMING: Required = { @@ -162,6 +165,7 @@ export function createStatusReactionController(params: { emojis.error, emojis.stallSoft, emojis.stallHard, + emojis.compacting, ]); /** @@ -306,6 +310,10 @@ export function createStatusReactionController(params: { scheduleEmoji(emoji); } + function setCompacting(): void { + scheduleEmoji(emojis.compacting); + } + function finishWithEmoji(emoji: string): Promise { if (!enabled) { return Promise.resolve(); @@ -375,6 +383,7 @@ export function createStatusReactionController(params: { setQueued, setThinking, setTool, + setCompacting, setDone, setError, clear, diff --git a/src/discord/monitor/message-handler.process.ts b/src/discord/monitor/message-handler.process.ts index ea64b37f98e..8aaa2c5d764 100644 --- a/src/discord/monitor/message-handler.process.ts +++ b/src/discord/monitor/message-handler.process.ts @@ -769,6 +769,18 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext) } await statusReactions.setTool(payload.name); }, + onCompactionStart: async () => { + if (isProcessAborted(abortSignal)) { + return; + } + await statusReactions.setCompacting(); + }, + onCompactionEnd: async () => { + if (isProcessAborted(abortSignal)) { + return; + } + await statusReactions.setThinking(); + }, }, }); if (isProcessAborted(abortSignal)) { diff --git a/src/telegram/bot-message-dispatch.ts b/src/telegram/bot-message-dispatch.ts index 4d8d2b678e8..4b2fce02b95 100644 --- a/src/telegram/bot-message-dispatch.ts +++ b/src/telegram/bot-message-dispatch.ts @@ -713,6 +713,12 @@ export const dispatchTelegramMessage = async ({ await statusReactionController.setTool(payload.name); } : undefined, + onCompactionStart: statusReactionController + ? () => statusReactionController.setCompacting() + : undefined, + onCompactionEnd: statusReactionController + ? () => statusReactionController.setThinking() + : undefined, onModelSelected, }, })); diff --git a/src/telegram/status-reaction-variants.ts b/src/telegram/status-reaction-variants.ts index 5f79b1cbadb..9ce3d033eb0 100644 --- a/src/telegram/status-reaction-variants.ts +++ b/src/telegram/status-reaction-variants.ts @@ -90,6 +90,7 @@ export const TELEGRAM_STATUS_REACTION_VARIANTS: Record