fix(native-chat): retry not-yet-flushed transcripts instead of settling into a permanent error (#8418)

* fix(native-chat): retry not-yet-flushed transcripts instead of settling into a permanent error

A freshly-created session's transcript .jsonl lands on disk seconds to
minutes after the process starts. Native chat's one-shot read raced that
first flush: a miss became a permanent "No transcript found" error and
the live-tail subscription silently degraded to a no-op, so the pane
never recovered even after the file appeared.

- transcript-reader/read-cache: mark the miss with notFound so callers
  can tell "not flushed yet" from a real parse/IO error (never cached).
- transcript-watch: poll resolve+install (500ms backoff, 5s cap) for the
  subscription's lifetime instead of returning a dead no-op watcher.
- use-native-chat-live-session: retry a notFound read with backoff for
  up to 60s while staying in the loading state, and let live appends
  render over a stale initial-read error.

Fixes #8401

Claude-Session: https://claude.ai/code/session_01HA5g3X7wCakBttBpDru9Fp

* fix(native-chat): address CodeRabbit review — ENOENT retryable, content over spinner, blank-id guard, unref poll timer

- transcript-reader: an ENOENT after a successful resolve is the same
  first-flush/rotation race as an unresolved path — mark it notFound.
- use-native-chat-live-session: live appends landing mid-retry render
  instead of the loading state (mirrors the stale-error gate).
- transcript-watch: bail out for a blank session id with no explicit
  file (nothing to resolve-poll), and unref the poll timer so headless
  serve shutdown is never held open by an unresolvable session.

Claude-Session: https://claude.ai/code/session_01HA5g3X7wCakBttBpDru9Fp

---------

Co-authored-by: kaynan <kaynan.camargo@terceiro-sky.com.br>
This commit is contained in:
Kaynan Sampaio de Camargo
2026-07-13 12:45:46 -07:00
committed by GitHub
co-authored by kaynan
parent 527c692b71
commit dc4fb2aa03
10 changed files with 649 additions and 51 deletions
+24 -2
View File
@@ -242,19 +242,41 @@ describe('readNativeChatTranscript (codex)', () => {
})
describe('readNativeChatTranscript (errors)', () => {
it('returns an error for an unreadable/missing file without throwing', async () => {
// Why: ENOENT after a successful resolve is the same first-flush/rotation
// race as an unresolved path (#8401) — it must stay retry-worthy.
it('marks an ENOENT on a directly-passed path as notFound (vanished after resolve)', async () => {
const result = await readNativeChatTranscript('claude', 'sess', {
filePath: join(tmpdir(), 'orca-native-chat-does-not-exist.jsonl')
})
expect('error' in result).toBe(true)
if ('error' in result) {
expect(result.notFound).toBe(true)
}
})
it('returns an error when no transcript can be resolved', async () => {
it('returns a real read error (no notFound) when the path exists but is unreadable', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-unreadable-'))
tempRoots.push(root)
// A directory instead of a file fails the read with a non-ENOENT error.
const result = await readNativeChatTranscript('claude', 'sess', { filePath: root })
expect('error' in result).toBe(true)
if ('error' in result) {
expect(result.notFound).toBeUndefined()
}
})
// Why: a just-created Claude Code session's transcript can take up to minutes
// to exist on disk (#8401) — the miss must be marked retry-worthy so callers
// above (cache, watch, renderer) don't settle into a permanent error.
it('marks an unresolved session as notFound so callers know to retry', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-native-chat-noresolve-'))
tempRoots.push(root)
const result = await readNativeChatTranscript('claude', 'missing', {
claudeProjectsDir: join(root, 'empty')
})
expect('error' in result).toBe(true)
if ('error' in result) {
expect(result.notFound).toBe(true)
}
})
})