* Dispatch the pending follow-up from a compaction summary message. * Called after compaction completes - the follow-up is stored on the summary * for crash safety. The user message persisted by sendMessage() serves as * proof of dispatch (no history rewrite needed).
(summaryMessageId?: string)
| 5313 | * proof of dispatch (no history rewrite needed). |
| 5314 | */ |
| 5315 | private async dispatchPendingFollowUp(summaryMessageId?: string): Promise<boolean> { |
| 5316 | if (this.disposed) { |
| 5317 | return false; |
| 5318 | } |
| 5319 | |
| 5320 | let summaryMessage: MuxMessage | undefined; |
| 5321 | if (summaryMessageId) { |
| 5322 | const historyResult = await this.historyService.getHistoryFromLatestBoundary( |
| 5323 | this.workspaceId |
| 5324 | ); |
| 5325 | if (!historyResult.success) { |
| 5326 | throw new Error( |
| 5327 | `Failed to read history for targeted follow-up recovery: ${historyResult.error}` |
| 5328 | ); |
| 5329 | } |
| 5330 | summaryMessage = historyResult.data.find((message) => message.id === summaryMessageId); |
| 5331 | if (!summaryMessage) { |
| 5332 | return false; |
| 5333 | } |
| 5334 | } else { |
| 5335 | // Read the last message from history — only need 1 message, avoid full-file read. |
| 5336 | // Startup recovery must retry on transient read failures, so bubble errors. |
| 5337 | const historyResult = await this.historyService.getLastMessages(this.workspaceId, 1); |
| 5338 | if (!historyResult.success) { |
| 5339 | const historyError = |
| 5340 | typeof historyResult.error === "string" |
| 5341 | ? historyResult.error |
| 5342 | : getErrorMessage(historyResult.error); |
| 5343 | throw new Error(`Failed to read history for startup follow-up recovery: ${historyError}`); |
| 5344 | } |
| 5345 | |
| 5346 | if (historyResult.data.length === 0) { |
| 5347 | return false; |
| 5348 | } |
| 5349 | summaryMessage = historyResult.data[0]; |
| 5350 | } |
| 5351 | |
| 5352 | const lastMessage = summaryMessage; |
| 5353 | const muxMeta = lastMessage.metadata?.muxMetadata; |
| 5354 | |
| 5355 | // Check if it's a compaction summary with a pending follow-up |
| 5356 | if (!isCompactionSummaryMetadata(muxMeta) || !muxMeta.pendingFollowUp) { |
| 5357 | return false; |
| 5358 | } |
| 5359 | |
| 5360 | // Handle legacy formats: older persisted requests may have `mode` instead of `agentId`, |
| 5361 | // and `imageParts` instead of `fileParts`. |
| 5362 | const followUp = muxMeta.pendingFollowUp as typeof muxMeta.pendingFollowUp & { |
| 5363 | mode?: "exec" | "plan"; |
| 5364 | imageParts?: FilePart[]; |
| 5365 | }; |
| 5366 | |
| 5367 | const hasQueuedMessages = this.hasPendingManualFollowUp(); |
| 5368 | const hasActiveNonCompletingTurn = this.isBusy() && this.turnPhase !== TurnPhase.COMPLETING; |
| 5369 | if ( |
| 5370 | followUp.dispatchOptions?.requireIdle === true && |
| 5371 | (hasQueuedMessages || hasActiveNonCompletingTurn) |
| 5372 | ) { |
no test coverage detected