Skip to content

Commit b4225ce

Browse files
committed
🤖 Harden auto-continue idempotency: use compaction requestId when available
- 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
1 parent d2a4808 commit b4225ce

File tree

3 files changed

+31
-12
lines changed

3 files changed

+31
-12
lines changed

‎src/hooks/useAutoCompactContinue.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,23 @@ export function useAutoCompactContinue() {
5959
// After compaction, history is replaced with a single summary message
6060
// The summary message has compaction-result metadata with the continueMessage
6161
const summaryMessage = state.cmuxMessages[0]; // Single compacted message
62-
const messageId = summaryMessage.id;
63-
64-
// Have we already processed this specific compaction message?
65-
// This check is race-safe because message IDs are unique and immutable.
66-
if (processedMessageIds.current.has(messageId)) continue;
67-
6862
const cmuxMeta = summaryMessage?.metadata?.cmuxMetadata;
6963
const continueMessage =
7064
cmuxMeta?.type === "compaction-result" ? cmuxMeta.continueMessage : undefined;
7165

7266
if (!continueMessage) continue;
7367

74-
// Mark THIS MESSAGE as processed before sending
75-
// Multiple concurrent checkAutoCompact() calls will all see the same message ID,
76-
// so only the first call that reaches this point will proceed
77-
processedMessageIds.current.add(messageId);
68+
// Prefer compaction-request ID for idempotency; fall back to summary message ID
69+
const idForGuard =
70+
cmuxMeta?.type === "compaction-result" && cmuxMeta.requestId
71+
? `req:${cmuxMeta.requestId}`
72+
: `msg:${summaryMessage.id}`;
73+
74+
// Have we already processed this specific compaction result?
75+
if (processedMessageIds.current.has(idForGuard)) continue;
76+
77+
// Mark THIS RESULT as processed before sending to prevent duplicates
78+
processedMessageIds.current.add(idForGuard);
7879

7980
console.log(
8081
`[useAutoCompactContinue] Sending continue message for ${workspaceId}:`,
@@ -86,7 +87,7 @@ export function useAutoCompactContinue() {
8687
window.api.workspace.sendMessage(workspaceId, continueMessage, options).catch((error) => {
8788
console.error("Failed to send continue message:", error);
8889
// If sending failed, remove from processed set to allow retry
89-
processedMessageIds.current.delete(messageId);
90+
processedMessageIds.current.delete(idForGuard);
9091
});
9192
}
9293
};

‎src/stores/WorkspaceStore.ts‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,9 @@ export class WorkspaceStore {
417417
* Handle compact_summary tool completion.
418418
* Returns true if compaction was handled (caller should early return).
419419
*/
420+
// Track processed compaction-request IDs to dedupe performCompaction across duplicated events
421+
private processedCompactionRequestIds = new Set<string>();
422+
420423
private handleCompactionCompletion(
421424
workspaceId: string,
422425
aggregator: StreamingMessageAggregator,
@@ -430,13 +433,27 @@ export class WorkspaceStore {
430433
return false;
431434
}
432435

436+
// Extract the compaction-request message to identify this compaction run
437+
const compactionRequestMsg = findCompactionRequestMessage(aggregator);
438+
if (!compactionRequestMsg) {
439+
return false;
440+
}
441+
442+
// Dedupe: If we've already processed this compaction-request, skip re-running
443+
if (this.processedCompactionRequestIds.has(compactionRequestMsg.id)) {
444+
return true; // Already handled compaction for this request
445+
}
446+
433447
// Extract the summary text from the assistant's response
434448
const summary = aggregator.getCompactionSummary(data.messageId);
435449
if (!summary) {
436450
console.warn("[WorkspaceStore] Compaction completed but no summary text found");
437451
return false;
438452
}
439453

454+
// Mark this compaction-request as processed before performing compaction
455+
this.processedCompactionRequestIds.add(compactionRequestMsg.id);
456+
440457
this.performCompaction(workspaceId, aggregator, data, summary);
441458
return true;
442459
}
@@ -544,7 +561,7 @@ export class WorkspaceStore {
544561
: undefined,
545562
// Store continueMessage in summary so it survives history replacement
546563
cmuxMetadata: continueMessage
547-
? { type: "compaction-result", continueMessage }
564+
? { type: "compaction-result", continueMessage, requestId: compactRequestMsg?.id }
548565
: { type: "normal" },
549566
}
550567
);

‎src/types/message.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type CmuxFrontendMetadata =
2121
| {
2222
type: "compaction-result";
2323
continueMessage: string; // Message to send after compaction completes
24+
requestId?: string; // ID of the compaction-request user message that produced this summary (for idempotency)
2425
}
2526
| {
2627
type: "normal"; // Regular messages

0 commit comments

Comments
 (0)