@volter/supercode-interchange 0.1.22 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE-diffy-MIT +23 -0
  2. package/README.md +422 -2
  3. package/channel-records.mjs +71 -0
  4. package/channel-session-counts.mjs +81 -0
  5. package/channels-read.mjs +90 -0
  6. package/corpus-audit-records.mjs +192 -0
  7. package/corpus-audit-render.mjs +47 -0
  8. package/corpus-audit-schema.mjs +89 -0
  9. package/corpus-audit.d.ts +16 -0
  10. package/corpus-audit.mjs +132 -0
  11. package/file-io.mjs +21 -2
  12. package/harness-settings.mjs +19 -0
  13. package/hermes-fire-links.mjs +135 -0
  14. package/hermes-fires.mjs +106 -0
  15. package/hermes-read-home.d.ts +3 -0
  16. package/hermes-read-home.mjs +235 -0
  17. package/hermes-read-store.mjs +49 -0
  18. package/hermes-read-validation.mjs +170 -0
  19. package/hermes-read-workflow.mjs +164 -0
  20. package/hermes-read-yaml.mjs +68 -0
  21. package/hermes-store.mjs +1 -1
  22. package/homes.mjs +2 -2
  23. package/index.d.ts +14 -1
  24. package/index.mjs +1 -1
  25. package/inspection-builtin-tools.json +261 -0
  26. package/inspection-config-fields.json +765 -0
  27. package/inspection-schemas.d.ts +4 -0
  28. package/inspection-schemas.mjs +117 -0
  29. package/job-notepad-read.mjs +97 -0
  30. package/jobs-control.mjs +250 -0
  31. package/jobs-read.mjs +198 -0
  32. package/jsonl.mjs +4 -3
  33. package/memory-show.mjs +187 -0
  34. package/metadata.mjs +45 -3
  35. package/native-json.mjs +46 -3
  36. package/ontology-contracts.d.ts +5 -0
  37. package/ontology-contracts.mjs +29 -0
  38. package/ontology-operations.json +198 -0
  39. package/ontology-schemas.json +7946 -0
  40. package/openclaw-jobs-read.mjs +117 -0
  41. package/openclaw-profile-home.mjs +142 -0
  42. package/orchestrator-read-records.mjs +112 -0
  43. package/orchestrator-store.mjs +21 -9
  44. package/package.json +59 -4
  45. package/parity-ledger.json +6984 -0
  46. package/parity-orchestration.json +5538 -0
  47. package/parity-report.d.ts +7 -0
  48. package/parity-report.mjs +46 -0
  49. package/preset-profiles.mjs +27 -0
  50. package/profile-command.mjs +32 -0
  51. package/profile-orchestrator-door.mjs +104 -0
  52. package/profile-paths.mjs +25 -0
  53. package/profiles-control.mjs +98 -0
  54. package/profiles-read.mjs +119 -0
  55. package/reduction-attribution.mjs +54 -0
  56. package/reduction-diff.mjs +165 -0
  57. package/reduction-log.mjs +177 -0
  58. package/reduction-view.mjs +145 -0
  59. package/retained-projection.mjs +9 -0
  60. package/routes-read.mjs +103 -0
  61. package/runs-read.mjs +224 -0
  62. package/session-activity.d.ts +23 -0
  63. package/session-activity.mjs +124 -0
  64. package/session-append-scan.mjs +27 -17
  65. package/session-branch-handoff.mjs +115 -0
  66. package/session-claude-graph.mjs +29 -7
  67. package/session-claude-index.mjs +732 -114
  68. package/session-claude-runtime.d.ts +28 -0
  69. package/session-claude-runtime.mjs +171 -6
  70. package/session-claude-scan-store.mjs +164 -0
  71. package/session-claude.mjs +9 -0
  72. package/session-codex-activity.d.ts +6 -0
  73. package/session-codex-activity.mjs +120 -0
  74. package/session-codex-window.mjs +24 -4
  75. package/session-config-scan.mjs +141 -0
  76. package/session-convert.d.ts +33 -0
  77. package/session-convert.mjs +179 -0
  78. package/session-emission-version.json +1 -0
  79. package/session-file.mjs +11 -1
  80. package/session-follower.mjs +17 -5
  81. package/session-fresh-id.mjs +15 -0
  82. package/session-hermes-import.mjs +61 -0
  83. package/session-hermes.mjs +1 -1
  84. package/session-import.mjs +37 -0
  85. package/session-inspect.d.ts +18 -0
  86. package/session-inspect.mjs +122 -0
  87. package/session-machine-tree.d.ts +33 -0
  88. package/session-machine-tree.mjs +274 -0
  89. package/session-materialize-files.mjs +78 -0
  90. package/session-materialize-opencode.mjs +137 -0
  91. package/session-materialize.mjs +186 -0
  92. package/session-message.mjs +85 -0
  93. package/session-model.d.ts +9 -0
  94. package/session-model.mjs +1 -0
  95. package/session-opencode-store.mjs +39 -3
  96. package/session-path.mjs +6 -3
  97. package/session-pi-content.mjs +7 -2
  98. package/session-read.d.ts +134 -2
  99. package/session-read.mjs +86 -9
  100. package/session-recorded-config.mjs +3 -2
  101. package/session-records.mjs +12 -6
  102. package/session-residue-store.mjs +90 -0
  103. package/session-residue.mjs +150 -0
  104. package/session-resume-instructions.mjs +1 -1
  105. package/session-saved-read.mjs +105 -0
  106. package/session-transfer.mjs +141 -0
  107. package/session-views.mjs +1 -0
  108. package/session-write-claude.mjs +135 -0
  109. package/session-write-codex.mjs +146 -0
  110. package/session-write-gemini.mjs +77 -0
  111. package/session-write-goose.mjs +99 -0
  112. package/session-write-grok.mjs +70 -0
  113. package/session-write-native.mjs +41 -0
  114. package/session-write-opencode.mjs +197 -0
  115. package/session-write-pi.mjs +130 -0
  116. package/session-write-residue.mjs +19 -0
  117. package/session-write-utils.mjs +77 -0
  118. package/session-write.d.ts +10 -0
  119. package/session-write.mjs +37 -0
  120. package/skill-toml.mjs +80 -0
  121. package/skills-control.mjs +193 -0
  122. package/skills-list.mjs +237 -0
  123. package/source-files.mjs +12 -2
  124. package/sqlite-reader.mjs +134 -18
  125. package/support-registry.json +402 -0
  126. package/support-report.mjs +69 -0
  127. package/trigger-records.mjs +77 -0
  128. package/triggers-read.mjs +67 -0
  129. package/workflow-read.mjs +165 -0
@@ -15,8 +15,21 @@ import { sourceStat } from './store-registry.mjs';
15
15
  import { isCompressedSessionPath, looksLikeSessionSqlite, sessionTextError } from './session-path.mjs';
16
16
  import { detectSessionSource } from './session-source.mjs';
17
17
  import { AppendScan, shortHash } from './session-append-scan.mjs';
18
+ import { ScanStore } from './session-claude-scan-store.mjs';
19
+ import { claudeBackgroundAgentFold } from './session-claude-runtime.mjs';
18
20
 
19
21
  const REFERENCE = 'supercode.internal.read_index';
22
+ // A window read takes in at most this much transcript it has not read before: a longer one is read through over
23
+ // several reads, each answering how far it has read (`window.reading`), so that no read takes in a whole long file.
24
+ const SCAN_STEP_BYTES = 32 * 1024 * 1024;
25
+ // The messages a window read replays again as the transcript grows, at least (WindowAnswer's tail), and the prompts
26
+ // tried as the place to settle the replay before it, at most, on each try (#settleFrom, #advance).
27
+ const SETTLE_KEEP = 256, SETTLE_TRIES = 8, TAIL_RECORDS = 4096;
28
+ /** Marks are listed as a chain: the digest of the first n marks is chainMark of the digest of the first n - 1 and the
29
+ * n-th mark ('' for none), and a reader names the digest at every MARK_CHECKPOINT-th mark it holds. A read that names
30
+ * the marks it holds this way is answered the marks after the first place they differ (loadClaudeRecordWindow). */
31
+ export const MARK_CHECKPOINT = 1024;
32
+ export const chainMark = (digest, mark) => shortHash(`${digest}:${mark}`);
20
33
 
21
34
  /** What one record is to the replay, with where it stands in the file. */
22
35
  function recordFacts(line, value) {
@@ -84,7 +97,8 @@ export async function supportsClaudeReadIndex(path, { metrics } = {}) {
84
97
 
85
98
  class ClaudeReadIndex {
86
99
  #reader;
87
- #records = [];
100
+ #lines = 0;
101
+ #rawRecord;
88
102
  #groups = [];
89
103
  #skeleton = [];
90
104
  #meta = createSession('claude_code').meta;
@@ -93,50 +107,24 @@ class ClaudeReadIndex {
93
107
 
94
108
  constructor(reader) { this.#reader = reader; }
95
109
  get length() { return this.#skeleton.length; }
96
- get rawRecordCount() { return this.#records.length; }
97
-
98
- async open(fidelity) {
99
- const graph = new ClaudeReplayIndex(), residueRecords = [];
100
- for await (const line of sessionFileLines(this.#reader)) {
101
- const index = this.#records.length;
102
- let record = { offset: line.offset, length: line.length, assistant: false, error: false, messageId: null };
103
- if (nativeTrim(line.text) !== '') {
104
- let value;
105
- try { value = parseNativeJson(line.text); } catch { this.#parseErrors++; }
106
- if (value !== undefined) {
107
- captureClaudeMeta(value, this.#meta, line.text.endsWith('\n') ? line.text.slice(0, -1) : line.text);
108
- graph.observe(index, value);
109
- record = recordFacts(line, value);
110
- if (claudeResidueKind(value) != null) residueRecords.push(index);
111
- }
112
- }
113
- this.#records.push(record);
114
- }
115
- const selection = graph.selectLines(fidelity); this.#residue = selection.residue;
116
- for (const index of residueRecords) {
117
- const text = await this.#raw(index), raw = text.endsWith('\n') ? text.slice(0, -1) : text;
118
- const value = parseNativeJson(raw);
119
- captureNativeResidue(this.#meta, 'claude_code', index, raw, value, claudeResidueKind(value));
120
- }
121
- for (const group of claudeGroups(selection.lines, (index) => this.#records[index])) await this.#addGroup(group);
122
- reorderClaudeToolResults(this.#skeleton); ensureToolResultsPaired(this.#skeleton);
123
- }
110
+ get rawRecordCount() { return this.#lines; }
124
111
 
125
- async #raw(index) {
126
- const record = this.#records[index];
127
- return decodeSessionUtf8(await this.#reader.read(record.offset, record.length));
112
+ async open(fidelity, metrics) {
113
+ let scan;
114
+ try {
115
+ scan = await scanOf(this.#reader.path, this.#reader);
116
+ const view = await scan.project(this.#reader, fidelity);
117
+ if (view.decodeError) throw view.decodeError;
118
+ this.#lines = view.lines; this.#parseErrors = view.parseErrors;
119
+ this.#groups = view.groups; this.#skeleton = view.skeleton; this.#residue = view.residue;
120
+ this.#meta = await scan.indexMeta(this.#reader, view);
121
+ this.#rawRecord = index => view.raw(index, this.#reader);
122
+ retainScan(this.#reader.path, scan, metrics);
123
+ } catch (error) { dropScan(this.#reader.path); throw error; }
128
124
  }
129
125
 
130
- #decodeGroup(group) { return decodeClaudeGroup((index) => this.#raw(index), group); }
131
-
132
- async #addGroup(group) {
133
- const groupId = this.#groups.length, decoded = await this.#decodeGroup(group);
134
- this.#groups.push(group);
135
- decoded.forEach((message, ordinal) => {
136
- skeletonMessage(message).metadata[REFERENCE] = `${groupId}:${ordinal}`;
137
- this.#skeleton.push(message);
138
- });
139
- }
126
+ async #raw(index) { return this.#rawRecord(index); }
127
+ #decodeGroup(group) { return decodeClaudeGroup(index => this.#raw(index), group); }
140
128
 
141
129
  itemCount(from, end) {
142
130
  if (!Number.isSafeInteger(from) || !Number.isSafeInteger(end) || from < 0 || end < from || end > this.length) throw new RangeError('invalid Claude message range');
@@ -189,166 +177,796 @@ class ClaudeReadIndex {
189
177
  * are verified before the result leaves the scope; any change refuses it. */
190
178
  export async function withClaudeReadIndex(path, work, { fidelity = 'byte_lossless', metrics } = {}) {
191
179
  if (!['byte_lossless', 'value_lossless', 'semantic'].includes(fidelity)) throw new TypeError(`unknown session fidelity: ${fidelity}`);
192
- return withSessionFile(path, async (reader) => {
193
- const index = new ClaudeReadIndex(reader); await index.open(fidelity);
194
- return work(index);
195
- }, metrics);
180
+ try {
181
+ return await withSessionFile(path, async (reader) => {
182
+ const index = new ClaudeReadIndex(reader); await index.open(fidelity, metrics);
183
+ return work(index);
184
+ }, metrics);
185
+ } catch (error) { dropScan(path); throw error; }
196
186
  }
197
187
 
198
188
  const REPLAYED = new Set(['user', 'assistant', 'attachment', 'system']);
199
189
 
200
190
  /** One line taken into a scan's state: its place among the file's lines, its header facts, its node in the graph,
201
191
  * what it changes of the recorded profile and, for a record the replay can select, where it stands and the hash of
202
- * its bytes. */
192
+ * its bytes. A scan (`state.store`) keeps its records, nodes and residue on disk; a line read for one answer alone
193
+ * keeps them in that answer's state. */
203
194
  function observeClaudeLine(state, line) {
204
195
  const index = state.lines++;
205
196
  if (nativeTrim(line.text) === '') return;
206
197
  let value;
207
198
  try { value = parseNativeJson(line.text); } catch { state.parseErrors++; return; }
208
199
  captureClaudeMeta(value, state.meta, line.text.endsWith('\n') ? line.text.slice(0, -1) : line.text);
200
+ const nodes = state.graph.nodes.length;
209
201
  state.graph.observe(index, value);
202
+ if (state.store && state.graph.nodes.length > nodes) state.store.addNode(state.graph.nodes.at(-1));
210
203
  state.profile.record(index, value);
204
+ state.agents.observe(value, index + 1);
211
205
  if (typeof value?.version === 'string') state.version = value.version;
212
- if (REPLAYED.has(stringField(value, 'type'))) state.records.set(index, { ...recordFacts(line, value), hash: shortHash(line.bytes) });
206
+ if (REPLAYED.has(stringField(value, 'type'))) {
207
+ const facts = { ...recordFacts(line, value), hash: shortHash(line.bytes) };
208
+ state.records.set(index, facts); state.store?.addRecord(index, facts);
209
+ }
210
+ const kind = claudeResidueKind(value);
211
+ if (kind != null) {
212
+ const captured = { native_residue_source: null, native_residue: [] };
213
+ captureNativeResidue(captured, 'claude_code', index, line.text.endsWith('\n') ? line.text.slice(0, -1) : line.text, value, kind);
214
+ if (captured.native_residue.length) {
215
+ // A scan keeps where each residue record stands, not its text: an options load reads it again (indexMeta).
216
+ const entry = captured.native_residue[0];
217
+ if (state.store) { delete entry.raw; state.store.addResidue(index, line.offset, line.length, entry); }
218
+ else state.nativeResidue.set(index, { entry });
219
+ }
220
+ }
213
221
  }
214
222
 
215
223
  /** A transcript's complete lines as one pass over them leaves them, kept between reads of the same file: the next
216
- * read takes only the lines written since (`extend`). Nothing a record says is kept, only where it is and what it is
217
- * to the replay. */
224
+ * read takes only the lines written since (`extend`). What it knows of each record is on disk (ScanStore); in memory
225
+ * it holds the records its next read replays (`graph` and `records`: the settled record and every one after it, or
226
+ * none while nothing is settled) and facts of a size the transcript's length does not set. */
218
227
  class ClaudeRecordScan extends AppendScan {
219
- records = new Map(); lines = 0; parseErrors = 0; version = null;
228
+ // A history scan (loadClaudeHistoryWindow) replays every segment before the latest compaction; any other replays
229
+ // from the latest compaction on.
230
+ history = false;
231
+ store = new ScanStore(); records = new Map(); indexMetaCache = null; lines = 0; parseErrors = 0; version = null;
220
232
  graph = new ClaudeReplayIndex(); meta = createSession('claude_code').meta; profile = recordedProfileFold('claude_code');
221
- skeletons = new Map(); view = null;
233
+ // Every record, before any compaction the replay applies: a background agent launched long before is still known.
234
+ agents = claudeBackgroundAgentFold();
235
+ // Whether the last pass stopped before the file's last whole line (SCAN_STEP_BYTES): the scan is not read through.
236
+ behind = false;
237
+ // The replay's settled prefix (settle), the tail's decoded groups and the last window answer made over it.
238
+ settled = null; regionMade = new Map(); answer = null;
222
239
 
223
- /** Charge retained replay records and derived entries, including one entry for the scan itself. Progress and other discarded lines
224
- * only advance `lines`: charging them as retained records made two long, mostly-progress transcripts evict one
225
- * another on every alternating read, though their replay state together was far below the bound. */
240
+ /** What the scan holds in memory, counted as entries: the scans kept between reads are bounded by them. */
226
241
  get retainedRecords() {
227
- // The record's graph node and linear-line index are views of that same retained record, not two more records.
228
- // They are bounded by `records.size`; fork anchors and derived profile/message entries are charged separately.
229
- return 1 + this.records.size + this.graph.segmentAnchors.size + this.profile.history.length + this.skeletons.size
230
- + (this.view?.skeleton.length ?? 0) + (this.view?.groups.length ?? 0);
242
+ return 1 + this.agents.retainedEntries + this.records.size + this.graph.nodes.length + this.graph.segmentAnchors.size + this.profile.history.length
243
+ + (this.indexMetaCache?.meta.native_residue.length ?? 0) + this.regionMade.size + (this.settled?.checkpoints.length ?? 0) + (this.settled?.dangling.size ?? 0)
244
+ + (this.answer?.skeleton.length ?? 0) + (this.answer?.groups.length ?? 0);
231
245
  }
232
246
 
233
- /** The lines completed since the last pass. A last line with no LF yet stays outside the scan: `view` reads it. */
234
- async extend(reader) {
235
- const from = this.bytes;
236
- for await (const line of sessionFileLines(reader, this.bytes)) {
237
- if (!line.terminated) break;
238
- observeClaudeLine(this, line); this.hashed.update(line.bytes); this.bytes = line.offset + line.length;
247
+ /** The lines completed since the last pass, at most `budget` bytes of them (a window read takes SCAN_STEP_BYTES a
248
+ * pass, so a long transcript is read through over several passes and no one pass reads it all). A last line with
249
+ * no LF yet stays outside the scan: `view` reads it. */
250
+ async extend(reader, budget = Infinity) {
251
+ const from = this.bytes; this.behind = false;
252
+ this.store.begin();
253
+ let failed = true;
254
+ try {
255
+ for await (const line of sessionFileLines(reader, this.bytes)) {
256
+ if (!line.terminated) break;
257
+ if (this.bytes - from >= budget) { this.behind = true; break; }
258
+ observeClaudeLine(this, line); this.took(line.bytes, line.offset + line.length); this.bytes = line.offset + line.length;
259
+ }
260
+ failed = false;
261
+ } finally { this.store.end(failed); }
262
+ await this.settle(reader, from, this.behind);
263
+ }
264
+
265
+ /** The whole replay's graph and records, read from disk: what a whole replay (project) and settling read, and let go
266
+ * when it is done. */
267
+ #whole() {
268
+ const graph = new ClaudeReplayIndex();
269
+ for (const node of this.store.allNodes()) { graph.byUuid.set(node.uuid, graph.nodes.length); graph.nodes.push(node); }
270
+ const records = this.store.allRecords();
271
+ graph.linearLines = [...records.keys()];
272
+ graph.segmentAnchors = new Set(this.graph.segmentAnchors); graph.lastPrompt = this.graph.lastPrompt == null ? null : [...this.graph.lastPrompt];
273
+ return { graph, records };
274
+ }
275
+
276
+ /** A group's decoded skeletons: the tail's, else those on disk, else decoded from its records (and kept on disk). */
277
+ async #made(mark, group, raw, reader) {
278
+ let decoded = this.regionMade.get(mark) ?? this.store.madeOf(mark);
279
+ if (!decoded) {
280
+ decoded = (await decodeClaudeGroup((index) => raw(index, reader), group)).map((message) => ({ model: Object.hasOwn(message.metadata, 'model') ? message.metadata.model : null, message: skeletonMessage(message) }));
281
+ this.store.addMade(mark, decoded);
239
282
  }
240
- await this.settle(reader, from);
283
+ return decoded;
241
284
  }
242
285
 
243
- /** The replay of the file as `reader` holds it: its messages in order, each as its skeleton with the mark of the
244
- * records that make it. A group's skeleton is decoded once and kept while the replay selects it. */
245
- async project(reader, fidelity) {
246
- const key = `${fidelity}:${this.bytes}:${reader.size}`;
247
- if (this.view?.key === key) return this.view;
248
- const state = { lines: this.lines, parseErrors: this.parseErrors, version: this.version, records: new Map(),
249
- graph: this.graph.clone(), meta: cloneNativeJson(this.meta), profile: this.profile.clone() };
286
+ /** The replay of the file as `reader` holds it, whole: its messages in order, each as its skeleton with the mark of
287
+ * the records that make it. Read from disk and let go after use: the scan keeps no whole replay. A scan not read
288
+ * through (`committed`) is replayed as far as it has read. */
289
+ async project(reader, fidelity, committed = false) {
290
+ const key = committed ? `${fidelity}:${this.bytes}:committed` : `${fidelity}:${this.bytes}:${reader.size}:${reader.size > this.bytes ? reader.fingerprint.mtime_ns : "complete"}`;
291
+ const whole = this.#whole();
292
+ const state = { lines: this.lines, parseErrors: this.parseErrors, version: this.version, records: new Map(), nativeResidue: new Map(),
293
+ graph: whole.graph, meta: cloneNativeJson(this.meta), profile: this.profile.clone(), agents: claudeBackgroundAgentFold() };
250
294
  // The file's last line while it has no LF yet is read for this answer alone. Cut inside a character, it is a
251
295
  // line still being written: counted as one that does not parse, as a cut record is.
252
- if (reader.size > this.bytes) {
296
+ if (!committed && reader.size > this.bytes) {
253
297
  const bytes = await reader.read(this.bytes, reader.size - this.bytes); let text = null;
254
- try { text = decodeSessionUtf8(bytes); } catch { state.lines++; state.parseErrors++; }
298
+ try { text = decodeSessionUtf8(bytes); } catch (error) { state.lines++; state.parseErrors++; state.decodeError = error; }
255
299
  if (text != null) observeClaudeLine(state, { offset: this.bytes, length: bytes.length, text, bytes });
256
300
  }
257
- const recordOf = (index) => state.records.get(index) ?? this.records.get(index);
301
+ const recordOf = (index) => state.records.get(index) ?? whole.records.get(index);
258
302
  const raw = async (index, at) => { const record = recordOf(index); return decodeSessionUtf8(await at.read(record.offset, record.length)); };
259
- const selection = state.graph.selectLines(fidelity), groups = [], sizes = [], skeleton = [], skeletons = new Map(), marks = new Map(), models = new Map();
303
+ const selection = state.graph.selectLines(fidelity, { history: this.history }), groups = [], sizes = [], skeleton = [], marks = new Map(), models = new Map(), made = new Map();
260
304
  for (const group of claudeGroups(selection.lines, recordOf)) {
261
305
  const id = groups.length, records = group.map(recordOf), mark = shortHash(records.map((record) => record.hash).join(','));
262
- let made = skeletons.get(mark) ?? this.skeletons.get(mark);
263
- if (!made) made = (await decodeClaudeGroup((index) => raw(index, reader), group)).map((message) => ({ model: Object.hasOwn(message.metadata, 'model') ? message.metadata.model : null, message: skeletonMessage(message) }));
264
- skeletons.set(mark, made); groups.push(group); sizes.push(records.reduce((sum, record) => sum + record.length, 0));
265
- made.forEach(({ message, model }, ordinal) => {
306
+ const decoded = made.get(mark) ?? await this.#made(mark, group, raw, reader);
307
+ made.set(mark, decoded); groups.push(group); sizes.push(records.reduce((sum, record) => sum + record.length, 0));
308
+ decoded.forEach(({ message, model }, ordinal) => {
266
309
  const placed = { ...message, metadata: Object.create(null) }; placed.metadata[REFERENCE] = `${id}:${ordinal}`;
267
310
  marks.set(placed, shortHash(`${mark}:${ordinal}`)); if (model != null) models.set(placed, model);
268
311
  skeleton.push(placed);
269
312
  });
270
313
  }
271
- this.skeletons = skeletons;
272
314
  reorderClaudeToolResults(skeleton); ensureToolResultsPaired(skeleton);
273
315
  // A result the pairing pass made up has no record: its mark is of what it says.
274
- return this.view = { key, raw, groups, sizes, skeleton, marks: skeleton.map((message) => marks.get(message) ?? shortHash(JSON.stringify(messageJson(message)))),
316
+ return { key, raw, groups, sizes, skeleton, marks: skeleton.map((message) => marks.get(message) ?? shortHash(JSON.stringify(messageJson(message)))),
275
317
  model: models.get(skeleton.findLast((message) => models.has(message))) ?? null, residue: selection.residue, meta: state.meta, lines: state.lines,
276
- parseErrors: state.parseErrors, version: state.version, history: state.profile.history };
318
+ parseErrors: state.parseErrors, version: state.version, history: state.profile.history, nativeResidue: [...state.nativeResidue],
319
+ decodeError: state.decodeError ?? null, agents: this.agents.finish({ also: state.agents }), graph: whole.graph, records: whole.records, made };
320
+ }
321
+
322
+ // Options loads historically include native metadata residue even when they
323
+ // return no messages. Keep its original post-header ordering, rather than
324
+ // folding it before a later provenance envelope restores metadata.
325
+ async indexMeta(reader, view) {
326
+ const key = view.key;
327
+ if (this.indexMetaCache?.key === key) return this.indexMetaCache.meta;
328
+ const meta = cloneNativeJson(view.meta);
329
+ if (meta.native_residue_source == null || meta.native_residue_source === 'claude_code') {
330
+ let any = false;
331
+ for (const [, { entry, offset, length }] of [...this.store.residues(), ...view.nativeResidue]) {
332
+ const placed = cloneNativeJson(entry); any = true;
333
+ if (offset != null) { const text = decodeSessionUtf8(await reader.read(offset, length)); placed.raw = text.endsWith('\n') ? text.slice(0, -1) : text; }
334
+ meta.native_residue.push(placed);
335
+ }
336
+ if (any) meta.native_residue_source = 'claude_code';
337
+ }
338
+ this.indexMetaCache = { key, meta };
339
+ return meta;
340
+ }
341
+
342
+ /**
343
+ * The replay a window read answers from, made without replaying the whole transcript on every read. The replay is
344
+ * held as a settled prefix (every message up to and including a user record, its places, marks and groups on disk,
345
+ * the digests of the chain of its marks in memory) and the tail after it, which alone is replayed again as the
346
+ * transcript grows: the tail's records are replayed as a graph of their own, rooted at that record, and the prefix is
347
+ * kept as it was. That is the whole replay's answer whenever nothing the tail adds reaches back past the record
348
+ * (settledAnswer says what is checked); anything that does (a branch from an older message, a compaction, a record
349
+ * whose parent is not in the tail, a tool result for an older call, a newer chunk of an older message) is replayed
350
+ * whole (project) and settled anew. In a history replay a compaction reaches back past nothing: the replay keeps the
351
+ * history before it, and its boundary (a record with no parent) opens a segment after every one before it, so a tail
352
+ * that holds one replays alone as any other does. The settled prefix moves on as the tail grows (advance), so each
353
+ * read replays a bounded tail, and the scan holds in memory only that tail.
354
+ */
355
+ /** Why this read replays the whole transcript: said in its answer (`window.replay`) and counted. */
356
+ #whole_(reason) { this.wholeReason = reason; return null; }
357
+
358
+ async window(reader, fidelity, committed = false) {
359
+ if (fidelity !== 'semantic') return wholeAnswer(await this.project(reader, fidelity, committed), this.store, 'a fidelity other than semantic');
360
+ const key = committed ? `${fidelity}:${this.bytes}:committed` : `${fidelity}:${this.bytes}:${reader.size}:${reader.size > this.bytes ? reader.fingerprint.mtime_ns : "complete"}`;
361
+ if (this.answer?.key === key) return this.answer;
362
+ this.wholeReason = this.unsettled ? 'no place to settle the replay' : 'nothing settled yet';
363
+ let answer = this.settled == null ? null : await this.#settledAnswer(reader, key, committed);
364
+ if (answer == null) { const reason = this.wholeReason; answer = this.#settleFrom(await this.project(reader, fidelity, committed)); answer.replay = { whole: true, reason }; }
365
+ else { this.#advance(answer); answer.replay = { whole: false, reason: null }; }
366
+ // only an answer over a settled prefix is kept: a whole one is let go with its read
367
+ this.answer = this.settled == null ? null : answer;
368
+ return answer;
369
+ }
370
+
371
+ /** The tail after the settled prefix replayed alone, with the transcript's unterminated last line for this answer;
372
+ * null when the whole transcript must be replayed instead. */
373
+ async #settledAnswer(reader, key, committed) {
374
+ const settled = this.settled, zone = this.graph, store = this.store;
375
+ // A fork-context reference written since changes which parents the whole replay finds absent.
376
+ if (zone.segmentAnchors.size !== settled.anchors) return this.#whole_("fork references written since");
377
+ // The tail's graph: the settled record as its root, and every record after it.
378
+ const tail = new ClaudeReplayIndex(), root = zone.nodes[0];
379
+ tail.segmentAnchors = new Set(zone.segmentAnchors);
380
+ tail.lastPrompt = zone.lastPrompt == null ? null : [...zone.lastPrompt];
381
+ tail.byUuid.set(root.uuid, 0); tail.nodes.push({ ...root, parent: null });
382
+ for (let index = 1; index < zone.nodes.length; index++) {
383
+ const node = zone.nodes[index];
384
+ tail.byUuid.set(node.uuid, tail.nodes.length); tail.nodes.push({ ...node });
385
+ }
386
+ tail.linearLines = zone.linearLines.slice(firstAbove(zone.linearLines, settled.lineIndex - 1));
387
+ const state = { lines: this.lines, parseErrors: this.parseErrors, version: this.version, records: new Map(), nativeResidue: new Map(),
388
+ graph: tail, meta: cloneNativeJson(this.meta), profile: this.profile.clone(), agents: claudeBackgroundAgentFold() };
389
+ if (!committed && reader.size > this.bytes) {
390
+ const bytes = await reader.read(this.bytes, reader.size - this.bytes); let text = null;
391
+ try { text = decodeSessionUtf8(bytes); } catch (error) { state.lines++; state.parseErrors++; state.decodeError = error; }
392
+ if (text != null) {
393
+ // A record that repeats a uuid the whole replay holds is refused by it, not by the tail's graph.
394
+ try { const uuid = stringField(parseNativeJson(text), 'uuid'); if (uuid != null && store.hasUuid(uuid)) return this.#whole_("a record repeats a uuid"); } catch { /* counted below */ }
395
+ try { observeClaudeLine(state, { offset: this.bytes, length: bytes.length, text, bytes }); } catch { return this.#whole_("a record repeats a uuid"); }
396
+ }
397
+ }
398
+ // What the tail adds must not reach back past the settled record.
399
+ for (let at = 1; at < tail.nodes.length; at++) {
400
+ const node = tail.nodes[at];
401
+ // a record a parent absent from the whole replay was waiting for would join what it anchored as a root
402
+ if (!this.history && (node.compact != null || node.parent == null || settled.dangling.has(node.uuid))) return this.#whole_("a compaction, a new root or an absent parent in the tail");
403
+ if (settled.dangling.has(node.uuid)) return this.#whole_("a record an absent parent was waiting for");
404
+ // in a history replay a compaction's boundary opens the segment after every one before it; any other record with
405
+ // no parent is a root the whole replay stitches in as a severed segment
406
+ if (node.parent == null) { if (node.compact == null) return this.#whole_("a new root in the tail"); continue; }
407
+ if (!tail.byUuid.has(node.parent) && !zone.segmentAnchors.has(node.parent)) return this.#whole_("a record whose parent is older than the settled prefix");
408
+ if (node.messageId != null && (store.firstLineOf(node.messageId) ?? node.lineIndex) <= settled.lineIndex) return this.#whole_("a message begun before the settled prefix");
409
+ }
410
+ if (tail.lastPrompt?.[1] && !tail.byUuid.has(tail.lastPrompt[0]) && store.hasUuid(tail.lastPrompt[0])) return this.#whole_("the last prompt names a record older than the settled prefix");
411
+ const selection = tail.selectLines('semantic', { history: this.history });
412
+ if (selection.residue.length) return this.#whole_("the tail does not replay alone");
413
+ const recordOf = (index) => state.records.get(index) ?? this.records.get(index);
414
+ const raw = async (index, at) => { const record = recordOf(index) ?? store.recordAt(index); return decodeSessionUtf8(await at.read(record.offset, record.length)); };
415
+ const made = new Map(), groups = [], sizes = [], skeleton = [], marks = new Map(), models = new Map(), base = settled.groups;
416
+ // The settled record's group opens the tail's replay, alone: it is the settled prefix's last, already placed.
417
+ const listed = [...claudeGroups(selection.lines, recordOf)];
418
+ if (listed[0]?.length !== 1 || listed[0][0] !== root.lineIndex) return this.#whole_("the tail does not replay alone");
419
+ listed.shift();
420
+ for (const group of listed) {
421
+ const id = base + groups.length, records = group.map(recordOf), mark = shortHash(records.map((record) => record.hash).join(','));
422
+ const decoded = made.get(mark) ?? await this.#made(mark, group, raw, reader);
423
+ made.set(mark, decoded); groups.push(group); sizes.push(records.reduce((sum, record) => sum + record.length, 0));
424
+ decoded.forEach(({ message, model }, ordinal) => {
425
+ const placed = { ...message, metadata: Object.create(null) }; placed.metadata[REFERENCE] = `${id}:${ordinal}`;
426
+ marks.set(placed, shortHash(`${mark}:${ordinal}`)); if (model != null) models.set(placed, model);
427
+ skeleton.push(placed);
428
+ });
429
+ }
430
+ // The tail's tool results answer only its own calls, and its calls are none the prefix made or answered: so the
431
+ // whole replay's pairing and ordering of results changes nothing in the prefix, and in the tail does what it does
432
+ // to the tail alone.
433
+ for (const message of skeleton) {
434
+ if (message.role === 'tool' && message.tool_call_id != null && message.tool_call_id !== '' && store.tool(message.tool_call_id)?.call) return this.#whole_("a tool result for an older call");
435
+ if (message.role === 'assistant') for (const call of message.tool_calls ?? []) if (call.id !== '' && store.tool(call.id) != null) return this.#whole_("a tool call naming an older one");
436
+ }
437
+ reorderClaudeToolResults(skeleton); ensureToolResultsPaired(skeleton);
438
+ this.regionMade = made;
439
+ return new WindowAnswer({ key, raw, settled, store, groups, sizes, skeleton, made, tailNodes: tail.nodes.slice(1), committed: zone.nodes.length - 1,
440
+ marks: skeleton.map((message) => marks.get(message) ?? shortHash(JSON.stringify(messageJson(message)))),
441
+ model: models.get(skeleton.findLast((message) => models.has(message))) ?? settled.model, residue: [...settled.residue], meta: state.meta, lines_read: state.lines,
442
+ parseErrors: state.parseErrors, version: state.version, history: state.profile.history, decodeError: state.decodeError ?? null, agents: this.agents.finish({ also: state.agents }) });
443
+ }
444
+
445
+ /** A whole replay (project's view) as a window answer, its prefix settled where it can be: the prefix is written to
446
+ * disk and the scan keeps in memory only the records after the settled one. */
447
+ #settleFrom(view) {
448
+ const graph = view.graph, store = this.store;
449
+ this.settled = null; this.regionMade = new Map();
450
+ const groupOf = (message) => { const reference = message.metadata[REFERENCE]; return reference === undefined ? null : Number(reference.split(':')[0]); };
451
+ const markOf = (group) => shortHash(group.map((index) => view.records.get(index).hash).join(','));
452
+ // a replay from the latest compaction on settles after it, at no record it keeps; a history replay anywhere
453
+ const boundary = this.history ? null : graph.nodes.findLast((node) => node.compact != null) ?? null;
454
+ const kept = new Set(boundary == null ? [] : [boundary.compact.anchor, ...boundary.compact.preserved, ...(boundary.compact.segment ?? [])]);
455
+ const lastLine = new Map(); view.groups.forEach((group, id) => lastLine.set(id, group.reduce((max, index) => Math.max(max, index), -1)));
456
+ // The parents the whole replay finds absent (anchored as roots, as its residue says): a record that later brings
457
+ // one is replayed whole again. Its residue stands for every tail replayed against this prefix.
458
+ const dangling = new Set();
459
+ for (const node of graph.nodes) if (node.parent != null && !graph.byUuid.has(node.parent) && !graph.segmentAnchors.has(node.parent)) dangling.add(node.parent);
460
+ let cut = null;
461
+ // A replay that fell back to every record in transcript order is not a graph a tail can be replayed against.
462
+ if (!view.decodeError && !view.residue.some((line) => line.startsWith('the Claude record graph could not be projected'))) {
463
+ // the last of the skeleton's groups past which SETTLE_KEEP messages stand (half of a shorter replay's) is where
464
+ // the search starts
465
+ let position = view.skeleton.length - Math.min(SETTLE_KEEP, Math.floor(view.skeleton.length / 2)), tries = 0;
466
+ for (; position > 0 && cut == null && tries < SETTLE_TRIES; position--) {
467
+ const id = groupOf(view.skeleton[position - 1]);
468
+ if (id == null || view.groups[id].length !== 1) continue;
469
+ const nodeIndex = nodeAt(graph.nodes, view.groups[id][0]), node = nodeIndex === undefined ? null : graph.nodes[nodeIndex];
470
+ if (!settles(node, this.lines, boundary, kept)) continue;
471
+ tries++;
472
+ if (groupOf(view.skeleton[position] ?? { metadata: {} }) === id) continue; // the record's group ends here
473
+ if (this.#cutHolds(view.skeleton, view.groups, position, id, node, lastLine, view.records) && this.#rooted(node, graph.nodes.slice(nodeIndex + 1), dangling, graph.segmentAnchors)) cut = { position, id, nodeIndex, node };
474
+ }
475
+ }
476
+ this.unsettled = cut == null;
477
+ if (cut == null) {
478
+ // nothing settles: every read replays the whole transcript from disk, and the scan keeps no record in memory
479
+ this.graph = zoneOf(this.graph, []); this.records = new Map();
480
+ return wholeAnswer(view, store);
481
+ }
482
+ const settled = { lineIndex: cut.node.lineIndex, uuid: cut.node.uuid, length: 0, groups: 0, digest: '', checkpoints: [], model: null,
483
+ residue: [...view.residue], dangling, anchors: graph.segmentAnchors.size };
484
+ store.batch(() => {
485
+ store.clearSettled();
486
+ for (let id = 0; id <= cut.id; id++) {
487
+ store.addGroup(id, view.groups[id], view.sizes[id]); settled.groups++;
488
+ for (const { model } of view.made.get(markOf(view.groups[id])) ?? []) if (model != null) settled.model = model;
489
+ }
490
+ appendSettled(settled, store, view.skeleton.slice(0, cut.position), view.marks.slice(0, cut.position));
491
+ });
492
+ this.settled = settled;
493
+ const groups = view.groups.slice(cut.id + 1), skeleton = view.skeleton.slice(cut.position);
494
+ for (const group of groups) { const mark = markOf(group), decoded = view.made.get(mark); if (decoded) this.regionMade.set(mark, decoded); }
495
+ // in memory, the settled record and every record after it
496
+ this.graph = zoneOf(graph, graph.nodes.slice(cut.nodeIndex));
497
+ this.records = new Map([...view.records].filter(([line]) => line >= cut.node.lineIndex));
498
+ return new WindowAnswer({ key: view.key, raw: view.raw, settled, store, groups, sizes: view.sizes.slice(cut.id + 1), skeleton, made: this.regionMade,
499
+ tailNodes: this.graph.nodes.slice(1), committed: this.graph.nodes.length - 1, marks: view.marks.slice(cut.position), model: view.model, residue: view.residue,
500
+ meta: view.meta, lines_read: view.lines, parseErrors: view.parseErrors, version: view.version, history: view.history, decodeError: view.decodeError, agents: view.agents });
501
+ }
502
+
503
+ /** The tail's prefix up to a later user record, settled when the tail has grown past twice SETTLE_KEEP messages or
504
+ * TAIL_RECORDS records (records that make no message, as progress records, are replayed too). */
505
+ #advance(answer) {
506
+ const settled = this.settled, store = this.store;
507
+ if (answer.skeleton.length <= 2 * SETTLE_KEEP && answer.tailNodes.length <= TAIL_RECORDS) return;
508
+ const base = settled.groups, groupOf = (message) => { const reference = message.metadata[REFERENCE]; return reference === undefined ? null : Number(reference.split(':')[0]) - base; };
509
+ const lastLine = new Map(); answer.groups.forEach((group, id) => lastLine.set(id, group.reduce((max, index) => Math.max(max, index), -1)));
510
+ let tries = 0;
511
+ for (let position = answer.skeleton.length - Math.min(SETTLE_KEEP, Math.floor(answer.skeleton.length / 2)); position > 0 && tries < SETTLE_TRIES; position--) {
512
+ const id = groupOf(answer.skeleton[position - 1]);
513
+ if (id == null || answer.groups[id].length !== 1) continue;
514
+ const at = answer.tailNodes.findIndex((node) => node.lineIndex === answer.groups[id][0]);
515
+ if (at === -1 || at >= answer.committed) continue;
516
+ const node = answer.tailNodes[at];
517
+ if (!settles(node, this.lines, null, new Set())) continue;
518
+ tries++;
519
+ if (groupOf(answer.skeleton[position] ?? { metadata: {} }) === id) continue;
520
+ if (!this.#cutHolds(answer.skeleton, answer.groups, position, id, node, lastLine, this.records, base)
521
+ || !this.#rooted(node, answer.tailNodes.slice(at + 1, answer.committed), settled.dangling, this.graph.segmentAnchors)) continue;
522
+ store.batch(() => {
523
+ for (let group = 0; group <= id; group++) {
524
+ const records = answer.groups[group], mark = shortHash(records.map((index) => this.records.get(index).hash).join(','));
525
+ store.addGroup(settled.groups++, records, answer.sizes[group]);
526
+ const decoded = answer.made.get(mark); this.regionMade.delete(mark);
527
+ for (const { model } of decoded ?? []) if (model != null) settled.model = model;
528
+ }
529
+ appendSettled(settled, store, answer.skeleton.slice(0, position), answer.marks.slice(0, position));
530
+ });
531
+ settled.lineIndex = node.lineIndex; settled.uuid = node.uuid;
532
+ // in memory, the newly settled record and every record after it
533
+ this.graph = zoneOf(this.graph, this.graph.nodes.slice(at + 1));
534
+ for (const line of this.records.keys()) if (line < node.lineIndex) this.records.delete(line);
535
+ // the answer already given stands: it is the same replay, its prefix now settled
536
+ answer.reframe(settled, id + 1, position, at + 1);
537
+ return;
538
+ }
539
+ }
540
+
541
+ /** Whether every record after `root` (`after`) is replayed in a tail rooted at it: as the tail replay requires
542
+ * (settledAnswer), none reaches back past it, none is a root (but a compaction's boundary, in a history replay),
543
+ * none brings an absent parent, and no assistant message among them began before it. */
544
+ #rooted(root, after, dangling, anchors) {
545
+ const inside = new Set([root.uuid]);
546
+ for (const node of after) {
547
+ if (!this.history && node.compact != null) return false;
548
+ if (dangling.has(node.uuid) || node.parent == null && node.compact == null) return false;
549
+ if (node.parent != null && !inside.has(node.parent) && !anchors.has(node.parent)) return false;
550
+ if (node.messageId != null && (this.store.firstLineOf(node.messageId) ?? node.lineIndex) <= root.lineIndex) return false;
551
+ inside.add(node.uuid);
552
+ }
553
+ return true;
554
+ }
555
+
556
+ /** Whether the replay `skeleton` (of `groups`) can be settled through the group `id` of the user record `node`, ending
557
+ * at `position`: every message before it is of a group up to that one and every one after of a later group (or made
558
+ * up by the pairing pass for a call of its own side), every record before it was written before that record and
559
+ * every one after after it, no result after it answers a call before it, no call after it repeats one before it,
560
+ * and no message after it began before it. */
561
+ #cutHolds(skeleton, groups, position, id, node, lastLine, records, base = 0) {
562
+ const groupOf = (message) => { const reference = message.metadata[REFERENCE]; return reference === undefined ? null : Number(reference.split(':')[0]) - base; };
563
+ for (let at = 0; at < skeleton.length; at++) {
564
+ const group = groupOf(skeleton[at]);
565
+ if (group != null && (at < position ? group > id : group <= id)) return false;
566
+ }
567
+ for (let group = 0; group < groups.length; group++) {
568
+ if (group <= id ? lastLine.get(group) > node.lineIndex : groups[group].some((index) => index <= node.lineIndex)) return false;
569
+ }
570
+ const before = { calls: new Set(), results: new Set() };
571
+ for (let at = 0; at < position; at++) {
572
+ const message = skeleton[at];
573
+ if (message.role === 'assistant') for (const call of message.tool_calls ?? []) if (call.id !== '') before.calls.add(call.id);
574
+ if (message.role === 'tool' && message.tool_call_id != null) before.results.add(message.tool_call_id);
575
+ }
576
+ for (let at = position; at < skeleton.length; at++) {
577
+ const message = skeleton[at];
578
+ if (message.role === 'tool' && message.tool_call_id != null && message.tool_call_id !== '' && before.calls.has(message.tool_call_id)) return false;
579
+ if (message.role === 'assistant') for (const call of message.tool_calls ?? []) if (call.id !== '' && (before.calls.has(call.id) || before.results.has(call.id))) return false;
580
+ }
581
+ for (let group = id + 1; group < groups.length; group++) {
582
+ for (const index of groups[group]) {
583
+ const record = records.get(index);
584
+ if (record?.messageId != null && record.assistant && (this.store.firstLineOf(record.messageId) ?? index) <= node.lineIndex) return false;
585
+ }
586
+ }
587
+ return true;
277
588
  }
278
589
  }
279
590
 
280
- // The scans of the files read most recently, at most this many retained replay entries between them: a transcript read again is
281
- // read from where its scan stopped, and one read long ago is scanned again. Of the files shorter than
282
- // SCAN_KEPT_BYTES only the one read last is kept (a pass reads its file more than once): scanning a short file again
283
- // costs less than keeping the scan of every one read.
284
- const SCANNED_RECORDS = 400_000, SCAN_KEPT_BYTES = 8 * 1024 * 1024;
591
+ /** The scan's in-memory graph holding `nodes` (the settled record first), with the scan's lastPrompt and fork anchors. */
592
+ function zoneOf(graph, nodes) {
593
+ const zone = new ClaudeReplayIndex();
594
+ for (const node of nodes) { zone.byUuid.set(node.uuid, zone.nodes.length); zone.nodes.push(node); }
595
+ zone.linearLines = graph.linearLines.slice(firstAbove(graph.linearLines, (nodes[0]?.lineIndex ?? Infinity) - 1));
596
+ zone.segmentAnchors = new Set(graph.segmentAnchors); zone.lastPrompt = graph.lastPrompt == null ? null : [...graph.lastPrompt];
597
+ return zone;
598
+ }
599
+
600
+ /** The index of the node of the record on line `line`, or undefined. */
601
+ function nodeAt(nodes, line) {
602
+ let low = 0, high = nodes.length;
603
+ while (low < high) { const middle = (low + high) >> 1; if (nodes[middle].lineIndex < line) low = middle + 1; else high = middle; }
604
+ return nodes[low]?.lineIndex === line ? low : undefined;
605
+ }
606
+
607
+ /** Whether a replayed record can be where the replay is settled: a user record on the main line (a person's prompt
608
+ * or a tool's result, a group of its own), a whole line already read, after the latest compaction (`boundary`, null
609
+ * for a history replay, which prunes nothing at a compaction) and no record it keeps. */
610
+ function settles(node, lines, boundary, kept) {
611
+ return node != null && node.kind === 'user' && !node.sidechain && node.compact == null && node.lineIndex < lines
612
+ && (boundary == null || node.lineIndex > boundary.lineIndex) && !kept.has(node.uuid);
613
+ }
614
+
615
+ /** The first index of a sorted array whose value is above `value`. */
616
+ function firstAbove(sorted, value) {
617
+ let low = 0, high = sorted.length;
618
+ while (low < high) { const middle = (low + high) >> 1; if (sorted[middle] <= value) low = middle + 1; else high = middle; }
619
+ return low;
620
+ }
621
+
622
+ /** Settled messages appended on disk: their places, their marks and the tool calls they make and answer; and in
623
+ * memory, the chain of digests over the marks (a checkpoint each MARK_CHECKPOINT messages). */
624
+ function appendSettled(settled, store, messages, marks) {
625
+ for (let at = 0; at < messages.length; at++) {
626
+ const message = messages[at], reference = message.metadata[REFERENCE];
627
+ if (reference === undefined) store.addPosition(settled.length, -1, -1, marks[at], message);
628
+ else { const [group, ordinal] = reference.split(':').map(Number); store.addPosition(settled.length, group, ordinal, marks[at], null); }
629
+ settled.length++;
630
+ settled.digest = chainMark(settled.digest, marks[at]);
631
+ if (settled.length % MARK_CHECKPOINT === 0) settled.checkpoints.push(settled.digest);
632
+ if (message.role === 'assistant') for (const call of message.tool_calls ?? []) if (call.id !== '') store.addTool(call.id, true, false);
633
+ if (message.role === 'tool' && message.tool_call_id != null) store.addTool(message.tool_call_id, false, true);
634
+ }
635
+ }
636
+
637
+ /** A replay as a window read answers it: the settled prefix (read from disk) and the tail, read by place. */
638
+ class WindowAnswer {
639
+ constructor(parts) { Object.assign(this, parts); this.base = this.settled.length; this.baseGroups = this.settled.groups; }
640
+ get total() { return this.base + this.skeleton.length; }
641
+ at(position) {
642
+ if (position >= this.base) return this.skeleton[position - this.base];
643
+ const { group, ordinal, synthetic } = this.store.positionAt(position);
644
+ if (synthetic) return synthetic;
645
+ const metadata = Object.create(null); metadata[REFERENCE] = `${group}:${ordinal}`;
646
+ return { metadata };
647
+ }
648
+ /** The marks from `from` to `to`, in order. */
649
+ *markRange(from, to) {
650
+ if (from < this.base) yield* this.store.marksFrom(from, Math.min(to, this.base));
651
+ for (let at = Math.max(from, this.base); at < to; at++) yield this.marks[at - this.base];
652
+ }
653
+ group(id) { return id < this.baseGroups ? this.store.groupAt(id).lines : this.groups[id - this.baseGroups]; }
654
+ size(id) { return id < this.baseGroups ? this.store.groupAt(id).size : this.sizes[id - this.baseGroups]; }
655
+ /** The digest of the chain of the first `count` marks. */
656
+ digestAt(count) {
657
+ let at, digest;
658
+ if (count <= this.base) { const checkpoint = Math.floor(count / MARK_CHECKPOINT); at = checkpoint * MARK_CHECKPOINT; digest = checkpoint ? this.settled.checkpoints[checkpoint - 1] : ''; }
659
+ else { at = this.base; digest = this.settled.digest; }
660
+ for (const mark of this.markRange(at, count)) digest = chainMark(digest, mark);
661
+ return digest;
662
+ }
663
+ /** The tail's first groups and messages settled since this answer was made: the same replay, read by place. */
664
+ reframe(settled, groups, messages, nodes) {
665
+ this.groups = this.groups.slice(groups); this.sizes = this.sizes.slice(groups); this.skeleton = this.skeleton.slice(messages); this.marks = this.marks.slice(messages);
666
+ this.tailNodes = this.tailNodes.slice(nodes); this.committed -= nodes; this.base = settled.length; this.baseGroups = settled.groups;
667
+ }
668
+ }
669
+
670
+ /** A whole replay answered with nothing settled. */
671
+ function wholeAnswer(view, store, reason = null) {
672
+ return new WindowAnswer({ key: view.key, raw: view.raw, store, replay: { whole: true, reason }, settled: { length: 0, groups: 0, digest: '', checkpoints: [], residue: [], dangling: new Set(), model: null },
673
+ groups: view.groups, sizes: view.sizes, skeleton: view.skeleton, marks: view.marks, made: new Map(), tailNodes: [], committed: 0, model: view.model,
674
+ residue: view.residue, meta: view.meta, lines_read: view.lines, parseErrors: view.parseErrors, version: view.version, history: view.history,
675
+ decodeError: view.decodeError, agents: view.agents });
676
+ }
677
+
678
+ // The scans of the files read most recently, at most KEPT_SCANS of them and at most this many retained in-memory entries
679
+ // between them (beyond the scans still reading their transcripts through, which are kept): a transcript read again is
680
+ // read from where its scan stopped, and one read long ago is scanned again. A scan keeps on disk what it knows of each
681
+ // record, so what it holds in memory is its tail, not its transcript.
682
+ const SCANNED_RECORDS = 400_000, KEPT_SCANS = 32;
285
683
  const scans = new Map();
286
684
 
287
- async function scanOf(path, reader) {
685
+ /** A scan let go: its file on disk with it. */
686
+ function dropScan(path) { const scan = scans.get(path); scans.delete(path); scan?.store.close(); }
687
+
688
+ async function scanOf(path, reader, budget = Infinity) {
288
689
  let scan = scans.get(path); scans.delete(path);
289
- if (!scan || !await scan.holds(reader)) scan = new ClaudeRecordScan(reader);
290
- await scan.extend(reader);
690
+ if (scan && !await scan.holds(reader)) { scan.store.close(); scan = null; }
691
+ scan ??= new ClaudeRecordScan(reader);
692
+ try { await scan.extend(reader, budget); }
693
+ catch (error) { scan.store.close(); throw error; }
291
694
  return scan;
292
695
  }
293
696
 
294
- // Project before admitting the scan: the projection adds skeletons and groups that the same finite bound charges.
295
- // As before, the current scan is kept even when it alone exceeds the bound; every older scan then goes.
296
697
  function retainScan(path, scan, metrics = {}) {
297
698
  metrics.claude_scan_physical_lines = scan.lines;
298
699
  metrics.claude_scan_record_positions = scan.records.size;
299
700
  metrics.claude_scan_graph_nodes = scan.graph.nodes.length;
300
701
  metrics.claude_scan_graph_lines = scan.graph.linearLines.length;
301
702
  metrics.claude_scan_profile_changes = scan.profile.history.length;
302
- metrics.claude_scan_skeletons = scan.skeletons.size;
303
- if (scan.bytes < SCAN_KEPT_BYTES) for (const [other, item] of scans) if (item.bytes < SCAN_KEPT_BYTES) scans.delete(other);
703
+ metrics.claude_scan_skeletons = scan.regionMade.size;
304
704
  scans.set(path, scan);
305
705
  let held = 0; for (const item of scans.values()) held += item.retainedRecords;
306
- for (const [other, item] of scans) {
307
- if (held <= SCANNED_RECORDS || item === scan) break;
308
- scans.delete(other); held -= item.retainedRecords;
706
+ for (const [other, item] of [...scans]) {
707
+ if (held <= SCANNED_RECORDS && scans.size <= KEPT_SCANS) break;
708
+ // the scan just read is kept, and so is any still reading its transcript through: let go, it would read it from
709
+ // its first byte again, and a long transcript read in turn with many others might never be read through
710
+ if (item === scan || item.behind) continue;
711
+ dropScan(other); held -= item.retainedRecords;
309
712
  metrics.claude_scan_evictions = (metrics.claude_scan_evictions ?? 0) + 1;
310
713
  }
311
714
  metrics.claude_scan_retained_entries = held;
312
715
  }
313
716
 
717
+ /** A transcript's background-agent fold alone, kept between reads of the same file as ClaudeRecordScan is, but holding
718
+ * no record positions: a follower of a long session keeps its agents without the replay index a window read needs. */
719
+ class ClaudeAgentScan extends AppendScan {
720
+ lines = 0; agents = claudeBackgroundAgentFold();
721
+ async extend(reader) {
722
+ const from = this.bytes;
723
+ for await (const line of sessionFileLines(reader, this.bytes)) {
724
+ if (!line.terminated) break;
725
+ const index = this.lines++;
726
+ if (nativeTrim(line.text) !== '') { try { this.agents.observe(parseNativeJson(line.text), index + 1); } catch { /* a malformed line holds no agent */ } }
727
+ this.took(line.bytes, line.offset + line.length); this.bytes = line.offset + line.length;
728
+ }
729
+ await this.settle(reader, from);
730
+ }
731
+ }
732
+ // Agent scans of the files read most recently. A 343 MB, 95,000-line Claude Code transcript that launched 132 background
733
+ // agents holds about 3,000 records this fold keeps (its Agent and SendMessage calls and results, and some 2,000 task
734
+ // notification records); counted from the file's records with grep, not from a run of the fold. These bounds keep
735
+ // dozens of such followed sessions without one evicting another; a scan above AGENT_SCAN_ENTRIES alone is not kept
736
+ // (its next read scans the file again) rather than evicting every other.
737
+ const AGENT_SCANS = 64, AGENT_SCANNED_ENTRIES = 1_000_000, AGENT_SCAN_ENTRIES = 250_000;
738
+ const agentScans = new Map();
739
+
740
+ /**
741
+ * The background agents of a Claude Code transcript (claudeBackgroundAgentFold over every record of the file, kept
742
+ * between reads: a read takes only what was written since, and a last line still being written is folded for this
743
+ * answer alone). Null when the locator names anything this index does not read, as loadClaudeRecordWindow.
744
+ */
745
+ export async function loadClaudeBackgroundAgents(locator, { metrics } = {}) {
746
+ const { path, kind } = locator.storage;
747
+ if (locator.harness !== 'claude-code' || kind !== 'file' || isCompressedSessionPath(path)) return null;
748
+ if ((await sourceStat(join(dirname(path), `${basename(path, extname(path))}.meta.json`)))?.isFile()) return null;
749
+ if (!await supportsClaudeReadIndex(path, { metrics })) return null;
750
+ return (await withSessionFile(path, async (reader) => {
751
+ let scan = agentScans.get(path); agentScans.delete(path);
752
+ if (!scan || !await scan.holds(reader)) scan = new ClaudeAgentScan(reader);
753
+ await scan.extend(reader);
754
+ if (scan.agents.retainedEntries <= AGENT_SCAN_ENTRIES) {
755
+ agentScans.set(path, scan);
756
+ let held = 0; for (const item of agentScans.values()) held += item.agents.retainedEntries;
757
+ for (const [other, item] of agentScans) {
758
+ if (agentScans.size <= AGENT_SCANS && held <= AGENT_SCANNED_ENTRIES || item === scan) break;
759
+ agentScans.delete(other); held -= item.agents.retainedEntries;
760
+ }
761
+ }
762
+ let tail = null;
763
+ if (reader.size > scan.bytes) {
764
+ try { const value = parseNativeJson(decodeSessionUtf8(await reader.read(scan.bytes, reader.size - scan.bytes))); tail = claudeBackgroundAgentFold(); tail.observe(value, scan.lines + 1); }
765
+ catch { /* a line still being written */ }
766
+ }
767
+ return scan.agents.finish({ also: tail });
768
+ }, metrics)).result;
769
+ }
770
+
314
771
  /**
315
772
  * A window of a Claude Code transcript's messages from the place `from`, read record by record: at most `messages` of
316
- * them and `bytes` of transcript (one message at least), with each one's mark and, when `marks` is asked, the mark of
317
- * every message of the session. A mark is of the bytes of the records that make a message: while it stands, so does
318
- * what a reader took of that message before. Null when the locator names anything else, which is loaded as its
319
- * document.
773
+ * them and `bytes` of transcript (one message at least), with each one's mark and, when `marks` is asked, the marks
774
+ * after the first place where those the reader holds (`known`: their count, the digest of their chain and its
775
+ * checkpoints, chainMark) differ from these: `marks_from` and `tail_marks` (from the first when nothing is named). A
776
+ * mark is of the bytes of the records that make a message: while it stands, so does what a reader took of that message
777
+ * before. A transcript not yet read through answers how far it is read (`reading`) and no messages: each read takes in
778
+ * at most SCAN_STEP_BYTES more. Each answer says whether its read replayed the whole transcript and why
779
+ * (`replay: { whole, reason }`, counted in `metrics.claude_whole_replays`): a read over a settled prefix replays its
780
+ * tail alone. Null when the locator names anything else, which is loaded as its document.
320
781
  */
321
- export async function loadClaudeRecordWindow(locator, { from, bytes = null, messages = null, marks = false }, { fidelity = 'semantic', metrics } = {}) {
782
+ export async function loadClaudeRecordWindow(locator, { from, bytes = null, messages = null, marks = false, known = null }, { fidelity = 'semantic', metrics } = {}) {
322
783
  const { path, kind } = locator.storage;
323
784
  if (kind !== 'file' || locator.harness === 'hermes' || isCompressedSessionPath(path)) return null;
324
785
  if ((await sourceStat(join(dirname(path), `${basename(path, extname(path))}.meta.json`)))?.isFile()) return null;
325
786
  if (!await supportsClaudeReadIndex(path, { metrics })) return null;
787
+ metrics ??= {};
326
788
  return (await withSessionFile(path, async (reader) => {
327
- let view;
789
+ let view, scan, replay;
328
790
  try {
329
- const scan = await scanOf(path, reader);
330
- view = await scan.project(reader, fidelity);
791
+ scan = await scanOf(path, reader, SCAN_STEP_BYTES);
792
+ // A transcript not read through is replayed as far as it is read, so that its replay is settled as it is read
793
+ // and the read that reaches its end replays only its last step.
794
+ const answer = await scan.window(reader, fidelity, scan.behind);
795
+ replay = answer.replay;
796
+ if (replay.whole) metrics.claude_whole_replays = (metrics.claude_whole_replays ?? 0) + 1;
797
+ if (!scan.behind) view = answer;
331
798
  retainScan(path, scan, metrics);
332
799
  }
333
800
  catch (error) {
334
- scans.delete(path);
801
+ dropScan(path);
335
802
  throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(path, error) : error;
336
803
  }
337
- const total = view.skeleton.length, offset = Math.min(from, total), session = createSession('claude_code'), counted = new Set();
804
+ if (view == null) {
805
+ const session = createSession('claude_code'); session.meta = cloneNativeJson(scan.meta);
806
+ return { session: { ...normalizedSessionJson(session), raw_record_count: scan.lines,
807
+ window: { offset: 0, returned: 0, total_messages: null, marks: [], reading: { bytes: scan.bytes, size: reader.size }, replay } } };
808
+ }
809
+ const total = view.total, offset = Math.min(from, total), session = createSession('claude_code'), counted = new Set();
338
810
  let end = offset, used = 0, decoded = null, decodedGroup = null;
339
811
  for (; end < total && (messages == null || end - offset < messages); end++) {
340
- const message = view.skeleton[end], reference = message.metadata[REFERENCE];
812
+ const message = view.at(end), reference = message.metadata[REFERENCE];
341
813
  if (reference === undefined) { session.messages.push(cloneNativeJson(message)); continue; }
342
- const [group, ordinal] = reference.split(':').map(Number), cost = counted.has(group) ? 0 : view.sizes[group];
814
+ const [group, ordinal] = reference.split(':').map(Number), cost = counted.has(group) ? 0 : view.size(group);
343
815
  if (bytes != null && end > offset && used + cost > bytes) break;
344
816
  counted.add(group); used += cost;
345
- if (decodedGroup !== group) { decoded = await decodeClaudeGroup((index) => view.raw(index, reader), view.groups[group]); decodedGroup = group; }
817
+ if (decodedGroup !== group) { decoded = await decodeClaudeGroup((index) => view.raw(index, reader), view.group(group)); decodedGroup = group; }
346
818
  session.messages.push(cloneNativeJson(decoded[ordinal]));
347
819
  }
348
820
  session.meta = cloneNativeJson(view.meta); session.imported_message_count = total;
349
821
  session.parse_error_lines = view.parseErrors; session.load_residue = [...view.residue];
350
- return { session: { ...normalizedSessionJson(session), raw_record_count: view.lines,
822
+ const window = { offset, returned: end - offset, total_messages: total, marks: [...view.markRange(offset, end)], replay };
823
+ if (marks) { const listed = markedFrom(view, known); window.marks_from = listed; window.tail_marks = [...view.markRange(listed, total)]; }
824
+ return { session: { ...normalizedSessionJson(session), raw_record_count: view.lines_read, background_agents: view.agents,
351
825
  resolved_profile: recordedSessionProfile(session, { model: view.model, version: view.version }), resolved_profile_history: view.history.map(cloneNativeJson),
352
- window: { offset, returned: end - offset, total_messages: total, marks: view.marks.slice(offset, end), ...(marks ? { all_marks: [...view.marks] } : {}) } } };
826
+ window } };
353
827
  }, metrics)).result;
354
828
  }
829
+
830
+ // The history scans of the files paged most recently (loadClaudeHistoryWindow), kept apart from the window reads'
831
+ // scans: paging a session's history never evicts the scan its capture reads, nor reads its transcript through again.
832
+ const HISTORY_SCANS = 4, HISTORY_SCANNED_RECORDS = 200_000;
833
+ const historyScans = new Map(), historyReads = new Map();
834
+ /** A page of history is read in steps of SCAN_STEP_BYTES for at most this long before answering how far it has read. */
835
+ const HISTORY_READ_MS = 2000;
836
+
837
+ /** The lines of the records a compact boundary keeps (its preserved messages, or the chain of its preserved segment). */
838
+ function keptLines(store, compact) {
839
+ const lines = new Set();
840
+ for (const uuid of compact.preserved) { const node = store.nodeOf(uuid); if (node) lines.add(node.line); }
841
+ if (compact.segment != null) {
842
+ const [head, tail] = compact.segment;
843
+ for (let uuid = tail, steps = 0; uuid != null && steps < 100_000; steps++) {
844
+ const node = store.nodeOf(uuid); if (!node) break;
845
+ lines.add(node.line); if (uuid === head) break; uuid = node.parent;
846
+ }
847
+ }
848
+ return lines;
849
+ }
850
+
851
+ /** The first line of the records that make the message at `position` of a window answer (one the pairing pass made up
852
+ * is read at the message after it), or Infinity. */
853
+ function firstLineAt(view, position) {
854
+ for (let at = position; at < Math.min(view.total, position + 8); at++) {
855
+ const reference = view.at(at).metadata[REFERENCE];
856
+ if (reference !== undefined) return Math.min(...view.group(Number(reference.split(':')[0])));
857
+ }
858
+ return Infinity;
859
+ }
860
+
861
+ /**
862
+ * A page of a Claude Code transcript's history, its segments before every compaction kept (a history read: the graph's
863
+ * `history` projection, replayed by a scan of its own): the messages of that read before the place `before`, or, with
864
+ * `before` null, before the first message of its latest segment, which is where a read from the latest compaction on
865
+ * (the Teams connector's capture, whose first message is its ordinal 0) begins. At most `messages` of them and `bytes`
866
+ * of transcript (one message at least), the newest of them first to be kept: `window.offset` is where the page starts,
867
+ * the `before` of the next older page. A transcript not read through is read on, SCAN_STEP_BYTES at a time, for at most
868
+ * HISTORY_READ_MS; one still not read through answers how far it has read (`window.reading`) and no messages, and the
869
+ * next read reads on from there. Null when the locator names anything this index does not read, as
870
+ * loadClaudeRecordWindow.
871
+ */
872
+ export async function loadClaudeHistoryWindow(locator, page = {}, options = {}) {
873
+ const { path, kind } = locator.storage;
874
+ if (locator.harness !== 'claude-code' || kind !== 'file' || isCompressedSessionPath(path)) return null;
875
+ if ((await sourceStat(join(dirname(path), `${basename(path, extname(path))}.meta.json`)))?.isFile()) return null;
876
+ if (!await supportsClaudeReadIndex(path, { metrics: options.metrics })) return null;
877
+ // one read of a file's history scan at a time: a page asked while another reads waits for it, and takes the scan it leaves
878
+ const read = (historyReads.get(path) ?? Promise.resolve()).catch(() => {}).then(() => historyWindow(path, page, options));
879
+ historyReads.set(path, read);
880
+ try { return await read; } finally { if (historyReads.get(path) === read) historyReads.delete(path); }
881
+ }
882
+
883
+ async function historyWindow(path, { before = null, messages = 50, bytes = null }, { metrics } = {}) {
884
+ metrics ??= {};
885
+ return (await withSessionFile(path, async (reader) => {
886
+ let scan = historyScans.get(path), view; historyScans.delete(path);
887
+ if (scan && !await scan.holds(reader)) { scan.store.close(); scan = null; }
888
+ if (!scan) { scan = new ClaudeRecordScan(reader); scan.history = true; scan.boundary = null; }
889
+ try {
890
+ const started = Date.now();
891
+ for (;;) {
892
+ // the newest compact boundary among the records this step takes in: where the latest segment begins
893
+ const known = scan.graph.nodes.length;
894
+ await scan.extend(reader, SCAN_STEP_BYTES);
895
+ for (let at = scan.graph.nodes.length - 1; at >= known; at--) if (scan.graph.nodes[at].compact != null) { scan.boundary = { line: scan.graph.nodes[at].lineIndex, compact: scan.graph.nodes[at].compact, kept: null }; break; }
896
+ view = await scan.window(reader, 'semantic', scan.behind);
897
+ if (!scan.behind || Date.now() - started >= HISTORY_READ_MS) break;
898
+ }
899
+ if (view.replay.whole) metrics.claude_whole_replays = (metrics.claude_whole_replays ?? 0) + 1;
900
+ } catch (error) {
901
+ scan.store.close();
902
+ throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(path, error) : error;
903
+ }
904
+ const replaced = historyScans.get(path);
905
+ if (replaced && replaced !== scan) replaced.store.close();
906
+ historyScans.set(path, scan);
907
+ // at most HISTORY_SCANS scans and HISTORY_SCANNED_RECORDS in-memory entries between them (ClaudeRecordScan's
908
+ // retainedRecords), the oldest let go first; the scan just read is kept
909
+ let held = 0; for (const item of historyScans.values()) held += item.retainedRecords;
910
+ for (const [other, item] of historyScans) {
911
+ if (historyScans.size <= HISTORY_SCANS && held <= HISTORY_SCANNED_RECORDS) break;
912
+ if (item !== scan) { historyScans.delete(other); held -= item.retainedRecords; item.store.close(); }
913
+ }
914
+ if (scan.behind) {
915
+ const session = createSession('claude_code'); session.meta = cloneNativeJson(scan.meta);
916
+ return { session: { ...normalizedSessionJson(session), raw_record_count: scan.lines,
917
+ window: { offset: 0, returned: 0, total_messages: null, segment_start: null, marks: [], reading: { bytes: scan.bytes, size: reader.size }, replay: view.replay } } };
918
+ }
919
+ const total = view.total;
920
+ // the first message of the latest segment: the first whose records begin at or after its boundary, and before it
921
+ // those whose records the boundary keeps (a read from the latest compaction on replays them after its summary)
922
+ const boundary = scan.boundary;
923
+ let low = 0, high = boundary == null ? 0 : total;
924
+ while (low < high) { const middle = (low + high) >> 1; if (firstLineAt(view, middle) < boundary.line) low = middle + 1; else high = middle; }
925
+ if (boundary != null && low > 0) {
926
+ boundary.kept ??= keptLines(scan.store, boundary.compact);
927
+ for (; low > 0; low--) {
928
+ const reference = view.at(low - 1).metadata[REFERENCE];
929
+ if (reference === undefined || !view.group(Number(reference.split(':')[0])).every((line) => boundary.kept.has(line))) break;
930
+ }
931
+ }
932
+ const segmentStart = low, end = Math.min(before ?? segmentStart, total);
933
+ // the page's first place: back from `end`, as many messages and bytes of their records as it may hold
934
+ let offset = end, used = 0;
935
+ const counted = new Set();
936
+ while (offset > 0 && (messages == null || end - offset < messages)) {
937
+ const reference = view.at(offset - 1).metadata[REFERENCE];
938
+ if (reference !== undefined) {
939
+ const group = Number(reference.split(':')[0]), cost = counted.has(group) ? 0 : view.size(group);
940
+ if (bytes != null && offset < end && used + cost > bytes) break;
941
+ counted.add(group); used += cost;
942
+ }
943
+ offset--;
944
+ }
945
+ const session = createSession('claude_code');
946
+ let decoded = null, decodedGroup = null;
947
+ for (let at = offset; at < end; at++) {
948
+ const message = view.at(at), reference = message.metadata[REFERENCE];
949
+ if (reference === undefined) { session.messages.push(cloneNativeJson(message)); continue; }
950
+ const [group, ordinal] = reference.split(':').map(Number);
951
+ if (decodedGroup !== group) { decoded = await decodeClaudeGroup((index) => view.raw(index, reader), view.group(group)); decodedGroup = group; }
952
+ session.messages.push(cloneNativeJson(decoded[ordinal]));
953
+ }
954
+ session.meta = cloneNativeJson(view.meta); session.imported_message_count = total;
955
+ session.parse_error_lines = view.parseErrors; session.load_residue = [...view.residue];
956
+ return { session: { ...normalizedSessionJson(session), raw_record_count: view.lines_read,
957
+ window: { offset, returned: end - offset, total_messages: total, segment_start: segmentStart, marks: [...view.markRange(offset, end)], replay: view.replay } } };
958
+ }, metrics)).result;
959
+ }
960
+
961
+ /** The first place whose mark the reader may not hold: the count it names when the chain of its marks there is this
962
+ * read's (`view.digestAt`), else the last checkpoint both chains share. */
963
+ export function markedFrom(view, known) {
964
+ if (known == null) return 0;
965
+ if (known.count <= view.total && view.digestAt(known.count) === known.digest) return known.count;
966
+ let low = 0, high = Math.min(known.checkpoints.length, Math.floor(view.total / MARK_CHECKPOINT));
967
+ while (low < high) {
968
+ const middle = (low + high + 1) >> 1;
969
+ if (view.digestAt(middle * MARK_CHECKPOINT) === known.checkpoints[middle - 1]) low = middle; else high = middle - 1;
970
+ }
971
+ return low * MARK_CHECKPOINT;
972
+ }