@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
@@ -33,3 +33,31 @@ export interface ClaudeRuntimeManifest {
33
33
  }
34
34
  /** A pure strict fold. It never executes scheduled work or launches a child. */
35
35
  export function claudeRuntimeManifest(session: Session): ClaudeRuntimeManifest;
36
+
37
+ /** One background agent of a Claude Code session, as its records say it (claudeBackgroundAgentFold). `running` means
38
+ * no notification has ended its latest run; whether its session still runs is the reader's to say. */
39
+ export interface ClaudeBackgroundAgent {
40
+ /** The agent id Claude Code gave it (`toolUseResult.agentId`), which its notifications and `SendMessage` name. */
41
+ id: string;
42
+ /** The `Agent` call that launched it. */
43
+ tool_use_id: string;
44
+ description: string | null; agent_type: string | null;
45
+ state: 'running' | 'completed' | 'failed' | 'stopped';
46
+ /** The first lines of the latest run's notification `<summary>` and `<result>`. */
47
+ summary: string | null; result: string | null;
48
+ /** When its launch was recorded; `started_at` is its latest run's start (a resume's, when one resumed it). */
49
+ launched_at: string | null; started_at: string | null; finished_at: string | null;
50
+ }
51
+ export interface ClaudeTaskNotification {
52
+ task_ids: string[]; tool_use_id: string | null; status: string | null; summary: string | null; result: string | null;
53
+ }
54
+ export function claudeTaskNotifications(text: string): ClaudeTaskNotification[];
55
+ export interface ClaudeBackgroundAgentFold {
56
+ observe(value: unknown, line: number): void;
57
+ clone(): ClaudeBackgroundAgentFold;
58
+ readonly retainedEntries: number;
59
+ /** Every running agent, then at most `settled` (default 20) that ended, the latest-started first. `also` is a fold of
60
+ * records not yet in this one (a line still being written), read beside it without copying either. */
61
+ finish(options?: { settled?: number; also?: ClaudeBackgroundAgentFold | null }): ClaudeBackgroundAgent[];
62
+ }
63
+ export function claudeBackgroundAgentFold(): ClaudeBackgroundAgentFold;
@@ -151,18 +151,171 @@ function applyNotification({ notification: note, timestamp, line }, children) {
151
151
  child.finished_at = timestamp; child.summary = note.summary;
152
152
  }
153
153
 
154
+ // ---- Background agents --------------------------------------------------------------------------------------------
155
+ // Claude Code's background agents as its own records say them, for a reader that shows what the terminal shows under
156
+ // its prompt. Lenient where the manifest is strict: an unrelated malformed record never hides an agent, and the facts
157
+ // are joined only at `finish`, so the fold is the same whatever order the records arrive in.
158
+ const NOTIFICATION_OPEN = '<task-notification>';
159
+ const AGENT_TEXT = 200, AGENT_LINE = 300;
160
+ const opens = text => typeof text === 'string' && text.trimStart().startsWith(NOTIFICATION_OPEN);
161
+ const firstLine = (text, max = AGENT_LINE) => {
162
+ const line = typeof text === 'string' ? text.trim().split('\n', 1)[0].trim() : '';
163
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
164
+ };
165
+ const blockText = content => typeof content === 'string' ? content
166
+ : Array.isArray(content) ? content.filter(block => stringField(block, 'type') === 'text').map(block => stringField(block, 'text') ?? '').join('') : null;
167
+ const allTags = (block, name) => [...block.matchAll(new RegExp(`<${name}>([\\s\\S]*?)</${name}>`, 'g'))].map(match => nativeTrim(match[1])).filter(Boolean);
168
+ /** Every task notification a text holds, each with all the tasks it names (an end-of-session summary names several). */
169
+ export function claudeTaskNotifications(text) {
170
+ if (typeof text !== 'string' || !text.includes(NOTIFICATION_OPEN)) return [];
171
+ return [...text.matchAll(/<task-notification>([\s\S]*?)<\/task-notification>/g)].map(([, block]) => ({
172
+ task_ids: allTags(block, 'task-id'), tool_use_id: allTags(block, 'tool-use-id')[0] ?? null, status: allTags(block, 'status')[0] ?? null,
173
+ summary: firstLine(allTags(block, 'summary')[0]) || null, result: firstLine(allTags(block, 'result')[0]) || null,
174
+ }));
175
+ }
176
+ const agentState = status => status === 'completed' ? 'completed' : status === 'failed' || status === 'error' ? 'failed' : 'stopped';
177
+ // A queue entry carries no stamp: only one that is notifications and nothing else is the runtime's own.
178
+ const onlyNotifications = text => text.replace(/<task-notification>[\s\S]*?<\/task-notification>/g, '').trim() === '';
179
+ const emptyAgentState = () => ({ launches: new Map(), sends: new Map(), results: new Map(), notes: [], queued: [] });
180
+
181
+ /**
182
+ * Claude Code's background agents, folded from a transcript's records (`observe` each parsed record with its 1-based
183
+ * line, in any order; `finish` answers):
184
+ * - an `Agent` (or `Task`) call starts an agent when it asked `run_in_background` or its result is async
185
+ * (`toolUseResult.isAsync`); the result's structured `agentId` names it;
186
+ * - a `SendMessage` whose result names the agent it resumed (`toolUseResult.resumedAgentId`) starts it again;
187
+ * - a task notification ends the run its `<tool-use-id>` names (else the agent's latest run before it), for every
188
+ * `<task-id>` it lists: `completed`, `failed`, or `stopped` (killed, stopped, or not finished when the session
189
+ * before it ended). Only the runtime's own notifications count: a user record Claude Code stamped
190
+ * `origin.kind: task-notification`, a queued command of `commandMode: task-notification`, or a queue entry that is
191
+ * notifications and nothing else, each naming by `<tool-use-id>` a run of the agent it names. A notification quoted
192
+ * in a tool result or written in anyone's message is text, not an ending.
193
+ * Sidechain records (a subagent's own transcript lines) are not the session's. An agent whose latest run has no ending
194
+ * is `running` as far as the transcript knows; whether its session still runs is the reader's to say.
195
+ */
196
+ // What the fold keeps is copied out of the record it was read from: a part cut from a record's text holds the whole
197
+ // text in memory for as long as the part is kept, and the fold keeps its parts for as long as its transcript is read.
198
+ const detach = value => typeof value === 'string' ? JSON.parse(JSON.stringify(value)) : Array.isArray(value) ? value.map(detach) : value;
199
+ const owned = entry => Object.fromEntries(Object.entries(entry).map(([key, value]) => [key, detach(value)]));
200
+
201
+ export function claudeBackgroundAgentFold(saved = null) {
202
+ const state = saved ?? emptyAgentState();
203
+ const notes = (text, at, line, into = state.notes) => { for (const note of claudeTaskNotifications(text)) into.push(owned({ ...note, at, line })); };
204
+ return {
205
+ get state() { return state; },
206
+ get retainedEntries() { return state.launches.size + state.sends.size + state.results.size + state.notes.length + state.queued.length; },
207
+ clone() {
208
+ return claudeBackgroundAgentFold({ launches: new Map(state.launches), sends: new Map(state.sends), results: new Map(state.results),
209
+ notes: [...state.notes], queued: [...state.queued] });
210
+ },
211
+ observe(value, line) {
212
+ if (field(value, 'isSidechain') === true) return;
213
+ const kind = stringField(value, 'type'), at = stringField(value, 'timestamp');
214
+ const content = field(field(value, 'message'), 'content');
215
+ if (kind === 'assistant' && Array.isArray(content)) {
216
+ for (const block of content) {
217
+ if (stringField(block, 'type') !== 'tool_use') continue;
218
+ const name = stringField(block, 'name'), id = stringField(block, 'id');
219
+ if (id == null || !['Agent', 'Task', 'SendMessage'].includes(name)) continue;
220
+ const input = field(block, 'input');
221
+ if (name === 'SendMessage') state.sends.set(detach(id), { line });
222
+ else state.launches.set(detach(id), owned({ description: firstLine(stringField(input, 'description'), AGENT_TEXT) || null,
223
+ agent_type: firstLine(stringField(input, 'subagent_type'), AGENT_TEXT) || null, background: bool(input, 'run_in_background'), at, line }));
224
+ }
225
+ } else if (kind === 'user') {
226
+ const results = Array.isArray(content) ? content.filter(block => stringField(block, 'type') === 'tool_result') : [];
227
+ if (results.length) {
228
+ // Each tool result's own structured result: the record's one, or its place in a list of them.
229
+ const native = field(value, 'toolUseResult');
230
+ results.forEach((block, index) => {
231
+ const id = stringField(block, 'tool_use_id'), own = Array.isArray(native) ? (native.length === results.length ? native[index] : null) : results.length === 1 ? native : null;
232
+ if (id == null || own == null || typeof own !== 'object') return;
233
+ const agentId = stringField(own, 'agentId'), resumed = stringField(own, 'resumedAgentId');
234
+ if (agentId != null || resumed != null || bool(own, 'isAsync'))
235
+ state.results.set(detach(id), owned({ agent_id: agentId, resumed, async: bool(own, 'isAsync'), error: bool(block, 'is_error'), at, line }));
236
+ });
237
+ return;
238
+ }
239
+ if (stringField(field(value, 'origin'), 'kind') === 'task-notification') notes(blockText(content), at, line);
240
+ } else if (kind === 'attachment') {
241
+ const attachment = field(value, 'attachment');
242
+ if (stringField(attachment, 'type') === 'queued_command' && stringField(attachment, 'commandMode') === 'task-notification')
243
+ notes(stringField(attachment, 'prompt'), at, line);
244
+ } else if (kind === 'queue-operation' && stringField(value, 'operation') === 'enqueue') {
245
+ const text = stringField(value, 'content');
246
+ if (opens(text) && onlyNotifications(text)) notes(text, at, line, state.queued);
247
+ }
248
+ },
249
+ /** The agents, the latest-started first: every one running, then at most `settled` that ended. `also` is a fold of
250
+ * records not yet in this one (a line still being written), read beside it without copying either. */
251
+ finish({ settled = 20, also = null } = {}) {
252
+ const states = also ? [state, also.state] : [state];
253
+ const each = function* (key) { for (const held of states) yield* held[key]; };
254
+ const results = id => states.map(held => held.results.get(id)).find(Boolean);
255
+ const agents = new Map(), runs = new Map();
256
+ for (const [call, launch] of each('launches')) {
257
+ const result = results(call);
258
+ if (!result || result.error || result.agent_id == null || !(launch.background || result.async)) continue;
259
+ const run = { call, line: result.line, started_at: launch.at ?? result.at, outcome: null, summary: null, result: null, finished_at: null };
260
+ const known = agents.get(result.agent_id);
261
+ if (known) { known.runs.push(run); runs.set(call, { agent: known, run }); continue; }
262
+ const agent = { id: result.agent_id, tool_use_id: call, description: launch.description, agent_type: launch.agent_type, launched_at: run.started_at, runs: [run] };
263
+ agents.set(agent.id, agent); runs.set(call, { agent, run });
264
+ }
265
+ for (const [call] of each('sends')) {
266
+ const result = results(call), agent = result?.resumed == null || result.error ? null : agents.get(result.resumed);
267
+ if (!agent) continue;
268
+ const run = { call, line: result.line, started_at: result.at, outcome: null, summary: null, result: null, finished_at: null };
269
+ agent.runs.push(run); runs.set(call, { agent, run });
270
+ }
271
+ for (const agent of agents.values()) agent.runs.sort((left, right) => left.line - right.line);
272
+ const all = [...[...each('notes')].map(note => ({ note, stamped: true })), ...[...each('queued')].map(note => ({ note, stamped: false }))]
273
+ .sort((left, right) => left.note.line - right.note.line);
274
+ for (const { note, stamped } of all) for (const task of note.task_ids) {
275
+ const agent = agents.get(task); if (!agent) continue;
276
+ const named = note.tool_use_id == null ? null : runs.get(note.tool_use_id);
277
+ // An unstamped queue entry counts only for a run of this agent it names.
278
+ if (!stamped && named?.agent !== agent) continue;
279
+ const run = named?.agent === agent ? named.run : agent.runs.findLast(candidate => candidate.line < note.line) ?? agent.runs.at(-1);
280
+ // The first notice of a run ends it; the same notice written again (queued, then delivered) changes nothing.
281
+ if (run.outcome) continue;
282
+ Object.assign(run, { outcome: agentState(note.status), summary: note.summary, result: note.result, finished_at: note.at });
283
+ }
284
+ const answer = [...agents.values()].map(agent => {
285
+ const latest = agent.runs.at(-1);
286
+ return { id: agent.id, tool_use_id: agent.tool_use_id, description: agent.description, agent_type: agent.agent_type,
287
+ state: latest.outcome ?? 'running', summary: latest.summary, result: latest.result, launched_at: agent.launched_at, started_at: latest.started_at,
288
+ finished_at: latest.finished_at, line: latest.line };
289
+ }).sort((left, right) => right.line - left.line);
290
+ const running = answer.filter(agent => agent.state === 'running'), ended = answer.filter(agent => agent.state !== 'running').slice(0, settled);
291
+ return [...running, ...ended].map(({ line: _line, ...agent }) => agent);
292
+ },
293
+ };
294
+ }
295
+
154
296
  /** The manifest's fold, one parsed record at a time: every record's calls
155
297
  * first, then every record again in order, so physically earlier async
156
298
  * results can resolve their origins. `raw` is kept only in the residue. */
157
- export function claudeRuntimeFold() {
158
- const state = { crons: new Map(), pending: new Map(), children: new Map(), wakeups: new Map(), notifications: [], residue: [] };
159
- const posture = { permission_mode: null, last_prompt_leaf_uuid: null, last_prompt: null, timestamp: null, entrypoint: null, user_type: null, version: null, cwd: null };
160
- const queue = { enqueued: 0, dequeued: 0, removed: 0, pending: [] };
161
- let incoming = [], outgoing = [];
162
- let reported = null;
299
+ export function claudeRuntimeFold(saved = null) {
300
+ const state = saved?.state ?? { crons: new Map(), pending: new Map(), children: new Map(), wakeups: new Map(), notifications: [], residue: [] };
301
+ const posture = saved?.posture ?? { permission_mode: null, last_prompt_leaf_uuid: null, last_prompt: null, timestamp: null, entrypoint: null, user_type: null, version: null, cwd: null };
302
+ const queue = saved?.queue ?? { enqueued: 0, dequeued: 0, removed: 0, pending: [] };
303
+ let incoming = saved?.incoming ?? [], outgoing = saved?.outgoing ?? [];
304
+ let reported = saved?.reported ?? null;
305
+ let permissionChecked = saved?.permissionChecked ?? 0;
306
+ // The background agents ride the same records, leniently: a refusal of the manifest never loses them.
307
+ const agents = claudeBackgroundAgentFold(saved?.agents ?? null);
163
308
  return {
309
+ /** The session's background agents (claudeBackgroundAgentFold), from every record this fold has seen. */
310
+ backgroundAgents(options) { return agents.finish(options); },
311
+ // Runtime state contains strings, booleans, native big integers and maps,
312
+ // never decoded transcript objects. A snapshot's finish cannot consume the
313
+ // retained queue or apply its notifications twice on the next append.
314
+ get retainedEntries() { return agents.retainedEntries + state.crons.size + state.pending.size + state.children.size + state.wakeups.size + state.notifications.length + state.residue.length + incoming.length + outgoing.length; },
315
+ clone() { return claudeRuntimeFold(structuredClone({ state, posture, queue, incoming, outgoing, reported, permissionChecked, agents: agents.state })); },
164
316
  call(value, line, raw) { if (stringField(value, 'type') === 'assistant') assistantCalls(value, line, raw, state); },
165
317
  record(value, line, raw) {
318
+ agents.observe(value, line);
166
319
  for (const [key, target] of [['timestamp', 'timestamp'], ['entrypoint', 'entrypoint'], ['userType', 'user_type'], ['version', 'version'], ['cwd', 'cwd']]) {
167
320
  const text = stringField(value, key); if (text != null) posture[target] = text;
168
321
  }
@@ -191,6 +344,18 @@ export function claudeRuntimeFold() {
191
344
  else if (stringField(value, 'subtype') === 'scheduled_task_fire') residue(state, line, 'scheduled-task-fire', raw);
192
345
  } else if (kind !== 'assistant' && runtimeType(kind)) residue(state, line, 'unknown-runtime-record', raw);
193
346
  },
347
+ // Finishing the manifest changes child summaries and materializes queue
348
+ // output, but none of those changes affects the permission reading. Its
349
+ // only deferred refusal is a notification with neither reference id.
350
+ // Validate new notifications once without consuming retained fold state.
351
+ permissionMode() {
352
+ for (; permissionChecked < state.notifications.length; permissionChecked++) {
353
+ const { notification: note, line } = state.notifications[permissionChecked];
354
+ if (note.tool_use_id == null && note.task_id == null)
355
+ fail(`malformed Claude task notification at line ${line}: missing tool-use-id and task-id`);
356
+ }
357
+ return posture.permission_mode;
358
+ },
194
359
  finish() {
195
360
  for (const notification of state.notifications) applyNotification(notification, state.children);
196
361
  queue.pending = outgoing.reverse().concat(incoming);
@@ -0,0 +1,164 @@
1
+ // What a Claude Code transcript's scan knows of each of its records, kept on disk rather than in the process: one
2
+ // SQLite file per scan, private to this process (a directory under the system's temporary directory, removed when the
3
+ // process exits, swept by the next process when it did not; a scan let go removes its own). The scan holds in memory only what its next read replays (the tail
4
+ // after its settled prefix) and the facts that do not grow with the transcript; everything per record, per message
5
+ // and per group is read from here when a read needs it.
6
+ import { DatabaseSync } from 'node:sqlite';
7
+ import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { join } from 'node:path';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { threadId } from 'node:worker_threads';
12
+ import { parseNativeJson, stringifyNativeJson } from './native-json.mjs';
13
+
14
+ // A scan directory is named for the process and the thread that made it: `supercode-claude-scans-<pid>-<thread>-…`.
15
+ const SCANS = /^supercode-claude-scans-(\d+)-(\d+)-/;
16
+ const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (error) { return error.code === 'EPERM'; } };
17
+
18
+ /**
19
+ * The scan directories of processes and threads that are gone, removed: every one whose process no longer runs (a
20
+ * process killed, aborted or crashed leaves its directory), or, named, those of one thread of this process (a reader
21
+ * worker stopped without running its exit hooks: its owner removes them, `session-read-client.mjs`).
22
+ */
23
+ export function sweepScanDirectories({ thread = null } = {}) {
24
+ let names = [];
25
+ try { names = readdirSync(tmpdir()); } catch { return; }
26
+ for (const name of names) {
27
+ const owner = SCANS.exec(name);
28
+ if (!owner) continue;
29
+ const pid = Number(owner[1]), of = Number(owner[2]);
30
+ if (thread == null ? alive(pid) : pid !== process.pid || of !== thread) continue;
31
+ try { rmSync(join(tmpdir(), name), { recursive: true, force: true }); } catch { /* another sweep took it */ }
32
+ }
33
+ }
34
+
35
+ let directory = null;
36
+ function storeDirectory() {
37
+ if (directory != null) return directory;
38
+ sweepScanDirectories();
39
+ directory = mkdtempSync(join(tmpdir(), `supercode-claude-scans-${process.pid}-${threadId}-`));
40
+ process.once('exit', () => { try { rmSync(directory, { recursive: true, force: true }); } catch { /* the next sweep takes it */ } });
41
+ return directory;
42
+ }
43
+
44
+ const SCHEMA = `
45
+ PRAGMA journal_mode=OFF; PRAGMA synchronous=OFF; PRAGMA locking_mode=EXCLUSIVE; PRAGMA temp_store=FILE; PRAGMA cache_size=-1024;
46
+ CREATE TABLE records(line INTEGER PRIMARY KEY, offset INTEGER NOT NULL, length INTEGER NOT NULL, assistant INTEGER NOT NULL,
47
+ error INTEGER NOT NULL, message_id TEXT, hash TEXT NOT NULL) STRICT;
48
+ CREATE TABLE nodes(line INTEGER PRIMARY KEY, uuid TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, parent TEXT, sidechain INTEGER NOT NULL,
49
+ message_id TEXT, tool_result INTEGER NOT NULL, compact TEXT) STRICT;
50
+ CREATE TABLE first_lines(message_id TEXT PRIMARY KEY, line INTEGER NOT NULL) STRICT, WITHOUT ROWID;
51
+ CREATE TABLE residue(line INTEGER PRIMARY KEY, offset INTEGER NOT NULL, length INTEGER NOT NULL, entry TEXT NOT NULL) STRICT;
52
+ CREATE TABLE made(mark TEXT PRIMARY KEY, body TEXT NOT NULL) STRICT, WITHOUT ROWID;
53
+ CREATE TABLE positions(position INTEGER PRIMARY KEY, grp INTEGER NOT NULL, ordinal INTEGER NOT NULL, mark TEXT NOT NULL, synthetic TEXT) STRICT;
54
+ CREATE TABLE groups(grp INTEGER PRIMARY KEY, lines TEXT NOT NULL, size INTEGER NOT NULL) STRICT;
55
+ CREATE TABLE tools(id TEXT PRIMARY KEY, call INTEGER NOT NULL, result INTEGER NOT NULL) STRICT, WITHOUT ROWID;`;
56
+
57
+ /** A duplicate uuid, as the replay graph names it (ClaudeReplayIndex.observe). */
58
+ const duplicate = (uuid) => Object.assign(new Error(`cannot reconstruct lossless Claude continuation: duplicate uuid \`${uuid}\` in Claude transcript`), { code: 'invalid_session' });
59
+
60
+ export class ScanStore {
61
+ constructor() {
62
+ this.path = join(storeDirectory(), `${randomUUID()}.sqlite`);
63
+ this.db = new DatabaseSync(this.path);
64
+ this.db.exec(SCHEMA);
65
+ const prepare = (sql) => this.db.prepare(sql);
66
+ this.statements = {
67
+ record: prepare('INSERT INTO records VALUES (?,?,?,?,?,?,?)'),
68
+ recordAt: prepare('SELECT offset,length,assistant,error,message_id,hash FROM records WHERE line=?'),
69
+ records: prepare('SELECT line,offset,length,assistant,error,message_id,hash FROM records ORDER BY line'),
70
+ node: prepare('INSERT INTO nodes VALUES (?,?,?,?,?,?,?,?)'),
71
+ nodeByUuid: prepare('SELECT line FROM nodes WHERE uuid=?'),
72
+ nodeLine: prepare('SELECT line,parent FROM nodes WHERE uuid=?'),
73
+ nodes: prepare('SELECT line,uuid,kind,parent,sidechain,message_id,tool_result,compact FROM nodes ORDER BY line'),
74
+ firstLine: prepare('INSERT OR IGNORE INTO first_lines VALUES (?,?)'),
75
+ firstLineOf: prepare('SELECT line FROM first_lines WHERE message_id=?'),
76
+ residue: prepare('INSERT INTO residue VALUES (?,?,?,?)'),
77
+ residues: prepare('SELECT line,offset,length,entry FROM residue ORDER BY line'),
78
+ residueCount: prepare('SELECT count(*) AS count FROM residue'),
79
+ made: prepare('INSERT OR REPLACE INTO made VALUES (?,?)'),
80
+ madeOf: prepare('SELECT body FROM made WHERE mark=?'),
81
+ position: prepare('INSERT INTO positions VALUES (?,?,?,?,?)'),
82
+ positionAt: prepare('SELECT grp,ordinal,mark,synthetic FROM positions WHERE position=?'),
83
+ marksFrom: prepare('SELECT mark FROM positions WHERE position>=? AND position<? ORDER BY position'),
84
+ group: prepare('INSERT INTO groups VALUES (?,?,?)'),
85
+ groupAt: prepare('SELECT lines,size FROM groups WHERE grp=?'),
86
+ tool: prepare('INSERT INTO tools VALUES (?,?,?) ON CONFLICT(id) DO UPDATE SET call=max(call,excluded.call),result=max(result,excluded.result)'),
87
+ toolOf: prepare('SELECT call,result FROM tools WHERE id=?'),
88
+ };
89
+ this.depth = 0;
90
+ }
91
+
92
+ /** A transaction across a pass's awaited reads: `end` commits it, or rolls it back when the pass failed. */
93
+ begin() { if (this.depth++ === 0) this.db.exec('BEGIN'); }
94
+ end(failed) { if (--this.depth === 0) this.db.exec(failed ? 'ROLLBACK' : 'COMMIT'); }
95
+
96
+ /** `work` in one transaction (nested calls join the outer one). */
97
+ batch(work) {
98
+ if (this.depth++ === 0) this.db.exec('BEGIN');
99
+ let failed = false;
100
+ try { return work(); } catch (error) { failed = true; throw error; }
101
+ finally { if (--this.depth === 0) this.db.exec(failed ? 'ROLLBACK' : 'COMMIT'); }
102
+ }
103
+
104
+ addRecord(line, facts) {
105
+ this.statements.record.run(line, facts.offset, facts.length, facts.assistant ? 1 : 0, facts.error ? 1 : 0, facts.messageId ?? null, facts.hash);
106
+ if (facts.assistant && facts.messageId != null) this.statements.firstLine.run(facts.messageId, line);
107
+ }
108
+ recordAt(line) {
109
+ const row = this.statements.recordAt.get(line);
110
+ return row == null ? undefined : { offset: row.offset, length: row.length, assistant: row.assistant === 1, error: row.error === 1, messageId: row.message_id, hash: row.hash };
111
+ }
112
+ /** Every record, by line: what a whole replay reads. */
113
+ allRecords() {
114
+ const records = new Map();
115
+ for (const row of this.statements.records.iterate()) records.set(row.line, { offset: row.offset, length: row.length, assistant: row.assistant === 1, error: row.error === 1, messageId: row.message_id, hash: row.hash });
116
+ return records;
117
+ }
118
+ addNode(node) {
119
+ try { this.statements.node.run(node.lineIndex, node.uuid, node.kind, node.parent ?? null, node.sidechain ? 1 : 0, node.messageId ?? null, node.toolResult ? 1 : 0, node.compact == null ? null : JSON.stringify(node.compact)); }
120
+ catch (error) { if (/UNIQUE/.test(error.message)) throw duplicate(node.uuid); throw error; }
121
+ }
122
+ hasUuid(uuid) { return this.statements.nodeByUuid.get(uuid) != null; }
123
+ /** The line of the record `uuid` names, and its parent's uuid, or null. */
124
+ nodeOf(uuid) { const row = this.statements.nodeLine.get(uuid); return row == null ? null : { line: row.line, parent: row.parent }; }
125
+ /** Every replayed node, by line: what a whole replay's graph is built from. */
126
+ *allNodes() {
127
+ for (const row of this.statements.nodes.iterate()) {
128
+ yield { lineIndex: row.line, uuid: row.uuid, kind: row.kind, parent: row.parent, sidechain: row.sidechain === 1, messageId: row.message_id,
129
+ toolResult: row.tool_result === 1, compact: row.compact == null ? null : JSON.parse(row.compact) };
130
+ }
131
+ }
132
+ firstLineOf(messageId) { return this.statements.firstLineOf.get(messageId)?.line; }
133
+ addResidue(line, offset, length, entry) { this.statements.residue.run(line, offset, length, stringifyNativeJson(entry)); }
134
+ *residues() { for (const row of this.statements.residues.iterate()) yield [row.line, { entry: parseNativeJson(row.entry), offset: row.offset, length: row.length }]; }
135
+ get residueCount() { return this.statements.residueCount.get().count; }
136
+
137
+ /** A group's decoded skeletons, kept by the group's mark. */
138
+ addMade(mark, decoded) { this.statements.made.run(mark, stringifyNativeJson(decoded)); }
139
+ madeOf(mark) {
140
+ const row = this.statements.madeOf.get(mark);
141
+ if (row == null) return undefined;
142
+ // a skeleton's metadata is a null-prototype object, as decoding leaves it
143
+ return parseNativeJson(row.body).map(({ model, message }) => ({ model, message: { ...message, metadata: Object.create(null) } }));
144
+ }
145
+
146
+ /** The settled prefix: each message's place (its group and its place in it, or the message itself), its mark, each
147
+ * group's records and size, and the tool calls made and answered. */
148
+ clearSettled() { this.db.exec('DELETE FROM positions; DELETE FROM groups; DELETE FROM tools;'); }
149
+ addPosition(position, group, ordinal, mark, synthetic) { this.statements.position.run(position, group, ordinal, mark, synthetic == null ? null : stringifyNativeJson(synthetic)); }
150
+ positionAt(position) {
151
+ const row = this.statements.positionAt.get(position);
152
+ return { group: row.grp, ordinal: row.ordinal, mark: row.mark, synthetic: row.synthetic == null ? null : parseNativeJson(row.synthetic) };
153
+ }
154
+ *marksFrom(from, to) { for (const row of this.statements.marksFrom.iterate(from, to)) yield row.mark; }
155
+ addGroup(group, lines, size) { this.statements.group.run(group, JSON.stringify(lines), size); }
156
+ groupAt(group) { const row = this.statements.groupAt.get(group); return { lines: JSON.parse(row.lines), size: row.size }; }
157
+ addTool(id, call, result) { this.statements.tool.run(id, call ? 1 : 0, result ? 1 : 0); }
158
+ tool(id) { const row = this.statements.toolOf.get(id); return row == null ? null : { call: row.call === 1, result: row.result === 1 }; }
159
+
160
+ close() {
161
+ try { this.db.close(); } catch { /* already closed */ }
162
+ try { rmSync(this.path, { force: true }); } catch { /* the directory goes with the process */ }
163
+ }
164
+ }
@@ -12,6 +12,15 @@ export function captureClaudeMeta(value, meta, rawLine) {
12
12
  meta.session_id ??= stringField(value, 'sessionId');
13
13
  meta.cwd ??= stringField(value, 'cwd');
14
14
  meta.model ??= stringField(field(value, 'message'), 'model');
15
+ // The native API-error assistant is deliberately absent from normalized
16
+ // conversation messages. Retain its bounded outcome separately: a live
17
+ // terminal is not evidence that its first request could run.
18
+ if (meta.first_response == null && stringField(value, 'type') === 'assistant' && field(value, 'isSidechain') === false
19
+ && stringField(value, 'sessionId') === meta.session_id) {
20
+ const failed = field(value, 'isApiErrorMessage') === true, code = stringField(value, 'error');
21
+ meta.first_response = { outcome: failed ? 'error' : 'response', at: stringField(value, 'timestamp'),
22
+ record_id: stringField(value, 'uuid'), error_code: failed && /^[a-z][a-z0-9_]{0,79}$/.test(code ?? '') ? code : null };
23
+ }
15
24
  if (stringField(value, 'type') === 'fork-context-ref' && !Object.hasOwn(meta.lineage, 'claude_fork_context_ref_raw')) meta.lineage.claude_fork_context_ref_raw = rawLine;
16
25
  }
17
26
 
@@ -0,0 +1,6 @@
1
+ export type CodexActivityStatus = 'running' | 'idle' | 'busy' | 'waiting';
2
+ export class CodexLifecycleCursor {
3
+ read(path: string): 'busy' | 'idle' | null;
4
+ turnStart(): { trigger: string | null; started_at_ms: number | null } | null;
5
+ }
6
+ export function reconcileCodexOwnership(previous: Iterable<string>, observed: Iterable<string>, misses: Map<string, number>): string[];
@@ -0,0 +1,120 @@
1
+ // Incremental Codex lifecycle evidence. A cursor owns no process or file:
2
+ // every sample opens its named rollout read-only and closes it before return.
3
+ // A kept cursor reads only what was appended since its last sample, and answers
4
+ // what a fresh scan of the whole file answers: its offset stops at the end of the
5
+ // last complete line, so a record still being written is read whole next time; a
6
+ // replaced file (another identity) or one shorter than, or rewritten before, the
7
+ // offset is scanned again from its start.
8
+ import { openSync, fstatSync, readSync, closeSync } from 'node:fs';
9
+ import { parseNativeJson, nativeInteger, setNativeInteger, cloneNativeJson } from './native-json.mjs';
10
+ import { field, stringField } from './session-codec-utils.mjs';
11
+
12
+ const BLOCK_BYTES = 1024n * 1024n, OVERLAP_BYTES = 64n * 1024n, TAIL_BYTES = 64n * 1024n, MARK_BYTES = 64n;
13
+ const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
14
+ const boundaries = ['"type":"task_started"', '"type":"task_complete"', '"type":"turn_aborted"'];
15
+
16
+ function lifecycleInTail(bytes, startsAtBoundary) {
17
+ const newline = bytes.indexOf(10), offset = startsAtBoundary ? 0 : newline < 0 ? bytes.length : newline + 1;
18
+ let text; try { text = decoder.decode(bytes.subarray(offset)); } catch { return null; }
19
+ let searchEnd = text.length;
20
+ while (searchEnd > 0) {
21
+ const before = text.slice(0, searchEnd), candidate = Math.max(...boundaries.map(boundary => before.lastIndexOf(boundary)));
22
+ if (candidate < 0) break;
23
+ const start = text.lastIndexOf('\n', candidate) + 1, next = text.indexOf('\n', candidate), end = next < 0 ? text.length : next;
24
+ try {
25
+ const event = parseNativeJson(text.slice(start, end));
26
+ if (stringField(event, 'type') === 'event_msg') {
27
+ const payload = field(event, 'payload'), kind = stringField(payload, 'type');
28
+ if (kind === 'task_started') {
29
+ const turn = { trigger: stringField(field(payload, 'turn_attribution'), 'turn_trigger'), started_at_ms: null };
30
+ const seconds = nativeInteger(payload, 'started_at');
31
+ if (seconds != null && seconds <= 0xffffffffffffffffn / 1000n) setNativeInteger(turn, 'started_at_ms', seconds * 1000n);
32
+ return { status: 'busy', turn };
33
+ }
34
+ if (kind === 'task_complete' || kind === 'turn_aborted') return { status: 'idle', turn: null };
35
+ }
36
+ } catch { /* malformed candidates establish no lifecycle boundary */ }
37
+ searchEnd = candidate;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function readExact(fd, bytes, position) {
43
+ let read = 0;
44
+ while (read < bytes.length) {
45
+ const count = readSync(fd, bytes, read, bytes.length - read, position + BigInt(read));
46
+ // a file cut short under the read establishes nothing this sample
47
+ if (!count) throw new Error('rollout ended before its stated length');
48
+ read += count;
49
+ }
50
+ return bytes;
51
+ }
52
+
53
+ function latestBetween(fd, floor, upper) {
54
+ let end = upper;
55
+ while (end > floor) {
56
+ const start = end - floor > BLOCK_BYTES ? end - BLOCK_BYTES : floor;
57
+ const status = lifecycleInTail(readExact(fd, Buffer.alloc(Number(end - start)), start), start === floor);
58
+ if (status != null) return status;
59
+ if (start === floor) break;
60
+ end = start + OVERLAP_BYTES;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /** The end of the last complete line in [floor, upper), or `floor` when it holds none. */
66
+ function completeEnd(fd, floor, upper) {
67
+ let end = upper;
68
+ while (end > floor) {
69
+ const start = end - floor > TAIL_BYTES ? end - TAIL_BYTES : floor, bytes = readExact(fd, Buffer.alloc(Number(end - start)), start);
70
+ const newline = bytes.lastIndexOf(10);
71
+ if (newline >= 0) return start + BigInt(newline) + 1n;
72
+ end = start;
73
+ }
74
+ return floor;
75
+ }
76
+
77
+ /** Keep a proven boundary through appends without re-reading earlier records. */
78
+ export class CodexLifecycleCursor {
79
+ #identity = null; #offset = 0n; #mark = null; #status = null; #turn = null; #tail = null;
80
+ #reset() { this.#offset = 0n; this.#mark = null; this.#status = null; this.#turn = null; }
81
+ /** The latest boundary's open turn, from the same incremental read. */
82
+ turnStart() { return cloneNativeJson(this.#tail ? this.#tail.turn : this.#turn); }
83
+ read(path) {
84
+ let fd;
85
+ try {
86
+ fd = openSync(path, 'r'); const stat = fstatSync(fd, { bigint: true }), length = stat.size, identity = `${stat.dev}:${stat.ino}`;
87
+ if (identity !== this.#identity || length < this.#offset) { this.#reset(); this.#identity = identity; }
88
+ // the bytes just before the offset, read again: a file rewritten in place (same identity, as long or longer) differs there
89
+ if (this.#mark != null && !readExact(fd, Buffer.alloc(this.#mark.length), this.#offset - BigInt(this.#mark.length)).equals(this.#mark)) this.#reset();
90
+ this.#tail = null;
91
+ if (length > this.#offset) {
92
+ const complete = completeEnd(fd, this.#offset, length);
93
+ const found = complete > this.#offset ? latestBetween(fd, this.#offset, complete) : null;
94
+ // a line not yet ended is read with the next sample too: its boundary answers this one but is not kept
95
+ const tail = complete < length ? lifecycleInTail(readExact(fd, Buffer.alloc(Number(length - complete)), complete), true) : null;
96
+ if (found != null) { this.#status = found.status; this.#turn = found.turn; }
97
+ if (complete > this.#offset) {
98
+ const kept = complete < MARK_BYTES ? complete : MARK_BYTES;
99
+ this.#mark = readExact(fd, Buffer.alloc(Number(kept)), complete - kept); this.#offset = complete;
100
+ }
101
+ this.#tail = tail;
102
+ }
103
+ return this.#tail ? this.#tail.status : this.#status;
104
+ } catch { this.#tail = null; return null; }
105
+ finally { if (fd != null) { try { closeSync(fd); } catch { /* the cursor keeps no descriptor */ } } }
106
+ }
107
+ }
108
+
109
+ /** One missed process snapshot retains ownership; the second retires it. */
110
+ export function reconcileCodexOwnership(previous, observed, misses) {
111
+ const current = new Set(observed), before = new Set(previous), retained = new Set(current);
112
+ for (const path of current) misses.delete(path);
113
+ for (const path of before) {
114
+ if (current.has(path)) continue;
115
+ const count = Math.min(255, (misses.get(path) ?? 0) + 1);
116
+ if (count < 2) { misses.set(path, count); retained.add(path); } else misses.delete(path);
117
+ }
118
+ for (const path of misses.keys()) if (!before.has(path) || current.has(path)) misses.delete(path);
119
+ return [...retained].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)));
120
+ }
@@ -11,6 +11,7 @@ import { detectSessionSource } from './session-source.mjs';
11
11
  import { codexDisplayFold } from './session-codex-display.mjs';
12
12
  import { clearCodexBookkeeping } from './session-codex-items.mjs';
13
13
  import { AppendScan, shortHash } from './session-append-scan.mjs';
14
+ import { MARK_CHECKPOINT, chainMark, markedFrom } from './session-claude-index.mjs';
14
15
 
15
16
  // What a message of the fold is made of, for a message held whole and for one kept without what it says: the records
16
17
  // that made it (where each stands: the one that made it, then each whose call joined its open turn), its place among
@@ -129,7 +130,7 @@ class CodexDisplayScan extends AppendScan {
129
130
  }
130
131
  for await (const line of sessionFileLines(reader, this.bytes)) {
131
132
  if (!line.terminated) break;
132
- await this.#take(this.state, line, reader); this.hashed.update(line.bytes); this.bytes = line.offset + line.length;
133
+ await this.#take(this.state, line, reader); this.took(line.bytes, line.offset + line.length); this.bytes = line.offset + line.length;
133
134
  }
134
135
  await this.settle(reader, from);
135
136
  }
@@ -168,15 +169,34 @@ async function scanOf(path, reader, anchor, first) {
168
169
  return scan;
169
170
  }
170
171
 
172
+ /** The marks a read lists after the first place the reader's (`known`) may differ: all of them when it names none. */
173
+ function listed(marks, known) {
174
+ const from = known == null ? 0 : markedFrom(markChainOf(marks), known);
175
+ return { marks_from: from, tail_marks: from ? marks.slice(from) : marks };
176
+ }
177
+
178
+ /** A read's marks as their chain (chainMark), for markedFrom: how many, and the digest of the first `count`. */
179
+ function markChainOf(marks) {
180
+ const checkpoints = []; let digest = '';
181
+ for (let at = 0; at < marks.length; at++) { digest = chainMark(digest, marks[at]); if ((at + 1) % MARK_CHECKPOINT === 0) checkpoints.push(digest); }
182
+ return { total: marks.length, digestAt(count) {
183
+ let at = Math.floor(count / MARK_CHECKPOINT) * MARK_CHECKPOINT, chain = at ? checkpoints[at / MARK_CHECKPOINT - 1] : '';
184
+ for (; at < count; at++) chain = chainMark(chain, marks[at]);
185
+ return chain;
186
+ } };
187
+ }
188
+
171
189
  /**
172
190
  * A window of a Codex rollout's display history as read from the byte `anchor` (loadSessionDisplayFrom), from the
173
191
  * message at place `from`: at most `messages` of them and `bytes` of rollout (one message at least), with each one's
174
- * mark and, when `marks` is asked, the mark of every message of that read. The rollout is read a line at a time and
192
+ * mark and, when `marks` is asked, the marks after the first place where those the reader holds (`known`, as
193
+ * loadClaudeRecordWindow names them) differ from these: `marks_from` and `tail_marks` (from the first when nothing is
194
+ * named), so a pass over a long rollout lists what changed, not every message. The rollout is read a line at a time and
175
195
  * only as far as it was not read before; a message is the same one the whole read answers at that place, and its
176
196
  * mark is the digest of it: while it stands at a place, so does what a reader took of that message there. Null when
177
197
  * the locator names anything else, which is loaded as its document.
178
198
  */
179
- export async function loadCodexRecordWindow(locator, anchor, { from, bytes = null, messages = null, marks = false }, { metrics } = {}) {
199
+ export async function loadCodexRecordWindow(locator, anchor, { from, bytes = null, messages = null, marks = false, known = null }, { metrics } = {}) {
180
200
  const { path, kind } = locator.storage;
181
201
  if (kind !== 'file' || locator.harness === 'hermes' || anchor === 0 || isCompressedSessionPath(path)) return null;
182
202
  if ((await sourceStat(path))?.isDirectory() || await looksLikeSessionSqlite(path, metrics)) return null;
@@ -206,7 +226,7 @@ export async function loadCodexRecordWindow(locator, anchor, { from, bytes = nul
206
226
  session.load_residue = ['display history is a bounded native-record projection, not resumable model context', `older native records remain before this display anchor (${anchor})`];
207
227
  return { session: { ...normalizedSessionJson(session), raw_record_count: state.fold.recordCount,
208
228
  resolved_profile: recordedSessionProfile(session, { model: view.model, version: null }), resolved_profile_history: recordedSessionProfileHistory(session),
209
- window: { offset, returned: end - offset, total_messages: total, marks: view.marks.slice(offset, end), ...(marks ? { all_marks: view.marks } : {}) } } };
229
+ window: { offset, returned: end - offset, total_messages: total, marks: view.marks.slice(offset, end), ...(marks ? listed(view.marks, known) : {}) } } };
210
230
  } catch (error) { scans.delete(path); throw error; }
211
231
  }, metrics)).result;
212
232
  }