@tangle-network/traces 0.8.27 → 0.9.0

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,6 +1,214 @@
1
+ // src/attributes.ts
2
+ var ATTR = {
3
+ /** = the trace/session id. Wire contract (dedup linkage). */
4
+ SESSION_ID: "tangle.sessionId",
5
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
6
+ * that also streamed live. Wire contract. */
7
+ INGEST_SOURCE: "tangle.ingest_source",
8
+ HARNESS: "tangle.harness",
9
+ CWD: "tangle.cwd",
10
+ /** THE per-session grouping key the spine derives subjects from
11
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
12
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
13
+ SUBJECT_KEY: "tangle.subject.key",
14
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
15
+ GIT_REPOSITORY: "git.repository",
16
+ /** Current branch at conversion time. */
17
+ GIT_BRANCH_NAME: "git.branch",
18
+ /** HEAD short sha at conversion time. */
19
+ GIT_COMMIT: "git.commit",
20
+ /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
21
+ REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
22
+ GIT_BRANCH: "tangle.git_branch",
23
+ HOST: "tangle.host",
24
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
25
+ * which collided with `tangle.ingest_source`.) */
26
+ SESSION_FILE: "tangle.session_file",
27
+ UPLOADED_AT: "tangle.uploaded_at",
28
+ UPLOADER: "tangle.uploader",
29
+ REDACTION_VERSION: "redaction.version",
30
+ REDACTION_COUNT: "redaction.count",
31
+ /** Upload contract: bounded root summary plus location-only receipt child spans.
32
+ * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
33
+ SESSION_INTEGRITY: "traces.session.integrity",
34
+ CORRUPTION_COUNT: "traces.session.corruption_count",
35
+ CORRUPTION_DIGEST: "traces.session.corruption_digest",
36
+ CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
37
+ CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
38
+ CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
39
+ CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
40
+ CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
41
+ CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
42
+ CORRUPTION_SHA256: "traces.session.corruption.sha256",
43
+ RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
44
+ };
45
+ var INGEST_SOURCE_CLI = "cli";
46
+ var DEFAULT_HARNESS = "claude-code";
47
+ var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
48
+ function sessionIdFromAttributes(attributes) {
49
+ return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
50
+ (value) => typeof value === "string" && value.length > 0
51
+ );
52
+ }
53
+ function indexSessionIdsByTrace(spans) {
54
+ const candidatesByTrace = /* @__PURE__ */ new Map();
55
+ for (const span2 of spans) {
56
+ const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
57
+ for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
58
+ const value = span2.attributes[key];
59
+ if (typeof value === "string" && value.length > 0) candidates.add(value);
60
+ }
61
+ if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
62
+ }
63
+ const sessionByTrace = /* @__PURE__ */ new Map();
64
+ const conflicts = [];
65
+ for (const [traceId, candidates] of candidatesByTrace) {
66
+ const sessionIds2 = [...candidates].sort();
67
+ if (sessionIds2.length === 1) {
68
+ sessionByTrace.set(traceId, sessionIds2[0]);
69
+ } else {
70
+ conflicts.push({ traceId, sessionIds: sessionIds2 });
71
+ }
72
+ }
73
+ conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
74
+ return { sessionByTrace, conflicts };
75
+ }
76
+ function stampSessionIdentity(spans, fallbackSessionId) {
77
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
78
+ const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
79
+ const traceIds = new Set(spans.map((span2) => span2.trace_id));
80
+ const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
81
+ for (const span2 of spans) {
82
+ if (conflictingTraceIds.has(span2.trace_id)) continue;
83
+ const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
84
+ if (!sessionIdFromAttributes(span2.attributes)) {
85
+ span2.attributes[ATTR.SESSION_ID] = sessionId;
86
+ }
87
+ }
88
+ }
89
+
1
90
  // src/adoption.ts
2
91
  import { readFile } from "fs/promises";
3
92
  import { join } from "path";
93
+
94
+ // src/adapters/tool-io.ts
95
+ import { createHash } from "crypto";
96
+ var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
97
+ var TOOL_IO_VALUE_KEYS = ["input.value", "output.value"];
98
+ var TOOL_IO_SIDES = ["input", "output"];
99
+ var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
100
+ var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
101
+ var TRUNCATION_MARKER = /\n\[truncated\]$/;
102
+ function canonicalJson(value) {
103
+ const sort = (item) => {
104
+ if (Array.isArray(item)) return item.map(sort);
105
+ if (!item || typeof item !== "object") return item;
106
+ return Object.fromEntries(
107
+ Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
108
+ );
109
+ };
110
+ const json2 = JSON.stringify(sort(value));
111
+ if (json2 === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
112
+ return json2;
113
+ }
114
+ function serialize(value) {
115
+ if (value === void 0) return void 0;
116
+ if (typeof value === "string") {
117
+ const trimmed = value.trim();
118
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
119
+ try {
120
+ return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
121
+ } catch {
122
+ }
123
+ }
124
+ return { value, mimeType: "text/plain" };
125
+ }
126
+ return { value: canonicalJson(value), mimeType: "application/json" };
127
+ }
128
+ function utf8Prefix(value, maxBytes) {
129
+ let bytes = 0;
130
+ let end = 0;
131
+ for (const char of value) {
132
+ const charBytes = Buffer.byteLength(char);
133
+ if (bytes + charBytes > maxBytes) break;
134
+ bytes += charBytes;
135
+ end += char.length;
136
+ }
137
+ return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
138
+ }
139
+ function truncatedValue(value, bytes) {
140
+ const longestMarker = `
141
+ [truncated ${bytes} bytes]`;
142
+ const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
143
+ const omittedBytes = bytes - Buffer.byteLength(prefix);
144
+ return `${prefix}
145
+ [truncated ${omittedBytes} bytes]`;
146
+ }
147
+ function valueAttributes(side, raw) {
148
+ const serialized = serialize(raw);
149
+ if (!serialized) return {};
150
+ const bytes = Buffer.byteLength(serialized.value);
151
+ const sha256 = createHash("sha256").update(serialized.value).digest("hex");
152
+ const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
153
+ const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
154
+ return {
155
+ [`${side}.value`]: value,
156
+ [`${side}.mime_type`]: serialized.mimeType,
157
+ [`traces.${side}.bytes`]: bytes,
158
+ [`traces.${side}.sha256`]: sha256,
159
+ [`traces.${side}.truncated`]: truncated
160
+ };
161
+ }
162
+ function toolIoAttributes(io) {
163
+ return {
164
+ ...valueAttributes("input", io.input),
165
+ ...valueAttributes("output", io.output),
166
+ ...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
167
+ };
168
+ }
169
+ function toolArgumentsFromAttributes(attributes) {
170
+ const declared = attributes["tool.args_captured"];
171
+ const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
172
+ const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
173
+ return {
174
+ args: argsCaptured ? attributes["input.value"] : void 0,
175
+ argsCaptured
176
+ };
177
+ }
178
+ function normalizeToolIoAttributes(attributes) {
179
+ for (const side of TOOL_IO_SIDES) {
180
+ const valueKey = `${side}.value`;
181
+ const mimeKey = `${side}.mime_type`;
182
+ const bytesKey = `traces.${side}.bytes`;
183
+ const digestKey = `traces.${side}.sha256`;
184
+ const truncatedKey = `traces.${side}.truncated`;
185
+ const rawValue = attributes[valueKey];
186
+ if (typeof rawValue !== "string") {
187
+ delete attributes[mimeKey];
188
+ delete attributes[bytesKey];
189
+ delete attributes[digestKey];
190
+ delete attributes[truncatedKey];
191
+ continue;
192
+ }
193
+ let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
194
+ let truncated = TRUNCATION_MARKER.test(value);
195
+ const unboundedBytes = Buffer.byteLength(value);
196
+ if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
197
+ value = truncatedValue(value, unboundedBytes);
198
+ truncated = true;
199
+ }
200
+ attributes[valueKey] = value;
201
+ attributes[bytesKey] = Buffer.byteLength(value);
202
+ attributes[digestKey] = createHash("sha256").update(value).digest("hex");
203
+ attributes[truncatedKey] = truncated;
204
+ }
205
+ }
206
+ function recordToolOutput(toolSpan, output) {
207
+ if (!toolSpan) return;
208
+ Object.assign(toolSpan.attributes, valueAttributes("output", output));
209
+ }
210
+
211
+ // src/adoption.ts
4
212
  function toolName(s) {
5
213
  const n = s.attributes["tool.name"];
6
214
  if (typeof n === "string") return n;
@@ -8,10 +216,14 @@ function toolName(s) {
8
216
  return null;
9
217
  }
10
218
  function parseInput(s) {
11
- const c = s.attributes.content;
12
- if (typeof c !== "string") return {};
219
+ const captured = toolArgumentsFromAttributes(s.attributes);
220
+ if (!captured.argsCaptured) return {};
221
+ if (captured.args && typeof captured.args === "object") {
222
+ return captured.args;
223
+ }
224
+ if (typeof captured.args !== "string") return {};
13
225
  try {
14
- const v = JSON.parse(c);
226
+ const v = JSON.parse(captured.args);
15
227
  return v && typeof v === "object" ? v : {};
16
228
  } catch {
17
229
  return {};
@@ -28,6 +240,28 @@ function subagentTypeOf(input) {
28
240
  function isSpawnAgentTool(name) {
29
241
  return name === "spawn_agent" || name.endsWith("__spawn_agent");
30
242
  }
243
+ function skillTelemetryCapability(service) {
244
+ if (service === "claude-code") return "supported";
245
+ if (service === "codex") return "unsupported";
246
+ return "unknown";
247
+ }
248
+ var MATERIALIZED_SKILL_RE = /<skills_instructions>|(?:^|\n)#{1,3}\s+(?:available\s+)?skills\b|base directory for this skill:/i;
249
+ function hasMaterializedSkillEvidence(s) {
250
+ if (s.name !== "message.developer" && s.attributes["tangle.actor"] !== "injected") return false;
251
+ const content = s.attributes.content;
252
+ return typeof content === "string" && MATERIALIZED_SKILL_RE.test(content);
253
+ }
254
+ function hasSkillFileReference(s) {
255
+ if (s.attributes["openinference.span.kind"] !== "TOOL") return false;
256
+ const input = s.attributes["input.value"];
257
+ return typeof input === "string" && input.includes("SKILL.md");
258
+ }
259
+ function telemetryStatus(measured, unsupported, unknown) {
260
+ if (measured > 0 && unsupported === 0 && unknown === 0) return "measured";
261
+ if (measured > 0) return "partial";
262
+ if (unsupported > 0 && unknown === 0) return "unsupported";
263
+ return "unknown";
264
+ }
31
265
  function countSkillRunsJsonl(raw) {
32
266
  const out = {};
33
267
  for (const line of raw.split("\n")) {
@@ -71,29 +305,47 @@ async function readLoopRuns(cwds) {
71
305
  return { counts, filesRead };
72
306
  }
73
307
  async function analyzeAdoption(spans, opts = {}) {
308
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
309
+ const identifiedSessions = new Set(sessionByTrace.values());
310
+ const unassignedTraceIds = new Set(
311
+ spans.map((span2) => span2.trace_id).filter((traceId) => !sessionByTrace.has(traceId))
312
+ );
313
+ const executionGroup = (span2) => sessionByTrace.get(span2.trace_id) ?? `trace:${span2.trace_id}`;
74
314
  const sessions = /* @__PURE__ */ new Set();
75
315
  const sessionsWithSkill = /* @__PURE__ */ new Set();
76
316
  const sessionsWithSubagent = /* @__PURE__ */ new Set();
317
+ const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
318
+ const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
77
319
  const skillInvocations = {};
78
320
  const subagentSpawns = {};
79
321
  const canonicalSubagentSessions = /* @__PURE__ */ new Set();
80
322
  const fallbackSubagents = /* @__PURE__ */ new Map();
323
+ const sessionCapabilities = /* @__PURE__ */ new Map();
81
324
  for (const s of spans) {
82
- sessions.add(s.trace_id);
325
+ const group = executionGroup(s);
326
+ sessions.add(group);
327
+ const observedCapability = skillTelemetryCapability(s.attributes["service.name"]);
328
+ const currentCapability = sessionCapabilities.get(group) ?? "unknown";
329
+ if (observedCapability === "supported" || currentCapability === "unknown") {
330
+ sessionCapabilities.set(group, observedCapability);
331
+ }
332
+ if (hasMaterializedSkillEvidence(s)) sessionsWithMaterializedSkills.add(group);
333
+ if (hasSkillFileReference(s)) sessionsWithSkillFileReference.add(group);
83
334
  const tn = toolName(s);
84
335
  if (tn === "Skill") {
336
+ sessionCapabilities.set(group, "supported");
85
337
  const name = skillNameOf(parseInput(s));
86
338
  skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
87
- sessionsWithSkill.add(s.trace_id);
339
+ sessionsWithSkill.add(group);
88
340
  } else if (tn === "Task" || tn === "Agent") {
89
341
  const type = subagentTypeOf(parseInput(s));
90
342
  subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
91
- sessionsWithSubagent.add(s.trace_id);
92
- canonicalSubagentSessions.add(s.trace_id);
343
+ sessionsWithSubagent.add(group);
344
+ canonicalSubagentSessions.add(group);
93
345
  } else if (tn && isSpawnAgentTool(tn)) {
94
- const types = fallbackSubagents.get(s.trace_id) ?? [];
346
+ const types = fallbackSubagents.get(group) ?? [];
95
347
  types.push(subagentTypeOf(parseInput(s)));
96
- fallbackSubagents.set(s.trace_id, types);
348
+ fallbackSubagents.set(group, types);
97
349
  }
98
350
  }
99
351
  for (const [traceId, types] of fallbackSubagents) {
@@ -107,10 +359,30 @@ async function analyzeAdoption(spans, opts = {}) {
107
359
  const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
108
360
  const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
109
361
  const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
362
+ let skillTelemetrySessions = 0;
363
+ let skillTelemetryUnsupportedSessions = 0;
364
+ let skillTelemetryUnknownSessions = 0;
365
+ for (const traceId of sessions) {
366
+ const capability = sessionCapabilities.get(traceId) ?? "unknown";
367
+ if (capability === "supported") skillTelemetrySessions += 1;
368
+ else if (capability === "unsupported") skillTelemetryUnsupportedSessions += 1;
369
+ else skillTelemetryUnknownSessions += 1;
370
+ }
110
371
  return {
111
- sessionCount: sessions.size,
372
+ executionGroupCount: sessions.size,
373
+ identifiedSessionCount: identifiedSessions.size,
374
+ unassignedTraceCount: unassignedTraceIds.size,
375
+ sessionIdentityConflicts: conflicts,
112
376
  sessionsWithSkill: sessionsWithSkill.size,
113
- skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
377
+ skillPenetration: skillTelemetrySessions === 0 ? null : sessionsWithSkill.size / skillTelemetrySessions,
378
+ skillTelemetryStatus: telemetryStatus(
379
+ skillTelemetrySessions,
380
+ skillTelemetryUnsupportedSessions,
381
+ skillTelemetryUnknownSessions
382
+ ),
383
+ skillTelemetrySessions,
384
+ sessionsWithMaterializedSkills: sessionsWithMaterializedSkills.size,
385
+ sessionsWithSkillFileReference: sessionsWithSkillFileReference.size,
114
386
  skillInvocations,
115
387
  totalSkillInvocations,
116
388
  subagentSpawns,
@@ -123,16 +395,27 @@ async function analyzeAdoption(spans, opts = {}) {
123
395
  }
124
396
 
125
397
  // src/otlp.ts
398
+ import {
399
+ applyLlmSpanOtlpAttributes,
400
+ OPENINFERENCE_SPAN_KIND,
401
+ TOOL_NAME
402
+ } from "@tangle-network/agent-eval/trace-attributes";
126
403
  function span(input) {
127
404
  const attributes = {
128
- "openinference.span.kind": input.kind
405
+ [OPENINFERENCE_SPAN_KIND]: input.kind
129
406
  };
130
407
  if (input.service != null) attributes["service.name"] = input.service;
131
408
  if (input.agent != null) attributes["agent.name"] = input.agent;
132
- if (input.model != null) attributes["llm.model_name"] = input.model;
133
- if (input.tool != null) attributes["tool.name"] = input.tool;
134
- if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
135
- if (input.outputTokens != null) attributes["llm.output_tokens"] = input.outputTokens;
409
+ if (input.tool != null) attributes[TOOL_NAME] = input.tool;
410
+ applyLlmSpanOtlpAttributes(attributes, {
411
+ model: input.model ?? void 0,
412
+ inputTokens: input.inputTokens ?? void 0,
413
+ outputTokens: input.outputTokens ?? void 0,
414
+ reasoningTokens: input.reasoningTokens ?? void 0,
415
+ cachedTokens: input.cachedInputTokens ?? void 0,
416
+ cacheWriteTokens: input.cacheWriteInputTokens ?? void 0,
417
+ costUsd: input.costUsd ?? void 0
418
+ });
136
419
  if (input.step != null) attributes.step = input.step;
137
420
  if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
138
421
  if (input.extra) Object.assign(attributes, input.extra);
@@ -162,7 +445,7 @@ function toOpenInferenceSpan(s) {
162
445
  span_id: s.span_id,
163
446
  parent_span_id: s.parent_span_id ?? "",
164
447
  name: s.name,
165
- kind: typeof a["openinference.span.kind"] === "string" ? a["openinference.span.kind"] : "CHAIN",
448
+ kind: typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "CHAIN",
166
449
  start_time: s.start_time,
167
450
  end_time: s.end_time,
168
451
  status: { code: s.status.code, message: s.status.message ?? "" },
@@ -185,20 +468,56 @@ async function writeOtlpFile(spans, outPath) {
185
468
  return path;
186
469
  }
187
470
 
471
+ // src/execution.ts
472
+ import {
473
+ summarizeExecution
474
+ } from "@tangle-network/agent-eval/contract";
475
+ import {
476
+ otlpRowsToRunRecords,
477
+ otlpToRunRecords
478
+ } from "@tangle-network/agent-eval/traces";
479
+ function runOptions(opts) {
480
+ return {
481
+ experimentId: opts.experimentId ?? "traces-observed",
482
+ candidateId: opts.candidateId ?? "observed",
483
+ fallbackModel: "unknown@otlp"
484
+ };
485
+ }
486
+ function* openInferenceRows(spans) {
487
+ for (const item of spans) yield toOpenInferenceSpan(item);
488
+ }
489
+ function summarizeOtlpExecution(otlpJsonl, opts = {}) {
490
+ const runs = otlpToRunRecords(otlpJsonl, runOptions(opts));
491
+ return summarizeExecution({ runs });
492
+ }
493
+ function summarizeSpanExecution(spans, opts = {}) {
494
+ if (spans.length === 0) throw new Error("summarizeSpanExecution: no spans to summarize");
495
+ return summarizeExecution({
496
+ runs: otlpRowsToRunRecords(openInferenceRows(spans), runOptions(opts))
497
+ });
498
+ }
499
+
188
500
  // src/analyze.ts
189
501
  import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
190
502
  import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
191
- var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
503
+ var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
192
504
  async function analyzeSpans(spans, opts = {}) {
193
505
  if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
194
506
  const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
195
507
  const runId = opts.runId ?? `traces-${Date.now()}`;
196
- const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
508
+ const execution = summarizeSpanExecution(spans, {
509
+ experimentId: runId
510
+ });
511
+ const detStore = new OtlpFileTraceStore({
512
+ path: otlpPath,
513
+ maxFileBytes: GENERATED_TRACE_FILE_CEILING,
514
+ perCallByteCeiling: GENERATED_TRACE_FILE_CEILING
515
+ });
197
516
  await detStore.ensureIndexed();
198
517
  const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
199
518
  const result = await detRegistry.run(runId, { traceStore: detStore });
200
519
  if (opts.ai) {
201
- const agStore = new OtlpFileTraceStore({ path: otlpPath });
520
+ const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
202
521
  await agStore.ensureIndexed();
203
522
  const agRegistry = buildDefaultAnalystRegistry({
204
523
  ai: opts.ai,
@@ -213,42 +532,32 @@ async function analyzeSpans(spans, opts = {}) {
213
532
  result.per_analyst.push(...agResult.per_analyst);
214
533
  result.total_cost_usd += agResult.total_cost_usd;
215
534
  }
216
- return { otlpPath, result };
535
+ return { otlpPath, execution, result };
217
536
  }
218
537
 
219
- // src/attributes.ts
220
- var ATTR = {
221
- /** = the trace/session id. Wire contract (dedup linkage). */
222
- SESSION_ID: "tangle.sessionId",
223
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
224
- * that also streamed live. Wire contract. */
225
- INGEST_SOURCE: "tangle.ingest_source",
226
- HARNESS: "tangle.harness",
227
- CWD: "tangle.cwd",
228
- /** THE per-session grouping key the spine derives subjects from
229
- * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
230
- * remote (host/owner/repo) when readable, else the cwd path basename. */
231
- SUBJECT_KEY: "tangle.subject.key",
232
- /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
233
- GIT_REPOSITORY: "git.repository",
234
- /** Current branch at conversion time. */
235
- GIT_BRANCH_NAME: "git.branch",
236
- /** HEAD short sha at conversion time. */
237
- GIT_COMMIT: "git.commit",
238
- /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
239
- REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
240
- GIT_BRANCH: "tangle.git_branch",
241
- HOST: "tangle.host",
242
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
243
- * which collided with `tangle.ingest_source`.) */
244
- SESSION_FILE: "tangle.session_file",
245
- UPLOADED_AT: "tangle.uploaded_at",
246
- UPLOADER: "tangle.uploader",
247
- REDACTION_VERSION: "redaction.version",
248
- REDACTION_COUNT: "redaction.count"
249
- };
250
- var INGEST_SOURCE_CLI = "cli";
251
- var DEFAULT_HARNESS = "claude-code";
538
+ // src/adapters/conversation.ts
539
+ var CONTENT_CAP = 8e3;
540
+ function capText(raw) {
541
+ const t = raw.trim();
542
+ if (t.length <= CONTENT_CAP) return t;
543
+ return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
544
+ }
545
+ var ACTOR_ATTR = "tangle.actor";
546
+ function userPromptSpan(o) {
547
+ return span({
548
+ traceId: o.traceId,
549
+ spanId: o.spanId,
550
+ parentSpanId: o.parentSpanId,
551
+ name: "user.prompt",
552
+ kind: "CHAIN",
553
+ startTime: o.startTime,
554
+ service: o.service ?? null,
555
+ agent: o.agent ?? null,
556
+ step: o.step,
557
+ content: o.content,
558
+ extra: { [ACTOR_ATTR]: o.actor ?? "human" }
559
+ });
560
+ }
252
561
 
253
562
  // src/time.ts
254
563
  function parseIsoToEpochMs(ts) {
@@ -280,73 +589,238 @@ var KIND = {
280
589
  };
281
590
  async function toRuntimeStore(spans) {
282
591
  const store = new InMemoryTraceStore();
283
- const runIds = /* @__PURE__ */ new Set();
592
+ const runs = /* @__PURE__ */ new Map();
593
+ for (const item of spans) {
594
+ const startedAt = parseIsoToEpochMs(item.start_time);
595
+ const endedAt = parseIsoToEpochMs(item.end_time);
596
+ const current = runs.get(item.trace_id);
597
+ runs.set(item.trace_id, current ? {
598
+ startedAt: Math.min(current.startedAt, startedAt),
599
+ endedAt: Math.max(current.endedAt, endedAt),
600
+ failed: current.failed || item.status.code === "ERROR"
601
+ } : { startedAt, endedAt, failed: item.status.code === "ERROR" });
602
+ }
603
+ for (const [runId, run] of runs) {
604
+ await store.appendRun({
605
+ runId,
606
+ scenarioId: "session",
607
+ startedAt: run.startedAt,
608
+ endedAt: run.endedAt,
609
+ status: run.failed ? "failed" : "completed"
610
+ });
611
+ }
284
612
  for (const s of spans) {
285
- if (!runIds.has(s.trace_id)) {
286
- runIds.add(s.trace_id);
287
- await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
288
- }
289
613
  const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
614
+ const startedAt = parseIsoToEpochMs(s.start_time);
615
+ const endedAt = parseIsoToEpochMs(s.end_time);
290
616
  const base = {
291
617
  spanId: s.span_id,
292
618
  parentSpanId: s.parent_span_id ?? void 0,
293
619
  runId: s.trace_id,
294
620
  name: s.name,
295
- startedAt: parseIsoToEpochMs(s.start_time),
296
- endedAt: parseIsoToEpochMs(s.end_time),
621
+ startedAt,
622
+ endedAt,
297
623
  status: s.status.code === "ERROR" ? "error" : "ok",
298
624
  error: s.status.message,
299
625
  attributes: s.attributes
300
626
  };
627
+ const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
301
628
  const span2 = kind === "tool" ? {
302
629
  ...base,
303
630
  kind: "tool",
304
631
  toolName: String(s.attributes["tool.name"] ?? "tool"),
305
- args: s.attributes.content ?? ""
632
+ args: tool.args,
633
+ argsCaptured: tool.argsCaptured,
634
+ latencyMs: Math.max(0, endedAt - startedAt)
306
635
  } : { ...base, kind };
307
636
  await store.appendSpan(span2);
308
637
  }
309
- return { store, runIds: [...runIds] };
638
+ return { store, runIds: [...runs.keys()] };
310
639
  }
311
640
 
312
641
  // src/pipelines.ts
313
642
  import { computeToolUseMetrics } from "@tangle-network/agent-eval";
314
- import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
643
+ import {
644
+ failureClusterView,
645
+ stuckLoopView
646
+ } from "@tangle-network/agent-eval/pipelines";
315
647
  async function runPipelines(spans, opts = {}) {
316
648
  const { store, runIds } = await toRuntimeStore(spans);
317
649
  const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
318
650
  const { store: loopStore } = await toRuntimeStore(loopEligible);
319
- const stuckLoops = await stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 });
320
- const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
321
- return { stuckLoops, toolUse };
651
+ const [stuckLoops, failureClusters, toolUse] = await Promise.all([
652
+ stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 }),
653
+ failureClusterView(store),
654
+ Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
655
+ ]);
656
+ return { stuckLoops, failureClusters, toolUse };
322
657
  }
323
658
 
324
- // src/adapters/conversation.ts
325
- var CONTENT_CAP = 8e3;
326
- function capText(raw) {
327
- const t = raw.trim();
328
- if (t.length <= CONTENT_CAP) return t;
329
- return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
659
+ // src/integrity.ts
660
+ import { createHash as createHash2 } from "crypto";
661
+ var receiptStates = /* @__PURE__ */ new WeakMap();
662
+ function receiptIdentity(receipt) {
663
+ return [
664
+ receipt.sourcePath,
665
+ receipt.lineNumber,
666
+ receipt.byteOffset,
667
+ receipt.byteLength,
668
+ receipt.sha256
669
+ ];
330
670
  }
331
- var ACTOR_ATTR = "tangle.actor";
332
- function userPromptSpan(o) {
333
- return span({
334
- traceId: o.traceId,
335
- spanId: o.spanId,
336
- parentSpanId: o.parentSpanId,
337
- name: "user.prompt",
338
- kind: "CHAIN",
339
- startTime: o.startTime,
340
- service: o.service ?? null,
341
- agent: o.agent ?? null,
342
- step: o.step,
343
- content: o.content,
344
- extra: { [ACTOR_ATTR]: o.actor ?? "human" }
345
- });
671
+ function receiptKey(receipt) {
672
+ return JSON.stringify(receiptIdentity(receipt));
673
+ }
674
+ function stateFor(ref, current) {
675
+ let state = receiptStates.get(ref);
676
+ if (!state || state.corruptions !== current) {
677
+ state = {
678
+ corruptions: current,
679
+ seen: new Set(current.map(receiptKey))
680
+ };
681
+ receiptStates.set(ref, state);
682
+ }
683
+ return state;
684
+ }
685
+ function recordSessionCorruption(ref, receipt) {
686
+ const current = ref.integrity?.corruptions ?? [];
687
+ const corruption = { ...receipt, harness: ref.harness, sessionId: ref.sessionId };
688
+ const state = stateFor(ref, current);
689
+ const key = receiptKey(receipt);
690
+ if (state.seen.has(key)) return;
691
+ if (ref.integrity) {
692
+ ref.integrity.corruptions.push(corruption);
693
+ } else {
694
+ ref.integrity = { status: "degraded_not_lossless", corruptions: [corruption] };
695
+ }
696
+ state.seen.add(key);
697
+ state.corruptions = ref.integrity.corruptions;
698
+ }
699
+ function sessionJsonlOptions(ref, options = {}) {
700
+ if (options.corruptionMode === "strict") return { mode: "strict" };
701
+ return {
702
+ mode: "recover",
703
+ onCorruption: (receipt) => recordSessionCorruption(ref, receipt)
704
+ };
705
+ }
706
+ function sessionIntegrityAttributes(ref) {
707
+ if (!ref.integrity) return {};
708
+ return {
709
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
710
+ [ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
711
+ [ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
712
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
713
+ };
714
+ }
715
+ function corruptionDigest(receipts) {
716
+ const hash2 = createHash2("sha256");
717
+ for (const receipt of receipts) {
718
+ hash2.update(JSON.stringify(receiptIdentity(receipt)));
719
+ hash2.update("\n");
720
+ }
721
+ return `sha256:${hash2.digest("hex")}`;
722
+ }
723
+ function receiptSpanId(parentSpanId, receipt) {
724
+ const digest = createHash2("sha256").update(JSON.stringify(receiptIdentity(receipt))).digest("hex");
725
+ return `${parentSpanId}:corruption:${digest}`;
726
+ }
727
+ function stampSessionIntegrity(ref, spans) {
728
+ if (!ref.integrity) return;
729
+ const parent = spans.find((item) => item.parent_span_id === null) ?? spans[0];
730
+ if (!parent) return;
731
+ const sessionId = parent.trace_id;
732
+ for (const receipt of ref.integrity.corruptions) {
733
+ receipt.harness = ref.harness;
734
+ receipt.sessionId = sessionId;
735
+ }
736
+ const attributes = sessionIntegrityAttributes(ref);
737
+ Object.assign(parent.attributes, attributes);
738
+ const existingSpanIds = new Set(spans.map((item) => item.span_id));
739
+ for (const receipt of ref.integrity.corruptions) {
740
+ const spanId = receiptSpanId(parent.span_id, receipt);
741
+ if (existingSpanIds.has(spanId)) continue;
742
+ spans.push(span({
743
+ traceId: parent.trace_id,
744
+ spanId,
745
+ parentSpanId: parent.span_id,
746
+ name: "source.corruption.receipt",
747
+ kind: "CHAIN",
748
+ startTime: parent.start_time,
749
+ endTime: parent.start_time,
750
+ status: "ERROR",
751
+ service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
752
+ extra: {
753
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
754
+ [ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
755
+ [ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
756
+ [ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
757
+ [ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
758
+ [ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
759
+ [ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
760
+ [ATTR.CORRUPTION_SHA256]: receipt.sha256,
761
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
762
+ }
763
+ }));
764
+ existingSpanIds.add(spanId);
765
+ }
766
+ }
767
+
768
+ // src/json.ts
769
+ import { readdir, readFile as readFile2 } from "fs/promises";
770
+ var JsonSourceError = class extends Error {
771
+ sourcePath;
772
+ kind;
773
+ code;
774
+ constructor(sourcePath, kind, cause) {
775
+ super(
776
+ kind === "parse" ? `Invalid JSON at ${sourcePath}` : `Unable to read JSON source at ${sourcePath}`,
777
+ { cause }
778
+ );
779
+ this.name = "JsonSourceError";
780
+ this.sourcePath = sourcePath;
781
+ this.kind = kind;
782
+ this.code = errorCode(cause);
783
+ }
784
+ };
785
+ function errorCode(error) {
786
+ return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
787
+ }
788
+ function pathSafeParseCause(cause) {
789
+ if (!(cause instanceof Error)) return cause;
790
+ const location = cause.message.match(/(?:at position \d+(?: \(line \d+ column \d+\))?|line \d+ column \d+)/i)?.[0];
791
+ cause.message = location ? `JSON parse failed ${location}` : "JSON parse failed";
792
+ cause.stack = `${cause.name}: ${cause.message}`;
793
+ return cause;
794
+ }
795
+ function isMissingPathError(error) {
796
+ return errorCode(error) === "ENOENT";
797
+ }
798
+ function isMissingJsonSource(error) {
799
+ return error instanceof JsonSourceError && error.kind === "read" && isMissingPathError(error);
800
+ }
801
+ async function readJsonFile(path) {
802
+ let raw;
803
+ try {
804
+ raw = await readFile2(path, "utf8");
805
+ } catch (error) {
806
+ throw new JsonSourceError(path, "read", error);
807
+ }
808
+ try {
809
+ return JSON.parse(raw);
810
+ } catch (error) {
811
+ throw new JsonSourceError(path, "parse", pathSafeParseCause(error));
812
+ }
813
+ }
814
+ async function listJsonFiles(path) {
815
+ try {
816
+ return (await readdir(path)).filter((file) => file.endsWith(".json"));
817
+ } catch (error) {
818
+ throw new JsonSourceError(path, "read", error);
819
+ }
346
820
  }
347
821
 
348
822
  // src/adapters/amp.ts
349
- import { readdir, readFile as readFile2, stat } from "fs/promises";
823
+ import { readdir as readdir2, stat } from "fs/promises";
350
824
  import { homedir } from "os";
351
825
  import { basename, join as join2 } from "path";
352
826
  var SERVICE = "amp";
@@ -365,9 +839,10 @@ var AmpAdapter = class {
365
839
  async locate(opts = {}) {
366
840
  let files;
367
841
  try {
368
- files = await readdir(this.threadsRoot());
369
- } catch {
370
- return [];
842
+ files = await readdir2(this.threadsRoot());
843
+ } catch (error) {
844
+ if (isMissingPathError(error)) return [];
845
+ throw error;
371
846
  }
372
847
  const refs = [];
373
848
  for (const f of files) {
@@ -376,8 +851,9 @@ var AmpAdapter = class {
376
851
  let st;
377
852
  try {
378
853
  st = await stat(path);
379
- } catch {
380
- continue;
854
+ } catch (error) {
855
+ if (isMissingPathError(error)) continue;
856
+ throw error;
381
857
  }
382
858
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
383
859
  if (opts.cwd) continue;
@@ -386,12 +862,7 @@ var AmpAdapter = class {
386
862
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
387
863
  }
388
864
  async parse(ref) {
389
- let thread;
390
- try {
391
- thread = JSON.parse(await readFile2(ref.path, "utf8"));
392
- } catch {
393
- return [];
394
- }
865
+ const thread = await readJsonFile(ref.path);
395
866
  const traceId = thread.id ?? ref.sessionId;
396
867
  const rootId = `root:${traceId}`;
397
868
  const start = new Date(thread.created ?? 0).toISOString();
@@ -423,7 +894,6 @@ var AmpAdapter = class {
423
894
  }
424
895
  } else {
425
896
  const u = m.usage;
426
- const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
427
897
  spans.push(
428
898
  span({
429
899
  traceId,
@@ -435,8 +905,10 @@ var AmpAdapter = class {
435
905
  service: SERVICE,
436
906
  agent: SERVICE,
437
907
  model: u?.model ?? null,
438
- inputTokens: inputTokens2,
908
+ inputTokens: u?.inputTokens ?? null,
439
909
  outputTokens: u?.outputTokens ?? null,
910
+ cachedInputTokens: u?.cacheReadInputTokens ?? null,
911
+ cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
440
912
  step,
441
913
  content: textOf(m.content) || null
442
914
  })
@@ -456,14 +928,17 @@ var AmpAdapter = class {
456
928
  agent: SERVICE,
457
929
  tool: b.name,
458
930
  step,
459
- content: b.input != null ? JSON.stringify(b.input) : null
931
+ extra: toolIoAttributes({ input: b.input })
460
932
  });
461
933
  spans.push(t);
462
934
  if (b.id) toolByUseId.set(b.id, t);
463
935
  step += 1;
464
936
  } else if (b.type === "tool_result" && b.tool_use_id) {
465
937
  const t = toolByUseId.get(b.tool_use_id);
466
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
938
+ if (t) {
939
+ t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
940
+ recordToolOutput(t, b.content ?? b.output);
941
+ }
467
942
  }
468
943
  }
469
944
  }
@@ -471,6 +946,108 @@ var AmpAdapter = class {
471
946
  }
472
947
  };
473
948
 
949
+ // src/jsonl.ts
950
+ import { isUtf8 } from "buffer";
951
+ import { createHash as createHash3 } from "crypto";
952
+ import { createReadStream } from "fs";
953
+ var JsonlParseError = class extends SyntaxError {
954
+ sourcePath;
955
+ lineNumber;
956
+ byteOffset;
957
+ byteLength;
958
+ sha256;
959
+ receipt;
960
+ constructor(receipt) {
961
+ super(`Invalid JSONL at ${receipt.sourcePath}:${receipt.lineNumber}`);
962
+ this.name = "JsonlParseError";
963
+ this.sourcePath = receipt.sourcePath;
964
+ this.lineNumber = receipt.lineNumber;
965
+ this.byteOffset = receipt.byteOffset;
966
+ this.byteLength = receipt.byteLength;
967
+ this.sha256 = receipt.sha256;
968
+ this.receipt = receipt;
969
+ }
970
+ };
971
+ function corruptionReceipt(rawLine, path, lineNumber, byteOffset) {
972
+ return {
973
+ receiptVersion: 1,
974
+ kind: "jsonl_corruption",
975
+ status: "degraded_not_lossless",
976
+ sourcePath: path,
977
+ lineNumber,
978
+ byteOffset,
979
+ byteLength: rawLine.length,
980
+ sha256: createHash3("sha256").update(rawLine).digest("hex"),
981
+ rawBytes: "local_source_only"
982
+ };
983
+ }
984
+ function parseLine(rawLine, path, lineNumber, byteOffset, options) {
985
+ const jsonBytes = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
986
+ if (jsonBytes.length === 0) return void 0;
987
+ if (!isUtf8(jsonBytes)) return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
988
+ const json2 = jsonBytes.toString("utf8");
989
+ if (json2.trim().length === 0) return void 0;
990
+ try {
991
+ return JSON.parse(json2);
992
+ } catch {
993
+ return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
994
+ }
995
+ }
996
+ function handleCorruption(rawLine, path, lineNumber, byteOffset, options) {
997
+ const receipt = corruptionReceipt(rawLine, path, lineNumber, byteOffset);
998
+ if (options.mode === "recover") {
999
+ options.onCorruption(receipt);
1000
+ return void 0;
1001
+ }
1002
+ throw new JsonlParseError(receipt);
1003
+ }
1004
+ async function* readJsonl(path, options = { mode: "strict" }) {
1005
+ if (options.mode === "recover" && typeof options.onCorruption !== "function") {
1006
+ throw new TypeError("JSONL recover mode requires an onCorruption callback");
1007
+ }
1008
+ const input = createReadStream(path);
1009
+ let lineNumber = 0;
1010
+ let byteOffset = 0;
1011
+ let fragments = [];
1012
+ try {
1013
+ for await (const chunk of input) {
1014
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1015
+ let start = 0;
1016
+ let newline = bytes.indexOf(10);
1017
+ while (newline !== -1) {
1018
+ fragments.push(bytes.subarray(start, newline));
1019
+ const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
1020
+ fragments = [];
1021
+ lineNumber += 1;
1022
+ const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
1023
+ if (parsed !== void 0) yield parsed;
1024
+ byteOffset += rawLine.length + 1;
1025
+ start = newline + 1;
1026
+ newline = bytes.indexOf(10, start);
1027
+ }
1028
+ if (start < bytes.length) fragments.push(bytes.subarray(start));
1029
+ }
1030
+ if (fragments.length > 0) {
1031
+ const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
1032
+ lineNumber += 1;
1033
+ const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
1034
+ if (parsed !== void 0) yield parsed;
1035
+ }
1036
+ } finally {
1037
+ input.destroy();
1038
+ }
1039
+ }
1040
+ async function takeJsonl(path, limit, options = { mode: "strict" }) {
1041
+ if (!Number.isInteger(limit) || limit < 0) throw new RangeError("limit must be a non-negative integer");
1042
+ const rows = [];
1043
+ if (limit === 0) return rows;
1044
+ for await (const row of readJsonl(path, options)) {
1045
+ rows.push(row);
1046
+ if (rows.length === limit) break;
1047
+ }
1048
+ return rows;
1049
+ }
1050
+
474
1051
  // src/adapters/actor.ts
475
1052
  var CMD_MARKERS = [
476
1053
  "<command-name>",
@@ -564,26 +1141,10 @@ function classifyReaction(text) {
564
1141
  var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
565
1142
 
566
1143
  // src/adapters/claude.ts
567
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
1144
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
568
1145
  import { homedir as homedir2 } from "os";
569
1146
  import { basename as basename2, join as join3 } from "path";
570
1147
  var SERVICE2 = "claude-code";
571
- function parseLines(raw) {
572
- const out = [];
573
- for (const line of raw.split("\n")) {
574
- if (!line) continue;
575
- try {
576
- out.push(JSON.parse(line));
577
- } catch {
578
- }
579
- }
580
- return out;
581
- }
582
- function inputTokens(u) {
583
- if (!u) return null;
584
- const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
585
- return v > 0 ? v : null;
586
- }
587
1148
  function asBlocks(content) {
588
1149
  return Array.isArray(content) ? content : [];
589
1150
  }
@@ -600,96 +1161,104 @@ function stringifyToolResult(content) {
600
1161
  }
601
1162
  return "";
602
1163
  }
603
- function parseClaudeStream(events, ctx) {
604
- const spans = [];
605
- const toolSpanByUseId = /* @__PURE__ */ new Map();
606
- let step = ctx.startStep;
607
- let sawUserTurn = false;
608
- for (const ev of events) {
609
- const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
610
- const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
611
- if (ev.type === "assistant" && ev.message) {
612
- const llmId = `${ctx.idPrefix}${uid}`;
613
- spans.push(
614
- span({
1164
+ function createClaudeStream(startStep) {
1165
+ return { spans: [], toolSpanByUseId: /* @__PURE__ */ new Map(), step: startStep, sawUserTurn: false };
1166
+ }
1167
+ function consumeClaudeEvent(ev, ctx, state) {
1168
+ const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1169
+ const uid = ev.uuid ?? `${ctx.idPrefix}step${state.step}`;
1170
+ if (ev.type === "assistant" && ev.message) {
1171
+ const llmId = `${ctx.idPrefix}${uid}`;
1172
+ state.spans.push(
1173
+ span({
1174
+ traceId: ctx.traceId,
1175
+ spanId: llmId,
1176
+ parentSpanId: ctx.rootParent,
1177
+ name: "llm.turn",
1178
+ kind: "LLM",
1179
+ startTime: ts,
1180
+ service: SERVICE2,
1181
+ agent: ctx.agent,
1182
+ model: ev.message.model ?? null,
1183
+ inputTokens: ev.message.usage?.input_tokens ?? null,
1184
+ outputTokens: ev.message.usage?.output_tokens ?? null,
1185
+ cachedInputTokens: ev.message.usage?.cache_read_input_tokens ?? null,
1186
+ cacheWriteInputTokens: ev.message.usage?.cache_creation_input_tokens ?? null,
1187
+ step: state.step,
1188
+ content: textOf2(ev.message.content) || null
1189
+ })
1190
+ );
1191
+ state.step += 1;
1192
+ let toolIdx = 0;
1193
+ for (const block of asBlocks(ev.message.content)) {
1194
+ if (block.type !== "tool_use" || !block.name) continue;
1195
+ const toolSpan = span({
1196
+ traceId: ctx.traceId,
1197
+ spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
1198
+ parentSpanId: llmId,
1199
+ name: `tool.${block.name}`,
1200
+ kind: "TOOL",
1201
+ startTime: ts,
1202
+ service: SERVICE2,
1203
+ agent: ctx.agent,
1204
+ tool: block.name,
1205
+ step: state.step,
1206
+ extra: toolIoAttributes({ input: block.input })
1207
+ });
1208
+ state.spans.push(toolSpan);
1209
+ if (block.id) state.toolSpanByUseId.set(block.id, toolSpan);
1210
+ toolIdx += 1;
1211
+ state.step += 1;
1212
+ }
1213
+ } else if (ev.type === "user" && ev.message) {
1214
+ const prompt = textOf2(ev.message.content);
1215
+ if (prompt) {
1216
+ const actor = claudeActor({
1217
+ text: prompt,
1218
+ isSidechain: ev.isSidechain,
1219
+ userType: ev.userType ?? null,
1220
+ isFirstUserTurn: !state.sawUserTurn
1221
+ });
1222
+ state.sawUserTurn = true;
1223
+ state.spans.push(
1224
+ userPromptSpan({
615
1225
  traceId: ctx.traceId,
616
- spanId: llmId,
1226
+ spanId: `${ctx.idPrefix}${uid}:user`,
617
1227
  parentSpanId: ctx.rootParent,
618
- name: "llm.turn",
619
- kind: "LLM",
620
1228
  startTime: ts,
621
1229
  service: SERVICE2,
622
1230
  agent: ctx.agent,
623
- model: ev.message.model ?? null,
624
- inputTokens: inputTokens(ev.message.usage),
625
- outputTokens: ev.message.usage?.output_tokens ?? null,
626
- step,
627
- content: textOf2(ev.message.content) || null
1231
+ step: state.step,
1232
+ content: prompt,
1233
+ actor
628
1234
  })
629
1235
  );
630
- step += 1;
631
- let toolIdx = 0;
632
- for (const block of asBlocks(ev.message.content)) {
633
- if (block.type !== "tool_use" || !block.name) continue;
634
- const toolSpan = span({
635
- traceId: ctx.traceId,
636
- spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
637
- parentSpanId: llmId,
638
- name: `tool.${block.name}`,
639
- kind: "TOOL",
640
- startTime: ts,
641
- service: SERVICE2,
642
- agent: ctx.agent,
643
- tool: block.name,
644
- step,
645
- content: block.input != null ? JSON.stringify(block.input) : null
646
- });
647
- spans.push(toolSpan);
648
- if (block.id) toolSpanByUseId.set(block.id, toolSpan);
649
- toolIdx += 1;
650
- step += 1;
651
- }
652
- } else if (ev.type === "user" && ev.message) {
653
- const prompt = textOf2(ev.message.content);
654
- if (prompt) {
655
- const actor = claudeActor({
656
- text: prompt,
657
- isSidechain: ev.isSidechain,
658
- userType: ev.userType ?? null,
659
- isFirstUserTurn: !sawUserTurn
660
- });
661
- sawUserTurn = true;
662
- spans.push(
663
- userPromptSpan({
664
- traceId: ctx.traceId,
665
- spanId: `${ctx.idPrefix}${uid}:user`,
666
- parentSpanId: ctx.rootParent,
667
- startTime: ts,
668
- service: SERVICE2,
669
- agent: ctx.agent,
670
- step,
671
- content: prompt,
672
- actor
673
- })
674
- );
675
- step += 1;
676
- }
677
- for (const block of asBlocks(ev.message.content)) {
678
- if (block.type !== "tool_result" || !block.tool_use_id) continue;
679
- backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
680
- }
681
- } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
682
- const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
683
- backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
1236
+ state.step += 1;
1237
+ }
1238
+ for (const block of asBlocks(ev.message.content)) {
1239
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
1240
+ backfillResult(
1241
+ state.toolSpanByUseId.get(block.tool_use_id),
1242
+ ts,
1243
+ block.is_error === true,
1244
+ block.content
1245
+ );
684
1246
  }
1247
+ } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
1248
+ const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
1249
+ backfillResult(state.toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
685
1250
  }
686
- return { spans, toolSpanByUseId, nextStep: step };
687
1251
  }
688
- function backfillResult(s, endTime, isError, message) {
1252
+ function finishClaudeStream(state) {
1253
+ return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
1254
+ }
1255
+ function backfillResult(s, endTime, isError, output) {
689
1256
  if (!s) return;
690
1257
  s.end_time = endTime;
691
1258
  const code = isError ? "ERROR" : "OK";
692
1259
  s.status = { code };
1260
+ recordToolOutput(s, output);
1261
+ const message = stringifyToolResult(output);
693
1262
  if (isError && message) s.status.message = message.slice(0, 500);
694
1263
  }
695
1264
  var ClaudeAdapter = class {
@@ -702,18 +1271,20 @@ var ClaudeAdapter = class {
702
1271
  const root = this.root();
703
1272
  let projectDirs;
704
1273
  try {
705
- projectDirs = await readdir2(root);
706
- } catch {
707
- return [];
1274
+ projectDirs = await readdir3(root);
1275
+ } catch (error) {
1276
+ if (isMissingPathError(error)) return [];
1277
+ throw error;
708
1278
  }
709
1279
  const refs = [];
710
1280
  for (const dir of projectDirs) {
711
1281
  const dirPath = join3(root, dir);
712
1282
  let files;
713
1283
  try {
714
- files = await readdir2(dirPath);
715
- } catch {
716
- continue;
1284
+ files = await readdir3(dirPath);
1285
+ } catch (error) {
1286
+ if (isMissingPathError(error)) continue;
1287
+ throw error;
717
1288
  }
718
1289
  for (const f of files) {
719
1290
  if (!f.endsWith(".jsonl")) continue;
@@ -721,8 +1292,9 @@ var ClaudeAdapter = class {
721
1292
  let st;
722
1293
  try {
723
1294
  st = await stat2(path);
724
- } catch {
725
- continue;
1295
+ } catch (error) {
1296
+ if (isMissingPathError(error)) continue;
1297
+ throw error;
726
1298
  }
727
1299
  if (!st.isFile()) continue;
728
1300
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -733,11 +1305,37 @@ var ClaudeAdapter = class {
733
1305
  }
734
1306
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
735
1307
  }
736
- async parse(ref) {
737
- const raw = await readFile3(ref.path, "utf8");
738
- const events = parseLines(raw);
739
- const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
1308
+ async parse(ref, options = {}) {
1309
+ const sourceTraceId = ref.sessionId;
1310
+ const sourceRootId = `root:${sourceTraceId}`;
1311
+ const ctx = {
1312
+ traceId: sourceTraceId,
1313
+ agent: SERVICE2,
1314
+ startStep: 0,
1315
+ idPrefix: "",
1316
+ rootParent: sourceRootId
1317
+ };
1318
+ const state = createClaudeStream(ctx.startStep);
1319
+ let discoveredTraceId;
1320
+ let firstTimestamp;
1321
+ let lastTimestamp;
1322
+ let sawEvent = false;
1323
+ for await (const event of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1324
+ if (!sawEvent) {
1325
+ firstTimestamp = event.timestamp;
1326
+ sawEvent = true;
1327
+ }
1328
+ lastTimestamp = event.timestamp;
1329
+ if (!discoveredTraceId && event.sessionId) discoveredTraceId = event.sessionId;
1330
+ consumeClaudeEvent(event, ctx, state);
1331
+ }
1332
+ const main = finishClaudeStream(state);
1333
+ const traceId = discoveredTraceId ?? sourceTraceId;
740
1334
  const rootId = `root:${traceId}`;
1335
+ for (const item of main.spans) {
1336
+ item.trace_id = traceId;
1337
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
1338
+ }
741
1339
  const spans = [
742
1340
  span({
743
1341
  traceId,
@@ -745,55 +1343,53 @@ var ClaudeAdapter = class {
745
1343
  parentSpanId: null,
746
1344
  name: "session",
747
1345
  kind: "AGENT",
748
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
749
- endTime: events.at(-1)?.timestamp,
1346
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1347
+ endTime: lastTimestamp,
750
1348
  service: SERVICE2,
751
1349
  agent: SERVICE2
752
- })
1350
+ }),
1351
+ ...main.spans
753
1352
  ];
754
- const main = parseClaudeStream(events, {
755
- traceId,
756
- agent: SERVICE2,
757
- startStep: 0,
758
- idPrefix: "",
759
- rootParent: rootId
760
- });
761
- spans.push(...main.spans);
762
- await this.foldSubagents(ref, traceId, main, spans);
1353
+ await this.foldSubagents(ref, traceId, main, spans, options);
763
1354
  return spans;
764
1355
  }
765
1356
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
766
- async foldSubagents(ref, traceId, main, out) {
1357
+ async foldSubagents(ref, traceId, main, out, options) {
767
1358
  const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
768
1359
  let files;
769
1360
  try {
770
- files = await readdir2(subDir);
771
- } catch {
772
- return;
1361
+ files = await readdir3(subDir);
1362
+ } catch (error) {
1363
+ if (isMissingPathError(error)) return;
1364
+ throw error;
773
1365
  }
774
1366
  let step = main.nextStep;
775
- for (const f of files) {
776
- if (!f.endsWith(".jsonl")) continue;
777
- const hash2 = basename2(f, ".jsonl");
1367
+ for (const file of files) {
1368
+ if (!file.endsWith(".jsonl")) continue;
1369
+ const hash2 = basename2(file, ".jsonl");
1370
+ const metaPath = join3(subDir, `${hash2}.meta.json`);
778
1371
  let meta = {};
779
1372
  try {
780
- meta = JSON.parse(await readFile3(join3(subDir, `${hash2}.meta.json`), "utf8"));
781
- } catch {
1373
+ meta = await readJsonFile(metaPath);
1374
+ } catch (error) {
1375
+ if (!isMissingJsonSource(error)) throw error;
782
1376
  }
783
1377
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
784
- let raw;
785
- try {
786
- raw = await readFile3(join3(subDir, f), "utf8");
787
- } catch {
788
- continue;
789
- }
790
- const parsed = parseClaudeStream(parseLines(raw), {
1378
+ const ctx = {
791
1379
  traceId,
792
1380
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
793
1381
  startStep: step,
794
1382
  idPrefix: `${hash2}:`,
795
1383
  rootParent: parent
796
- });
1384
+ };
1385
+ const state = createClaudeStream(ctx.startStep);
1386
+ for await (const event of readJsonl(
1387
+ join3(subDir, file),
1388
+ sessionJsonlOptions(ref, options)
1389
+ )) {
1390
+ consumeClaudeEvent(event, ctx, state);
1391
+ }
1392
+ const parsed = finishClaudeStream(state);
797
1393
  out.push(...parsed.spans);
798
1394
  step = parsed.nextStep;
799
1395
  }
@@ -801,21 +1397,11 @@ var ClaudeAdapter = class {
801
1397
  };
802
1398
 
803
1399
  // src/adapters/codex.ts
804
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
1400
+ import { readdir as readdir4, stat as stat3 } from "fs/promises";
805
1401
  import { homedir as homedir3 } from "os";
806
1402
  import { basename as basename3, join as join4 } from "path";
807
1403
  var SERVICE3 = "codex";
808
- function parseLines2(raw) {
809
- const out = [];
810
- for (const line of raw.split("\n")) {
811
- if (!line) continue;
812
- try {
813
- out.push(JSON.parse(line));
814
- } catch {
815
- }
816
- }
817
- return out;
818
- }
1404
+ var SESSION_HEAD_LINES = 40;
819
1405
  function contentToString(content) {
820
1406
  if (typeof content === "string") return content;
821
1407
  if (Array.isArray(content)) {
@@ -842,6 +1428,28 @@ function toolInputToString(input) {
842
1428
  if (input == null) return void 0;
843
1429
  return JSON.stringify(input);
844
1430
  }
1431
+ var CODEX_SESSION_ID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
1432
+ function sessionIds(value) {
1433
+ return [...new Set(contentToString(value).match(CODEX_SESSION_ID) ?? [])];
1434
+ }
1435
+ function spawnedSessionIds(output) {
1436
+ const text = contentToString(output);
1437
+ const ids = [...text.matchAll(/["']?agent_id["']?\s*:\s*["']([0-9a-f-]{36})["']/gi)].map((match) => match[1]).filter((id) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id));
1438
+ return [...new Set(ids)];
1439
+ }
1440
+ function multiAgentOperation(name) {
1441
+ const prefix = "multi_agent_v1__";
1442
+ return name.startsWith(prefix) ? name.slice(prefix.length) : null;
1443
+ }
1444
+ function setAgentSessionIds(toolSpan, ids) {
1445
+ if (ids.length === 0) return;
1446
+ const unique = [...new Set(ids)];
1447
+ toolSpan.attributes["traces.codex.agent_session_ids"] = JSON.stringify(unique);
1448
+ toolSpan.attributes["traces.codex.agent_session_count"] = unique.length;
1449
+ if (toolSpan.attributes["traces.codex.agent_operation"] === "spawn_agent") {
1450
+ toolSpan.attributes["traces.child_session_ids"] = JSON.stringify(unique);
1451
+ }
1452
+ }
845
1453
  var verificationCommand = /\b(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?(?:test|typecheck|lint|build|check)(?::[A-Za-z0-9:_-]+)?\b|\b(?:vitest|jest|pytest|tsc|biome|eslint|sha256sum|pdfinfo|pdftotext)\b|\bgo\s+test\b|\bcargo\s+(?:test|check|clippy|build)\b|\bgit\s+(?:status|diff|show|merge-tree)\b|\bgh-drew\s+pr\s+(?:view|checks)\b/i;
846
1454
  function hasReadOnlyCurl(input) {
847
1455
  if (!/\bcurl\b/i.test(input)) return false;
@@ -861,33 +1469,37 @@ function isExpectedBlockingTool(name, input) {
861
1469
  async function* walkRollouts(root) {
862
1470
  let years;
863
1471
  try {
864
- years = await readdir3(root);
865
- } catch {
866
- return;
1472
+ years = await readdir4(root);
1473
+ } catch (error) {
1474
+ if (isMissingPathError(error)) return;
1475
+ throw error;
867
1476
  }
868
1477
  for (const y of years) {
869
1478
  const yp = join4(root, y);
870
1479
  let months;
871
1480
  try {
872
- months = await readdir3(yp);
873
- } catch {
874
- continue;
1481
+ months = await readdir4(yp);
1482
+ } catch (error) {
1483
+ if (isMissingPathError(error)) continue;
1484
+ throw error;
875
1485
  }
876
1486
  for (const m of months) {
877
1487
  const mp = join4(yp, m);
878
1488
  let days;
879
1489
  try {
880
- days = await readdir3(mp);
881
- } catch {
882
- continue;
1490
+ days = await readdir4(mp);
1491
+ } catch (error) {
1492
+ if (isMissingPathError(error)) continue;
1493
+ throw error;
883
1494
  }
884
1495
  for (const d of days) {
885
1496
  const dp = join4(mp, d);
886
1497
  let files;
887
1498
  try {
888
- files = await readdir3(dp);
889
- } catch {
890
- continue;
1499
+ files = await readdir4(dp);
1500
+ } catch (error) {
1501
+ if (isMissingPathError(error)) continue;
1502
+ throw error;
891
1503
  }
892
1504
  for (const f of files) {
893
1505
  if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
@@ -908,75 +1520,106 @@ var CodexAdapter = class {
908
1520
  let st;
909
1521
  try {
910
1522
  st = await stat3(path);
911
- } catch {
912
- continue;
1523
+ } catch (error) {
1524
+ if (isMissingPathError(error)) continue;
1525
+ throw error;
913
1526
  }
914
1527
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
915
- const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
916
1528
  let cwd = null;
917
1529
  let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
918
- for (const line of head) {
919
- if (!line) continue;
920
- try {
921
- const l = JSON.parse(line);
922
- if (l.type === "session_meta" && l.payload?.id) id = l.payload.id;
923
- if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
924
- } catch {
925
- }
1530
+ const ref = { harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs };
1531
+ const head = await takeJsonl(path, SESSION_HEAD_LINES, sessionJsonlOptions(ref));
1532
+ for (const parsed of head) {
1533
+ if (parsed.type === "session_meta" && parsed.payload?.id) id = parsed.payload.id;
1534
+ if (!cwd && parsed.payload?.cwd) cwd = parsed.payload.cwd;
926
1535
  if (cwd) break;
927
1536
  }
928
- if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
929
- refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
1537
+ ref.sessionId = id;
1538
+ ref.cwd = cwd;
1539
+ if (ref.integrity) {
1540
+ ref.integrity.corruptions = ref.integrity.corruptions.map((receipt) => ({ ...receipt, sessionId: id }));
1541
+ }
1542
+ if (opts.cwd && cwd && !cwd.startsWith(opts.cwd)) continue;
1543
+ if (opts.cwd && !cwd && !ref.integrity) continue;
1544
+ refs.push(ref);
930
1545
  }
931
1546
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
932
1547
  }
933
- async parse(ref) {
934
- const lines = parseLines2(await readFile4(ref.path, "utf8"));
935
- const meta = lines.find((l) => l.type === "session_meta");
1548
+ async parse(ref, options = {}) {
1549
+ const jsonl2 = sessionJsonlOptions(ref, options);
1550
+ const head = await takeJsonl(ref.path, SESSION_HEAD_LINES, jsonl2);
1551
+ let first = head[0];
1552
+ let meta = head.find((line) => line.type === "session_meta");
1553
+ let model = head.find((line) => line.type === "turn_context")?.payload?.model ?? null;
1554
+ if (!first || !meta) {
1555
+ for await (const line of readJsonl(ref.path, jsonl2)) {
1556
+ first ??= line;
1557
+ if (!meta && line.type === "session_meta") meta = line;
1558
+ if (!model && line.type === "turn_context") model = line.payload?.model ?? null;
1559
+ if (first && meta) break;
1560
+ }
1561
+ }
936
1562
  const traceId = meta?.payload?.id ?? ref.sessionId;
937
- const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
1563
+ const spawnMeta = meta?.payload?.source?.subagent?.thread_spawn;
1564
+ const parentSessionId = meta?.payload?.parent_thread_id ?? spawnMeta?.parent_thread_id;
1565
+ const sessionRole = parentSessionId || meta?.payload?.thread_source === "subagent" ? "child" : "operator";
938
1566
  const rootId = `root:${traceId}`;
939
- const spans = [
940
- span({
941
- traceId,
942
- spanId: rootId,
943
- parentSpanId: null,
944
- name: "session",
945
- kind: "AGENT",
946
- startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
947
- endTime: lines.at(-1)?.timestamp,
948
- service: SERVICE3,
949
- agent: SERVICE3,
950
- model
951
- })
952
- ];
1567
+ const root = span({
1568
+ traceId,
1569
+ spanId: rootId,
1570
+ parentSpanId: null,
1571
+ name: "session",
1572
+ kind: "AGENT",
1573
+ startTime: meta?.timestamp ?? first?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1574
+ service: SERVICE3,
1575
+ agent: SERVICE3,
1576
+ model,
1577
+ extra: {
1578
+ "traces.session.role": sessionRole,
1579
+ ...parentSessionId ? { "traces.parent_session_id": parentSessionId } : {},
1580
+ ...spawnMeta?.depth != null ? { "traces.codex.agent_depth": spawnMeta.depth } : {},
1581
+ ...meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname ? { "traces.codex.agent_nickname": meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname } : {},
1582
+ ...meta?.payload?.agent_role ?? spawnMeta?.agent_role ? { "traces.codex.agent_role": meta?.payload?.agent_role ?? spawnMeta?.agent_role } : {},
1583
+ ...spawnMeta?.agent_path ? { "traces.codex.agent_path": spawnMeta.agent_path } : {}
1584
+ }
1585
+ });
1586
+ const spans = [root];
953
1587
  const toolByCallId = /* @__PURE__ */ new Map();
954
1588
  const subagentByThreadId = /* @__PURE__ */ new Map();
955
1589
  let step = 0;
956
1590
  let lastLlm = rootId;
957
1591
  let sawUserTurn = false;
958
- for (const l of lines) {
1592
+ let lastTimestamp;
1593
+ const awaitingModel = model ? [] : [root];
1594
+ for await (const l of readJsonl(ref.path, jsonl2)) {
1595
+ lastTimestamp = l.timestamp;
959
1596
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
960
- if (l.type === "event_msg" && l.payload?.type === "token_count") {
1597
+ if (!model && l.type === "turn_context" && l.payload?.model) {
1598
+ model = l.payload.model;
1599
+ for (const pending of awaitingModel) pending.attributes["llm.model_name"] = model;
1600
+ awaitingModel.length = 0;
1601
+ } else if (l.type === "event_msg" && l.payload?.type === "token_count") {
961
1602
  const u = l.payload.info?.last_token_usage;
962
1603
  if (u && (u.input_tokens || u.output_tokens)) {
963
1604
  const id = `llm:${step}`;
964
- spans.push(
965
- span({
966
- traceId,
967
- spanId: id,
968
- parentSpanId: rootId,
969
- name: "llm.turn",
970
- kind: "LLM",
971
- startTime: ts,
972
- service: SERVICE3,
973
- agent: SERVICE3,
974
- model,
975
- inputTokens: u.input_tokens ?? null,
976
- outputTokens: u.output_tokens ?? null,
977
- step
978
- })
979
- );
1605
+ const llm = span({
1606
+ traceId,
1607
+ spanId: id,
1608
+ parentSpanId: rootId,
1609
+ name: "llm.turn",
1610
+ kind: "LLM",
1611
+ startTime: ts,
1612
+ service: SERVICE3,
1613
+ agent: SERVICE3,
1614
+ model,
1615
+ inputTokens: u.input_tokens ?? null,
1616
+ outputTokens: u.output_tokens ?? null,
1617
+ reasoningTokens: u.reasoning_output_tokens ?? null,
1618
+ cachedInputTokens: u.cached_input_tokens ?? null,
1619
+ step
1620
+ });
1621
+ spans.push(llm);
1622
+ if (!model) awaitingModel.push(llm);
980
1623
  lastLlm = id;
981
1624
  step += 1;
982
1625
  }
@@ -988,6 +1631,7 @@ var CodexAdapter = class {
988
1631
  );
989
1632
  const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
990
1633
  const name = classifyNestedTool(nestedName ?? outerName, input);
1634
+ const agentOperation = multiAgentOperation(nestedName ?? outerName);
991
1635
  const toolSpan = span({
992
1636
  traceId,
993
1637
  spanId: `tool:${callId}`,
@@ -999,14 +1643,16 @@ var CodexAdapter = class {
999
1643
  agent: SERVICE3,
1000
1644
  tool: name,
1001
1645
  step,
1002
- content: input ?? null,
1003
1646
  extra: {
1647
+ ...toolIoAttributes({ input }),
1004
1648
  "traces.codex.call_type": l.payload.type,
1005
1649
  ...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
1006
1650
  ...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
1007
- ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
1651
+ ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
1652
+ ...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
1008
1653
  }
1009
1654
  });
1655
+ if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
1010
1656
  spans.push(toolSpan);
1011
1657
  toolByCallId.set(callId, toolSpan);
1012
1658
  step += 1;
@@ -1016,6 +1662,10 @@ var CodexAdapter = class {
1016
1662
  const { error, message } = outputIsError(l.payload.output);
1017
1663
  t.end_time = ts;
1018
1664
  t.status = error ? { code: "ERROR", message } : { code: "OK" };
1665
+ recordToolOutput(t, l.payload.output);
1666
+ if (t.attributes["traces.codex.agent_operation"] === "spawn_agent") {
1667
+ setAgentSessionIds(t, spawnedSessionIds(l.payload.output));
1668
+ }
1019
1669
  }
1020
1670
  } else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
1021
1671
  const threadId = l.payload.agent_thread_id;
@@ -1035,12 +1685,14 @@ var CodexAdapter = class {
1035
1685
  agent: SERVICE3,
1036
1686
  tool: "Agent",
1037
1687
  step,
1038
- content: JSON.stringify({
1039
- subagent_type: subagentType,
1040
- agent_path: agentPath,
1041
- agent_thread_id: threadId
1042
- }),
1043
1688
  extra: {
1689
+ ...toolIoAttributes({
1690
+ input: {
1691
+ subagent_type: subagentType,
1692
+ agent_path: agentPath,
1693
+ agent_thread_id: threadId
1694
+ }
1695
+ }),
1044
1696
  "traces.codex.subagent_path": agentPath,
1045
1697
  "traces.codex.subagent_thread_id": threadId
1046
1698
  }
@@ -1102,26 +1754,16 @@ var CodexAdapter = class {
1102
1754
  }
1103
1755
  }
1104
1756
  }
1757
+ root.end_time = lastTimestamp ?? root.start_time;
1105
1758
  return spans;
1106
1759
  }
1107
1760
  };
1108
1761
 
1109
1762
  // src/adapters/copilot.ts
1110
- import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
1763
+ import { readdir as readdir5, stat as stat4 } from "fs/promises";
1111
1764
  import { homedir as homedir4 } from "os";
1112
1765
  import { join as join5 } from "path";
1113
1766
  var SERVICE4 = "github-copilot";
1114
- function parseLines3(raw) {
1115
- const out = [];
1116
- for (const line of raw.split("\n")) {
1117
- if (!line.trim()) continue;
1118
- try {
1119
- out.push(JSON.parse(line));
1120
- } catch {
1121
- }
1122
- }
1123
- return out;
1124
- }
1125
1767
  var CopilotAdapter = class {
1126
1768
  harness = "github-copilot";
1127
1769
  aliases = ["copilot"];
@@ -1131,9 +1773,10 @@ var CopilotAdapter = class {
1131
1773
  async locate(opts = {}) {
1132
1774
  let dirs;
1133
1775
  try {
1134
- dirs = await readdir4(this.root());
1135
- } catch {
1136
- return [];
1776
+ dirs = await readdir5(this.root());
1777
+ } catch (error) {
1778
+ if (isMissingPathError(error)) return [];
1779
+ throw error;
1137
1780
  }
1138
1781
  const refs = [];
1139
1782
  for (const id of dirs) {
@@ -1141,8 +1784,9 @@ var CopilotAdapter = class {
1141
1784
  let st;
1142
1785
  try {
1143
1786
  st = await stat4(path);
1144
- } catch {
1145
- continue;
1787
+ } catch (error) {
1788
+ if (isMissingPathError(error)) continue;
1789
+ throw error;
1146
1790
  }
1147
1791
  if (!st.isFile()) continue;
1148
1792
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1151,28 +1795,23 @@ var CopilotAdapter = class {
1151
1795
  }
1152
1796
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1153
1797
  }
1154
- async parse(ref) {
1155
- const events = parseLines3(await readFile5(ref.path, "utf8"));
1798
+ async parse(ref, options = {}) {
1156
1799
  const traceId = ref.sessionId;
1157
1800
  const rootId = `root:${traceId}`;
1158
- const spans = [
1159
- span({
1160
- traceId,
1161
- spanId: rootId,
1162
- parentSpanId: null,
1163
- name: "session",
1164
- kind: "AGENT",
1165
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1166
- endTime: events.at(-1)?.timestamp,
1167
- service: SERVICE4,
1168
- agent: SERVICE4
1169
- })
1170
- ];
1801
+ const spans = [];
1171
1802
  const toolByCallId = /* @__PURE__ */ new Map();
1803
+ let firstTimestamp;
1804
+ let lastTimestamp;
1805
+ let sawEvent = false;
1172
1806
  let step = 0;
1173
1807
  let lastLlm = rootId;
1174
1808
  let pendingInputTokens = null;
1175
- for (const ev of events) {
1809
+ for await (const ev of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1810
+ if (!sawEvent) {
1811
+ firstTimestamp = ev.timestamp;
1812
+ sawEvent = true;
1813
+ }
1814
+ lastTimestamp = ev.timestamp;
1176
1815
  const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1177
1816
  const d = ev.data ?? {};
1178
1817
  if (ev.type === "assistant.usage") {
@@ -1214,7 +1853,7 @@ var CopilotAdapter = class {
1214
1853
  agent: SERVICE4,
1215
1854
  tool: name,
1216
1855
  step,
1217
- content: d.arguments != null ? JSON.stringify(d.arguments) : null
1856
+ extra: toolIoAttributes({ input: d.arguments })
1218
1857
  });
1219
1858
  spans.push(t);
1220
1859
  toolByCallId.set(d.toolCallId, t);
@@ -1225,15 +1864,27 @@ var CopilotAdapter = class {
1225
1864
  t.end_time = ts;
1226
1865
  const err = d.success === false;
1227
1866
  t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
1867
+ recordToolOutput(t, d.output ?? d.result ?? d.error?.message);
1228
1868
  }
1229
1869
  }
1230
1870
  }
1231
- return spans;
1871
+ const root = span({
1872
+ traceId,
1873
+ spanId: rootId,
1874
+ parentSpanId: null,
1875
+ name: "session",
1876
+ kind: "AGENT",
1877
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1878
+ endTime: lastTimestamp,
1879
+ service: SERVICE4,
1880
+ agent: SERVICE4
1881
+ });
1882
+ return [root, ...spans];
1232
1883
  }
1233
1884
  };
1234
1885
 
1235
1886
  // src/adapters/factory.ts
1236
- import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
1887
+ import { readdir as readdir6, stat as stat5 } from "fs/promises";
1237
1888
  import { homedir as homedir5 } from "os";
1238
1889
  import { basename as basename4, join as join6 } from "path";
1239
1890
  var SERVICE5 = "factory";
@@ -1242,17 +1893,6 @@ function textOf4(content) {
1242
1893
  (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
1243
1894
  );
1244
1895
  }
1245
- function parseLines4(raw) {
1246
- const out = [];
1247
- for (const line of raw.split("\n")) {
1248
- if (!line) continue;
1249
- try {
1250
- out.push(JSON.parse(line));
1251
- } catch {
1252
- }
1253
- }
1254
- return out;
1255
- }
1256
1896
  var FactoryAdapter = class {
1257
1897
  harness = "factory";
1258
1898
  aliases = ["factory-droids", "droid"];
@@ -1263,18 +1903,20 @@ var FactoryAdapter = class {
1263
1903
  const root = this.root();
1264
1904
  let dirs;
1265
1905
  try {
1266
- dirs = await readdir5(root);
1267
- } catch {
1268
- return [];
1906
+ dirs = await readdir6(root);
1907
+ } catch (error) {
1908
+ if (isMissingPathError(error)) return [];
1909
+ throw error;
1269
1910
  }
1270
1911
  const refs = [];
1271
1912
  for (const dir of dirs) {
1272
1913
  const dp = join6(root, dir);
1273
1914
  let files;
1274
1915
  try {
1275
- files = await readdir5(dp);
1276
- } catch {
1277
- continue;
1916
+ files = await readdir6(dp);
1917
+ } catch (error) {
1918
+ if (isMissingPathError(error)) continue;
1919
+ throw error;
1278
1920
  }
1279
1921
  const cwd = dir.replace(/-/g, "/");
1280
1922
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
@@ -1284,8 +1926,9 @@ var FactoryAdapter = class {
1284
1926
  let st;
1285
1927
  try {
1286
1928
  st = await stat5(path);
1287
- } catch {
1288
- continue;
1929
+ } catch (error) {
1930
+ if (isMissingPathError(error)) continue;
1931
+ throw error;
1289
1932
  }
1290
1933
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1291
1934
  refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
@@ -1293,38 +1936,32 @@ var FactoryAdapter = class {
1293
1936
  }
1294
1937
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1295
1938
  }
1296
- async parse(ref) {
1297
- const lines = parseLines4(await readFile6(ref.path, "utf8"));
1298
- const start = lines.find((l) => l.type === "session_start");
1299
- const traceId = start?.id ?? ref.sessionId;
1300
- const rootId = `root:${traceId}`;
1939
+ async parse(ref, options = {}) {
1301
1940
  let settings = {};
1302
1941
  try {
1303
- settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
1304
- } catch {
1942
+ settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
1943
+ } catch (error) {
1944
+ if (!isMissingJsonSource(error)) throw error;
1305
1945
  }
1306
- const spans = [
1307
- span({
1308
- traceId,
1309
- spanId: rootId,
1310
- parentSpanId: null,
1311
- name: "session",
1312
- kind: "AGENT",
1313
- startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1314
- endTime: lines.at(-1)?.timestamp,
1315
- service: SERVICE5,
1316
- agent: SERVICE5,
1317
- model: settings.model ?? null,
1318
- extra: settings.tokenUsage ? {
1319
- "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
1320
- "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
1321
- } : void 0
1322
- })
1323
- ];
1946
+ const sourceTraceId = ref.sessionId;
1947
+ const sourceRootId = `root:${sourceTraceId}`;
1948
+ const spans = [];
1324
1949
  const toolByUseId = /* @__PURE__ */ new Map();
1950
+ let firstTimestamp;
1951
+ let lastTimestamp;
1952
+ let sessionLine;
1953
+ let sawLine = false;
1325
1954
  let step = 0;
1326
- let lastLlm = rootId;
1327
- for (const l of lines) {
1955
+ let lastLlm = sourceRootId;
1956
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1957
+ if (!sawLine) {
1958
+ firstTimestamp = l.timestamp;
1959
+ sawLine = true;
1960
+ }
1961
+ lastTimestamp = l.timestamp;
1962
+ if (!sessionLine && l.type === "session_start") {
1963
+ sessionLine = { id: l.id, timestamp: l.timestamp };
1964
+ }
1328
1965
  if (l.type !== "message" || !l.message) continue;
1329
1966
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1330
1967
  const role = l.message.role;
@@ -1333,9 +1970,9 @@ var FactoryAdapter = class {
1333
1970
  if (text) {
1334
1971
  spans.push(
1335
1972
  userPromptSpan({
1336
- traceId,
1973
+ traceId: sourceTraceId,
1337
1974
  spanId: `user:${l.id ?? step}`,
1338
- parentSpanId: rootId,
1975
+ parentSpanId: sourceRootId,
1339
1976
  startTime: ts,
1340
1977
  content: text,
1341
1978
  service: SERVICE5,
@@ -1349,9 +1986,9 @@ var FactoryAdapter = class {
1349
1986
  const llmId = `llm:${l.id ?? step}`;
1350
1987
  spans.push(
1351
1988
  span({
1352
- traceId,
1989
+ traceId: sourceTraceId,
1353
1990
  spanId: llmId,
1354
- parentSpanId: rootId,
1991
+ parentSpanId: sourceRootId,
1355
1992
  name: `message.${role ?? "unknown"}`,
1356
1993
  kind: "LLM",
1357
1994
  startTime: ts,
@@ -1368,7 +2005,7 @@ var FactoryAdapter = class {
1368
2005
  for (const b of l.message.content ?? []) {
1369
2006
  if (b.type === "tool_use" && b.name) {
1370
2007
  const t = span({
1371
- traceId,
2008
+ traceId: sourceTraceId,
1372
2009
  spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
1373
2010
  parentSpanId: lastLlm,
1374
2011
  name: `tool.${b.name}`,
@@ -1378,23 +2015,48 @@ var FactoryAdapter = class {
1378
2015
  agent: SERVICE5,
1379
2016
  tool: b.name,
1380
2017
  step,
1381
- content: b.input != null ? JSON.stringify(b.input) : null
2018
+ extra: toolIoAttributes({ input: b.input })
1382
2019
  });
1383
2020
  spans.push(t);
1384
2021
  if (b.id) toolByUseId.set(b.id, t);
1385
2022
  step += 1;
1386
2023
  } else if (b.type === "tool_result" && b.tool_use_id) {
1387
2024
  const t = toolByUseId.get(b.tool_use_id);
1388
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2025
+ if (t) {
2026
+ t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2027
+ recordToolOutput(t, b.content);
2028
+ }
1389
2029
  }
1390
2030
  }
1391
2031
  }
1392
- return spans;
2032
+ const traceId = sessionLine?.id ?? sourceTraceId;
2033
+ const rootId = `root:${traceId}`;
2034
+ for (const item of spans) {
2035
+ item.trace_id = traceId;
2036
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
2037
+ }
2038
+ const root = span({
2039
+ traceId,
2040
+ spanId: rootId,
2041
+ parentSpanId: null,
2042
+ name: "session",
2043
+ kind: "AGENT",
2044
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2045
+ endTime: lastTimestamp,
2046
+ service: SERVICE5,
2047
+ agent: SERVICE5,
2048
+ model: settings.model ?? null,
2049
+ extra: settings.tokenUsage ? {
2050
+ "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
2051
+ "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
2052
+ } : void 0
2053
+ });
2054
+ return [root, ...spans];
1393
2055
  }
1394
2056
  };
1395
2057
 
1396
2058
  // src/adapters/forge.ts
1397
- import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
2059
+ import { readdir as readdir7, stat as stat6 } from "fs/promises";
1398
2060
  import { homedir as homedir6 } from "os";
1399
2061
  import { basename as basename5, join as join7 } from "path";
1400
2062
  var SERVICE6 = "forge";
@@ -1418,9 +2080,10 @@ var ForgeAdapter = class {
1418
2080
  for (const dir of dirs) {
1419
2081
  let files;
1420
2082
  try {
1421
- files = await readdir6(dir);
1422
- } catch {
1423
- continue;
2083
+ files = await readdir7(dir);
2084
+ } catch (error) {
2085
+ if (isMissingPathError(error)) continue;
2086
+ throw error;
1424
2087
  }
1425
2088
  for (const f of files) {
1426
2089
  if (!f.endsWith("-dump.json")) continue;
@@ -1428,8 +2091,9 @@ var ForgeAdapter = class {
1428
2091
  let st;
1429
2092
  try {
1430
2093
  st = await stat6(path);
1431
- } catch {
1432
- continue;
2094
+ } catch (error) {
2095
+ if (isMissingPathError(error)) continue;
2096
+ throw error;
1433
2097
  }
1434
2098
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1435
2099
  refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
@@ -1438,12 +2102,7 @@ var ForgeAdapter = class {
1438
2102
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1439
2103
  }
1440
2104
  async parse(ref) {
1441
- let dump;
1442
- try {
1443
- dump = JSON.parse(await readFile7(ref.path, "utf8"));
1444
- } catch {
1445
- return [];
1446
- }
2105
+ const dump = await readJsonFile(ref.path);
1447
2106
  const ctx = dump.context ?? dump;
1448
2107
  const traceId = ctx.conversation_id ?? ref.sessionId;
1449
2108
  const rootId = `root:${traceId}`;
@@ -1508,7 +2167,7 @@ var ForgeAdapter = class {
1508
2167
  agent: SERVICE6,
1509
2168
  tool: tc.name ?? "tool",
1510
2169
  step,
1511
- content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
2170
+ extra: toolIoAttributes({ input: tc.arguments })
1512
2171
  });
1513
2172
  spans.push(t);
1514
2173
  toolByCallId.set(id, t);
@@ -1516,7 +2175,10 @@ var ForgeAdapter = class {
1516
2175
  }
1517
2176
  } else if (entry.tool) {
1518
2177
  const t = toolByCallId.get(entry.tool.call_id ?? "");
1519
- if (t) t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2178
+ if (t) {
2179
+ t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2180
+ recordToolOutput(t, entry.tool.output?.values ?? entry.tool.output);
2181
+ }
1520
2182
  }
1521
2183
  }
1522
2184
  return spans;
@@ -1524,7 +2186,7 @@ var ForgeAdapter = class {
1524
2186
  };
1525
2187
 
1526
2188
  // src/adapters/gemini.ts
1527
- import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
2189
+ import { readdir as readdir8, stat as stat7 } from "fs/promises";
1528
2190
  import { homedir as homedir7 } from "os";
1529
2191
  import { join as join8 } from "path";
1530
2192
  function iso(ts, fallback) {
@@ -1560,10 +2222,12 @@ var GeminiFamilyAdapter = class {
1560
2222
  async loadProjectCwds() {
1561
2223
  const nameToPath = /* @__PURE__ */ new Map();
1562
2224
  try {
1563
- const raw = await readFile8(join8(homedir7(), this.homeDirName, "projects.json"), "utf8");
1564
- const projects = JSON.parse(raw).projects ?? {};
2225
+ const projects = (await readJsonFile(
2226
+ join8(homedir7(), this.homeDirName, "projects.json")
2227
+ )).projects ?? {};
1565
2228
  for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
1566
- } catch {
2229
+ } catch (error) {
2230
+ if (!isMissingJsonSource(error)) throw error;
1567
2231
  }
1568
2232
  return nameToPath;
1569
2233
  }
@@ -1571,9 +2235,10 @@ var GeminiFamilyAdapter = class {
1571
2235
  const root = this.chatsRoot();
1572
2236
  let projectDirs;
1573
2237
  try {
1574
- projectDirs = await readdir7(root);
1575
- } catch {
1576
- return [];
2238
+ projectDirs = await readdir8(root);
2239
+ } catch (error) {
2240
+ if (isMissingPathError(error)) return [];
2241
+ throw error;
1577
2242
  }
1578
2243
  const projectCwds = await this.loadProjectCwds();
1579
2244
  const refs = [];
@@ -1582,9 +2247,10 @@ var GeminiFamilyAdapter = class {
1582
2247
  const chatsDir = join8(root, pd, "chats");
1583
2248
  let files;
1584
2249
  try {
1585
- files = await readdir7(chatsDir);
1586
- } catch {
1587
- continue;
2250
+ files = await readdir8(chatsDir);
2251
+ } catch (error) {
2252
+ if (isMissingPathError(error)) continue;
2253
+ throw error;
1588
2254
  }
1589
2255
  for (const f of files) {
1590
2256
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
@@ -1592,8 +2258,9 @@ var GeminiFamilyAdapter = class {
1592
2258
  let st;
1593
2259
  try {
1594
2260
  st = await stat7(path);
1595
- } catch {
1596
- continue;
2261
+ } catch (error) {
2262
+ if (isMissingPathError(error)) continue;
2263
+ throw error;
1597
2264
  }
1598
2265
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1599
2266
  if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
@@ -1603,12 +2270,7 @@ var GeminiFamilyAdapter = class {
1603
2270
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1604
2271
  }
1605
2272
  async parse(ref) {
1606
- let session;
1607
- try {
1608
- session = JSON.parse(await readFile8(ref.path, "utf8"));
1609
- } catch {
1610
- return [];
1611
- }
2273
+ const session = await readJsonFile(ref.path);
1612
2274
  const traceId = session.sessionId ?? ref.sessionId;
1613
2275
  const messages = session.messages ?? [];
1614
2276
  const rootId = `root:${traceId}`;
@@ -1683,7 +2345,7 @@ var GeminiFamilyAdapter = class {
1683
2345
  agent: this.service,
1684
2346
  tool: tc.name ?? "tool",
1685
2347
  step,
1686
- content: tc.args != null ? JSON.stringify(tc.args) : null
2348
+ extra: toolIoAttributes({ input: tc.args, output: tc.result })
1687
2349
  })
1688
2350
  );
1689
2351
  step += 1;
@@ -1699,7 +2361,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1699
2361
  };
1700
2362
 
1701
2363
  // src/adapters/opencode.ts
1702
- import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
2364
+ import { readdir as readdir9, stat as stat8 } from "fs/promises";
1703
2365
  import { homedir as homedir8 } from "os";
1704
2366
  import { join as join9 } from "path";
1705
2367
  var SERVICE7 = "opencode";
@@ -1721,9 +2383,10 @@ var OpencodeAdapter = class {
1721
2383
  const msgRoot = join9(this.storage(), "message");
1722
2384
  let sessionDirs;
1723
2385
  try {
1724
- sessionDirs = await readdir8(msgRoot);
1725
- } catch {
1726
- return [];
2386
+ sessionDirs = await readdir9(msgRoot);
2387
+ } catch (error) {
2388
+ if (isMissingPathError(error)) return [];
2389
+ throw error;
1727
2390
  }
1728
2391
  const refs = [];
1729
2392
  for (const sid of sessionDirs) {
@@ -1731,8 +2394,9 @@ var OpencodeAdapter = class {
1731
2394
  let st;
1732
2395
  try {
1733
2396
  st = await stat8(dir);
1734
- } catch {
1735
- continue;
2397
+ } catch (error) {
2398
+ if (isMissingPathError(error)) continue;
2399
+ throw error;
1736
2400
  }
1737
2401
  if (!st.isDirectory()) continue;
1738
2402
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1745,20 +2409,12 @@ var OpencodeAdapter = class {
1745
2409
  const traceId = ref.sessionId;
1746
2410
  const rootId = `root:${traceId}`;
1747
2411
  const partRoot = join9(this.storage(), "part");
1748
- let msgFiles;
1749
- try {
1750
- msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
1751
- } catch {
1752
- return [];
1753
- }
2412
+ const msgFiles = await listJsonFiles(ref.path);
1754
2413
  const messages = [];
1755
2414
  for (const f of msgFiles) {
1756
- try {
1757
- messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
1758
- } catch {
1759
- }
2415
+ messages.push(await readJsonFile(join9(ref.path, f)));
1760
2416
  }
1761
- messages.sort((a, b) => (a.msg.time?.created ?? 0) - (b.msg.time?.created ?? 0));
2417
+ messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
1762
2418
  const spans = [
1763
2419
  span({
1764
2420
  traceId,
@@ -1766,59 +2422,65 @@ var OpencodeAdapter = class {
1766
2422
  parentSpanId: null,
1767
2423
  name: "session",
1768
2424
  kind: "AGENT",
1769
- startTime: isoOf(messages[0]?.msg.time?.created),
1770
- endTime: isoOf(messages.at(-1)?.msg.time?.completed ?? messages.at(-1)?.msg.time?.created),
2425
+ startTime: isoOf(messages[0]?.time?.created),
2426
+ endTime: isoOf(messages.at(-1)?.time?.completed ?? messages.at(-1)?.time?.created),
1771
2427
  service: SERVICE7,
1772
2428
  agent: SERVICE7
1773
2429
  })
1774
2430
  ];
1775
2431
  let step = 0;
1776
- for (const { msg } of messages) {
2432
+ for (const msg of messages) {
1777
2433
  const mid = msg.id ?? `m${step}`;
1778
2434
  const llmId = `llm:${mid}`;
1779
2435
  const pdir = join9(partRoot, mid);
1780
2436
  const parts = [];
2437
+ let partFiles = [];
1781
2438
  try {
1782
- const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1783
- for (const pf of partFiles) {
1784
- try {
1785
- parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
1786
- } catch {
1787
- }
1788
- }
1789
- } catch {
2439
+ partFiles = await listJsonFiles(pdir);
2440
+ } catch (error) {
2441
+ if (!isMissingJsonSource(error)) throw error;
2442
+ }
2443
+ for (const pf of partFiles) {
2444
+ parts.push(await readJsonFile(join9(pdir, pf)));
1790
2445
  }
1791
2446
  const turnText = textOf7(parts);
1792
- spans.push(
1793
- span({
1794
- traceId,
1795
- spanId: llmId,
1796
- parentSpanId: rootId,
1797
- name: `message.${msg.role ?? "unknown"}`,
1798
- kind: "LLM",
1799
- startTime: isoOf(msg.time?.created),
1800
- endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1801
- service: SERVICE7,
1802
- agent: SERVICE7,
1803
- model: msg.modelID ?? null,
1804
- inputTokens: msg.tokens?.input ?? null,
1805
- outputTokens: msg.tokens?.output ?? null,
1806
- step,
1807
- content: turnText || null
1808
- })
1809
- );
1810
- step += 1;
1811
- if (msg.role === "user" && turnText) {
2447
+ const isUser = msg.role === "user";
2448
+ if (isUser) {
2449
+ if (turnText) {
2450
+ spans.push(
2451
+ userPromptSpan({
2452
+ traceId,
2453
+ spanId: `${llmId}:user`,
2454
+ parentSpanId: rootId,
2455
+ startTime: isoOf(msg.time?.created),
2456
+ service: SERVICE7,
2457
+ agent: SERVICE7,
2458
+ step,
2459
+ content: turnText
2460
+ })
2461
+ );
2462
+ step += 1;
2463
+ }
2464
+ } else {
1812
2465
  spans.push(
1813
- userPromptSpan({
2466
+ span({
1814
2467
  traceId,
1815
- spanId: `${llmId}:user`,
2468
+ spanId: llmId,
1816
2469
  parentSpanId: rootId,
2470
+ name: `message.${msg.role ?? "unknown"}`,
2471
+ kind: "LLM",
1817
2472
  startTime: isoOf(msg.time?.created),
2473
+ endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1818
2474
  service: SERVICE7,
1819
2475
  agent: SERVICE7,
2476
+ model: msg.modelID ?? null,
2477
+ inputTokens: msg.tokens?.input ?? null,
2478
+ outputTokens: msg.tokens?.output ?? null,
2479
+ reasoningTokens: msg.tokens?.reasoning ?? null,
2480
+ cachedInputTokens: msg.tokens?.cache?.read ?? null,
2481
+ cacheWriteInputTokens: msg.tokens?.cache?.write ?? null,
1820
2482
  step,
1821
- content: turnText
2483
+ content: turnText || null
1822
2484
  })
1823
2485
  );
1824
2486
  step += 1;
@@ -1830,7 +2492,7 @@ var OpencodeAdapter = class {
1830
2492
  span({
1831
2493
  traceId,
1832
2494
  spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
1833
- parentSpanId: llmId,
2495
+ parentSpanId: isUser ? rootId : llmId,
1834
2496
  name: `tool.${part.tool}`,
1835
2497
  kind: "TOOL",
1836
2498
  startTime: isoOf(msg.time?.created),
@@ -1841,7 +2503,10 @@ var OpencodeAdapter = class {
1841
2503
  agent: SERVICE7,
1842
2504
  tool: part.tool,
1843
2505
  step,
1844
- content: part.state?.input != null ? JSON.stringify(part.state.input) : null
2506
+ extra: toolIoAttributes({
2507
+ input: part.state?.input,
2508
+ output: part.state?.output ?? part.state?.error
2509
+ })
1845
2510
  })
1846
2511
  );
1847
2512
  step += 1;
@@ -1852,21 +2517,10 @@ var OpencodeAdapter = class {
1852
2517
  };
1853
2518
 
1854
2519
  // src/adapters/pi.ts
1855
- import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
2520
+ import { readdir as readdir10, stat as stat9 } from "fs/promises";
1856
2521
  import { homedir as homedir9 } from "os";
1857
2522
  import { basename as basename6, join as join10 } from "path";
1858
2523
  var SERVICE8 = "pi";
1859
- function parseLines5(raw) {
1860
- const out = [];
1861
- for (const line of raw.split("\n")) {
1862
- if (!line) continue;
1863
- try {
1864
- out.push(JSON.parse(line));
1865
- } catch {
1866
- }
1867
- }
1868
- return out;
1869
- }
1870
2524
  function isToolBlock(b) {
1871
2525
  return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
1872
2526
  }
@@ -1888,18 +2542,20 @@ var PiAdapter = class {
1888
2542
  const root = this.root();
1889
2543
  let dirs;
1890
2544
  try {
1891
- dirs = await readdir9(root);
1892
- } catch {
1893
- return [];
2545
+ dirs = await readdir10(root);
2546
+ } catch (error) {
2547
+ if (isMissingPathError(error)) return [];
2548
+ throw error;
1894
2549
  }
1895
2550
  const refs = [];
1896
2551
  for (const dir of dirs) {
1897
2552
  const dp = join10(root, dir);
1898
2553
  let files;
1899
2554
  try {
1900
- files = await readdir9(dp);
1901
- } catch {
1902
- continue;
2555
+ files = await readdir10(dp);
2556
+ } catch (error) {
2557
+ if (isMissingPathError(error)) continue;
2558
+ throw error;
1903
2559
  }
1904
2560
  const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
1905
2561
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
@@ -1909,8 +2565,9 @@ var PiAdapter = class {
1909
2565
  let st;
1910
2566
  try {
1911
2567
  st = await stat9(path);
1912
- } catch {
1913
- continue;
2568
+ } catch (error) {
2569
+ if (isMissingPathError(error)) continue;
2570
+ throw error;
1914
2571
  }
1915
2572
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1916
2573
  const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
@@ -1919,27 +2576,25 @@ var PiAdapter = class {
1919
2576
  }
1920
2577
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1921
2578
  }
1922
- async parse(ref) {
1923
- const lines = parseLines5(await readFile10(ref.path, "utf8"));
1924
- const sessionLine = lines.find((l) => l.type === "session");
1925
- const traceId = sessionLine?.id ?? ref.sessionId;
1926
- const rootId = `root:${traceId}`;
1927
- const spans = [
1928
- span({
1929
- traceId,
1930
- spanId: rootId,
1931
- parentSpanId: null,
1932
- name: "session",
1933
- kind: "AGENT",
1934
- startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1935
- endTime: lines.at(-1)?.timestamp,
1936
- service: SERVICE8,
1937
- agent: SERVICE8
1938
- })
1939
- ];
2579
+ async parse(ref, options = {}) {
2580
+ const sourceTraceId = ref.sessionId;
2581
+ const sourceRootId = `root:${sourceTraceId}`;
2582
+ const spans = [];
1940
2583
  const toolByCallId = /* @__PURE__ */ new Map();
2584
+ let firstTimestamp;
2585
+ let lastTimestamp;
2586
+ let sessionLine;
2587
+ let sawLine = false;
1941
2588
  let step = 0;
1942
- for (const l of lines) {
2589
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
2590
+ if (!sawLine) {
2591
+ firstTimestamp = l.timestamp;
2592
+ sawLine = true;
2593
+ }
2594
+ lastTimestamp = l.timestamp;
2595
+ if (!sessionLine && l.type === "session") {
2596
+ sessionLine = { id: l.id, timestamp: l.timestamp };
2597
+ }
1943
2598
  if (l.type !== "message" || !l.message) continue;
1944
2599
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1945
2600
  const mid = l.id ?? `m${step}`;
@@ -1950,9 +2605,9 @@ var PiAdapter = class {
1950
2605
  if (prompt) {
1951
2606
  spans.push(
1952
2607
  userPromptSpan({
1953
- traceId,
2608
+ traceId: sourceTraceId,
1954
2609
  spanId: `${mid}:user`,
1955
- parentSpanId: rootId,
2610
+ parentSpanId: sourceRootId,
1956
2611
  startTime: ts,
1957
2612
  service: SERVICE8,
1958
2613
  agent: SERVICE8,
@@ -1966,9 +2621,9 @@ var PiAdapter = class {
1966
2621
  const errored = !!msg.errorMessage;
1967
2622
  spans.push(
1968
2623
  span({
1969
- traceId,
2624
+ traceId: sourceTraceId,
1970
2625
  spanId: llmId,
1971
- parentSpanId: rootId,
2626
+ parentSpanId: sourceRootId,
1972
2627
  name: `message.${msg.role ?? "unknown"}`,
1973
2628
  kind: "LLM",
1974
2629
  startTime: ts,
@@ -1990,7 +2645,7 @@ var PiAdapter = class {
1990
2645
  const name = b.toolName ?? b.name ?? "tool";
1991
2646
  const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
1992
2647
  const toolSpan = span({
1993
- traceId,
2648
+ traceId: sourceTraceId,
1994
2649
  spanId: `tool:${callId}`,
1995
2650
  parentSpanId: llmId,
1996
2651
  name: `tool.${name}`,
@@ -2000,7 +2655,7 @@ var PiAdapter = class {
2000
2655
  agent: SERVICE8,
2001
2656
  tool: name,
2002
2657
  step,
2003
- content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
2658
+ extra: toolIoAttributes({ input: b.input ?? b.args })
2004
2659
  });
2005
2660
  spans.push(toolSpan);
2006
2661
  toolByCallId.set(callId, toolSpan);
@@ -2012,30 +2667,37 @@ var PiAdapter = class {
2012
2667
  const err = b.isError === true || b.is_error === true;
2013
2668
  t.end_time = ts;
2014
2669
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2670
+ recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
2015
2671
  }
2016
2672
  }
2017
2673
  }
2018
2674
  }
2019
- return spans;
2675
+ const traceId = sessionLine?.id ?? sourceTraceId;
2676
+ const rootId = `root:${traceId}`;
2677
+ for (const item of spans) {
2678
+ item.trace_id = traceId;
2679
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
2680
+ }
2681
+ const root = span({
2682
+ traceId,
2683
+ spanId: rootId,
2684
+ parentSpanId: null,
2685
+ name: "session",
2686
+ kind: "AGENT",
2687
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2688
+ endTime: lastTimestamp,
2689
+ service: SERVICE8,
2690
+ agent: SERVICE8
2691
+ });
2692
+ return [root, ...spans];
2020
2693
  }
2021
2694
  };
2022
2695
 
2023
2696
  // src/adapters/qwen.ts
2024
- import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
2697
+ import { readdir as readdir11, stat as stat10 } from "fs/promises";
2025
2698
  import { homedir as homedir10 } from "os";
2026
2699
  import { basename as basename7, join as join11 } from "path";
2027
2700
  var SERVICE9 = "qwen";
2028
- function parseLines6(raw) {
2029
- const out = [];
2030
- for (const line of raw.split("\n")) {
2031
- if (!line) continue;
2032
- try {
2033
- out.push(JSON.parse(line));
2034
- } catch {
2035
- }
2036
- }
2037
- return out;
2038
- }
2039
2701
  function textOf9(parts) {
2040
2702
  if (!parts) return "";
2041
2703
  return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
@@ -2050,18 +2712,20 @@ var QwenAdapter = class {
2050
2712
  const root = this.root();
2051
2713
  let projectDirs;
2052
2714
  try {
2053
- projectDirs = await readdir10(root);
2054
- } catch {
2055
- return [];
2715
+ projectDirs = await readdir11(root);
2716
+ } catch (error) {
2717
+ if (isMissingPathError(error)) return [];
2718
+ throw error;
2056
2719
  }
2057
2720
  const refs = [];
2058
2721
  for (const pd of projectDirs) {
2059
2722
  const chatsDir = join11(root, pd, "chats");
2060
2723
  let files;
2061
2724
  try {
2062
- files = await readdir10(chatsDir);
2063
- } catch {
2064
- continue;
2725
+ files = await readdir11(chatsDir);
2726
+ } catch (error) {
2727
+ if (isMissingPathError(error)) continue;
2728
+ throw error;
2065
2729
  }
2066
2730
  if (opts.cwd) continue;
2067
2731
  for (const f of files) {
@@ -2070,8 +2734,9 @@ var QwenAdapter = class {
2070
2734
  let st;
2071
2735
  try {
2072
2736
  st = await stat10(path);
2073
- } catch {
2074
- continue;
2737
+ } catch (error) {
2738
+ if (isMissingPathError(error)) continue;
2739
+ throw error;
2075
2740
  }
2076
2741
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
2077
2742
  refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
@@ -2079,36 +2744,32 @@ var QwenAdapter = class {
2079
2744
  }
2080
2745
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
2081
2746
  }
2082
- async parse(ref) {
2083
- const records = parseLines6(await readFile11(ref.path, "utf8"));
2084
- const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
2085
- const rootId = `root:${traceId}`;
2086
- const spans = [
2087
- span({
2088
- traceId,
2089
- spanId: rootId,
2090
- parentSpanId: null,
2091
- name: "session",
2092
- kind: "AGENT",
2093
- startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2094
- endTime: records.at(-1)?.timestamp,
2095
- service: SERVICE9,
2096
- agent: SERVICE9
2097
- })
2098
- ];
2747
+ async parse(ref, options = {}) {
2748
+ const sourceTraceId = ref.sessionId;
2749
+ const sourceRootId = `root:${sourceTraceId}`;
2750
+ const spans = [];
2099
2751
  const openToolsByName = /* @__PURE__ */ new Map();
2752
+ let discoveredTraceId;
2753
+ let firstTimestamp;
2754
+ let lastTimestamp;
2755
+ let sawRecord = false;
2100
2756
  let step = 0;
2101
- let lastLlm = rootId;
2102
- for (const r of records) {
2757
+ for await (const r of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
2758
+ if (!sawRecord) {
2759
+ firstTimestamp = r.timestamp;
2760
+ sawRecord = true;
2761
+ }
2762
+ lastTimestamp = r.timestamp;
2763
+ if (!discoveredTraceId && r.sessionId) discoveredTraceId = r.sessionId;
2103
2764
  const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
2104
2765
  const role = r.message?.role;
2105
2766
  if (r.type === "assistant" || role === "model") {
2106
2767
  const llmId = `llm:${r.uuid ?? step}`;
2107
2768
  spans.push(
2108
2769
  span({
2109
- traceId,
2770
+ traceId: sourceTraceId,
2110
2771
  spanId: llmId,
2111
- parentSpanId: rootId,
2772
+ parentSpanId: sourceRootId,
2112
2773
  name: "llm.turn",
2113
2774
  kind: "LLM",
2114
2775
  startTime: ts,
@@ -2121,13 +2782,12 @@ var QwenAdapter = class {
2121
2782
  content: capText(textOf9(r.message?.parts)) || null
2122
2783
  })
2123
2784
  );
2124
- lastLlm = llmId;
2125
2785
  step += 1;
2126
2786
  for (const p of r.message?.parts ?? []) {
2127
2787
  if (!p.functionCall?.name) continue;
2128
2788
  const name = p.functionCall.name;
2129
2789
  const t = span({
2130
- traceId,
2790
+ traceId: sourceTraceId,
2131
2791
  spanId: `tool:${name}:${step}`,
2132
2792
  parentSpanId: llmId,
2133
2793
  name: `tool.${name}`,
@@ -2137,7 +2797,7 @@ var QwenAdapter = class {
2137
2797
  agent: SERVICE9,
2138
2798
  tool: name,
2139
2799
  step,
2140
- content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
2800
+ extra: toolIoAttributes({ input: p.functionCall.args })
2141
2801
  });
2142
2802
  spans.push(t);
2143
2803
  const q = openToolsByName.get(name) ?? [];
@@ -2150,9 +2810,9 @@ var QwenAdapter = class {
2150
2810
  if (prompt) {
2151
2811
  spans.push(
2152
2812
  userPromptSpan({
2153
- traceId,
2813
+ traceId: sourceTraceId,
2154
2814
  spanId: `user:${r.uuid ?? step}`,
2155
- parentSpanId: rootId,
2815
+ parentSpanId: sourceRootId,
2156
2816
  startTime: ts,
2157
2817
  service: SERVICE9,
2158
2818
  agent: SERVICE9,
@@ -2171,11 +2831,29 @@ var QwenAdapter = class {
2171
2831
  if (t) {
2172
2832
  t.end_time = ts;
2173
2833
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2834
+ recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
2174
2835
  }
2175
2836
  }
2176
2837
  }
2177
2838
  }
2178
- return spans;
2839
+ const traceId = discoveredTraceId ?? sourceTraceId;
2840
+ const rootId = `root:${traceId}`;
2841
+ for (const item of spans) {
2842
+ item.trace_id = traceId;
2843
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
2844
+ }
2845
+ const root = span({
2846
+ traceId,
2847
+ spanId: rootId,
2848
+ parentSpanId: null,
2849
+ name: "session",
2850
+ kind: "AGENT",
2851
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2852
+ endTime: lastTimestamp,
2853
+ service: SERVICE9,
2854
+ agent: SERVICE9
2855
+ });
2856
+ return [root, ...spans];
2179
2857
  }
2180
2858
  };
2181
2859
 
@@ -2575,14 +3253,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
2575
3253
  }
2576
3254
 
2577
3255
  // src/session-source.ts
2578
- async function parseSession(adapter, ref) {
2579
- const spans = await adapter.parse(ref);
3256
+ async function parseSession(adapter, ref, options = {}) {
3257
+ const spans = await adapter.parse(ref, options);
3258
+ if (spans.length === 0) throw new EmptySessionError(ref.path);
3259
+ stampSessionIntegrity(ref, spans);
3260
+ stampSessionIdentity(spans, ref.sessionId);
2580
3261
  const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
2581
3262
  if (repo.cwd) ref.cwd = repo.cwd;
2582
3263
  stampRepoAttrs(spans, repo.attrs);
2583
3264
  await stampSpanWorkdirRepoAttrs(spans);
2584
3265
  return spans;
2585
3266
  }
3267
+ var EmptySessionError = class extends Error {
3268
+ sourcePath;
3269
+ constructor(sourcePath) {
3270
+ super(`Session parser produced no spans at ${sourcePath}`);
3271
+ this.name = "EmptySessionError";
3272
+ this.sourcePath = sourcePath;
3273
+ }
3274
+ };
2586
3275
  function refKey(ref) {
2587
3276
  return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
2588
3277
  }
@@ -2592,7 +3281,7 @@ async function locateSessions(adapter, opts = {}) {
2592
3281
  const refs = await adapter.locate({ sinceMs: opts.sinceMs });
2593
3282
  const byKey = /* @__PURE__ */ new Map();
2594
3283
  for (const ref of refs) {
2595
- if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
3284
+ if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
2596
3285
  byKey.set(refKey(ref), ref);
2597
3286
  }
2598
3287
  return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -2604,7 +3293,8 @@ async function* scanSessions(opts) {
2604
3293
  try {
2605
3294
  refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
2606
3295
  } catch (err) {
2607
- opts.onError?.(err);
3296
+ if (!opts.onError) throw err;
3297
+ opts.onError(err);
2608
3298
  continue;
2609
3299
  }
2610
3300
  if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
@@ -2612,12 +3302,12 @@ async function* scanSessions(opts) {
2612
3302
  if (opts.signal?.aborted) return;
2613
3303
  let spans;
2614
3304
  try {
2615
- spans = await parseSession(adapter, ref);
3305
+ spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
2616
3306
  } catch (err) {
2617
- opts.onError?.(err, ref);
3307
+ if (!opts.onError) throw err;
3308
+ opts.onError(err, ref);
2618
3309
  continue;
2619
3310
  }
2620
- if (spans.length === 0) continue;
2621
3311
  yield { adapter, ref, spans };
2622
3312
  }
2623
3313
  }
@@ -2627,16 +3317,16 @@ async function* scanSessions(opts) {
2627
3317
  import { mkdtemp, writeFile } from "fs/promises";
2628
3318
  import { tmpdir } from "os";
2629
3319
  import { join as join13 } from "path";
3320
+ import {
3321
+ OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
3322
+ TOOL_NAME as TOOL_NAME2
3323
+ } from "@tangle-network/agent-eval/trace-attributes";
2630
3324
  function stringAttr(span2, key) {
2631
3325
  const value = span2.attributes[key];
2632
3326
  return typeof value === "string" && value.length > 0 ? value : void 0;
2633
3327
  }
2634
- function numberAttr(span2, key) {
2635
- const value = span2.attributes[key];
2636
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
2637
- }
2638
3328
  function spanKind(span2) {
2639
- return stringAttr(span2, "openinference.span.kind");
3329
+ return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
2640
3330
  }
2641
3331
  function repoFromSpans(spans) {
2642
3332
  const attrs = {};
@@ -2662,7 +3352,7 @@ function summarizeTools(spans) {
2662
3352
  const byTool = /* @__PURE__ */ new Map();
2663
3353
  for (const span2 of spans) {
2664
3354
  if (spanKind(span2) !== "TOOL") continue;
2665
- const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
3355
+ const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
2666
3356
  const current = byTool.get(name) ?? { calls: 0, errors: 0 };
2667
3357
  current.calls += 1;
2668
3358
  if (span2.status.code === "ERROR") current.errors += 1;
@@ -2671,7 +3361,6 @@ function summarizeTools(spans) {
2671
3361
  return [...byTool.entries()].map(([name, row]) => ({ name, calls: row.calls, errors: row.errors })).sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name));
2672
3362
  }
2673
3363
  async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2674
- const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
2675
3364
  const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
2676
3365
  const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
2677
3366
  const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
@@ -2679,10 +3368,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2679
3368
  const loopFindings = pipelines.stuckLoops.findings;
2680
3369
  const { firstSpanAt, lastSpanAt } = timeBounds(spans);
2681
3370
  const repo = repoFromSpans(spans);
3371
+ const execution = summarizeSpanExecution(spans, {
3372
+ experimentId: `traces-policy-evidence:${ref.sessionId}`
3373
+ });
2682
3374
  return {
2683
3375
  schemaVersion: 1,
2684
3376
  kind: "traces.policy_evidence.session",
2685
3377
  generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3378
+ execution,
2686
3379
  session: {
2687
3380
  harness: ref.harness,
2688
3381
  sessionId: ref.sessionId,
@@ -2693,12 +3386,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2693
3386
  repo,
2694
3387
  metrics: {
2695
3388
  spanCount: spans.length,
2696
- llmTurnCount: llmSpans.length,
3389
+ llmTurnCount: execution.execution.modelCalls.events,
2697
3390
  toolCallCount: toolSpans.length,
2698
3391
  erroredToolCallCount,
2699
- inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
2700
- outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
2701
- models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
3392
+ inputTokens: execution.execution.tokenUsage.totals.input,
3393
+ outputTokens: execution.execution.tokenUsage.totals.output,
3394
+ models: execution.execution.models.map(({ model }) => model),
2702
3395
  tools: summarizeTools(spans),
2703
3396
  firstSpanAt,
2704
3397
  lastSpanAt
@@ -2880,6 +3573,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2880
3573
  const report = { redactionCount: 0, byRule: {} };
2881
3574
  const out = spans.map((s) => {
2882
3575
  const attributes = redactValue(s.attributes, rules, report).value;
3576
+ normalizeToolIoAttributes(attributes);
2883
3577
  let status = s.status;
2884
3578
  if (status.message) {
2885
3579
  const m = redactValue(status.message, rules, report).value;
@@ -2890,33 +3584,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2890
3584
  return { spans: out, report };
2891
3585
  }
2892
3586
  async function applyRedactor(spans, redactor) {
2893
- const idx = [];
3587
+ const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
3588
+ const fields = [];
2894
3589
  const texts = [];
2895
3590
  spans.forEach((s, i) => {
2896
- const c = s.attributes["content"];
2897
- if (typeof c === "string" && c.length > 0) {
2898
- idx.push(i);
2899
- texts.push(c);
3591
+ for (const key of capturedKeys) {
3592
+ const value = s.attributes[key];
3593
+ if (typeof value === "string" && value.length > 0) {
3594
+ fields.push({ spanIndex: i, key });
3595
+ texts.push(value);
3596
+ }
3597
+ }
3598
+ if (s.status.message) {
3599
+ fields.push({ spanIndex: i, key: "status.message" });
3600
+ texts.push(s.status.message);
2900
3601
  }
2901
3602
  });
2902
3603
  const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
2903
- if (texts.length === 0) return { spans: out, changed: 0 };
3604
+ if (texts.length === 0) {
3605
+ for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
3606
+ return { spans: out, changed: 0 };
3607
+ }
2904
3608
  const scrubbed = await redactor.redactText(texts);
3609
+ if (scrubbed.length !== texts.length) {
3610
+ throw new Error(`redactor ${redactor.name}: expected ${texts.length} values, received ${scrubbed.length}`);
3611
+ }
2905
3612
  let changed = 0;
2906
- idx.forEach((spanI, k) => {
3613
+ fields.forEach(({ spanIndex, key }, k) => {
2907
3614
  if (scrubbed[k] !== texts[k]) {
2908
- out[spanI].attributes["content"] = scrubbed[k];
3615
+ if (key === "status.message") {
3616
+ out[spanIndex].status = { ...out[spanIndex].status, message: scrubbed[k] };
3617
+ } else {
3618
+ out[spanIndex].attributes[key] = scrubbed[k];
3619
+ }
2909
3620
  changed += 1;
2910
3621
  }
2911
3622
  });
3623
+ for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
2912
3624
  return { spans: out, changed };
2913
3625
  }
2914
3626
 
2915
3627
  // src/file-export.ts
2916
- import { createHash } from "crypto";
2917
- import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
3628
+ import { createHash as createHash4 } from "crypto";
3629
+ import { mkdtemp as mkdtemp2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
2918
3630
  import { tmpdir as tmpdir2 } from "os";
2919
3631
  import { join as join14 } from "path";
3632
+ import {
3633
+ firstNumberAttr,
3634
+ LLM_CACHED_TOKEN_ATTR_KEYS,
3635
+ LLM_CACHED_TOKENS,
3636
+ LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
3637
+ LLM_CACHE_WRITE_TOKENS,
3638
+ LLM_COST_ATTR_KEYS,
3639
+ LLM_COST_USD,
3640
+ LLM_INPUT_TOKEN_ATTR_KEYS,
3641
+ LLM_INPUT_TOKENS,
3642
+ LLM_OUTPUT_TOKEN_ATTR_KEYS,
3643
+ LLM_OUTPUT_TOKENS,
3644
+ LLM_REASONING_TOKEN_ATTR_KEYS,
3645
+ LLM_REASONING_TOKENS
3646
+ } from "@tangle-network/agent-eval/trace-attributes";
2920
3647
  function isObject(value) {
2921
3648
  return value != null && typeof value === "object" && !Array.isArray(value);
2922
3649
  }
@@ -2936,7 +3663,7 @@ function stableJson(value) {
2936
3663
  });
2937
3664
  }
2938
3665
  function hashId(value, chars = 16) {
2939
- return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
3666
+ return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
2940
3667
  }
2941
3668
  function isoTime(value) {
2942
3669
  if (typeof value === "string") {
@@ -3039,6 +3766,11 @@ function requireOpenInferenceRows(rows) {
3039
3766
  return rows;
3040
3767
  }
3041
3768
  function requireIntelligenceSpanRows(rows) {
3769
+ if (rows.length > 0 && rows.every(isOpenInferenceRow)) {
3770
+ throw new Error(
3771
+ "intelligence-spans input is already complete OpenInference; use --format auto or --format openinference to preserve it losslessly"
3772
+ );
3773
+ }
3042
3774
  if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
3043
3775
  return rows;
3044
3776
  }
@@ -3070,6 +3802,7 @@ function policyEvidenceToSpans(records) {
3070
3802
  "traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
3071
3803
  "traces.metrics.input_tokens": record.metrics.inputTokens,
3072
3804
  "traces.metrics.output_tokens": record.metrics.outputTokens,
3805
+ "traces.execution": stableJson(record.execution),
3073
3806
  "traces.metrics.models": record.metrics.models,
3074
3807
  "traces.metrics.tools": stableJson(record.metrics.tools),
3075
3808
  "traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
@@ -3097,6 +3830,11 @@ function policyEvidenceToSpans(records) {
3097
3830
  endTime: end,
3098
3831
  service: record.session.harness,
3099
3832
  agent: "traces",
3833
+ inputTokens: record.execution.execution.tokenUsage.totals.input,
3834
+ outputTokens: record.execution.execution.tokenUsage.totals.output,
3835
+ reasoningTokens: record.execution.execution.tokenUsage.totals.reasoning,
3836
+ cachedInputTokens: record.execution.execution.tokenUsage.totals.cached,
3837
+ cacheWriteInputTokens: record.execution.execution.tokenUsage.totals.cacheWrite,
3100
3838
  content: capText(stableJson({
3101
3839
  session: record.session,
3102
3840
  repo: record.repo,
@@ -3127,6 +3865,16 @@ function findStringKey(value, keys, depth = 4) {
3127
3865
  }
3128
3866
  return void 0;
3129
3867
  }
3868
+ function eventValue(row, keys) {
3869
+ const scopes = [row, objectValue(row.data), objectValue(row.payload)];
3870
+ for (const scope of scopes) {
3871
+ if (!scope) continue;
3872
+ for (const key of keys) {
3873
+ if (scope[key] !== void 0) return scope[key];
3874
+ }
3875
+ }
3876
+ return void 0;
3877
+ }
3130
3878
  function eventTimestamp(row) {
3131
3879
  return firstTimeDeep(row, [
3132
3880
  "timestamp",
@@ -3169,7 +3917,8 @@ function eventKind(row, type) {
3169
3917
  }
3170
3918
  function sandboxEventsToSpans(rows, wrapper) {
3171
3919
  const context = wrapper ?? {};
3172
- const traceId = findStringKey(context, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 3) ?? findStringKey(rows, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
3920
+ const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
3921
+ const traceId = findStringKey(context, ["trace_id", "traceId"], 3) ?? findStringKey(rows, ["trace_id", "traceId"], 4) ?? sessionId ?? findStringKey(context, ["run_id", "runId"], 3) ?? findStringKey(rows, ["run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
3173
3922
  const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
3174
3923
  const times = rows.flatMap((row) => {
3175
3924
  const time = eventTimestamp(row);
@@ -3205,10 +3954,16 @@ function sandboxEventsToSpans(rows, wrapper) {
3205
3954
  const { status, message } = eventStatus(row, type);
3206
3955
  const { kind, tool } = eventKind(row, type);
3207
3956
  const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
3957
+ const toolInput = kind === "TOOL" ? eventValue(row, ["toolInput", "tool_input", "input", "args", "arguments", "full_command"]) : void 0;
3208
3958
  const extra = {
3209
3959
  "traces.source_format": "sandbox-events",
3210
3960
  "traces.event.type": type,
3211
- "traces.event.index": index
3961
+ "traces.event.index": index,
3962
+ ...kind === "TOOL" ? toolIoAttributes({
3963
+ input: toolInput,
3964
+ output: eventValue(row, ["toolOutput", "tool_output", "output", "result"]),
3965
+ argsCaptured: toolInput !== void 0
3966
+ }) : {}
3212
3967
  };
3213
3968
  copyDefined(extra, {
3214
3969
  "traces.event.id": stringValue(row.id),
@@ -3233,7 +3988,11 @@ function sandboxEventsToSpans(rows, wrapper) {
3233
3988
  extra
3234
3989
  }));
3235
3990
  });
3236
- return spans;
3991
+ if (!sessionId) return spans;
3992
+ return spans.map((item) => ({
3993
+ ...item,
3994
+ attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
3995
+ }));
3237
3996
  }
3238
3997
  function statusCode(value) {
3239
3998
  return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
@@ -3257,8 +4016,8 @@ function unixNanoTime(value) {
3257
4016
  }
3258
4017
  function firstNumber(row, attrs, keys) {
3259
4018
  for (const key of keys) {
3260
- const value = numberValue(row[key]) ?? numberValue(attrs[key]);
3261
- if (value != null) return value;
4019
+ const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
4020
+ if (value !== null) return value;
3262
4021
  }
3263
4022
  return void 0;
3264
4023
  }
@@ -3270,6 +4029,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
3270
4029
  "tool.name",
3271
4030
  "llm.input_tokens",
3272
4031
  "llm.output_tokens",
4032
+ LLM_INPUT_TOKENS,
4033
+ LLM_OUTPUT_TOKENS,
4034
+ LLM_REASONING_TOKENS,
4035
+ LLM_CACHED_TOKENS,
4036
+ LLM_CACHE_WRITE_TOKENS,
4037
+ LLM_COST_USD,
3273
4038
  "step",
3274
4039
  "content"
3275
4040
  ]);
@@ -3284,10 +4049,27 @@ function preserveRawAttributes(attrs) {
3284
4049
  }
3285
4050
  return preserved;
3286
4051
  }
4052
+ function capturedToolIo(attrs) {
4053
+ const input = attrs["input.value"] ?? attrs["tool.input"] ?? attrs.tool_input ?? attrs["tool.arguments"] ?? attrs.tool_arguments ?? attrs.arguments ?? attrs.args ?? attrs.full_command;
4054
+ const capture = toolArgumentsFromAttributes({ ...attrs, "input.value": input });
4055
+ return toolIoAttributes({
4056
+ input: capture.args,
4057
+ output: attrs["output.value"] ?? attrs["tool.output"] ?? attrs.tool_output,
4058
+ argsCaptured: capture.argsCaptured
4059
+ });
4060
+ }
4061
+ function isToolTelemetrySpan(attrs) {
4062
+ const spanType = stringValue(attrs["span.type"])?.toLowerCase();
4063
+ return spanType === "tool.execution" || spanType === "tool.blocked_on_user";
4064
+ }
4065
+ function otlpSpanKind(value) {
4066
+ return value === "AGENT" || value === "LLM" || value === "TOOL" || value === "CHAIN" || value === "SPAN" ? value : void 0;
4067
+ }
3287
4068
  function intelligenceSpanKind(name, attrs) {
3288
4069
  const lowered = name.toLowerCase();
3289
4070
  const spanType = stringValue(attrs["span.type"])?.toLowerCase();
3290
4071
  if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
4072
+ if (isToolTelemetrySpan(attrs)) return "CHAIN";
3291
4073
  if (spanType === "tool" || lowered.includes("tool") || attrs["tool.name"] != null || attrs.tool_name != null || attrs["mcp.tool.name"] != null || attrs["tool.input"] != null || attrs["tool.output"] != null) {
3292
4074
  return "TOOL";
3293
4075
  }
@@ -3302,9 +4084,12 @@ function intelligenceSpansToSpans(rows) {
3302
4084
  const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
3303
4085
  const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
3304
4086
  const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
3305
- const sessionId = stringValue(row.session_id) ?? stringValue(attrs["session.id"]);
4087
+ const kind = intelligenceSpanKind(name, attrs);
4088
+ const sourceStatus = objectValue(row.status);
4089
+ const sessionId = stringValue(row.session_id) ?? sessionIdFromAttributes(attrs);
3306
4090
  const extra = {
3307
4091
  ...preserveRawAttributes(attrs),
4092
+ ...kind === "TOOL" ? capturedToolIo(attrs) : {},
3308
4093
  "traces.source_format": "intelligence-spans",
3309
4094
  "traces.row.index": index
3310
4095
  };
@@ -3318,7 +4103,6 @@ function intelligenceSpansToSpans(rows) {
3318
4103
  "generation.id": stringValue(row.generation),
3319
4104
  "cell.id": stringValue(row.cell_id),
3320
4105
  "project.key": stringValue(row.project_key),
3321
- "llm.cost_usd": numberValue(row.cost_usd),
3322
4106
  "redaction.version": stringValue(row.redaction_version),
3323
4107
  "traces.received_at": isoTime(row.received_at)
3324
4108
  });
@@ -3327,17 +4111,21 @@ function intelligenceSpansToSpans(rows) {
3327
4111
  spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
3328
4112
  parentSpanId: stringValue(row.parent_span_id) ?? null,
3329
4113
  name,
3330
- kind: intelligenceSpanKind(name, attrs),
4114
+ kind,
3331
4115
  startTime,
3332
4116
  endTime,
3333
- status: statusCode(row.status_code),
3334
- statusMessage: stringValue(row.status_message),
4117
+ status: statusCode(row.status_code ?? sourceStatus?.code),
4118
+ statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
3335
4119
  service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
3336
4120
  agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
3337
4121
  model,
3338
4122
  tool,
3339
- inputTokens: firstNumber(row, attrs, ["input_tokens", "llm.input_tokens"]),
3340
- outputTokens: firstNumber(row, attrs, ["output_tokens", "llm.output_tokens"]),
4123
+ inputTokens: firstNumber(row, attrs, ["input_tokens", ...LLM_INPUT_TOKEN_ATTR_KEYS]),
4124
+ outputTokens: firstNumber(row, attrs, ["output_tokens", ...LLM_OUTPUT_TOKEN_ATTR_KEYS]),
4125
+ reasoningTokens: firstNumber(row, attrs, LLM_REASONING_TOKEN_ATTR_KEYS),
4126
+ cachedInputTokens: firstNumber(row, attrs, LLM_CACHED_TOKEN_ATTR_KEYS),
4127
+ cacheWriteInputTokens: firstNumber(row, attrs, LLM_CACHE_WRITE_TOKEN_ATTR_KEYS),
4128
+ costUsd: firstNumber(row, attrs, ["cost_usd", ...LLM_COST_ATTR_KEYS]),
3341
4129
  step: numberValue(attrs["interaction.sequence"]) ?? index,
3342
4130
  content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
3343
4131
  extra
@@ -3350,15 +4138,21 @@ function openInferenceToSpans(rows) {
3350
4138
  const resource = objectValue(row.resource);
3351
4139
  const resourceAttrs = objectValue(resource?.attributes);
3352
4140
  if (resourceAttrs) copyDefined(attrs, resourceAttrs);
3353
- const kind = stringValue(row.kind);
3354
- if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
4141
+ const name = stringValue(row.name) ?? "span";
4142
+ const declaredKind = otlpSpanKind(attrs["openinference.span.kind"]) ?? otlpSpanKind(row.kind);
4143
+ const kind = isToolTelemetrySpan(attrs) ? "CHAIN" : declaredKind ?? intelligenceSpanKind(name, attrs);
4144
+ if (declaredKind && declaredKind !== kind) {
4145
+ attrs["traces.raw_attribute.openinference.span.kind"] = declaredKind;
4146
+ }
4147
+ attrs["openinference.span.kind"] = kind;
4148
+ if (kind === "TOOL") Object.assign(attrs, capturedToolIo(attrs));
3355
4149
  const status = objectValue(row.status);
3356
4150
  const message = stringValue(status?.message);
3357
4151
  return {
3358
4152
  trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
3359
4153
  span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
3360
4154
  parent_span_id: stringValue(row.parent_span_id) || null,
3361
- name: stringValue(row.name) ?? "span",
4155
+ name,
3362
4156
  start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3363
4157
  end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3364
4158
  status: { code: statusCode(status?.code), ...message ? { message } : {} },
@@ -3405,7 +4199,40 @@ function exportTraceEvidenceText(text, opts = {}) {
3405
4199
  return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
3406
4200
  }
3407
4201
  async function exportTraceEvidenceFile(inputPath, opts = {}) {
3408
- const text = await readFile12(inputPath, "utf8");
4202
+ if (/\.(?:jsonl|ndjson)$/i.test(inputPath)) {
4203
+ let format;
4204
+ const spans = [];
4205
+ const sandboxRows = [];
4206
+ let redactionCount = 0;
4207
+ const redactionsByRule = {};
4208
+ for await (const row of readJsonl(inputPath)) {
4209
+ format ??= detectFormat([row], opts.format ?? "auto");
4210
+ if (format === "sandbox-events") {
4211
+ sandboxRows.push(row);
4212
+ continue;
4213
+ }
4214
+ const converted = exportTraceEvidenceRows([row], {
4215
+ ...opts,
4216
+ sourcePath: inputPath,
4217
+ format
4218
+ });
4219
+ spans.push(...converted.spans);
4220
+ redactionCount += converted.redactionCount;
4221
+ for (const [rule, count2] of Object.entries(converted.redactionsByRule)) {
4222
+ redactionsByRule[rule] = (redactionsByRule[rule] ?? 0) + count2;
4223
+ }
4224
+ }
4225
+ if (!format) throw new Error("input file is empty");
4226
+ if (format === "sandbox-events") {
4227
+ return exportTraceEvidenceRows(sandboxRows, {
4228
+ ...opts,
4229
+ sourcePath: inputPath,
4230
+ format
4231
+ });
4232
+ }
4233
+ return { format, spans, redactionCount, redactionsByRule };
4234
+ }
4235
+ const text = await readFile3(inputPath, "utf8");
3409
4236
  return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
3410
4237
  }
3411
4238
  async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
@@ -3416,7 +4243,7 @@ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
3416
4243
  }
3417
4244
 
3418
4245
  // src/inspect.ts
3419
- import { mkdtemp as mkdtemp3, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
4246
+ import { mkdtemp as mkdtemp3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
3420
4247
  import { tmpdir as tmpdir3 } from "os";
3421
4248
  import { basename as basename8, join as join15 } from "path";
3422
4249
  var severityRank = {
@@ -3695,14 +4522,14 @@ function assertSessionIndex(value) {
3695
4522
  }
3696
4523
  const candidate = value;
3697
4524
  if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
3698
- throw new Error("input is not a traces.session_index v1 file");
4525
+ throw new Error("input is not a traces.session_index file with schemaVersion 1");
3699
4526
  }
3700
4527
  if (!candidate.totals || typeof candidate.totals !== "object") {
3701
4528
  throw new Error("session index is missing totals");
3702
4529
  }
3703
4530
  }
3704
4531
  async function readSessionIndexFile(path) {
3705
- const parsed = JSON.parse(await readFile13(path, "utf8"));
4532
+ const parsed = JSON.parse(await readFile4(path, "utf8"));
3706
4533
  assertSessionIndex(parsed);
3707
4534
  return parsed;
3708
4535
  }
@@ -3800,6 +4627,8 @@ function analyzeReactions(spans) {
3800
4627
 
3801
4628
  // src/report.ts
3802
4629
  var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
4630
+ var CORRUPTION_RECEIPT_DISPLAY_LIMIT = 100;
4631
+ var SOURCE_DISPLAY_LIMIT = 20;
3803
4632
  var SEVERITY_BADGE = {
3804
4633
  critical: "\u{1F534} CRITICAL",
3805
4634
  high: "\u{1F7E0} HIGH",
@@ -3811,23 +4640,105 @@ function deterministicSummaryText(summary) {
3811
4640
  const parts = [];
3812
4641
  if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
3813
4642
  if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
3814
- if (summary.toolErrorRuns > 0) parts.push(`${summary.toolErrorRuns} tool-error run(s)`);
4643
+ if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
3815
4644
  if (parts.length === 0) return "0 deterministic signals";
3816
4645
  if (parts.length === 1) return parts[0];
3817
4646
  return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
3818
4647
  }
3819
- function plural(count, singular, pluralForm = `${singular}s`) {
3820
- return count === 1 ? singular : pluralForm;
4648
+ function plural(count2, singular, pluralForm = `${singular}s`) {
4649
+ return count2 === 1 ? singular : pluralForm;
4650
+ }
4651
+ function tableCell(value) {
4652
+ return value.replace(/\s+/g, " ").replaceAll("|", "\\|").trim();
4653
+ }
4654
+ function count(value) {
4655
+ return Math.round(value).toLocaleString("en-US");
4656
+ }
4657
+ function decimal(value, digits = 1) {
4658
+ return value.toLocaleString("en-US", {
4659
+ minimumFractionDigits: digits,
4660
+ maximumFractionDigits: digits
4661
+ });
4662
+ }
4663
+ function distributionRow(label, distribution, unit = "") {
4664
+ if (distribution.n === 0) return `| ${label} | 0 | not captured | not captured | not captured | not captured | not captured |`;
4665
+ const value = (number) => `${decimal(number)}${unit}`;
4666
+ return `| ${label} | ${count(distribution.n)} | ${value(distribution.min)} | ${value(distribution.mean)} | ${value(distribution.p50)} | ${value(distribution.p95)} | ${value(distribution.max)} |`;
4667
+ }
4668
+ function tokenRows(usage) {
4669
+ const rows = [
4670
+ ["Input", usage.totals.input, usage.input],
4671
+ ["Output", usage.totals.output, usage.output],
4672
+ ["Reasoning (output subset)", usage.totals.reasoning, usage.reasoning],
4673
+ ["Cache read", usage.totals.cached, usage.cached],
4674
+ ["Cache write", usage.totals.cacheWrite, usage.cacheWrite]
4675
+ ];
4676
+ return rows.map(
4677
+ ([label, total, values]) => `| ${label} | ${count(total)} | ${count(values.n)} | ${decimal(values.mean)} | ${decimal(values.p50)} | ${decimal(values.p95)} | ${count(values.max)} |`
4678
+ );
4679
+ }
4680
+ function renderExecution(report) {
4681
+ const execution = report.execution;
4682
+ const lines = ["## execution facts", ""];
4683
+ lines.push(
4684
+ `- **Runs:** ${count(execution.durationMs.n)} | **Model-call runs:** ${count(execution.modelCalls.runs)} | **Model-call events:** ${count(execution.modelCalls.events)} | **Failed runs:** ${count(execution.failures.runs)}/${count(execution.failures.reportingRuns)} (${decimal(execution.failures.fraction * 100, 2)}%)`
4685
+ );
4686
+ lines.push("- **Task quality:** not measured; these traces do not include comparable task outcome labels.");
4687
+ lines.push("");
4688
+ lines.push("| Time | Runs measured | Min | Mean | p50 | p95 | Max |");
4689
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4690
+ lines.push(distributionRow("Duration", execution.durationMs, "ms"));
4691
+ lines.push(distributionRow("Queue", execution.queueMs, "ms"));
4692
+ lines.push("");
4693
+ lines.push("### Direct model usage");
4694
+ lines.push("");
4695
+ lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
4696
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4697
+ lines.push(...tokenRows(execution.tokenUsage));
4698
+ lines.push("");
4699
+ lines.push("| Model | Runs |");
4700
+ lines.push("|---|---:|");
4701
+ for (const model of execution.models) {
4702
+ lines.push(`| \`${tableCell(model.model)}\` | ${count(model.runs)} |`);
4703
+ }
4704
+ lines.push("");
4705
+ lines.push("### Cost coverage");
4706
+ lines.push("");
4707
+ lines.push("| Source | Runs | Total USD |");
4708
+ lines.push("|---|---:|---:|");
4709
+ lines.push(`| Observed | ${count(report.costProvenance.observed.n)} | $${report.costProvenance.observed.totalUsd.toFixed(6)} |`);
4710
+ lines.push(`| Estimated | ${count(report.costProvenance.estimated.n)} | $${report.costProvenance.estimated.totalUsd.toFixed(6)} |`);
4711
+ lines.push(`| Uncaptured | ${count(report.costProvenance.uncaptured.n)} | not available |`);
4712
+ lines.push("");
4713
+ lines.push(`Known cost coverage: ${decimal(report.costProvenance.knownFraction * 100, 2)}%.`);
4714
+ lines.push("");
4715
+ if (execution.aggregateUsage.runs > 0) {
4716
+ lines.push("### Orchestration-reported usage");
4717
+ lines.push("");
4718
+ lines.push(
4719
+ `Kept separate from direct model usage because orchestration totals can overlap model-call telemetry. ${count(execution.aggregateUsage.runs)} run(s) reported aggregate values.`
4720
+ );
4721
+ lines.push("");
4722
+ lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
4723
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4724
+ lines.push(...tokenRows(execution.aggregateUsage.tokenUsage));
4725
+ lines.push("");
4726
+ lines.push(
4727
+ `Aggregate cost: $${execution.aggregateUsage.totalCostUsd.toFixed(6)} across ${count(execution.aggregateUsage.costUsd.n)} reporting run(s).`
4728
+ );
4729
+ lines.push("");
4730
+ }
4731
+ return lines.join("\n");
3821
4732
  }
3822
4733
  function summarizeDeterministicSignals(pipelines, reactions) {
3823
4734
  const stuckLoops = pipelines.stuckLoops.findings.length;
3824
- const reactionSignals = Object.values(reactions.signals).reduce((total, count) => total + count, 0);
3825
- const toolErrorRuns2 = pipelines.toolUse.filter((run) => run.errorRate > 0).length;
4735
+ const reactionSignals = Object.values(reactions.signals).reduce((total, count2) => total + count2, 0);
4736
+ const failedRuns = pipelines.failureClusters.totalFailures;
3826
4737
  return {
3827
4738
  stuckLoops,
3828
4739
  reactionSignals,
3829
- toolErrorRuns: toolErrorRuns2,
3830
- totalSignals: stuckLoops + reactionSignals + toolErrorRuns2
4740
+ failedRuns,
4741
+ totalSignals: stuckLoops + reactionSignals + failedRuns
3831
4742
  };
3832
4743
  }
3833
4744
  function renderReport(result, meta) {
@@ -3835,14 +4746,93 @@ function renderReport(result, meta) {
3835
4746
  const findings = [...result.findings].sort(
3836
4747
  (a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
3837
4748
  );
3838
- const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, toolErrorRuns: 0, totalSignals: 0 };
3839
- const findingSummary = deterministic.totalSignals > 0 ? `${findings.length} analyst ${plural(findings.length, "finding")} + ${deterministic.totalSignals} deterministic ${plural(deterministic.totalSignals, "signal")}` : `${findings.length} ${plural(findings.length, "finding")}`;
4749
+ const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, failedRuns: 0, totalSignals: 0 };
4750
+ const findingSummary = deterministic.totalSignals > 0 ? `${findings.length} ${plural(findings.length, "finding")}; ${deterministic.totalSignals} raw deterministic ${plural(deterministic.totalSignals, "signal")}` : `${findings.length} ${plural(findings.length, "finding")}`;
3840
4751
  lines.push(`# Trace analysis \u2014 ${meta.harness}`);
3841
4752
  lines.push("");
4753
+ const sessionSummary = meta.unassignedTraceCount ? `${meta.sessionCount} identified session(s) + ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} without a single stable session identity` : `${meta.sessionCount} session(s)`;
3842
4754
  lines.push(
3843
- `${meta.sessionCount} session(s), ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Cost: $${result.total_cost_usd.toFixed(4)}.`
4755
+ `${sessionSummary}, ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Analysis cost: $${result.total_cost_usd.toFixed(4)}.`
3844
4756
  );
3845
4757
  lines.push("");
4758
+ if (meta.unassignedTraceCount) {
4759
+ lines.push(
4760
+ `> ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} lacked a single stable session identity. Their spans were analyzed but were not relabeled as sessions.`
4761
+ );
4762
+ lines.push("");
4763
+ }
4764
+ lines.push(renderExecution(meta.execution));
4765
+ lines.push("");
4766
+ if (meta.sources && meta.sources.length > 0) {
4767
+ lines.push("## Selected sessions");
4768
+ lines.push("");
4769
+ lines.push("| Role | Session ID | Parent session | Integrity | Subject (first prompt line) | Source path |");
4770
+ lines.push("|---|---|---|---|---|---|");
4771
+ for (const source of meta.sources.slice(0, SOURCE_DISPLAY_LIMIT)) {
4772
+ const corruptionCount = source.corruptionCount ?? source.corruptions?.length ?? 0;
4773
+ const integrity = source.integrity === "degraded_not_lossless" ? `degraded, not lossless (${corruptionCount} corrupt ${plural(corruptionCount, "record")})` : "complete";
4774
+ lines.push(
4775
+ `| ${source.role} | \`${tableCell(source.sessionId)}\` | ${source.parentSessionId ? `\`${tableCell(source.parentSessionId)}\`` : "\u2014"} | ${integrity} | ${tableCell(source.subject) || "(no prompt captured)"} | \`${tableCell(source.path)}\` |`
4776
+ );
4777
+ }
4778
+ if (meta.sources.length > SOURCE_DISPLAY_LIMIT) {
4779
+ lines.push(
4780
+ `| \u2026 | ${meta.sources.length - SOURCE_DISPLAY_LIMIT} additional sessions omitted | \u2014 | \u2014 | \u2014 | \u2014 |`
4781
+ );
4782
+ }
4783
+ lines.push("");
4784
+ const childCount = meta.sources.filter((source) => source.role === "child").length;
4785
+ if (childCount > 0) {
4786
+ lines.push(
4787
+ `> Scope: ${childCount}/${meta.sources.length} selected session(s) are children. Counts below describe only the selected files, not their parent operator sessions.`
4788
+ );
4789
+ lines.push("");
4790
+ }
4791
+ const totalCorruptionCount = meta.sources.reduce(
4792
+ (total, source) => total + (source.corruptionCount ?? source.corruptions?.length ?? 0),
4793
+ 0
4794
+ );
4795
+ const corruptions = [];
4796
+ for (const source of meta.sources) {
4797
+ for (const receipt of source.corruptions ?? []) {
4798
+ if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
4799
+ corruptions.push(receipt);
4800
+ }
4801
+ if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
4802
+ }
4803
+ if (totalCorruptionCount > 0) {
4804
+ lines.push("## Source corruption receipts");
4805
+ lines.push("");
4806
+ lines.push(
4807
+ "> Degraded, not lossless: every valid JSONL record was analyzed. Malformed content is fingerprinted, not retained; exact bytes are retrievable only while the local source file still contains that byte range."
4808
+ );
4809
+ lines.push("");
4810
+ for (const source of meta.sources) {
4811
+ const count2 = source.corruptionCount ?? source.corruptions?.length ?? 0;
4812
+ if (count2 === 0) continue;
4813
+ const digest = source.corruptionDigest ? `, digest \`${source.corruptionDigest}\`` : "";
4814
+ lines.push(`- Session \`${tableCell(source.sessionId)}\`: ${count2} ${plural(count2, "receipt")}${digest}.`);
4815
+ }
4816
+ lines.push("");
4817
+ if (corruptions.length > 0) {
4818
+ lines.push("| Session ID | Source path | Line | Byte offset | Byte length | SHA-256 | Raw bytes |");
4819
+ lines.push("|---|---|---:|---:|---:|---|---|");
4820
+ for (const receipt of corruptions) {
4821
+ lines.push(
4822
+ `| \`${tableCell(receipt.sessionId)}\` | \`${tableCell(receipt.sourcePath)}\` | ${receipt.lineNumber} | ${receipt.byteOffset} | ${receipt.byteLength} | \`${receipt.sha256}\` | local source only |`
4823
+ );
4824
+ }
4825
+ lines.push("");
4826
+ }
4827
+ const omitted = totalCorruptionCount - corruptions.length;
4828
+ if (omitted > 0) {
4829
+ lines.push(
4830
+ `_${omitted} additional ${plural(omitted, "receipt")} omitted from this report; all receipts remain in \`source.corruption.receipt\` child spans._`
4831
+ );
4832
+ lines.push("");
4833
+ }
4834
+ }
4835
+ }
3846
4836
  lines.push("| Analyst | Status | Findings | Latency |");
3847
4837
  lines.push("|---|---|---|---|");
3848
4838
  for (const s of result.per_analyst) {
@@ -3850,7 +4840,7 @@ function renderReport(result, meta) {
3850
4840
  }
3851
4841
  lines.push("");
3852
4842
  if (findings.length === 0 && deterministic.totalSignals === 0) {
3853
- lines.push("_No findings \u2014 no behavioral inefficiencies or failure modes detected._");
4843
+ lines.push("_No supported behavioral findings in the captured fields._");
3854
4844
  } else if (findings.length === 0) {
3855
4845
  lines.push(
3856
4846
  `_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
@@ -3871,6 +4861,7 @@ function renderReport(result, meta) {
3871
4861
  if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
3872
4862
  lines.push(`- **Confidence:** ${f.confidence}`);
3873
4863
  if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
4864
+ if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
3874
4865
  for (const ev of f.evidence_refs.slice(0, 3)) {
3875
4866
  const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
3876
4867
  lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
@@ -3885,20 +4876,79 @@ function renderReport(result, meta) {
3885
4876
  return lines.join("\n");
3886
4877
  }
3887
4878
  function renderPipelines(pr) {
3888
- const lines = ["## loops & waste (deterministic)", ""];
4879
+ const lines = ["## reliability, loops & waste (deterministic)", ""];
4880
+ lines.push(
4881
+ `- **Execution failures:** ${pr.failureClusters.totalFailures}/${pr.failureClusters.totalRuns} run(s)`
4882
+ );
4883
+ if (pr.failureClusters.clusters.length > 0) {
4884
+ lines.push("");
4885
+ lines.push("| Failure class | Runs | Tool | Example trace | Example error |");
4886
+ lines.push("|---|---:|---|---|---|");
4887
+ for (const cluster of pr.failureClusters.clusters.slice(0, 10)) {
4888
+ lines.push(
4889
+ `| ${tableCell(cluster.failureClass)} | ${cluster.runCount} | ${cluster.toolName ? `\`${tableCell(cluster.toolName)}\`` : "not captured"} | ${cluster.exampleRunId ? `\`${tableCell(cluster.exampleRunId)}\`` : "not captured"} | ${tableCell(cluster.exampleError ?? "not captured")} |`
4890
+ );
4891
+ }
4892
+ lines.push("");
4893
+ }
3889
4894
  if (pr.stuckLoops.findings.length === 0) {
3890
- lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args).");
4895
+ lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args in a short interval).");
3891
4896
  } else {
3892
4897
  lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
3893
4898
  for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
3894
4899
  lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
3895
4900
  }
3896
4901
  }
3897
- for (const m of pr.toolUse) {
3898
- if (m.totalCalls === 0) continue;
4902
+ const runsWithTools = pr.toolUse.filter((metrics) => metrics.totalCalls > 0);
4903
+ if (runsWithTools.length > 0) {
4904
+ let totalCalls = 0;
4905
+ let callsWithCapturedArgs = 0;
4906
+ let duplicateCalls = 0;
4907
+ let errorCalls = 0;
4908
+ let followedFailures = 0;
4909
+ const byTool = /* @__PURE__ */ new Map();
4910
+ for (const metrics of runsWithTools) {
4911
+ const toolStats = Object.values(metrics.byTool);
4912
+ const duplicates = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.duplicates, 0) : Math.round(metrics.duplicateRate * metrics.totalCalls);
4913
+ const errors = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.errors, 0) : Math.round(metrics.errorRate * metrics.totalCalls);
4914
+ totalCalls += metrics.totalCalls;
4915
+ callsWithCapturedArgs += metrics.callsWithCapturedArgs;
4916
+ duplicateCalls += duplicates;
4917
+ errorCalls += errors;
4918
+ followedFailures += Math.round(metrics.retryRate * errors);
4919
+ for (const [name, stats] of Object.entries(metrics.byTool)) {
4920
+ const aggregate = byTool.get(name) ?? {
4921
+ calls: 0,
4922
+ captured: 0,
4923
+ duplicates: 0,
4924
+ errors: 0,
4925
+ latencyMs: 0
4926
+ };
4927
+ aggregate.calls += stats.calls;
4928
+ aggregate.captured += stats.callsWithCapturedArgs;
4929
+ aggregate.duplicates += stats.duplicates;
4930
+ aggregate.errors += stats.errors;
4931
+ aggregate.latencyMs += stats.avgLatencyMs * stats.calls;
4932
+ byTool.set(name, aggregate);
4933
+ }
4934
+ }
4935
+ const failureFollowUp = errorCalls > 0 ? `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)` : "";
4936
+ const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
3899
4937
  lines.push(
3900
- `- **Tool use** (${m.totalCalls} calls): ${(m.duplicateRate * 100).toFixed(0)}% duplicate, ${(m.retryRate * 100).toFixed(0)}% retry, ${(m.errorRate * 100).toFixed(0)}% error`
4938
+ `- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
3901
4939
  );
4940
+ if (byTool.size > 0) {
4941
+ lines.push("");
4942
+ lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
4943
+ lines.push("|---|---:|---:|---:|---:|---:|");
4944
+ for (const [name, stats] of [...byTool].sort(
4945
+ (left, right) => right[1].calls - left[1].calls || left[0].localeCompare(right[0])
4946
+ )) {
4947
+ lines.push(
4948
+ `| \`${tableCell(name)}\` | ${stats.calls} | ${stats.captured}/${stats.calls} | ${stats.duplicates}/${stats.captured} | ${stats.errors}/${stats.calls} | ${(stats.latencyMs / stats.calls).toFixed(1)}ms |`
4949
+ );
4950
+ }
4951
+ }
3902
4952
  }
3903
4953
  lines.push("");
3904
4954
  return lines.join("\n");
@@ -3952,14 +5002,41 @@ function renderReactions(rr) {
3952
5002
  }
3953
5003
  function renderAdoption(ar) {
3954
5004
  const lines = ["## skill & subagent adoption (deterministic)", ""];
5005
+ const scopeParts = [`${ar.identifiedSessionCount} identified session(s)`];
5006
+ if (ar.unassignedTraceCount > 0) {
5007
+ scopeParts.push(`${ar.unassignedTraceCount} trace(s) without a single stable session identity`);
5008
+ }
5009
+ lines.push(`- **Scope:** ${scopeParts.join(" + ")}`);
5010
+ if (ar.sessionIdentityConflicts.length > 0) {
5011
+ const displayed = ar.sessionIdentityConflicts.slice(0, SOURCE_DISPLAY_LIMIT);
5012
+ const details = displayed.map((conflict) => `\`${conflict.traceId}\` (${conflict.sessionIds.map((id) => `\`${id}\``).join(", ")})`).join("; ");
5013
+ const omitted = ar.sessionIdentityConflicts.length - displayed.length;
5014
+ lines.push(
5015
+ `- **Conflicting session identity:** ${ar.sessionIdentityConflicts.length} trace(s): ${details}${omitted > 0 ? `; ${omitted} more omitted from this report` : ""}`
5016
+ );
5017
+ }
5018
+ lines.push(
5019
+ "- **Prompt, tools, MCP, hooks, and full agent profile:** not assessed; no baseline profile was supplied."
5020
+ );
5021
+ if (ar.skillPenetration === null) {
5022
+ const label = ar.skillTelemetryStatus === "unsupported" ? "uncaptured/unsupported" : "uncaptured/unknown";
5023
+ lines.push(
5024
+ `- **Explicit skill invocation rate:** ${label} (${ar.skillTelemetrySessions}/${ar.executionGroupCount} observed group(s) expose a dedicated Skill event)`
5025
+ );
5026
+ } else {
5027
+ const excluded = ar.executionGroupCount - ar.skillTelemetrySessions;
5028
+ lines.push(
5029
+ `- **Explicit skill invocation rate:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.skillTelemetrySessions} measurable group(s)${excluded > 0 ? `; ${excluded} unmeasurable group(s) excluded` : ""})`
5030
+ );
5031
+ }
3955
5032
  lines.push(
3956
- `- **Skill penetration:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.sessionCount} session(s) invoked a skill explicitly)`
5033
+ `- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **SKILL.md tool references:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount} observed group(s)`
3957
5034
  );
3958
5035
  lines.push(
3959
5036
  `- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
3960
5037
  );
3961
5038
  lines.push(
3962
- `- **Subagent spawns:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} session(s)`
5039
+ `- **Subagent spawns observed:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} observed group(s); capture completeness depends on source telemetry.`
3963
5040
  );
3964
5041
  lines.push("");
3965
5042
  const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
@@ -4002,9 +5079,6 @@ var severityRank2 = {
4002
5079
  low: 3,
4003
5080
  info: 4
4004
5081
  };
4005
- function slug(input) {
4006
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
4007
- }
4008
5082
  function json(value) {
4009
5083
  return `${JSON.stringify(value, null, 2)}
4010
5084
  `;
@@ -4017,13 +5091,76 @@ function evidence(kind, uri, excerpt) {
4017
5091
  return excerpt ? { kind, uri, excerpt } : { kind, uri };
4018
5092
  }
4019
5093
  function totalReactionSignals(reactions) {
4020
- return Object.values(reactions.signals).reduce((sum, count) => sum + count, 0);
5094
+ return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
4021
5095
  }
4022
- function toolErrorRuns(pipelines) {
4023
- return pipelines.toolUse.filter((run) => run.errorRate > 0).length;
5096
+ function traceCoverageFinding(pipelines, adoption) {
5097
+ const toolCalls = pipelines.toolUse.reduce((total, run) => total + run.totalCalls, 0);
5098
+ const capturedToolCalls = pipelines.toolUse.reduce(
5099
+ (total, run) => total + run.callsWithCapturedArgs,
5100
+ 0
5101
+ );
5102
+ const missingToolArgs = toolCalls - capturedToolCalls;
5103
+ const traces = pipelines.toolUse.length;
5104
+ const missingSessionIds = adoption.unassignedTraceCount;
5105
+ const sessionIdentityConflicts = adoption.sessionIdentityConflicts;
5106
+ const unmeasurableSkillGroups = adoption.executionGroupCount - adoption.skillTelemetrySessions;
5107
+ if (missingToolArgs === 0 && missingSessionIds === 0 && unmeasurableSkillGroups === 0) {
5108
+ return void 0;
5109
+ }
5110
+ const parts = [];
5111
+ const refs = [];
5112
+ if (missingToolArgs > 0) {
5113
+ parts.push(`${missingToolArgs}/${toolCalls} tool call(s) lacked comparable arguments`);
5114
+ refs.push(evidence("metric", "coverage.tool_arguments", `${capturedToolCalls}/${toolCalls} captured`));
5115
+ }
5116
+ if (missingSessionIds > 0) {
5117
+ parts.push(`${missingSessionIds}/${traces} trace(s) lacked a single stable session identity`);
5118
+ refs.push(evidence("metric", "coverage.session_identity", `${traces - missingSessionIds}/${traces} identified`));
5119
+ }
5120
+ if (sessionIdentityConflicts.length > 0) {
5121
+ refs.push(evidence(
5122
+ "span",
5123
+ "coverage.session_identity_conflicts",
5124
+ sessionIdentityConflicts.map((conflict) => conflict.traceId).join(", ")
5125
+ ));
5126
+ }
5127
+ if (unmeasurableSkillGroups > 0) {
5128
+ parts.push(`${unmeasurableSkillGroups}/${adoption.executionGroupCount} execution group(s) lacked measurable skill events`);
5129
+ refs.push(evidence(
5130
+ "metric",
5131
+ "coverage.skill_events",
5132
+ `${adoption.skillTelemetrySessions}/${adoption.executionGroupCount} measurable`
5133
+ ));
5134
+ }
5135
+ const argumentCoverage = toolCalls === 0 ? 1 : capturedToolCalls / toolCalls;
5136
+ const sessionCoverage = traces === 0 ? 1 : (traces - missingSessionIds) / traces;
5137
+ const severity = argumentCoverage < 0.5 || sessionCoverage < 0.5 ? "high" : argumentCoverage < 0.9 || sessionCoverage < 0.9 || unmeasurableSkillGroups > 0 ? "medium" : "low";
5138
+ return makeFinding({
5139
+ analyst_id: "traces-deterministic",
5140
+ area: "instrumentation",
5141
+ claim: `Trace capture is incomplete: ${parts.join("; ")}`,
5142
+ severity,
5143
+ rationale: "Missing identity or agent-profile evidence prevents reliable cross-run comparison and can hide tool, prompt, skill, or subagent failures.",
5144
+ evidence_refs: refs,
5145
+ recommended_action: "Update the source telemetry bridge once: emit a stable session ID, complete canonical tool arguments, explicit argument-capture status, and dedicated skill/subagent events before optimizing the agent profile.",
5146
+ validation_plan: "Rerun the same source and require every trace to carry exactly one session identity, every tool call to declare and preserve its full arguments, and every supported execution group to expose skill events.",
5147
+ confidence: 1,
5148
+ metadata: {
5149
+ source: "traces.coverage",
5150
+ toolCalls,
5151
+ capturedToolCalls,
5152
+ traces,
5153
+ missingSessionIds,
5154
+ sessionIdentityConflictCount: sessionIdentityConflicts.length,
5155
+ skillTelemetryGroups: adoption.skillTelemetrySessions,
5156
+ executionGroups: adoption.executionGroupCount
5157
+ }
5158
+ });
4024
5159
  }
4025
5160
  function deterministicFindings(pipelines, reactions, adoption) {
4026
5161
  const findings = [];
5162
+ const coverage = traceCoverageFinding(pipelines, adoption);
5163
+ if (coverage) findings.push(coverage);
4027
5164
  const loops = pipelines.stuckLoops.findings;
4028
5165
  if (loops.length > 0) {
4029
5166
  const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
@@ -4043,19 +5180,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
4043
5180
  metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
4044
5181
  }));
4045
5182
  }
4046
- const erroredRuns = toolErrorRuns(pipelines);
4047
- if (erroredRuns > 0) {
5183
+ const failedRuns = pipelines.failureClusters.totalFailures;
5184
+ if (failedRuns > 0) {
5185
+ const top = pipelines.failureClusters.clusters[0];
5186
+ const failureRate = failedRuns / Math.max(1, pipelines.failureClusters.totalRuns);
4048
5187
  findings.push(makeFinding({
4049
5188
  analyst_id: "traces-deterministic",
4050
- area: "tool-use",
4051
- claim: `${erroredRuns} run(s) included tool errors`,
4052
- severity: erroredRuns >= 5 ? "high" : "medium",
4053
- rationale: "Tool errors are expensive when the agent keeps planning as if the call succeeded.",
4054
- evidence_refs: [evidence("metric", "pipelines.tool_error_runs", `${erroredRuns} run(s) with errorRate > 0`)],
4055
- recommended_action: "Fix the highest-frequency failing tool path first, then make the agent inspect failure output before retrying.",
4056
- validation_plan: "Rerun the same trace window and require the tool-error run count and repeated retries to decrease.",
4057
- confidence: 0.9,
4058
- metadata: { source: "traces.pipeline.computeToolUseMetrics", erroredRuns }
5189
+ area: "reliability",
5190
+ claim: `${failedRuns}/${pipelines.failureClusters.totalRuns} run(s) had execution errors`,
5191
+ severity: failureRate >= 0.1 ? "high" : "medium",
5192
+ rationale: "Execution errors consume time and tokens, and sparse error telemetry can hide whether the agent recovered or repeated the same failure.",
5193
+ evidence_refs: [
5194
+ evidence(
5195
+ "metric",
5196
+ `pipelines.failure_cluster.${top?.failureClass ?? "unknown"}`,
5197
+ top ? `${top.runCount} run(s); example: ${top.exampleError ?? "error details not captured"}` : `${failedRuns} failed run(s)`
5198
+ ),
5199
+ ...top?.exampleRunId ? [evidence("span", `trace:${top.exampleRunId}`, top.exampleError)] : []
5200
+ ],
5201
+ recommended_action: top?.failureClass === "unknown" ? "Instrument the failing operation name and arguments, then start with the highest-frequency example error and verify the agent changes state before retrying." : "Fix the highest-frequency failure cluster first, then require the agent to inspect the error and change state before retrying.",
5202
+ validation_plan: "Rerun a comparable trace window and require failed runs and repeated failures in the top cluster to decrease.",
5203
+ confidence: 1,
5204
+ metadata: {
5205
+ source: "agent-eval.failureClusterView",
5206
+ failedRuns,
5207
+ totalRuns: pipelines.failureClusters.totalRuns,
5208
+ topFailureClass: top?.failureClass,
5209
+ exampleRunId: top?.exampleRunId
5210
+ }
4059
5211
  }));
4060
5212
  }
4061
5213
  const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
@@ -4074,7 +5226,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
4074
5226
  }));
4075
5227
  }
4076
5228
  const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
4077
- if (adoption.sessionCount > 0 && skillRuns === 0) {
5229
+ const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
5230
+ if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
4078
5231
  findings.push(makeFinding({
4079
5232
  analyst_id: "traces-deterministic",
4080
5233
  area: "workflow",
@@ -4085,7 +5238,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
4085
5238
  recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
4086
5239
  validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
4087
5240
  confidence: 0.75,
4088
- metadata: { source: "traces.adoption", sessionCount: adoption.sessionCount }
5241
+ metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
4089
5242
  }));
4090
5243
  }
4091
5244
  return findings;
@@ -4146,41 +5299,6 @@ function normalizeExternalFindings(results) {
4146
5299
  }
4147
5300
  return out;
4148
5301
  }
4149
- function sourceOf(finding2) {
4150
- if (finding2.analyst_id.startsWith("external:")) return "external";
4151
- if (finding2.analyst_id === "traces-deterministic") return "deterministic";
4152
- return "analyst";
4153
- }
4154
- function recommendationTitle(finding2) {
4155
- if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
4156
- return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
4157
- }
4158
- function buildRecommendations(findings) {
4159
- return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
4160
- schemaVersion: 1,
4161
- id: `rec-${index + 1}-${slug(finding2.area)}`,
4162
- severity: finding2.severity,
4163
- title: recommendationTitle(finding2),
4164
- action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
4165
- rationale: finding2.rationale ?? finding2.claim,
4166
- evidenceRefs: finding2.evidence_refs,
4167
- findingIds: [finding2.finding_id],
4168
- validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
4169
- source: sourceOf(finding2)
4170
- }));
4171
- }
4172
- function buildClaims(findings) {
4173
- return findings.map((finding2, index) => ({
4174
- schemaVersion: 1,
4175
- id: `claim-${index + 1}-${slug(finding2.area)}`,
4176
- severity: finding2.severity,
4177
- text: finding2.claim,
4178
- area: finding2.area,
4179
- sourceFindingId: finding2.finding_id,
4180
- evidenceRefs: finding2.evidence_refs,
4181
- validationPlan: finding2.validation_plan
4182
- }));
4183
- }
4184
5302
  function buildEvidenceRows(findings) {
4185
5303
  return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
4186
5304
  schemaVersion: 1,
@@ -4192,31 +5310,10 @@ function buildEvidenceRows(findings) {
4192
5310
  evidence: ref
4193
5311
  })));
4194
5312
  }
4195
- function renderRecommendations(recommendations) {
4196
- const lines = ["## recommendations", ""];
4197
- if (recommendations.length === 0) {
4198
- lines.push("- No recommendations emitted.");
4199
- lines.push("");
4200
- return lines.join("\n");
4201
- }
4202
- for (const rec of recommendations.slice(0, 10)) {
4203
- lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
4204
- lines.push("");
4205
- lines.push(`- **Action:** ${rec.action}`);
4206
- lines.push(`- **Why:** ${rec.rationale}`);
4207
- lines.push(`- **Check:** ${rec.validationPlan}`);
4208
- for (const ref of rec.evidenceRefs.slice(0, 3)) {
4209
- lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
4210
- }
4211
- lines.push("");
4212
- }
4213
- return lines.join("\n");
4214
- }
4215
5313
  function renderFindingPacket(packet, title = "Trace finding packet") {
4216
5314
  const lines = [`# ${title}`, ""];
4217
- lines.push(`${packet.findings.length} finding(s), ${packet.recommendations.length} recommendation(s), ${packet.claims.length} claim(s).`);
5315
+ lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
4218
5316
  lines.push("");
4219
- lines.push(renderRecommendations(packet.recommendations));
4220
5317
  if (packet.findings.length > 0) {
4221
5318
  lines.push("## findings");
4222
5319
  lines.push("");
@@ -4242,33 +5339,10 @@ function buildTraceFindingPacket(opts) {
4242
5339
  kind: "traces.finding_packet",
4243
5340
  generatedAt,
4244
5341
  source: opts.source ?? "traces",
4245
- findings: opts.findings,
4246
- recommendations: buildRecommendations(opts.findings),
4247
- claims: buildClaims(opts.findings)
5342
+ findings: opts.findings
4248
5343
  };
4249
5344
  return { ...packet, report: renderFindingPacket(packet, opts.title) };
4250
5345
  }
4251
- function buildDefaultImprovementProposals(input) {
4252
- return input.recommendations.slice(0, 5).map((recommendation, index) => {
4253
- const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
4254
- const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
4255
- return {
4256
- id: `proposal-${index + 1}-${slug(recommendation.title)}`,
4257
- title: recommendation.title,
4258
- description: [
4259
- recommendation.action,
4260
- "",
4261
- `Why: ${recommendation.rationale}`,
4262
- `Validation: ${recommendation.validationPlan}`,
4263
- "",
4264
- "This is a proposal-only artifact. It does not mutate code, prompts, skills, tools, MCP config, hooks, subagents, or external systems until a human applies it and reruns validation."
4265
- ].join("\n"),
4266
- recommendationIds: [recommendation.id],
4267
- validationCommand: "traces improve --last 1 --dir .traces/improvement",
4268
- evidenceRefs
4269
- };
4270
- });
4271
- }
4272
5346
  function renderExternal(results) {
4273
5347
  if (results.length === 0) return "";
4274
5348
  const lines = ["## external analyzers", ""];
@@ -4284,11 +5358,13 @@ function renderInvestigationReport(result, analystResult) {
4284
5358
  const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
4285
5359
  harness: result.harness,
4286
5360
  sessionCount: result.sessionCount,
5361
+ unassignedTraceCount: result.unassignedTraceCount,
4287
5362
  spanCount: result.spanCount,
4288
5363
  otlpPath: result.otlpPath,
4289
- deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions)
5364
+ execution: result.execution,
5365
+ deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
5366
+ sources: result.sources
4290
5367
  })}
4291
- ${renderRecommendations(result.recommendations)}
4292
5368
  ${renderPipelines(result.pipelines)}
4293
5369
  ${renderReactions(result.reactions)}
4294
5370
  ${renderAdoption(result.adoption)}`;
@@ -4365,19 +5441,18 @@ async function runTraceInvestigation(opts) {
4365
5441
  ...deterministicFindings(pipelines, reactions, adoption),
4366
5442
  ...normalizeExternalFindings(external)
4367
5443
  ];
4368
- const recommendations = buildRecommendations(findings);
4369
- const claims = buildClaims(findings);
4370
5444
  const partial = {
4371
5445
  schemaVersion: 1,
4372
5446
  kind: "traces.investigation",
4373
5447
  generatedAt,
4374
5448
  harness: opts.harness,
4375
- sessionCount: opts.sessionCount,
5449
+ sessionCount: adoption.identifiedSessionCount,
5450
+ unassignedTraceCount: adoption.unassignedTraceCount,
5451
+ sources: opts.sources,
4376
5452
  spanCount: opts.spans.length,
4377
5453
  otlpPath: analysis.otlpPath,
5454
+ execution: analysis.execution,
4378
5455
  findings,
4379
- recommendations,
4380
- claims,
4381
5456
  pipelines,
4382
5457
  reactions,
4383
5458
  adoption,
@@ -4409,70 +5484,38 @@ async function runTraceStoreInvestigation(opts) {
4409
5484
  analystResult
4410
5485
  };
4411
5486
  }
4412
- function replayProof(result, proposals) {
4413
- const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
4414
- return {
4415
- schemaVersion: 1,
4416
- kind: "traces.improvement_replay",
4417
- generatedAt: result.generatedAt,
4418
- status: proposals.length > 0 ? "proposal-only" : "not-run",
4419
- candidateApplied: false,
4420
- baseline: {
4421
- spanCount: result.spanCount,
4422
- findingCount: result.findings.length,
4423
- recommendationCount: result.recommendations.length,
4424
- stuckLoops: deterministic.stuckLoops,
4425
- reactionSignals: deterministic.reactionSignals,
4426
- toolErrorRuns: deterministic.toolErrorRuns
4427
- },
4428
- proposals,
4429
- note: proposals.length > 0 ? "traces produced reviewable proposals only; apply one and rerun traces on a comparable window to populate after metrics." : "no proposal adapter was configured, so no candidate was applied; recommendations remain reviewable guidance."
4430
- };
4431
- }
4432
5487
  async function writeTraceImprovementArtifacts(result, outDir) {
4433
5488
  const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
4434
5489
  await mkdir(directory, { recursive: true });
4435
5490
  const paths = {
4436
5491
  directory,
4437
- findings: join16(directory, "findings.json"),
4438
- recommendations: join16(directory, "recommendations.json"),
4439
- proposals: join16(directory, "proposals.json"),
5492
+ result: join16(directory, "result.json"),
4440
5493
  evidence: join16(directory, "evidence.jsonl"),
4441
- claims: join16(directory, "claims.json"),
4442
5494
  report: join16(directory, "report.md"),
4443
- replay: join16(directory, "replay-before-after.json")
5495
+ traces: result.otlpPath
4444
5496
  };
5497
+ const { report, ...machineResult } = result;
4445
5498
  await Promise.all([
4446
- writeFile4(paths.findings, json(result.findings), "utf8"),
4447
- writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
4448
- writeFile4(paths.proposals, json(result.proposals), "utf8"),
5499
+ writeFile4(paths.result, json(machineResult), "utf8"),
4449
5500
  writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
4450
- writeFile4(paths.claims, json(result.claims), "utf8"),
4451
- writeFile4(paths.report, result.report, "utf8"),
4452
- writeFile4(paths.replay, json(result.replay), "utf8")
5501
+ writeFile4(paths.report, report, "utf8")
4453
5502
  ]);
4454
5503
  return paths;
4455
5504
  }
4456
- async function runTraceImprovementLoop(opts) {
4457
- const investigation = await runTraceInvestigation(opts);
4458
- const adapterInput = {
4459
- findings: investigation.findings,
4460
- recommendations: investigation.recommendations,
4461
- claims: investigation.claims,
4462
- spans: opts.spans,
4463
- otlpPath: investigation.otlpPath
4464
- };
4465
- const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
4466
- const replay = replayProof(investigation, proposals);
5505
+ async function runTraceImprovement(opts) {
5506
+ const directory = opts.outDir ? resolve(opts.outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
5507
+ await mkdir(directory, { recursive: true });
5508
+ const investigation = await runTraceInvestigation({
5509
+ ...opts,
5510
+ otlpOutPath: opts.otlpOutPath ?? join16(directory, "traces.otlp.jsonl")
5511
+ });
4467
5512
  const result = {
4468
5513
  ...investigation,
4469
- kind: "traces.improvement",
4470
- proposals,
4471
- replay
5514
+ kind: "traces.improvement"
4472
5515
  };
4473
5516
  return {
4474
5517
  ...result,
4475
- artifacts: await writeTraceImprovementArtifacts(result, opts.outDir)
5518
+ artifacts: await writeTraceImprovementArtifacts(result, directory)
4476
5519
  };
4477
5520
  }
4478
5521
  async function saveReport(path, report) {
@@ -4481,7 +5524,7 @@ async function saveReport(path, report) {
4481
5524
  }
4482
5525
 
4483
5526
  // src/live.ts
4484
- import { createHash as createHash2 } from "crypto";
5527
+ import { createHash as createHash5 } from "crypto";
4485
5528
  var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
4486
5529
  var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
4487
5530
  var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
@@ -4501,7 +5544,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
4501
5544
  signal?.addEventListener("abort", onAbort, { once: true });
4502
5545
  });
4503
5546
  function hash(parts) {
4504
- return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
5547
+ return createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
4505
5548
  }
4506
5549
  function asString(value) {
4507
5550
  if (typeof value === "string" && value.trim()) return value;
@@ -4955,7 +5998,7 @@ async function watchSessions(opts) {
4955
5998
  }
4956
5999
 
4957
6000
  // src/session-index.ts
4958
- import { mkdtemp as mkdtemp5, readdir as readdir11, readFile as readFile14, stat as stat12, writeFile as writeFile5 } from "fs/promises";
6001
+ import { mkdtemp as mkdtemp5, readdir as readdir12, readFile as readFile5, stat as stat12, writeFile as writeFile5 } from "fs/promises";
4959
6002
  import { tmpdir as tmpdir5 } from "os";
4960
6003
  import { dirname as dirname3, join as join17 } from "path";
4961
6004
  function indexRow(record) {
@@ -4977,7 +6020,8 @@ function indexRow(record) {
4977
6020
  },
4978
6021
  models: record.metrics.models,
4979
6022
  tools: record.metrics.tools,
4980
- signals: record.signals
6023
+ signals: record.signals,
6024
+ execution: record.execution
4981
6025
  };
4982
6026
  }
4983
6027
  function uniqueSorted(values) {
@@ -5072,7 +6116,7 @@ async function summarizeFile(path, kind) {
5072
6116
  const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
5073
6117
  if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
5074
6118
  try {
5075
- const text = await readFile14(path, "utf8");
6119
+ const text = await readFile5(path, "utf8");
5076
6120
  const lines = countLines(text);
5077
6121
  const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
5078
6122
  return {
@@ -5091,7 +6135,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
5091
6135
  if (out.length >= maxFiles) return;
5092
6136
  let entries;
5093
6137
  try {
5094
- entries = await readdir11(dir, { withFileTypes: true });
6138
+ entries = await readdir12(dir, { withFileTypes: true });
5095
6139
  } catch {
5096
6140
  return;
5097
6141
  }
@@ -5177,10 +6221,10 @@ async function writeSessionIndexFile(index, outPath) {
5177
6221
  }
5178
6222
 
5179
6223
  // src/upload-state.ts
5180
- import { createHash as createHash3 } from "crypto";
5181
- import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
6224
+ import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
5182
6225
  import { homedir as homedir11 } from "os";
5183
6226
  import { dirname as dirname4, join as join18 } from "path";
6227
+ import { contentHash } from "@tangle-network/agent-eval";
5184
6228
  function stateFile() {
5185
6229
  const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
5186
6230
  return join18(base, "tangle-traces", "uploaded.json");
@@ -5189,16 +6233,14 @@ function uploadKey(harness, sessionId) {
5189
6233
  return `${harness}:${sessionId}`;
5190
6234
  }
5191
6235
  function sessionHash(spans) {
5192
- const h = createHash3("sha256");
5193
- for (const s of spans) {
5194
- h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
5195
- `);
5196
- }
5197
- return h.digest("hex").slice(0, 32);
6236
+ return contentHash(spans).slice(0, 32);
6237
+ }
6238
+ function outboundHash(events, identity) {
6239
+ return contentHash({ events, identity }).slice(0, 32);
5198
6240
  }
5199
6241
  async function loadState() {
5200
6242
  try {
5201
- return JSON.parse(await readFile15(stateFile(), "utf8"));
6243
+ return JSON.parse(await readFile6(stateFile(), "utf8"));
5202
6244
  } catch {
5203
6245
  return {};
5204
6246
  }
@@ -5214,7 +6256,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
5214
6256
  }
5215
6257
 
5216
6258
  // src/upload.ts
5217
- import { readFile as readFile16 } from "fs/promises";
6259
+ import { readFile as readFile7 } from "fs/promises";
5218
6260
  import { createRequire } from "module";
5219
6261
  import { hostname } from "os";
5220
6262
  import { basename as basename9, join as join19 } from "path";
@@ -5233,7 +6275,7 @@ async function planUpload(opts = {}) {
5233
6275
  spans,
5234
6276
  redaction: report,
5235
6277
  hash: hash2,
5236
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
6278
+ isNew: true
5237
6279
  });
5238
6280
  }
5239
6281
  return { items, state, sinceMs: opts.sinceMs };
@@ -5241,19 +6283,18 @@ async function planUpload(opts = {}) {
5241
6283
  async function gitBranch(cwd) {
5242
6284
  if (!cwd) return void 0;
5243
6285
  try {
5244
- const head = await readFile16(join19(cwd, ".git", "HEAD"), "utf8");
6286
+ const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
5245
6287
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
5246
6288
  return m?.[1]?.trim();
5247
6289
  } catch {
5248
6290
  return void 0;
5249
6291
  }
5250
6292
  }
5251
- async function sessionMeta(item, uploadedAt) {
6293
+ async function sessionMeta(item) {
5252
6294
  const meta = {
5253
6295
  [ATTR.HARNESS]: item.ref.harness,
5254
6296
  [ATTR.SESSION_FILE]: basename9(item.ref.path),
5255
6297
  [ATTR.HOST]: hostname(),
5256
- [ATTR.UPLOADED_AT]: uploadedAt,
5257
6298
  [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
5258
6299
  [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
5259
6300
  [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
@@ -5287,25 +6328,69 @@ function toTraceSpanEvents(spans, rootMeta) {
5287
6328
  };
5288
6329
  });
5289
6330
  }
6331
+ var PartialUploadError = class extends Error {
6332
+ sessionKey;
6333
+ accepted;
6334
+ expected;
6335
+ constructor(sessionKey, accepted, expected) {
6336
+ super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
6337
+ this.name = "PartialUploadError";
6338
+ this.sessionKey = sessionKey;
6339
+ this.accepted = accepted;
6340
+ this.expected = expected;
6341
+ }
6342
+ };
5290
6343
  function stripSpanContent(spans) {
5291
6344
  return spans.map((s) => {
5292
- if (s.attributes["content"] == null) return s;
5293
- const { content: _drop, ...attributes } = s.attributes;
5294
- return { ...s, attributes };
6345
+ const attributes = { ...s.attributes };
6346
+ delete attributes.content;
6347
+ for (const key of TOOL_IO_VALUE_KEYS) delete attributes[key];
6348
+ normalizeToolIoAttributes(attributes);
6349
+ const status = s.status.message ? { code: s.status.code } : s.status;
6350
+ return { ...s, attributes, status };
5295
6351
  });
5296
6352
  }
5297
- async function executeUpload(plan, opts = {}) {
5298
- let newItems = plan.items.filter((i) => i.isNew);
6353
+ async function prepareOutbound(plan, opts, uploadedAt) {
6354
+ let candidates = plan.items;
5299
6355
  if (opts.redactor && !opts.stripContent) {
5300
- const r = opts.redactor;
5301
- newItems = await Promise.all(newItems.map(async (i) => ({ ...i, spans: (await applyRedactor(i.spans, r)).spans })));
6356
+ const redactor = opts.redactor;
6357
+ candidates = await Promise.all(
6358
+ candidates.map(async (item) => ({ ...item, spans: (await applyRedactor(item.spans, redactor)).spans }))
6359
+ );
5302
6360
  }
5303
- if (opts.stripContent) newItems = newItems.map((i) => ({ ...i, spans: stripSpanContent(i.spans) }));
5304
- const skipped = plan.items.length - newItems.length;
5305
- const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
6361
+ if (opts.stripContent) {
6362
+ candidates = candidates.map((item) => ({ ...item, spans: stripSpanContent(item.spans) }));
6363
+ }
6364
+ const identity = {
6365
+ stripContent: opts.stripContent === true,
6366
+ redactor: opts.stripContent ? null : opts.redactor?.name ?? null,
6367
+ redactionVersion: REDACTION_VERSION
6368
+ };
6369
+ const items = [];
6370
+ for (const item of candidates) {
6371
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
6372
+ const previous = plan.state[key];
6373
+ const meta = await sessionMeta(item);
6374
+ const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
6375
+ let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
6376
+ let events = eventsAt(eventTimestamp2);
6377
+ let hash2 = outboundHash(events, identity);
6378
+ if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash2)) continue;
6379
+ if (eventTimestamp2 !== uploadedAt) {
6380
+ eventTimestamp2 = uploadedAt;
6381
+ events = eventsAt(eventTimestamp2);
6382
+ hash2 = outboundHash(events, identity);
6383
+ }
6384
+ items.push({ item, events, hash: hash2 });
6385
+ }
6386
+ return { items, skipped: plan.items.length - items.length };
6387
+ }
6388
+ async function executeUpload(plan, opts = {}) {
5306
6389
  const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
6390
+ const { items, skipped } = await prepareOutbound(plan, opts, uploadedAt);
6391
+ const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
5307
6392
  if (opts.dryRun) {
5308
- const allSpans = newItems.flatMap((i) => i.spans);
6393
+ const allSpans = items.flatMap(({ item }) => item.spans);
5309
6394
  const path = await writeOtlpFile(allSpans, opts.otlpOut);
5310
6395
  return {
5311
6396
  uploadedSessions: 0,
@@ -5316,6 +6401,15 @@ async function executeUpload(plan, opts = {}) {
5316
6401
  otlpPath: path
5317
6402
  };
5318
6403
  }
6404
+ if (items.length === 0) {
6405
+ return {
6406
+ uploadedSessions: 0,
6407
+ skippedSessions: skipped,
6408
+ acceptedSpans: 0,
6409
+ redactionCount,
6410
+ dryRun: false
6411
+ };
6412
+ }
5319
6413
  const client = opts.backend ?? hostedClientFromEnv();
5320
6414
  if (!client) {
5321
6415
  throw new Error(
@@ -5324,23 +6418,28 @@ async function executeUpload(plan, opts = {}) {
5324
6418
  }
5325
6419
  const state = plan.state;
5326
6420
  let acceptedSpans = 0;
5327
- for (const item of newItems) {
5328
- const meta = await sessionMeta(item, uploadedAt);
5329
- const events = toTraceSpanEvents(item.spans, meta);
5330
- const idempotencyKey = `${uploadKey(item.ref.harness, item.ref.sessionId)}:${item.hash}`;
6421
+ const completed = [];
6422
+ for (const outbound of items) {
6423
+ const { item, events, hash: hash2 } = outbound;
6424
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
6425
+ const idempotencyKey = `${key}:${hash2}`;
5331
6426
  const res = await client.ingestTraces(events, idempotencyKey);
6427
+ if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
5332
6428
  acceptedSpans += res.accepted;
5333
- state[uploadKey(item.ref.harness, item.ref.sessionId)] = {
5334
- hash: item.hash,
6429
+ completed.push({ key, item, hash: hash2, spanCount: events.length });
6430
+ opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
6431
+ }
6432
+ for (const { key, item, hash: hash2, spanCount } of completed) {
6433
+ state[key] = {
6434
+ hash: hash2,
5335
6435
  uploadedAt,
5336
6436
  harness: item.ref.harness,
5337
- spanCount: item.spans.length
6437
+ spanCount
5338
6438
  };
5339
- opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
5340
6439
  }
5341
6440
  await saveState(state);
5342
6441
  return {
5343
- uploadedSessions: newItems.length,
6442
+ uploadedSessions: items.length,
5344
6443
  skippedSessions: skipped,
5345
6444
  acceptedSpans,
5346
6445
  redactionCount,
@@ -5349,25 +6448,38 @@ async function executeUpload(plan, opts = {}) {
5349
6448
  }
5350
6449
 
5351
6450
  export {
6451
+ ATTR,
6452
+ INGEST_SOURCE_CLI,
6453
+ DEFAULT_HARNESS,
6454
+ sessionIdFromAttributes,
6455
+ indexSessionIdsByTrace,
6456
+ stampSessionIdentity,
5352
6457
  countSkillRunsJsonl,
5353
6458
  analyzeAdoption,
5354
6459
  span,
5355
6460
  toOpenInferenceSpan,
5356
6461
  serializeSpans,
5357
6462
  writeOtlpFile,
6463
+ summarizeOtlpExecution,
6464
+ summarizeSpanExecution,
5358
6465
  analyzeSpans,
5359
- ATTR,
5360
- INGEST_SOURCE_CLI,
5361
- DEFAULT_HARNESS,
5362
- parseIsoToEpochMs,
5363
- parseSince,
5364
- toRuntimeStore,
5365
- runPipelines,
5366
6466
  CONTENT_CAP,
5367
6467
  capText,
5368
6468
  ACTOR_ATTR,
5369
6469
  userPromptSpan,
6470
+ parseIsoToEpochMs,
6471
+ parseSince,
6472
+ toRuntimeStore,
6473
+ runPipelines,
6474
+ recordSessionCorruption,
6475
+ sessionIntegrityAttributes,
6476
+ stampSessionIntegrity,
6477
+ JsonSourceError,
6478
+ readJsonFile,
5370
6479
  AmpAdapter,
6480
+ JsonlParseError,
6481
+ readJsonl,
6482
+ takeJsonl,
5371
6483
  textIsSynthetic,
5372
6484
  looksLikeAgentPrompt,
5373
6485
  claudeActor,
@@ -5395,6 +6507,7 @@ export {
5395
6507
  stampRepoAttrs,
5396
6508
  stampSpanWorkdirRepoAttrs,
5397
6509
  parseSession,
6510
+ EmptySessionError,
5398
6511
  locateSessions,
5399
6512
  scanSessions,
5400
6513
  buildPolicyEvidenceRecord,
@@ -5421,6 +6534,7 @@ export {
5421
6534
  writeInspectionReportFile,
5422
6535
  REACTIONS,
5423
6536
  analyzeReactions,
6537
+ CORRUPTION_RECEIPT_DISPLAY_LIMIT,
5424
6538
  summarizeDeterministicSignals,
5425
6539
  renderReport,
5426
6540
  renderPipelines,
@@ -5432,7 +6546,7 @@ export {
5432
6546
  runTraceInvestigation,
5433
6547
  runTraceStoreInvestigation,
5434
6548
  writeTraceImprovementArtifacts,
5435
- runTraceImprovementLoop,
6549
+ runTraceImprovement,
5436
6550
  saveReport,
5437
6551
  classifyLiveActions,
5438
6552
  defaultTraceLiveAnalysts,
@@ -5448,10 +6562,12 @@ export {
5448
6562
  writeSessionIndexFile,
5449
6563
  uploadKey,
5450
6564
  sessionHash,
6565
+ outboundHash,
5451
6566
  loadState,
5452
6567
  saveState,
5453
6568
  alreadyUploaded,
5454
6569
  planUpload,
5455
6570
  toTraceSpanEvents,
6571
+ PartialUploadError,
5456
6572
  executeUpload
5457
6573
  };