Merge remote-tracking branch 'origin/main' into brennanb2025/ua-fallback-impl

This commit is contained in:
Merge Sim
2026-09-13 11:54:38 -07:00
3 changed files with 47 additions and 2 deletions
@@ -9,6 +9,8 @@ const MAX_CACHE_ENTRIES = 4096
export type SessionParseResumePoint = {
state: ResumableSessionParseState
mtimeMs: number
sizeBytes: number | undefined
// Byte offset just past the last complete ('\n'-terminated) line consumed;
// a trailing unterminated line is deliberately left before this point.
byteOffset: number
@@ -1,4 +1,4 @@
import { appendFile, mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'
import { appendFile, mkdir, mkdtemp, rm, stat, truncate, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
@@ -170,6 +170,8 @@ it('replays only the appended lines on a resumed read', async () => {
expect(firstRead?.outcome?.incomplete).toBe(false)
await appendFile(transcript, `${jsonLines(claudeTurns(5, 5))}\n`)
const changedAt = new Date(firstRead!.start.candidate.file.mtimeMs + 2000)
await utimes(transcript, changedAt, changedAt)
consumer.reads.length = 0
await scanAiVaultSessions({ ...roots, platform: 'darwin', limit: 20 })
@@ -183,6 +185,38 @@ it('replays only the appended lines on a resumed read', async () => {
])
})
it.each(['rewrite', 'truncate then regrow'])(
're-reads a same-size %s from zero',
async (operation) => {
const { transcript } = await writeClaudeFixture()
const before = await claudeCandidate(transcript)
await parseAgentSessionFileCached(before, 'darwin')
const consumer = recordingConsumer()
const rewritten = `${jsonLines(claudeTurns(1, 4))}\n`.replace('reply 4', 'fresh 4')
expect(Buffer.byteLength(rewritten)).toBe(before.file.sizeBytes)
if (operation === 'truncate then regrow') {
await truncate(transcript, 0)
await appendFile(transcript, rewritten)
} else {
await writeFile(transcript, rewritten)
}
const changedAt = new Date(before.file.mtimeMs + 2000)
await utimes(transcript, changedAt, changedAt)
const session = await parseAgentSessionFileCached(await claudeCandidate(transcript), 'darwin')
expect(consumer.reads).toHaveLength(1)
expect(consumer.reads[0].start.mode).toBe('replace')
expect(consumer.reads[0].start.previousByteOffset).toBe(0)
expect(textsFor(consumer.reads, 'claude')).toContain('assistant:fresh 4')
expect(textsFor(consumer.reads, 'claude')).not.toContain('assistant:reply 4')
resetSessionParseCacheForTests()
expect(session).toEqual(
await parseAgentSessionFileCached(await claudeCandidate(transcript), 'darwin')
)
}
)
it('publishes a trailing unterminated line once, when it is complete', async () => {
const { roots, transcript } = await writeClaudeFixture()
const consumer = recordingConsumer()
+10 -1
View File
@@ -68,6 +68,9 @@ export async function readResumableTranscript(args: {
resume !== null &&
typeof file.sizeBytes === 'number' &&
file.sizeBytes >= resume.byteOffset &&
file.mtimeMs >= resume.mtimeMs &&
// A changed timestamp without growth signals a rewrite, even at a valid line boundary.
(file.mtimeMs === resume.mtimeMs || file.sizeBytes > (resume.sizeBytes ?? resume.byteOffset)) &&
(resume.byteOffset === 0 || (await endsWithNewlineAt(file.path, resume.byteOffset)))
// Clone before consuming: a failed read must not corrupt the cached state,
@@ -127,7 +130,13 @@ export async function readResumableTranscript(args: {
channel.finishRead({ session, byteOffset: readResult.consumedThrough, incomplete: false })
return {
session,
resume: { state, byteOffset: readResult.consumedThrough, channel }
resume: {
state,
byteOffset: readResult.consumedThrough,
mtimeMs: file.mtimeMs,
sizeBytes: file.sizeBytes,
channel
}
}
} catch (error) {
channel.finishRead({ session: null, byteOffset: startOffset, incomplete: true })