fix(native-chat): constrain live transcript retries

This commit is contained in:
OrcaWin
2026-08-12 02:10:35 -07:00
parent c94d241543
commit ef6fa7366c
4 changed files with 126 additions and 12 deletions
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
isGuestAbsoluteLinuxPath,
isTranscriptPathCompatibleWithHost,
needsWslHostTranslation,
resetHostReadableTranscriptPathCacheForTests,
toHostReadableTranscriptPath,
@@ -44,6 +45,28 @@ describe('needsWslHostTranslation', () => {
})
})
describe('isTranscriptPathCompatibleWithHost', () => {
it('constrains exact Windows paths to their authoritative host', () => {
expect(
isTranscriptPathCompatibleWithHost(ROLLOUT_UNC, { kind: 'wsl', distro: 'Ubuntu' }, 'win32')
).toBe(true)
expect(
isTranscriptPathCompatibleWithHost(ROLLOUT_UNC, { kind: 'wsl', distro: 'Debian' }, 'win32')
).toBe(false)
expect(isTranscriptPathCompatibleWithHost(ROLLOUT_UNC, { kind: 'host' }, 'win32')).toBe(false)
expect(
isTranscriptPathCompatibleWithHost('C:\\Users\\ada\\x.jsonl', { kind: 'host' }, 'win32')
).toBe(true)
expect(
isTranscriptPathCompatibleWithHost(
'C:\\Users\\ada\\x.jsonl',
{ kind: 'wsl', distro: 'Ubuntu' },
'win32'
)
).toBe(false)
})
})
describe('toHostReadableTranscriptPath', () => {
it('translates a WSL guest path to its UNC twin on Windows (#10326)', async () => {
await expect(
@@ -31,6 +31,27 @@ export function needsWslHostTranslation(
return platform === 'win32' && isGuestAbsoluteLinuxPath(path.trim())
}
export function isTranscriptPathCompatibleWithHost(
path: string,
transcriptHost: NativeChatTranscriptHost,
platform: NodeJS.Platform = process.platform
): boolean {
if (platform !== 'win32') {
return true
}
const unc = parseWslUncPath(path)
if (transcriptHost.kind === 'host') {
return !unc && !needsWslHostTranslation(path, platform)
}
const distro = transcriptHost.distro.trim()
return (
distro.length > 0 &&
(unc
? unc.distro.toLowerCase() === distro.toLowerCase()
: needsWslHostTranslation(path, platform))
)
}
export type HostReadableTranscriptPathDeps = {
platform?: NodeJS.Platform
pathExists?: (path: string) => Promise<boolean>
@@ -135,19 +156,13 @@ export async function toHostReadableTranscriptPath(
deps.pathExists ?? ((candidate: string) => pathExistsAsync(candidate, deps.signal))
const platform = deps.platform ?? process.platform
if (platform === 'win32' && deps.transcriptHost) {
if (!isTranscriptPathCompatibleWithHost(path, deps.transcriptHost, platform)) {
return null
}
const unc = parseWslUncPath(path)
if (unc) {
if (
deps.transcriptHost.kind === 'host' ||
unc.distro.toLowerCase() !== deps.transcriptHost.distro.trim().toLowerCase()
) {
return null
}
return (await pathExists(path)) ? path : null
}
if (deps.transcriptHost.kind === 'wsl' && !needsWslHostTranslation(path, platform)) {
return null
}
}
// Why: classify BEFORE probing — Win32 resolves a bare `/home/…` against the
// current drive (`C:\home\…`), so a probe first could bind chat to a local
@@ -117,6 +117,76 @@ describe('native chat transcript resolve polling', () => {
subscription.unsubscribe()
})
it.each([
[
'host provenance on WSL UNC',
{ kind: 'host' as const },
'\\\\wsl.localhost\\Ubuntu\\home\\ada\\session.jsonl'
],
[
'wrong WSL distro provenance',
{ kind: 'wsl' as const, distro: 'Debian' },
'\\\\wsl.localhost\\Ubuntu\\home\\ada\\session.jsonl'
],
[
'WSL provenance on a host drive',
{ kind: 'wsl' as const, distro: 'Ubuntu' },
'C:\\Users\\ada\\session.jsonl'
],
[
'host provenance on extended WSL UNC',
{ kind: 'host' as const },
'\\\\?\\UNC\\wsl.localhost\\Ubuntu\\home\\ada\\session.jsonl'
]
])('does not fast-install %s after an initial miss', async (_name, transcriptHost, path) => {
setPlatform('win32')
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
sessionId: 'session-id',
transcriptPath: path,
transcriptHost,
resolvePollIntervalMs: 10,
onAppend: () => {}
})
await vi.advanceTimersByTimeAsync(100)
expect(mocks.install).not.toHaveBeenCalled()
subscription.unsubscribe()
})
it('preserves raw exact-path retries when provenance is unavailable', async () => {
setPlatform('win32')
const unc = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\session.jsonl'
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
sessionId: 'session-id',
transcriptPath: unc,
resolvePollIntervalMs: 10,
onAppend: () => {}
})
await vi.advanceTimersByTimeAsync(100)
expect(mocks.install).toHaveBeenCalledWith(unc, expect.any(Function), expect.any(Object))
subscription.unsubscribe()
})
it('fast-installs an exact path that matches WSL provenance', async () => {
setPlatform('win32')
const unc = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\session.jsonl'
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
sessionId: 'session-id',
transcriptPath: unc,
transcriptHost: { kind: 'wsl', distro: 'Ubuntu' },
resolvePollIntervalMs: 10,
onAppend: () => {}
})
await vi.advanceTimersByTimeAsync(100)
expect(mocks.install).toHaveBeenCalledWith(unc, expect.any(Function), expect.any(Object))
subscription.unsubscribe()
})
it('keeps resolving on every retry when no exact hook path is available', async () => {
const subscription = await subscribeNativeChatTranscript({
agent: 'claude',
+9 -3
View File
@@ -1,6 +1,7 @@
import { extname } from 'node:path'
import type { NativeChatMessage } from '../../shared/native-chat-types'
import {
isTranscriptPathCompatibleWithHost,
needsWslHostTranslation,
toHostReadableTranscriptPath
} from './host-readable-transcript-path'
@@ -111,9 +112,14 @@ function subscribeViaResolvePoll(
try {
if (exactPath && !hostReadableExactPath) {
if (!needsWslHostTranslation(exactPath)) {
// Non-WSL paths stay raw: installTranscriptWatcher already handles a
// not-yet-created file, so don't spend an extra probe per tick.
hostReadableExactPath = exactPath
// Why: live retries must enforce the same PTY provenance as reads
// before installing a watcher on an exact UNC or drive path.
if (
!args.transcriptHost ||
isTranscriptPathCompatibleWithHost(exactPath, args.transcriptHost)
) {
hostReadableExactPath = exactPath
}
} else if (Date.now() - lastWslTranslateAt >= FALLBACK_RESOLVE_POLL_MS) {
// Why: translating probes the UNC twin per distro over the 9P
// share, and the guest file usually appears well after the hook does,