Files
orca/src/main/native-chat/transcript-reader.ts
T
Brennan Benson 028f609b5a Keep chat thinking before its answer across history loads (#33628)
* fix(chat): preserve transcript source order across history windows

* test(chat): use template literal for mobile transcript fixture

* test(chat): place composed transcript contracts across project boundaries

* test(chat): move reader-to-phone contract across project boundaries

* test(chat): reuse transcript input setup and caller contracts

* test(chat): align mobile recordings with source-order capability

* test(chat): scope phone transforms and await history snapshot
2026-10-26 18:45:21 -07:00

108 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readOpenCodeNativeChatTranscriptFull } from './transcript-opencode'
import { isENOENT } from '../ipc/filesystem/filesystem-path-containment'
import type {
AgentType,
NativeChatMessage,
NativeChatTurnLifecycle
} from '../../shared/native-chat-types'
import { resolveNativeChatTranscriptAgent } from '../../shared/native-chat-agent-support'
import { errorMessage } from '../ai-vault/session-scanner-values'
import { resolveSessionFilePath, type ResolveSessionFileOptions } from './session-file-resolver'
import { openTranscriptReadStream } from './wsl-transcript-fs-access'
import { wslTranscriptFsRefusal } from './wsl-transcript-fs-gate'
import {
decodeClaudeTranscriptLine,
decodeCodexTranscriptLine,
decodeGrokTranscriptLine,
decodeOmpTranscriptLine
} from './transcript-line-decoders'
import { decodeTranscriptStream } from './transcript-stream-lines'
export type ReadTranscriptResult =
| {
messages: NativeChatMessage[]
lifecycle?: NativeChatTurnLifecycle
sourceEmpty?: boolean
}
// notFound is retryable: the agent may not have flushed its transcript yet.
| { error: string; notFound?: true }
export type ReadTranscriptOptions = ResolveSessionFileOptions & {
filePath?: string
sourceOrder?: boolean
}
/** Read the full transcript with no message cap; skip malformed/unknown records using the tailer’s decoders. */
export async function readNativeChatTranscript(
agent: AgentType,
sessionId: string,
options: ReadTranscriptOptions = {}
): Promise<ReadTranscriptResult> {
if (resolveNativeChatTranscriptAgent(agent) === 'opencode') {
return readOpenCodeNativeChatTranscriptFull(
sessionId,
{},
undefined,
agent === 'zcode' ? 'zcode' : undefined
)
}
let filePath: string | null
try {
filePath = options.filePath ?? (await resolveSessionFilePath(agent, sessionId, options))
} catch (err) {
// Gate refusal is unavailable, never evidence that the transcript is missing.
return { error: wslTranscriptFsRefusal(err).message }
}
if (!filePath) {
return { error: `No transcript found for ${agent} session ${sessionId}`, notFound: true }
}
try {
const transcriptAgent = resolveNativeChatTranscriptAgent(agent)
if (transcriptAgent === 'claude') {
return {
messages: await readTranscript(filePath, decodeClaudeTranscriptLine, options.sourceOrder)
}
}
if (transcriptAgent === 'codex') {
return {
messages: await readTranscript(filePath, decodeCodexTranscriptLine, options.sourceOrder)
}
}
if (transcriptAgent === 'grok') {
return {
messages: await readTranscript(filePath, decodeGrokTranscriptLine, options.sourceOrder)
}
}
if (transcriptAgent === 'omp') {
return {
messages: await readTranscript(filePath, decodeOmpTranscriptLine, options.sourceOrder)
}
}
return { error: `Unsupported agent for Chat UI transcript: ${agent}` }
} catch (err) {
// First flush/rotation can race successful resolution; ENOENT remains retryable.
if (isENOENT(err)) {
return { error: errorMessage(err), notFound: true }
}
return { error: errorMessage(err) }
}
}
async function readTranscript(
filePath: string,
decode: (line: string, fallbackId: string) => NativeChatMessage | null,
sourceOrder = false
): Promise<NativeChatMessage[]> {
const stream = openTranscriptReadStream(filePath, { encoding: 'utf-8' }, 'exact')
const { messages } = await decodeTranscriptStream(
stream,
filePath,
0,
decode,
true,
Infinity,
sourceOrder
)
return messages
}