* Tear down a /btw placeholder whose stream failed or produced empty text * before the next candidate is tried. * * Closes the stream with an empty `stream-end` first so side-question * terminal-event bookkeeping (including the aggregator/store side-answer * terminal guards) unwinds while the p
(opts: {
workspaceId: string;
assistantMessageId: string;
assistantHistorySequence: number;
modelString: string;
historyService: HistoryService;
emitChatEvent: (workspaceId: string, message: WorkspaceChatMessage) => void;
})
| 526 | * chat.jsonl, which is the same outcome we used to ship anyway. |
| 527 | */ |
| 528 | async function deleteSideQuestionPlaceholder(opts: { |
| 529 | workspaceId: string; |
| 530 | assistantMessageId: string; |
| 531 | assistantHistorySequence: number; |
| 532 | modelString: string; |
| 533 | historyService: HistoryService; |
| 534 | emitChatEvent: (workspaceId: string, message: WorkspaceChatMessage) => void; |
| 535 | }): Promise<void> { |
| 536 | const { |
| 537 | workspaceId, |
| 538 | assistantMessageId, |
| 539 | assistantHistorySequence, |
| 540 | modelString, |
| 541 | historyService, |
| 542 | emitChatEvent, |
| 543 | } = opts; |
| 544 | |
| 545 | // Close the stream slot while the side-answer placeholder still exists. |
| 546 | // The aggregator and WorkspaceStore look up this message by id to check |
| 547 | // its muxMetadata before dispatching their side-answer-aware branches |
| 548 | // (e.g. WorkspaceStore.bufferedEventHandlers["stream-end"] skips |
| 549 | // collapsePinnedTodoOnStreamStop iff the terminal stream belongs to a |
| 550 | // side answer; the aggregator's handleStreamEnd skips main-agent |
| 551 | // onResponseComplete / lastCompletedStreamStats updates). Deleting the |
| 552 | // placeholder first would make those lookups fail, and the terminal |
| 553 | // event would fall through to the main-agent code paths — clobbering |
| 554 | // pinned-todo state and producing stale completion stats for a stream |
| 555 | // that should be invisible to main-agent lifecycle. |
| 556 | emitChatEvent(workspaceId, { |
| 557 | type: "stream-end", |
| 558 | workspaceId, |
| 559 | messageId: assistantMessageId, |
| 560 | metadata: { model: modelString, historySequence: assistantHistorySequence }, |
| 561 | parts: [], |
| 562 | }); |
| 563 | |
| 564 | const deleteResult = await historyService.deleteMessage(workspaceId, assistantMessageId); |
| 565 | if (!deleteResult.success) { |
| 566 | log.warn("Side question: failed to delete placeholder for retry", { |
| 567 | workspaceId, |
| 568 | assistantMessageId, |
| 569 | error: deleteResult.error, |
| 570 | }); |
| 571 | } |
| 572 | |
| 573 | emitChatEvent(workspaceId, { |
| 574 | type: "delete", |
| 575 | historySequences: [assistantHistorySequence], |
| 576 | }); |
| 577 | } |
| 578 | |
| 579 | async function buildSideQuestionTranscript( |
| 580 | workspaceId: string, |
no test coverage detected