* Update an existing message in history by historySequence * Reads the active chat.jsonl, replaces the matching message, and rewrites the file. * * This runs on every stream end, so it must stay O(active epoch): targets are * always in the active epoch (stream placeholders, compaction su
(workspaceId: string, message: MuxMessage)
| 1531 | * never in the sealed archive. |
| 1532 | */ |
| 1533 | async updateHistory(workspaceId: string, message: MuxMessage): Promise<Result<void>> { |
| 1534 | return this.fileLocks.withLock(workspaceId, async () => { |
| 1535 | try { |
| 1536 | const historyPath = this.getChatHistoryPath(workspaceId); |
| 1537 | |
| 1538 | // Read the active epoch — structural rewrite requires full file content |
| 1539 | const messages = await this.readChatHistory(workspaceId); |
| 1540 | const targetSequence = message.metadata?.historySequence; |
| 1541 | |
| 1542 | if (targetSequence === undefined) { |
| 1543 | return Err("Cannot update message without historySequence"); |
| 1544 | } |
| 1545 | |
| 1546 | assert( |
| 1547 | isNonNegativeInteger(targetSequence), |
| 1548 | "updateHistory requires historySequence to be a non-negative integer" |
| 1549 | ); |
| 1550 | |
| 1551 | // Find and replace the message with matching historySequence |
| 1552 | let found = false; |
| 1553 | let persistedMessage: MuxMessage | undefined; |
| 1554 | for (let i = 0; i < messages.length; i++) { |
| 1555 | if (messages[i].metadata?.historySequence === targetSequence) { |
| 1556 | const existingMessage = messages[i]; |
| 1557 | assert(existingMessage, "updateHistory matched message must exist"); |
| 1558 | |
| 1559 | // Preserve compaction boundary metadata during late in-place rewrites. |
| 1560 | // Compaction may update an assistant row first, then a late stream rewrite can |
| 1561 | // update that same historySequence and accidentally drop compaction markers. |
| 1562 | const preservedCompactionMetadata = getCompactionMetadataToPreserve( |
| 1563 | workspaceId, |
| 1564 | existingMessage, |
| 1565 | message |
| 1566 | ); |
| 1567 | |
| 1568 | // Preserve the historySequence, update everything else. |
| 1569 | messages[i] = { |
| 1570 | ...message, |
| 1571 | metadata: { |
| 1572 | ...message.metadata, |
| 1573 | ...(preservedCompactionMetadata ?? {}), |
| 1574 | historySequence: targetSequence, |
| 1575 | }, |
| 1576 | }; |
| 1577 | persistedMessage = messages[i]; |
| 1578 | found = true; |
| 1579 | break; |
| 1580 | } |
| 1581 | } |
| 1582 | |
| 1583 | if (!found || !persistedMessage) { |
| 1584 | return Err(`No message found with historySequence ${targetSequence}`); |
| 1585 | } |
| 1586 | |
| 1587 | // Rewrite entire file |
| 1588 | const historyEntries = this.serializeHistoryEntries(messages, workspaceId); |
| 1589 | |
| 1590 | // Atomic write prevents corruption if app crashes mid-write |
no test coverage detected