fix(preemptive-compaction): notify user on failure and reduce timeout

Reports from david_66 on Discord: sessions get perceived as 'stuck' when
context usage crosses the 78% threshold. Investigation confirmed two
issues in the preemptive compaction hook:

1. PREEMPTIVE_COMPACTION_TIMEOUT_MS was 120s. While the summarize
   request is in flight, tool.execute.after short-circuits via the
   compactionInProgress guard. A hung summarize blocked the session
   for two full minutes before giving up, which users reasonably
   experience as a hang.

2. On failure (timeout or exception) only a log line was emitted. The
   user had no visibility into why their session was unresponsive or
   why auto-compaction never ran, so a transient upstream error could
   silently leave them well above the threshold with no signal.

Fix:

- Reduce timeout 120s -> 60s. Still gives the upstream a generous
  window, but caps the worst-case perceived hang at one minute.
- Show a warning toast via ctx.client.tui.showToast whenever the
  catch block fires, including the underlying error string so users
  can act (retry, manual /compact, or adjust provider).
- Include providerID/modelID in the Compaction failed log entry so
  wild failures are easier to correlate to a specific target model.

Two existing failure-path assertions were updated to match the new
log shape and a new test covers the toast notification contract.

Discord report: https://discord.com/channels/1452487457085063218/1490536332961906829/1491345441399505037
This commit is contained in:
YeonGyu-Kim
2026-04-08 20:10:25 +09:00
parent 686f903d1f
commit 1ea0ee4319
2 changed files with 69 additions and 2 deletions
+49
View File
@@ -284,10 +284,57 @@ describe("preemptive-compaction", () => {
//#then
expect(logMock).toHaveBeenCalledWith("[preemptive-compaction] Compaction failed", {
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
error: String(summarizeError),
})
})
// #given compaction fails
// #when tool.execute.after completes the catch block
// #then should show a warning toast explaining the failure to the user
it("should show a warning toast when preemptive compaction fails", async () => {
//#given
const hook = createPreemptiveCompactionHook(ctx as never, {} as never)
const sessionID = "ses_toast_on_failure"
const summarizeError = new Error("upstream rate limited")
ctx.client.session.summarize.mockRejectedValueOnce(summarizeError)
await hook.event({
event: {
type: "message.updated",
properties: {
info: {
role: "assistant",
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
finish: true,
tokens: {
input: 170000,
output: 0,
reasoning: 0,
cache: { read: 10000, write: 0 },
},
},
},
},
})
//#when
await hook["tool.execute.after"](
{ tool: "bash", sessionID, callID: "call_toast" },
{ title: "", output: "test", metadata: null },
)
//#then
expect(ctx.client.tui.showToast).toHaveBeenCalledTimes(1)
const toastCall = ctx.client.tui.showToast.mock.calls[0]?.[0]
expect(toastCall?.body?.title).toBe("Preemptive compaction failed")
expect(toastCall?.body?.variant).toBe("warning")
expect(String(toastCall?.body?.message)).toContain("upstream rate limited")
})
// #given compaction fails
// #when tool.execute.after is called again immediately
// #then should NOT retry due to cooldown
@@ -475,6 +522,8 @@ describe("preemptive-compaction", () => {
expect(ctx.client.session.summarize).toHaveBeenCalledTimes(1)
expect(logMock).toHaveBeenCalledWith("[preemptive-compaction] Compaction failed", {
sessionID,
providerID: "anthropic",
modelID: "claude-sonnet-4-6",
error: expect.stringContaining("Compaction summarize timed out"),
})
+20 -2
View File
@@ -8,7 +8,7 @@ import {
import { resolveCompactionModel } from "./shared/compaction-model-resolver"
import { createPostCompactionDegradationMonitor } from "./preemptive-compaction-degradation-monitor"
const PREEMPTIVE_COMPACTION_TIMEOUT_MS = 120_000
const PREEMPTIVE_COMPACTION_TIMEOUT_MS = 60_000
const PREEMPTIVE_COMPACTION_THRESHOLD = 0.78
const PREEMPTIVE_COMPACTION_COOLDOWN_MS = 60_000
@@ -134,7 +134,25 @@ export function createPreemptiveCompactionHook(
compactedSessions.add(sessionID)
} catch (error) {
log("[preemptive-compaction] Compaction failed", { sessionID, error: String(error) })
log("[preemptive-compaction] Compaction failed", {
sessionID,
providerID: cached.providerID,
modelID: cached.modelID,
error: String(error),
})
ctx.client.tui.showToast({
body: {
title: "Preemptive compaction failed",
message: `Context window is above ${Math.round(PREEMPTIVE_COMPACTION_THRESHOLD * 100)}% and auto-compaction could not run. The session may grow large. Error: ${String(error)}`,
variant: "warning",
duration: 10000,
},
}).catch((toastError: unknown) => {
log("[preemptive-compaction] Failed to show toast", {
sessionID,
toastError: String(toastError),
})
})
} finally {
compactionInProgress.delete(sessionID)
}