(params: {
workspaceId: string;
chatPath?: string;
/** Sealed pre-boundary history (chat-archive.jsonl) for live sessions. */
chatArchivePath?: string;
partialPath?: string;
logLabel: string;
})
| 4450 | } |
| 4451 | |
| 4452 | const readTranscriptFromPaths = async (params: { |
| 4453 | workspaceId: string; |
| 4454 | chatPath?: string; |
| 4455 | /** Sealed pre-boundary history (chat-archive.jsonl) for live sessions. */ |
| 4456 | chatArchivePath?: string; |
| 4457 | partialPath?: string; |
| 4458 | logLabel: string; |
| 4459 | }): Promise<MuxMessage[]> => { |
| 4460 | const workspaceSessionDir = context.config.getSessionDir(params.workspaceId); |
| 4461 | |
| 4462 | // Defense-in-depth: refuse path traversal from a corrupted index file. |
| 4463 | if (params.chatPath && !isPathInsideDir(workspaceSessionDir, params.chatPath)) { |
| 4464 | throw new Error("Refusing to read transcript outside workspace session dir"); |
| 4465 | } |
| 4466 | if ( |
| 4467 | params.chatArchivePath && |
| 4468 | !isPathInsideDir(workspaceSessionDir, params.chatArchivePath) |
| 4469 | ) { |
| 4470 | throw new Error("Refusing to read transcript archive outside workspace session dir"); |
| 4471 | } |
| 4472 | if (params.partialPath && !isPathInsideDir(workspaceSessionDir, params.partialPath)) { |
| 4473 | throw new Error("Refusing to read partial outside workspace session dir"); |
| 4474 | } |
| 4475 | |
| 4476 | const partial = params.partialPath |
| 4477 | ? await readPartialJsonBestEffort(params.partialPath) |
| 4478 | : null; |
| 4479 | // Live sessions may have rotated sealed history into chat-archive.jsonl |
| 4480 | // (older rows), which precedes chat.jsonl (active epoch). |
| 4481 | const archivedMessages = params.chatArchivePath |
| 4482 | ? await readChatJsonlAllowMissing({ |
| 4483 | chatPath: params.chatArchivePath, |
| 4484 | logLabel: `${params.logLabel} (archive)`, |
| 4485 | }) |
| 4486 | : null; |
| 4487 | const messages = params.chatPath |
| 4488 | ? await readChatJsonlAllowMissing({ |
| 4489 | chatPath: params.chatPath, |
| 4490 | logLabel: params.logLabel, |
| 4491 | }) |
| 4492 | : null; |
| 4493 | |
| 4494 | // If we only archived partial.json (e.g. interrupted stream), still allow viewing. |
| 4495 | if (!messages && !archivedMessages && !partial) { |
| 4496 | throw new Error(`Transcript not found (missing ${params.logLabel})`); |
| 4497 | } |
| 4498 | |
| 4499 | return mergePartialIntoHistory( |
| 4500 | [...(archivedMessages ?? []), ...(messages ?? [])], |
| 4501 | partial |
| 4502 | ); |
| 4503 | }; |
| 4504 | |
| 4505 | let resolved: { |
| 4506 | workspaceId: string; |
no test coverage detected