( workspaceId: string, historyService: HistoryService, liveStream: ReturnType<AIService["getStreamInfo"]> )
| 577 | } |
| 578 | |
| 579 | async function buildSideQuestionTranscript( |
| 580 | workspaceId: string, |
| 581 | historyService: HistoryService, |
| 582 | liveStream: ReturnType<AIService["getStreamInfo"]> |
| 583 | ): Promise<string> { |
| 584 | // Read the active history window before applying the /btw cap. Prior side |
| 585 | // questions are filtered below; capping the raw tail first would let many |
| 586 | // side-question rows crowd out recent main-chat context. |
| 587 | const result = await historyService.getHistoryFromLatestBoundary(workspaceId); |
| 588 | if (!result.success) return ""; |
| 589 | |
| 590 | const messages: MuxMessage[] = [...result.data]; |
| 591 | const partial = await historyService.readPartial(workspaceId); |
| 592 | if (partial) messages.push(partial); |
| 593 | |
| 594 | // Partial files are throttled; merge the synchronous StreamManager snapshot |
| 595 | // so /btw sees the main-agent text already visible in the UI. |
| 596 | if (liveStream) { |
| 597 | const liveMessage: MuxMessage = { |
| 598 | id: liveStream.messageId, |
| 599 | role: "assistant", |
| 600 | metadata: { |
| 601 | historySequence: liveStream.historySequence, |
| 602 | model: liveStream.model, |
| 603 | }, |
| 604 | parts: liveStream.parts, |
| 605 | }; |
| 606 | const isLiveStreamMessage = (message: MuxMessage): boolean => |
| 607 | message.id === liveStream.messageId || |
| 608 | message.metadata?.historySequence === liveStream.historySequence; |
| 609 | const existingIndex = messages.findIndex(isLiveStreamMessage); |
| 610 | if (existingIndex === -1) { |
| 611 | messages.push(liveMessage); |
| 612 | } else { |
| 613 | const existing = messages[existingIndex]; |
| 614 | messages[existingIndex] = { |
| 615 | ...existing, |
| 616 | metadata: { |
| 617 | ...existing.metadata, |
| 618 | historySequence: liveStream.historySequence, |
| 619 | model: existing.metadata?.model ?? liveStream.model, |
| 620 | }, |
| 621 | parts: liveStream.parts, |
| 622 | }; |
| 623 | // Drop older partial.json copies for the same active stream; the live |
| 624 | // snapshot above is the single source of truth for visible streamed text. |
| 625 | for (let i = messages.length - 1; i >= 0; i--) { |
| 626 | if (i !== existingIndex && isLiveStreamMessage(messages[i])) { |
| 627 | messages.splice(i, 1); |
| 628 | } |
| 629 | } |
| 630 | } |
| 631 | } |
| 632 | |
| 633 | // Exclude PRIOR side-question exchanges from the transcript so /btw answers |
| 634 | // don't pollute the context the model uses to answer the current side |
| 635 | // question. Each /btw is independent — chained /btw turns would otherwise |
| 636 | // amplify their own (potentially-wrong) prior answers. Then honor the same |
no test coverage detected