Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions src/hooks/useAutoCompactContinue.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand Down Expand Up @@ -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<Map<string, WorkspaceState>>(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.
Expand All @@ -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) {
Expand All @@ -59,22 +56,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}:`,
Expand All @@ -86,7 +84,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);
});
}
};
Expand Down
19 changes: 18 additions & 1 deletion src/stores/WorkspaceStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();

private handleCompactionCompletion(
workspaceId: string,
aggregator: StreamingMessageAggregator,
Expand All @@ -430,13 +433,27 @@ 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) {
console.warn("[WorkspaceStore] Compaction completed but no summary text found");
return false;
}

// Mark this compaction-request as processed before performing compaction
this.processedCompactionRequestIds.add(compactionRequestMsg.id);

this.performCompaction(workspaceId, aggregator, data, summary);
return true;
}
Expand Down Expand Up @@ -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" },
}
);
Expand Down
1 change: 1 addition & 0 deletions src/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down