From 1b83337487d47c1368b28c3604a079a2466a9fae Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Fri, 28 Aug 2026 13:46:44 -0600 Subject: [PATCH] fix(clients): ignore stale compaction after session stops --- .../src/state/use-thread-composer-state.ts | 1 + apps/web/src/components/chat/ChatComposer.tsx | 1 + .../src/state/contextCompaction.test.ts | 36 +++++++++++++------ .../src/state/contextCompaction.ts | 8 +++-- 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/mobile/src/state/use-thread-composer-state.ts b/apps/mobile/src/state/use-thread-composer-state.ts index 6413e6603..760416863 100644 --- a/apps/mobile/src/state/use-thread-composer-state.ts +++ b/apps/mobile/src/state/use-thread-composer-state.ts @@ -327,6 +327,7 @@ export function useThreadComposerState() { ? sessionCompactionMutation.scopeKey : null; const sessionCompactionBlocksSubmission = isSessionCompactionSubmissionBlocked({ + hasActiveScope: sessionCompactionScope !== null, current: selectedThreadCompaction, activity: activitySessionCompaction, compactPending: diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 7b517f6a0..37562929c 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -1288,6 +1288,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) ? authoritativeSessionCompaction.snapshot : activitySessionCompaction; const sessionCompactionBlocksSubmission = isSessionCompactionSubmissionBlocked({ + hasActiveScope: sessionCompactionScopeKey !== null, current: sessionCompaction, activity: activitySessionCompaction, compactPending: diff --git a/packages/client-runtime/src/state/contextCompaction.test.ts b/packages/client-runtime/src/state/contextCompaction.test.ts index 11f11db77..2e03d78ea 100644 --- a/packages/client-runtime/src/state/contextCompaction.test.ts +++ b/packages/client-runtime/src/state/contextCompaction.test.ts @@ -133,21 +133,23 @@ describe("session compaction state", () => { }); it("blocks composer submission for every active compaction status", () => { - expect(isSessionCompactionInProgress({ status: "starting" })).toBe(true); - expect(isSessionCompactionInProgress({ status: "compacting" })).toBe(true); - expect(isSessionCompactionInProgress({ status: "abort-requested" })).toBe(true); + for (const status of ["starting", "compacting", "abort-requested"] as const) { + expect(isSessionCompactionInProgress({ status })).toBe(true); + expect( + isSessionCompactionSubmissionBlocked({ + hasActiveScope: true, + current: { status: "idle" }, + activity: { status }, + compactPending: false, + }), + ).toBe(true); + } expect(isSessionCompactionInProgress({ status: "idle" })).toBe(false); expect(isSessionCompactionInProgress(null)).toBe(false); expect( isSessionCompactionSubmissionBlocked({ - current: { status: "idle" }, - activity: { status: "starting" }, - compactPending: false, - }), - ).toBe(true); - expect( - isSessionCompactionSubmissionBlocked({ + hasActiveScope: true, current: { status: "idle" }, activity: { status: "idle" }, compactPending: true, @@ -155,6 +157,7 @@ describe("session compaction state", () => { ).toBe(true); expect( isSessionCompactionSubmissionBlocked({ + hasActiveScope: true, current: { status: "idle" }, activity: { status: "idle" }, compactPending: false, @@ -162,6 +165,19 @@ describe("session compaction state", () => { ).toBe(false); }); + it("ignores stale compaction state after the session stops", () => { + for (const compactPending of [false, true]) { + expect( + isSessionCompactionSubmissionBlocked({ + hasActiveScope: false, + current: { status: "compacting" }, + activity: { status: "compacting" }, + compactPending, + }), + ).toBe(false); + } + }); + it("accepts only successful mutations superseded by authoritative activity", () => { expect( isAcceptedSessionCompactionMutationResult({ diff --git a/packages/client-runtime/src/state/contextCompaction.ts b/packages/client-runtime/src/state/contextCompaction.ts index 752637dc4..0350d5106 100644 --- a/packages/client-runtime/src/state/contextCompaction.ts +++ b/packages/client-runtime/src/state/contextCompaction.ts @@ -101,13 +101,15 @@ export const isSessionCompactionInProgress = ( snapshot?.status === "abort-requested"; export const isSessionCompactionSubmissionBlocked = (input: { + readonly hasActiveScope: boolean; readonly current: Pick | null | undefined; readonly activity: Pick | null | undefined; readonly compactPending: boolean; }): boolean => - input.compactPending || - isSessionCompactionInProgress(input.activity) || - isSessionCompactionInProgress(input.current); + input.hasActiveScope && + (input.compactPending || + isSessionCompactionInProgress(input.activity) || + isSessionCompactionInProgress(input.current)); export const canStartSessionCompaction = ( provider: Pick | null | undefined,