mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 00:02:35 +00:00
251 lines
10 KiB
Diff
251 lines
10 KiB
Diff
===================================================================
|
|
--- a/src/main/claude/claude-transcript-branch-proof.ts
|
|
+++ b/src/main/claude/claude-transcript-branch-proof.ts
|
|
@@ -1,1 +1,2 @@
|
|
-import { readFile } from 'node:fs/promises'
|
|
+import { open } from 'node:fs/promises'
|
|
+import { splitTranscriptStreamLines } from '../native-chat/transcript-stream-lines'
|
|
@@ -4,0 +6,6 @@
|
|
+class ClaudeTranscriptMarkerMissingError extends Error {
|
|
+ constructor() {
|
|
+ super('Claude transcript branch proof failed: missing last-prompt marker')
|
|
+ }
|
|
+}
|
|
+
|
|
@@ -82,2 +89,1 @@
|
|
-export function proveClaudeTranscriptBranchFromJsonl(input: {
|
|
- contents: string
|
|
+type BranchProofInput = {
|
|
@@ -87,1 +93,3 @@
|
|
-}): ClaudeTranscriptBranchProof {
|
|
+}
|
|
+
|
|
+function createBranchProof(input: BranchProofInput) {
|
|
@@ -91,2 +99,3 @@
|
|
- const lines = input.contents.split('\n')
|
|
- for (const [index, line] of lines.entries()) {
|
|
+ return { add, finish }
|
|
+
|
|
+ function add(line: string, index: number, terminated: boolean): void {
|
|
@@ -94,1 +103,1 @@
|
|
- continue
|
|
+ return
|
|
@@ -100,1 +109,1 @@
|
|
- if (index === lines.length - 1 && !input.contents.endsWith('\n')) {
|
|
+ if (!terminated) {
|
|
@@ -107,0 +117,1 @@
|
|
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The parsed value is a non-array object checked above.
|
|
@@ -120,1 +130,1 @@
|
|
- continue
|
|
+ return
|
|
@@ -149,14 +159,4 @@
|
|
- if (!leafUuid) {
|
|
- throw transcriptError('missing last-prompt marker')
|
|
- }
|
|
- const leaf = nodes.get(leafUuid)
|
|
- if (!leaf || leaf.sessionId !== input.providerSessionId || leaf.disallowedLeaf) {
|
|
- throw transcriptError('marker leaf is missing from the session graph')
|
|
- }
|
|
- if (leaf.lineIndex > leafMarkerLineIndex) {
|
|
- throw transcriptError('marker precedes its leaf record')
|
|
- }
|
|
- const previousLeafUuid = input.previousLeafUuid
|
|
- if (input.intentionalRewindUuid !== undefined) {
|
|
- if (leafUuid !== input.intentionalRewindUuid || !input.previousLeafUuid) {
|
|
- throw transcriptError('rewind target does not match the observed leaf')
|
|
+
|
|
+ function finish(): ClaudeTranscriptBranchProof {
|
|
+ if (!leafUuid) {
|
|
+ throw new ClaudeTranscriptMarkerMissingError()
|
|
@@ -164,6 +164,11 @@
|
|
- proveMainLineAncestry(nodes, input.previousLeafUuid, input.providerSessionId)
|
|
- proveAppendOrder(nodes)
|
|
- let ancestor = nodes.get(input.previousLeafUuid)?.parentUuid ?? null
|
|
- for (let depth = 0; ancestor !== null && depth < MAX_CLAUDE_TRANSCRIPT_ANCESTRY; depth += 1) {
|
|
- if (ancestor === leafUuid) {
|
|
- return { leafUuid, relation: 'intentional-rewind' }
|
|
+ const leaf = nodes.get(leafUuid)
|
|
+ if (!leaf || leaf.sessionId !== input.providerSessionId || leaf.disallowedLeaf) {
|
|
+ throw transcriptError('marker leaf is missing from the session graph')
|
|
+ }
|
|
+ if (leaf.lineIndex > leafMarkerLineIndex) {
|
|
+ throw transcriptError('marker precedes its leaf record')
|
|
+ }
|
|
+ const previousLeafUuid = input.previousLeafUuid
|
|
+ if (input.intentionalRewindUuid !== undefined) {
|
|
+ if (leafUuid !== input.intentionalRewindUuid || !input.previousLeafUuid) {
|
|
+ throw transcriptError('rewind target does not match the observed leaf')
|
|
@@ -171,1 +176,10 @@
|
|
- ancestor = nodes.get(ancestor)?.parentUuid ?? null
|
|
+ proveMainLineAncestry(nodes, input.previousLeafUuid, input.providerSessionId)
|
|
+ proveAppendOrder(nodes)
|
|
+ let ancestor = nodes.get(input.previousLeafUuid)?.parentUuid ?? null
|
|
+ for (let depth = 0; ancestor !== null && depth < MAX_CLAUDE_TRANSCRIPT_ANCESTRY; depth += 1) {
|
|
+ if (ancestor === leafUuid) {
|
|
+ return { leafUuid, relation: 'intentional-rewind' }
|
|
+ }
|
|
+ ancestor = nodes.get(ancestor)?.parentUuid ?? null
|
|
+ }
|
|
+ throw transcriptError('rewind target is not an ancestor of the previous cursor')
|
|
@@ -173,31 +187,7 @@
|
|
- throw transcriptError('rewind target is not an ancestor of the previous cursor')
|
|
- }
|
|
- if (!previousLeafUuid) {
|
|
- proveMainLineAncestry(nodes, leafUuid, input.providerSessionId)
|
|
- // A branch proof is based on an append-only snapshot. A child that appears
|
|
- // before its claimed parent is not a post-snapshot descendant observation;
|
|
- // accepting that graph would turn reordered/torn rows into durable ancestry.
|
|
- proveAppendOrder(nodes)
|
|
- return { leafUuid, relation: 'initial' }
|
|
- }
|
|
- const previous = nodes.get(previousLeafUuid)
|
|
- if (!previous) {
|
|
- throw new ClaudeTranscriptPreviousCursorMissingError()
|
|
- }
|
|
- if (previous.sessionId !== input.providerSessionId || previous.disallowedLeaf) {
|
|
- throw transcriptError('previous cursor is not on the main transcript')
|
|
- }
|
|
- // The latest marker can be equal to, or descend from, a sampled cursor. In
|
|
- // either case prove the sampled cursor's own ancestry before accepting it;
|
|
- // otherwise a cursor that descended through a parent-tool-use sidechain
|
|
- // could be persisted and resumed as if it were on the main transcript.
|
|
- proveMainLineAncestry(nodes, previousLeafUuid, input.providerSessionId)
|
|
- if (leafUuid === previousLeafUuid) {
|
|
- proveAppendOrder(nodes)
|
|
- return { leafUuid, relation: 'same' }
|
|
- }
|
|
- const visited = new Set<string>()
|
|
- let cursor: string | null = leafUuid
|
|
- for (let depth = 0; cursor !== null && depth < MAX_CLAUDE_TRANSCRIPT_ANCESTRY; depth += 1) {
|
|
- if (visited.has(cursor)) {
|
|
- throw transcriptError('cycle in parentUuid ancestry')
|
|
+ if (!previousLeafUuid) {
|
|
+ proveMainLineAncestry(nodes, leafUuid, input.providerSessionId)
|
|
+ // A branch proof is based on an append-only snapshot. A child that appears
|
|
+ // before its claimed parent is not a post-snapshot descendant observation;
|
|
+ // accepting that graph would turn reordered/torn rows into durable ancestry.
|
|
+ proveAppendOrder(nodes)
|
|
+ return { leafUuid, relation: 'initial' }
|
|
@@ -205,4 +195,3 @@
|
|
- visited.add(cursor)
|
|
- const node = nodes.get(cursor)
|
|
- if (!node || node.sessionId !== input.providerSessionId) {
|
|
- throw transcriptError(`missing ancestor ${cursor}`)
|
|
+ const previous = nodes.get(previousLeafUuid)
|
|
+ if (!previous) {
|
|
+ throw new ClaudeTranscriptPreviousCursorMissingError()
|
|
@@ -210,2 +199,2 @@
|
|
- if (node.disallowedLeaf) {
|
|
- throw transcriptError(`ancestor ${cursor} is not on the main transcript`)
|
|
+ if (previous.sessionId !== input.providerSessionId || previous.disallowedLeaf) {
|
|
+ throw transcriptError('previous cursor is not on the main transcript')
|
|
@@ -213,2 +202,6 @@
|
|
- cursor = node.parentUuid
|
|
- if (cursor === previousLeafUuid) {
|
|
+ // The latest marker can be equal to, or descend from, a sampled cursor. In
|
|
+ // either case prove the sampled cursor's own ancestry before accepting it;
|
|
+ // otherwise a cursor that descended through a parent-tool-use sidechain
|
|
+ // could be persisted and resumed as if it were on the main transcript.
|
|
+ proveMainLineAncestry(nodes, previousLeafUuid, input.providerSessionId)
|
|
+ if (leafUuid === previousLeafUuid) {
|
|
@@ -216,1 +209,1 @@
|
|
- return { leafUuid, relation: 'descendant' }
|
|
+ return { leafUuid, relation: 'same' }
|
|
@@ -217,0 +211,24 @@
|
|
+ const visited = new Set<string>()
|
|
+ let cursor: string | null = leafUuid
|
|
+ for (let depth = 0; cursor !== null && depth < MAX_CLAUDE_TRANSCRIPT_ANCESTRY; depth += 1) {
|
|
+ if (visited.has(cursor)) {
|
|
+ throw transcriptError('cycle in parentUuid ancestry')
|
|
+ }
|
|
+ visited.add(cursor)
|
|
+ const node = nodes.get(cursor)
|
|
+ if (!node || node.sessionId !== input.providerSessionId) {
|
|
+ throw transcriptError(`missing ancestor ${cursor}`)
|
|
+ }
|
|
+ if (node.disallowedLeaf) {
|
|
+ throw transcriptError(`ancestor ${cursor} is not on the main transcript`)
|
|
+ }
|
|
+ cursor = node.parentUuid
|
|
+ if (cursor === previousLeafUuid) {
|
|
+ proveAppendOrder(nodes)
|
|
+ return { leafUuid, relation: 'descendant' }
|
|
+ }
|
|
+ }
|
|
+ if (cursor !== null) {
|
|
+ throw transcriptError('ancestry exceeds the bounded proof limit')
|
|
+ }
|
|
+ throw transcriptError('latest marker is on a sibling branch')
|
|
@@ -219,2 +236,9 @@
|
|
- if (cursor !== null) {
|
|
- throw transcriptError('ancestry exceeds the bounded proof limit')
|
|
+}
|
|
+
|
|
+export function proveClaudeTranscriptBranchFromJsonl(
|
|
+ input: BranchProofInput & { contents: string }
|
|
+): ClaudeTranscriptBranchProof {
|
|
+ const proof = createBranchProof(input)
|
|
+ const lines = input.contents.split('\n')
|
|
+ for (const [index, line] of lines.entries()) {
|
|
+ proof.add(line, index, index < lines.length - 1)
|
|
@@ -222,1 +246,1 @@
|
|
- throw transcriptError('latest marker is on a sibling branch')
|
|
+ return proof.finish()
|
|
@@ -225,12 +249,44 @@
|
|
-export async function proveClaudeTranscriptBranch(input: {
|
|
- transcriptPath: string
|
|
- providerSessionId: string
|
|
- previousLeafUuid: string | null
|
|
- intentionalRewindUuid?: string
|
|
-}): Promise<ClaudeTranscriptBranchProof> {
|
|
- return proveClaudeTranscriptBranchFromJsonl({
|
|
- contents: await readFile(input.transcriptPath, 'utf8'),
|
|
- providerSessionId: input.providerSessionId,
|
|
- previousLeafUuid: input.previousLeafUuid,
|
|
- intentionalRewindUuid: input.intentionalRewindUuid
|
|
- })
|
|
+export async function proveClaudeTranscriptBranch(
|
|
+ input: BranchProofInput & { transcriptPath: string }
|
|
+): Promise<ClaudeTranscriptBranchProof> {
|
|
+ const handle = await open(input.transcriptPath, 'r')
|
|
+ try {
|
|
+ let size = (await handle.stat()).size
|
|
+ let refreshed = false
|
|
+ while (true) {
|
|
+ const proof = createBranchProof(input)
|
|
+ let index = 0
|
|
+ try {
|
|
+ if (size > 0) {
|
|
+ const stream = handle.createReadStream({ start: 0, end: size - 1, autoClose: false })
|
|
+ for await (const record of splitTranscriptStreamLines(stream)) {
|
|
+ proof.add(record.line, index++, record.terminated)
|
|
+ }
|
|
+ }
|
|
+ return proof.finish()
|
|
+ } catch (error) {
|
|
+ if (
|
|
+ !(error instanceof ClaudeTranscriptMarkerMissingError) &&
|
|
+ !(error instanceof ClaudeTranscriptPreviousCursorMissingError) &&
|
|
+ !(error instanceof ClaudeTranscriptTailIncompleteError)
|
|
+ ) {
|
|
+ throw error
|
|
+ }
|
|
+ if (refreshed) {
|
|
+ throw new ClaudeTranscriptTailIncompleteError()
|
|
+ }
|
|
+ const nextSize = await handle.stat().then(
|
|
+ (current) => current.size,
|
|
+ () => size
|
|
+ )
|
|
+ if (nextSize <= size) {
|
|
+ throw error
|
|
+ }
|
|
+ // Finish an already-appended repair without making the caller retry.
|
|
+ size = nextSize
|
|
+ refreshed = true
|
|
+ }
|
|
+ }
|
|
+ } finally {
|
|
+ await handle.close()
|
|
+ }
|