From b4225ce9f12b66bb60a744aa484e33b715d41340 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 20:37:13 -0500 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=A4=96=20Harden=20auto-continue=20ide?= =?UTF-8?q?mpotency:=20use=20compaction=20requestId=20when=20available?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add requestId to compaction-result metadata (source compaction-request user msg) - WorkspaceStore: dedupe performCompaction by compaction-request id - Hook: guard on requestId (fallback to summary message id) - Fix retry cleanup to remove the right id from the processed set Generated with cmux --- src/hooks/useAutoCompactContinue.ts | 23 ++++++++++++----------- src/stores/WorkspaceStore.ts | 19 ++++++++++++++++++- src/types/message.ts | 1 + 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/hooks/useAutoCompactContinue.ts b/src/hooks/useAutoCompactContinue.ts index 6a857d130..2892be429 100644 --- a/src/hooks/useAutoCompactContinue.ts +++ b/src/hooks/useAutoCompactContinue.ts @@ -59,22 +59,23 @@ export function useAutoCompactContinue() { // After compaction, history is replaced with a single summary message // The summary message has compaction-result metadata with the continueMessage const summaryMessage = state.cmuxMessages[0]; // Single compacted message - const messageId = summaryMessage.id; - - // Have we already processed this specific compaction message? - // This check is race-safe because message IDs are unique and immutable. - if (processedMessageIds.current.has(messageId)) continue; - const cmuxMeta = summaryMessage?.metadata?.cmuxMetadata; const continueMessage = cmuxMeta?.type === "compaction-result" ? cmuxMeta.continueMessage : undefined; if (!continueMessage) continue; - // Mark THIS MESSAGE as processed before sending - // Multiple concurrent checkAutoCompact() calls will all see the same message ID, - // so only the first call that reaches this point will proceed - processedMessageIds.current.add(messageId); + // Prefer compaction-request ID for idempotency; fall back to summary message ID + const idForGuard = + cmuxMeta?.type === "compaction-result" && cmuxMeta.requestId + ? `req:${cmuxMeta.requestId}` + : `msg:${summaryMessage.id}`; + + // Have we already processed this specific compaction result? + if (processedMessageIds.current.has(idForGuard)) continue; + + // Mark THIS RESULT as processed before sending to prevent duplicates + processedMessageIds.current.add(idForGuard); console.log( `[useAutoCompactContinue] Sending continue message for ${workspaceId}:`, @@ -86,7 +87,7 @@ export function useAutoCompactContinue() { window.api.workspace.sendMessage(workspaceId, continueMessage, options).catch((error) => { console.error("Failed to send continue message:", error); // If sending failed, remove from processed set to allow retry - processedMessageIds.current.delete(messageId); + processedMessageIds.current.delete(idForGuard); }); } }; diff --git a/src/stores/WorkspaceStore.ts b/src/stores/WorkspaceStore.ts index be1f0218f..afc265210 100644 --- a/src/stores/WorkspaceStore.ts +++ b/src/stores/WorkspaceStore.ts @@ -417,6 +417,9 @@ export class WorkspaceStore { * Handle compact_summary tool completion. * Returns true if compaction was handled (caller should early return). */ + // Track processed compaction-request IDs to dedupe performCompaction across duplicated events + private processedCompactionRequestIds = new Set(); + private handleCompactionCompletion( workspaceId: string, aggregator: StreamingMessageAggregator, @@ -430,6 +433,17 @@ export class WorkspaceStore { return false; } + // Extract the compaction-request message to identify this compaction run + const compactionRequestMsg = findCompactionRequestMessage(aggregator); + if (!compactionRequestMsg) { + return false; + } + + // Dedupe: If we've already processed this compaction-request, skip re-running + if (this.processedCompactionRequestIds.has(compactionRequestMsg.id)) { + return true; // Already handled compaction for this request + } + // Extract the summary text from the assistant's response const summary = aggregator.getCompactionSummary(data.messageId); if (!summary) { @@ -437,6 +451,9 @@ export class WorkspaceStore { return false; } + // Mark this compaction-request as processed before performing compaction + this.processedCompactionRequestIds.add(compactionRequestMsg.id); + this.performCompaction(workspaceId, aggregator, data, summary); return true; } @@ -544,7 +561,7 @@ export class WorkspaceStore { : undefined, // Store continueMessage in summary so it survives history replacement cmuxMetadata: continueMessage - ? { type: "compaction-result", continueMessage } + ? { type: "compaction-result", continueMessage, requestId: compactRequestMsg?.id } : { type: "normal" }, } ); diff --git a/src/types/message.ts b/src/types/message.ts index a307766a4..4da548409 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -21,6 +21,7 @@ export type CmuxFrontendMetadata = | { type: "compaction-result"; continueMessage: string; // Message to send after compaction completes + requestId?: string; // ID of the compaction-request user message that produced this summary (for idempotency) } | { type: "normal"; // Regular messages From 5b22f477f94f4de9ce4c72d9dac3939b53799104 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 21:05:35 -0500 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=A4=96=20Simplify=20auto-continue=20h?= =?UTF-8?q?ook:=20remove=20unused=20ref=20and=20assignments\n\nNo=20behavi?= =?UTF-8?q?or=20change.=20The=20workspaceStatesRef=20was=20written=20but?= =?UTF-8?q?=20never=20read.\nThis=20reduces=20cognitive=20load=20and=20avo?= =?UTF-8?q?ids=20confusion=20during=20future=20edits.\n\nGenerated=20with?= =?UTF-8?q?=20cmux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useAutoCompactContinue.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/hooks/useAutoCompactContinue.ts b/src/hooks/useAutoCompactContinue.ts index 2892be429..c9c4f33ce 100644 --- a/src/hooks/useAutoCompactContinue.ts +++ b/src/hooks/useAutoCompactContinue.ts @@ -28,8 +28,6 @@ export function useAutoCompactContinue() { // re-rendering AppInner on every workspace state change. This hook only needs // to react when messages change to a single compacted message state. const store = useWorkspaceStoreRaw(); - const workspaceStatesRef = useRef>(new Map()); - // Track which specific compaction summary messages we've already processed. // Key insight: Each compaction creates a unique message. Track by message ID, // not workspace ID, to prevent processing the same compaction result multiple times. @@ -39,7 +37,6 @@ export function useAutoCompactContinue() { // Update ref and check for auto-continue condition const checkAutoCompact = () => { const newStates = store.getAllStates(); - workspaceStatesRef.current = newStates; // Check all workspaces for completed compaction for (const [workspaceId, state] of newStates) { From 70aafba052ddbeb666518240d401750c3759c996 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 21:10:17 -0500 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A4=96=20Fix=20lint:=20remove=20unuse?= =?UTF-8?q?d=20WorkspaceState=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated with cmux --- src/hooks/useAutoCompactContinue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAutoCompactContinue.ts b/src/hooks/useAutoCompactContinue.ts index c9c4f33ce..bb23f0406 100644 --- a/src/hooks/useAutoCompactContinue.ts +++ b/src/hooks/useAutoCompactContinue.ts @@ -1,5 +1,5 @@ import { useRef, useEffect } from "react"; -import { useWorkspaceStoreRaw, type WorkspaceState } from "@/stores/WorkspaceStore"; +import { useWorkspaceStoreRaw } from "@/stores/WorkspaceStore"; import { buildSendMessageOptions } from "@/hooks/useSendMessageOptions"; /**