* Read messages from a compaction boundary onward. * Falls back to full history if no boundary exists (new/uncompacted workspace). * * @param skip How many boundaries to skip (counting from the latest, across * chat.jsonl and the sealed archive). 0 = read from the latest *
(workspaceId: string, skip = 0)
| 957 | * that only needs the active compaction epoch. |
| 958 | */ |
| 959 | async getHistoryFromLatestBoundary(workspaceId: string, skip = 0): Promise<Result<MuxMessage[]>> { |
| 960 | try { |
| 961 | // One-time lazy migration: seal any pre-boundary prefix left in chat.jsonl |
| 962 | // by older builds so this read (and every later one) stays O(active epoch). |
| 963 | await this.ensureSealedHistoryRotated(workspaceId); |
| 964 | |
| 965 | const chatPath = this.getChatHistoryPath(workspaceId); |
| 966 | const archivePath = this.getChatArchivePath(workspaceId); |
| 967 | |
| 968 | // Try the requested boundary in chat.jsonl, falling back to less-skipped boundaries. |
| 969 | let chatBoundaryCount = 0; |
| 970 | let chatFallbackOffset: number | null = null; |
| 971 | for (let s = skip; s >= 0; s--) { |
| 972 | const offset = await this.findLastBoundaryByteOffset(chatPath, s); |
| 973 | if (offset !== null) { |
| 974 | if (s === skip) { |
| 975 | return Ok(await this.readHistoryFromOffset(chatPath, offset)); |
| 976 | } |
| 977 | // chat.jsonl has fewer boundaries than requested; remember its oldest |
| 978 | // boundary as a fallback and keep counting into the archive. |
| 979 | chatBoundaryCount = s + 1; |
| 980 | chatFallbackOffset = offset; |
| 981 | break; |
| 982 | } |
| 983 | } |
| 984 | |
| 985 | // Boundaries older than chat.jsonl live in the sealed archive. A window that |
| 986 | // starts at an archive boundary spans the archive tail plus all of chat.jsonl. |
| 987 | for (let s = skip - chatBoundaryCount; s >= 0; s--) { |
| 988 | const offset = await this.findLastBoundaryByteOffset(archivePath, s); |
| 989 | if (offset !== null) { |
| 990 | const archived = await this.readHistoryFromOffset(archivePath, offset); |
| 991 | const active = await this.readChatHistory(workspaceId); |
| 992 | return Ok([...archived, ...active]); |
| 993 | } |
| 994 | } |
| 995 | |
| 996 | if (chatFallbackOffset !== null) { |
| 997 | return Ok(await this.readHistoryFromOffset(chatPath, chatFallbackOffset)); |
| 998 | } |
| 999 | |
| 1000 | // No boundaries at all — workspace is uncompacted, full read is the only option |
| 1001 | const archived = await this.readArchivedHistory(workspaceId); |
| 1002 | const active = await this.readChatHistory(workspaceId); |
| 1003 | return Ok([...archived, ...active]); |
| 1004 | } catch (error) { |
| 1005 | const message = getErrorMessage(error); |
| 1006 | return Err(`Failed to read history from boundary: ${message}`); |
| 1007 | } |
| 1008 | } |
| 1009 | |
| 1010 | // ── Sealed-history rotation ───────────────────────────────────────────── |
| 1011 | // Compaction (and /clear --soft) appends a durable context boundary but, by |