mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
refactor(native-chat): drop the transcript helpers only the handoff called
appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -137,19 +137,6 @@ async function runPinnedTranscriptPasses<T>(
|
||||
}
|
||||
}
|
||||
|
||||
export async function proveClaudeTranscriptBranch(
|
||||
input: BranchProofInput & { transcriptPath: string }
|
||||
): Promise<ClaudeTranscriptBranchProof> {
|
||||
return runPinnedTranscriptPasses(input.transcriptPath, undefined, async (readLines) => {
|
||||
const builder = createBranchProof(input)
|
||||
let index = 0
|
||||
for await (const record of readLines()) {
|
||||
builder.add(record.line, index++, record.terminated)
|
||||
}
|
||||
return builder.finish()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Prove the branch from the file's last transcript row back to the anchor, then
|
||||
* replay the anchor..tail records off the SAME pinned bytes. Two bounded passes
|
||||
|
||||
@@ -73,28 +73,35 @@ import { appendFile, mkdtemp, rename, rm, truncate, unlink, writeFile } from 'no
|
||||
import {
|
||||
ClaudeTranscriptPreviousCursorMissingError,
|
||||
ClaudeTranscriptTailIncompleteError,
|
||||
proveClaudeTranscriptBranch,
|
||||
proveClaudeTranscriptBranchFromJsonl
|
||||
replayClaudeTranscriptBranchAncestry,
|
||||
replayClaudeTranscriptBranchAncestryFromJsonl
|
||||
} from './claude-transcript-branch-proof'
|
||||
|
||||
const row = (uuid: string, parentUuid: string | null, extra = {}) =>
|
||||
`${JSON.stringify({ type: 'user', uuid, parentUuid, sessionId: 'provider', ...extra })}\n`
|
||||
const marker = (leafUuid: string, sessionId = 'provider') =>
|
||||
`${JSON.stringify({ type: 'last-prompt', leafUuid, sessionId })}\n`
|
||||
/** Carries no uuid, so it can be neither a tip nor a marker. */
|
||||
const SUMMARY = `${JSON.stringify({ type: 'summary', summary: 'title' })}\n`
|
||||
const ROOT = row('root', null)
|
||||
const CHILD = row('child', 'root')
|
||||
const SOURCE = ROOT + marker('root')
|
||||
let directory = ''
|
||||
const read = (previousLeafUuid: string | null = null, intentionalRewindUuid?: string) =>
|
||||
proveClaudeTranscriptBranch({
|
||||
let replayed: string[] = []
|
||||
// The live caller always anchors the replay at the cursor it already holds.
|
||||
const replay = (anchorUuid = 'root') =>
|
||||
replayClaudeTranscriptBranchAncestry({
|
||||
transcriptPath: state.path,
|
||||
providerSessionId: 'provider',
|
||||
previousLeafUuid,
|
||||
...(intentionalRewindUuid === undefined ? {} : { intentionalRewindUuid })
|
||||
previousLeafUuid: anchorUuid,
|
||||
ancestryAnchorUuid: anchorUuid,
|
||||
onAncestorRecord: (_record, uuid) => replayed.push(uuid)
|
||||
})
|
||||
const read = async (anchorUuid?: string) => (await replay(anchorUuid)).proof
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), 'orca-branch-streaming-'))
|
||||
replayed = []
|
||||
Object.assign(state, {
|
||||
path: join(directory, 'transcript.jsonl'),
|
||||
opens: 0,
|
||||
@@ -121,7 +128,7 @@ afterEach(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('decodes UTF-8 across chunks and proves old ancestry beyond large message bodies', async () => {
|
||||
it('decodes UTF-8 across chunks and replays ancestry beyond large message bodies', async () => {
|
||||
const prefix = JSON.stringify({ type: 'comment', content: '' }).indexOf('"content":"') + 11
|
||||
const comment = `${JSON.stringify({ type: 'comment', content: `${'x'.repeat(65535 - prefix)}🙂é漢字` })}\n`
|
||||
const contents =
|
||||
@@ -130,13 +137,18 @@ it('decodes UTF-8 across chunks and proves old ancestry beyond large message bod
|
||||
CHILD +
|
||||
marker('child').trimEnd()
|
||||
await writeFile(state.path, contents)
|
||||
expect(await read('root')).toEqual(
|
||||
proveClaudeTranscriptBranchFromJsonl({
|
||||
const fromString: string[] = []
|
||||
expect(await replay()).toEqual(
|
||||
replayClaudeTranscriptBranchAncestryFromJsonl({
|
||||
contents,
|
||||
providerSessionId: 'provider',
|
||||
previousLeafUuid: 'root'
|
||||
previousLeafUuid: 'root',
|
||||
ancestryAnchorUuid: 'root',
|
||||
onAncestorRecord: (_record, uuid) => fromString.push(uuid)
|
||||
})
|
||||
)
|
||||
expect(replayed).toEqual(fromString)
|
||||
expect(replayed).toEqual(['child'])
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -156,31 +168,36 @@ it.each([
|
||||
|
||||
it('proves only the original finite prefix while the same file grows', async () => {
|
||||
state.afterStat = () => appendFile(state.path, CHILD + marker('child') + ' '.repeat(1024 * 1024))
|
||||
expect(await read()).toEqual({ leafUuid: 'root', relation: 'initial' })
|
||||
expect(await read()).toEqual({ leafUuid: 'root', relation: 'same' })
|
||||
expect(state.bytesRead).toBe(Buffer.byteLength(SOURCE))
|
||||
})
|
||||
|
||||
it('keeps a proved rewind tied to the original prefix', async () => {
|
||||
await writeFile(state.path, ROOT + CHILD + marker('root'))
|
||||
state.afterStat = () => appendFile(state.path, marker('child'))
|
||||
expect(await read('child', 'root')).toEqual({ leafUuid: 'root', relation: 'intentional-rewind' })
|
||||
it('replays the ancestry off the same pinned prefix the proof read', async () => {
|
||||
const contents = ROOT + CHILD
|
||||
await writeFile(state.path, contents)
|
||||
state.afterStat = () => appendFile(state.path, row('grandchild', 'child'))
|
||||
expect(await replay()).toEqual({
|
||||
proof: { leafUuid: 'child', relation: 'descendant' },
|
||||
chain: ['child']
|
||||
})
|
||||
expect(replayed).toEqual(['child'])
|
||||
expect(state.bytesRead).toBe(Buffer.byteLength(contents) * 2)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['empty', '', SOURCE, null],
|
||||
['missing marker', ROOT, marker('root'), null],
|
||||
['empty', '', SOURCE, 'root'],
|
||||
['missing previous cursor', SOURCE, CHILD + marker('child'), 'child']
|
||||
] as const)(
|
||||
'finishes a growing %s proof on the same open handle',
|
||||
async (_name, contents, growth, previous) => {
|
||||
async (_name, contents, growth, anchor) => {
|
||||
await writeFile(state.path, contents)
|
||||
state.afterStat = () => appendFile(state.path, growth)
|
||||
expect((await read(previous)).leafUuid).toBe(previous ?? 'root')
|
||||
expect((await read(anchor)).leafUuid).toBe(anchor)
|
||||
expect(state.opens).toBe(1)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['', ROOT])('keeps a static missing marker fatal', async (contents) => {
|
||||
it.each(['', SUMMARY])('keeps a static missing tip fatal', async (contents) => {
|
||||
await writeFile(state.path, contents)
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
@@ -190,22 +207,22 @@ it('preserves the typed static missing-cursor error for existing root reproof',
|
||||
})
|
||||
|
||||
it.each([
|
||||
['conflict', ROOT + row('root', 'foreign') + marker('root'), 'conflicting ancestry'],
|
||||
['wrong session', ROOT + marker('root', 'foreign'), 'invalid last-prompt'],
|
||||
['append order', CHILD + ROOT + marker('child'), 'parent row follows'],
|
||||
['missing ancestor', CHILD + marker('child'), 'missing ancestor']
|
||||
['conflict', ROOT + row('root', 'foreign') + marker('root'), 'root', 'conflicting ancestry'],
|
||||
['wrong session', ROOT + marker('root', 'foreign'), 'root', 'invalid last-prompt'],
|
||||
['append order', CHILD + ROOT, 'root', 'parent row follows'],
|
||||
['missing ancestor', CHILD, 'child', 'missing ancestor']
|
||||
] as const)(
|
||||
'does not turn %s into a retry when the file grows',
|
||||
async (_name, contents, message) => {
|
||||
async (_name, contents, anchor, message) => {
|
||||
await writeFile(state.path, contents)
|
||||
state.afterStat = () => appendFile(state.path, CHILD)
|
||||
await expect(read()).rejects.toThrow(message)
|
||||
await expect(read(anchor)).rejects.toThrow(message)
|
||||
}
|
||||
)
|
||||
|
||||
it('does not infer growth from a failed second stat', async () => {
|
||||
await writeFile(state.path, ROOT)
|
||||
state.afterStat = () => appendFile(state.path, marker('root'))
|
||||
await writeFile(state.path, '')
|
||||
state.afterStat = () => appendFile(state.path, SOURCE)
|
||||
state.statErrorOn = 2
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
@@ -219,7 +236,7 @@ it('keeps reading the original handle after pathname replacement', async () => {
|
||||
})
|
||||
|
||||
it('does not use a replacement file to establish growth', async () => {
|
||||
await writeFile(state.path, ROOT)
|
||||
await writeFile(state.path, '')
|
||||
state.afterStat = async () => {
|
||||
await rename(state.path, `${state.path}.original`)
|
||||
await writeFile(state.path, ROOT + CHILD + marker('child'))
|
||||
@@ -252,16 +269,18 @@ it('completes a record appended after the first observed extent without caller r
|
||||
})
|
||||
|
||||
it('validates the entire refreshed prefix instead of accepting a malformed repair', async () => {
|
||||
await writeFile(state.path, ROOT)
|
||||
state.afterStat = () => appendFile(state.path, row('root', 'foreign') + marker('root'))
|
||||
const conflicting = row('root', 'foreign')
|
||||
await writeFile(state.path, ROOT + conflicting.slice(0, -5))
|
||||
state.afterStat = () => appendFile(state.path, conflicting.slice(-5))
|
||||
await expect(read()).rejects.toThrow('conflicting ancestry')
|
||||
expect(state.opens).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps an unfinished growing repair retryable after one internal refresh', async () => {
|
||||
await writeFile(state.path, ROOT)
|
||||
state.afterStat = () => appendFile(state.path, ROOT)
|
||||
const torn = ROOT.slice(0, -5)
|
||||
await writeFile(state.path, torn)
|
||||
state.afterStat = () => appendFile(state.path, ' ')
|
||||
await expect(read()).rejects.toBeInstanceOf(ClaudeTranscriptTailIncompleteError)
|
||||
expect(state.opens).toBe(1)
|
||||
expect(state.bytesRead).toBe(Buffer.byteLength(ROOT) * 3)
|
||||
expect(state.bytesRead).toBe(Buffer.byteLength(torn) * 2 + 1)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
// The branch rules a Claude transcript must satisfy, read off the marker tip. The live replay
|
||||
// proves from the file tail instead and falls back to these rules when no tail row qualifies.
|
||||
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ClaudeTranscriptTailIncompleteError,
|
||||
proveClaudeTranscriptBranchFromJsonl
|
||||
} from './claude-transcript-branch-proof'
|
||||
|
||||
let tempRoots: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
|
||||
tempRoots = []
|
||||
})
|
||||
|
||||
async function makeRoot(prefix: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), prefix))
|
||||
tempRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
async function proveMarkerLeafUuid(
|
||||
transcriptPath: string,
|
||||
providerSessionId: string,
|
||||
previousLeafUuid: string | null = null
|
||||
): Promise<string> {
|
||||
const contents = await readFile(transcriptPath, 'utf8')
|
||||
return proveClaudeTranscriptBranchFromJsonl({ contents, providerSessionId, previousLeafUuid })
|
||||
.leafUuid
|
||||
}
|
||||
|
||||
describe('Claude transcript marker branch proof', () => {
|
||||
it('reads Claude last-prompt leaf metadata as the durable branch marker', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-leaf-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'leaf-old', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'leaf-current',
|
||||
parentUuid: 'leaf-old',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'leaf-current', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1', 'leaf-old')).resolves.toBe(
|
||||
'leaf-current'
|
||||
)
|
||||
})
|
||||
|
||||
it('fails closed when a Claude transcript has no branch marker', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-no-leaf-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
'{"type":"assistant","uuid":"not-a-leaf","parentUuid":null,"sessionId":"session-1"}\n',
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'missing last-prompt marker'
|
||||
)
|
||||
})
|
||||
|
||||
it('distinguishes an incomplete final Claude JSONL record from durable malformed content', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-torn-tail-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(transcript, '{"type":"last-prompt"', 'utf8')
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.toBeInstanceOf(
|
||||
ClaudeTranscriptTailIncompleteError
|
||||
)
|
||||
|
||||
await writeFile(transcript, '{"type":"last-prompt"\n', 'utf8')
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.not.toBeInstanceOf(
|
||||
ClaudeTranscriptTailIncompleteError
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses a Claude marker on a sibling branch', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-sibling-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'root', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'expected', parentUuid: 'root', sessionId: 'session-1' },
|
||||
{ type: 'system', uuid: 'sibling', parentUuid: 'root', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'sibling', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1', 'expected')).rejects.toThrow(
|
||||
'sibling branch'
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses missing and cyclic Claude parent chains', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-invalid-ancestry-')
|
||||
const missing = join(root, 'missing.jsonl')
|
||||
const cycle = join(root, 'cycle.jsonl')
|
||||
await writeFile(
|
||||
missing,
|
||||
[
|
||||
{ type: 'user', uuid: 'expected', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'leaf', parentUuid: 'absent', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'leaf', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
await writeFile(
|
||||
cycle,
|
||||
[
|
||||
{ type: 'user', uuid: 'expected', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'left', parentUuid: 'right', sessionId: 'session-1' },
|
||||
{ type: 'system', uuid: 'right', parentUuid: 'left', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'right', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(missing, 'session-1', 'expected')).rejects.toThrow(
|
||||
'missing ancestor absent'
|
||||
)
|
||||
await expect(proveMarkerLeafUuid(cycle, 'session-1', 'expected')).rejects.toThrow(
|
||||
'cycle in parentUuid ancestry'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects non-transcript and sidechain UUIDs as the durable leaf', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-leaf-filter-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'sidechain-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
isSidechain: true
|
||||
},
|
||||
{ type: 'result', uuid: 'result-frame', parentUuid: 'main-user', sessionId: 'session-1' },
|
||||
{
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
uuid: 'init-frame',
|
||||
parentUuid: null,
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'stream_event', uuid: 'stream-frame', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'sidechain-assistant', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'marker leaf is missing from the session graph'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a main leaf whose ancestry crosses a subagent sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-sidechain-ancestry-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'sidechain-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
isSidechain: true
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'sidechain-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'not on the main transcript'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a main leaf whose ancestry crosses a parent-tool-use sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-ancestry-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'not on the main transcript'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a previous cursor descended from a parent-tool-use sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-cursor-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(
|
||||
proveMarkerLeafUuid(transcript, 'session-1', 'main-after-sidechain')
|
||||
).rejects.toThrow('not on the main transcript')
|
||||
})
|
||||
|
||||
it('rejects a latest marker descended from a parent-tool-use cursor sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-cursor-descendant-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'latest-after-sidechain',
|
||||
parentUuid: 'main-after-sidechain',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'latest-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(
|
||||
proveMarkerLeafUuid(transcript, 'session-1', 'main-after-sidechain')
|
||||
).rejects.toThrow('not on the main transcript')
|
||||
})
|
||||
|
||||
it('rejects a post-snapshot descendant whose parent row was observed later', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-post-snapshot-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'descendant',
|
||||
parentUuid: 'previous',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'assistant', uuid: 'previous', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'descendant', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(proveMarkerLeafUuid(transcript, 'session-1', 'previous')).rejects.toThrow(
|
||||
'parent row follows descendant'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -5,21 +5,32 @@
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { agentJournalItemKey } from '../../../shared/agent-session-journal-item-key'
|
||||
import type {
|
||||
AgentSessionJournalIdentity,
|
||||
AgentSessionProviderHandle
|
||||
} from '../../../shared/agent-session-journal-types'
|
||||
import type { NativeChatMessage } from '../../../shared/native-chat-types'
|
||||
import type * as TranscriptLineDecoders from '../transcript-line-decoders'
|
||||
import { createLegacyIdentityTracker } from './journal-legacy-identity'
|
||||
import {
|
||||
appendLegacyTranscriptMessages,
|
||||
importLegacyTranscriptIntoJournal
|
||||
} from './journal-legacy-import'
|
||||
import { importLegacyTranscriptIntoJournal } from './journal-legacy-import'
|
||||
import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from './journal-payload-bounds'
|
||||
import { openAgentSessionJournal } from './journal-store-factory'
|
||||
import type { AgentSessionJournal } from './journal-store'
|
||||
|
||||
// No shipped decoder emits a subagent roster, so the roster bounds are reached by standing one in.
|
||||
const decodedClaudeOverride = vi.hoisted(() => ({ message: null as NativeChatMessage | null }))
|
||||
|
||||
vi.mock('../transcript-line-decoders', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof TranscriptLineDecoders>()
|
||||
return {
|
||||
...actual,
|
||||
decodeClaudeTranscriptLine: (...args: Parameters<typeof actual.decodeClaudeTranscriptLine>) =>
|
||||
decodedClaudeOverride.message ?? actual.decodeClaudeTranscriptLine(...args)
|
||||
}
|
||||
})
|
||||
|
||||
const CLAUDE_SESSION = '29eb22a4-6a5f-4f21-9b0c-1d7f3a2e5c88'
|
||||
const CODEX_SESSION = '019fd532-7c11-7a90-b6de-4e1a2c3d5f60'
|
||||
|
||||
@@ -189,6 +200,7 @@ beforeEach(async () => {
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
decodedClaudeOverride.message = null
|
||||
await rm(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
@@ -297,46 +309,6 @@ describe('claude import', () => {
|
||||
})
|
||||
|
||||
describe('codex import', () => {
|
||||
it('upserts live transcript messages without rolling the structured epoch', async () => {
|
||||
const journal = await open('codex', CODEX_SESSION)
|
||||
const epoch = journal.epoch
|
||||
const message = {
|
||||
id: 'live-tui-message',
|
||||
role: 'assistant' as const,
|
||||
blocks: [{ type: 'text' as const, text: 'first version' }],
|
||||
timestamp: 1_800_000_000_000,
|
||||
source: 'transcript' as const
|
||||
}
|
||||
|
||||
await appendLegacyTranscriptMessages({
|
||||
journal,
|
||||
agent: 'codex',
|
||||
sessionId: CODEX_SESSION,
|
||||
fence: 2,
|
||||
messages: [message]
|
||||
})
|
||||
await appendLegacyTranscriptMessages({
|
||||
journal,
|
||||
agent: 'codex',
|
||||
sessionId: CODEX_SESSION,
|
||||
fence: 2,
|
||||
messages: [{ ...message, blocks: [{ type: 'text', text: 'final version' }] }]
|
||||
})
|
||||
|
||||
expect(journal.epoch).toBe(epoch)
|
||||
expect(journal.snapshot().items).toMatchObject([
|
||||
{
|
||||
itemId: legacyKey('live-tui-message'),
|
||||
revision: 2,
|
||||
body: {
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
blocks: [{ type: 'text', text: 'final version' }]
|
||||
}
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('keys rollout records in the import-scoped namespace, not as app-server ordinals', async () => {
|
||||
const filePath = await writeFixture('rollout.jsonl', CODEX_LINES)
|
||||
const journal = await open('codex', CODEX_SESSION)
|
||||
@@ -396,6 +368,32 @@ describe('codex import', () => {
|
||||
})
|
||||
})
|
||||
|
||||
type RosterAgents = Extract<
|
||||
NativeChatMessage['blocks'][number],
|
||||
{ type: 'subagent-group' }
|
||||
>['agents']
|
||||
|
||||
async function importDecodedRoster(agents: RosterAgents): Promise<AgentSessionJournal> {
|
||||
decodedClaudeOverride.message = {
|
||||
id: 'legacy-roster',
|
||||
role: 'assistant',
|
||||
timestamp: null,
|
||||
source: 'transcript',
|
||||
blocks: [{ type: 'subagent-group', groupId: 'group-1', agents }]
|
||||
}
|
||||
const filePath = await writeFixture('claude-roster.jsonl', [CLAUDE_LINES[2]])
|
||||
const journal = await open('claude', CLAUDE_SESSION)
|
||||
const result = await importLegacyTranscriptIntoJournal({
|
||||
journal,
|
||||
agent: 'claude',
|
||||
sessionId: CLAUDE_SESSION,
|
||||
fence: 1,
|
||||
options: { filePath }
|
||||
})
|
||||
expect(result).toMatchObject({ ok: true, imported: 1 })
|
||||
return journal
|
||||
}
|
||||
|
||||
describe('payload bounds on import', () => {
|
||||
it('marks a clipped tool result and discards the remainder', async () => {
|
||||
const output = 'y'.repeat(64 * 1024)
|
||||
@@ -436,32 +434,13 @@ describe('payload bounds on import', () => {
|
||||
it('bounds an imported subagent roster by entry count, label and id', async () => {
|
||||
// The import reads an untrusted file: nothing upstream bounded either string.
|
||||
const oversized = 'z'.repeat(20 * 1024)
|
||||
const journal = await open('claude', CLAUDE_SESSION)
|
||||
await appendLegacyTranscriptMessages({
|
||||
journal,
|
||||
agent: 'claude',
|
||||
sessionId: CLAUDE_SESSION,
|
||||
fence: 1,
|
||||
messages: [
|
||||
{
|
||||
id: 'legacy-roster',
|
||||
role: 'assistant',
|
||||
timestamp: null,
|
||||
source: 'transcript',
|
||||
blocks: [
|
||||
{
|
||||
type: 'subagent-group',
|
||||
groupId: 'group-1',
|
||||
agents: Array.from({ length: 80 }, (_, index) => ({
|
||||
id: index === 0 ? oversized : `task-${index}`,
|
||||
label: index === 0 ? oversized : `label-${index}`,
|
||||
state: 'working' as const
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
const journal = await importDecodedRoster(
|
||||
Array.from({ length: 80 }, (_, index) => ({
|
||||
id: index === 0 ? oversized : `task-${index}`,
|
||||
label: index === 0 ? oversized : `label-${index}`,
|
||||
state: 'working' as const
|
||||
}))
|
||||
)
|
||||
|
||||
const body = journal.snapshot().items[0]?.body
|
||||
const block = body?.kind === 'message' ? body.blocks[0] : undefined
|
||||
@@ -478,31 +457,10 @@ describe('payload bounds on import', () => {
|
||||
// The id is the roster key: it takes the same bounded-id format the wires
|
||||
// use, so a later wire bound is a no-op instead of a second, merging clip.
|
||||
const head = 'y'.repeat(512)
|
||||
const journal = await open('claude', CLAUDE_SESSION)
|
||||
await appendLegacyTranscriptMessages({
|
||||
journal,
|
||||
agent: 'claude',
|
||||
sessionId: CLAUDE_SESSION,
|
||||
fence: 1,
|
||||
messages: [
|
||||
{
|
||||
id: 'legacy-roster-collision',
|
||||
role: 'assistant',
|
||||
timestamp: null,
|
||||
source: 'transcript',
|
||||
blocks: [
|
||||
{
|
||||
type: 'subagent-group',
|
||||
groupId: 'group-1',
|
||||
agents: [
|
||||
{ id: `${head}-one`, label: 'Audit', state: 'working' as const },
|
||||
{ id: `${head}-two`, label: 'Audit', state: 'working' as const }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
const journal = await importDecodedRoster([
|
||||
{ id: `${head}-one`, label: 'Audit', state: 'working' as const },
|
||||
{ id: `${head}-two`, label: 'Audit', state: 'working' as const }
|
||||
])
|
||||
|
||||
const body = journal.snapshot().items[0]?.body
|
||||
const block = body?.kind === 'message' ? body.blocks[0] : undefined
|
||||
|
||||
@@ -62,30 +62,6 @@ export type LegacyImportResult =
|
||||
}
|
||||
| { ok: false; error: string }
|
||||
|
||||
export async function appendLegacyTranscriptMessages(input: {
|
||||
journal: AgentSessionJournal
|
||||
agent: AgentType
|
||||
sessionId: string
|
||||
fence: number
|
||||
messages: NativeChatMessage[]
|
||||
}): Promise<number> {
|
||||
let appended = 0
|
||||
for (const message of input.messages) {
|
||||
await input.journal.appendItem(
|
||||
{
|
||||
provider: 'legacy',
|
||||
agent: input.agent,
|
||||
sessionId: input.sessionId,
|
||||
recordId: message.id
|
||||
},
|
||||
legacyItemBody(message, DEFAULT_JOURNAL_PAYLOAD_LIMITS),
|
||||
{ fence: input.fence, observedAt: message.timestamp ?? undefined }
|
||||
)
|
||||
appended += 1
|
||||
}
|
||||
return appended
|
||||
}
|
||||
|
||||
export async function importLegacyTranscriptIntoJournal(input: {
|
||||
journal: AgentSessionJournal
|
||||
agent: AgentType
|
||||
|
||||
@@ -3,10 +3,6 @@ import { tmpdir } from 'node:os'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ClaudeTranscriptTailIncompleteError,
|
||||
proveClaudeTranscriptBranch
|
||||
} from '../claude/claude-transcript-branch-proof'
|
||||
import { resolveSessionFilePath } from './session-file-resolver'
|
||||
|
||||
let tempRoots: string[] = []
|
||||
@@ -16,16 +12,6 @@ afterEach(async () => {
|
||||
tempRoots = []
|
||||
})
|
||||
|
||||
async function readClaudeTranscriptLeafUuid(
|
||||
transcriptPath: string,
|
||||
providerSessionId: string,
|
||||
previousLeafUuid: string | null = null
|
||||
): Promise<string> {
|
||||
return (
|
||||
await proveClaudeTranscriptBranch({ transcriptPath, providerSessionId, previousLeafUuid })
|
||||
).leafUuid
|
||||
}
|
||||
|
||||
async function makeRoot(prefix: string): Promise<string> {
|
||||
const root = await mkdtemp(join(tmpdir(), prefix))
|
||||
tempRoots.push(root)
|
||||
@@ -41,311 +27,6 @@ function restoreEnv(key: string, previous: string | undefined): void {
|
||||
}
|
||||
|
||||
describe('resolveSessionFilePath', () => {
|
||||
it('reads Claude last-prompt leaf metadata as the durable branch marker', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-leaf-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'leaf-old', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'leaf-current',
|
||||
parentUuid: 'leaf-old',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'leaf-current', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1', 'leaf-old')).resolves.toBe(
|
||||
'leaf-current'
|
||||
)
|
||||
})
|
||||
|
||||
it('fails closed when a Claude transcript has no branch marker', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-no-leaf-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
'{"type":"assistant","uuid":"not-a-leaf","parentUuid":null,"sessionId":"session-1"}\n',
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'missing last-prompt marker'
|
||||
)
|
||||
})
|
||||
|
||||
it('distinguishes an incomplete final Claude JSONL record from durable malformed content', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-torn-tail-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(transcript, '{"type":"last-prompt"', 'utf8')
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.toBeInstanceOf(
|
||||
ClaudeTranscriptTailIncompleteError
|
||||
)
|
||||
|
||||
await writeFile(transcript, '{"type":"last-prompt"\n', 'utf8')
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.not.toBeInstanceOf(
|
||||
ClaudeTranscriptTailIncompleteError
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses a Claude marker on a sibling branch', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-sibling-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'root', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'expected', parentUuid: 'root', sessionId: 'session-1' },
|
||||
{ type: 'system', uuid: 'sibling', parentUuid: 'root', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'sibling', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1', 'expected')).rejects.toThrow(
|
||||
'sibling branch'
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses missing and cyclic Claude parent chains', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-invalid-ancestry-')
|
||||
const missing = join(root, 'missing.jsonl')
|
||||
const cycle = join(root, 'cycle.jsonl')
|
||||
await writeFile(
|
||||
missing,
|
||||
[
|
||||
{ type: 'user', uuid: 'expected', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'leaf', parentUuid: 'absent', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'leaf', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
await writeFile(
|
||||
cycle,
|
||||
[
|
||||
{ type: 'user', uuid: 'expected', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'assistant', uuid: 'left', parentUuid: 'right', sessionId: 'session-1' },
|
||||
{ type: 'system', uuid: 'right', parentUuid: 'left', sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'right', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(missing, 'session-1', 'expected')).rejects.toThrow(
|
||||
'missing ancestor absent'
|
||||
)
|
||||
await expect(readClaudeTranscriptLeafUuid(cycle, 'session-1', 'expected')).rejects.toThrow(
|
||||
'cycle in parentUuid ancestry'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects non-transcript and sidechain UUIDs as the durable leaf', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-leaf-filter-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'sidechain-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
isSidechain: true
|
||||
},
|
||||
{ type: 'result', uuid: 'result-frame', parentUuid: 'main-user', sessionId: 'session-1' },
|
||||
{
|
||||
type: 'system',
|
||||
subtype: 'init',
|
||||
uuid: 'init-frame',
|
||||
parentUuid: null,
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'stream_event', uuid: 'stream-frame', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'sidechain-assistant', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'marker leaf is missing from the session graph'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a main leaf whose ancestry crosses a subagent sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-sidechain-ancestry-')
|
||||
const transcript = join(root, 'session.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'sidechain-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
isSidechain: true
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'sidechain-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'not on the main transcript'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a main leaf whose ancestry crosses a parent-tool-use sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-ancestry-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1')).rejects.toThrow(
|
||||
'not on the main transcript'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a previous cursor descended from a parent-tool-use sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-cursor-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'main-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(
|
||||
readClaudeTranscriptLeafUuid(transcript, 'session-1', 'main-after-sidechain')
|
||||
).rejects.toThrow('not on the main transcript')
|
||||
})
|
||||
|
||||
it('rejects a latest marker descended from a parent-tool-use cursor sidechain', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-parent-tool-cursor-descendant-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{ type: 'user', uuid: 'main-user', parentUuid: null, sessionId: 'session-1' },
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'subagent-assistant',
|
||||
parentUuid: 'main-user',
|
||||
sessionId: 'session-1',
|
||||
parent_tool_use_id: 'tool-use-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'main-after-sidechain',
|
||||
parentUuid: 'subagent-assistant',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'latest-after-sidechain',
|
||||
parentUuid: 'main-after-sidechain',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'last-prompt', leafUuid: 'latest-after-sidechain', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(
|
||||
readClaudeTranscriptLeafUuid(transcript, 'session-1', 'main-after-sidechain')
|
||||
).rejects.toThrow('not on the main transcript')
|
||||
})
|
||||
|
||||
it('rejects a post-snapshot descendant whose parent row was observed later', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-post-snapshot-')
|
||||
const transcript = join(root, 'transcript.jsonl')
|
||||
await writeFile(
|
||||
transcript,
|
||||
[
|
||||
{
|
||||
type: 'assistant',
|
||||
uuid: 'descendant',
|
||||
parentUuid: 'previous',
|
||||
sessionId: 'session-1'
|
||||
},
|
||||
{ type: 'assistant', uuid: 'previous', parentUuid: null, sessionId: 'session-1' },
|
||||
{ type: 'last-prompt', leafUuid: 'descendant', sessionId: 'session-1' }
|
||||
]
|
||||
.map((record) => JSON.stringify(record))
|
||||
.join('\n'),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
await expect(readClaudeTranscriptLeafUuid(transcript, 'session-1', 'previous')).rejects.toThrow(
|
||||
'parent row follows descendant'
|
||||
)
|
||||
})
|
||||
|
||||
it('globs Claude project subdirs for <sessionId>.jsonl', async () => {
|
||||
const root = await makeRoot('orca-native-chat-resolve-claude-')
|
||||
const claudeProjectsDir = join(root, 'claude-projects')
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// that ship. The fake app-server answers the same JSON-RPC calls the real one
|
||||
// does and pushes the same notifications and blocking requests back.
|
||||
|
||||
import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -31,7 +31,7 @@ import type {
|
||||
import { attachFingerprintFields } from '../native-chat/agent-session-wire/structured-agent-session-attach'
|
||||
import { getStructuredAgentSessionHost } from '../native-chat/agent-session-wire/structured-agent-session-registry'
|
||||
import { journalDirectoryFor } from '../native-chat/agent-session-journal/journal-paths'
|
||||
import { appendLegacyTranscriptMessages } from '../native-chat/agent-session-journal/journal-legacy-import'
|
||||
import { importLegacyTranscriptIntoJournal } from '../native-chat/agent-session-journal/journal-legacy-import'
|
||||
import type { AgentSessionJournal } from '../native-chat/agent-session-journal/journal-store'
|
||||
import { createTrackedJournalOpener } from '../native-chat/agent-session-journal/journal-store-test-open'
|
||||
import type { OrcaRuntimeService } from './orca-runtime'
|
||||
@@ -407,20 +407,21 @@ describe('a structured codex session over agentSession.*', () => {
|
||||
identity,
|
||||
journalDir: journalDirectoryFor(root, identity)
|
||||
})
|
||||
await appendLegacyTranscriptMessages({
|
||||
const rollout = join(root, 'legacy-rollout.jsonl')
|
||||
await writeFile(
|
||||
rollout,
|
||||
`${JSON.stringify({
|
||||
type: 'event_msg',
|
||||
timestamp: '2026-08-05T10:00:02.000Z',
|
||||
payload: { type: 'user_message', message: 'legacy question', kind: 'plain' }
|
||||
})}\n`
|
||||
)
|
||||
await importLegacyTranscriptIntoJournal({
|
||||
journal,
|
||||
agent: 'codex',
|
||||
sessionId: THREAD,
|
||||
fence: 0,
|
||||
messages: [
|
||||
{
|
||||
id: 'legacy-user-1',
|
||||
role: 'user',
|
||||
source: 'transcript',
|
||||
timestamp: 1_800_000_000_000,
|
||||
blocks: [{ type: 'text', text: 'legacy question' }]
|
||||
}
|
||||
]
|
||||
options: { filePath: rollout }
|
||||
})
|
||||
|
||||
const created = await ok<{ page: { items: AgentJournalRenderItem[] } }>(
|
||||
|
||||
Reference in New Issue
Block a user