* Perform history compaction by persisting a durable summary boundary. * * Steps: * 1. Delete partial state to avoid stale partial replay * 2. Persist post-compaction attachment state * 3. Prefer updating the streamed summary in-place, otherwise append a fallback summary * 4. Emit
(
summary: string,
metadata: {
model: string;
usage?: LanguageModelV2Usage;
contextUsage?: LanguageModelV2Usage;
duration?: number;
providerMetadata?: Record<string, unknown>;
contextProviderMetadata?: Record<string, unknown>;
systemMessageTokens?: number;
},
messages: MuxMessage[],
streamedSummaryMessageId: string,
compactionRequestMessageId: string,
isIdleCompaction = false,
pendingFollowUp?: CompactionFollowUpRequest
)
| 1007 | * 4. Emit summary message to frontend |
| 1008 | */ |
| 1009 | private async performCompaction( |
| 1010 | summary: string, |
| 1011 | metadata: { |
| 1012 | model: string; |
| 1013 | usage?: LanguageModelV2Usage; |
| 1014 | contextUsage?: LanguageModelV2Usage; |
| 1015 | duration?: number; |
| 1016 | providerMetadata?: Record<string, unknown>; |
| 1017 | contextProviderMetadata?: Record<string, unknown>; |
| 1018 | systemMessageTokens?: number; |
| 1019 | }, |
| 1020 | messages: MuxMessage[], |
| 1021 | streamedSummaryMessageId: string, |
| 1022 | compactionRequestMessageId: string, |
| 1023 | isIdleCompaction = false, |
| 1024 | pendingFollowUp?: CompactionFollowUpRequest |
| 1025 | ): Promise<Result<CompactionCompletionMetadata, string>> { |
| 1026 | assert(summary.trim().length > 0, "performCompaction requires a non-empty summary"); |
| 1027 | assert(metadata.model.trim().length > 0, "Compaction summary requires a model"); |
| 1028 | assert( |
| 1029 | streamedSummaryMessageId.trim().length > 0, |
| 1030 | "performCompaction requires streamed summary message ID" |
| 1031 | ); |
| 1032 | |
| 1033 | // CRITICAL: Delete partial.json BEFORE persisting compaction summary. |
| 1034 | // This prevents a race condition where: |
| 1035 | // 1. CompactionHandler persists summary |
| 1036 | // 2. sendQueuedMessages triggers commitPartial |
| 1037 | // 3. commitPartial finds stale partial.json and appends it to history |
| 1038 | // By deleting partial first, commitPartial becomes a no-op |
| 1039 | const deletePartialResult = await this.historyService.deletePartial(this.workspaceId); |
| 1040 | if (!deletePartialResult.success) { |
| 1041 | log.warn(`Failed to delete partial before compaction: ${deletePartialResult.error}`); |
| 1042 | // Continue anyway - the partial may not exist, which is fine |
| 1043 | } |
| 1044 | |
| 1045 | // Extract diffs from the latest compaction epoch only, so append-only history |
| 1046 | // does not re-inject stale pre-boundary edits after subsequent compactions. |
| 1047 | // If boundary markers are malformed, slicing self-heals by falling back to |
| 1048 | // full history instead of crashing or dropping all diffs. |
| 1049 | await this.preparePendingStateFromMessages(messages); |
| 1050 | |
| 1051 | const nextCompactionEpoch = getNextCompactionEpoch(messages); |
| 1052 | assert(Number.isInteger(nextCompactionEpoch), "next compaction epoch must be an integer"); |
| 1053 | |
| 1054 | const previousBoundaryHistorySequence = getLatestBoundaryHistorySequence(messages); |
| 1055 | const maxExistingHistorySequence = this.getMaxExistingHistorySequence(messages); |
| 1056 | |
| 1057 | // For idle compaction, preserve the original recency timestamp so the workspace |
| 1058 | // doesn't appear "recently used" in the sidebar. Use the shared recency utility |
| 1059 | // to ensure consistency with how the sidebar computes recency. |
| 1060 | let timestamp = Date.now(); |
| 1061 | if (isIdleCompaction) { |
| 1062 | const recency = computeRecencyFromMessages(messages); |
| 1063 | if (recency !== null) { |
| 1064 | timestamp = recency; |
| 1065 | } |
| 1066 | } |
no test coverage detected