mirror of
https://github.com/stablyai/orca.git
synced 2026-10-12 16:03:03 +00:00
fix(claude): stream transcript branch proofs from a finite prefix
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Claude transcript branch-proof allocation
|
||||
|
||||
Production implementation with preserved intermediate experiments. The proof reads the current source and reconstructs the original reader by reversing `fix.patch` in memory; it does not modify checkout files. The `windowCandidate` result key now names the actual production implementation.
|
||||
|
||||
The original main-process branch reader loads the complete JSONL file, splits every line, and retains the source while constructing the ancestry graph. Message bodies are discarded after each parse. This is transient allocation, not evidence of memory retained after the proof returns.
|
||||
|
||||
## Reproduce
|
||||
|
||||
From the primary checkout with its existing dependencies:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/claude-branch-proof-streaming/reproduce.cjs /tmp/claude-branch-streaming-results.json
|
||||
ORCA_BACKGROUND_LAUNCH=1 node docs/audits/claude-branch-proof-streaming/regressions.mjs /tmp/claude-branch-streaming-regressions.json
|
||||
```
|
||||
|
||||
No install, Electron window, network call, or product edit occurs. The existing `diff` package reverses the patch, and the original source hash plus unique experiment anchors reject drift. Modules are built with the installed esbuild into a temporary directory, loaded with normal `require`, and removed with their require-cache entries in `finally`. The original allocation observation is preserved in `baseline-initial-results.json`; subsequent comparisons do not overwrite it.
|
||||
|
||||
## Production reachability and version
|
||||
|
||||
- `structured-agent-session-runtime.ts:287` constructs the local Claude runtime adapter in main. The adapter's `readTranscriptLeaf` resolves the pinned account's `projects` transcript and calls `session-file-resolver.ts:168`, which delegates to the full branch proof.
|
||||
- `claude-structured-session-close.ts:100` first awaits successful connection close, then reads the transcript at line 116 before persisting the handle and removing the session. A failed proof preserves the previously observed leaf. `claude-structured-session-adapter.ts:182` also invokes this reader when persisting a handle after unexpected exit.
|
||||
- `orca-runtime-stop-structured-session-process.ts:184` calls the full reader during local resumed-PTY verification. A torn final record retries on a 100ms cadence within its 15-second deadline, re-reading the file. These are sequential reads; the proof does not establish accumulating retained copies or the actual number of attempts in any incident.
|
||||
- The unrestricted `readFile` and close/resume wiring already exist in **v1.4.198**. The resume verifier is identical in that tag. Current explicit rewind handling was added later; it is a current compatibility control, not attributed to the tag. `provenance.json` records both versions' source hashes and exact call-site lines.
|
||||
|
||||
This is source reachability and synthetic allocation evidence. No affected user's transcript size, concurrent-close count, or triggering call is known, so it does not independently explain #19831 or another reported OOM.
|
||||
|
||||
## Candidates and complete proof semantics
|
||||
|
||||
Both candidates extract the existing parser and final graph validation into one accumulator shared by the synchronous string API and asynchronous file reader. They reuse `splitTranscriptStreamLines` with its default unlimited record size. Every UUID remains in the metadata graph; the existing 10,000-ancestor limit remains unchanged. No file-size quota, record limit, tail-only selection, or message eviction is added.
|
||||
|
||||
`candidate-transform.json` is the initial **open-ended stream** experiment. It reduces allocation on static files, but changes concurrent-append observations and is not suitable for promotion as written.
|
||||
|
||||
`window-candidate-transform.json` preserves the reviewed **same-handle initial-byte window** experiment that was promoted: open once, stat that handle, stream bytes 0 through initial size minus one, and await handle closure in `finally`. An initially empty prefix fails the existing missing-marker proof without opening a stream. This bounds the observation to a finite prefix without imposing a fixed transcript quota. The stream never reopens a pathname after stat. The current proof and regressions execute the checked-out production implementation for this variant.
|
||||
|
||||
The current window candidate then handles two specific absence errors: an internal typed missing-marker error (public name/text unchanged) or the existing missing-previous-cursor error. Only if a second stat of that same open handle proves growth does it report `ClaudeTranscriptTailIncompleteError`, allowing the existing caller retry. Failed stat, shrink, no growth, path replacement, and static invalid files retain the original error. Parser conflicts, wrong sessions, global append order, ancestry, and rewind failures are never reclassified. A successful prefix stays successful without observing later rows. The initial window-only experiment and its liveness gap are preserved separately in `initial-window-candidate-transform.json` and `initial-window-results.json`.
|
||||
|
||||
The shared graph still checks duplicate UUID conflicts, every node's append order (including disconnected branches), old ancestry, sidechains, session identity, last marker ordering, missing previous cursor, and intentional rewind. UTF-8 decoding and terminated versus unterminated record information come from the existing line splitter. Malformed middle records remain ordinary errors; an invalid unterminated final record remains `ClaudeTranscriptTailIncompleteError`.
|
||||
|
||||
## Concurrent append boundary
|
||||
|
||||
This is an explicit race-behavior change, not exact emulation of Node's read chunk sizes. On Node 26.6.0, the actual UTF-8 `readFile` excludes appended rows for a small file or an exactly 512KiB prefix, but includes them for a tested 512KiB+100B prefix or an initially empty file. The open-ended candidate admits them in every case. The window candidate consistently uses the initial byte prefix.
|
||||
|
||||
The source's existing append-only-snapshot proof contract supports that finite prefix. An appended marker cannot repair a prefix with a missing previous cursor/marker or incomplete final record. A previously proved descendant or intentional rewind remains tied to that prefix even if a newer marker is appended during reading. This does not provide an atomic filesystem snapshot: concurrent in-place mutation/truncation can still change bytes inside the window, as with the existing reader.
|
||||
|
||||
Caller behavior differs by error: the TUI resume verifier retries only `ClaudeTranscriptTailIncompleteError`; a static missing marker is fatal there. The separate `readClaudeTranscriptLeafWithReproof` wrapper may retry a static missing previous cursor from the root. Growth classification keeps the original cursor during a retry instead of triggering that root reproof while new bytes arrive. Close catches proof errors and preserves the previously observed leaf. The candidate adds no timer or retry loop; it reuses the existing incomplete-tail classification only on the two growth-proven absence failures. An append after the final stat can still miss this observation; no atomic filesystem snapshot or retry guarantee is claimed.
|
||||
|
||||
The active-writer ordering is reachable: fresh TUI launch waits for idle and a recent matching provider hook but has no transcript flush barrier; recovery and reproof invoke the same verifier on a live owner without an idle wait. No captured CLI transcript establishes how often an initially empty file occurs at exactly this boundary. The artifact's 10 verifier cases execute the exact current method body with controlled live-owner, hook, and path-resolution ports, the actual branch reader, and the actual 100ms timer. Growing empty/missing-marker/missing-previous cases succeed on attempt two. Static absence and growing conflict/session/order failures stay fatal on attempt one. This is an actual-method fixture, not a full running Claude process.
|
||||
|
||||
`growth.cjs` injects real-file append, rename/replacement, deletion, and truncation after the native stat captures its size (or before the first read for the open-ended experiment). It also injects stat/read failures. Actual descriptor closure is checked. The initial open-ended reader closes asynchronously after an early parse error; the window candidate awaits closure before resolving/rejecting, including empty/error cases. The fixture uses a private Node binding only to make these timing boundaries deterministic; it is not production code or a public API proposal.
|
||||
|
||||
## Evidence and limits
|
||||
|
||||
`results.json` contains 51 static real-file cases compared across file and string APIs for all three variants, 24 growth/lifecycle controls, 10 verifier cases, and sampled allocation. `regression-results.json` records 68 existing resolver, rewind, structured recovery, and history-window tests passing on each source variant (204 tests total).
|
||||
|
||||
The production regression file, `src/main/claude/claude-transcript-branch-streaming.test.ts`, adds 23 focused real-file tests for prefix bounds, UTF-8/old ancestors, growth classification, successful rewind, malformed data, handle identity, and awaited cleanup. The reused splitter/framer already exists unchanged in main commit `291b4ddd6f`; unrelated source-budget additions to `decodeTranscriptStream` are not required by this change.
|
||||
|
||||
The allocation fixture writes real files incrementally, uses realistic 36-character UUIDs and session IDs, then invokes the actual bundled reader. Forced GC at each actual `JSON.parse` boundary measures live bytes while the source is in use. A 32MiB file containing 64 bodies of 512KiB needs about **34.6MB** additional live heap in the original reader versus about **1.2MB** in the streaming candidates. After return, both release nearly all of that allocation. Exact figures and source/bundle hashes are recorded in JSON.
|
||||
|
||||
This is not RSS, a natural peak, or a throughput benchmark; sampling excludes the native read buffer's possible peak before parsing. GC noise can make retained deltas negative. A single 8MiB record still uses about **16.8MB** under all variants. The complete UUID graph still grows with record count, and exceptionally large identity strings remain accepted. Streaming bounds retained message payload to the largest current record plus framing buffers; it is not a total memory cap. This proof does not justify any new quota or incident attribution.
|
||||
|
||||
A separate simple duration control reads one stable 2.79MB file containing 8,193 small records. It uses two warm rounds and five measured rounds in alternating order, without GC during timing. Node 26 medians were 7.92ms before and 10.06ms after; installed Electron's Node 24 medians were 5.09ms and 6.84ms. This is a modest absolute cost and measurable per-line overhead, not a throughput guarantee. Per-run durations are included rather than setting a fragile timing gate.
|
||||
|
||||
`electron-results.json` repeats the real-file proof under installed Electron 43.7.0 in `ELECTRON_RUN_AS_NODE=1` mode (Node 24.21.0), with no app/window launch. All proof controls pass there too. This is compatibility evidence for the installed runtime, not the exact historical Electron 43.4.1 / Node 24.18.1 binary. On this macOS checkout the command is:
|
||||
|
||||
```sh
|
||||
ELECTRON_RUN_AS_NODE=1 ORCA_BACKGROUND_LAUNCH=1 node_modules/electron/dist/Electron.app/Contents/MacOS/Electron --expose-gc docs/audits/claude-branch-proof-streaming/reproduce.cjs /tmp/claude-branch-streaming-electron.json
|
||||
```
|
||||
|
||||
Other platforms should use their installed Electron executable with the same environment and arguments. No package installation is needed.
|
||||
|
||||
Existing tail-window readers are not substitutes: they omit old ancestors, conflicts, or global append-order evidence. The similarly named `claude-tui-exit.ts` leaf reader finds a tail marker without establishing this full graph proof.
|
||||
@@ -0,0 +1,106 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const sessionId = '11111111-1111-4111-8111-111111111111'
|
||||
const nodeId = (index) => `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`
|
||||
|
||||
function collect() {
|
||||
for (let index = 0; index < 5; index++) {
|
||||
global.gc()
|
||||
}
|
||||
return process.memoryUsage()
|
||||
}
|
||||
|
||||
function writeFixture(file, count, payloadBytes) {
|
||||
const descriptor = fs.openSync(file, 'w')
|
||||
try {
|
||||
for (let index = 0; index < count; index++) {
|
||||
fs.writeSync(
|
||||
descriptor,
|
||||
`${JSON.stringify({
|
||||
type: 'user',
|
||||
uuid: nodeId(index),
|
||||
parentUuid: index === 0 ? null : nodeId(index - 1),
|
||||
sessionId,
|
||||
message: { role: 'user', content: 'x'.repeat(payloadBytes) }
|
||||
})}\n`
|
||||
)
|
||||
}
|
||||
fs.writeSync(
|
||||
descriptor,
|
||||
`${JSON.stringify({ type: 'last-prompt', sessionId, leafUuid: nodeId(count - 1) })}\n`
|
||||
)
|
||||
} finally {
|
||||
fs.closeSync(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
async function sample(prove, file, count) {
|
||||
const before = collect()
|
||||
let peakHeap = before.heapUsed
|
||||
let peakExternal = before.external
|
||||
let records = 0
|
||||
const originalParse = JSON.parse
|
||||
JSON.parse = function (...args) {
|
||||
const row = originalParse(...args)
|
||||
records++
|
||||
const memory = collect()
|
||||
peakHeap = Math.max(peakHeap, memory.heapUsed)
|
||||
peakExternal = Math.max(peakExternal, memory.external)
|
||||
return row
|
||||
}
|
||||
let result
|
||||
try {
|
||||
result = await prove({
|
||||
transcriptPath: file,
|
||||
providerSessionId: sessionId,
|
||||
previousLeafUuid: nodeId(0)
|
||||
})
|
||||
} finally {
|
||||
JSON.parse = originalParse
|
||||
}
|
||||
assert.deepEqual(result, {
|
||||
leafUuid: nodeId(count - 1),
|
||||
relation: count === 1 ? 'same' : 'descendant'
|
||||
})
|
||||
const after = collect()
|
||||
return {
|
||||
records,
|
||||
peakHeapIncrease: peakHeap - before.heapUsed,
|
||||
peakExternalIncrease: peakExternal - before.external,
|
||||
retainedHeapIncrease: after.heapUsed - before.heapUsed,
|
||||
retainedExternalIncrease: after.external - before.external
|
||||
}
|
||||
}
|
||||
|
||||
async function compareAllocation(scratch, modules) {
|
||||
const reports = []
|
||||
for (const [count, payloadBytes] of [
|
||||
[16, 512 * 1024],
|
||||
[64, 512 * 1024],
|
||||
[64, 0],
|
||||
[1, 8 * 1024 * 1024]
|
||||
]) {
|
||||
const file = path.join(scratch, `allocation-${count}-${payloadBytes}.jsonl`)
|
||||
writeFixture(file, count, payloadBytes)
|
||||
const row = { count, payloadBytes, sourceBytes: fs.statSync(file).size }
|
||||
for (const [phase, methods] of Object.entries(modules)) {
|
||||
row[phase] = await sample(methods.proveClaudeTranscriptBranch, file, count)
|
||||
}
|
||||
if (count === 64 && payloadBytes > 0) {
|
||||
assert(
|
||||
row.candidate.peakHeapIncrease < row.baseline.peakHeapIncrease / 2,
|
||||
'Streaming must release prior payload rows'
|
||||
)
|
||||
assert(
|
||||
row.windowCandidate.peakHeapIncrease < row.baseline.peakHeapIncrease / 2,
|
||||
'Initial-window streaming must release prior payload rows'
|
||||
)
|
||||
}
|
||||
reports.push(row)
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
return reports
|
||||
}
|
||||
|
||||
module.exports = { compareAllocation }
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"sourceSha256": "3224594befe1edae514724caf5eff6a4cda85212e2985be4920b6164b088b82c",
|
||||
"bundleSha256": "e2d5ee6e5a7e2a15be217eab8066f9596cc9279f1e81709c1d228a575b40acc5",
|
||||
"measurement": "forced GC at each actual JSON.parse boundary; transient live bytes, not process RSS or leak",
|
||||
"rows": [
|
||||
{
|
||||
"records": 17,
|
||||
"sourceBytes": 8390708,
|
||||
"peakHeapIncrease": 9554312,
|
||||
"peakExternalIncrease": 39165,
|
||||
"retainedHeapIncrease": 155896,
|
||||
"retainedExternalIncrease": 69573,
|
||||
"elapsedMs": 76
|
||||
},
|
||||
{
|
||||
"records": 65,
|
||||
"sourceBytes": 33562676,
|
||||
"peakHeapIncrease": 34626848,
|
||||
"peakExternalIncrease": 0,
|
||||
"retainedHeapIncrease": 2416,
|
||||
"retainedExternalIncrease": 0,
|
||||
"elapsedMs": 284
|
||||
},
|
||||
{
|
||||
"records": 65,
|
||||
"sourceBytes": 8244,
|
||||
"peakHeapIncrease": 27160,
|
||||
"peakExternalIncrease": 0,
|
||||
"retainedHeapIncrease": 4752,
|
||||
"retainedExternalIncrease": 0,
|
||||
"elapsedMs": 269
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,65 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
async function compareDuration(scratch, modules) {
|
||||
const count = 8192
|
||||
const sessionId = '11111111-1111-4111-8111-111111111111'
|
||||
const id = (index) => `00000000-0000-4000-8000-${index.toString(16).padStart(12, '0')}`
|
||||
const file = path.join(scratch, 'small-record-duration.jsonl')
|
||||
const fd = fs.openSync(file, 'w')
|
||||
try {
|
||||
for (let index = 0; index < count; index++) {
|
||||
fs.writeSync(
|
||||
fd,
|
||||
`${JSON.stringify({
|
||||
type: index % 2 === 0 ? 'user' : 'assistant',
|
||||
uuid: id(index),
|
||||
parentUuid: index === 0 ? null : id(index - 1),
|
||||
sessionId,
|
||||
message: {
|
||||
role: index % 2 === 0 ? 'user' : 'assistant',
|
||||
content: 'Synthetic short transcript body. '.repeat(4)
|
||||
}
|
||||
})}\n`
|
||||
)
|
||||
}
|
||||
fs.writeSync(
|
||||
fd,
|
||||
`${JSON.stringify({ type: 'last-prompt', leafUuid: id(count - 1), sessionId })}\n`
|
||||
)
|
||||
} finally {
|
||||
fs.closeSync(fd)
|
||||
}
|
||||
const durations = { baseline: [], windowCandidate: [] }
|
||||
const input = { transcriptPath: file, providerSessionId: sessionId, previousLeafUuid: id(0) }
|
||||
for (let round = 0; round < 7; round++) {
|
||||
const phases =
|
||||
round % 2 === 0 ? ['baseline', 'windowCandidate'] : ['windowCandidate', 'baseline']
|
||||
for (const phase of phases) {
|
||||
const start = performance.now()
|
||||
const result = await modules[phase].proveClaudeTranscriptBranch(input)
|
||||
const elapsed = performance.now() - start
|
||||
assert.deepEqual(result, { leafUuid: id(count - 1), relation: 'descendant' })
|
||||
if (round >= 2) {
|
||||
durations[phase].push(Math.round(elapsed * 100) / 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
const medians = Object.fromEntries(
|
||||
Object.entries(durations).map(([phase, values]) => [phase, values.toSorted((a, b) => a - b)[2]])
|
||||
)
|
||||
const report = {
|
||||
node: process.version,
|
||||
records: count + 1,
|
||||
sourceBytes: fs.statSync(file).size,
|
||||
method:
|
||||
'One stable real file, two warm rounds and five measured rounds, alternating phase order; no GC forced during timing. Whole proof, not an isolated parser benchmark.',
|
||||
durationsMs: durations,
|
||||
medianMs: medians
|
||||
}
|
||||
fs.unlinkSync(file)
|
||||
return report
|
||||
}
|
||||
|
||||
module.exports = { compareDuration }
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,243 @@
|
||||
diff --git a/src/main/claude/claude-transcript-branch-proof.ts b/src/main/claude/claude-transcript-branch-proof.ts
|
||||
index c27f128134..d5a0c918cf 100644
|
||||
--- a/src/main/claude/claude-transcript-branch-proof.ts
|
||||
+++ b/src/main/claude/claude-transcript-branch-proof.ts
|
||||
@@ -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 @@ const MAX_CLAUDE_TRANSCRIPT_ANCESTRY = 10_000
|
||||
+class ClaudeTranscriptMarkerMissingError extends Error {
|
||||
+ constructor() {
|
||||
+ super('Claude transcript branch proof failed: missing last-prompt marker')
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
@@ -82,2 +89 @@ function proveAppendOrder(nodes: Map<string, TranscriptNode>): void {
|
||||
-export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- contents: string
|
||||
+type BranchProofInput = {
|
||||
@@ -87 +93,3 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
-}): ClaudeTranscriptBranchProof {
|
||||
+}
|
||||
+
|
||||
+function createBranchProof(input: BranchProofInput) {
|
||||
@@ -91,2 +99,3 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 +103 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- continue
|
||||
+ return
|
||||
@@ -100 +109 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- if (index === lines.length - 1 && !input.contents.endsWith('\n')) {
|
||||
+ if (!terminated) {
|
||||
@@ -107,0 +117 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The parsed value is a non-array object checked above.
|
||||
@@ -120 +130 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- continue
|
||||
+ return
|
||||
@@ -149,21 +159,16 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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')
|
||||
- }
|
||||
- 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' }
|
||||
+
|
||||
+ function finish(): ClaudeTranscriptBranchProof {
|
||||
+ if (!leafUuid) {
|
||||
+ throw new ClaudeTranscriptMarkerMissingError()
|
||||
+ }
|
||||
+ 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 +176,10 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 +209 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- return { leafUuid, relation: 'descendant' }
|
||||
+ return { leafUuid, relation: 'same' }
|
||||
@@ -217,0 +211,24 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
+ 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 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- 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 +246 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
- throw transcriptError('latest marker is on a sibling branch')
|
||||
+ return proof.finish()
|
||||
@@ -225,12 +249,35 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
-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 {
|
||||
+ // Bound this observation to the opened file's initial prefix.
|
||||
+ const stats = await handle.stat()
|
||||
+ const proof = createBranchProof(input)
|
||||
+ let index = 0
|
||||
+ if (stats.size > 0) {
|
||||
+ const stream = handle.createReadStream({ start: 0, end: stats.size - 1, autoClose: false })
|
||||
+ for await (const record of splitTranscriptStreamLines(stream)) {
|
||||
+ proof.add(record.line, index++, record.terminated)
|
||||
+ }
|
||||
+ }
|
||||
+ try {
|
||||
+ return proof.finish()
|
||||
+ } catch (error) {
|
||||
+ if (
|
||||
+ error instanceof ClaudeTranscriptMarkerMissingError ||
|
||||
+ error instanceof ClaudeTranscriptPreviousCursorMissingError
|
||||
+ ) {
|
||||
+ const grew = await handle.stat().then(
|
||||
+ (current) => current.size > stats.size,
|
||||
+ () => false
|
||||
+ )
|
||||
+ if (grew) {
|
||||
+ throw new ClaudeTranscriptTailIncompleteError()
|
||||
+ }
|
||||
+ }
|
||||
+ throw error
|
||||
+ }
|
||||
+ } finally {
|
||||
+ await handle.close()
|
||||
+ }
|
||||
@@ -0,0 +1,277 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { sessionId } = require('./parity-cases.cjs')
|
||||
|
||||
const rootRow = `${JSON.stringify({ type: 'user', uuid: 'root', parentUuid: null, sessionId })}\n`
|
||||
const rootMarker = `${JSON.stringify({ type: 'last-prompt', leafUuid: 'root', sessionId })}\n`
|
||||
const append = `${JSON.stringify({
|
||||
type: 'user',
|
||||
uuid: 'child',
|
||||
parentUuid: 'root',
|
||||
sessionId
|
||||
})}\n${JSON.stringify({ type: 'last-prompt', leafUuid: 'child', sessionId })}\n`
|
||||
const makeRow = (uuid, parentUuid) =>
|
||||
`${JSON.stringify({ type: 'user', uuid, parentUuid, sessionId })}\n`
|
||||
const makeMarker = (leafUuid) => `${JSON.stringify({ type: 'last-prompt', leafUuid, sessionId })}\n`
|
||||
function paddedSource(bytes) {
|
||||
const emptyComment = '{"comment":""}\n'
|
||||
return `${rootRow}{"comment":"${'x'.repeat(
|
||||
bytes - Buffer.byteLength(rootRow + rootMarker + emptyComment)
|
||||
)}"}\n${rootMarker}`
|
||||
}
|
||||
|
||||
async function compareGrowth(scratch, modules) {
|
||||
const scenarios = [
|
||||
{ name: 'small-regular-file', contents: rootRow + rootMarker, append },
|
||||
{ name: 'exact-512k-regular-file', contents: paddedSource(512 * 1024), append },
|
||||
{ name: 'nonaligned-large-regular-file', contents: paddedSource(512 * 1024 + 100), append },
|
||||
{ name: 'initially-empty-file', contents: '', append: rootRow + rootMarker },
|
||||
{ name: 'new-malformed-tail', contents: rootRow + rootMarker, append: '{"incomplete":' },
|
||||
{ name: 'parse-error-closes-reader', contents: `{"broken":\n${rootRow}${rootMarker}` },
|
||||
{ name: 'empty-file-closes-reader', contents: '' },
|
||||
{ name: 'read-error-closes-reader', contents: rootRow + rootMarker, readError: true },
|
||||
{ name: 'stat-error-closes-reader', contents: rootRow + rootMarker, statError: true },
|
||||
{
|
||||
name: 'restat-failure-keeps-missing-marker',
|
||||
contents: rootRow,
|
||||
append: rootMarker,
|
||||
restatError: true
|
||||
},
|
||||
{ name: 'replacement-cannot-prove-original-growth', contents: rootRow, replace: true },
|
||||
{ name: 'shrink-cannot-prove-growth', contents: `${rootRow} \n`, truncate: 0 },
|
||||
{
|
||||
name: 'growth-preserves-conflict',
|
||||
contents: rootRow + makeRow('root', 'other') + rootMarker,
|
||||
append
|
||||
},
|
||||
{
|
||||
name: 'growth-preserves-wrong-session',
|
||||
contents: `${rootRow + JSON.stringify({ type: 'last-prompt', sessionId: 'foreign', leafUuid: 'root' })}\n`,
|
||||
append
|
||||
},
|
||||
{
|
||||
name: 'growth-preserves-append-order',
|
||||
contents: makeRow('child', 'root') + rootRow + makeMarker('child'),
|
||||
append
|
||||
},
|
||||
{ name: 'path-replacement-keeps-opened-file', contents: rootRow + rootMarker, replace: true },
|
||||
{ name: 'path-deletion-keeps-opened-file', contents: rootRow + rootMarker, delete: true },
|
||||
{
|
||||
name: 'truncation-leaves-complete-prefix',
|
||||
contents: rootRow + rootMarker + append,
|
||||
truncate: Buffer.byteLength(rootRow + rootMarker)
|
||||
},
|
||||
{
|
||||
name: 'truncation-leaves-torn-tail',
|
||||
contents: paddedSource(1024),
|
||||
truncate: Buffer.byteLength(rootRow) + 8
|
||||
},
|
||||
{
|
||||
name: 'prefix-descendant-under-growth',
|
||||
contents: rootRow + append,
|
||||
append: makeRow('grandchild', 'child') + makeMarker('grandchild'),
|
||||
options: { previousLeafUuid: 'root' }
|
||||
},
|
||||
{
|
||||
name: 'prefix-rewind-under-growth',
|
||||
contents: rootRow + makeRow('kept', 'root') + makeRow('old', 'kept') + makeMarker('kept'),
|
||||
append: makeMarker('old'),
|
||||
options: { previousLeafUuid: 'old', intentionalRewindUuid: 'kept' }
|
||||
},
|
||||
{
|
||||
name: 'missing-previous-not-repaired-by-growth',
|
||||
contents: rootRow + rootMarker,
|
||||
append,
|
||||
options: { previousLeafUuid: 'child' }
|
||||
},
|
||||
{ name: 'missing-marker-not-repaired-by-growth', contents: rootRow, append: rootMarker },
|
||||
{
|
||||
name: 'torn-prefix-not-repaired-by-growth',
|
||||
contents: rootRow + rootMarker.slice(0, -2),
|
||||
append: '}\n'
|
||||
}
|
||||
]
|
||||
const reports = []
|
||||
for (const scenario of scenarios) {
|
||||
const phases = {}
|
||||
for (const [phase, methods] of Object.entries(modules)) {
|
||||
const file = path.join(scratch, `growth-${phase}.jsonl`)
|
||||
fs.writeFileSync(file, scenario.contents)
|
||||
const binding = process.binding('fs')
|
||||
const originalRead = binding.read
|
||||
const originalFstat = binding.fstat
|
||||
const originalCreateReadStream = fs.createReadStream
|
||||
let streamClosed = Promise.resolve()
|
||||
fs.createReadStream = function (...args) {
|
||||
const stream = originalCreateReadStream.apply(this, args)
|
||||
streamClosed = new Promise((resolve) => stream.once('close', resolve))
|
||||
return stream
|
||||
}
|
||||
let descriptor
|
||||
let injected = false
|
||||
let statsRead = 0
|
||||
function inject(fd) {
|
||||
if (!injected) {
|
||||
injected = true
|
||||
descriptor = fd
|
||||
if (scenario.append) {
|
||||
fs.appendFileSync(file, scenario.append)
|
||||
}
|
||||
if (scenario.replace) {
|
||||
fs.renameSync(file, `${file}.opened`)
|
||||
fs.writeFileSync(file, rootRow + append)
|
||||
}
|
||||
if (scenario.delete) {
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
if (scenario.truncate !== undefined) {
|
||||
fs.truncateSync(file, scenario.truncate)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.fstat = function (...args) {
|
||||
const result = originalFstat.apply(this, args)
|
||||
if (typeof result?.then !== 'function') {
|
||||
return result
|
||||
}
|
||||
return result.then((stats) => {
|
||||
inject(args[0])
|
||||
statsRead++
|
||||
if (scenario.statError || (scenario.restatError && statsRead > 1)) {
|
||||
throw Object.assign(new Error('Synthetic stat failure after actual open'), {
|
||||
code: 'EIO'
|
||||
})
|
||||
}
|
||||
return stats
|
||||
})
|
||||
}
|
||||
binding.read = function (...args) {
|
||||
inject(args[0])
|
||||
if (scenario.readError) {
|
||||
const error = Object.assign(new Error('Synthetic read failure after actual open'), {
|
||||
code: 'EIO'
|
||||
})
|
||||
const request = args.at(-1)
|
||||
if (typeof request?.oncomplete === 'function') {
|
||||
queueMicrotask(() => request.oncomplete(error))
|
||||
return
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return originalRead.apply(this, args)
|
||||
}
|
||||
let outcome
|
||||
try {
|
||||
outcome = {
|
||||
status: 'fulfilled',
|
||||
value: await methods.proveClaudeTranscriptBranch({
|
||||
transcriptPath: file,
|
||||
providerSessionId: sessionId,
|
||||
previousLeafUuid: null,
|
||||
...scenario.options
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
outcome = {
|
||||
status: 'rejected',
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
...(error.code ? { code: error.code } : {})
|
||||
}
|
||||
} finally {
|
||||
binding.read = originalRead
|
||||
binding.fstat = originalFstat
|
||||
fs.createReadStream = originalCreateReadStream
|
||||
}
|
||||
let descriptorClosedAtReturn = false
|
||||
try {
|
||||
fs.fstatSync(descriptor)
|
||||
} catch (error) {
|
||||
descriptorClosedAtReturn = error.code === 'EBADF'
|
||||
}
|
||||
await streamClosed
|
||||
let descriptorClosed = false
|
||||
try {
|
||||
fs.fstatSync(descriptor)
|
||||
} catch (error) {
|
||||
descriptorClosed = error.code === 'EBADF'
|
||||
}
|
||||
assert(injected, 'Actual file read must reach deterministic injection point')
|
||||
assert(descriptorClosed, `${scenario.name}/${phase} leaked its actual file descriptor`)
|
||||
if (phase === 'windowCandidate') {
|
||||
assert(descriptorClosedAtReturn)
|
||||
}
|
||||
phases[phase] = { outcome, descriptorClosedAtReturn, descriptorClosed }
|
||||
if (!scenario.delete) {
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
if (scenario.replace) {
|
||||
fs.unlinkSync(`${file}.opened`)
|
||||
}
|
||||
}
|
||||
if (scenario.name === 'small-regular-file' || scenario.name === 'exact-512k-regular-file') {
|
||||
assert.equal(phases.baseline.outcome.value.leafUuid, 'root')
|
||||
assert.equal(phases.candidate.outcome.value.leafUuid, 'child')
|
||||
} else if (scenario.name === 'new-malformed-tail') {
|
||||
assert.equal(phases.baseline.outcome.status, 'fulfilled')
|
||||
assert.equal(phases.candidate.outcome.name, 'ClaudeTranscriptTailIncompleteError')
|
||||
} else if (
|
||||
!scenario.options &&
|
||||
!scenario.name.includes('not-repaired') &&
|
||||
!scenario.statError &&
|
||||
!scenario.restatError
|
||||
) {
|
||||
assert.deepEqual(phases.baseline.outcome, phases.candidate.outcome, scenario.name)
|
||||
}
|
||||
if (
|
||||
[
|
||||
'initially-empty-file',
|
||||
'missing-previous-not-repaired-by-growth',
|
||||
'missing-marker-not-repaired-by-growth'
|
||||
].includes(scenario.name)
|
||||
) {
|
||||
assert.equal(phases.windowCandidate.outcome.name, 'ClaudeTranscriptTailIncompleteError')
|
||||
} else if (
|
||||
[
|
||||
'small-regular-file',
|
||||
'exact-512k-regular-file',
|
||||
'nonaligned-large-regular-file',
|
||||
'new-malformed-tail'
|
||||
].includes(scenario.name)
|
||||
) {
|
||||
assert.equal(phases.windowCandidate.outcome.value.leafUuid, 'root')
|
||||
} else {
|
||||
assert.deepEqual(phases.windowCandidate.outcome, phases.baseline.outcome)
|
||||
}
|
||||
if (scenario.name === 'prefix-descendant-under-growth') {
|
||||
assert.deepEqual(phases.windowCandidate.outcome.value, {
|
||||
leafUuid: 'child',
|
||||
relation: 'descendant'
|
||||
})
|
||||
}
|
||||
if (scenario.name === 'prefix-rewind-under-growth') {
|
||||
assert.deepEqual(phases.windowCandidate.outcome.value, {
|
||||
leafUuid: 'kept',
|
||||
relation: 'intentional-rewind'
|
||||
})
|
||||
}
|
||||
if (scenario.name === 'torn-prefix-not-repaired-by-growth') {
|
||||
assert.equal(phases.windowCandidate.outcome.name, 'ClaudeTranscriptTailIncompleteError')
|
||||
}
|
||||
reports.push({
|
||||
name: scenario.name,
|
||||
initialBytes: Buffer.byteLength(scenario.contents),
|
||||
appendedBytes: Buffer.byteLength(scenario.append ?? ''),
|
||||
phases
|
||||
})
|
||||
}
|
||||
return {
|
||||
node: process.version,
|
||||
method:
|
||||
'Append after native stat captures size; open-ended stream has no stat, so append before its first native read. Real files and actual proof methods.',
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { compareGrowth }
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,838 @@
|
||||
{
|
||||
"status": "unshipped candidate; product files unchanged",
|
||||
"measurement": "Forced GC at actual JSON.parse boundaries; sampled transient live bytes, not RSS, natural peak, or a retained-leak proof.",
|
||||
"sourceHashes": {
|
||||
"src/shared/node-readable-text.ts": "3e077b04805e343f8ac1e56843a512bfdb591fbb613f9e25d87ef7c2f6f64460",
|
||||
"src/main/native-chat/transcript-fallback-id.ts": "1fec8edecb738e64fb218ee9cdf530a20c51b804c35dcb7deb98f47c8e5499ea",
|
||||
"src/main/native-chat/transcript-stream-lines.ts": "5f3a0a1e5b3d003076452fa22234af00502808ed6463672c0e58aa49ae5b6aa1",
|
||||
"src/main/claude/claude-transcript-branch-proof.ts": {
|
||||
"baseline": "3224594befe1edae514724caf5eff6a4cda85212e2985be4920b6164b088b82c",
|
||||
"candidate": "c2e0403d4a98eb5963da23988ad0fde24fc91d22b9e064e342fcfccb875e446f"
|
||||
}
|
||||
},
|
||||
"bundleHashes": {
|
||||
"baseline": "e2d5ee6e5a7e2a15be217eab8066f9596cc9279f1e81709c1d228a575b40acc5",
|
||||
"candidate": "5d99e9e133a382fb0788269b254e90e2d557518df0176628e6bf6861d9dfd3c2"
|
||||
},
|
||||
"parity": [
|
||||
{
|
||||
"name": "initial",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "same",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "same"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "descendant",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "descendant"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "valid-unterminated-tail",
|
||||
"sourceBytes": 318,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "blank-lines-crlf",
|
||||
"sourceBytes": 331,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "torn-tail",
|
||||
"sourceBytes": 327,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "ClaudeTranscriptTailIncompleteError",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "terminated-malformed-tail",
|
||||
"sourceBytes": 328,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "malformed-middle",
|
||||
"sourceBytes": 328,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "malformed-before-blank-tail",
|
||||
"sourceBytes": 329,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "non-object-null",
|
||||
"sourceBytes": 324,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: non-object record"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "non-object-array",
|
||||
"sourceBytes": 322,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: non-object record"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing-marker",
|
||||
"sourceBytes": 246,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: missing last-prompt marker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "marker-before-leaf",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker precedes its leaf record"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wrong-marker-session",
|
||||
"sourceBytes": 308,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: invalid last-prompt marker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "empty-marker-leaf",
|
||||
"sourceBytes": 316,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: invalid last-prompt marker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing-marker-leaf",
|
||||
"sourceBytes": 322,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last-marker-wins",
|
||||
"sourceBytes": 393,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "descendant"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last-marker-sibling",
|
||||
"sourceBytes": 482,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: latest marker is on a sibling branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate-identical-after-marker",
|
||||
"sourceBytes": 401,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate-conflicting-parent",
|
||||
"sourceBytes": 401,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: record old has conflicting ancestry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate-conflicting-session",
|
||||
"sourceBytes": 390,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: record old has conflicting ancestry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "duplicate-conflicting-sidechain",
|
||||
"sourceBytes": 420,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: record old has conflicting ancestry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "append-order-on-proof-path",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: parent row follows descendant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "append-order-outside-proof-path",
|
||||
"sourceBytes": 527,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: parent row follows descendant"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing-old-ancestor",
|
||||
"sourceBytes": 238,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: missing ancestor root"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cycle",
|
||||
"sourceBytes": 320,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: cycle in parentUuid ancestry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "missing-previous-cursor",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "ClaudeTranscriptPreviousCursorMissingError",
|
||||
"message": "Claude transcript branch proof failed: previous cursor is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "previous-foreign-session",
|
||||
"sourceBytes": 308,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: previous cursor is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-leaf-sidechain",
|
||||
"sourceBytes": 338,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-old-ancestor-sidechain",
|
||||
"sourceBytes": 338,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestor root is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-leaf-parent-tool",
|
||||
"sourceBytes": 347,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-old-ancestor-parent-tool",
|
||||
"sourceBytes": 347,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestor root is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-leaf-result",
|
||||
"sourceBytes": 321,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-old-ancestor-result",
|
||||
"sourceBytes": 321,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestor root is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-leaf-stream",
|
||||
"sourceBytes": 327,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-old-ancestor-stream",
|
||||
"sourceBytes": 327,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestor root is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-leaf-init",
|
||||
"sourceBytes": 338,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: marker leaf is missing from the session graph"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "disallowed-old-ancestor-init",
|
||||
"sourceBytes": 338,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestor root is not on the main transcript"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind",
|
||||
"sourceBytes": 320,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "kept",
|
||||
"relation": "intentional-rewind"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind-without-intent",
|
||||
"sourceBytes": 320,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: latest marker is on a sibling branch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind-wrong-target",
|
||||
"sourceBytes": 320,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: rewind target does not match the observed leaf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind-no-previous",
|
||||
"sourceBytes": 320,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: rewind target does not match the observed leaf"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind-to-same",
|
||||
"sourceBytes": 319,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: rewind target is not an ancestor of the previous cursor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "rewind-to-sibling",
|
||||
"sourceBytes": 409,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: rewind target is not an ancestor of the previous cursor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "old-ancestor-before-large-body",
|
||||
"sourceBytes": 3146060,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "descendant"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "utf8-crosses-64k-file-chunk",
|
||||
"sourceBytes": 65788,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unicode-identities",
|
||||
"sourceBytes": 246,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "終é",
|
||||
"relation": "descendant"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invalid-utf8-replacement-in-message",
|
||||
"sourceBytes": 337,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "old",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "invalid-utf8-in-torn-tail",
|
||||
"sourceBytes": 333,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "ClaudeTranscriptTailIncompleteError",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ancestry-depth-10000",
|
||||
"sourceBytes": 947852,
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "depth-9999",
|
||||
"relation": "initial"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ancestry-depth-10001",
|
||||
"sourceBytes": 947949,
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: ancestry exceeds the bounded proof limit"
|
||||
}
|
||||
}
|
||||
],
|
||||
"missingFile": "Both readers reject with ENOENT",
|
||||
"growth": {
|
||||
"node": "v26.6.0",
|
||||
"method": "Append at first native read after actual open/stat, before bytes are read; real files and actual proof methods.",
|
||||
"reports": [
|
||||
{
|
||||
"name": "small-regular-file",
|
||||
"initialBytes": 155,
|
||||
"appendedBytes": 159,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "child",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "exact-512k-regular-file",
|
||||
"initialBytes": 524288,
|
||||
"appendedBytes": 159,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "child",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "nonaligned-large-regular-file",
|
||||
"initialBytes": 524388,
|
||||
"appendedBytes": 159,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "child",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "child",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "initially-empty-file",
|
||||
"initialBytes": 0,
|
||||
"appendedBytes": 155,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "new-malformed-tail",
|
||||
"initialBytes": 155,
|
||||
"appendedBytes": 14,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "ClaudeTranscriptTailIncompleteError",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parse-error-closes-reader",
|
||||
"initialBytes": 166,
|
||||
"appendedBytes": 0,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: malformed JSONL"
|
||||
},
|
||||
"descriptorClosedAtReturn": false,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "empty-file-closes-reader",
|
||||
"initialBytes": 0,
|
||||
"appendedBytes": 0,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: missing last-prompt marker"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Claude transcript branch proof failed: missing last-prompt marker"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read-error-closes-reader",
|
||||
"initialBytes": 155,
|
||||
"appendedBytes": 0,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Synthetic read failure after actual open",
|
||||
"code": "EIO"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "rejected",
|
||||
"name": "Error",
|
||||
"message": "Synthetic read failure after actual open",
|
||||
"code": "EIO"
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path-replacement-keeps-opened-file",
|
||||
"initialBytes": 155,
|
||||
"appendedBytes": 0,
|
||||
"phases": {
|
||||
"baseline": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
},
|
||||
"candidate": {
|
||||
"outcome": {
|
||||
"status": "fulfilled",
|
||||
"value": {
|
||||
"leafUuid": "root",
|
||||
"relation": "initial"
|
||||
}
|
||||
},
|
||||
"descriptorClosedAtReturn": true,
|
||||
"descriptorClosed": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"allocations": [
|
||||
{
|
||||
"count": 16,
|
||||
"payloadBytes": 524288,
|
||||
"sourceBytes": 8391962,
|
||||
"baseline": {
|
||||
"records": 17,
|
||||
"peakHeapIncrease": 9435352,
|
||||
"peakExternalIncrease": 40,
|
||||
"retainedHeapIncrease": -11384,
|
||||
"retainedExternalIncrease": 40
|
||||
},
|
||||
"candidate": {
|
||||
"records": 17,
|
||||
"peakHeapIncrease": 1129864,
|
||||
"peakExternalIncrease": 134433,
|
||||
"retainedHeapIncrease": 7200,
|
||||
"retainedExternalIncrease": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"count": 64,
|
||||
"payloadBytes": 524288,
|
||||
"sourceBytes": 33567578,
|
||||
"baseline": {
|
||||
"records": 65,
|
||||
"peakHeapIncrease": 34639744,
|
||||
"peakExternalIncrease": 0,
|
||||
"retainedHeapIncrease": 3968,
|
||||
"retainedExternalIncrease": 0
|
||||
},
|
||||
"candidate": {
|
||||
"records": 65,
|
||||
"peakHeapIncrease": 1205416,
|
||||
"peakExternalIncrease": 144225,
|
||||
"retainedHeapIncrease": 63840,
|
||||
"retainedExternalIncrease": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"count": 64,
|
||||
"payloadBytes": 0,
|
||||
"sourceBytes": 13146,
|
||||
"baseline": {
|
||||
"records": 65,
|
||||
"peakHeapIncrease": 2520,
|
||||
"peakExternalIncrease": 0,
|
||||
"retainedHeapIncrease": -30328,
|
||||
"retainedExternalIncrease": 0
|
||||
},
|
||||
"candidate": {
|
||||
"records": 65,
|
||||
"peakHeapIncrease": 42936,
|
||||
"peakExternalIncrease": 144225,
|
||||
"retainedHeapIncrease": 3944,
|
||||
"retainedExternalIncrease": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"payloadBytes": 8388608,
|
||||
"sourceBytes": 8388902,
|
||||
"baseline": {
|
||||
"records": 2,
|
||||
"peakHeapIncrease": 16776088,
|
||||
"peakExternalIncrease": 0,
|
||||
"retainedHeapIncrease": -2856,
|
||||
"retainedExternalIncrease": 0
|
||||
},
|
||||
"candidate": {
|
||||
"records": 2,
|
||||
"peakHeapIncrease": 16786040,
|
||||
"peakExternalIncrease": 131373,
|
||||
"retainedHeapIncrease": 3944,
|
||||
"retainedExternalIncrease": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
const sessionId = 'synthetic-provider'
|
||||
const row = (uuid, parentUuid, extra = {}) => ({
|
||||
type: 'user',
|
||||
uuid,
|
||||
parentUuid,
|
||||
sessionId,
|
||||
...extra
|
||||
})
|
||||
const marker = (leafUuid, extra = {}) => ({ type: 'last-prompt', sessionId, leafUuid, ...extra })
|
||||
const jsonl = (rows, trailing = true) =>
|
||||
rows.map((item) => JSON.stringify(item)).join('\n') + (trailing ? '\n' : '')
|
||||
const graph = [row('root', null), row('kept', 'root'), row('old', 'kept')]
|
||||
|
||||
function parityCases() {
|
||||
const cases = []
|
||||
const add = (name, contents, options = {}, error = null) =>
|
||||
cases.push({ name, contents, options, error })
|
||||
const good = jsonl([...graph, marker('old')])
|
||||
add('initial', good)
|
||||
add('same', good, { previousLeafUuid: 'old' })
|
||||
add('descendant', good, { previousLeafUuid: 'root' })
|
||||
add('valid-unterminated-tail', good.trimEnd())
|
||||
add('blank-lines-crlf', `\r\n \r\n${good.replaceAll('\n', '\r\n')}\r\n`)
|
||||
add('torn-tail', `${good}{"type":`, {}, 'ClaudeTranscriptTailIncompleteError')
|
||||
add('terminated-malformed-tail', `${good}{"type":\n`, {}, 'Error')
|
||||
add('malformed-middle', `{"type":\n${good}`, {}, 'Error')
|
||||
add('malformed-before-blank-tail', `${good}{"type":\n `, {}, 'Error')
|
||||
add('non-object-null', `null\n${good}`, {}, 'Error')
|
||||
add('non-object-array', `[]\n${good}`, {}, 'Error')
|
||||
add('missing-marker', jsonl(graph), {}, 'Error')
|
||||
add('marker-before-leaf', jsonl([marker('old'), ...graph]), {}, 'Error')
|
||||
add(
|
||||
'wrong-marker-session',
|
||||
jsonl([...graph, marker('old', { sessionId: 'foreign' })]),
|
||||
{},
|
||||
'Error'
|
||||
)
|
||||
add('empty-marker-leaf', jsonl([...graph, marker('')]), {}, 'Error')
|
||||
add('missing-marker-leaf', jsonl([...graph, marker('absent')]), {}, 'Error')
|
||||
add('last-marker-wins', jsonl([...graph, marker('root'), marker('old')]), {
|
||||
previousLeafUuid: 'root'
|
||||
})
|
||||
add(
|
||||
'last-marker-sibling',
|
||||
jsonl([...graph, row('sibling', 'root'), marker('old'), marker('sibling')]),
|
||||
{ previousLeafUuid: 'old' },
|
||||
'Error'
|
||||
)
|
||||
add('duplicate-identical-after-marker', good + jsonl([row('old', 'kept')]))
|
||||
for (const [name, extra] of [
|
||||
['parent', { parentUuid: 'root' }],
|
||||
['session', { sessionId: 'foreign' }],
|
||||
['sidechain', { isSidechain: true }]
|
||||
]) {
|
||||
add(`duplicate-conflicting-${name}`, good + jsonl([row('old', 'kept', extra)]), {}, 'Error')
|
||||
}
|
||||
add(
|
||||
'append-order-on-proof-path',
|
||||
jsonl([graph[1], graph[0], graph[2], marker('old')]),
|
||||
{},
|
||||
'Error'
|
||||
)
|
||||
add(
|
||||
'append-order-outside-proof-path',
|
||||
good +
|
||||
jsonl([row('disconnected-child', 'disconnected-parent'), row('disconnected-parent', null)]),
|
||||
{},
|
||||
'Error'
|
||||
)
|
||||
add('missing-old-ancestor', jsonl([...graph.slice(1), marker('old')]), {}, 'Error')
|
||||
add('cycle', jsonl([row('root', 'old'), ...graph.slice(1), marker('old')]), {}, 'Error')
|
||||
add(
|
||||
'missing-previous-cursor',
|
||||
good,
|
||||
{ previousLeafUuid: 'absent' },
|
||||
'ClaudeTranscriptPreviousCursorMissingError'
|
||||
)
|
||||
add(
|
||||
'previous-foreign-session',
|
||||
jsonl([row('root', null, { sessionId: 'foreign' }), ...graph.slice(1), marker('old')]),
|
||||
{ previousLeafUuid: 'root' },
|
||||
'Error'
|
||||
)
|
||||
for (const [name, extra] of [
|
||||
['sidechain', { isSidechain: true }],
|
||||
['parent-tool', { parent_tool_use_id: 'tool' }],
|
||||
['result', { type: 'result' }],
|
||||
['stream', { type: 'stream_event' }],
|
||||
['init', { type: 'system', subtype: 'init' }]
|
||||
]) {
|
||||
add(
|
||||
`disallowed-leaf-${name}`,
|
||||
jsonl([...graph.slice(0, 2), row('old', 'kept', extra), marker('old')]),
|
||||
{},
|
||||
'Error'
|
||||
)
|
||||
add(
|
||||
`disallowed-old-ancestor-${name}`,
|
||||
jsonl([row('root', null, extra), ...graph.slice(1), marker('old')]),
|
||||
{},
|
||||
'Error'
|
||||
)
|
||||
}
|
||||
add('rewind', jsonl([...graph, marker('kept')]), {
|
||||
previousLeafUuid: 'old',
|
||||
intentionalRewindUuid: 'kept'
|
||||
})
|
||||
add(
|
||||
'rewind-without-intent',
|
||||
jsonl([...graph, marker('kept')]),
|
||||
{ previousLeafUuid: 'old' },
|
||||
'Error'
|
||||
)
|
||||
add(
|
||||
'rewind-wrong-target',
|
||||
jsonl([...graph, marker('kept')]),
|
||||
{ previousLeafUuid: 'old', intentionalRewindUuid: 'root' },
|
||||
'Error'
|
||||
)
|
||||
add(
|
||||
'rewind-no-previous',
|
||||
jsonl([...graph, marker('kept')]),
|
||||
{ intentionalRewindUuid: 'kept' },
|
||||
'Error'
|
||||
)
|
||||
add('rewind-to-same', good, { previousLeafUuid: 'old', intentionalRewindUuid: 'old' }, 'Error')
|
||||
add(
|
||||
'rewind-to-sibling',
|
||||
jsonl([...graph, row('sibling', 'root'), marker('sibling')]),
|
||||
{ previousLeafUuid: 'old', intentionalRewindUuid: 'sibling' },
|
||||
'Error'
|
||||
)
|
||||
add(
|
||||
'old-ancestor-before-large-body',
|
||||
jsonl([
|
||||
graph[0],
|
||||
row('kept', 'root', { message: 'x'.repeat(3 * 1024 * 1024) }),
|
||||
graph[2],
|
||||
marker('old')
|
||||
]),
|
||||
{ previousLeafUuid: 'root' }
|
||||
)
|
||||
const unicode = row('root', null, { message: '' })
|
||||
const messageOffset = JSON.stringify(unicode).indexOf('"message":"') + '"message":"'.length
|
||||
unicode.message = `${'x'.repeat(65535 - messageOffset)}🙂é漢字`
|
||||
add('utf8-crosses-64k-file-chunk', jsonl([unicode, ...graph.slice(1), marker('old')]))
|
||||
add('unicode-identities', jsonl([row('起🙂', null), row('終é', '起🙂'), marker('終é')]), {
|
||||
previousLeafUuid: '起🙂'
|
||||
})
|
||||
add(
|
||||
'invalid-utf8-replacement-in-message',
|
||||
Buffer.concat([
|
||||
Buffer.from(jsonl(graph)),
|
||||
Buffer.from('{"comment":"'),
|
||||
Buffer.from([0xf0, 0x80, 0x80]),
|
||||
Buffer.from(`"}\n${jsonl([marker('old')])}`)
|
||||
])
|
||||
)
|
||||
add(
|
||||
'invalid-utf8-in-torn-tail',
|
||||
Buffer.concat([Buffer.from(`${good}{"comment":"`), Buffer.from([0xf0, 0x9f])]),
|
||||
{},
|
||||
'ClaudeTranscriptTailIncompleteError'
|
||||
)
|
||||
for (const count of [10_000, 10_001]) {
|
||||
const rows = Array.from({ length: count }, (_, index) =>
|
||||
row(`depth-${index}`, index === 0 ? null : `depth-${index - 1}`)
|
||||
)
|
||||
add(
|
||||
`ancestry-depth-${count}`,
|
||||
jsonl([...rows, marker(`depth-${count - 1}`)]),
|
||||
{},
|
||||
count === 10_001 ? 'Error' : null
|
||||
)
|
||||
}
|
||||
return cases
|
||||
}
|
||||
|
||||
module.exports = { parityCases, sessionId }
|
||||
@@ -0,0 +1,275 @@
|
||||
{
|
||||
"reportedTag": "v1.4.198",
|
||||
"reportedTagCommit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"currentCommitAtCapture": "2c49d285115af945b7be13cdc6da4803671f0eeb",
|
||||
"limits": "Source reachability/version evidence only; no user transcript or incident trigger evidence. Rewind branch support is newer than this tag.",
|
||||
"files": [
|
||||
{
|
||||
"path": "src/main/claude/claude-transcript-branch-proof.ts",
|
||||
"currentSha256": "3224594befe1edae514724caf5eff6a4cda85212e2985be4920b6164b088b82c",
|
||||
"reportedTagSha256": "79409696fc5bf385727d8710a5383acfe3ff350df5bdda93a79f7e97dcc54d1d",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 232,
|
||||
"text": "contents: await readFile(input.transcriptPath, 'utf8'),"
|
||||
},
|
||||
{
|
||||
"line": 240,
|
||||
"text": "export async function readClaudeTranscriptLeafWithReproof(input: {"
|
||||
},
|
||||
{
|
||||
"line": 241,
|
||||
"text": "readTranscriptLeaf: (input: {"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 215,
|
||||
"text": "contents: await readFile(input.transcriptPath, 'utf8'),"
|
||||
},
|
||||
{
|
||||
"line": 222,
|
||||
"text": "export async function readClaudeTranscriptLeafWithReproof(input: {"
|
||||
},
|
||||
{
|
||||
"line": 223,
|
||||
"text": "readTranscriptLeaf: (input: {"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/structured-claude-runtime-adapter.ts",
|
||||
"currentSha256": "10497152ac595562f19c7db67180ae93fdb0517946b4997977e677397ae957ec",
|
||||
"reportedTagSha256": "4b347b8222ca18eb4bd728a57c307226ba2a279e14fd5ebd4fbfac877aacc8f0",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 41,
|
||||
"text": "export function createStructuredClaudeRuntimeAdapter("
|
||||
},
|
||||
{
|
||||
"line": 74,
|
||||
"text": "readTranscriptLeaf: async ({"
|
||||
},
|
||||
{
|
||||
"line": 94,
|
||||
"text": "? await readClaudeTranscriptLeafUuid(transcriptPath, providerSessionId, previousLeafUuid)"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 39,
|
||||
"text": "export function createStructuredClaudeRuntimeAdapter("
|
||||
},
|
||||
{
|
||||
"line": 72,
|
||||
"text": "readTranscriptLeaf: async ({ providerSessionId, previousLeafUuid, claudeConfigDir }) => {"
|
||||
},
|
||||
{
|
||||
"line": 77,
|
||||
"text": "? await readClaudeTranscriptLeafUuid(transcriptPath, providerSessionId, previousLeafUuid)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/native-chat/session-file-resolver.ts",
|
||||
"currentSha256": "bf332461d0401c44ab5995822f2a9be097fd24b0b92996afecb04de304a70053",
|
||||
"reportedTagSha256": "573abd5403cc58183e0b266faae09cecaeb8583855a44f0122f03f0d543efde4",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 168,
|
||||
"text": "export async function readClaudeTranscriptLeafUuid("
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 162,
|
||||
"text": "export async function readClaudeTranscriptLeafUuid("
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/claude/claude-structured-session-close.ts",
|
||||
"currentSha256": "40080bf4ae7b52573f797639863399edece42e254afda642dac07791996d8653",
|
||||
"reportedTagSha256": "dc6c831eb322309b8a7ba89043546c71b2edb1c00eda1521e716498201356938",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 100,
|
||||
"text": "if ((await session.connection.close()) !== true) {"
|
||||
},
|
||||
{
|
||||
"line": 116,
|
||||
"text": "? await readClaudeTranscriptLeafWithReproof({"
|
||||
},
|
||||
{
|
||||
"line": 117,
|
||||
"text": "readTranscriptLeaf: input.readTranscriptLeaf,"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 77,
|
||||
"text": "if ((await session.connection.close()) !== true) {"
|
||||
},
|
||||
{
|
||||
"line": 85,
|
||||
"text": "? await readClaudeTranscriptLeafWithReproof({"
|
||||
},
|
||||
{
|
||||
"line": 86,
|
||||
"text": "readTranscriptLeaf: input.readTranscriptLeaf,"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/claude/claude-structured-session-adapter.ts",
|
||||
"currentSha256": "4a2fc01bab206adf2de7dd63ee5e29e9732dcadbf9530146f81bd05a49881d19",
|
||||
"reportedTagSha256": "87a40dea24a0d742164c9ab2c28f14a05d5a44bf6e511e0b72a280dc083d93d0",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 97,
|
||||
"text": "const closePromise = session.connection.close().catch(() => false)"
|
||||
},
|
||||
{
|
||||
"line": 145,
|
||||
"text": "await this.persistSessionHandle(sessionId, exit.session).catch(() => undefined)"
|
||||
},
|
||||
{
|
||||
"line": 183,
|
||||
"text": "? await readClaudeTranscriptLeafWithReproof({"
|
||||
},
|
||||
{
|
||||
"line": 184,
|
||||
"text": "readTranscriptLeaf: this.deps.readTranscriptLeaf,"
|
||||
},
|
||||
{
|
||||
"line": 314,
|
||||
"text": "...(this.deps.readTranscriptLeaf ? { readTranscriptLeaf: this.deps.readTranscriptLeaf } : {}),"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 92,
|
||||
"text": "const closePromise = session.connection.close().catch(() => false)"
|
||||
},
|
||||
{
|
||||
"line": 140,
|
||||
"text": "await this.persistSessionHandle(sessionId, exit.session).catch(() => undefined)"
|
||||
},
|
||||
{
|
||||
"line": 166,
|
||||
"text": "? await readClaudeTranscriptLeafWithReproof({"
|
||||
},
|
||||
{
|
||||
"line": 167,
|
||||
"text": "readTranscriptLeaf: this.deps.readTranscriptLeaf,"
|
||||
},
|
||||
{
|
||||
"line": 298,
|
||||
"text": "...(this.deps.readTranscriptLeaf ? { readTranscriptLeaf: this.deps.readTranscriptLeaf } : {}),"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/orca-runtime-stop-structured-session-process.ts",
|
||||
"currentSha256": "11b71f61bdad9cde89ac174a22f91937f2ba138e34bb58a86edeb568252a64b9",
|
||||
"reportedTagSha256": "11b71f61bdad9cde89ac174a22f91937f2ba138e34bb58a86edeb568252a64b9",
|
||||
"identicalToTag": true,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 42,
|
||||
"text": "const deadline = Date.now() + 15_000"
|
||||
},
|
||||
{
|
||||
"line": 48,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 66,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 150,
|
||||
"text": "const deadline = Date.now() + 15_000"
|
||||
},
|
||||
{
|
||||
"line": 172,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 184,
|
||||
"text": "const leafUuid = await readClaudeTranscriptLeafUuid("
|
||||
},
|
||||
{
|
||||
"line": 197,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 42,
|
||||
"text": "const deadline = Date.now() + 15_000"
|
||||
},
|
||||
{
|
||||
"line": 48,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 66,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 150,
|
||||
"text": "const deadline = Date.now() + 15_000"
|
||||
},
|
||||
{
|
||||
"line": 172,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
},
|
||||
{
|
||||
"line": 184,
|
||||
"text": "const leafUuid = await readClaudeTranscriptLeafUuid("
|
||||
},
|
||||
{
|
||||
"line": 197,
|
||||
"text": "await new Promise((resolve) => setTimeout(resolve, 100))"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/structured-agent-session-runtime.ts",
|
||||
"currentSha256": "de0fbb7968e922e21c4d374c3b0315fcb8c7099c9ec2c9aab163e090965e8c0b",
|
||||
"reportedTagSha256": "65f43aaa10363e54159cac9e84e62e89be122c4372c2f41136b053b81a252bae",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 287,
|
||||
"text": "const claude = createStructuredClaudeRuntimeAdapter({"
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": [
|
||||
{
|
||||
"line": 238,
|
||||
"text": "const claude = createStructuredClaudeRuntimeAdapter({"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/native-chat/transcript-stream-lines.ts",
|
||||
"currentSha256": "5f3a0a1e5b3d003076452fa22234af00502808ed6463672c0e58aa49ae5b6aa1",
|
||||
"reportedTagSha256": "4f3ed634f81521f05e39cc9c9a4e96450e891440372ddb34854d1c295af1f1bc",
|
||||
"identicalToTag": false,
|
||||
"currentCallSites": [
|
||||
{
|
||||
"line": 53,
|
||||
"text": "export async function* splitTranscriptStreamLines("
|
||||
}
|
||||
],
|
||||
"reportedTagCallSites": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"reports": [
|
||||
{
|
||||
"phase": "baseline",
|
||||
"passed": 68,
|
||||
"failed": 0,
|
||||
"files": [
|
||||
"src/main/native-chat/session-file-resolver.test.ts",
|
||||
"src/main/claude/claude-transcript-rewind-proof.test.ts",
|
||||
"src/main/claude/claude-structured-session-recovery.test.ts",
|
||||
"src/main/claude/claude-structured-history-window.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "candidate",
|
||||
"passed": 68,
|
||||
"failed": 0,
|
||||
"files": [
|
||||
"src/main/native-chat/session-file-resolver.test.ts",
|
||||
"src/main/claude/claude-transcript-rewind-proof.test.ts",
|
||||
"src/main/claude/claude-structured-session-recovery.test.ts",
|
||||
"src/main/claude/claude-structured-history-window.test.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"phase": "windowCandidate",
|
||||
"passed": 68,
|
||||
"failed": 0,
|
||||
"files": [
|
||||
"src/main/native-chat/session-file-resolver.test.ts",
|
||||
"src/main/claude/claude-transcript-rewind-proof.test.ts",
|
||||
"src/main/claude/claude-structured-session-recovery.test.ts",
|
||||
"src/main/claude/claude-structured-history-window.test.ts"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import loadSources from './sources.cjs'
|
||||
import { startVitest } from 'vitest/node'
|
||||
|
||||
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1') {
|
||||
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1.')
|
||||
}
|
||||
const { root, sourceRelativePath, baseline, candidate, windowCandidate } = loadSources()
|
||||
const files = [
|
||||
'src/main/native-chat/session-file-resolver.test.ts',
|
||||
'src/main/claude/claude-transcript-rewind-proof.test.ts',
|
||||
'src/main/claude/claude-structured-session-recovery.test.ts',
|
||||
'src/main/claude/claude-structured-history-window.test.ts'
|
||||
]
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'orca-branch-streaming-regressions-'))
|
||||
const reports = []
|
||||
try {
|
||||
for (const [phase, source] of Object.entries({ baseline, candidate, windowCandidate })) {
|
||||
const config = join(scratch, `${phase}.config.mjs`)
|
||||
const report = join(scratch, `${phase}.json`)
|
||||
await writeFile(
|
||||
config,
|
||||
`
|
||||
import base from ${JSON.stringify(pathToFileURL(join(root, 'config/vitest.config.ts')).href)}
|
||||
export default {
|
||||
...base,
|
||||
plugins: [{ name: 'branch-streaming-before-after', enforce: 'pre', transform(code, id) {
|
||||
if (${JSON.stringify(phase)} !== 'windowCandidate' && id.replaceAll('\\\\', '/').endsWith(${JSON.stringify(`/${sourceRelativePath}`)})) return ${JSON.stringify(source)}
|
||||
}}],
|
||||
test: { ...base.test, include: ${JSON.stringify(files.map((file) => join(root, file)))}, maxWorkers: 1, fileParallelism: false }
|
||||
}
|
||||
`
|
||||
)
|
||||
const runner = await startVitest('test', [], {
|
||||
root,
|
||||
config,
|
||||
watch: false,
|
||||
reporters: ['dot', 'json'],
|
||||
outputFile: { json: report }
|
||||
})
|
||||
assert(runner, 'Vitest did not start')
|
||||
await runner.close()
|
||||
const result = JSON.parse(await readFile(report, 'utf8'))
|
||||
assert.equal(result.success, true)
|
||||
reports.push({ phase, passed: result.numPassedTests, failed: result.numFailedTests, files })
|
||||
}
|
||||
const output = `${JSON.stringify({ reports }, null, 2)}\n`
|
||||
if (process.argv[2]) {
|
||||
await writeFile(resolve(process.argv[2]), output)
|
||||
}
|
||||
process.stdout.write(output)
|
||||
} finally {
|
||||
await rm(scratch, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { createHash } = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const { createRequire } = require('node:module')
|
||||
const { tmpdir } = require('node:os')
|
||||
const path = require('node:path')
|
||||
const { parityCases, sessionId } = require('./parity-cases.cjs')
|
||||
const { compareAllocation } = require('./allocation.cjs')
|
||||
const { compareGrowth } = require('./growth.cjs')
|
||||
const { compareDuration } = require('./duration.cjs')
|
||||
const { verifierEntry, compareVerifier } = require('./verifier.cjs')
|
||||
|
||||
const { root, sourcePath, sourceRelativePath, baseline, candidate, windowCandidate } =
|
||||
require('./sources.cjs')()
|
||||
const { buildSync } = createRequire(path.join(root, 'package.json'))('esbuild')
|
||||
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1' || !global.gc) {
|
||||
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1 and node --expose-gc.')
|
||||
}
|
||||
const hash = (text) => createHash('sha256').update(text).digest('hex')
|
||||
const verifierPath = 'src/main/runtime/orca-runtime-stop-structured-session-process.ts'
|
||||
const verifierSource = fs.readFileSync(path.join(root, verifierPath), 'utf8')
|
||||
const scratch = fs.mkdtempSync(path.join(tmpdir(), 'orca-claude-branch-streaming-'))
|
||||
const modulePaths = []
|
||||
|
||||
async function capture(action) {
|
||||
try {
|
||||
return { status: 'fulfilled', value: await action() }
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'rejected',
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
...(error.code ? { code: error.code } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const modules = {}
|
||||
const sourceHashes = {}
|
||||
const bundleHashes = {}
|
||||
for (const [phase, contents] of Object.entries({ baseline, candidate, windowCandidate })) {
|
||||
const built = buildSync({
|
||||
stdin: {
|
||||
contents: contents + verifierEntry(verifierSource),
|
||||
sourcefile: sourcePath,
|
||||
resolveDir: path.dirname(sourcePath),
|
||||
loader: 'ts'
|
||||
},
|
||||
absWorkingDir: root,
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
metafile: true
|
||||
})
|
||||
const modulePath = path.join(scratch, `${phase}.cjs`)
|
||||
fs.writeFileSync(modulePath, built.outputFiles[0].text)
|
||||
modulePaths.push(modulePath)
|
||||
modules[phase] = require(modulePath)
|
||||
bundleHashes[phase] = hash(built.outputFiles[0].text)
|
||||
for (const input of Object.keys(built.metafile.inputs)) {
|
||||
if (path.resolve(root, input) !== sourcePath) {
|
||||
sourceHashes[input] = hash(fs.readFileSync(path.resolve(root, input)))
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceHashes[sourceRelativePath] = {
|
||||
baseline: hash(baseline),
|
||||
candidate: hash(candidate),
|
||||
windowCandidate: hash(windowCandidate)
|
||||
}
|
||||
sourceHashes[verifierPath] = hash(verifierSource)
|
||||
const parity = []
|
||||
for (const scenario of parityCases()) {
|
||||
const file = path.join(scratch, 'parity.jsonl')
|
||||
fs.writeFileSync(file, scenario.contents)
|
||||
const input = { providerSessionId: sessionId, previousLeafUuid: null, ...scenario.options }
|
||||
const outcomes = {}
|
||||
for (const [phase, methods] of Object.entries(modules)) {
|
||||
outcomes[`${phase}File`] = await capture(() =>
|
||||
methods.proveClaudeTranscriptBranch({ ...input, transcriptPath: file })
|
||||
)
|
||||
outcomes[`${phase}String`] = await capture(() =>
|
||||
methods.proveClaudeTranscriptBranchFromJsonl({
|
||||
...input,
|
||||
contents: Buffer.from(scenario.contents).toString('utf8')
|
||||
})
|
||||
)
|
||||
}
|
||||
for (const outcome of Object.values(outcomes)) {
|
||||
assert.deepEqual(outcome, outcomes.baselineFile, scenario.name)
|
||||
}
|
||||
assert.equal(
|
||||
outcomes.baselineFile.status,
|
||||
scenario.error ? 'rejected' : 'fulfilled',
|
||||
scenario.name
|
||||
)
|
||||
if (scenario.error) {
|
||||
assert.equal(outcomes.baselineFile.name, scenario.error, scenario.name)
|
||||
}
|
||||
parity.push({
|
||||
name: scenario.name,
|
||||
sourceBytes: fs.statSync(file).size,
|
||||
outcome: outcomes.baselineFile
|
||||
})
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
const missing = {}
|
||||
for (const [phase, methods] of Object.entries(modules)) {
|
||||
missing[phase] = await capture(() =>
|
||||
methods.proveClaudeTranscriptBranch({
|
||||
transcriptPath: path.join(scratch, 'missing'),
|
||||
providerSessionId: sessionId,
|
||||
previousLeafUuid: null
|
||||
})
|
||||
)
|
||||
assert.equal(missing[phase].code, 'ENOENT')
|
||||
}
|
||||
const growth = await compareGrowth(scratch, modules)
|
||||
const allocations = await compareAllocation(scratch, modules)
|
||||
const duration = await compareDuration(scratch, modules)
|
||||
const verifier = await compareVerifier(scratch, modules)
|
||||
const report = {
|
||||
status:
|
||||
'Actual production source; baseline reconstructed by reverse fix.patch; open-ended experiment retained as a control',
|
||||
measurement:
|
||||
'Forced GC at actual JSON.parse boundaries; sampled transient live bytes, not RSS, natural peak, or a retained-leak proof.',
|
||||
sourceHashes,
|
||||
bundleHashes,
|
||||
parity,
|
||||
missingFile: 'All three readers reject with ENOENT',
|
||||
growth,
|
||||
allocations,
|
||||
duration,
|
||||
verifier
|
||||
}
|
||||
const output = `${JSON.stringify(report, null, 2)}\n`
|
||||
if (process.argv[2]) {
|
||||
fs.writeFileSync(path.resolve(process.argv[2]), output)
|
||||
}
|
||||
process.stdout.write(output)
|
||||
}
|
||||
|
||||
run()
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
.finally(() => {
|
||||
for (const modulePath of modulePaths) {
|
||||
delete require.cache[require.resolve(modulePath)]
|
||||
}
|
||||
fs.rmSync(scratch, { recursive: true, force: true })
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { createHash } = require('node:crypto')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { applyPatch, parsePatch, reversePatch } = require('diff')
|
||||
|
||||
module.exports = function loadSources() {
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const transform = JSON.parse(
|
||||
fs.readFileSync(path.join(__dirname, 'candidate-transform.json'), 'utf8')
|
||||
)
|
||||
const sourcePath = path.join(root, transform.path)
|
||||
const windowCandidate = fs.readFileSync(sourcePath, 'utf8')
|
||||
const patch = parsePatch(fs.readFileSync(path.join(__dirname, 'fix.patch'), 'utf8'))
|
||||
assert.equal(patch.length, 1)
|
||||
const baseline = applyPatch(windowCandidate, reversePatch(patch[0]))
|
||||
assert.notEqual(baseline, false, 'Source changed; review fix.patch')
|
||||
assert.equal(
|
||||
createHash('sha256').update(baseline).digest('hex'),
|
||||
transform.sha256,
|
||||
'Baseline drift'
|
||||
)
|
||||
let candidate = baseline
|
||||
for (const change of transform.changes) {
|
||||
assert.equal(candidate.split(change.before).length, 2)
|
||||
candidate = candidate.replace(change.before, change.after)
|
||||
}
|
||||
return {
|
||||
root,
|
||||
sourcePath,
|
||||
sourceRelativePath: transform.path,
|
||||
baseline,
|
||||
candidate,
|
||||
windowCandidate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { sessionId } = require('./parity-cases.cjs')
|
||||
|
||||
function verifierEntry(source) {
|
||||
const marker = ' protected async waitForStructuredClaudeTuiProof(input: {'
|
||||
assert.equal(source.split(marker).length, 2)
|
||||
const method = source.slice(source.indexOf(marker), source.lastIndexOf('\n}'))
|
||||
return `
|
||||
export async function runActualVerifier(fixture: { transcriptPath: string; previousLeafUuid?: string | null }) {
|
||||
let attempts = 0
|
||||
const resolveSessionFilePath = async () => { attempts++; return fixture.transcriptPath }
|
||||
const readClaudeTranscriptLeafUuid = async (file: string, session: string, previous: string | null) =>
|
||||
(await proveClaudeTranscriptBranch({ transcriptPath: file, providerSessionId: session, previousLeafUuid: previous })).leafUuid
|
||||
const isPathWithinDirectory = () => true
|
||||
class ActualVerifier {
|
||||
getLivePtyForHandle() { return { pty: { connected: true, paneKey: 'tab:leaf', launchAgent: 'claude' } } }
|
||||
hasProviderSessionObservationSource() { return true }
|
||||
findAdoptedProviderSession() { return { launchToken: 'fresh-launch', receivedAt: 1 } }
|
||||
run() { return this.waitForStructuredClaudeTuiProof({ handle: 'handle', paneKey: 'tab:leaf', sessionId: ${JSON.stringify(sessionId)}, projectsDir: 'fixture-root', previousLeafUuid: fixture.previousLeafUuid ?? null, spawnToken: 'fresh-launch', minimumProviderSessionReceivedAt: 1 }) }
|
||||
${method}
|
||||
}
|
||||
try {
|
||||
const value = await new ActualVerifier().run()
|
||||
return { attempts, outcome: { status: 'fulfilled', value } }
|
||||
} catch (error) { return { attempts, outcome: { status: 'rejected', name: error.name, message: error.message } } }
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
async function compareVerifier(scratch, modules) {
|
||||
const row = (uuid, parentUuid) =>
|
||||
`${JSON.stringify({ type: 'user', uuid, parentUuid, sessionId })}\n`
|
||||
const marker = (leafUuid, provider = sessionId) =>
|
||||
`${JSON.stringify({ type: 'last-prompt', leafUuid, sessionId: provider })}\n`
|
||||
const root = row('root', null)
|
||||
const child = row('child', 'root')
|
||||
const cases = [
|
||||
{
|
||||
name: 'growing-empty-prefix-retries',
|
||||
contents: '',
|
||||
append: root + marker('root'),
|
||||
retry: true
|
||||
},
|
||||
{ name: 'growing-missing-marker-retries', contents: root, append: marker('root'), retry: true },
|
||||
{
|
||||
name: 'growing-missing-previous-retries',
|
||||
contents: root + marker('root'),
|
||||
append: child + marker('child'),
|
||||
previousLeafUuid: 'child',
|
||||
retry: true
|
||||
},
|
||||
{ name: 'static-empty-fatal', contents: '', error: 'Error' },
|
||||
{ name: 'static-missing-marker-fatal', contents: root, error: 'Error' },
|
||||
{
|
||||
name: 'static-missing-previous-fatal',
|
||||
contents: root + marker('root'),
|
||||
previousLeafUuid: 'child',
|
||||
error: 'ClaudeTranscriptPreviousCursorMissingError'
|
||||
},
|
||||
{
|
||||
name: 'growing-conflict-fatal',
|
||||
contents: root + row('root', 'foreign') + marker('root'),
|
||||
append: child,
|
||||
error: 'Error'
|
||||
},
|
||||
{
|
||||
name: 'growing-session-mismatch-fatal',
|
||||
contents: root + marker('root', 'foreign'),
|
||||
append: child,
|
||||
error: 'Error'
|
||||
},
|
||||
{
|
||||
name: 'growing-append-order-fatal',
|
||||
contents: child + root + marker('child'),
|
||||
append: child,
|
||||
error: 'Error'
|
||||
},
|
||||
{
|
||||
name: 'successful-prefix-stays-prefix',
|
||||
contents: root + marker('root'),
|
||||
append: child + marker('child')
|
||||
}
|
||||
]
|
||||
const reports = []
|
||||
for (const scenario of cases) {
|
||||
const phases = {}
|
||||
for (const phase of ['baseline', 'windowCandidate']) {
|
||||
const file = path.join(scratch, `verifier-${phase}.jsonl`)
|
||||
fs.writeFileSync(file, scenario.contents)
|
||||
const binding = process.binding('fs')
|
||||
const originalFstat = binding.fstat
|
||||
let injected = false
|
||||
binding.fstat = function (...args) {
|
||||
const result = originalFstat.apply(this, args)
|
||||
if (typeof result?.then !== 'function') {
|
||||
return result
|
||||
}
|
||||
return result.then((stats) => {
|
||||
if (!injected) {
|
||||
injected = true
|
||||
if (scenario.append) {
|
||||
fs.appendFileSync(file, scenario.append)
|
||||
}
|
||||
}
|
||||
return stats
|
||||
})
|
||||
}
|
||||
try {
|
||||
phases[phase] = await modules[phase].runActualVerifier({
|
||||
transcriptPath: file,
|
||||
previousLeafUuid: scenario.previousLeafUuid
|
||||
})
|
||||
} finally {
|
||||
binding.fstat = originalFstat
|
||||
fs.unlinkSync(file)
|
||||
}
|
||||
if (scenario.error) {
|
||||
assert.equal(phases[phase].outcome.name, scenario.error, `${scenario.name}/${phase}`)
|
||||
assert.equal(phases[phase].attempts, 1)
|
||||
}
|
||||
if (phase === 'windowCandidate' && scenario.retry) {
|
||||
assert.equal(phases[phase].outcome.status, 'fulfilled')
|
||||
assert.equal(phases[phase].attempts, 2)
|
||||
}
|
||||
if (scenario.name === 'successful-prefix-stays-prefix') {
|
||||
assert.equal(phases[phase].outcome.value.leafUuid, 'root')
|
||||
assert.equal(phases[phase].attempts, 1)
|
||||
}
|
||||
if (phases[phase].outcome.value) {
|
||||
delete phases[phase].outcome.value.transcriptPath
|
||||
}
|
||||
}
|
||||
reports.push({ name: scenario.name, phases })
|
||||
}
|
||||
return {
|
||||
method:
|
||||
'Exact current waitForStructuredClaudeTuiProof method body; live-owner/fresh-hook/path-resolution ports controlled, actual file reader and actual100ms retry timer.',
|
||||
reports
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { verifierEntry, compareVerifier }
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,14 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { open } from 'node:fs/promises'
|
||||
import { splitTranscriptStreamLines } from '../native-chat/transcript-stream-lines'
|
||||
|
||||
const MAX_CLAUDE_TRANSCRIPT_ANCESTRY = 10_000
|
||||
|
||||
class ClaudeTranscriptMarkerMissingError extends Error {
|
||||
constructor() {
|
||||
super('Claude transcript branch proof failed: missing last-prompt marker')
|
||||
}
|
||||
}
|
||||
|
||||
type TranscriptNode = {
|
||||
parentUuid: string | null
|
||||
sessionId: string | null
|
||||
@@ -79,25 +86,27 @@ function proveAppendOrder(nodes: Map<string, TranscriptNode>): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
contents: string
|
||||
type BranchProofInput = {
|
||||
providerSessionId: string
|
||||
previousLeafUuid: string | null
|
||||
intentionalRewindUuid?: string
|
||||
}): ClaudeTranscriptBranchProof {
|
||||
}
|
||||
|
||||
function createBranchProof(input: BranchProofInput) {
|
||||
const nodes = new Map<string, TranscriptNode>()
|
||||
let leafUuid: string | null = null
|
||||
let leafMarkerLineIndex = -1
|
||||
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 {
|
||||
if (!line.trim()) {
|
||||
continue
|
||||
return
|
||||
}
|
||||
let record: unknown
|
||||
try {
|
||||
record = JSON.parse(line)
|
||||
} catch {
|
||||
if (index === lines.length - 1 && !input.contents.endsWith('\n')) {
|
||||
if (!terminated) {
|
||||
throw new ClaudeTranscriptTailIncompleteError()
|
||||
}
|
||||
throw transcriptError('malformed JSONL')
|
||||
@@ -105,6 +114,7 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
if (typeof record !== 'object' || record === null || Array.isArray(record)) {
|
||||
throw transcriptError('non-object record')
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The parsed value is a non-array object checked above.
|
||||
const row = record as Record<string, unknown>
|
||||
if (row.type === 'last-prompt') {
|
||||
const markerSessionId = nonEmptyString(row.sessionId)
|
||||
@@ -117,7 +127,7 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
}
|
||||
const uuid = nonEmptyString(row.uuid)
|
||||
if (!uuid) {
|
||||
continue
|
||||
return
|
||||
}
|
||||
const parentUuid = row.parentUuid === null ? null : nonEmptyString(row.parentUuid)
|
||||
if (row.parentUuid !== null && !parentUuid) {
|
||||
@@ -146,94 +156,131 @@ export function proveClaudeTranscriptBranchFromJsonl(input: {
|
||||
disallowedLeaf
|
||||
})
|
||||
}
|
||||
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()
|
||||
}
|
||||
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')
|
||||
}
|
||||
ancestor = nodes.get(ancestor)?.parentUuid ?? null
|
||||
}
|
||||
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')
|
||||
}
|
||||
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) {
|
||||
proveMainLineAncestry(nodes, input.previousLeafUuid, input.providerSessionId)
|
||||
proveAppendOrder(nodes)
|
||||
return { leafUuid, relation: 'descendant' }
|
||||
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')
|
||||
}
|
||||
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')
|
||||
}
|
||||
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')
|
||||
}
|
||||
if (cursor !== null) {
|
||||
throw transcriptError('ancestry exceeds the bounded proof limit')
|
||||
}
|
||||
throw transcriptError('latest marker is on a sibling branch')
|
||||
}
|
||||
|
||||
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 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)
|
||||
}
|
||||
return proof.finish()
|
||||
}
|
||||
|
||||
export async function proveClaudeTranscriptBranch(
|
||||
input: BranchProofInput & { transcriptPath: string }
|
||||
): Promise<ClaudeTranscriptBranchProof> {
|
||||
const handle = await open(input.transcriptPath, 'r')
|
||||
try {
|
||||
// Bound this observation to the opened file's initial prefix.
|
||||
const stats = await handle.stat()
|
||||
const proof = createBranchProof(input)
|
||||
let index = 0
|
||||
if (stats.size > 0) {
|
||||
const stream = handle.createReadStream({ start: 0, end: stats.size - 1, autoClose: false })
|
||||
for await (const record of splitTranscriptStreamLines(stream)) {
|
||||
proof.add(record.line, index++, record.terminated)
|
||||
}
|
||||
}
|
||||
try {
|
||||
return proof.finish()
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof ClaudeTranscriptMarkerMissingError ||
|
||||
error instanceof ClaudeTranscriptPreviousCursorMissingError
|
||||
) {
|
||||
const grew = await handle.stat().then(
|
||||
(current) => current.size > stats.size,
|
||||
() => false
|
||||
)
|
||||
if (grew) {
|
||||
throw new ClaudeTranscriptTailIncompleteError()
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
} finally {
|
||||
await handle.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** Re-run a durable branch proof from the transcript root when a sampled cursor is stale. */
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
import { fstatSync } from 'node:fs'
|
||||
import type { FileHandle } from 'node:fs/promises'
|
||||
import type * as FsPromises from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
|
||||
type ReaderState = {
|
||||
path: string
|
||||
opens: number
|
||||
closes: number
|
||||
fd: number
|
||||
bytesRead: number
|
||||
statErrorOn: number
|
||||
readError: boolean
|
||||
afterStat: (() => Promise<void>) | null
|
||||
}
|
||||
|
||||
const state = vi.hoisted((): ReaderState => ({
|
||||
path: '',
|
||||
opens: 0,
|
||||
closes: 0,
|
||||
fd: -1,
|
||||
bytesRead: 0,
|
||||
statErrorOn: 0,
|
||||
readError: false,
|
||||
afterStat: null
|
||||
}))
|
||||
|
||||
vi.mock('node:fs/promises', async (importOriginal) => {
|
||||
const fs = await importOriginal<typeof FsPromises>()
|
||||
return {
|
||||
...fs,
|
||||
open: async (path: string, flags: string) => {
|
||||
const handle = await fs.open(path, flags)
|
||||
if (path !== state.path) {
|
||||
return handle
|
||||
}
|
||||
state.opens++
|
||||
state.fd = handle.fd
|
||||
let statsRead = 0
|
||||
return {
|
||||
stat: async () => {
|
||||
statsRead++
|
||||
if (statsRead === state.statErrorOn) {
|
||||
throw new Error('Injected stat failure')
|
||||
}
|
||||
const snapshot = await handle.stat()
|
||||
const afterStat = state.afterStat
|
||||
state.afterStat = null
|
||||
await afterStat?.()
|
||||
return snapshot
|
||||
},
|
||||
createReadStream: (options: Parameters<FileHandle['createReadStream']>[0]) => {
|
||||
const stream = handle.createReadStream(options)
|
||||
stream.once('end', () => {
|
||||
state.bytesRead += stream.bytesRead
|
||||
})
|
||||
if (state.readError) {
|
||||
queueMicrotask(() => stream.destroy(new Error('Injected read failure')))
|
||||
}
|
||||
return stream
|
||||
},
|
||||
close: async () => {
|
||||
await handle.close()
|
||||
state.closes++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
import { appendFile, mkdtemp, rename, rm, truncate, unlink, writeFile } from 'node:fs/promises'
|
||||
import {
|
||||
ClaudeTranscriptPreviousCursorMissingError,
|
||||
ClaudeTranscriptTailIncompleteError,
|
||||
proveClaudeTranscriptBranch,
|
||||
proveClaudeTranscriptBranchFromJsonl
|
||||
} 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`
|
||||
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({
|
||||
transcriptPath: state.path,
|
||||
providerSessionId: 'provider',
|
||||
previousLeafUuid,
|
||||
...(intentionalRewindUuid === undefined ? {} : { intentionalRewindUuid })
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
directory = await mkdtemp(join(tmpdir(), 'orca-branch-streaming-'))
|
||||
Object.assign(state, {
|
||||
path: join(directory, 'transcript.jsonl'),
|
||||
opens: 0,
|
||||
closes: 0,
|
||||
fd: -1,
|
||||
bytesRead: 0,
|
||||
statErrorOn: 0,
|
||||
readError: false,
|
||||
afterStat: null
|
||||
})
|
||||
await writeFile(state.path, SOURCE)
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
expect(state.closes).toBe(state.opens)
|
||||
if (state.fd >= 0) {
|
||||
expect(() => fstatSync(state.fd)).toThrow()
|
||||
}
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('decodes UTF-8 across chunks and proves old 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 =
|
||||
comment +
|
||||
row('root', null, { message: 'x'.repeat(2 * 1024 * 1024) }) +
|
||||
CHILD +
|
||||
marker('child').trimEnd()
|
||||
await writeFile(state.path, contents)
|
||||
expect(await read('root')).toEqual(
|
||||
proveClaudeTranscriptBranchFromJsonl({
|
||||
contents,
|
||||
providerSessionId: 'provider',
|
||||
previousLeafUuid: 'root'
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['malformed middle', `{"broken":\n${SOURCE}`, false],
|
||||
['unterminated malformed tail', `${SOURCE}{"broken":`, true],
|
||||
['terminated malformed tail', `${SOURCE}{"broken":\n`, false]
|
||||
] as const)(
|
||||
'preserves error classification for %s and closes the file',
|
||||
async (_name, contents, incomplete) => {
|
||||
await writeFile(state.path, contents)
|
||||
const result = read()
|
||||
await (incomplete
|
||||
? expect(result).rejects.toBeInstanceOf(ClaudeTranscriptTailIncompleteError)
|
||||
: expect(result).rejects.not.toBeInstanceOf(ClaudeTranscriptTailIncompleteError))
|
||||
}
|
||||
)
|
||||
|
||||
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(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.each([
|
||||
['empty', '', SOURCE, null],
|
||||
['missing marker', ROOT, marker('root'), null],
|
||||
['missing previous cursor', SOURCE, CHILD + marker('child'), 'child']
|
||||
] as const)(
|
||||
'marks a growing %s prefix incomplete without accepting appended proof',
|
||||
async (_name, contents, growth, previous) => {
|
||||
await writeFile(state.path, contents)
|
||||
state.afterStat = () => appendFile(state.path, growth)
|
||||
await expect(read(previous)).rejects.toBeInstanceOf(ClaudeTranscriptTailIncompleteError)
|
||||
expect((await read(previous)).leafUuid).toBe(previous ?? 'root')
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['', ROOT])('keeps a static missing marker fatal', async (contents) => {
|
||||
await writeFile(state.path, contents)
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
|
||||
it('preserves the typed static missing-cursor error for existing root reproof', async () => {
|
||||
await expect(read('absent')).rejects.toBeInstanceOf(ClaudeTranscriptPreviousCursorMissingError)
|
||||
})
|
||||
|
||||
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']
|
||||
] as const)(
|
||||
'does not turn %s into a retry when the file grows',
|
||||
async (_name, contents, message) => {
|
||||
await writeFile(state.path, contents)
|
||||
state.afterStat = () => appendFile(state.path, CHILD)
|
||||
await expect(read()).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'))
|
||||
state.statErrorOn = 2
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
|
||||
it('keeps reading the original handle after pathname replacement', async () => {
|
||||
state.afterStat = async () => {
|
||||
await rename(state.path, `${state.path}.original`)
|
||||
await writeFile(state.path, ROOT + CHILD + marker('child'))
|
||||
}
|
||||
expect((await read()).leafUuid).toBe('root')
|
||||
})
|
||||
|
||||
it('does not use a replacement file to establish growth', async () => {
|
||||
await writeFile(state.path, ROOT)
|
||||
state.afterStat = async () => {
|
||||
await rename(state.path, `${state.path}.original`)
|
||||
await writeFile(state.path, ROOT + CHILD + marker('child'))
|
||||
}
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
|
||||
it('can finish an opened file after unlink', async () => {
|
||||
state.afterStat = () => unlink(state.path)
|
||||
expect((await read()).leafUuid).toBe('root')
|
||||
})
|
||||
|
||||
it('does not infer growth from truncation', async () => {
|
||||
state.afterStat = () => truncate(state.path, 0)
|
||||
await expect(read()).rejects.toThrow('missing last-prompt marker')
|
||||
})
|
||||
|
||||
it.each(['stat', 'read'] as const)('awaits closure after a %s failure', async (failure) => {
|
||||
state.statErrorOn = failure === 'stat' ? 1 : 0
|
||||
state.readError = failure === 'read'
|
||||
await expect(read()).rejects.toThrow(`Injected ${failure} failure`)
|
||||
})
|
||||
Reference in New Issue
Block a user