mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* 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
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
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
|
||
}
|