diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 7de02cc3941ad..520658182b267 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -1094,16 +1094,21 @@ describe("planComposerTurn", () => { }); }); - it("queues behind a compaction of a stale session, except for /compact itself", () => { + it("queues behind a compaction of a stale session, except for /compact and /clear", () => { const stale = { ...base, resumeCompactionTokens: 120_000 }; - for (const messageText of ["hello", "/compactfoo"]) { + for (const messageText of ["hello", "/compactfoo", "/clearly not a command"]) { expect(planComposerTurn({ ...stale, messageText })).toMatchObject({ compactBeforeSend: true, turnDispatchMode: "queue", shouldQueueBehindActiveRun: true, }); } - for (const messageText of ["/COMPACT", "/compact keep the API notes", "/compact\n tests"]) { + for (const messageText of [ + "/COMPACT", + "/compact keep the API notes", + "/compact\n tests", + "/clear", + ]) { expect(planComposerTurn({ ...stale, messageText })).toMatchObject({ compactBeforeSend: false, turnDispatchMode: "auto", diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index 09a25ee55910d..e108c7f2ce39a 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -4,7 +4,7 @@ import { deriveThreadRuntime, } from "@t3tools/client-runtime/state/thread-execution"; import { checkpointFileTotals } from "@t3tools/client-runtime/state/thread-checkpoints"; -import { COMPACT_COMMAND_PATTERN } from "@t3tools/client-runtime/state/context-window"; +import { NO_COMPACT_BEFORE_COMMAND_PATTERN } from "@t3tools/client-runtime/state/context-window"; import { derivePendingBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; import { latestUnheldRun, @@ -1319,7 +1319,7 @@ export function planComposerTurn(input: { !input.freshContext && input.resumeCompactionTokens !== null && !input.keepFullHistory && - !COMPACT_COMMAND_PATTERN.test(input.messageText); + !NO_COMPACT_BEFORE_COMMAND_PATTERN.test(input.messageText); const dispatchMode = input.freshContext && (input.dispatchMode === "steer" || input.dispatchMode === "restart") ? "queue" diff --git a/packages/client-runtime/src/state/contextWindow.test.ts b/packages/client-runtime/src/state/contextWindow.test.ts index 3f1a4d3d41994..3f7150b812b35 100644 --- a/packages/client-runtime/src/state/contextWindow.test.ts +++ b/packages/client-runtime/src/state/contextWindow.test.ts @@ -256,6 +256,7 @@ describe("shouldCompactBeforeSend", () => { { name: "while the thread is working", threadIdle: false, expected: false }, { name: "with no conversation but /compact", onlyCompactMessages: true, expected: false }, { name: "for a typed /compact", text: "/compact keep the plan", expected: false }, + { name: "for a typed /clear", text: "/clear", expected: false }, ] as const)("$name", ({ name: _name, expected, ...overrides }) => { expect(shouldCompactBeforeSend(sendInput(overrides as Parameters[0]))).toBe( expected, diff --git a/packages/client-runtime/src/state/contextWindow.ts b/packages/client-runtime/src/state/contextWindow.ts index 66c48807465af..0701f91b1c59b 100644 --- a/packages/client-runtime/src/state/contextWindow.ts +++ b/packages/client-runtime/src/state/contextWindow.ts @@ -22,8 +22,9 @@ import type { ThreadStructure, ThreadStructureTurnItem } from "./models.ts"; const CLAUDE_RESUME_COMPACTION_FALLBACK_MS = 70 * 60_000; // A typed `/compact`, with or without instructions, is already the compaction. Queueing a -// bare one ahead of it would run an uninstructed compaction first and charge for two. -export const COMPACT_COMMAND_PATTERN = /^\/compact(?:\s|$)/i; +// bare one ahead of it would run an uninstructed compaction first and charge for two. A typed +// `/clear` discards the old context, so compacting it first pays for a summary nobody reads. +export const NO_COMPACT_BEFORE_COMMAND_PATTERN = /^\/(?:compact|clear)(?:\s|$)/i; function asFiniteNumber(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; @@ -265,7 +266,7 @@ export function shouldCompactBeforeSend(input: { if ( provider === undefined || !input.threadIdle || - COMPACT_COMMAND_PATTERN.test(input.text) || + NO_COMPACT_BEFORE_COMMAND_PATTERN.test(input.text) || resolveIdleCompaction(input.settings, provider.instanceId) === "off" || !provider.slashCommands.some((command) => command.name === "compact") || resolvePendingHandoff(projection, input.selection, input.providers) !== null ||