@plurnk/plurnk-digest 3.2.2 → 3.2.4

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.
@@ -1,152 +1,24 @@
1
- // Digest renderers ({§digest-programmatic-surface}): the markdown, JSON, reasoning, and packet
1
+ // Digest renderers ({§share-packet-names}): the markdown, JSON, reasoning, and packet
2
2
  // artifacts of one DigestModel, reading heavy evidence on demand through DigestEvidence.
3
- import { mkdirSync, writeFileSync } from "node:fs";
4
- import { dirname, join, posix } from "node:path";
5
- import { aggregateProviderAccounting, } from "@plurnk/plurnk-providers";
6
- import { Validator } from "@plurnk/plurnk-contracts";
7
- import { EDIT_FORMS } from "./digest-rows.js";
8
- import { isExecutionOp } from "@plurnk/plurnk-contracts";
9
- function* projectRows(rows, project) {
10
- for (const row of rows)
11
- yield project(row);
12
- }
3
+ import { isRecord } from "@plurnk/plurnk-contracts";
4
+ import { accounting, costSummary, environmentOf, envLine, indentLines, link, literal, parseJson, renderStream, rowProblem, terminalResult, usageSummary, wireLine, } from "./DigestFormat.js";
5
+ import { cacheBadge, cacheLedger, cacheLine, countsAsError, editCensus, editForm, loopHealth, renderEditCensus, renderEmissions, renderReasonings, renderRoom, } from "./DigestLedgers.js";
6
+ import { json, packetFiles, packetStems, request, turnLinks } from "./DigestArtifacts.js";
13
7
  export default class DigestRender {
14
- static #literal(body, language = "text") {
15
- let length = 3;
16
- for (const match of body.matchAll(/`+/gu))
17
- length = Math.max(length, match[0].length + 1);
18
- const fence = "`".repeat(length);
19
- return `${fence}${language}\n${body}\n${fence}`;
20
- }
21
- static #link(from, target, label) {
22
- return `[${label}](${posix.relative(posix.dirname(from), target)})`;
23
- }
24
- static #turnLinks(m, turn, from) {
25
- const stem = DigestRender.packetStems(m).get(turn.id);
26
- if (stem === undefined)
27
- return [];
28
- return [
29
- ...(turn.has_packet === 1 ? [DigestRender.#link(from, `${stem}.request.md`, "Input")] : []),
30
- ...(turn.program !== null ? [DigestRender.#link(from, `${stem}.assistant.md`, "Output")] : []),
31
- ...(turn.has_reasoning === 1 ? [DigestRender.#link(from, `${stem}.reasoning.md`, "Reasoning")] : []),
32
- ];
33
- }
34
- static #summarize(text, n = 80) {
35
- if (text === null || text === undefined)
36
- return "";
37
- const flat = String(text).replace(/\s+/g, " ").trim();
38
- if (flat.length <= n)
39
- return flat;
40
- return `${flat.slice(0, n)}…`;
41
- }
42
- static #indentLines(text, prefix = " ") {
43
- return text.split("\n").map((line) => line.length > 0 ? `${prefix}${line}` : line).join("\n");
44
- }
45
- static parseJson(s, fallback = null) {
46
- if (s === null || s === undefined)
47
- return fallback;
48
- try {
49
- return JSON.parse(String(s));
50
- }
51
- catch {
52
- return fallback;
53
- }
54
- }
55
- static #requestAccounting(row) {
56
- if (row.state !== "settled" || row.accounting === null) {
57
- throw new TypeError(`provider request ${row.id} is not settled`);
58
- }
59
- return row.accounting;
60
- }
61
- static #accounting(rows) {
62
- return rows.some((row) => row.state !== "settled")
63
- ? null
64
- : aggregateProviderAccounting(rows.map((row) => DigestRender.#requestAccounting(row)));
65
- }
66
- static usageSummary(accounting) {
67
- if (accounting === null)
68
- return "accounting=incomplete";
69
- const usage = accounting.usage;
70
- const known = accounting.knownUsage;
71
- const quantity = (total, subtotal) => total !== undefined ? String(total) : subtotal === undefined ? "unknown" : `${subtotal}+?`;
72
- return [
73
- `input=${quantity(usage?.inputTokens, known?.inputTokens)}`,
74
- `output=${quantity(usage?.outputTokens, known?.outputTokens)}`,
75
- `reasoning=${quantity(usage?.outputTokenDetails?.reasoningTokens, known?.outputTokenDetails?.reasoningTokens)}`,
76
- `cache-read=${quantity(usage?.inputTokenDetails?.cacheReadTokens, known?.inputTokenDetails?.cacheReadTokens)}`,
77
- ].join(" ");
78
- }
79
- static costSummary(accounting) {
80
- if (accounting?.costUsd != null)
81
- return `$${accounting.costUsd}`;
82
- return accounting?.knownCostUsd == null ? "unknown" : `$${accounting.knownCostUsd} + ? (incomplete)`;
83
- }
84
- static #operationResult(raw, subject) {
85
- try {
86
- return Validator.assertOperationResult(DigestRender.parseJson(raw));
87
- }
88
- catch (cause) {
89
- throw new Error(`${subject} does not contain a valid operation result`, { cause });
90
- }
91
- }
92
- static #rowProblem(row) {
93
- const result = DigestRender.#operationResult(row.rx, `failed log entry ${row.id}`);
94
- if (result.problem === undefined) {
95
- throw new Error(`failed log entry ${row.id} does not contain Problem Details`);
96
- }
97
- return result.problem;
98
- }
99
- static #terminalResult(loop) {
100
- return loop.terminal_result === null
101
- ? null
102
- : DigestRender.#operationResult(loop.terminal_result, `terminal loop ${loop.id}`);
103
- }
104
- static #renderTarget(le) {
105
- return le.target;
106
- }
107
- static #renderStream(le) {
108
- if (!isExecutionOp(le.op))
109
- return null;
110
- const stream = DigestRender.parseJson(le.attrs, {}).stream;
111
- return typeof stream === "string" ? stream : null;
112
- }
113
- // {§exec-env-scoped} — the environment a spawn received, as recorded on its row: host names by
114
- // name, the Worker's own values, inherited values by their source, masked names. Host values
115
- // stay in digest.json. Part of the rendered line, so spawns under different environments never
116
- // collapse into one group.
117
- static envLine(env) {
118
- if (env === undefined || env === null || typeof env !== "object")
119
- return null;
120
- const host = [];
121
- const rest = [];
122
- for (const [name, raw] of Object.entries(env).toSorted(([left], [right]) => left.localeCompare(right))) {
123
- const record = raw;
124
- const from = typeof record.from === "string" ? record.from : null;
125
- if (record.source === "host")
126
- host.push(name);
127
- else if (record.source === "masked")
128
- rest.push(`${name} (masked${from === null ? "" : ` by ${from}`})`);
129
- else if (record.source === "modifier")
130
- rest.push(`${name}=${DigestRender.#summarize(record.value, 60)} (modifier)`);
131
- else if (record.source === "workspace")
132
- rest.push(`${name}=${DigestRender.#summarize(record.value, 60)} (workspace)`);
133
- else
134
- rest.push(`${name}=${DigestRender.#summarize(record.value, 60)} (${from === null ? "worker" : `from ${from}`})`);
135
- }
136
- const parts = [...(host.length === 0 ? [] : [`host ${host.join(",")}`]), ...rest];
137
- return `env: ${parts.length === 0 ? "(empty)" : parts.join(" · ")}`;
138
- }
139
- // The environment a spawn recorded on its output, found through the row's stream address.
140
- static #environmentOf(le, m) {
141
- const stream = DigestRender.#renderStream(le);
142
- if (stream === null)
143
- return undefined;
144
- const worker = m.workersById.get(le.worker_id);
145
- return worker === undefined ? undefined : m.environments.get(`${worker.workspace_id}:${stream}`);
146
- }
8
+ static parseJson = parseJson;
9
+ static usageSummary = usageSummary;
10
+ static costSummary = costSummary;
11
+ static envLine = envLine;
12
+ static wireLine = wireLine;
13
+ static editForm = editForm;
14
+ static cacheLedger = cacheLedger;
15
+ static packetStems = packetStems;
16
+ static request = request;
17
+ static packetFiles = packetFiles;
18
+ static json = json;
147
19
  static #renderOpLine(le, label = le.op ?? "source artifact", environment) {
148
- const target = DigestRender.#renderTarget(le) ?? "—";
149
- const stream = DigestRender.#renderStream(le);
20
+ const target = le.target ?? "—";
21
+ const stream = renderStream(le);
150
22
  const state = le.state !== "resolved" ? ` state=${le.state}` : "";
151
23
  const outcome = le.outcome !== null ? ` outcome=${le.outcome}` : "";
152
24
  const streamLink = stream === null ? "" : ` stream=${stream}`;
@@ -156,30 +28,31 @@ export default class DigestRender {
156
28
  // the waterfall explains WHY each failure happened without opening packets.
157
29
  let errLine = "";
158
30
  if (le.status_rx >= 400) {
159
- errLine = `\n -> ${DigestRender.#rowProblem(le).detail.trim()}`;
31
+ errLine = `\n -> ${rowProblem(le).detail.trim()}`;
160
32
  }
161
- const envLine = DigestRender.envLine(environment);
162
- const envText = envLine === null ? "" : `\n ${envLine}`;
33
+ const env = envLine(environment);
34
+ const envText = env === null ? "" : `\n ${env}`;
163
35
  const decision = le.proposalDecision;
164
36
  const approval = decision === undefined ? "" : `\n decision=${decision.decision} resolver=${decision.resolver} at=${decision.decided_at}${decision.body === null ? "" : " body override recorded"}${decision.outcome === null ? "" : ` outcome=${decision.outcome}`}`;
165
37
  return ` ← [${le.origin}] ${label}[${le.status_rx}] ${target}${source}${state}${outcome}${streamLink}${fail}${errLine}${envText}${approval}`;
166
38
  }
167
39
  static #renderGroupedOpLine(row, m) {
168
- const attrs = DigestRender.parseJson(row.attrs, {});
169
- // {§emission-row}: the announcement of an admitted emission, and whether the model retired it.
40
+ const parsedAttrs = parseJson(row.attrs, {});
41
+ const attrs = isRecord(parsedAttrs) ? parsedAttrs : {};
42
+ // {§turn-source-resources}: the announcement of an admitted emission, and whether the model retired it.
170
43
  if (row.op === "READ" && attrs.kind === "emission") {
171
- return DigestRender.#renderOpLine(row, row.projection_active === 1 ? "emission" : "emission (killed)", DigestRender.#environmentOf(row, m));
44
+ return DigestRender.#renderOpLine(row, row.projection_active === 1 ? "emission" : "emission (killed)", environmentOf(row, m));
172
45
  }
173
- // {§reasoning-row}: the turn's reasoning landed as a row, and whether the model retired it.
46
+ // {§turn-source-resources}: the turn's reasoning landed as a row, and whether the model retired it.
174
47
  if (row.op === "READ" && attrs.kind === "reasoning") {
175
- return DigestRender.#renderOpLine(row, row.projection_active === 1 ? "reasoning" : "reasoning (killed)", DigestRender.#environmentOf(row, m));
48
+ return DigestRender.#renderOpLine(row, row.projection_active === 1 ? "reasoning" : "reasoning (killed)", environmentOf(row, m));
176
49
  }
177
50
  const materialized = row.origin === "_plurnk" && row.op === "EDIT" && attrs.kind === "entry_materialized";
178
51
  const actionlessKind = row.op === null ? attrs.kind : null;
179
52
  const label = actionlessKind === "emissionAttempt"
180
53
  ? "emission attempt"
181
54
  : row.op ?? `unrecognized actionless row (kind=${JSON.stringify(actionlessKind) ?? "absent"})`;
182
- return DigestRender.#renderOpLine(row, materialized ? "materialized entry" : label, DigestRender.#environmentOf(row, m));
55
+ return DigestRender.#renderOpLine(row, materialized ? "materialized entry" : label, environmentOf(row, m));
183
56
  }
184
57
  // Human triage is not a row dump. Preserve every row in digest.json, but
185
58
  // collapse consecutive identical rendered outcomes in the Markdown waterfall. Using the
@@ -202,260 +75,52 @@ export default class DigestRender {
202
75
  return count === 1 ? line : `${line} ×${count} (seq ${firstSeq}–${lastSeq})`;
203
76
  });
204
77
  }
205
- // The "degenerate win" lens (owner ask): a loop's health = how many errors/strikes it earned
206
- // vs whether it still concluded. A green that limped across on 16 errors is a FAILING artifact
207
- // wearing a passing badge; the digest must make that impossible to miss. errors = ≥400 op rows;
208
- // errorItems = minted op='error' rows (truncation/budget/steer/cycle — the strike signals).
209
- // {§digest-executor-evidence} (#436) — a failed command's completion rows carry the
210
- // executor's problem identity; a red test run is the loop's normal work, not a
211
- // defect. They stay visible but never count as errors in health or the errs
212
- // badge — the digest mirror of the strike rail's exemption (#425 F1).
213
- static #isExecutorEvidence(le) {
214
- if (le.status_rx < 400 || le.rx === null)
215
- return false;
216
- const rx = DigestRender.parseJson(le.rx, null);
217
- return typeof rx?.problem?.type === "string"
218
- && rx.problem.type.startsWith("https://problems.plurnk.xyz/executor/");
219
- }
220
- // {§digest-capacity-evidence} (#1096) — a 413 is the capacity contract working: a result that did
221
- // not fit, the over-budget mandate, a limit met. It renders like any row and never counts as an
222
- // error, an error item or in the errs badge; a loop that concluded under a binding budget is CLEAN.
223
- static #isCapacityEvidence(le) {
224
- return le.status_rx === 413;
225
- }
226
- static #countsAsError(le) {
227
- return le.status_rx >= 400 && !DigestRender.#isExecutorEvidence(le) && !DigestRender.#isCapacityEvidence(le);
228
- }
229
- // {§digest-storage} — size, free pages, vacuum mode and the largest tables, one line.
78
+ // {§share-packet-names} — size, free pages, vacuum mode and the largest tables, one line.
230
79
  static #storageLine(storage) {
231
80
  const mb = (bytes) => `${(bytes / 1_048_576).toFixed(1)} MB`;
232
81
  const mode = ["off", "full", "incremental"][storage.auto_vacuum] ?? String(storage.auto_vacuum);
233
82
  return `Storage: ${mb(storage.bytes)} (free ${mb(storage.free_bytes)}, auto_vacuum ${mode}) · largest: ${storage.tables.map(({ name, bytes }) => `${name} ${mb(bytes)}`).join(", ")}`;
234
83
  }
235
- // {§loop-claim-latency} — claim to first model turn: a stall between them is a number, not a gap.
84
+ // {§session-wait-timing} — claim to first model turn: a stall between them is a number, not a gap.
236
85
  static #wakeLatency(loop, m) {
237
86
  if (loop.claimed_at === null)
238
87
  return null;
239
- const first = (m.turnsByLoop.get(loop.id) ?? []).filter((t) => t.kind === "inference").sort((a, b) => a.sequence - b.sequence)[0];
88
+ const first = (m.turnsByLoop.get(loop.id) ?? []).filter((t) => t.kind === "inference").toSorted((a, b) => a.sequence - b.sequence)[0];
240
89
  if (first === undefined)
241
90
  return `Claimed: ${loop.claimed_at} · no model turn`;
242
91
  const seconds = (Date.parse(first.timestamp) - Date.parse(loop.claimed_at)) / 1000;
243
92
  return `Claimed: ${loop.claimed_at} · first model turn +${seconds.toFixed(1)} s`;
244
93
  }
245
- // {§digest-cache-ledger} — the reconstructed text envelope, each role above its content,
246
- // as `.wire.json` holds it ({§share-packet-names}); null when no valid packet is stored.
247
- static #promptText(turn, m) {
248
- const { packet } = m.evidence.packet(turn);
249
- if (packet === null)
250
- return null;
251
- const messages = packet.messages();
252
- if (messages.length === 0)
94
+ // Among its loop's model turns, this one's ordinal: the model's own count, which skips the
95
+ // harness turns a reader would otherwise have to subtract.
96
+ static #modelTurnOrdinal(turn, m) {
97
+ if (turn.producer !== "model")
253
98
  return null;
254
- return messages
255
- .map(({ role, content }) => `${role}\n${content}`)
256
- .join("\n");
257
- }
258
- static #commonPrefixLength(left, right) {
259
- const bound = Math.min(left.length, right.length);
260
- let n = 0;
261
- while (n < bound && left.charCodeAt(n) === right.charCodeAt(n))
262
- n += 1;
263
- return n;
264
- }
265
- // {§digest-edit-census}: the form an EDIT authored, read from its stored marker and pattern.
266
- static editForm(row) {
267
- if (row.pattern !== null)
268
- return "pattern";
269
- if (row.line_marker === null)
270
- return "whole";
271
- const { marks } = DigestRender.parseJson(row.line_marker, { marks: [] });
272
- if (marks.length === 1) {
273
- const [mark] = marks;
274
- if (typeof mark === "string")
275
- return "hash";
276
- if (mark === -1)
277
- return "append";
278
- if (mark === 0)
279
- return "prepend";
280
- return "line";
281
- }
282
- if (marks.length === 2)
283
- return "range";
284
- if (marks.length === 3)
285
- return "offset";
286
- if (marks.length === 4 && marks[0] === marks[2] && marks[1] === 1 && marks[3] === 1)
287
- return "insert";
288
- return "column";
289
- }
290
- static #editCensusCache = new WeakMap();
291
- // {§digest-edit-census}: per worker, every model EDIT by authored form and status, and the
292
- // revisits — an EDIT of a path the same worker edited within its previous two model turns.
293
- static #editCensus(m) {
294
- const cached = DigestRender.#editCensusCache.get(m);
295
- if (cached !== undefined)
296
- return cached;
297
- const loopWorker = new Map(m.loops.map((loop) => [loop.id, loop.worker_id]));
298
- // A worker's model turns in order; the ordinal is the distance the revisit window counts in.
299
- const ordinal = new Map();
300
- const perWorker = new Map();
301
- for (const turn of [...m.turns].sort((a, b) => a.id - b.id)) {
302
- if (turn.producer !== "model")
303
- continue;
304
- const workerId = loopWorker.get(turn.loop_id);
305
- if (workerId === undefined)
306
- throw new TypeError(`turn ${turn.id} has no loop in scope`);
307
- const next = (perWorker.get(workerId) ?? 0) + 1;
308
- perWorker.set(workerId, next);
309
- ordinal.set(turn.id, next);
310
- }
311
- const byWorker = new Map();
312
- const formById = new Map();
313
- const revisitById = new Set();
314
- for (const [workerId, rows] of m.editRowsByWorker) {
315
- const census = { edits: 0, refused: 0, revisits: 0, forms: Object.fromEntries(EDIT_FORMS.map((form) => [form, 0])) };
316
- const lastEdited = new Map();
317
- for (const row of rows) {
318
- const form = DigestRender.editForm(row);
319
- formById.set(row.id, form);
320
- census.edits += 1;
321
- census.forms[form] += 1;
322
- if (row.status_rx >= 400)
323
- census.refused += 1;
324
- const at = ordinal.get(row.turn_id);
325
- if (at === undefined)
326
- throw new TypeError(`EDIT ${row.id} sits on a turn no model produced`);
327
- if (row.pathname !== null) {
328
- const previous = lastEdited.get(row.pathname);
329
- if (previous !== undefined && previous < at && at - previous <= 2) {
330
- census.revisits += 1;
331
- revisitById.add(row.id);
332
- }
333
- lastEdited.set(row.pathname, at);
334
- }
335
- }
336
- byWorker.set(workerId, census);
337
- }
338
- const result = { byWorker, formById, revisitById };
339
- DigestRender.#editCensusCache.set(m, result);
340
- return result;
341
- }
342
- static #renderEditCensus(census) {
343
- if (census === undefined || census.edits === 0)
344
- return "(no edits)";
345
- const forms = EDIT_FORMS.filter((form) => census.forms[form] > 0).map((form) => `${form}=${census.forms[form]}`).join(" ");
346
- return `${census.edits} · ${forms} · refused=${census.refused} · revisits=${census.revisits}`;
347
- }
348
- static #cacheLedgerCache = new WeakMap();
349
- // {§digest-cache-ledger} — estimate adjacent prefixes with at most two prompts resident per loop.
350
- static cacheLedger(m) {
351
- const cached = DigestRender.#cacheLedgerCache.get(m);
352
- if (cached !== undefined)
353
- return cached;
354
- const turnsById = new Map(m.turns.map((turn) => [turn.id, turn]));
355
- const ledger = new Map();
356
- for (const loop of m.loops) {
357
- let memo;
358
- const promptOf = (turn) => {
359
- if (memo?.turnId !== turn.id)
360
- memo = { turnId: turn.id, text: DigestRender.#promptText(turn, m) };
361
- return memo.text;
362
- };
363
- // undefined: no previous request in this loop; null: the previous request's packet is not valid.
364
- let previousText;
365
- for (const request of m.requestsByLoop.get(loop.id) ?? []) {
366
- const turn = turnsById.get(request.turn_id);
367
- if (turn === undefined)
368
- throw new TypeError(`provider request ${request.id} has no turn in scope`);
369
- const text = request.kind === "emission" ? promptOf(turn) : null;
370
- const adjacentPrefixTokensEstimate = text === null || request.usage_input === null
371
- ? null
372
- : previousText === undefined
373
- ? 0
374
- : previousText === null
375
- ? null
376
- : text.length === 0
377
- ? 0
378
- : Math.round(request.usage_input * m.evidence.textWeight(text.slice(0, DigestRender.#commonPrefixLength(previousText, text))) / m.evidence.textWeight(text));
379
- ledger.set(request.id, {
380
- adjacentPrefixTokensEstimate,
381
- cachedTokens: request.usage_input_cache_read,
382
- inputTokens: request.usage_input,
383
- });
384
- previousText = text;
385
- }
386
- }
387
- for (const request of m.providerRequests) {
388
- if (!ledger.has(request.id))
389
- throw new TypeError(`provider request ${request.id} has no loop in scope`);
390
- }
391
- DigestRender.#cacheLedgerCache.set(m, ledger);
392
- return ledger;
393
- }
394
- static #cacheEntries(requests, m) {
395
- const ledger = DigestRender.cacheLedger(m);
396
- return requests.map((request) => ledger.get(request.id));
397
- }
398
- // {§digest-cache-ledger} — sum each provider-reported counter; one absent quantity makes that sum unknown.
399
- static #cacheBadge(turn, m) {
400
- const entries = DigestRender.#cacheEntries(m.requestsByTurn.get(turn.id) ?? [], m);
401
- if (entries.length === 0)
402
- return "";
403
- const sum = (values) => values.some((value) => value === null)
404
- ? "?"
405
- : String(values.reduce((total, value) => total + value, 0));
406
- return ` cache=${sum(entries.map((entry) => entry.cachedTokens))}/${sum(entries.map((entry) => entry.inputTokens))}`;
407
- }
408
- // {§digest-cache-ledger} — one workspace line; unreported requests are named and left out of the percentage.
409
- static #cacheLine(requests, m) {
410
- const entries = DigestRender.#cacheEntries(requests, m);
411
- const reported = entries.filter((entry) => entry.cachedTokens !== null && entry.inputTokens !== null);
412
- const unreported = entries.length - reported.length;
413
- const cached = reported.reduce((total, entry) => total + entry.cachedTokens, 0);
414
- const input = reported.reduce((total, entry) => total + entry.inputTokens, 0);
415
- const pct = input === 0 ? "n/a" : `${((100 * cached) / input).toFixed(1)}%`;
416
- const plural = (n) => n === 1 ? "request" : "requests";
417
- return [
418
- `Cache: ${cached} of ${input} reported input tokens read from cache (${pct}) over ${reported.length} ${plural(reported.length)}`,
419
- unreported > 0 ? `${unreported} missing input or cache usage (excluded)` : null,
420
- ].filter((part) => part !== null).join(" · ");
421
- }
422
- static #loopHealth(loop, m) {
423
- let errors = 0;
424
- let errorItems = 0;
425
- for (const t of m.turnsByLoop.get(loop.id) ?? []) {
426
- for (const le of m.logEntriesByTurn.get(t.id) ?? []) {
427
- if (DigestRender.#countsAsError(le))
428
- errors += 1;
429
- if (le.op === "error" && !DigestRender.#isCapacityEvidence(le))
430
- errorItems += 1;
431
- }
432
- }
433
- const s = loop.status;
434
- const verdict = s >= 400 ? `FAILED(${s})`
435
- : s >= 200 && s < 300 ? (errors > 0 ? "DEGENERATE-WIN" : "CLEAN")
436
- : `status=${s}`;
437
- return { errors, errorItems, verdict };
99
+ const siblings = (m.turnsByLoop.get(turn.loop_id) ?? []).filter((t) => t.producer === "model");
100
+ const index = siblings.findIndex((t) => t.id === turn.id);
101
+ return index < 0 ? null : index + 1;
438
102
  }
439
103
  static #renderTurnLine(turn, m) {
440
104
  const { packet, packetFailure } = m.evidence.packet(turn);
441
105
  const assistant = packet?.assistant ?? null;
442
106
  const content = assistant?.content ?? "";
443
107
  const reasoning = assistant?.reasoning ?? null;
444
- const accounting = DigestRender.#accounting(m.requestsByTurn.get(turn.id) ?? []);
445
- const tokens = DigestRender.usageSummary(accounting);
446
- const cost = ` cost=${DigestRender.costSummary(accounting)}`;
108
+ const acct = accounting(m.requestsByTurn.get(turn.id) ?? []);
109
+ const tokens = usageSummary(acct);
110
+ const cost = ` cost=${costSummary(acct)}`;
447
111
  const finishReason = turn.finish_reason ?? "—";
448
112
  // Render only observed transport metadata; absence makes no claim
449
- // about endpoint-owned settings. {§operator-grammar}
450
- const tm = DigestRender.parseJson(turn.meta ?? "null", null);
451
- const attached = tm?.railsAttached;
113
+ // about endpoint-owned settings. {§provider-instantiation}
114
+ const tm = parseJson(turn.meta ?? "null", null);
115
+ const railsFlag = isRecord(tm) ? tm.railsAttached : undefined;
116
+ const attached = typeof railsFlag === "boolean" || typeof railsFlag === "string" ? railsFlag : undefined;
452
117
  const rails = attached === undefined || attached === false ? ""
453
118
  : ` rails=${attached === true ? "client" : attached}`;
454
119
  const model = turn.model ?? "—";
455
120
  // {§outside-text}: the weight the model was told, so a digest reader sees the discard at a glance.
456
121
  const outside = turn.outside === null ? "" : ` outside=${m.evidence.textWeight(turn.outside)} tok`;
457
122
  const errs = (m.logEntriesByTurn.get(turn.id) ?? [])
458
- .filter((le) => DigestRender.#countsAsError(le)).length;
123
+ .filter((le) => countsAsError(le)).length;
459
124
  const errBadge = errs > 0 ? ` ⚠ errs=${errs}` : "";
460
125
  const attempts = m.attemptsByTurn.get(turn.id) ?? [];
461
126
  const rejected = attempts.filter((attempt) => attempt.accepted === 0).length;
@@ -470,12 +135,12 @@ export default class DigestRender {
470
135
  ? ""
471
136
  : ` ⚠ ${attemptConditions.join(" ")}/${attempts.length}`;
472
137
  const packetBadge = packetFailure === null ? "" : " ⚠ packet=invalid";
473
- const stem = DigestRender.packetStems(m).get(turn.id);
138
+ const stem = packetStems(m).get(turn.id);
474
139
  const modelTurn = DigestRender.#modelTurnOrdinal(turn, m);
475
140
  const provenance = [modelTurn === null ? null : `model turn ${modelTurn}`, stem].filter((part) => part !== null).join(" · ");
476
141
  const lifecycle = `T${turn.sequence}${provenance === "" ? "" : ` (${provenance})`}: producer=${turn.producer} kind=${turn.kind} status=${turn.status}${turn.completed_at === null ? " state=open" : ""}`;
477
142
  const head = turn.kind === "inference"
478
- ? `${lifecycle} finish=${finishReason}${rails} model=${model} ${tokens}${cost}${DigestRender.#cacheBadge(turn, m)}${outside}${errBadge}${attemptBadge}${packetBadge}`
143
+ ? `${lifecycle} finish=${finishReason}${rails} model=${model} ${tokens}${cost}${cacheBadge(turn, m)}${outside}${errBadge}${attemptBadge}${packetBadge}`
479
144
  : `${lifecycle}${errBadge}${packetBadge}`;
480
145
  let summary = null;
481
146
  if (packetFailure !== null) {
@@ -486,7 +151,7 @@ export default class DigestRender {
486
151
  }
487
152
  else if (content.length > 0) {
488
153
  summary = content.includes("\n")
489
- ? ` ↳ emission:\n${DigestRender.#indentLines(content.trimEnd(), " ")}`
154
+ ? ` ↳ emission:\n${indentLines(content.trimEnd(), " ")}`
490
155
  : ` ↳ emission: ${content.trim()}`;
491
156
  }
492
157
  else if (assistant !== null) {
@@ -500,52 +165,36 @@ export default class DigestRender {
500
165
  }
501
166
  const reasoningLine = reasoning && reasoning.length > 0
502
167
  ? (reasoning.includes("\n")
503
- ? ` ↳ reasoning:\n${DigestRender.#indentLines(reasoning.trimEnd(), " ")}`
168
+ ? ` ↳ reasoning:\n${indentLines(reasoning.trimEnd(), " ")}`
504
169
  : ` ↳ reasoning: ${reasoning.trim()}`)
505
170
  : null;
506
- // {§provider-wire-emission} — an empty emission is read from what the wire carried, never guessed at.
507
- const wireLine = content.length === 0 && packet?.assistant != null ? DigestRender.wireLine(packet.assistantRaw) : null;
171
+ // {§provider-failure-normalization} — an empty emission is read from what the wire carried, never guessed at.
172
+ const wire = content.length === 0 && packet?.assistant != null ? wireLine(packet.assistantRaw) : null;
508
173
  const roles = packet?.messages().map(({ role }) => role).join(" → ");
509
174
  const requestLine = packet === null ? null
510
175
  : ` ↳ request: ${roles || "input sections not retained"} (${stem}.request.md)`;
511
176
  const opLines = DigestRender.#renderOpLines(m.logEntriesByTurn.get(turn.id) ?? [], m);
512
- return [head, ...(requestLine ? [requestLine] : []), ...(summary ? [summary] : []), ...(reasoningLine ? [reasoningLine] : []), ...(wireLine ? [wireLine] : []), ...opLines].join("\n");
513
- }
514
- // {§provider-wire-emission} — public so the line can be witnessed on its own.
515
- static wireLine(assistantRaw) {
516
- const wire = typeof assistantRaw === "object" && assistantRaw !== null ? assistantRaw.wire : undefined;
517
- if (typeof wire !== "object" || wire === null)
518
- return null;
519
- const { chunks, emptyChunks, fields, channels, toolCalls, unmappedChunks } = wire;
520
- const parts = [
521
- `${chunks ?? 0} chunks`,
522
- (emptyChunks ?? 0) > 0 ? `${emptyChunks} carried nothing` : null,
523
- ...Object.entries(fields ?? {}).map(([field, count]) => `${field}×${count}`),
524
- ...(toolCalls ?? []).map((call) => `tool call ${call.name ?? "?"}(${DigestRender.#summarize(call.arguments, 60)})`),
525
- ...Object.entries(channels ?? {}).map(([channel, text]) => `${channel}: ${DigestRender.#summarize(text, 60)}`),
526
- (unmappedChunks?.length ?? 0) > 0 ? `${unmappedChunks.length} unmapped frames retained` : null,
527
- ].filter((part) => part !== null);
528
- return ` ↳ wire: ${parts.join(", ")}`;
529
- }
530
- static #renderWorkerShape(worker, m) {
531
- // every worker has exactly one rollup row — digest_worker_rollups is FROM workers
532
- const roll = m.workerRollups.get(worker.id);
533
- const opMix = (m.opMixByWorker.get(worker.id) ?? []).map((o) => `${o.op}=${o.n}`).join(" ");
534
- const requests = m.requestsByWorker.get(worker.id) ?? [];
535
- const accounting = DigestRender.#accounting(requests);
177
+ return [head, ...(requestLine ? [requestLine] : []), ...(summary ? [summary] : []), ...(reasoningLine ? [reasoningLine] : []), ...(wire ? [wire] : []), ...opLines].join("\n");
178
+ }
179
+ static #renderSessionShape(session, m) {
180
+ // every session has exactly one rollup row — digest_session_rollups is FROM sessions
181
+ const roll = m.sessionRollups.get(session.id);
182
+ const opMix = (m.opMixBySession.get(session.id) ?? []).map((o) => `${o.op}=${o.n}`).join(" ");
183
+ const requests = m.requestsBySession.get(session.id) ?? [];
184
+ const acct = accounting(requests);
536
185
  const usageStr = requests.length === 0
537
186
  ? "no provider requests"
538
- : DigestRender.usageSummary(accounting);
187
+ : usageSummary(acct);
539
188
  const costStr = requests.length === 0
540
189
  ? "n/a"
541
- : DigestRender.costSummary(accounting);
190
+ : costSummary(acct);
542
191
  // {§digest-forensic-fidelity} (#461): settled exchanges that carry no usage at
543
192
  // all (errored/aborted) billed server-side invisibly; say so, never price-as-zero.
544
193
  const usageless = requests.filter((row) => row.state === "settled" && row.usage_total === null).length;
545
194
  const usagelessStr = usageless > 0
546
195
  ? ` (+${usageless} usage-less request${usageless === 1 ? "" : "s"} — server-side spend unrecorded)`
547
196
  : "";
548
- // {§digest-cost-kind} (#473): a dollar figure without its basis reads as billed
197
+ // {§digest-forensic-fidelity} (#473): a dollar figure without its basis reads as billed
549
198
  // truth; carry the kind so an estimate can never impersonate a charge.
550
199
  const kinds = new Set(requests
551
200
  .filter((row) => row.state === "settled" && row.cost_kind !== null)
@@ -557,7 +206,7 @@ export default class DigestRender {
557
206
  : kinds.has("charged")
558
207
  ? " (charged)"
559
208
  : "";
560
- // {§digest-wire-line} (#473): provider-level errors are absorbed by retries below
209
+ // {§digest-forensic-fidelity} (#473): provider-level errors are absorbed by retries below
561
210
  // the packet stream, so only an aggregate line makes a rate-limit storm visible.
562
211
  const wireErrors = requests.filter((row) => row.outcome === "error").length;
563
212
  const wireStr = requests.length === 0
@@ -570,82 +219,16 @@ export default class DigestRender {
570
219
  `Tokens: ${usageStr}`,
571
220
  `Cost: ${costStr}${kindStr}${usagelessStr}`,
572
221
  `Wire: ${wireStr}`,
573
- `Room: ${DigestRender.#renderRoom(worker, m)}`,
222
+ `Room: ${renderRoom(session, m)}`,
574
223
  `Op mix: ${opMix.length > 0 ? opMix : "(no ops)"}`,
575
- `EDITs: ${DigestRender.#renderEditCensus(DigestRender.#editCensus(m).byWorker.get(worker.id))}`,
576
- `Emissions: ${DigestRender.#renderEmissions(worker, m)}`,
577
- `Reasonings: ${DigestRender.#renderReasonings(worker, m)}`,
224
+ `EDITs: ${renderEditCensus(editCensus(m).bySession.get(session.id))}`,
225
+ `Emissions: ${renderEmissions(session, m)}`,
226
+ `Reasonings: ${renderReasonings(session, m)}`,
578
227
  ].join("\n");
579
228
  }
580
- // {§digest-room-line} — the room in provider tokens: the largest budget the model was shown, at its
581
- // request's own ratio, against the capacity; the wall's estimate against the provider's count; and the
582
- // requests the estimate put under the wall while the count was over it.
583
- static #renderRoom(worker, m) {
584
- const measured = DigestRender.#roomMeasures(worker, m);
585
- if (measured.length === 0)
586
- return "(no measured requests)";
587
- const room = Math.max(...measured.map(({ weight, budget, count, capacity }) => budget * count / weight / capacity));
588
- const errors = measured.map(({ estimate, count }) => (estimate - count) / count);
589
- const missed = measured.filter(({ estimate, count, wall }) => estimate <= wall && count > wall).length;
590
- const percent = (ratio) => `${ratio < 0 ? "-" : "+"}${Math.abs(100 * ratio).toFixed(1)}%`;
591
- return `budget up to ${Math.round(100 * room)}% of capacity · wall estimate ${percent(Math.min(...errors))} to ${percent(Math.max(...errors))} of the count`
592
- + (missed === 0 ? "" : ` ⚠ ${missed} over the wall the estimate admitted`);
593
- }
594
- // One measure per packet-bearing inference turn whose request records a known capacity and wall and a
595
- // provider count: the exact preflight measurement, else the reported input.
596
- static #roomMeasures(worker, m) {
597
- const measures = [];
598
- for (const loop of m.loopsByWorker.get(worker.id) ?? []) {
599
- for (const turn of m.turnsByLoop.get(loop.id) ?? []) {
600
- if (turn.kind !== "inference" || turn.has_packet !== 1)
601
- continue;
602
- const packet = m.evidence.packet(turn).packet;
603
- if (packet === null || packet.budget === null || packet.budget === 0 || packet.weight === 0)
604
- continue;
605
- const call = m.modelCalls.findLast((row) => row.turn_id === turn.id && row.kind === "emission" && row.capacity !== null);
606
- const capacity = DigestRender.parseJson(call?.capacity);
607
- if (capacity?.inputCapacity == null || capacity.inputWall == null)
608
- continue;
609
- const reported = (m.requestsByTurn.get(turn.id) ?? [])
610
- .findLast((row) => row.kind === "emission" && row.outcome === "response" && row.usage_input !== null)?.usage_input ?? null;
611
- const count = capacity.prompt?.kind === "exact" ? capacity.prompt.tokens ?? null : reported;
612
- if (count === null || count === 0)
613
- continue;
614
- measures.push({
615
- weight: packet.weight, budget: packet.budget, capacity: capacity.inputCapacity, wall: capacity.inputWall, count,
616
- estimate: Math.ceil(packet.weight * capacity.inputCapacity / packet.budget),
617
- });
618
- }
619
- }
620
- return measures;
621
- }
622
- // {§emission-row} — announced, retired by the model, and echoed back as headings in its own text.
623
- static #renderEmissions(worker, m) {
624
- const rows = m.emissionRows.filter((row) => row.worker_id === worker.id);
625
- const killed = rows.filter((row) => row.active === 0).length;
626
- const echoes = (m.loopsByWorker.get(worker.id) ?? [])
627
- .flatMap((loop) => m.turnsByLoop.get(loop.id) ?? [])
628
- .reduce((sum, turn) => sum + turn.packetEchoes, 0);
629
- return `${rows.length} announced · ${killed} killed · ${echoes} header echo${echoes === 1 ? "" : "es"}`;
630
- }
631
- // {§reasoning-row} — landed, retired by the model, and turns that reasoned.
632
- static #renderReasonings(worker, m) {
633
- const rows = m.reasoningRows.filter((row) => row.worker_id === worker.id);
634
- const killed = rows.filter((row) => row.active === 0).length;
635
- const reasonedTurns = (m.loopsByWorker.get(worker.id) ?? [])
636
- .flatMap((loop) => m.turnsByLoop.get(loop.id) ?? [])
637
- .filter((turn) => turn.has_reasoning === 1).length;
638
- if (reasonedTurns === 0 && rows.length === 0)
639
- return "(no reasoning)";
640
- if (rows.length === 0)
641
- return `0 of ${reasonedTurns} landed (unbudgeted or disabled)`;
642
- if (reasonedTurns > rows.length)
643
- return `${rows.length} of ${reasonedTurns} landed · ${killed} killed`;
644
- return `${rows.length} landed · ${killed} killed`;
645
- }
646
229
  static waterfall(m) {
647
230
  const lines = [];
648
- lines.push(`# plurnk-service digest`);
231
+ lines.push("# plurnk-service digest");
649
232
  lines.push("");
650
233
  lines.push(`DB: ${m.dbPath}`);
651
234
  lines.push(DigestRender.#storageLine(m.storage));
@@ -653,13 +236,13 @@ export default class DigestRender {
653
236
  const erroredCalls = m.inferenceCalls.filter((call) => call.state === "error").length;
654
237
  const pendingCalls = m.inferenceCalls.filter((call) => call.state === "pending").length;
655
238
  const bareCalls = m.modelCalls.filter((call) => call.kind === "bare").length;
656
- const pendingRequests = m.providerRequests.filter((request) => request.state === "pending").length;
239
+ const pendingRequests = m.providerRequests.filter((req) => req.state === "pending").length;
657
240
  const packetFailures = m.turns.filter((turn) => m.evidence.packet(turn).packetFailure !== null).length;
658
- lines.push(`Workspaces: ${m.workspaces.length} Workers: ${m.workers.length} Loops: ${m.loops.length} Turns: ${m.turns.length} Inference calls: ${m.inferenceCalls.length} (${bareCalls} BARE, ${erroredCalls} errored, ${pendingCalls} open) Emission attempts: ${m.turnAttempts.length} (${rejectedAttempts} rejected) Provider requests: ${m.providerRequests.length} (${pendingRequests} open) Log entries: ${m.logEntries.length}`);
241
+ lines.push(`Workspaces: ${m.workspaces.length} Sessions: ${m.sessions.length} Loops: ${m.loops.length} Turns: ${m.turns.length} Inference calls: ${m.inferenceCalls.length} (${bareCalls} BARE, ${erroredCalls} errored, ${pendingCalls} open) Emission attempts: ${m.turnAttempts.length} (${rejectedAttempts} rejected) Provider requests: ${m.providerRequests.length} (${pendingRequests} open) Log entries: ${m.logEntries.length}`);
659
242
  if (packetFailures > 0)
660
243
  lines.push(`Stored packet failures: ${packetFailures}`);
661
244
  lines.push(`Search: channels=${m.search.channel_entries} attached=${m.search.derivation_complete} (indexed=${m.search.indexed} excluded=${m.search.excluded} unsearchable=${m.search.unsearchable} failed=${m.search.failed}) unattached=${m.search.unfinished} artifacts=${m.search.derivation_artifacts_complete} complete/${m.search.derivation_artifacts_building} building`);
662
- const health = m.loops.map((l) => DigestRender.#loopHealth(l, m));
245
+ const health = m.loops.map((l) => loopHealth(l, m));
663
246
  const clean = health.filter((h) => h.verdict === "CLEAN").length;
664
247
  const degen = health.filter((h) => h.verdict === "DEGENERATE-WIN").length;
665
248
  const failed = health.filter((h) => h.verdict.startsWith("FAILED")).length;
@@ -670,28 +253,28 @@ export default class DigestRender {
670
253
  lines.push("");
671
254
  lines.push(`## Workspace #${workspace.id} — ${workspace.name}`);
672
255
  lines.push("");
673
- lines.push(DigestRender.#cacheLine(m.requestsByWorkspace.get(workspace.id) ?? [], m));
674
- const workspaceWorkers = m.workersByWorkspace.get(workspace.id) ?? [];
675
- for (const worker of workspaceWorkers) {
256
+ lines.push(cacheLine(m.requestsByWorkspace.get(workspace.id) ?? [], m));
257
+ const workspaceSessions = m.sessionsByWorkspace.get(workspace.id) ?? [];
258
+ for (const session of workspaceSessions) {
676
259
  lines.push("");
677
- lines.push(`### Worker #${worker.id} — ${worker.name}`);
678
- lines.push(`Owner: ${worker.owner}`);
260
+ lines.push(`### Session #${session.id} — ${session.name}`);
261
+ lines.push(`Owner: ${session.owner}`);
679
262
  lines.push("");
680
263
  lines.push("```");
681
- lines.push(DigestRender.#renderWorkerShape(worker, m));
264
+ lines.push(DigestRender.#renderSessionShape(session, m));
682
265
  lines.push("```");
683
- const workerLoops = m.loopsByWorker.get(worker.id) ?? [];
684
- for (const loop of workerLoops) {
685
- const terminal = DigestRender.#terminalResult(loop);
266
+ const sessionLoops = m.loopsBySession.get(session.id) ?? [];
267
+ for (const loop of sessionLoops) {
268
+ const terminal = terminalResult(loop);
686
269
  lines.push("");
687
- const h = DigestRender.#loopHealth(loop, m);
270
+ const h = loopHealth(loop, m);
688
271
  const badge = h.verdict === "CLEAN"
689
272
  ? " — CLEAN"
690
273
  : ` — ${h.verdict === "DEGENERATE-WIN" ? "⚠ DEGENERATE-WIN" : h.verdict} (${h.errors} errors, ${h.errorItems} error-items)`;
691
274
  lines.push(`#### Loop ${loop.sequence} (id=${loop.id}, status=${loop.status})${badge}`);
692
275
  lines.push("");
693
276
  if (loop.prompt.includes("\n")) {
694
- lines.push("Prompt:\n" + DigestRender.#indentLines(loop.prompt.trimEnd(), " "));
277
+ lines.push("Prompt:\n" + indentLines(loop.prompt.trimEnd(), " "));
695
278
  }
696
279
  else {
697
280
  lines.push(`Prompt: ${loop.prompt}`);
@@ -705,12 +288,12 @@ export default class DigestRender {
705
288
  lines.push("");
706
289
  lines.push("| Turn | Packet artifacts | Physical requests |", "| --- | --- | --- |");
707
290
  for (const turn of m.turnsByLoop.get(loop.id) ?? []) {
708
- const requests = (m.requestsByTurn.get(turn.id) ?? []).map((request) => DigestRender.#link("digest.md", `requests/${request.id}.md`, `${request.id}: ${request.kind} ${request.outcome ?? request.state}`));
709
- lines.push(`| ${turn.sequence} (${turn.kind}) | ${DigestRender.#turnLinks(m, turn, "digest.md").join(" · ") || "—"} | ${requests.join(" · ") || "—"} |`);
291
+ const requests = (m.requestsByTurn.get(turn.id) ?? []).map((req) => link("digest.md", `requests/${req.id}.md`, `${req.id}: ${req.kind} ${req.outcome ?? req.state}`));
292
+ lines.push(`| ${turn.sequence} (${turn.kind}) | ${turnLinks(m, turn, "digest.md").join(" · ") || "—"} | ${requests.join(" · ") || "—"} |`);
710
293
  }
711
294
  lines.push("");
712
295
  const turnLines = (m.turnsByLoop.get(loop.id) ?? []).map((t) => DigestRender.#renderTurnLine(t, m));
713
- lines.push(DigestRender.#literal(turnLines.join("\n"), ""));
296
+ lines.push(literal(turnLines.join("\n"), ""));
714
297
  }
715
298
  }
716
299
  }
@@ -718,20 +301,20 @@ export default class DigestRender {
718
301
  }
719
302
  static reasoning(m) {
720
303
  const lines = [];
721
- lines.push(`# plurnk-service reasoning`);
304
+ lines.push("# plurnk-service reasoning");
722
305
  lines.push("");
723
306
  lines.push("Turn chronology with every provider attempt. Rejected attempts remain explicit forensic evidence.");
724
307
  for (const t of m.turns) {
725
308
  const loop = m.loopsById.get(t.loop_id);
726
- const worker = loop ? m.workersById.get(loop.worker_id) : undefined;
309
+ const session = loop ? m.sessionsById.get(loop.session_id) : undefined;
727
310
  lines.push("");
728
- lines.push(`## Worker ${worker?.id ?? "?"} / Loop ${loop?.sequence ?? "?"} / Turn ${t.sequence} (id=${t.id}, producer=${t.producer}, kind=${t.kind})`);
311
+ lines.push(`## Session ${session?.id ?? "?"} / Loop ${loop?.sequence ?? "?"} / Turn ${t.sequence} (id=${t.id}, producer=${t.producer}, kind=${t.kind})`);
729
312
  if (t.kind !== "inference") {
730
313
  lines.push("");
731
314
  lines.push("(operation turn; no provider inference)");
732
315
  const reasoning = m.evidence.reasoning(t);
733
316
  if (reasoning !== null)
734
- lines.push("", DigestRender.#literal(reasoning));
317
+ lines.push("", literal(reasoning));
735
318
  continue;
736
319
  }
737
320
  const attempts = m.attemptsByTurn.get(t.id) ?? [];
@@ -744,15 +327,17 @@ export default class DigestRender {
744
327
  if (packetFailure !== null)
745
328
  lines.push("(stored provider packet is invalid; see its packet artifacts)");
746
329
  else if (typeof reasoning === "string" && reasoning.length > 0)
747
- lines.push(DigestRender.#literal(reasoning));
330
+ lines.push(literal(reasoning));
748
331
  else
749
332
  lines.push("(no admitted provider reasoning)");
750
333
  continue;
751
334
  }
752
335
  for (const attempt of attempts) {
753
336
  const retainedResponse = m.evidence.response(attempt.model_call_id);
754
- const response = DigestRender.parseJson(retainedResponse, {});
755
- const parseErrors = DigestRender.parseJson(attempt.parse_errors, []);
337
+ const response = parseJson(retainedResponse, {});
338
+ const assistant = isRecord(response) && isRecord(response.assistant) ? response.assistant : null;
339
+ const parsedErrors = parseJson(attempt.parse_errors, []);
340
+ const parseErrors = Array.isArray(parsedErrors) ? parsedErrors : [];
756
341
  lines.push("");
757
342
  const disposition = attempt.state === "error"
758
343
  ? "call error"
@@ -762,28 +347,30 @@ export default class DigestRender {
762
347
  ? "admitted"
763
348
  : "rejected";
764
349
  lines.push(`### Attempt ${attempt.sequence} - ${disposition}`);
765
- const attributions = DigestRender.parseJson(attempt.attributions, []);
350
+ const parsedAttributions = parseJson(attempt.attributions, []);
351
+ const attributions = Array.isArray(parsedAttributions) ? parsedAttributions : [];
766
352
  lines.push(`Attributions: ${attributions.length === 0 ? "(none)" : attributions.join(", ")}`);
767
353
  if (attempt.failure !== null) {
768
- lines.push(`Failure: ${JSON.stringify(DigestRender.parseJson(attempt.failure, attempt.failure))}`);
354
+ lines.push(`Failure: ${JSON.stringify(parseJson(attempt.failure, attempt.failure))}`);
769
355
  }
770
- for (const request of m.requestsByAttempt.get(attempt.id) ?? []) {
771
- lines.push(`Physical request ${request.sequence}: [${request.state === "settled" ? request.outcome : request.state} evidence](requests/${request.id}.md)`);
356
+ for (const req of m.requestsByAttempt.get(attempt.id) ?? []) {
357
+ lines.push(`Physical request ${req.sequence}: [${req.state === "settled" ? req.outcome : req.state} evidence](requests/${req.id}.md)`);
772
358
  }
773
359
  if (attempt.accepted !== 1) {
774
360
  for (const error of parseErrors) {
775
- if (typeof error.message === "string")
776
- lines.push(`- ${error.message}`);
361
+ const message = isRecord(error) ? error.message : undefined;
362
+ if (typeof message === "string")
363
+ lines.push(`- ${message}`);
777
364
  }
778
365
  lines.push("");
779
366
  }
780
- const reasoning = response.assistant?.reasoning ?? null;
367
+ const reasoning = assistant?.reasoning ?? null;
781
368
  if (typeof reasoning === "string" && reasoning.length > 0)
782
- lines.push(DigestRender.#literal(reasoning));
369
+ lines.push(literal(reasoning));
783
370
  else if (retainedResponse === null && attempt.state === "response")
784
371
  lines.push("(response evidence is not retained; readable reasoning availability is unknown)");
785
372
  else {
786
- const reasoningTokens = DigestRender.#accounting(m.requestsByAttempt.get(attempt.id) ?? [])?.usage?.outputTokenDetails?.reasoningTokens;
373
+ const reasoningTokens = accounting(m.requestsByAttempt.get(attempt.id) ?? [])?.usage?.outputTokenDetails?.reasoningTokens;
787
374
  lines.push(reasoningTokens !== undefined && reasoningTokens > 0
788
375
  ? `(provider reported ${reasoningTokens} reasoning tokens; no readable reasoning content returned)`
789
376
  : attempt.state === "error" ? "(no admitted reasoning; partial output, when available, is in physical-request evidence)"
@@ -793,442 +380,5 @@ export default class DigestRender {
793
380
  }
794
381
  return lines.join("\n");
795
382
  }
796
- // {§share-packet-names}: render the owned envelope, never infer roles from filenames or body text.
797
- static request(messages, preamble = []) {
798
- const blocks = messages.map(({ role, content }, index) => {
799
- return `### ${index + 1}. ${role}\n\n${DigestRender.#literal(content)}`;
800
- });
801
- return ["# Request", ...preamble, "## Input messages", messages.map(({ role }) => role).join(" → "),
802
- messages.length === 0 ? "No input message sections are retained; the original input cannot be reconstructed."
803
- : "Stored text-message envelope, not a transport capture. Native payloads and provider transformations are not shown.",
804
- ...blocks, ""].join("\n\n");
805
- }
806
- // {§digest-navigation}: navigation surrounds evidence; channel files stay verbatim.
807
- static #requestPreamble(m, turn) {
808
- const stems = DigestRender.packetStems(m);
809
- const stem = stems.get(turn.id);
810
- const from = `${stem}.request.md`;
811
- const loop = m.loopsById.get(turn.loop_id);
812
- const siblings = m.turns.filter((row) => row.has_packet === 1
813
- && m.loopsById.get(row.loop_id)?.worker_id === loop.worker_id).toSorted((a, b) => a.id - b.id);
814
- const index = siblings.findIndex(({ id }) => id === turn.id);
815
- const previous = siblings[index - 1];
816
- const next = siblings[index + 1];
817
- const navigation = [
818
- DigestRender.#link(from, "digest.md", "Digest"),
819
- ...(previous === undefined ? [] : [DigestRender.#link(from, `${stems.get(previous.id)}.request.md`, "Previous")]),
820
- ...(next === undefined ? [] : [DigestRender.#link(from, `${stems.get(next.id)}.request.md`, "Next")]),
821
- ...DigestRender.#turnLinks(m, turn, from).filter((link) => !link.startsWith("[Input]")),
822
- ];
823
- const lines = [posix.basename(stem), navigation.join(" · ")];
824
- const requests = m.requestsByTurn.get(turn.id) ?? [];
825
- if (requests.length > 0) {
826
- const table = ["| Request | Call | Admission | Outcome |", "| --- | --- | --- | --- |"];
827
- for (const request of requests) {
828
- table.push(`| ${DigestRender.#link(from, `requests/${request.id}.md`, String(request.id))} | ${request.kind} ${request.inference_call_id} | ${DigestRender.#admission(m, request)} | ${request.outcome ?? request.state} |`);
829
- }
830
- lines.push("## Attempts", table.join("\n"));
831
- }
832
- for (const attempt of m.attemptsByTurn.get(turn.id) ?? []) {
833
- if (attempt.accepted === 1)
834
- continue;
835
- const ordinal = String(attempt.sequence).padStart(3, "0");
836
- const prefix = `${stem}.attempt${ordinal}`;
837
- if (attempt.state !== "response") {
838
- lines.push(`### Emission attempt ${attempt.sequence}: ${attempt.state}`, DigestRender.#link(from, `${prefix}.${attempt.state}.json`, "Call evidence"));
839
- if (attempt.failure !== null)
840
- lines.push(DigestRender.#literal(attempt.failure, "json"));
841
- continue;
842
- }
843
- const admission = attempt.accepted === 0 ? "rejected" : "unadmitted";
844
- lines.push(`### Emission attempt ${attempt.sequence}: ${admission}`, m.evidence.response(attempt.model_call_id) === null ? "Response evidence is not retained."
845
- : DigestRender.#link(from, `${prefix}.${admission}.assistant.md`, "Exact output"), DigestRender.#literal(JSON.stringify(DigestRender.parseJson(attempt.parse_errors, []), null, 2), "json"));
846
- }
847
- return lines;
848
- }
849
- static #admission(m, request) {
850
- if (request.kind === "bare")
851
- return "not an emission";
852
- if (request.outcome !== "response")
853
- return "not admitted";
854
- const call = m.modelCalls.find(({ id }) => id === request.inference_call_id);
855
- return call?.accepted === 1 ? "admitted" : call?.accepted === 0 ? "rejected" : "not admitted";
856
- }
857
- static #physicalRequest(m, request, evidence) {
858
- const from = `requests/${request.id}.md`;
859
- const turn = m.turns.find(({ id }) => id === request.turn_id);
860
- const fields = typeof evidence === "object" && evidence !== null ? evidence : {};
861
- const capture = fields.request;
862
- const lines = [
863
- `# Physical request ${request.id}`,
864
- [DigestRender.#link(from, "digest.md", "Digest"),
865
- DigestRender.#link(from, `requests/${request.id}.json`, "Complete evidence")].join(" · "),
866
- ...(turn === undefined ? [] : [`Originating turn: ${DigestRender.#turnLinks(m, turn, from).join(" · ") || "no packet artifacts retained"}`]),
867
- DigestRender.#literal(JSON.stringify({
868
- provider: request.provider, model: request.model, kind: request.kind,
869
- inferenceCallId: request.inference_call_id, sequence: request.sequence,
870
- state: request.state, outcome: request.outcome, admission: DigestRender.#admission(m, request),
871
- startedAt: request.started_at, completedAt: request.completed_at,
872
- ...(request.payloads_retired_at == null ? {} : { payloadsRetiredAt: request.payloads_retired_at }),
873
- }, null, 2), "json"),
874
- "## Accounting",
875
- request.state === "pending" ? "Request is unsettled; accounting is not yet available."
876
- : DigestRender.#literal(JSON.stringify(DigestRender.#requestAccounting(request), null, 2), "json"),
877
- "## Dispatched request",
878
- typeof capture?.body !== "string" ? "Dispatched request body was not retained."
879
- : `${capture.method} ${capture.origin}\n\nExact serialized body; URL details and request headers are not captured.\n\n${DigestRender.#literal(capture.body, "json")}`,
880
- ];
881
- const call = m.modelCalls.find(({ id }) => id === request.inference_call_id);
882
- if (request.outcome === "response" && call?.accepted === 0) {
883
- lines.push("## Admission", "The provider returned a response, but Plurnk rejected its emission.", DigestRender.#literal(JSON.stringify(DigestRender.parseJson(call.parse_errors, []), null, 2), "json"));
884
- }
885
- if (fields.error !== undefined)
886
- lines.push("## Failure", DigestRender.#literal(JSON.stringify(fields.error, null, 2), "json"));
887
- if (evidence === null)
888
- lines.push("## Response", request.payloads_retired_at == null
889
- ? "No settled response evidence is retained."
890
- : `Payload retention applied at ${request.payloads_retired_at}; request identity and accounting remain.`);
891
- else {
892
- if (request.outcome === "error")
893
- lines.push("Partial output; this physical request failed. It is not an admitted response.");
894
- for (const [field, heading] of [["reasoning", "Reasoning"], ["content", "Content"]]) {
895
- lines.push(`## ${heading}`, typeof fields[field] === "string"
896
- ? DigestRender.#literal(fields[field]) : `No readable ${field} is retained in this request record.`);
897
- }
898
- }
899
- return `${lines.join("\n\n")}\n`;
900
- }
901
- // Per-turn forensic files. turnOps is the source authority; PacketWire
902
- // reproduces provider request slots, and assistantRaw preserves provider bytes.
903
- // {§share-packet-names}: the stem each turn's packet files carry is its log coordinate, worker, loop and
904
- // turn, as `log:///<loop>/<turn>/…` addresses it; a digest spanning workspaces nests one folder per
905
- // workspace. Shared with the turn lines so a reader never counts files by hand.
906
- static #stemCache = new WeakMap();
907
- static packetStems(m) {
908
- const cached = DigestRender.#stemCache.get(m);
909
- if (cached !== undefined)
910
- return cached;
911
- const nested = m.workspaces.length > 1;
912
- // {§share-packet-names} — a workspace is commonly named by a path (`~/ptl/x`), which cannot name a
913
- // file: the stem carries a slug of the name (#1001), the digest text keeps the name verbatim, and two
914
- // names that slug alike are told apart by the row's id.
915
- const slugged = new Map();
916
- const segment = (name, id) => {
917
- const slug = name.replace(/^~\//u, "").replace(/[^A-Za-z0-9_.-]+/gu, "-").replace(/^[^A-Za-z0-9_]+/u, "").replace(/-+$/u, "");
918
- const base = slug.length === 0 ? String(id) : slug;
919
- const owner = slugged.get(base);
920
- if (owner === undefined) {
921
- slugged.set(base, id);
922
- return base;
923
- }
924
- return owner === id ? base : `${base}-${id}`;
925
- };
926
- const stems = new Map();
927
- const taken = new Set();
928
- for (const turn of m.turns.filter((row) => row.has_packet === 1 || row.program !== null || row.has_reasoning === 1).toSorted((a, b) => a.id - b.id)) {
929
- const loop = m.loopsById.get(turn.loop_id);
930
- const worker = loop === undefined ? undefined : m.workersById.get(loop.worker_id);
931
- if (loop === undefined || worker === undefined)
932
- throw new TypeError(`turn ${turn.id} has no loop or worker in scope`);
933
- const workspace = m.workspaces.find(({ id }) => id === worker.workspace_id);
934
- const stem = `${nested ? `${segment(workspace?.name ?? String(worker.workspace_id), worker.workspace_id)}/` : ""}${segment(worker.name, worker.id)}-${loop.sequence}-${turn.sequence}`;
935
- if (taken.has(stem))
936
- throw new TypeError(`two turns share the packet name ${stem}`);
937
- taken.add(stem);
938
- stems.set(turn.id, stem);
939
- }
940
- DigestRender.#stemCache.set(m, stems);
941
- return stems;
942
- }
943
- // Among its loop's model turns, this one's ordinal: the model's own count, which skips the
944
- // harness turns a reader would otherwise have to subtract.
945
- static #modelTurnOrdinal(turn, m) {
946
- if (turn.producer !== "model")
947
- return null;
948
- const siblings = (m.turnsByLoop.get(turn.loop_id) ?? []).filter((t) => t.producer === "model");
949
- const index = siblings.findIndex((t) => t.id === turn.id);
950
- return index < 0 ? null : index + 1;
951
- }
952
- static packetFiles(m) {
953
- const written = [];
954
- const stems = DigestRender.packetStems(m);
955
- const write = (file, body) => {
956
- mkdirSync(dirname(join(m.digestDir, file)), { recursive: true });
957
- writeFileSync(join(m.digestDir, file), body);
958
- written.push(file);
959
- };
960
- m.turns
961
- .map((turn) => ({ turn, source: turn.program }))
962
- .filter(({ turn, source }) => turn.has_packet === 1 || source !== null || turn.has_reasoning === 1)
963
- .toSorted((a, b) => a.turn.id - b.turn.id)
964
- .forEach(({ turn, source }) => {
965
- const padded = stems.get(turn.id);
966
- const files = [];
967
- const { packet, packetFailure } = m.evidence.packet(turn);
968
- const preamble = turn.has_packet === 1 ? DigestRender.#requestPreamble(m, turn) : [];
969
- if (packetFailure !== null) {
970
- files.push([`${padded}.request.md`, ["# Request", ...preamble, "Stored packet is invalid; no input messages were reconstructed.",
971
- DigestRender.#literal(JSON.stringify(packetFailure.error, null, 2), "json"),
972
- DigestRender.#link(`${padded}.request.md`, `${padded}.packet.raw.txt`, "Stored packet")].join("\n\n")], [`${padded}.packet.raw.txt`, packetFailure.raw], [`${padded}.packet.invalid.json`, JSON.stringify({
973
- turnId: turn.id,
974
- error: packetFailure.error,
975
- }, null, 2)]);
976
- }
977
- if (packet !== null) {
978
- // {§packet-wire-envelope} — the exact text messages the request carried; a stored packet
979
- // whose log cannot be projected is evidence of its own, never a reason to stop the digest.
980
- try {
981
- const messages = packet.messages();
982
- if (messages.length > 0)
983
- files.push([`${padded}.system.md`, packet.slot("system")], [`${padded}.user.md`, packet.slot("user")], [`${padded}.wire.json`, JSON.stringify(messages, null, 2)]);
984
- files.push([`${padded}.request.md`, DigestRender.request(messages, [
985
- ...preamble,
986
- ...(packet.attachments.length === 0 ? [] : ["## Native attachments", "Stored descriptors; selection alone does not prove transport or provider acceptance.", DigestRender.#literal(JSON.stringify(packet.attachments, null, 2), "json")]),
987
- ])]);
988
- }
989
- catch (cause) {
990
- files.push([`${padded}.wire.invalid.json`, JSON.stringify({ turnId: turn.id, error: cause instanceof Error ? cause.message : String(cause) }, null, 2)]);
991
- files.push([`${padded}.request.md`, ["# Request", ...preamble,
992
- "Stored input could not be projected; no message order was reconstructed.",
993
- DigestRender.#link(`${padded}.request.md`, `${padded}.wire.invalid.json`, "Projection failure")].join("\n\n")]);
994
- }
995
- }
996
- if (source !== null) {
997
- files.push([`${padded}.assistant.md`, source]);
998
- }
999
- const reasoning = m.evidence.reasoning(turn);
1000
- if (reasoning !== null)
1001
- files.push([`${padded}.reasoning.md`, reasoning]);
1002
- if (packet?.assistant != null) {
1003
- if (source !== null && packet.assistant.content !== source) {
1004
- throw new TypeError(`turn ${turn.id} packet assistant differs from its turnOps source`);
1005
- }
1006
- files.push([`${padded}.assistantRaw.json`, JSON.stringify(packet.assistantRaw, null, 2)]);
1007
- }
1008
- else if (packet !== null) {
1009
- files.push([
1010
- `${padded}.response.md`,
1011
- `# ${padded} — request only\n\nNo provider response was admitted. Rejected attempt evidence, when present, is written separately.\n`,
1012
- ]);
1013
- }
1014
- for (const [file, body] of files)
1015
- write(file, body);
1016
- for (const attempt of m.attemptsByTurn.get(turn.id) ?? []) {
1017
- if (attempt.accepted === 1)
1018
- continue;
1019
- const attemptPadded = String(attempt.sequence).padStart(3, "0");
1020
- if (attempt.state !== "response") {
1021
- write(`${padded}.attempt${attemptPadded}.${attempt.state}.json`, JSON.stringify({
1022
- state: attempt.state,
1023
- failure: DigestRender.parseJson(attempt.failure),
1024
- attributions: DigestRender.parseJson(attempt.attributions, []),
1025
- openedAt: attempt.timestamp,
1026
- completedAt: attempt.completed_at,
1027
- }, null, 2));
1028
- continue;
1029
- }
1030
- const retainedResponse = m.evidence.response(attempt.model_call_id);
1031
- const admission = attempt.accepted === 0 ? "rejected" : "unadmitted";
1032
- const prefix = `${padded}.attempt${attemptPadded}.${admission}`;
1033
- write(`${prefix}.parse-errors.json`, JSON.stringify(DigestRender.parseJson(attempt.parse_errors, []), null, 2));
1034
- write(`${prefix}.attributions.json`, JSON.stringify(DigestRender.parseJson(attempt.attributions, []), null, 2));
1035
- if (retainedResponse === null) {
1036
- write(`${prefix}.response.md`, `# Emission attempt ${attempt.sequence}: ${admission}\n\nResponse evidence is not retained.\n`);
1037
- continue;
1038
- }
1039
- const response = DigestRender.parseJson(retainedResponse, {});
1040
- const attemptFiles = [
1041
- [
1042
- `${prefix}.assistant.md`,
1043
- typeof response.assistant?.content === "string" ? response.assistant.content : "",
1044
- ],
1045
- [`${prefix}.response.json`, JSON.stringify(response, null, 2)],
1046
- ];
1047
- for (const [file, body] of attemptFiles)
1048
- write(file, body);
1049
- }
1050
- });
1051
- for (const request of m.providerRequests) {
1052
- const evidence = DigestRender.parseJson(m.evidence.request(request.id));
1053
- write(`requests/${request.id}.md`, DigestRender.#physicalRequest(m, request, evidence));
1054
- write(`requests/${request.id}.json`, JSON.stringify({
1055
- id: request.id,
1056
- inferenceCallId: request.inference_call_id,
1057
- sequence: request.sequence,
1058
- provider: request.provider,
1059
- model: request.model,
1060
- state: request.state,
1061
- startedAt: request.started_at,
1062
- completedAt: request.completed_at,
1063
- accounting: request.state === "settled" ? DigestRender.#requestAccounting(request) : null,
1064
- evidence,
1065
- }, null, 2));
1066
- }
1067
- return written;
1068
- }
1069
- static *json(m) {
1070
- const fields = {
1071
- dbPath: m.dbPath,
1072
- storage: m.storage,
1073
- search: m.search,
1074
- workspaces: m.workspaces.map((s) => ({
1075
- id: s.id,
1076
- name: s.name,
1077
- accounting: DigestRender.#accounting(m.requestsByWorkspace.get(s.id) ?? []),
1078
- })),
1079
- workers: m.workers.map((r) => ({
1080
- id: r.id,
1081
- workspace_id: r.workspace_id,
1082
- name: r.name,
1083
- owner: r.owner,
1084
- accounting: DigestRender.#accounting(m.requestsByWorker.get(r.id) ?? []),
1085
- edit_census: DigestRender.#editCensus(m).byWorker.get(r.id) ?? null,
1086
- })),
1087
- loops: m.loops.map((l) => ({
1088
- id: l.id, worker_id: l.worker_id, sequence: l.sequence, status: l.status,
1089
- prompt: l.prompt,
1090
- terminated_by: l.terminated_by,
1091
- claimed_at: l.claimed_at,
1092
- terminated_at: l.terminated_at,
1093
- result: DigestRender.#terminalResult(l),
1094
- accounting: DigestRender.#accounting(m.requestsByLoop.get(l.id) ?? []),
1095
- })),
1096
- turns: projectRows(m.turns, (t) => {
1097
- const { packet, packetFailure } = m.evidence.packet(t);
1098
- return {
1099
- id: t.id, loop_id: t.loop_id, sequence: t.sequence,
1100
- // {§share-packet-names}: the stem of this turn's packet files, or null when it wrote none.
1101
- artifact: DigestRender.packetStems(m).get(t.id) ?? null,
1102
- producer: t.producer, kind: t.kind,
1103
- program: t.program,
1104
- status: t.status, completed_at: t.completed_at,
1105
- accounting: DigestRender.#accounting(m.requestsByTurn.get(t.id) ?? []),
1106
- finish_reason: t.finish_reason, model: t.model,
1107
- attributions: packet?.attributions ?? [],
1108
- attachments: packet === null ? null : packet.attachments ?? [],
1109
- packet_failure: packetFailure,
1110
- // Preserve the opaque provider and engine metadata for aggregate
1111
- // tooling. {§meta-passthrough}, {§operator-grammar}
1112
- meta: DigestRender.parseJson(t.meta ?? "null", null),
1113
- };
1114
- }),
1115
- inference_calls: m.inferenceCalls.map((call) => ({
1116
- id: call.id,
1117
- workspace_id: call.workspace_id,
1118
- turn_id: call.turn_id,
1119
- sequence: call.sequence,
1120
- kind: call.kind,
1121
- state: call.state,
1122
- attributions: DigestRender.parseJson(call.attributions, []),
1123
- request_model: call.request_model,
1124
- accounting: DigestRender.#accounting(m.requestsByInferenceCall.get(call.id) ?? []),
1125
- timestamp: call.timestamp,
1126
- completed_at: call.completed_at,
1127
- })),
1128
- model_calls: projectRows(m.modelCalls, (call) => ({
1129
- id: call.id,
1130
- turn_id: call.turn_id,
1131
- sequence: call.sequence,
1132
- kind: call.kind,
1133
- state: call.state,
1134
- response: DigestRender.parseJson(m.evidence.response(call.id)),
1135
- failure: DigestRender.parseJson(call.failure),
1136
- attributions: DigestRender.parseJson(call.attributions, []),
1137
- accounting: DigestRender.#accounting(m.requestsByInferenceCall.get(call.id) ?? []),
1138
- finish_reason: call.finish_reason,
1139
- model: call.model,
1140
- request_model: call.request_model,
1141
- response_model: call.response_model,
1142
- timestamp: call.timestamp,
1143
- completed_at: call.completed_at,
1144
- turn_attempt_id: call.turn_attempt_id,
1145
- accepted: call.accepted === null ? null : call.accepted === 1,
1146
- parse_errors: DigestRender.parseJson(call.parse_errors, []),
1147
- log_entry_id: call.log_entry_id,
1148
- })),
1149
- turn_attempts: projectRows(m.turnAttempts, (attempt) => ({
1150
- id: attempt.id,
1151
- turn_id: attempt.turn_id,
1152
- sequence: attempt.sequence,
1153
- state: attempt.state,
1154
- accepted: attempt.accepted === null ? null : attempt.accepted === 1,
1155
- response: DigestRender.parseJson(m.evidence.response(attempt.model_call_id)),
1156
- failure: DigestRender.parseJson(attempt.failure),
1157
- parse_errors: DigestRender.parseJson(attempt.parse_errors, []),
1158
- attributions: DigestRender.parseJson(attempt.attributions, []),
1159
- accounting: DigestRender.#accounting(m.requestsByAttempt.get(attempt.id) ?? []),
1160
- finish_reason: attempt.finish_reason,
1161
- model: attempt.model,
1162
- timestamp: attempt.timestamp,
1163
- completed_at: attempt.completed_at,
1164
- })),
1165
- provider_requests: m.providerRequests.map((request) => ({
1166
- id: request.id,
1167
- evidence: `requests/${request.id}.json`,
1168
- inference_call_id: request.inference_call_id,
1169
- turn_attempt_id: request.turn_attempt_id,
1170
- kind: request.kind,
1171
- sequence: request.sequence,
1172
- state: request.state,
1173
- accounting: request.state === "settled"
1174
- ? DigestRender.#requestAccounting(request)
1175
- : null,
1176
- ...DigestRender.cacheLedger(m).get(request.id),
1177
- started_at: request.started_at,
1178
- completed_at: request.completed_at,
1179
- ...(request.payloads_retired_at == null ? {} : { payloadsRetiredAt: request.payloads_retired_at }),
1180
- })),
1181
- log_entries: m.logEntries.map((le) => ({
1182
- id: le.id, worker_id: le.worker_id, loop_id: le.loop_id,
1183
- turn_id: le.turn_id, sequence: le.sequence, origin: le.origin,
1184
- source: le.source, model_call_id: le.model_call_id,
1185
- ...(le.inherited_history === undefined ? {} : { inherited_history: le.inherited_history === 1 }),
1186
- ...(le.ambient_event_id === undefined ? {} : { ambient_event_id: le.ambient_event_id }),
1187
- attrs: DigestRender.parseJson(le.attrs, {}),
1188
- op: le.op, target: DigestRender.#renderTarget(le),
1189
- status_rx: le.status_rx, state: le.state, outcome: le.outcome,
1190
- ...(le.proposalDecision === undefined ? {} : { proposalDecision: le.proposalDecision }),
1191
- initial_folded: DigestRender.parseJson(le.initial_folded, []),
1192
- projection: {
1193
- active: le.projection_active === 1,
1194
- folded: DigestRender.parseJson(le.projection_folded, []),
1195
- },
1196
- ...(DigestRender.#renderStream(le) === null
1197
- ? {}
1198
- : { stream: DigestRender.#renderStream(le) }),
1199
- ...(DigestRender.#environmentOf(le, m) === undefined ? {} : { env: DigestRender.#environmentOf(le, m) }),
1200
- ...(le.status_rx >= 400 ? { problem: DigestRender.#rowProblem(le) } : {}),
1201
- ...(DigestRender.#editCensus(m).formById.has(le.id)
1202
- ? { edit_form: DigestRender.#editCensus(m).formById.get(le.id), edit_revisit: DigestRender.#editCensus(m).revisitById.has(le.id) }
1203
- : {}),
1204
- })),
1205
- log_curation_effects: m.curationEffects.map(({ active_before, active_after, folded_before, folded_after, ...effect }) => ({
1206
- ...effect,
1207
- active_before: active_before === 1,
1208
- active_after: active_after === 1,
1209
- folded_before: DigestRender.parseJson(folded_before, []),
1210
- folded_after: DigestRender.parseJson(folded_after, []),
1211
- })),
1212
- };
1213
- yield "{";
1214
- let fieldSeparator = "";
1215
- for (const [name, value] of Object.entries(fields)) {
1216
- yield `${fieldSeparator}\n ${JSON.stringify(name)}: `;
1217
- if (typeof value === "object" && value !== null && Symbol.iterator in value) {
1218
- yield "[";
1219
- let itemSeparator = "";
1220
- for (const row of value) {
1221
- yield `${itemSeparator}\n${JSON.stringify(row, null, 2).replace(/^/gm, " ")}`;
1222
- itemSeparator = ",";
1223
- }
1224
- yield itemSeparator === "" ? "]" : "\n ]";
1225
- }
1226
- else {
1227
- yield JSON.stringify(value, null, 2);
1228
- }
1229
- fieldSeparator = ",";
1230
- }
1231
- yield "\n}\n";
1232
- }
1233
383
  }
1234
384
  //# sourceMappingURL=DigestRender.js.map