@tangle-network/traces 0.8.26 → 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 {};
@@ -22,9 +234,34 @@ function skillNameOf(input) {
22
234
  return typeof v === "string" && v.length > 0 ? v : "?";
23
235
  }
24
236
  function subagentTypeOf(input) {
25
- const v = input.subagent_type;
237
+ const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
26
238
  return typeof v === "string" && v.length > 0 ? v : "?";
27
239
  }
240
+ function isSpawnAgentTool(name) {
241
+ return name === "spawn_agent" || name.endsWith("__spawn_agent");
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
+ }
28
265
  function countSkillRunsJsonl(raw) {
29
266
  const out = {};
30
267
  for (const line of raw.split("\n")) {
@@ -68,32 +305,84 @@ async function readLoopRuns(cwds) {
68
305
  return { counts, filesRead };
69
306
  }
70
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}`;
71
314
  const sessions = /* @__PURE__ */ new Set();
72
315
  const sessionsWithSkill = /* @__PURE__ */ new Set();
73
316
  const sessionsWithSubagent = /* @__PURE__ */ new Set();
317
+ const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
318
+ const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
74
319
  const skillInvocations = {};
75
320
  const subagentSpawns = {};
321
+ const canonicalSubagentSessions = /* @__PURE__ */ new Set();
322
+ const fallbackSubagents = /* @__PURE__ */ new Map();
323
+ const sessionCapabilities = /* @__PURE__ */ new Map();
76
324
  for (const s of spans) {
77
- 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);
78
334
  const tn = toolName(s);
79
335
  if (tn === "Skill") {
336
+ sessionCapabilities.set(group, "supported");
80
337
  const name = skillNameOf(parseInput(s));
81
338
  skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
82
- sessionsWithSkill.add(s.trace_id);
339
+ sessionsWithSkill.add(group);
83
340
  } else if (tn === "Task" || tn === "Agent") {
84
341
  const type = subagentTypeOf(parseInput(s));
85
342
  subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
86
- sessionsWithSubagent.add(s.trace_id);
343
+ sessionsWithSubagent.add(group);
344
+ canonicalSubagentSessions.add(group);
345
+ } else if (tn && isSpawnAgentTool(tn)) {
346
+ const types = fallbackSubagents.get(group) ?? [];
347
+ types.push(subagentTypeOf(parseInput(s)));
348
+ fallbackSubagents.set(group, types);
349
+ }
350
+ }
351
+ for (const [traceId, types] of fallbackSubagents) {
352
+ if (canonicalSubagentSessions.has(traceId)) continue;
353
+ sessionsWithSubagent.add(traceId);
354
+ for (const type of types) {
355
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
87
356
  }
88
357
  }
89
358
  const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
90
359
  const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
91
360
  const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
92
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
+ }
93
371
  return {
94
- sessionCount: sessions.size,
372
+ executionGroupCount: sessions.size,
373
+ identifiedSessionCount: identifiedSessions.size,
374
+ unassignedTraceCount: unassignedTraceIds.size,
375
+ sessionIdentityConflicts: conflicts,
95
376
  sessionsWithSkill: sessionsWithSkill.size,
96
- 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,
97
386
  skillInvocations,
98
387
  totalSkillInvocations,
99
388
  subagentSpawns,
@@ -106,16 +395,27 @@ async function analyzeAdoption(spans, opts = {}) {
106
395
  }
107
396
 
108
397
  // src/otlp.ts
398
+ import {
399
+ applyLlmSpanOtlpAttributes,
400
+ OPENINFERENCE_SPAN_KIND,
401
+ TOOL_NAME
402
+ } from "@tangle-network/agent-eval/trace-attributes";
109
403
  function span(input) {
110
404
  const attributes = {
111
- "openinference.span.kind": input.kind
405
+ [OPENINFERENCE_SPAN_KIND]: input.kind
112
406
  };
113
407
  if (input.service != null) attributes["service.name"] = input.service;
114
408
  if (input.agent != null) attributes["agent.name"] = input.agent;
115
- if (input.model != null) attributes["llm.model_name"] = input.model;
116
- if (input.tool != null) attributes["tool.name"] = input.tool;
117
- if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
118
- 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
+ });
119
419
  if (input.step != null) attributes.step = input.step;
120
420
  if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
121
421
  if (input.extra) Object.assign(attributes, input.extra);
@@ -145,7 +445,7 @@ function toOpenInferenceSpan(s) {
145
445
  span_id: s.span_id,
146
446
  parent_span_id: s.parent_span_id ?? "",
147
447
  name: s.name,
148
- 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",
149
449
  start_time: s.start_time,
150
450
  end_time: s.end_time,
151
451
  status: { code: s.status.code, message: s.status.message ?? "" },
@@ -168,20 +468,56 @@ async function writeOtlpFile(spans, outPath) {
168
468
  return path;
169
469
  }
170
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
+
171
500
  // src/analyze.ts
172
501
  import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
173
502
  import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
174
- var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
503
+ var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
175
504
  async function analyzeSpans(spans, opts = {}) {
176
505
  if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
177
506
  const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
178
507
  const runId = opts.runId ?? `traces-${Date.now()}`;
179
- 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
+ });
180
516
  await detStore.ensureIndexed();
181
517
  const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
182
518
  const result = await detRegistry.run(runId, { traceStore: detStore });
183
519
  if (opts.ai) {
184
- const agStore = new OtlpFileTraceStore({ path: otlpPath });
520
+ const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
185
521
  await agStore.ensureIndexed();
186
522
  const agRegistry = buildDefaultAnalystRegistry({
187
523
  ai: opts.ai,
@@ -196,42 +532,32 @@ async function analyzeSpans(spans, opts = {}) {
196
532
  result.per_analyst.push(...agResult.per_analyst);
197
533
  result.total_cost_usd += agResult.total_cost_usd;
198
534
  }
199
- return { otlpPath, result };
535
+ return { otlpPath, execution, result };
200
536
  }
201
537
 
202
- // src/attributes.ts
203
- var ATTR = {
204
- /** = the trace/session id. Wire contract (dedup linkage). */
205
- SESSION_ID: "tangle.sessionId",
206
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
207
- * that also streamed live. Wire contract. */
208
- INGEST_SOURCE: "tangle.ingest_source",
209
- HARNESS: "tangle.harness",
210
- CWD: "tangle.cwd",
211
- /** THE per-session grouping key the spine derives subjects from
212
- * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
213
- * remote (host/owner/repo) when readable, else the cwd path basename. */
214
- SUBJECT_KEY: "tangle.subject.key",
215
- /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
216
- GIT_REPOSITORY: "git.repository",
217
- /** Current branch at conversion time. */
218
- GIT_BRANCH_NAME: "git.branch",
219
- /** HEAD short sha at conversion time. */
220
- GIT_COMMIT: "git.commit",
221
- /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
222
- REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
223
- GIT_BRANCH: "tangle.git_branch",
224
- HOST: "tangle.host",
225
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
226
- * which collided with `tangle.ingest_source`.) */
227
- SESSION_FILE: "tangle.session_file",
228
- UPLOADED_AT: "tangle.uploaded_at",
229
- UPLOADER: "tangle.uploader",
230
- REDACTION_VERSION: "redaction.version",
231
- REDACTION_COUNT: "redaction.count"
232
- };
233
- var INGEST_SOURCE_CLI = "cli";
234
- 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
+ }
235
561
 
236
562
  // src/time.ts
237
563
  function parseIsoToEpochMs(ts) {
@@ -263,73 +589,238 @@ var KIND = {
263
589
  };
264
590
  async function toRuntimeStore(spans) {
265
591
  const store = new InMemoryTraceStore();
266
- 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
+ }
267
612
  for (const s of spans) {
268
- if (!runIds.has(s.trace_id)) {
269
- runIds.add(s.trace_id);
270
- await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
271
- }
272
613
  const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
614
+ const startedAt = parseIsoToEpochMs(s.start_time);
615
+ const endedAt = parseIsoToEpochMs(s.end_time);
273
616
  const base = {
274
617
  spanId: s.span_id,
275
618
  parentSpanId: s.parent_span_id ?? void 0,
276
619
  runId: s.trace_id,
277
620
  name: s.name,
278
- startedAt: parseIsoToEpochMs(s.start_time),
279
- endedAt: parseIsoToEpochMs(s.end_time),
621
+ startedAt,
622
+ endedAt,
280
623
  status: s.status.code === "ERROR" ? "error" : "ok",
281
624
  error: s.status.message,
282
625
  attributes: s.attributes
283
626
  };
627
+ const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
284
628
  const span2 = kind === "tool" ? {
285
629
  ...base,
286
630
  kind: "tool",
287
631
  toolName: String(s.attributes["tool.name"] ?? "tool"),
288
- args: s.attributes.content ?? ""
632
+ args: tool.args,
633
+ argsCaptured: tool.argsCaptured,
634
+ latencyMs: Math.max(0, endedAt - startedAt)
289
635
  } : { ...base, kind };
290
636
  await store.appendSpan(span2);
291
637
  }
292
- return { store, runIds: [...runIds] };
638
+ return { store, runIds: [...runs.keys()] };
293
639
  }
294
640
 
295
641
  // src/pipelines.ts
296
642
  import { computeToolUseMetrics } from "@tangle-network/agent-eval";
297
- import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
643
+ import {
644
+ failureClusterView,
645
+ stuckLoopView
646
+ } from "@tangle-network/agent-eval/pipelines";
298
647
  async function runPipelines(spans, opts = {}) {
299
648
  const { store, runIds } = await toRuntimeStore(spans);
300
649
  const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
301
650
  const { store: loopStore } = await toRuntimeStore(loopEligible);
302
- const stuckLoops = await stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 });
303
- const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
304
- 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 };
305
657
  }
306
658
 
307
- // src/adapters/conversation.ts
308
- var CONTENT_CAP = 8e3;
309
- function capText(raw) {
310
- const t = raw.trim();
311
- if (t.length <= CONTENT_CAP) return t;
312
- 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
+ ];
313
670
  }
314
- var ACTOR_ATTR = "tangle.actor";
315
- function userPromptSpan(o) {
316
- return span({
317
- traceId: o.traceId,
318
- spanId: o.spanId,
319
- parentSpanId: o.parentSpanId,
320
- name: "user.prompt",
321
- kind: "CHAIN",
322
- startTime: o.startTime,
323
- service: o.service ?? null,
324
- agent: o.agent ?? null,
325
- step: o.step,
326
- content: o.content,
327
- extra: { [ACTOR_ATTR]: o.actor ?? "human" }
328
- });
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
+ }
329
820
  }
330
821
 
331
822
  // src/adapters/amp.ts
332
- import { readdir, readFile as readFile2, stat } from "fs/promises";
823
+ import { readdir as readdir2, stat } from "fs/promises";
333
824
  import { homedir } from "os";
334
825
  import { basename, join as join2 } from "path";
335
826
  var SERVICE = "amp";
@@ -348,9 +839,10 @@ var AmpAdapter = class {
348
839
  async locate(opts = {}) {
349
840
  let files;
350
841
  try {
351
- files = await readdir(this.threadsRoot());
352
- } catch {
353
- return [];
842
+ files = await readdir2(this.threadsRoot());
843
+ } catch (error) {
844
+ if (isMissingPathError(error)) return [];
845
+ throw error;
354
846
  }
355
847
  const refs = [];
356
848
  for (const f of files) {
@@ -359,8 +851,9 @@ var AmpAdapter = class {
359
851
  let st;
360
852
  try {
361
853
  st = await stat(path);
362
- } catch {
363
- continue;
854
+ } catch (error) {
855
+ if (isMissingPathError(error)) continue;
856
+ throw error;
364
857
  }
365
858
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
366
859
  if (opts.cwd) continue;
@@ -369,12 +862,7 @@ var AmpAdapter = class {
369
862
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
370
863
  }
371
864
  async parse(ref) {
372
- let thread;
373
- try {
374
- thread = JSON.parse(await readFile2(ref.path, "utf8"));
375
- } catch {
376
- return [];
377
- }
865
+ const thread = await readJsonFile(ref.path);
378
866
  const traceId = thread.id ?? ref.sessionId;
379
867
  const rootId = `root:${traceId}`;
380
868
  const start = new Date(thread.created ?? 0).toISOString();
@@ -406,7 +894,6 @@ var AmpAdapter = class {
406
894
  }
407
895
  } else {
408
896
  const u = m.usage;
409
- const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
410
897
  spans.push(
411
898
  span({
412
899
  traceId,
@@ -418,8 +905,10 @@ var AmpAdapter = class {
418
905
  service: SERVICE,
419
906
  agent: SERVICE,
420
907
  model: u?.model ?? null,
421
- inputTokens: inputTokens2,
908
+ inputTokens: u?.inputTokens ?? null,
422
909
  outputTokens: u?.outputTokens ?? null,
910
+ cachedInputTokens: u?.cacheReadInputTokens ?? null,
911
+ cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
423
912
  step,
424
913
  content: textOf(m.content) || null
425
914
  })
@@ -439,14 +928,17 @@ var AmpAdapter = class {
439
928
  agent: SERVICE,
440
929
  tool: b.name,
441
930
  step,
442
- content: b.input != null ? JSON.stringify(b.input) : null
931
+ extra: toolIoAttributes({ input: b.input })
443
932
  });
444
933
  spans.push(t);
445
934
  if (b.id) toolByUseId.set(b.id, t);
446
935
  step += 1;
447
936
  } else if (b.type === "tool_result" && b.tool_use_id) {
448
937
  const t = toolByUseId.get(b.tool_use_id);
449
- 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
+ }
450
942
  }
451
943
  }
452
944
  }
@@ -454,6 +946,108 @@ var AmpAdapter = class {
454
946
  }
455
947
  };
456
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
+
457
1051
  // src/adapters/actor.ts
458
1052
  var CMD_MARKERS = [
459
1053
  "<command-name>",
@@ -471,6 +1065,10 @@ var INJECT_MARKERS = [
471
1065
  // not a person. (Observed in Claude Code task/loop sessions.)
472
1066
  "<task-notification>",
473
1067
  "<task-prompt>",
1068
+ // Prefix match: Codex adds attributes such as source="goal" to this tag.
1069
+ "<codex_internal_context",
1070
+ "<subagent_notification>",
1071
+ "# AGENTS.md instructions for ",
474
1072
  "<local-command-stdout>",
475
1073
  "This session is being continued from a previous conversation",
476
1074
  // Slash-command skill bodies are expanded into a user turn (no <command-name>
@@ -543,26 +1141,10 @@ function classifyReaction(text) {
543
1141
  var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
544
1142
 
545
1143
  // src/adapters/claude.ts
546
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
1144
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
547
1145
  import { homedir as homedir2 } from "os";
548
1146
  import { basename as basename2, join as join3 } from "path";
549
1147
  var SERVICE2 = "claude-code";
550
- function parseLines(raw) {
551
- const out = [];
552
- for (const line of raw.split("\n")) {
553
- if (!line) continue;
554
- try {
555
- out.push(JSON.parse(line));
556
- } catch {
557
- }
558
- }
559
- return out;
560
- }
561
- function inputTokens(u) {
562
- if (!u) return null;
563
- const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
564
- return v > 0 ? v : null;
565
- }
566
1148
  function asBlocks(content) {
567
1149
  return Array.isArray(content) ? content : [];
568
1150
  }
@@ -579,96 +1161,104 @@ function stringifyToolResult(content) {
579
1161
  }
580
1162
  return "";
581
1163
  }
582
- function parseClaudeStream(events, ctx) {
583
- const spans = [];
584
- const toolSpanByUseId = /* @__PURE__ */ new Map();
585
- let step = ctx.startStep;
586
- let sawUserTurn = false;
587
- for (const ev of events) {
588
- const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
589
- const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
590
- if (ev.type === "assistant" && ev.message) {
591
- const llmId = `${ctx.idPrefix}${uid}`;
592
- spans.push(
593
- 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({
594
1225
  traceId: ctx.traceId,
595
- spanId: llmId,
1226
+ spanId: `${ctx.idPrefix}${uid}:user`,
596
1227
  parentSpanId: ctx.rootParent,
597
- name: "llm.turn",
598
- kind: "LLM",
599
1228
  startTime: ts,
600
1229
  service: SERVICE2,
601
1230
  agent: ctx.agent,
602
- model: ev.message.model ?? null,
603
- inputTokens: inputTokens(ev.message.usage),
604
- outputTokens: ev.message.usage?.output_tokens ?? null,
605
- step,
606
- content: textOf2(ev.message.content) || null
1231
+ step: state.step,
1232
+ content: prompt,
1233
+ actor
607
1234
  })
608
1235
  );
609
- step += 1;
610
- let toolIdx = 0;
611
- for (const block of asBlocks(ev.message.content)) {
612
- if (block.type !== "tool_use" || !block.name) continue;
613
- const toolSpan = span({
614
- traceId: ctx.traceId,
615
- spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
616
- parentSpanId: llmId,
617
- name: `tool.${block.name}`,
618
- kind: "TOOL",
619
- startTime: ts,
620
- service: SERVICE2,
621
- agent: ctx.agent,
622
- tool: block.name,
623
- step,
624
- content: block.input != null ? JSON.stringify(block.input) : null
625
- });
626
- spans.push(toolSpan);
627
- if (block.id) toolSpanByUseId.set(block.id, toolSpan);
628
- toolIdx += 1;
629
- step += 1;
630
- }
631
- } else if (ev.type === "user" && ev.message) {
632
- const prompt = textOf2(ev.message.content);
633
- if (prompt) {
634
- const actor = claudeActor({
635
- text: prompt,
636
- isSidechain: ev.isSidechain,
637
- userType: ev.userType ?? null,
638
- isFirstUserTurn: !sawUserTurn
639
- });
640
- sawUserTurn = true;
641
- spans.push(
642
- userPromptSpan({
643
- traceId: ctx.traceId,
644
- spanId: `${ctx.idPrefix}${uid}:user`,
645
- parentSpanId: ctx.rootParent,
646
- startTime: ts,
647
- service: SERVICE2,
648
- agent: ctx.agent,
649
- step,
650
- content: prompt,
651
- actor
652
- })
653
- );
654
- step += 1;
655
- }
656
- for (const block of asBlocks(ev.message.content)) {
657
- if (block.type !== "tool_result" || !block.tool_use_id) continue;
658
- backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
659
- }
660
- } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
661
- const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
662
- 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
+ );
663
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 ?? "");
664
1250
  }
665
- return { spans, toolSpanByUseId, nextStep: step };
666
1251
  }
667
- 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) {
668
1256
  if (!s) return;
669
1257
  s.end_time = endTime;
670
1258
  const code = isError ? "ERROR" : "OK";
671
1259
  s.status = { code };
1260
+ recordToolOutput(s, output);
1261
+ const message = stringifyToolResult(output);
672
1262
  if (isError && message) s.status.message = message.slice(0, 500);
673
1263
  }
674
1264
  var ClaudeAdapter = class {
@@ -681,18 +1271,20 @@ var ClaudeAdapter = class {
681
1271
  const root = this.root();
682
1272
  let projectDirs;
683
1273
  try {
684
- projectDirs = await readdir2(root);
685
- } catch {
686
- return [];
1274
+ projectDirs = await readdir3(root);
1275
+ } catch (error) {
1276
+ if (isMissingPathError(error)) return [];
1277
+ throw error;
687
1278
  }
688
1279
  const refs = [];
689
1280
  for (const dir of projectDirs) {
690
1281
  const dirPath = join3(root, dir);
691
1282
  let files;
692
1283
  try {
693
- files = await readdir2(dirPath);
694
- } catch {
695
- continue;
1284
+ files = await readdir3(dirPath);
1285
+ } catch (error) {
1286
+ if (isMissingPathError(error)) continue;
1287
+ throw error;
696
1288
  }
697
1289
  for (const f of files) {
698
1290
  if (!f.endsWith(".jsonl")) continue;
@@ -700,8 +1292,9 @@ var ClaudeAdapter = class {
700
1292
  let st;
701
1293
  try {
702
1294
  st = await stat2(path);
703
- } catch {
704
- continue;
1295
+ } catch (error) {
1296
+ if (isMissingPathError(error)) continue;
1297
+ throw error;
705
1298
  }
706
1299
  if (!st.isFile()) continue;
707
1300
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -712,11 +1305,37 @@ var ClaudeAdapter = class {
712
1305
  }
713
1306
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
714
1307
  }
715
- async parse(ref) {
716
- const raw = await readFile3(ref.path, "utf8");
717
- const events = parseLines(raw);
718
- 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;
719
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
+ }
720
1339
  const spans = [
721
1340
  span({
722
1341
  traceId,
@@ -724,55 +1343,53 @@ var ClaudeAdapter = class {
724
1343
  parentSpanId: null,
725
1344
  name: "session",
726
1345
  kind: "AGENT",
727
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
728
- endTime: events.at(-1)?.timestamp,
1346
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1347
+ endTime: lastTimestamp,
729
1348
  service: SERVICE2,
730
1349
  agent: SERVICE2
731
- })
1350
+ }),
1351
+ ...main.spans
732
1352
  ];
733
- const main = parseClaudeStream(events, {
734
- traceId,
735
- agent: SERVICE2,
736
- startStep: 0,
737
- idPrefix: "",
738
- rootParent: rootId
739
- });
740
- spans.push(...main.spans);
741
- await this.foldSubagents(ref, traceId, main, spans);
1353
+ await this.foldSubagents(ref, traceId, main, spans, options);
742
1354
  return spans;
743
1355
  }
744
1356
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
745
- async foldSubagents(ref, traceId, main, out) {
1357
+ async foldSubagents(ref, traceId, main, out, options) {
746
1358
  const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
747
1359
  let files;
748
1360
  try {
749
- files = await readdir2(subDir);
750
- } catch {
751
- return;
1361
+ files = await readdir3(subDir);
1362
+ } catch (error) {
1363
+ if (isMissingPathError(error)) return;
1364
+ throw error;
752
1365
  }
753
1366
  let step = main.nextStep;
754
- for (const f of files) {
755
- if (!f.endsWith(".jsonl")) continue;
756
- 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`);
757
1371
  let meta = {};
758
1372
  try {
759
- meta = JSON.parse(await readFile3(join3(subDir, `${hash2}.meta.json`), "utf8"));
760
- } catch {
1373
+ meta = await readJsonFile(metaPath);
1374
+ } catch (error) {
1375
+ if (!isMissingJsonSource(error)) throw error;
761
1376
  }
762
1377
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
763
- let raw;
764
- try {
765
- raw = await readFile3(join3(subDir, f), "utf8");
766
- } catch {
767
- continue;
768
- }
769
- const parsed = parseClaudeStream(parseLines(raw), {
1378
+ const ctx = {
770
1379
  traceId,
771
1380
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
772
1381
  startStep: step,
773
1382
  idPrefix: `${hash2}:`,
774
1383
  rootParent: parent
775
- });
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);
776
1393
  out.push(...parsed.spans);
777
1394
  step = parsed.nextStep;
778
1395
  }
@@ -780,21 +1397,11 @@ var ClaudeAdapter = class {
780
1397
  };
781
1398
 
782
1399
  // src/adapters/codex.ts
783
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
1400
+ import { readdir as readdir4, stat as stat3 } from "fs/promises";
784
1401
  import { homedir as homedir3 } from "os";
785
1402
  import { basename as basename3, join as join4 } from "path";
786
1403
  var SERVICE3 = "codex";
787
- function parseLines2(raw) {
788
- const out = [];
789
- for (const line of raw.split("\n")) {
790
- if (!line) continue;
791
- try {
792
- out.push(JSON.parse(line));
793
- } catch {
794
- }
795
- }
796
- return out;
797
- }
1404
+ var SESSION_HEAD_LINES = 40;
798
1405
  function contentToString(content) {
799
1406
  if (typeof content === "string") return content;
800
1407
  if (Array.isArray(content)) {
@@ -821,6 +1428,28 @@ function toolInputToString(input) {
821
1428
  if (input == null) return void 0;
822
1429
  return JSON.stringify(input);
823
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
+ }
824
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;
825
1454
  function hasReadOnlyCurl(input) {
826
1455
  if (!/\bcurl\b/i.test(input)) return false;
@@ -840,33 +1469,37 @@ function isExpectedBlockingTool(name, input) {
840
1469
  async function* walkRollouts(root) {
841
1470
  let years;
842
1471
  try {
843
- years = await readdir3(root);
844
- } catch {
845
- return;
1472
+ years = await readdir4(root);
1473
+ } catch (error) {
1474
+ if (isMissingPathError(error)) return;
1475
+ throw error;
846
1476
  }
847
1477
  for (const y of years) {
848
1478
  const yp = join4(root, y);
849
1479
  let months;
850
1480
  try {
851
- months = await readdir3(yp);
852
- } catch {
853
- continue;
1481
+ months = await readdir4(yp);
1482
+ } catch (error) {
1483
+ if (isMissingPathError(error)) continue;
1484
+ throw error;
854
1485
  }
855
1486
  for (const m of months) {
856
1487
  const mp = join4(yp, m);
857
1488
  let days;
858
1489
  try {
859
- days = await readdir3(mp);
860
- } catch {
861
- continue;
1490
+ days = await readdir4(mp);
1491
+ } catch (error) {
1492
+ if (isMissingPathError(error)) continue;
1493
+ throw error;
862
1494
  }
863
1495
  for (const d of days) {
864
1496
  const dp = join4(mp, d);
865
1497
  let files;
866
1498
  try {
867
- files = await readdir3(dp);
868
- } catch {
869
- continue;
1499
+ files = await readdir4(dp);
1500
+ } catch (error) {
1501
+ if (isMissingPathError(error)) continue;
1502
+ throw error;
870
1503
  }
871
1504
  for (const f of files) {
872
1505
  if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
@@ -887,75 +1520,106 @@ var CodexAdapter = class {
887
1520
  let st;
888
1521
  try {
889
1522
  st = await stat3(path);
890
- } catch {
891
- continue;
1523
+ } catch (error) {
1524
+ if (isMissingPathError(error)) continue;
1525
+ throw error;
892
1526
  }
893
1527
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
894
- const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
895
1528
  let cwd = null;
896
1529
  let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
897
- for (const line of head) {
898
- if (!line) continue;
899
- try {
900
- const l = JSON.parse(line);
901
- if (l.type === "session_meta" && l.payload?.id) id = l.payload.id;
902
- if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
903
- } catch {
904
- }
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;
905
1535
  if (cwd) break;
906
1536
  }
907
- if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
908
- 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);
909
1545
  }
910
1546
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
911
1547
  }
912
- async parse(ref) {
913
- const lines = parseLines2(await readFile4(ref.path, "utf8"));
914
- 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
+ }
915
1562
  const traceId = meta?.payload?.id ?? ref.sessionId;
916
- 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";
917
1566
  const rootId = `root:${traceId}`;
918
- const spans = [
919
- span({
920
- traceId,
921
- spanId: rootId,
922
- parentSpanId: null,
923
- name: "session",
924
- kind: "AGENT",
925
- startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
926
- endTime: lines.at(-1)?.timestamp,
927
- service: SERVICE3,
928
- agent: SERVICE3,
929
- model
930
- })
931
- ];
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];
932
1587
  const toolByCallId = /* @__PURE__ */ new Map();
933
1588
  const subagentByThreadId = /* @__PURE__ */ new Map();
934
1589
  let step = 0;
935
1590
  let lastLlm = rootId;
936
1591
  let sawUserTurn = false;
937
- 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;
938
1596
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
939
- 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") {
940
1602
  const u = l.payload.info?.last_token_usage;
941
1603
  if (u && (u.input_tokens || u.output_tokens)) {
942
1604
  const id = `llm:${step}`;
943
- spans.push(
944
- span({
945
- traceId,
946
- spanId: id,
947
- parentSpanId: rootId,
948
- name: "llm.turn",
949
- kind: "LLM",
950
- startTime: ts,
951
- service: SERVICE3,
952
- agent: SERVICE3,
953
- model,
954
- inputTokens: u.input_tokens ?? null,
955
- outputTokens: u.output_tokens ?? null,
956
- step
957
- })
958
- );
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);
959
1623
  lastLlm = id;
960
1624
  step += 1;
961
1625
  }
@@ -967,6 +1631,7 @@ var CodexAdapter = class {
967
1631
  );
968
1632
  const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
969
1633
  const name = classifyNestedTool(nestedName ?? outerName, input);
1634
+ const agentOperation = multiAgentOperation(nestedName ?? outerName);
970
1635
  const toolSpan = span({
971
1636
  traceId,
972
1637
  spanId: `tool:${callId}`,
@@ -978,14 +1643,16 @@ var CodexAdapter = class {
978
1643
  agent: SERVICE3,
979
1644
  tool: name,
980
1645
  step,
981
- content: input ?? null,
982
1646
  extra: {
1647
+ ...toolIoAttributes({ input }),
983
1648
  "traces.codex.call_type": l.payload.type,
984
1649
  ...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
985
1650
  ...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
986
- ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
1651
+ ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
1652
+ ...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
987
1653
  }
988
1654
  });
1655
+ if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
989
1656
  spans.push(toolSpan);
990
1657
  toolByCallId.set(callId, toolSpan);
991
1658
  step += 1;
@@ -995,6 +1662,10 @@ var CodexAdapter = class {
995
1662
  const { error, message } = outputIsError(l.payload.output);
996
1663
  t.end_time = ts;
997
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
+ }
998
1669
  }
999
1670
  } else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
1000
1671
  const threadId = l.payload.agent_thread_id;
@@ -1014,12 +1685,14 @@ var CodexAdapter = class {
1014
1685
  agent: SERVICE3,
1015
1686
  tool: "Agent",
1016
1687
  step,
1017
- content: JSON.stringify({
1018
- subagent_type: subagentType,
1019
- agent_path: agentPath,
1020
- agent_thread_id: threadId
1021
- }),
1022
1688
  extra: {
1689
+ ...toolIoAttributes({
1690
+ input: {
1691
+ subagent_type: subagentType,
1692
+ agent_path: agentPath,
1693
+ agent_thread_id: threadId
1694
+ }
1695
+ }),
1023
1696
  "traces.codex.subagent_path": agentPath,
1024
1697
  "traces.codex.subagent_thread_id": threadId
1025
1698
  }
@@ -1081,26 +1754,16 @@ var CodexAdapter = class {
1081
1754
  }
1082
1755
  }
1083
1756
  }
1757
+ root.end_time = lastTimestamp ?? root.start_time;
1084
1758
  return spans;
1085
1759
  }
1086
1760
  };
1087
1761
 
1088
1762
  // src/adapters/copilot.ts
1089
- import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
1763
+ import { readdir as readdir5, stat as stat4 } from "fs/promises";
1090
1764
  import { homedir as homedir4 } from "os";
1091
1765
  import { join as join5 } from "path";
1092
1766
  var SERVICE4 = "github-copilot";
1093
- function parseLines3(raw) {
1094
- const out = [];
1095
- for (const line of raw.split("\n")) {
1096
- if (!line.trim()) continue;
1097
- try {
1098
- out.push(JSON.parse(line));
1099
- } catch {
1100
- }
1101
- }
1102
- return out;
1103
- }
1104
1767
  var CopilotAdapter = class {
1105
1768
  harness = "github-copilot";
1106
1769
  aliases = ["copilot"];
@@ -1110,9 +1773,10 @@ var CopilotAdapter = class {
1110
1773
  async locate(opts = {}) {
1111
1774
  let dirs;
1112
1775
  try {
1113
- dirs = await readdir4(this.root());
1114
- } catch {
1115
- return [];
1776
+ dirs = await readdir5(this.root());
1777
+ } catch (error) {
1778
+ if (isMissingPathError(error)) return [];
1779
+ throw error;
1116
1780
  }
1117
1781
  const refs = [];
1118
1782
  for (const id of dirs) {
@@ -1120,8 +1784,9 @@ var CopilotAdapter = class {
1120
1784
  let st;
1121
1785
  try {
1122
1786
  st = await stat4(path);
1123
- } catch {
1124
- continue;
1787
+ } catch (error) {
1788
+ if (isMissingPathError(error)) continue;
1789
+ throw error;
1125
1790
  }
1126
1791
  if (!st.isFile()) continue;
1127
1792
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1130,28 +1795,23 @@ var CopilotAdapter = class {
1130
1795
  }
1131
1796
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1132
1797
  }
1133
- async parse(ref) {
1134
- const events = parseLines3(await readFile5(ref.path, "utf8"));
1798
+ async parse(ref, options = {}) {
1135
1799
  const traceId = ref.sessionId;
1136
1800
  const rootId = `root:${traceId}`;
1137
- const spans = [
1138
- span({
1139
- traceId,
1140
- spanId: rootId,
1141
- parentSpanId: null,
1142
- name: "session",
1143
- kind: "AGENT",
1144
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1145
- endTime: events.at(-1)?.timestamp,
1146
- service: SERVICE4,
1147
- agent: SERVICE4
1148
- })
1149
- ];
1801
+ const spans = [];
1150
1802
  const toolByCallId = /* @__PURE__ */ new Map();
1803
+ let firstTimestamp;
1804
+ let lastTimestamp;
1805
+ let sawEvent = false;
1151
1806
  let step = 0;
1152
1807
  let lastLlm = rootId;
1153
1808
  let pendingInputTokens = null;
1154
- 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;
1155
1815
  const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1156
1816
  const d = ev.data ?? {};
1157
1817
  if (ev.type === "assistant.usage") {
@@ -1193,7 +1853,7 @@ var CopilotAdapter = class {
1193
1853
  agent: SERVICE4,
1194
1854
  tool: name,
1195
1855
  step,
1196
- content: d.arguments != null ? JSON.stringify(d.arguments) : null
1856
+ extra: toolIoAttributes({ input: d.arguments })
1197
1857
  });
1198
1858
  spans.push(t);
1199
1859
  toolByCallId.set(d.toolCallId, t);
@@ -1204,15 +1864,27 @@ var CopilotAdapter = class {
1204
1864
  t.end_time = ts;
1205
1865
  const err = d.success === false;
1206
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);
1207
1868
  }
1208
1869
  }
1209
1870
  }
1210
- 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];
1211
1883
  }
1212
1884
  };
1213
1885
 
1214
1886
  // src/adapters/factory.ts
1215
- import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
1887
+ import { readdir as readdir6, stat as stat5 } from "fs/promises";
1216
1888
  import { homedir as homedir5 } from "os";
1217
1889
  import { basename as basename4, join as join6 } from "path";
1218
1890
  var SERVICE5 = "factory";
@@ -1221,17 +1893,6 @@ function textOf4(content) {
1221
1893
  (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
1222
1894
  );
1223
1895
  }
1224
- function parseLines4(raw) {
1225
- const out = [];
1226
- for (const line of raw.split("\n")) {
1227
- if (!line) continue;
1228
- try {
1229
- out.push(JSON.parse(line));
1230
- } catch {
1231
- }
1232
- }
1233
- return out;
1234
- }
1235
1896
  var FactoryAdapter = class {
1236
1897
  harness = "factory";
1237
1898
  aliases = ["factory-droids", "droid"];
@@ -1242,18 +1903,20 @@ var FactoryAdapter = class {
1242
1903
  const root = this.root();
1243
1904
  let dirs;
1244
1905
  try {
1245
- dirs = await readdir5(root);
1246
- } catch {
1247
- return [];
1906
+ dirs = await readdir6(root);
1907
+ } catch (error) {
1908
+ if (isMissingPathError(error)) return [];
1909
+ throw error;
1248
1910
  }
1249
1911
  const refs = [];
1250
1912
  for (const dir of dirs) {
1251
1913
  const dp = join6(root, dir);
1252
1914
  let files;
1253
1915
  try {
1254
- files = await readdir5(dp);
1255
- } catch {
1256
- continue;
1916
+ files = await readdir6(dp);
1917
+ } catch (error) {
1918
+ if (isMissingPathError(error)) continue;
1919
+ throw error;
1257
1920
  }
1258
1921
  const cwd = dir.replace(/-/g, "/");
1259
1922
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
@@ -1263,8 +1926,9 @@ var FactoryAdapter = class {
1263
1926
  let st;
1264
1927
  try {
1265
1928
  st = await stat5(path);
1266
- } catch {
1267
- continue;
1929
+ } catch (error) {
1930
+ if (isMissingPathError(error)) continue;
1931
+ throw error;
1268
1932
  }
1269
1933
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1270
1934
  refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
@@ -1272,38 +1936,32 @@ var FactoryAdapter = class {
1272
1936
  }
1273
1937
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1274
1938
  }
1275
- async parse(ref) {
1276
- const lines = parseLines4(await readFile6(ref.path, "utf8"));
1277
- const start = lines.find((l) => l.type === "session_start");
1278
- const traceId = start?.id ?? ref.sessionId;
1279
- const rootId = `root:${traceId}`;
1939
+ async parse(ref, options = {}) {
1280
1940
  let settings = {};
1281
1941
  try {
1282
- settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
1283
- } catch {
1942
+ settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
1943
+ } catch (error) {
1944
+ if (!isMissingJsonSource(error)) throw error;
1284
1945
  }
1285
- const spans = [
1286
- span({
1287
- traceId,
1288
- spanId: rootId,
1289
- parentSpanId: null,
1290
- name: "session",
1291
- kind: "AGENT",
1292
- startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1293
- endTime: lines.at(-1)?.timestamp,
1294
- service: SERVICE5,
1295
- agent: SERVICE5,
1296
- model: settings.model ?? null,
1297
- extra: settings.tokenUsage ? {
1298
- "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
1299
- "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
1300
- } : void 0
1301
- })
1302
- ];
1946
+ const sourceTraceId = ref.sessionId;
1947
+ const sourceRootId = `root:${sourceTraceId}`;
1948
+ const spans = [];
1303
1949
  const toolByUseId = /* @__PURE__ */ new Map();
1950
+ let firstTimestamp;
1951
+ let lastTimestamp;
1952
+ let sessionLine;
1953
+ let sawLine = false;
1304
1954
  let step = 0;
1305
- let lastLlm = rootId;
1306
- 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
+ }
1307
1965
  if (l.type !== "message" || !l.message) continue;
1308
1966
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1309
1967
  const role = l.message.role;
@@ -1312,9 +1970,9 @@ var FactoryAdapter = class {
1312
1970
  if (text) {
1313
1971
  spans.push(
1314
1972
  userPromptSpan({
1315
- traceId,
1973
+ traceId: sourceTraceId,
1316
1974
  spanId: `user:${l.id ?? step}`,
1317
- parentSpanId: rootId,
1975
+ parentSpanId: sourceRootId,
1318
1976
  startTime: ts,
1319
1977
  content: text,
1320
1978
  service: SERVICE5,
@@ -1328,9 +1986,9 @@ var FactoryAdapter = class {
1328
1986
  const llmId = `llm:${l.id ?? step}`;
1329
1987
  spans.push(
1330
1988
  span({
1331
- traceId,
1989
+ traceId: sourceTraceId,
1332
1990
  spanId: llmId,
1333
- parentSpanId: rootId,
1991
+ parentSpanId: sourceRootId,
1334
1992
  name: `message.${role ?? "unknown"}`,
1335
1993
  kind: "LLM",
1336
1994
  startTime: ts,
@@ -1347,7 +2005,7 @@ var FactoryAdapter = class {
1347
2005
  for (const b of l.message.content ?? []) {
1348
2006
  if (b.type === "tool_use" && b.name) {
1349
2007
  const t = span({
1350
- traceId,
2008
+ traceId: sourceTraceId,
1351
2009
  spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
1352
2010
  parentSpanId: lastLlm,
1353
2011
  name: `tool.${b.name}`,
@@ -1357,23 +2015,48 @@ var FactoryAdapter = class {
1357
2015
  agent: SERVICE5,
1358
2016
  tool: b.name,
1359
2017
  step,
1360
- content: b.input != null ? JSON.stringify(b.input) : null
2018
+ extra: toolIoAttributes({ input: b.input })
1361
2019
  });
1362
2020
  spans.push(t);
1363
2021
  if (b.id) toolByUseId.set(b.id, t);
1364
2022
  step += 1;
1365
2023
  } else if (b.type === "tool_result" && b.tool_use_id) {
1366
2024
  const t = toolByUseId.get(b.tool_use_id);
1367
- 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
+ }
1368
2029
  }
1369
2030
  }
1370
2031
  }
1371
- 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];
1372
2055
  }
1373
2056
  };
1374
2057
 
1375
2058
  // src/adapters/forge.ts
1376
- import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
2059
+ import { readdir as readdir7, stat as stat6 } from "fs/promises";
1377
2060
  import { homedir as homedir6 } from "os";
1378
2061
  import { basename as basename5, join as join7 } from "path";
1379
2062
  var SERVICE6 = "forge";
@@ -1397,9 +2080,10 @@ var ForgeAdapter = class {
1397
2080
  for (const dir of dirs) {
1398
2081
  let files;
1399
2082
  try {
1400
- files = await readdir6(dir);
1401
- } catch {
1402
- continue;
2083
+ files = await readdir7(dir);
2084
+ } catch (error) {
2085
+ if (isMissingPathError(error)) continue;
2086
+ throw error;
1403
2087
  }
1404
2088
  for (const f of files) {
1405
2089
  if (!f.endsWith("-dump.json")) continue;
@@ -1407,8 +2091,9 @@ var ForgeAdapter = class {
1407
2091
  let st;
1408
2092
  try {
1409
2093
  st = await stat6(path);
1410
- } catch {
1411
- continue;
2094
+ } catch (error) {
2095
+ if (isMissingPathError(error)) continue;
2096
+ throw error;
1412
2097
  }
1413
2098
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1414
2099
  refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
@@ -1417,12 +2102,7 @@ var ForgeAdapter = class {
1417
2102
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1418
2103
  }
1419
2104
  async parse(ref) {
1420
- let dump;
1421
- try {
1422
- dump = JSON.parse(await readFile7(ref.path, "utf8"));
1423
- } catch {
1424
- return [];
1425
- }
2105
+ const dump = await readJsonFile(ref.path);
1426
2106
  const ctx = dump.context ?? dump;
1427
2107
  const traceId = ctx.conversation_id ?? ref.sessionId;
1428
2108
  const rootId = `root:${traceId}`;
@@ -1487,7 +2167,7 @@ var ForgeAdapter = class {
1487
2167
  agent: SERVICE6,
1488
2168
  tool: tc.name ?? "tool",
1489
2169
  step,
1490
- content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
2170
+ extra: toolIoAttributes({ input: tc.arguments })
1491
2171
  });
1492
2172
  spans.push(t);
1493
2173
  toolByCallId.set(id, t);
@@ -1495,7 +2175,10 @@ var ForgeAdapter = class {
1495
2175
  }
1496
2176
  } else if (entry.tool) {
1497
2177
  const t = toolByCallId.get(entry.tool.call_id ?? "");
1498
- 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
+ }
1499
2182
  }
1500
2183
  }
1501
2184
  return spans;
@@ -1503,7 +2186,7 @@ var ForgeAdapter = class {
1503
2186
  };
1504
2187
 
1505
2188
  // src/adapters/gemini.ts
1506
- import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
2189
+ import { readdir as readdir8, stat as stat7 } from "fs/promises";
1507
2190
  import { homedir as homedir7 } from "os";
1508
2191
  import { join as join8 } from "path";
1509
2192
  function iso(ts, fallback) {
@@ -1539,10 +2222,12 @@ var GeminiFamilyAdapter = class {
1539
2222
  async loadProjectCwds() {
1540
2223
  const nameToPath = /* @__PURE__ */ new Map();
1541
2224
  try {
1542
- const raw = await readFile8(join8(homedir7(), this.homeDirName, "projects.json"), "utf8");
1543
- const projects = JSON.parse(raw).projects ?? {};
2225
+ const projects = (await readJsonFile(
2226
+ join8(homedir7(), this.homeDirName, "projects.json")
2227
+ )).projects ?? {};
1544
2228
  for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
1545
- } catch {
2229
+ } catch (error) {
2230
+ if (!isMissingJsonSource(error)) throw error;
1546
2231
  }
1547
2232
  return nameToPath;
1548
2233
  }
@@ -1550,9 +2235,10 @@ var GeminiFamilyAdapter = class {
1550
2235
  const root = this.chatsRoot();
1551
2236
  let projectDirs;
1552
2237
  try {
1553
- projectDirs = await readdir7(root);
1554
- } catch {
1555
- return [];
2238
+ projectDirs = await readdir8(root);
2239
+ } catch (error) {
2240
+ if (isMissingPathError(error)) return [];
2241
+ throw error;
1556
2242
  }
1557
2243
  const projectCwds = await this.loadProjectCwds();
1558
2244
  const refs = [];
@@ -1561,9 +2247,10 @@ var GeminiFamilyAdapter = class {
1561
2247
  const chatsDir = join8(root, pd, "chats");
1562
2248
  let files;
1563
2249
  try {
1564
- files = await readdir7(chatsDir);
1565
- } catch {
1566
- continue;
2250
+ files = await readdir8(chatsDir);
2251
+ } catch (error) {
2252
+ if (isMissingPathError(error)) continue;
2253
+ throw error;
1567
2254
  }
1568
2255
  for (const f of files) {
1569
2256
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
@@ -1571,8 +2258,9 @@ var GeminiFamilyAdapter = class {
1571
2258
  let st;
1572
2259
  try {
1573
2260
  st = await stat7(path);
1574
- } catch {
1575
- continue;
2261
+ } catch (error) {
2262
+ if (isMissingPathError(error)) continue;
2263
+ throw error;
1576
2264
  }
1577
2265
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1578
2266
  if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
@@ -1582,12 +2270,7 @@ var GeminiFamilyAdapter = class {
1582
2270
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1583
2271
  }
1584
2272
  async parse(ref) {
1585
- let session;
1586
- try {
1587
- session = JSON.parse(await readFile8(ref.path, "utf8"));
1588
- } catch {
1589
- return [];
1590
- }
2273
+ const session = await readJsonFile(ref.path);
1591
2274
  const traceId = session.sessionId ?? ref.sessionId;
1592
2275
  const messages = session.messages ?? [];
1593
2276
  const rootId = `root:${traceId}`;
@@ -1662,7 +2345,7 @@ var GeminiFamilyAdapter = class {
1662
2345
  agent: this.service,
1663
2346
  tool: tc.name ?? "tool",
1664
2347
  step,
1665
- content: tc.args != null ? JSON.stringify(tc.args) : null
2348
+ extra: toolIoAttributes({ input: tc.args, output: tc.result })
1666
2349
  })
1667
2350
  );
1668
2351
  step += 1;
@@ -1678,7 +2361,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1678
2361
  };
1679
2362
 
1680
2363
  // src/adapters/opencode.ts
1681
- import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
2364
+ import { readdir as readdir9, stat as stat8 } from "fs/promises";
1682
2365
  import { homedir as homedir8 } from "os";
1683
2366
  import { join as join9 } from "path";
1684
2367
  var SERVICE7 = "opencode";
@@ -1700,9 +2383,10 @@ var OpencodeAdapter = class {
1700
2383
  const msgRoot = join9(this.storage(), "message");
1701
2384
  let sessionDirs;
1702
2385
  try {
1703
- sessionDirs = await readdir8(msgRoot);
1704
- } catch {
1705
- return [];
2386
+ sessionDirs = await readdir9(msgRoot);
2387
+ } catch (error) {
2388
+ if (isMissingPathError(error)) return [];
2389
+ throw error;
1706
2390
  }
1707
2391
  const refs = [];
1708
2392
  for (const sid of sessionDirs) {
@@ -1710,8 +2394,9 @@ var OpencodeAdapter = class {
1710
2394
  let st;
1711
2395
  try {
1712
2396
  st = await stat8(dir);
1713
- } catch {
1714
- continue;
2397
+ } catch (error) {
2398
+ if (isMissingPathError(error)) continue;
2399
+ throw error;
1715
2400
  }
1716
2401
  if (!st.isDirectory()) continue;
1717
2402
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1724,20 +2409,12 @@ var OpencodeAdapter = class {
1724
2409
  const traceId = ref.sessionId;
1725
2410
  const rootId = `root:${traceId}`;
1726
2411
  const partRoot = join9(this.storage(), "part");
1727
- let msgFiles;
1728
- try {
1729
- msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
1730
- } catch {
1731
- return [];
1732
- }
2412
+ const msgFiles = await listJsonFiles(ref.path);
1733
2413
  const messages = [];
1734
2414
  for (const f of msgFiles) {
1735
- try {
1736
- messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
1737
- } catch {
1738
- }
2415
+ messages.push(await readJsonFile(join9(ref.path, f)));
1739
2416
  }
1740
- 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));
1741
2418
  const spans = [
1742
2419
  span({
1743
2420
  traceId,
@@ -1745,59 +2422,65 @@ var OpencodeAdapter = class {
1745
2422
  parentSpanId: null,
1746
2423
  name: "session",
1747
2424
  kind: "AGENT",
1748
- startTime: isoOf(messages[0]?.msg.time?.created),
1749
- 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),
1750
2427
  service: SERVICE7,
1751
2428
  agent: SERVICE7
1752
2429
  })
1753
2430
  ];
1754
2431
  let step = 0;
1755
- for (const { msg } of messages) {
2432
+ for (const msg of messages) {
1756
2433
  const mid = msg.id ?? `m${step}`;
1757
2434
  const llmId = `llm:${mid}`;
1758
2435
  const pdir = join9(partRoot, mid);
1759
2436
  const parts = [];
2437
+ let partFiles = [];
1760
2438
  try {
1761
- const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1762
- for (const pf of partFiles) {
1763
- try {
1764
- parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
1765
- } catch {
1766
- }
1767
- }
1768
- } 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)));
1769
2445
  }
1770
2446
  const turnText = textOf7(parts);
1771
- spans.push(
1772
- span({
1773
- traceId,
1774
- spanId: llmId,
1775
- parentSpanId: rootId,
1776
- name: `message.${msg.role ?? "unknown"}`,
1777
- kind: "LLM",
1778
- startTime: isoOf(msg.time?.created),
1779
- endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1780
- service: SERVICE7,
1781
- agent: SERVICE7,
1782
- model: msg.modelID ?? null,
1783
- inputTokens: msg.tokens?.input ?? null,
1784
- outputTokens: msg.tokens?.output ?? null,
1785
- step,
1786
- content: turnText || null
1787
- })
1788
- );
1789
- step += 1;
1790
- 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 {
1791
2465
  spans.push(
1792
- userPromptSpan({
2466
+ span({
1793
2467
  traceId,
1794
- spanId: `${llmId}:user`,
2468
+ spanId: llmId,
1795
2469
  parentSpanId: rootId,
2470
+ name: `message.${msg.role ?? "unknown"}`,
2471
+ kind: "LLM",
1796
2472
  startTime: isoOf(msg.time?.created),
2473
+ endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1797
2474
  service: SERVICE7,
1798
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,
1799
2482
  step,
1800
- content: turnText
2483
+ content: turnText || null
1801
2484
  })
1802
2485
  );
1803
2486
  step += 1;
@@ -1809,7 +2492,7 @@ var OpencodeAdapter = class {
1809
2492
  span({
1810
2493
  traceId,
1811
2494
  spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
1812
- parentSpanId: llmId,
2495
+ parentSpanId: isUser ? rootId : llmId,
1813
2496
  name: `tool.${part.tool}`,
1814
2497
  kind: "TOOL",
1815
2498
  startTime: isoOf(msg.time?.created),
@@ -1820,7 +2503,10 @@ var OpencodeAdapter = class {
1820
2503
  agent: SERVICE7,
1821
2504
  tool: part.tool,
1822
2505
  step,
1823
- 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
+ })
1824
2510
  })
1825
2511
  );
1826
2512
  step += 1;
@@ -1831,21 +2517,10 @@ var OpencodeAdapter = class {
1831
2517
  };
1832
2518
 
1833
2519
  // src/adapters/pi.ts
1834
- import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
2520
+ import { readdir as readdir10, stat as stat9 } from "fs/promises";
1835
2521
  import { homedir as homedir9 } from "os";
1836
2522
  import { basename as basename6, join as join10 } from "path";
1837
2523
  var SERVICE8 = "pi";
1838
- function parseLines5(raw) {
1839
- const out = [];
1840
- for (const line of raw.split("\n")) {
1841
- if (!line) continue;
1842
- try {
1843
- out.push(JSON.parse(line));
1844
- } catch {
1845
- }
1846
- }
1847
- return out;
1848
- }
1849
2524
  function isToolBlock(b) {
1850
2525
  return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
1851
2526
  }
@@ -1867,18 +2542,20 @@ var PiAdapter = class {
1867
2542
  const root = this.root();
1868
2543
  let dirs;
1869
2544
  try {
1870
- dirs = await readdir9(root);
1871
- } catch {
1872
- return [];
2545
+ dirs = await readdir10(root);
2546
+ } catch (error) {
2547
+ if (isMissingPathError(error)) return [];
2548
+ throw error;
1873
2549
  }
1874
2550
  const refs = [];
1875
2551
  for (const dir of dirs) {
1876
2552
  const dp = join10(root, dir);
1877
2553
  let files;
1878
2554
  try {
1879
- files = await readdir9(dp);
1880
- } catch {
1881
- continue;
2555
+ files = await readdir10(dp);
2556
+ } catch (error) {
2557
+ if (isMissingPathError(error)) continue;
2558
+ throw error;
1882
2559
  }
1883
2560
  const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
1884
2561
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
@@ -1888,8 +2565,9 @@ var PiAdapter = class {
1888
2565
  let st;
1889
2566
  try {
1890
2567
  st = await stat9(path);
1891
- } catch {
1892
- continue;
2568
+ } catch (error) {
2569
+ if (isMissingPathError(error)) continue;
2570
+ throw error;
1893
2571
  }
1894
2572
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1895
2573
  const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
@@ -1898,27 +2576,25 @@ var PiAdapter = class {
1898
2576
  }
1899
2577
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1900
2578
  }
1901
- async parse(ref) {
1902
- const lines = parseLines5(await readFile10(ref.path, "utf8"));
1903
- const sessionLine = lines.find((l) => l.type === "session");
1904
- const traceId = sessionLine?.id ?? ref.sessionId;
1905
- const rootId = `root:${traceId}`;
1906
- const spans = [
1907
- span({
1908
- traceId,
1909
- spanId: rootId,
1910
- parentSpanId: null,
1911
- name: "session",
1912
- kind: "AGENT",
1913
- startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1914
- endTime: lines.at(-1)?.timestamp,
1915
- service: SERVICE8,
1916
- agent: SERVICE8
1917
- })
1918
- ];
2579
+ async parse(ref, options = {}) {
2580
+ const sourceTraceId = ref.sessionId;
2581
+ const sourceRootId = `root:${sourceTraceId}`;
2582
+ const spans = [];
1919
2583
  const toolByCallId = /* @__PURE__ */ new Map();
2584
+ let firstTimestamp;
2585
+ let lastTimestamp;
2586
+ let sessionLine;
2587
+ let sawLine = false;
1920
2588
  let step = 0;
1921
- 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
+ }
1922
2598
  if (l.type !== "message" || !l.message) continue;
1923
2599
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1924
2600
  const mid = l.id ?? `m${step}`;
@@ -1929,9 +2605,9 @@ var PiAdapter = class {
1929
2605
  if (prompt) {
1930
2606
  spans.push(
1931
2607
  userPromptSpan({
1932
- traceId,
2608
+ traceId: sourceTraceId,
1933
2609
  spanId: `${mid}:user`,
1934
- parentSpanId: rootId,
2610
+ parentSpanId: sourceRootId,
1935
2611
  startTime: ts,
1936
2612
  service: SERVICE8,
1937
2613
  agent: SERVICE8,
@@ -1945,9 +2621,9 @@ var PiAdapter = class {
1945
2621
  const errored = !!msg.errorMessage;
1946
2622
  spans.push(
1947
2623
  span({
1948
- traceId,
2624
+ traceId: sourceTraceId,
1949
2625
  spanId: llmId,
1950
- parentSpanId: rootId,
2626
+ parentSpanId: sourceRootId,
1951
2627
  name: `message.${msg.role ?? "unknown"}`,
1952
2628
  kind: "LLM",
1953
2629
  startTime: ts,
@@ -1969,7 +2645,7 @@ var PiAdapter = class {
1969
2645
  const name = b.toolName ?? b.name ?? "tool";
1970
2646
  const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
1971
2647
  const toolSpan = span({
1972
- traceId,
2648
+ traceId: sourceTraceId,
1973
2649
  spanId: `tool:${callId}`,
1974
2650
  parentSpanId: llmId,
1975
2651
  name: `tool.${name}`,
@@ -1979,7 +2655,7 @@ var PiAdapter = class {
1979
2655
  agent: SERVICE8,
1980
2656
  tool: name,
1981
2657
  step,
1982
- content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
2658
+ extra: toolIoAttributes({ input: b.input ?? b.args })
1983
2659
  });
1984
2660
  spans.push(toolSpan);
1985
2661
  toolByCallId.set(callId, toolSpan);
@@ -1991,30 +2667,37 @@ var PiAdapter = class {
1991
2667
  const err = b.isError === true || b.is_error === true;
1992
2668
  t.end_time = ts;
1993
2669
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2670
+ recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
1994
2671
  }
1995
2672
  }
1996
2673
  }
1997
2674
  }
1998
- 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];
1999
2693
  }
2000
2694
  };
2001
2695
 
2002
2696
  // src/adapters/qwen.ts
2003
- import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
2697
+ import { readdir as readdir11, stat as stat10 } from "fs/promises";
2004
2698
  import { homedir as homedir10 } from "os";
2005
2699
  import { basename as basename7, join as join11 } from "path";
2006
2700
  var SERVICE9 = "qwen";
2007
- function parseLines6(raw) {
2008
- const out = [];
2009
- for (const line of raw.split("\n")) {
2010
- if (!line) continue;
2011
- try {
2012
- out.push(JSON.parse(line));
2013
- } catch {
2014
- }
2015
- }
2016
- return out;
2017
- }
2018
2701
  function textOf9(parts) {
2019
2702
  if (!parts) return "";
2020
2703
  return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
@@ -2029,18 +2712,20 @@ var QwenAdapter = class {
2029
2712
  const root = this.root();
2030
2713
  let projectDirs;
2031
2714
  try {
2032
- projectDirs = await readdir10(root);
2033
- } catch {
2034
- return [];
2715
+ projectDirs = await readdir11(root);
2716
+ } catch (error) {
2717
+ if (isMissingPathError(error)) return [];
2718
+ throw error;
2035
2719
  }
2036
2720
  const refs = [];
2037
2721
  for (const pd of projectDirs) {
2038
2722
  const chatsDir = join11(root, pd, "chats");
2039
2723
  let files;
2040
2724
  try {
2041
- files = await readdir10(chatsDir);
2042
- } catch {
2043
- continue;
2725
+ files = await readdir11(chatsDir);
2726
+ } catch (error) {
2727
+ if (isMissingPathError(error)) continue;
2728
+ throw error;
2044
2729
  }
2045
2730
  if (opts.cwd) continue;
2046
2731
  for (const f of files) {
@@ -2049,8 +2734,9 @@ var QwenAdapter = class {
2049
2734
  let st;
2050
2735
  try {
2051
2736
  st = await stat10(path);
2052
- } catch {
2053
- continue;
2737
+ } catch (error) {
2738
+ if (isMissingPathError(error)) continue;
2739
+ throw error;
2054
2740
  }
2055
2741
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
2056
2742
  refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
@@ -2058,36 +2744,32 @@ var QwenAdapter = class {
2058
2744
  }
2059
2745
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
2060
2746
  }
2061
- async parse(ref) {
2062
- const records = parseLines6(await readFile11(ref.path, "utf8"));
2063
- const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
2064
- const rootId = `root:${traceId}`;
2065
- const spans = [
2066
- span({
2067
- traceId,
2068
- spanId: rootId,
2069
- parentSpanId: null,
2070
- name: "session",
2071
- kind: "AGENT",
2072
- startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2073
- endTime: records.at(-1)?.timestamp,
2074
- service: SERVICE9,
2075
- agent: SERVICE9
2076
- })
2077
- ];
2747
+ async parse(ref, options = {}) {
2748
+ const sourceTraceId = ref.sessionId;
2749
+ const sourceRootId = `root:${sourceTraceId}`;
2750
+ const spans = [];
2078
2751
  const openToolsByName = /* @__PURE__ */ new Map();
2752
+ let discoveredTraceId;
2753
+ let firstTimestamp;
2754
+ let lastTimestamp;
2755
+ let sawRecord = false;
2079
2756
  let step = 0;
2080
- let lastLlm = rootId;
2081
- 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;
2082
2764
  const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
2083
2765
  const role = r.message?.role;
2084
2766
  if (r.type === "assistant" || role === "model") {
2085
2767
  const llmId = `llm:${r.uuid ?? step}`;
2086
2768
  spans.push(
2087
2769
  span({
2088
- traceId,
2770
+ traceId: sourceTraceId,
2089
2771
  spanId: llmId,
2090
- parentSpanId: rootId,
2772
+ parentSpanId: sourceRootId,
2091
2773
  name: "llm.turn",
2092
2774
  kind: "LLM",
2093
2775
  startTime: ts,
@@ -2100,13 +2782,12 @@ var QwenAdapter = class {
2100
2782
  content: capText(textOf9(r.message?.parts)) || null
2101
2783
  })
2102
2784
  );
2103
- lastLlm = llmId;
2104
2785
  step += 1;
2105
2786
  for (const p of r.message?.parts ?? []) {
2106
2787
  if (!p.functionCall?.name) continue;
2107
2788
  const name = p.functionCall.name;
2108
2789
  const t = span({
2109
- traceId,
2790
+ traceId: sourceTraceId,
2110
2791
  spanId: `tool:${name}:${step}`,
2111
2792
  parentSpanId: llmId,
2112
2793
  name: `tool.${name}`,
@@ -2116,7 +2797,7 @@ var QwenAdapter = class {
2116
2797
  agent: SERVICE9,
2117
2798
  tool: name,
2118
2799
  step,
2119
- content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
2800
+ extra: toolIoAttributes({ input: p.functionCall.args })
2120
2801
  });
2121
2802
  spans.push(t);
2122
2803
  const q = openToolsByName.get(name) ?? [];
@@ -2129,9 +2810,9 @@ var QwenAdapter = class {
2129
2810
  if (prompt) {
2130
2811
  spans.push(
2131
2812
  userPromptSpan({
2132
- traceId,
2813
+ traceId: sourceTraceId,
2133
2814
  spanId: `user:${r.uuid ?? step}`,
2134
- parentSpanId: rootId,
2815
+ parentSpanId: sourceRootId,
2135
2816
  startTime: ts,
2136
2817
  service: SERVICE9,
2137
2818
  agent: SERVICE9,
@@ -2150,11 +2831,29 @@ var QwenAdapter = class {
2150
2831
  if (t) {
2151
2832
  t.end_time = ts;
2152
2833
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2834
+ recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
2153
2835
  }
2154
2836
  }
2155
2837
  }
2156
2838
  }
2157
- 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];
2158
2857
  }
2159
2858
  };
2160
2859
 
@@ -2554,14 +3253,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
2554
3253
  }
2555
3254
 
2556
3255
  // src/session-source.ts
2557
- async function parseSession(adapter, ref) {
2558
- 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);
2559
3261
  const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
2560
3262
  if (repo.cwd) ref.cwd = repo.cwd;
2561
3263
  stampRepoAttrs(spans, repo.attrs);
2562
3264
  await stampSpanWorkdirRepoAttrs(spans);
2563
3265
  return spans;
2564
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
+ };
2565
3275
  function refKey(ref) {
2566
3276
  return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
2567
3277
  }
@@ -2571,7 +3281,7 @@ async function locateSessions(adapter, opts = {}) {
2571
3281
  const refs = await adapter.locate({ sinceMs: opts.sinceMs });
2572
3282
  const byKey = /* @__PURE__ */ new Map();
2573
3283
  for (const ref of refs) {
2574
- if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
3284
+ if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
2575
3285
  byKey.set(refKey(ref), ref);
2576
3286
  }
2577
3287
  return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -2583,7 +3293,8 @@ async function* scanSessions(opts) {
2583
3293
  try {
2584
3294
  refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
2585
3295
  } catch (err) {
2586
- opts.onError?.(err);
3296
+ if (!opts.onError) throw err;
3297
+ opts.onError(err);
2587
3298
  continue;
2588
3299
  }
2589
3300
  if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
@@ -2591,12 +3302,12 @@ async function* scanSessions(opts) {
2591
3302
  if (opts.signal?.aborted) return;
2592
3303
  let spans;
2593
3304
  try {
2594
- spans = await parseSession(adapter, ref);
3305
+ spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
2595
3306
  } catch (err) {
2596
- opts.onError?.(err, ref);
3307
+ if (!opts.onError) throw err;
3308
+ opts.onError(err, ref);
2597
3309
  continue;
2598
3310
  }
2599
- if (spans.length === 0) continue;
2600
3311
  yield { adapter, ref, spans };
2601
3312
  }
2602
3313
  }
@@ -2606,16 +3317,16 @@ async function* scanSessions(opts) {
2606
3317
  import { mkdtemp, writeFile } from "fs/promises";
2607
3318
  import { tmpdir } from "os";
2608
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";
2609
3324
  function stringAttr(span2, key) {
2610
3325
  const value = span2.attributes[key];
2611
3326
  return typeof value === "string" && value.length > 0 ? value : void 0;
2612
3327
  }
2613
- function numberAttr(span2, key) {
2614
- const value = span2.attributes[key];
2615
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
2616
- }
2617
3328
  function spanKind(span2) {
2618
- return stringAttr(span2, "openinference.span.kind");
3329
+ return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
2619
3330
  }
2620
3331
  function repoFromSpans(spans) {
2621
3332
  const attrs = {};
@@ -2641,7 +3352,7 @@ function summarizeTools(spans) {
2641
3352
  const byTool = /* @__PURE__ */ new Map();
2642
3353
  for (const span2 of spans) {
2643
3354
  if (spanKind(span2) !== "TOOL") continue;
2644
- const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
3355
+ const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
2645
3356
  const current = byTool.get(name) ?? { calls: 0, errors: 0 };
2646
3357
  current.calls += 1;
2647
3358
  if (span2.status.code === "ERROR") current.errors += 1;
@@ -2650,7 +3361,6 @@ function summarizeTools(spans) {
2650
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));
2651
3362
  }
2652
3363
  async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2653
- const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
2654
3364
  const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
2655
3365
  const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
2656
3366
  const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
@@ -2658,10 +3368,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2658
3368
  const loopFindings = pipelines.stuckLoops.findings;
2659
3369
  const { firstSpanAt, lastSpanAt } = timeBounds(spans);
2660
3370
  const repo = repoFromSpans(spans);
3371
+ const execution = summarizeSpanExecution(spans, {
3372
+ experimentId: `traces-policy-evidence:${ref.sessionId}`
3373
+ });
2661
3374
  return {
2662
3375
  schemaVersion: 1,
2663
3376
  kind: "traces.policy_evidence.session",
2664
3377
  generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3378
+ execution,
2665
3379
  session: {
2666
3380
  harness: ref.harness,
2667
3381
  sessionId: ref.sessionId,
@@ -2672,12 +3386,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2672
3386
  repo,
2673
3387
  metrics: {
2674
3388
  spanCount: spans.length,
2675
- llmTurnCount: llmSpans.length,
3389
+ llmTurnCount: execution.execution.modelCalls.events,
2676
3390
  toolCallCount: toolSpans.length,
2677
3391
  erroredToolCallCount,
2678
- inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
2679
- outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
2680
- 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),
2681
3395
  tools: summarizeTools(spans),
2682
3396
  firstSpanAt,
2683
3397
  lastSpanAt
@@ -2859,6 +3573,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2859
3573
  const report = { redactionCount: 0, byRule: {} };
2860
3574
  const out = spans.map((s) => {
2861
3575
  const attributes = redactValue(s.attributes, rules, report).value;
3576
+ normalizeToolIoAttributes(attributes);
2862
3577
  let status = s.status;
2863
3578
  if (status.message) {
2864
3579
  const m = redactValue(status.message, rules, report).value;
@@ -2869,33 +3584,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2869
3584
  return { spans: out, report };
2870
3585
  }
2871
3586
  async function applyRedactor(spans, redactor) {
2872
- const idx = [];
3587
+ const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
3588
+ const fields = [];
2873
3589
  const texts = [];
2874
3590
  spans.forEach((s, i) => {
2875
- const c = s.attributes["content"];
2876
- if (typeof c === "string" && c.length > 0) {
2877
- idx.push(i);
2878
- 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);
2879
3601
  }
2880
3602
  });
2881
3603
  const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
2882
- 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
+ }
2883
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
+ }
2884
3612
  let changed = 0;
2885
- idx.forEach((spanI, k) => {
3613
+ fields.forEach(({ spanIndex, key }, k) => {
2886
3614
  if (scrubbed[k] !== texts[k]) {
2887
- 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
+ }
2888
3620
  changed += 1;
2889
3621
  }
2890
3622
  });
3623
+ for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
2891
3624
  return { spans: out, changed };
2892
3625
  }
2893
3626
 
2894
3627
  // src/file-export.ts
2895
- import { createHash } from "crypto";
2896
- 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";
2897
3630
  import { tmpdir as tmpdir2 } from "os";
2898
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";
2899
3647
  function isObject(value) {
2900
3648
  return value != null && typeof value === "object" && !Array.isArray(value);
2901
3649
  }
@@ -2915,7 +3663,7 @@ function stableJson(value) {
2915
3663
  });
2916
3664
  }
2917
3665
  function hashId(value, chars = 16) {
2918
- return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
3666
+ return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
2919
3667
  }
2920
3668
  function isoTime(value) {
2921
3669
  if (typeof value === "string") {
@@ -3018,6 +3766,11 @@ function requireOpenInferenceRows(rows) {
3018
3766
  return rows;
3019
3767
  }
3020
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
+ }
3021
3774
  if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
3022
3775
  return rows;
3023
3776
  }
@@ -3049,6 +3802,7 @@ function policyEvidenceToSpans(records) {
3049
3802
  "traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
3050
3803
  "traces.metrics.input_tokens": record.metrics.inputTokens,
3051
3804
  "traces.metrics.output_tokens": record.metrics.outputTokens,
3805
+ "traces.execution": stableJson(record.execution),
3052
3806
  "traces.metrics.models": record.metrics.models,
3053
3807
  "traces.metrics.tools": stableJson(record.metrics.tools),
3054
3808
  "traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
@@ -3076,6 +3830,11 @@ function policyEvidenceToSpans(records) {
3076
3830
  endTime: end,
3077
3831
  service: record.session.harness,
3078
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,
3079
3838
  content: capText(stableJson({
3080
3839
  session: record.session,
3081
3840
  repo: record.repo,
@@ -3106,6 +3865,16 @@ function findStringKey(value, keys, depth = 4) {
3106
3865
  }
3107
3866
  return void 0;
3108
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
+ }
3109
3878
  function eventTimestamp(row) {
3110
3879
  return firstTimeDeep(row, [
3111
3880
  "timestamp",
@@ -3148,7 +3917,8 @@ function eventKind(row, type) {
3148
3917
  }
3149
3918
  function sandboxEventsToSpans(rows, wrapper) {
3150
3919
  const context = wrapper ?? {};
3151
- 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)}`;
3152
3922
  const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
3153
3923
  const times = rows.flatMap((row) => {
3154
3924
  const time = eventTimestamp(row);
@@ -3184,10 +3954,16 @@ function sandboxEventsToSpans(rows, wrapper) {
3184
3954
  const { status, message } = eventStatus(row, type);
3185
3955
  const { kind, tool } = eventKind(row, type);
3186
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;
3187
3958
  const extra = {
3188
3959
  "traces.source_format": "sandbox-events",
3189
3960
  "traces.event.type": type,
3190
- "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
+ }) : {}
3191
3967
  };
3192
3968
  copyDefined(extra, {
3193
3969
  "traces.event.id": stringValue(row.id),
@@ -3212,7 +3988,11 @@ function sandboxEventsToSpans(rows, wrapper) {
3212
3988
  extra
3213
3989
  }));
3214
3990
  });
3215
- return spans;
3991
+ if (!sessionId) return spans;
3992
+ return spans.map((item) => ({
3993
+ ...item,
3994
+ attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
3995
+ }));
3216
3996
  }
3217
3997
  function statusCode(value) {
3218
3998
  return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
@@ -3236,8 +4016,8 @@ function unixNanoTime(value) {
3236
4016
  }
3237
4017
  function firstNumber(row, attrs, keys) {
3238
4018
  for (const key of keys) {
3239
- const value = numberValue(row[key]) ?? numberValue(attrs[key]);
3240
- if (value != null) return value;
4019
+ const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
4020
+ if (value !== null) return value;
3241
4021
  }
3242
4022
  return void 0;
3243
4023
  }
@@ -3249,6 +4029,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
3249
4029
  "tool.name",
3250
4030
  "llm.input_tokens",
3251
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,
3252
4038
  "step",
3253
4039
  "content"
3254
4040
  ]);
@@ -3263,10 +4049,27 @@ function preserveRawAttributes(attrs) {
3263
4049
  }
3264
4050
  return preserved;
3265
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
+ }
3266
4068
  function intelligenceSpanKind(name, attrs) {
3267
4069
  const lowered = name.toLowerCase();
3268
4070
  const spanType = stringValue(attrs["span.type"])?.toLowerCase();
3269
4071
  if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
4072
+ if (isToolTelemetrySpan(attrs)) return "CHAIN";
3270
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) {
3271
4074
  return "TOOL";
3272
4075
  }
@@ -3281,9 +4084,12 @@ function intelligenceSpansToSpans(rows) {
3281
4084
  const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
3282
4085
  const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
3283
4086
  const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
3284
- 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);
3285
4090
  const extra = {
3286
4091
  ...preserveRawAttributes(attrs),
4092
+ ...kind === "TOOL" ? capturedToolIo(attrs) : {},
3287
4093
  "traces.source_format": "intelligence-spans",
3288
4094
  "traces.row.index": index
3289
4095
  };
@@ -3297,7 +4103,6 @@ function intelligenceSpansToSpans(rows) {
3297
4103
  "generation.id": stringValue(row.generation),
3298
4104
  "cell.id": stringValue(row.cell_id),
3299
4105
  "project.key": stringValue(row.project_key),
3300
- "llm.cost_usd": numberValue(row.cost_usd),
3301
4106
  "redaction.version": stringValue(row.redaction_version),
3302
4107
  "traces.received_at": isoTime(row.received_at)
3303
4108
  });
@@ -3306,17 +4111,21 @@ function intelligenceSpansToSpans(rows) {
3306
4111
  spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
3307
4112
  parentSpanId: stringValue(row.parent_span_id) ?? null,
3308
4113
  name,
3309
- kind: intelligenceSpanKind(name, attrs),
4114
+ kind,
3310
4115
  startTime,
3311
4116
  endTime,
3312
- status: statusCode(row.status_code),
3313
- statusMessage: stringValue(row.status_message),
4117
+ status: statusCode(row.status_code ?? sourceStatus?.code),
4118
+ statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
3314
4119
  service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
3315
4120
  agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
3316
4121
  model,
3317
4122
  tool,
3318
- inputTokens: firstNumber(row, attrs, ["input_tokens", "llm.input_tokens"]),
3319
- 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]),
3320
4129
  step: numberValue(attrs["interaction.sequence"]) ?? index,
3321
4130
  content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
3322
4131
  extra
@@ -3329,15 +4138,21 @@ function openInferenceToSpans(rows) {
3329
4138
  const resource = objectValue(row.resource);
3330
4139
  const resourceAttrs = objectValue(resource?.attributes);
3331
4140
  if (resourceAttrs) copyDefined(attrs, resourceAttrs);
3332
- const kind = stringValue(row.kind);
3333
- 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));
3334
4149
  const status = objectValue(row.status);
3335
4150
  const message = stringValue(status?.message);
3336
4151
  return {
3337
4152
  trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
3338
4153
  span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
3339
4154
  parent_span_id: stringValue(row.parent_span_id) || null,
3340
- name: stringValue(row.name) ?? "span",
4155
+ name,
3341
4156
  start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3342
4157
  end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3343
4158
  status: { code: statusCode(status?.code), ...message ? { message } : {} },
@@ -3384,7 +4199,40 @@ function exportTraceEvidenceText(text, opts = {}) {
3384
4199
  return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
3385
4200
  }
3386
4201
  async function exportTraceEvidenceFile(inputPath, opts = {}) {
3387
- 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");
3388
4236
  return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
3389
4237
  }
3390
4238
  async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
@@ -3395,7 +4243,7 @@ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
3395
4243
  }
3396
4244
 
3397
4245
  // src/inspect.ts
3398
- 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";
3399
4247
  import { tmpdir as tmpdir3 } from "os";
3400
4248
  import { basename as basename8, join as join15 } from "path";
3401
4249
  var severityRank = {
@@ -3674,14 +4522,14 @@ function assertSessionIndex(value) {
3674
4522
  }
3675
4523
  const candidate = value;
3676
4524
  if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
3677
- 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");
3678
4526
  }
3679
4527
  if (!candidate.totals || typeof candidate.totals !== "object") {
3680
4528
  throw new Error("session index is missing totals");
3681
4529
  }
3682
4530
  }
3683
4531
  async function readSessionIndexFile(path) {
3684
- const parsed = JSON.parse(await readFile13(path, "utf8"));
4532
+ const parsed = JSON.parse(await readFile4(path, "utf8"));
3685
4533
  assertSessionIndex(parsed);
3686
4534
  return parsed;
3687
4535
  }
@@ -3779,6 +4627,8 @@ function analyzeReactions(spans) {
3779
4627
 
3780
4628
  // src/report.ts
3781
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;
3782
4632
  var SEVERITY_BADGE = {
3783
4633
  critical: "\u{1F534} CRITICAL",
3784
4634
  high: "\u{1F7E0} HIGH",
@@ -3790,23 +4640,105 @@ function deterministicSummaryText(summary) {
3790
4640
  const parts = [];
3791
4641
  if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
3792
4642
  if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
3793
- if (summary.toolErrorRuns > 0) parts.push(`${summary.toolErrorRuns} tool-error run(s)`);
4643
+ if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
3794
4644
  if (parts.length === 0) return "0 deterministic signals";
3795
4645
  if (parts.length === 1) return parts[0];
3796
4646
  return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
3797
4647
  }
3798
- function plural(count, singular, pluralForm = `${singular}s`) {
3799
- 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");
3800
4732
  }
3801
4733
  function summarizeDeterministicSignals(pipelines, reactions) {
3802
4734
  const stuckLoops = pipelines.stuckLoops.findings.length;
3803
- const reactionSignals = Object.values(reactions.signals).reduce((total, count) => total + count, 0);
3804
- 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;
3805
4737
  return {
3806
4738
  stuckLoops,
3807
4739
  reactionSignals,
3808
- toolErrorRuns: toolErrorRuns2,
3809
- totalSignals: stuckLoops + reactionSignals + toolErrorRuns2
4740
+ failedRuns,
4741
+ totalSignals: stuckLoops + reactionSignals + failedRuns
3810
4742
  };
3811
4743
  }
3812
4744
  function renderReport(result, meta) {
@@ -3814,14 +4746,93 @@ function renderReport(result, meta) {
3814
4746
  const findings = [...result.findings].sort(
3815
4747
  (a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
3816
4748
  );
3817
- const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, toolErrorRuns: 0, totalSignals: 0 };
3818
- 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")}`;
3819
4751
  lines.push(`# Trace analysis \u2014 ${meta.harness}`);
3820
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)`;
3821
4754
  lines.push(
3822
- `${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)}.`
3823
4756
  );
3824
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
+ }
3825
4836
  lines.push("| Analyst | Status | Findings | Latency |");
3826
4837
  lines.push("|---|---|---|---|");
3827
4838
  for (const s of result.per_analyst) {
@@ -3829,7 +4840,7 @@ function renderReport(result, meta) {
3829
4840
  }
3830
4841
  lines.push("");
3831
4842
  if (findings.length === 0 && deterministic.totalSignals === 0) {
3832
- lines.push("_No findings \u2014 no behavioral inefficiencies or failure modes detected._");
4843
+ lines.push("_No supported behavioral findings in the captured fields._");
3833
4844
  } else if (findings.length === 0) {
3834
4845
  lines.push(
3835
4846
  `_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
@@ -3850,6 +4861,7 @@ function renderReport(result, meta) {
3850
4861
  if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
3851
4862
  lines.push(`- **Confidence:** ${f.confidence}`);
3852
4863
  if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
4864
+ if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
3853
4865
  for (const ev of f.evidence_refs.slice(0, 3)) {
3854
4866
  const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
3855
4867
  lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
@@ -3864,20 +4876,79 @@ function renderReport(result, meta) {
3864
4876
  return lines.join("\n");
3865
4877
  }
3866
4878
  function renderPipelines(pr) {
3867
- 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
+ }
3868
4894
  if (pr.stuckLoops.findings.length === 0) {
3869
- 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).");
3870
4896
  } else {
3871
4897
  lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
3872
4898
  for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
3873
4899
  lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
3874
4900
  }
3875
4901
  }
3876
- for (const m of pr.toolUse) {
3877
- 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";
3878
4937
  lines.push(
3879
- `- **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}`
3880
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
+ }
3881
4952
  }
3882
4953
  lines.push("");
3883
4954
  return lines.join("\n");
@@ -3931,14 +5002,41 @@ function renderReactions(rr) {
3931
5002
  }
3932
5003
  function renderAdoption(ar) {
3933
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
+ }
3934
5032
  lines.push(
3935
- `- **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)`
3936
5034
  );
3937
5035
  lines.push(
3938
5036
  `- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
3939
5037
  );
3940
5038
  lines.push(
3941
- `- **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.`
3942
5040
  );
3943
5041
  lines.push("");
3944
5042
  const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
@@ -3981,9 +5079,6 @@ var severityRank2 = {
3981
5079
  low: 3,
3982
5080
  info: 4
3983
5081
  };
3984
- function slug(input) {
3985
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
3986
- }
3987
5082
  function json(value) {
3988
5083
  return `${JSON.stringify(value, null, 2)}
3989
5084
  `;
@@ -3996,13 +5091,76 @@ function evidence(kind, uri, excerpt) {
3996
5091
  return excerpt ? { kind, uri, excerpt } : { kind, uri };
3997
5092
  }
3998
5093
  function totalReactionSignals(reactions) {
3999
- return Object.values(reactions.signals).reduce((sum, count) => sum + count, 0);
5094
+ return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
4000
5095
  }
4001
- function toolErrorRuns(pipelines) {
4002
- 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
+ });
4003
5159
  }
4004
5160
  function deterministicFindings(pipelines, reactions, adoption) {
4005
5161
  const findings = [];
5162
+ const coverage = traceCoverageFinding(pipelines, adoption);
5163
+ if (coverage) findings.push(coverage);
4006
5164
  const loops = pipelines.stuckLoops.findings;
4007
5165
  if (loops.length > 0) {
4008
5166
  const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
@@ -4022,19 +5180,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
4022
5180
  metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
4023
5181
  }));
4024
5182
  }
4025
- const erroredRuns = toolErrorRuns(pipelines);
4026
- 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);
4027
5187
  findings.push(makeFinding({
4028
5188
  analyst_id: "traces-deterministic",
4029
- area: "tool-use",
4030
- claim: `${erroredRuns} run(s) included tool errors`,
4031
- severity: erroredRuns >= 5 ? "high" : "medium",
4032
- rationale: "Tool errors are expensive when the agent keeps planning as if the call succeeded.",
4033
- evidence_refs: [evidence("metric", "pipelines.tool_error_runs", `${erroredRuns} run(s) with errorRate > 0`)],
4034
- recommended_action: "Fix the highest-frequency failing tool path first, then make the agent inspect failure output before retrying.",
4035
- validation_plan: "Rerun the same trace window and require the tool-error run count and repeated retries to decrease.",
4036
- confidence: 0.9,
4037
- 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
+ }
4038
5211
  }));
4039
5212
  }
4040
5213
  const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
@@ -4053,7 +5226,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
4053
5226
  }));
4054
5227
  }
4055
5228
  const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
4056
- if (adoption.sessionCount > 0 && skillRuns === 0) {
5229
+ const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
5230
+ if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
4057
5231
  findings.push(makeFinding({
4058
5232
  analyst_id: "traces-deterministic",
4059
5233
  area: "workflow",
@@ -4064,7 +5238,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
4064
5238
  recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
4065
5239
  validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
4066
5240
  confidence: 0.75,
4067
- metadata: { source: "traces.adoption", sessionCount: adoption.sessionCount }
5241
+ metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
4068
5242
  }));
4069
5243
  }
4070
5244
  return findings;
@@ -4125,41 +5299,6 @@ function normalizeExternalFindings(results) {
4125
5299
  }
4126
5300
  return out;
4127
5301
  }
4128
- function sourceOf(finding2) {
4129
- if (finding2.analyst_id.startsWith("external:")) return "external";
4130
- if (finding2.analyst_id === "traces-deterministic") return "deterministic";
4131
- return "analyst";
4132
- }
4133
- function recommendationTitle(finding2) {
4134
- if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
4135
- return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
4136
- }
4137
- function buildRecommendations(findings) {
4138
- 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) => ({
4139
- schemaVersion: 1,
4140
- id: `rec-${index + 1}-${slug(finding2.area)}`,
4141
- severity: finding2.severity,
4142
- title: recommendationTitle(finding2),
4143
- action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
4144
- rationale: finding2.rationale ?? finding2.claim,
4145
- evidenceRefs: finding2.evidence_refs,
4146
- findingIds: [finding2.finding_id],
4147
- validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
4148
- source: sourceOf(finding2)
4149
- }));
4150
- }
4151
- function buildClaims(findings) {
4152
- return findings.map((finding2, index) => ({
4153
- schemaVersion: 1,
4154
- id: `claim-${index + 1}-${slug(finding2.area)}`,
4155
- severity: finding2.severity,
4156
- text: finding2.claim,
4157
- area: finding2.area,
4158
- sourceFindingId: finding2.finding_id,
4159
- evidenceRefs: finding2.evidence_refs,
4160
- validationPlan: finding2.validation_plan
4161
- }));
4162
- }
4163
5302
  function buildEvidenceRows(findings) {
4164
5303
  return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
4165
5304
  schemaVersion: 1,
@@ -4171,31 +5310,10 @@ function buildEvidenceRows(findings) {
4171
5310
  evidence: ref
4172
5311
  })));
4173
5312
  }
4174
- function renderRecommendations(recommendations) {
4175
- const lines = ["## recommendations", ""];
4176
- if (recommendations.length === 0) {
4177
- lines.push("- No recommendations emitted.");
4178
- lines.push("");
4179
- return lines.join("\n");
4180
- }
4181
- for (const rec of recommendations.slice(0, 10)) {
4182
- lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
4183
- lines.push("");
4184
- lines.push(`- **Action:** ${rec.action}`);
4185
- lines.push(`- **Why:** ${rec.rationale}`);
4186
- lines.push(`- **Check:** ${rec.validationPlan}`);
4187
- for (const ref of rec.evidenceRefs.slice(0, 3)) {
4188
- lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
4189
- }
4190
- lines.push("");
4191
- }
4192
- return lines.join("\n");
4193
- }
4194
5313
  function renderFindingPacket(packet, title = "Trace finding packet") {
4195
5314
  const lines = [`# ${title}`, ""];
4196
- 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.`);
4197
5316
  lines.push("");
4198
- lines.push(renderRecommendations(packet.recommendations));
4199
5317
  if (packet.findings.length > 0) {
4200
5318
  lines.push("## findings");
4201
5319
  lines.push("");
@@ -4221,33 +5339,10 @@ function buildTraceFindingPacket(opts) {
4221
5339
  kind: "traces.finding_packet",
4222
5340
  generatedAt,
4223
5341
  source: opts.source ?? "traces",
4224
- findings: opts.findings,
4225
- recommendations: buildRecommendations(opts.findings),
4226
- claims: buildClaims(opts.findings)
5342
+ findings: opts.findings
4227
5343
  };
4228
5344
  return { ...packet, report: renderFindingPacket(packet, opts.title) };
4229
5345
  }
4230
- function buildDefaultImprovementProposals(input) {
4231
- return input.recommendations.slice(0, 5).map((recommendation, index) => {
4232
- const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
4233
- const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
4234
- return {
4235
- id: `proposal-${index + 1}-${slug(recommendation.title)}`,
4236
- title: recommendation.title,
4237
- description: [
4238
- recommendation.action,
4239
- "",
4240
- `Why: ${recommendation.rationale}`,
4241
- `Validation: ${recommendation.validationPlan}`,
4242
- "",
4243
- "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."
4244
- ].join("\n"),
4245
- recommendationIds: [recommendation.id],
4246
- validationCommand: "traces improve --last 1 --dir .traces/improvement",
4247
- evidenceRefs
4248
- };
4249
- });
4250
- }
4251
5346
  function renderExternal(results) {
4252
5347
  if (results.length === 0) return "";
4253
5348
  const lines = ["## external analyzers", ""];
@@ -4263,11 +5358,13 @@ function renderInvestigationReport(result, analystResult) {
4263
5358
  const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
4264
5359
  harness: result.harness,
4265
5360
  sessionCount: result.sessionCount,
5361
+ unassignedTraceCount: result.unassignedTraceCount,
4266
5362
  spanCount: result.spanCount,
4267
5363
  otlpPath: result.otlpPath,
4268
- deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions)
5364
+ execution: result.execution,
5365
+ deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
5366
+ sources: result.sources
4269
5367
  })}
4270
- ${renderRecommendations(result.recommendations)}
4271
5368
  ${renderPipelines(result.pipelines)}
4272
5369
  ${renderReactions(result.reactions)}
4273
5370
  ${renderAdoption(result.adoption)}`;
@@ -4344,19 +5441,18 @@ async function runTraceInvestigation(opts) {
4344
5441
  ...deterministicFindings(pipelines, reactions, adoption),
4345
5442
  ...normalizeExternalFindings(external)
4346
5443
  ];
4347
- const recommendations = buildRecommendations(findings);
4348
- const claims = buildClaims(findings);
4349
5444
  const partial = {
4350
5445
  schemaVersion: 1,
4351
5446
  kind: "traces.investigation",
4352
5447
  generatedAt,
4353
5448
  harness: opts.harness,
4354
- sessionCount: opts.sessionCount,
5449
+ sessionCount: adoption.identifiedSessionCount,
5450
+ unassignedTraceCount: adoption.unassignedTraceCount,
5451
+ sources: opts.sources,
4355
5452
  spanCount: opts.spans.length,
4356
5453
  otlpPath: analysis.otlpPath,
5454
+ execution: analysis.execution,
4357
5455
  findings,
4358
- recommendations,
4359
- claims,
4360
5456
  pipelines,
4361
5457
  reactions,
4362
5458
  adoption,
@@ -4388,70 +5484,38 @@ async function runTraceStoreInvestigation(opts) {
4388
5484
  analystResult
4389
5485
  };
4390
5486
  }
4391
- function replayProof(result, proposals) {
4392
- const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
4393
- return {
4394
- schemaVersion: 1,
4395
- kind: "traces.improvement_replay",
4396
- generatedAt: result.generatedAt,
4397
- status: proposals.length > 0 ? "proposal-only" : "not-run",
4398
- candidateApplied: false,
4399
- baseline: {
4400
- spanCount: result.spanCount,
4401
- findingCount: result.findings.length,
4402
- recommendationCount: result.recommendations.length,
4403
- stuckLoops: deterministic.stuckLoops,
4404
- reactionSignals: deterministic.reactionSignals,
4405
- toolErrorRuns: deterministic.toolErrorRuns
4406
- },
4407
- proposals,
4408
- 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."
4409
- };
4410
- }
4411
5487
  async function writeTraceImprovementArtifacts(result, outDir) {
4412
5488
  const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
4413
5489
  await mkdir(directory, { recursive: true });
4414
5490
  const paths = {
4415
5491
  directory,
4416
- findings: join16(directory, "findings.json"),
4417
- recommendations: join16(directory, "recommendations.json"),
4418
- proposals: join16(directory, "proposals.json"),
5492
+ result: join16(directory, "result.json"),
4419
5493
  evidence: join16(directory, "evidence.jsonl"),
4420
- claims: join16(directory, "claims.json"),
4421
5494
  report: join16(directory, "report.md"),
4422
- replay: join16(directory, "replay-before-after.json")
5495
+ traces: result.otlpPath
4423
5496
  };
5497
+ const { report, ...machineResult } = result;
4424
5498
  await Promise.all([
4425
- writeFile4(paths.findings, json(result.findings), "utf8"),
4426
- writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
4427
- writeFile4(paths.proposals, json(result.proposals), "utf8"),
5499
+ writeFile4(paths.result, json(machineResult), "utf8"),
4428
5500
  writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
4429
- writeFile4(paths.claims, json(result.claims), "utf8"),
4430
- writeFile4(paths.report, result.report, "utf8"),
4431
- writeFile4(paths.replay, json(result.replay), "utf8")
5501
+ writeFile4(paths.report, report, "utf8")
4432
5502
  ]);
4433
5503
  return paths;
4434
5504
  }
4435
- async function runTraceImprovementLoop(opts) {
4436
- const investigation = await runTraceInvestigation(opts);
4437
- const adapterInput = {
4438
- findings: investigation.findings,
4439
- recommendations: investigation.recommendations,
4440
- claims: investigation.claims,
4441
- spans: opts.spans,
4442
- otlpPath: investigation.otlpPath
4443
- };
4444
- const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
4445
- 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
+ });
4446
5512
  const result = {
4447
5513
  ...investigation,
4448
- kind: "traces.improvement",
4449
- proposals,
4450
- replay
5514
+ kind: "traces.improvement"
4451
5515
  };
4452
5516
  return {
4453
5517
  ...result,
4454
- artifacts: await writeTraceImprovementArtifacts(result, opts.outDir)
5518
+ artifacts: await writeTraceImprovementArtifacts(result, directory)
4455
5519
  };
4456
5520
  }
4457
5521
  async function saveReport(path, report) {
@@ -4460,7 +5524,7 @@ async function saveReport(path, report) {
4460
5524
  }
4461
5525
 
4462
5526
  // src/live.ts
4463
- import { createHash as createHash2 } from "crypto";
5527
+ import { createHash as createHash5 } from "crypto";
4464
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;
4465
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;
4466
5530
  var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
@@ -4480,7 +5544,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
4480
5544
  signal?.addEventListener("abort", onAbort, { once: true });
4481
5545
  });
4482
5546
  function hash(parts) {
4483
- 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);
4484
5548
  }
4485
5549
  function asString(value) {
4486
5550
  if (typeof value === "string" && value.trim()) return value;
@@ -4934,7 +5998,7 @@ async function watchSessions(opts) {
4934
5998
  }
4935
5999
 
4936
6000
  // src/session-index.ts
4937
- 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";
4938
6002
  import { tmpdir as tmpdir5 } from "os";
4939
6003
  import { dirname as dirname3, join as join17 } from "path";
4940
6004
  function indexRow(record) {
@@ -4956,7 +6020,8 @@ function indexRow(record) {
4956
6020
  },
4957
6021
  models: record.metrics.models,
4958
6022
  tools: record.metrics.tools,
4959
- signals: record.signals
6023
+ signals: record.signals,
6024
+ execution: record.execution
4960
6025
  };
4961
6026
  }
4962
6027
  function uniqueSorted(values) {
@@ -5051,7 +6116,7 @@ async function summarizeFile(path, kind) {
5051
6116
  const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
5052
6117
  if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
5053
6118
  try {
5054
- const text = await readFile14(path, "utf8");
6119
+ const text = await readFile5(path, "utf8");
5055
6120
  const lines = countLines(text);
5056
6121
  const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
5057
6122
  return {
@@ -5070,7 +6135,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
5070
6135
  if (out.length >= maxFiles) return;
5071
6136
  let entries;
5072
6137
  try {
5073
- entries = await readdir11(dir, { withFileTypes: true });
6138
+ entries = await readdir12(dir, { withFileTypes: true });
5074
6139
  } catch {
5075
6140
  return;
5076
6141
  }
@@ -5156,10 +6221,10 @@ async function writeSessionIndexFile(index, outPath) {
5156
6221
  }
5157
6222
 
5158
6223
  // src/upload-state.ts
5159
- import { createHash as createHash3 } from "crypto";
5160
- 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";
5161
6225
  import { homedir as homedir11 } from "os";
5162
6226
  import { dirname as dirname4, join as join18 } from "path";
6227
+ import { contentHash } from "@tangle-network/agent-eval";
5163
6228
  function stateFile() {
5164
6229
  const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
5165
6230
  return join18(base, "tangle-traces", "uploaded.json");
@@ -5168,16 +6233,14 @@ function uploadKey(harness, sessionId) {
5168
6233
  return `${harness}:${sessionId}`;
5169
6234
  }
5170
6235
  function sessionHash(spans) {
5171
- const h = createHash3("sha256");
5172
- for (const s of spans) {
5173
- h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
5174
- `);
5175
- }
5176
- 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);
5177
6240
  }
5178
6241
  async function loadState() {
5179
6242
  try {
5180
- return JSON.parse(await readFile15(stateFile(), "utf8"));
6243
+ return JSON.parse(await readFile6(stateFile(), "utf8"));
5181
6244
  } catch {
5182
6245
  return {};
5183
6246
  }
@@ -5193,7 +6256,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
5193
6256
  }
5194
6257
 
5195
6258
  // src/upload.ts
5196
- import { readFile as readFile16 } from "fs/promises";
6259
+ import { readFile as readFile7 } from "fs/promises";
5197
6260
  import { createRequire } from "module";
5198
6261
  import { hostname } from "os";
5199
6262
  import { basename as basename9, join as join19 } from "path";
@@ -5212,7 +6275,7 @@ async function planUpload(opts = {}) {
5212
6275
  spans,
5213
6276
  redaction: report,
5214
6277
  hash: hash2,
5215
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
6278
+ isNew: true
5216
6279
  });
5217
6280
  }
5218
6281
  return { items, state, sinceMs: opts.sinceMs };
@@ -5220,19 +6283,18 @@ async function planUpload(opts = {}) {
5220
6283
  async function gitBranch(cwd) {
5221
6284
  if (!cwd) return void 0;
5222
6285
  try {
5223
- const head = await readFile16(join19(cwd, ".git", "HEAD"), "utf8");
6286
+ const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
5224
6287
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
5225
6288
  return m?.[1]?.trim();
5226
6289
  } catch {
5227
6290
  return void 0;
5228
6291
  }
5229
6292
  }
5230
- async function sessionMeta(item, uploadedAt) {
6293
+ async function sessionMeta(item) {
5231
6294
  const meta = {
5232
6295
  [ATTR.HARNESS]: item.ref.harness,
5233
6296
  [ATTR.SESSION_FILE]: basename9(item.ref.path),
5234
6297
  [ATTR.HOST]: hostname(),
5235
- [ATTR.UPLOADED_AT]: uploadedAt,
5236
6298
  [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
5237
6299
  [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
5238
6300
  [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
@@ -5266,25 +6328,69 @@ function toTraceSpanEvents(spans, rootMeta) {
5266
6328
  };
5267
6329
  });
5268
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
+ };
5269
6343
  function stripSpanContent(spans) {
5270
6344
  return spans.map((s) => {
5271
- if (s.attributes["content"] == null) return s;
5272
- const { content: _drop, ...attributes } = s.attributes;
5273
- 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 };
5274
6351
  });
5275
6352
  }
5276
- async function executeUpload(plan, opts = {}) {
5277
- let newItems = plan.items.filter((i) => i.isNew);
6353
+ async function prepareOutbound(plan, opts, uploadedAt) {
6354
+ let candidates = plan.items;
5278
6355
  if (opts.redactor && !opts.stripContent) {
5279
- const r = opts.redactor;
5280
- 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
+ );
5281
6360
  }
5282
- if (opts.stripContent) newItems = newItems.map((i) => ({ ...i, spans: stripSpanContent(i.spans) }));
5283
- const skipped = plan.items.length - newItems.length;
5284
- 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 = {}) {
5285
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);
5286
6392
  if (opts.dryRun) {
5287
- const allSpans = newItems.flatMap((i) => i.spans);
6393
+ const allSpans = items.flatMap(({ item }) => item.spans);
5288
6394
  const path = await writeOtlpFile(allSpans, opts.otlpOut);
5289
6395
  return {
5290
6396
  uploadedSessions: 0,
@@ -5295,6 +6401,15 @@ async function executeUpload(plan, opts = {}) {
5295
6401
  otlpPath: path
5296
6402
  };
5297
6403
  }
6404
+ if (items.length === 0) {
6405
+ return {
6406
+ uploadedSessions: 0,
6407
+ skippedSessions: skipped,
6408
+ acceptedSpans: 0,
6409
+ redactionCount,
6410
+ dryRun: false
6411
+ };
6412
+ }
5298
6413
  const client = opts.backend ?? hostedClientFromEnv();
5299
6414
  if (!client) {
5300
6415
  throw new Error(
@@ -5303,23 +6418,28 @@ async function executeUpload(plan, opts = {}) {
5303
6418
  }
5304
6419
  const state = plan.state;
5305
6420
  let acceptedSpans = 0;
5306
- for (const item of newItems) {
5307
- const meta = await sessionMeta(item, uploadedAt);
5308
- const events = toTraceSpanEvents(item.spans, meta);
5309
- 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}`;
5310
6426
  const res = await client.ingestTraces(events, idempotencyKey);
6427
+ if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
5311
6428
  acceptedSpans += res.accepted;
5312
- state[uploadKey(item.ref.harness, item.ref.sessionId)] = {
5313
- 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,
5314
6435
  uploadedAt,
5315
6436
  harness: item.ref.harness,
5316
- spanCount: item.spans.length
6437
+ spanCount
5317
6438
  };
5318
- opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
5319
6439
  }
5320
6440
  await saveState(state);
5321
6441
  return {
5322
- uploadedSessions: newItems.length,
6442
+ uploadedSessions: items.length,
5323
6443
  skippedSessions: skipped,
5324
6444
  acceptedSpans,
5325
6445
  redactionCount,
@@ -5328,25 +6448,38 @@ async function executeUpload(plan, opts = {}) {
5328
6448
  }
5329
6449
 
5330
6450
  export {
6451
+ ATTR,
6452
+ INGEST_SOURCE_CLI,
6453
+ DEFAULT_HARNESS,
6454
+ sessionIdFromAttributes,
6455
+ indexSessionIdsByTrace,
6456
+ stampSessionIdentity,
5331
6457
  countSkillRunsJsonl,
5332
6458
  analyzeAdoption,
5333
6459
  span,
5334
6460
  toOpenInferenceSpan,
5335
6461
  serializeSpans,
5336
6462
  writeOtlpFile,
6463
+ summarizeOtlpExecution,
6464
+ summarizeSpanExecution,
5337
6465
  analyzeSpans,
5338
- ATTR,
5339
- INGEST_SOURCE_CLI,
5340
- DEFAULT_HARNESS,
5341
- parseIsoToEpochMs,
5342
- parseSince,
5343
- toRuntimeStore,
5344
- runPipelines,
5345
6466
  CONTENT_CAP,
5346
6467
  capText,
5347
6468
  ACTOR_ATTR,
5348
6469
  userPromptSpan,
6470
+ parseIsoToEpochMs,
6471
+ parseSince,
6472
+ toRuntimeStore,
6473
+ runPipelines,
6474
+ recordSessionCorruption,
6475
+ sessionIntegrityAttributes,
6476
+ stampSessionIntegrity,
6477
+ JsonSourceError,
6478
+ readJsonFile,
5349
6479
  AmpAdapter,
6480
+ JsonlParseError,
6481
+ readJsonl,
6482
+ takeJsonl,
5350
6483
  textIsSynthetic,
5351
6484
  looksLikeAgentPrompt,
5352
6485
  claudeActor,
@@ -5374,6 +6507,7 @@ export {
5374
6507
  stampRepoAttrs,
5375
6508
  stampSpanWorkdirRepoAttrs,
5376
6509
  parseSession,
6510
+ EmptySessionError,
5377
6511
  locateSessions,
5378
6512
  scanSessions,
5379
6513
  buildPolicyEvidenceRecord,
@@ -5400,6 +6534,7 @@ export {
5400
6534
  writeInspectionReportFile,
5401
6535
  REACTIONS,
5402
6536
  analyzeReactions,
6537
+ CORRUPTION_RECEIPT_DISPLAY_LIMIT,
5403
6538
  summarizeDeterministicSignals,
5404
6539
  renderReport,
5405
6540
  renderPipelines,
@@ -5411,7 +6546,7 @@ export {
5411
6546
  runTraceInvestigation,
5412
6547
  runTraceStoreInvestigation,
5413
6548
  writeTraceImprovementArtifacts,
5414
- runTraceImprovementLoop,
6549
+ runTraceImprovement,
5415
6550
  saveReport,
5416
6551
  classifyLiveActions,
5417
6552
  defaultTraceLiveAnalysts,
@@ -5427,10 +6562,12 @@ export {
5427
6562
  writeSessionIndexFile,
5428
6563
  uploadKey,
5429
6564
  sessionHash,
6565
+ outboundHash,
5430
6566
  loadState,
5431
6567
  saveState,
5432
6568
  alreadyUploaded,
5433
6569
  planUpload,
5434
6570
  toTraceSpanEvents,
6571
+ PartialUploadError,
5435
6572
  executeUpload
5436
6573
  };