@volter/supercode-interchange 0.1.20 → 0.1.21

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-interchange",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "type": "module",
5
5
  "description": "Read-only harness metadata, transcript codecs and session projections for Supercode",
6
6
  "license": "MIT OR Apache-2.0",
@@ -220,6 +220,16 @@ class ClaudeRecordScan extends AppendScan {
220
220
  graph = new ClaudeReplayIndex(); meta = createSession('claude_code').meta; profile = recordedProfileFold('claude_code');
221
221
  skeletons = new Map(); view = null;
222
222
 
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. */
226
+ 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);
231
+ }
232
+
223
233
  /** The lines completed since the last pass. A last line with no LF yet stays outside the scan: `view` reads it. */
224
234
  async extend(reader) {
225
235
  const from = this.bytes;
@@ -267,7 +277,7 @@ class ClaudeRecordScan extends AppendScan {
267
277
  }
268
278
  }
269
279
 
270
- // The scans of the files read most recently, at most this many records between them: a transcript read again is
280
+ // The scans of the files read most recently, at most this many retained replay entries between them: a transcript read again is
271
281
  // read from where its scan stopped, and one read long ago is scanned again. Of the files shorter than
272
282
  // SCAN_KEPT_BYTES only the one read last is kept (a pass reads its file more than once): scanning a short file again
273
283
  // costs less than keeping the scan of every one read.
@@ -278,11 +288,27 @@ async function scanOf(path, reader) {
278
288
  let scan = scans.get(path); scans.delete(path);
279
289
  if (!scan || !await scan.holds(reader)) scan = new ClaudeRecordScan(reader);
280
290
  await scan.extend(reader);
291
+ return scan;
292
+ }
293
+
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
+ function retainScan(path, scan, metrics = {}) {
297
+ metrics.claude_scan_physical_lines = scan.lines;
298
+ metrics.claude_scan_record_positions = scan.records.size;
299
+ metrics.claude_scan_graph_nodes = scan.graph.nodes.length;
300
+ metrics.claude_scan_graph_lines = scan.graph.linearLines.length;
301
+ metrics.claude_scan_profile_changes = scan.profile.history.length;
302
+ metrics.claude_scan_skeletons = scan.skeletons.size;
281
303
  if (scan.bytes < SCAN_KEPT_BYTES) for (const [other, item] of scans) if (item.bytes < SCAN_KEPT_BYTES) scans.delete(other);
282
304
  scans.set(path, scan);
283
- let held = 0; for (const item of scans.values()) held += item.lines;
284
- for (const [other, item] of scans) { if (held <= SCANNED_RECORDS || item === scan) break; scans.delete(other); held -= item.lines; }
285
- return scan;
305
+ 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;
309
+ metrics.claude_scan_evictions = (metrics.claude_scan_evictions ?? 0) + 1;
310
+ }
311
+ metrics.claude_scan_retained_entries = held;
286
312
  }
287
313
 
288
314
  /**
@@ -299,7 +325,11 @@ export async function loadClaudeRecordWindow(locator, { from, bytes = null, mess
299
325
  if (!await supportsClaudeReadIndex(path, { metrics })) return null;
300
326
  return (await withSessionFile(path, async (reader) => {
301
327
  let view;
302
- try { view = await (await scanOf(path, reader)).project(reader, fidelity); }
328
+ try {
329
+ const scan = await scanOf(path, reader);
330
+ view = await scan.project(reader, fidelity);
331
+ retainScan(path, scan, metrics);
332
+ }
303
333
  catch (error) {
304
334
  scans.delete(path);
305
335
  throw ['ERR_ENCODING_INVALID_ENCODED_DATA', 'ERR_STRING_TOO_LONG'].includes(error?.code) ? sessionTextError(path, error) : error;