@tangle-network/traces 0.8.27 → 0.9.1

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,138 +1,25 @@
1
- // src/adoption.ts
2
- import { readFile } from "fs/promises";
3
- import { join } from "path";
4
- function toolName(s) {
5
- const n = s.attributes["tool.name"];
6
- if (typeof n === "string") return n;
7
- if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
8
- return null;
9
- }
10
- function parseInput(s) {
11
- const c = s.attributes.content;
12
- if (typeof c !== "string") return {};
13
- try {
14
- const v = JSON.parse(c);
15
- return v && typeof v === "object" ? v : {};
16
- } catch {
17
- return {};
18
- }
19
- }
20
- function skillNameOf(input) {
21
- const v = input.skill ?? input.command;
22
- return typeof v === "string" && v.length > 0 ? v : "?";
23
- }
24
- function subagentTypeOf(input) {
25
- const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
26
- return typeof v === "string" && v.length > 0 ? v : "?";
27
- }
28
- function isSpawnAgentTool(name) {
29
- return name === "spawn_agent" || name.endsWith("__spawn_agent");
30
- }
31
- function countSkillRunsJsonl(raw) {
32
- const out = {};
33
- for (const line of raw.split("\n")) {
34
- const t = line.trim();
35
- if (!t) continue;
36
- let o;
37
- try {
38
- o = JSON.parse(t);
39
- } catch {
40
- continue;
41
- }
42
- if (!o || typeof o !== "object") continue;
43
- const rec = o;
44
- const names = [];
45
- if (Array.isArray(rec.skills)) {
46
- for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
47
- }
48
- if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
49
- if (names.length === 0) names.push("?");
50
- for (const n of names) out[n] = (out[n] ?? 0) + 1;
51
- }
52
- return out;
53
- }
54
- async function readLoopRuns(cwds) {
55
- const counts = {};
56
- let filesRead = 0;
57
- const seen = /* @__PURE__ */ new Set();
58
- for (const cwd of cwds) {
59
- if (!cwd || seen.has(cwd)) continue;
60
- seen.add(cwd);
61
- const path = join(cwd, ".evolve", "skill-runs.jsonl");
62
- let raw;
63
- try {
64
- raw = await readFile(path, "utf8");
65
- } catch {
66
- continue;
67
- }
68
- filesRead += 1;
69
- for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
70
- }
71
- return { counts, filesRead };
72
- }
73
- async function analyzeAdoption(spans, opts = {}) {
74
- const sessions = /* @__PURE__ */ new Set();
75
- const sessionsWithSkill = /* @__PURE__ */ new Set();
76
- const sessionsWithSubagent = /* @__PURE__ */ new Set();
77
- const skillInvocations = {};
78
- const subagentSpawns = {};
79
- const canonicalSubagentSessions = /* @__PURE__ */ new Set();
80
- const fallbackSubagents = /* @__PURE__ */ new Map();
81
- for (const s of spans) {
82
- sessions.add(s.trace_id);
83
- const tn = toolName(s);
84
- if (tn === "Skill") {
85
- const name = skillNameOf(parseInput(s));
86
- skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
87
- sessionsWithSkill.add(s.trace_id);
88
- } else if (tn === "Task" || tn === "Agent") {
89
- const type = subagentTypeOf(parseInput(s));
90
- subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
91
- sessionsWithSubagent.add(s.trace_id);
92
- canonicalSubagentSessions.add(s.trace_id);
93
- } else if (tn && isSpawnAgentTool(tn)) {
94
- const types = fallbackSubagents.get(s.trace_id) ?? [];
95
- types.push(subagentTypeOf(parseInput(s)));
96
- fallbackSubagents.set(s.trace_id, types);
97
- }
98
- }
99
- for (const [traceId, types] of fallbackSubagents) {
100
- if (canonicalSubagentSessions.has(traceId)) continue;
101
- sessionsWithSubagent.add(traceId);
102
- for (const type of types) {
103
- subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
104
- }
105
- }
106
- const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
107
- const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
108
- const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
109
- const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
110
- return {
111
- sessionCount: sessions.size,
112
- sessionsWithSkill: sessionsWithSkill.size,
113
- skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
114
- skillInvocations,
115
- totalSkillInvocations,
116
- subagentSpawns,
117
- totalSubagentSpawns,
118
- sessionsWithSubagent: sessionsWithSubagent.size,
119
- loopDispatchedRuns,
120
- totalLoopDispatchedRuns,
121
- skillRunFilesRead: filesRead
122
- };
123
- }
124
-
125
1
  // src/otlp.ts
2
+ import {
3
+ applyLlmSpanOtlpAttributes,
4
+ OPENINFERENCE_SPAN_KIND,
5
+ TOOL_NAME
6
+ } from "@tangle-network/agent-eval/trace-attributes";
126
7
  function span(input) {
127
8
  const attributes = {
128
- "openinference.span.kind": input.kind
9
+ [OPENINFERENCE_SPAN_KIND]: input.kind
129
10
  };
130
11
  if (input.service != null) attributes["service.name"] = input.service;
131
12
  if (input.agent != null) attributes["agent.name"] = input.agent;
132
- if (input.model != null) attributes["llm.model_name"] = input.model;
133
- if (input.tool != null) attributes["tool.name"] = input.tool;
134
- if (input.inputTokens != null) attributes["llm.input_tokens"] = input.inputTokens;
135
- if (input.outputTokens != null) attributes["llm.output_tokens"] = input.outputTokens;
13
+ if (input.tool != null) attributes[TOOL_NAME] = input.tool;
14
+ applyLlmSpanOtlpAttributes(attributes, {
15
+ model: input.model ?? void 0,
16
+ inputTokens: input.inputTokens ?? void 0,
17
+ outputTokens: input.outputTokens ?? void 0,
18
+ reasoningTokens: input.reasoningTokens ?? void 0,
19
+ cachedTokens: input.cachedInputTokens ?? void 0,
20
+ cacheWriteTokens: input.cacheWriteInputTokens ?? void 0,
21
+ costUsd: input.costUsd ?? void 0
22
+ });
136
23
  if (input.step != null) attributes.step = input.step;
137
24
  if (input.content != null && input.content.length > 0) attributes["content"] = input.content;
138
25
  if (input.extra) Object.assign(attributes, input.extra);
@@ -162,7 +49,7 @@ function toOpenInferenceSpan(s) {
162
49
  span_id: s.span_id,
163
50
  parent_span_id: s.parent_span_id ?? "",
164
51
  name: s.name,
165
- kind: typeof a["openinference.span.kind"] === "string" ? a["openinference.span.kind"] : "CHAIN",
52
+ kind: typeof a[OPENINFERENCE_SPAN_KIND] === "string" ? a[OPENINFERENCE_SPAN_KIND] : "CHAIN",
166
53
  start_time: s.start_time,
167
54
  end_time: s.end_time,
168
55
  status: { code: s.status.code, message: s.status.message ?? "" },
@@ -185,35 +72,33 @@ async function writeOtlpFile(spans, outPath) {
185
72
  return path;
186
73
  }
187
74
 
188
- // src/analyze.ts
189
- import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
190
- import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
191
- var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
192
- async function analyzeSpans(spans, opts = {}) {
193
- if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
194
- const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
195
- const runId = opts.runId ?? `traces-${Date.now()}`;
196
- const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
197
- await detStore.ensureIndexed();
198
- const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
199
- const result = await detRegistry.run(runId, { traceStore: detStore });
200
- if (opts.ai) {
201
- const agStore = new OtlpFileTraceStore({ path: otlpPath });
202
- await agStore.ensureIndexed();
203
- const agRegistry = buildDefaultAnalystRegistry({
204
- ai: opts.ai,
205
- model: opts.model,
206
- includeBehavioral: false,
207
- registry: { log: opts.log }
208
- });
209
- const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
210
- budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
211
- });
212
- result.findings.push(...agResult.findings);
213
- result.per_analyst.push(...agResult.per_analyst);
214
- result.total_cost_usd += agResult.total_cost_usd;
215
- }
216
- return { otlpPath, result };
75
+ // src/adapters/conversation.ts
76
+ var CONTENT_CAP = 8e3;
77
+ function capText(raw) {
78
+ const t = raw.trim();
79
+ if (t.length <= CONTENT_CAP) return t;
80
+ return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
81
+ }
82
+ var ACTOR_ATTR = "tangle.actor";
83
+ function userPromptSpan(o) {
84
+ return span({
85
+ traceId: o.traceId,
86
+ spanId: o.spanId,
87
+ parentSpanId: o.parentSpanId,
88
+ name: "user.prompt",
89
+ kind: "CHAIN",
90
+ startTime: o.startTime,
91
+ service: o.service ?? null,
92
+ agent: o.agent ?? null,
93
+ step: o.step,
94
+ content: o.content,
95
+ extra: { [ACTOR_ATTR]: o.actor ?? "human" }
96
+ });
97
+ }
98
+
99
+ // src/arrays.ts
100
+ function appendAll(target, values) {
101
+ for (const value of values) target.push(value);
217
102
  }
218
103
 
219
104
  // src/attributes.ts
@@ -245,10 +130,94 @@ var ATTR = {
245
130
  UPLOADED_AT: "tangle.uploaded_at",
246
131
  UPLOADER: "tangle.uploader",
247
132
  REDACTION_VERSION: "redaction.version",
248
- REDACTION_COUNT: "redaction.count"
133
+ REDACTION_COUNT: "redaction.count",
134
+ /** Upload contract: bounded root summary plus location-only receipt child spans.
135
+ * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
136
+ SESSION_INTEGRITY: "traces.session.integrity",
137
+ CORRUPTION_COUNT: "traces.session.corruption_count",
138
+ CORRUPTION_DIGEST: "traces.session.corruption_digest",
139
+ CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
140
+ CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
141
+ CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
142
+ CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
143
+ CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
144
+ CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
145
+ CORRUPTION_SHA256: "traces.session.corruption.sha256",
146
+ RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
249
147
  };
250
148
  var INGEST_SOURCE_CLI = "cli";
251
149
  var DEFAULT_HARNESS = "claude-code";
150
+ var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
151
+ function sessionIdFromAttributes(attributes) {
152
+ return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
153
+ (value) => typeof value === "string" && value.length > 0
154
+ );
155
+ }
156
+ function indexSessionIdsByTrace(spans) {
157
+ const candidatesByTrace = /* @__PURE__ */ new Map();
158
+ for (const span2 of spans) {
159
+ const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
160
+ for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
161
+ const value = span2.attributes[key];
162
+ if (typeof value === "string" && value.length > 0) candidates.add(value);
163
+ }
164
+ if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
165
+ }
166
+ const sessionByTrace = /* @__PURE__ */ new Map();
167
+ const conflicts = [];
168
+ for (const [traceId, candidates] of candidatesByTrace) {
169
+ const sessionIds2 = [...candidates].sort();
170
+ if (sessionIds2.length === 1) {
171
+ sessionByTrace.set(traceId, sessionIds2[0]);
172
+ } else {
173
+ conflicts.push({ traceId, sessionIds: sessionIds2 });
174
+ }
175
+ }
176
+ conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
177
+ return { sessionByTrace, conflicts };
178
+ }
179
+ function stampSessionIdentity(spans, fallbackSessionId) {
180
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
181
+ const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
182
+ const traceIds = new Set(spans.map((span2) => span2.trace_id));
183
+ const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
184
+ for (const span2 of spans) {
185
+ if (conflictingTraceIds.has(span2.trace_id)) continue;
186
+ const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
187
+ if (!sessionIdFromAttributes(span2.attributes)) {
188
+ span2.attributes[ATTR.SESSION_ID] = sessionId;
189
+ }
190
+ }
191
+ }
192
+
193
+ // src/execution.ts
194
+ import {
195
+ summarizeExecution
196
+ } from "@tangle-network/agent-eval/contract";
197
+ import {
198
+ otlpRowsToRunRecords,
199
+ otlpToRunRecords
200
+ } from "@tangle-network/agent-eval/traces";
201
+ function runOptions(opts) {
202
+ return {
203
+ experimentId: opts.experimentId ?? "traces-observed",
204
+ candidateId: opts.candidateId ?? "observed",
205
+ fallbackModel: "unknown@otlp"
206
+ };
207
+ }
208
+ function* openInferenceRows(spans) {
209
+ for (const item of spans) yield toOpenInferenceSpan(item);
210
+ }
211
+ function summarizeOtlpExecution(otlpJsonl, opts = {}) {
212
+ const runs = otlpToRunRecords(otlpJsonl, runOptions(opts));
213
+ return summarizeExecution({ runs });
214
+ }
215
+ function summarizeSpanExecution(spans, opts = {}) {
216
+ if (spans.length === 0) throw new Error("summarizeSpanExecution: no spans to summarize");
217
+ return summarizeExecution({
218
+ runs: otlpRowsToRunRecords(openInferenceRows(spans), runOptions(opts))
219
+ });
220
+ }
252
221
 
253
222
  // src/time.ts
254
223
  function parseIsoToEpochMs(ts) {
@@ -271,6 +240,125 @@ function parseSince(value) {
271
240
 
272
241
  // src/runtime-store.ts
273
242
  import { InMemoryTraceStore } from "@tangle-network/agent-eval";
243
+
244
+ // src/adapters/tool-io.ts
245
+ import { createHash } from "crypto";
246
+ var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
247
+ var TOOL_IO_VALUE_KEYS = ["input.value", "output.value"];
248
+ var TOOL_IO_SIDES = ["input", "output"];
249
+ var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
250
+ var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
251
+ var TRUNCATION_MARKER = /\n\[truncated\]$/;
252
+ function canonicalJson(value) {
253
+ const sort = (item) => {
254
+ if (Array.isArray(item)) return item.map(sort);
255
+ if (!item || typeof item !== "object") return item;
256
+ return Object.fromEntries(
257
+ Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
258
+ );
259
+ };
260
+ const json2 = JSON.stringify(sort(value));
261
+ if (json2 === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
262
+ return json2;
263
+ }
264
+ function serialize(value) {
265
+ if (value === void 0) return void 0;
266
+ if (typeof value === "string") {
267
+ const trimmed = value.trim();
268
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
269
+ try {
270
+ return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
271
+ } catch {
272
+ }
273
+ }
274
+ return { value, mimeType: "text/plain" };
275
+ }
276
+ return { value: canonicalJson(value), mimeType: "application/json" };
277
+ }
278
+ function utf8Prefix(value, maxBytes) {
279
+ let bytes = 0;
280
+ let end = 0;
281
+ for (const char of value) {
282
+ const charBytes = Buffer.byteLength(char);
283
+ if (bytes + charBytes > maxBytes) break;
284
+ bytes += charBytes;
285
+ end += char.length;
286
+ }
287
+ return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
288
+ }
289
+ function truncatedValue(value, bytes) {
290
+ const longestMarker = `
291
+ [truncated ${bytes} bytes]`;
292
+ const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
293
+ const omittedBytes = bytes - Buffer.byteLength(prefix);
294
+ return `${prefix}
295
+ [truncated ${omittedBytes} bytes]`;
296
+ }
297
+ function valueAttributes(side, raw) {
298
+ const serialized = serialize(raw);
299
+ if (!serialized) return {};
300
+ const bytes = Buffer.byteLength(serialized.value);
301
+ const sha256 = createHash("sha256").update(serialized.value).digest("hex");
302
+ const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
303
+ const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
304
+ return {
305
+ [`${side}.value`]: value,
306
+ [`${side}.mime_type`]: serialized.mimeType,
307
+ [`traces.${side}.bytes`]: bytes,
308
+ [`traces.${side}.sha256`]: sha256,
309
+ [`traces.${side}.truncated`]: truncated
310
+ };
311
+ }
312
+ function toolIoAttributes(io) {
313
+ return {
314
+ ...valueAttributes("input", io.input),
315
+ ...valueAttributes("output", io.output),
316
+ ...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
317
+ };
318
+ }
319
+ function toolArgumentsFromAttributes(attributes) {
320
+ const declared = attributes["tool.args_captured"];
321
+ const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
322
+ const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
323
+ return {
324
+ args: argsCaptured ? attributes["input.value"] : void 0,
325
+ argsCaptured
326
+ };
327
+ }
328
+ function normalizeToolIoAttributes(attributes) {
329
+ for (const side of TOOL_IO_SIDES) {
330
+ const valueKey = `${side}.value`;
331
+ const mimeKey = `${side}.mime_type`;
332
+ const bytesKey = `traces.${side}.bytes`;
333
+ const digestKey = `traces.${side}.sha256`;
334
+ const truncatedKey = `traces.${side}.truncated`;
335
+ const rawValue = attributes[valueKey];
336
+ if (typeof rawValue !== "string") {
337
+ delete attributes[mimeKey];
338
+ delete attributes[bytesKey];
339
+ delete attributes[digestKey];
340
+ delete attributes[truncatedKey];
341
+ continue;
342
+ }
343
+ let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
344
+ let truncated = TRUNCATION_MARKER.test(value);
345
+ const unboundedBytes = Buffer.byteLength(value);
346
+ if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
347
+ value = truncatedValue(value, unboundedBytes);
348
+ truncated = true;
349
+ }
350
+ attributes[valueKey] = value;
351
+ attributes[bytesKey] = Buffer.byteLength(value);
352
+ attributes[digestKey] = createHash("sha256").update(value).digest("hex");
353
+ attributes[truncatedKey] = truncated;
354
+ }
355
+ }
356
+ function recordToolOutput(toolSpan, output) {
357
+ if (!toolSpan) return;
358
+ Object.assign(toolSpan.attributes, valueAttributes("output", output));
359
+ }
360
+
361
+ // src/runtime-store.ts
274
362
  var KIND = {
275
363
  AGENT: "agent",
276
364
  LLM: "llm",
@@ -280,75 +368,240 @@ var KIND = {
280
368
  };
281
369
  async function toRuntimeStore(spans) {
282
370
  const store = new InMemoryTraceStore();
283
- const runIds = /* @__PURE__ */ new Set();
371
+ const runs = /* @__PURE__ */ new Map();
372
+ for (const item of spans) {
373
+ const startedAt = parseIsoToEpochMs(item.start_time);
374
+ const endedAt = parseIsoToEpochMs(item.end_time);
375
+ const current = runs.get(item.trace_id);
376
+ runs.set(item.trace_id, current ? {
377
+ startedAt: Math.min(current.startedAt, startedAt),
378
+ endedAt: Math.max(current.endedAt, endedAt),
379
+ failed: current.failed || item.status.code === "ERROR"
380
+ } : { startedAt, endedAt, failed: item.status.code === "ERROR" });
381
+ }
382
+ for (const [runId, run] of runs) {
383
+ await store.appendRun({
384
+ runId,
385
+ scenarioId: "session",
386
+ startedAt: run.startedAt,
387
+ endedAt: run.endedAt,
388
+ status: run.failed ? "failed" : "completed"
389
+ });
390
+ }
284
391
  for (const s of spans) {
285
- if (!runIds.has(s.trace_id)) {
286
- runIds.add(s.trace_id);
287
- await store.appendRun({ runId: s.trace_id, scenarioId: "session" });
288
- }
289
392
  const kind = KIND[s.attributes["openinference.span.kind"]] ?? "custom";
393
+ const startedAt = parseIsoToEpochMs(s.start_time);
394
+ const endedAt = parseIsoToEpochMs(s.end_time);
290
395
  const base = {
291
396
  spanId: s.span_id,
292
397
  parentSpanId: s.parent_span_id ?? void 0,
293
398
  runId: s.trace_id,
294
399
  name: s.name,
295
- startedAt: parseIsoToEpochMs(s.start_time),
296
- endedAt: parseIsoToEpochMs(s.end_time),
400
+ startedAt,
401
+ endedAt,
297
402
  status: s.status.code === "ERROR" ? "error" : "ok",
298
403
  error: s.status.message,
299
404
  attributes: s.attributes
300
405
  };
406
+ const tool = kind === "tool" ? toolArgumentsFromAttributes(s.attributes) : void 0;
301
407
  const span2 = kind === "tool" ? {
302
408
  ...base,
303
409
  kind: "tool",
304
410
  toolName: String(s.attributes["tool.name"] ?? "tool"),
305
- args: s.attributes.content ?? ""
411
+ args: tool.args,
412
+ argsCaptured: tool.argsCaptured,
413
+ latencyMs: Math.max(0, endedAt - startedAt)
306
414
  } : { ...base, kind };
307
415
  await store.appendSpan(span2);
308
416
  }
309
- return { store, runIds: [...runIds] };
417
+ return { store, runIds: [...runs.keys()] };
310
418
  }
311
419
 
312
420
  // src/pipelines.ts
313
421
  import { computeToolUseMetrics } from "@tangle-network/agent-eval";
314
- import { stuckLoopView } from "@tangle-network/agent-eval/pipelines";
422
+ import {
423
+ failureClusterView,
424
+ stuckLoopView
425
+ } from "@tangle-network/agent-eval/pipelines";
315
426
  async function runPipelines(spans, opts = {}) {
316
427
  const { store, runIds } = await toRuntimeStore(spans);
317
428
  const loopEligible = spans.filter((item) => item.attributes["traces.expected_blocking"] !== true);
318
429
  const { store: loopStore } = await toRuntimeStore(loopEligible);
319
- const stuckLoops = await stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 });
320
- const toolUse = await Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)));
321
- return { stuckLoops, toolUse };
430
+ const [stuckLoops, failureClusters, toolUse] = await Promise.all([
431
+ stuckLoopView(loopStore, { minOccurrences: opts.minLoopOccurrences ?? 3 }),
432
+ failureClusterView(store),
433
+ Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
434
+ ]);
435
+ return { stuckLoops, failureClusters, toolUse };
322
436
  }
323
437
 
324
- // src/adapters/conversation.ts
325
- var CONTENT_CAP = 8e3;
326
- function capText(raw) {
327
- const t = raw.trim();
328
- if (t.length <= CONTENT_CAP) return t;
329
- return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
438
+ // src/integrity.ts
439
+ import { createHash as createHash2 } from "crypto";
440
+ var receiptStates = /* @__PURE__ */ new WeakMap();
441
+ function receiptIdentity(receipt) {
442
+ return [
443
+ receipt.sourcePath,
444
+ receipt.lineNumber,
445
+ receipt.byteOffset,
446
+ receipt.byteLength,
447
+ receipt.sha256
448
+ ];
330
449
  }
331
- var ACTOR_ATTR = "tangle.actor";
332
- function userPromptSpan(o) {
333
- return span({
334
- traceId: o.traceId,
335
- spanId: o.spanId,
336
- parentSpanId: o.parentSpanId,
337
- name: "user.prompt",
338
- kind: "CHAIN",
339
- startTime: o.startTime,
340
- service: o.service ?? null,
341
- agent: o.agent ?? null,
342
- step: o.step,
343
- content: o.content,
344
- extra: { [ACTOR_ATTR]: o.actor ?? "human" }
345
- });
450
+ function receiptKey(receipt) {
451
+ return JSON.stringify(receiptIdentity(receipt));
452
+ }
453
+ function stateFor(ref, current) {
454
+ let state = receiptStates.get(ref);
455
+ if (!state || state.corruptions !== current) {
456
+ state = {
457
+ corruptions: current,
458
+ seen: new Set(current.map(receiptKey))
459
+ };
460
+ receiptStates.set(ref, state);
461
+ }
462
+ return state;
463
+ }
464
+ function recordSessionCorruption(ref, receipt) {
465
+ const current = ref.integrity?.corruptions ?? [];
466
+ const corruption = { ...receipt, harness: ref.harness, sessionId: ref.sessionId };
467
+ const state = stateFor(ref, current);
468
+ const key = receiptKey(receipt);
469
+ if (state.seen.has(key)) return;
470
+ if (ref.integrity) {
471
+ ref.integrity.corruptions.push(corruption);
472
+ } else {
473
+ ref.integrity = { status: "degraded_not_lossless", corruptions: [corruption] };
474
+ }
475
+ state.seen.add(key);
476
+ state.corruptions = ref.integrity.corruptions;
477
+ }
478
+ function sessionJsonlOptions(ref, options = {}) {
479
+ if (options.corruptionMode === "strict") return { mode: "strict" };
480
+ return {
481
+ mode: "recover",
482
+ onCorruption: (receipt) => recordSessionCorruption(ref, receipt)
483
+ };
484
+ }
485
+ function sessionIntegrityAttributes(ref) {
486
+ if (!ref.integrity) return {};
487
+ return {
488
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
489
+ [ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
490
+ [ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
491
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
492
+ };
493
+ }
494
+ function corruptionDigest(receipts) {
495
+ const hash2 = createHash2("sha256");
496
+ for (const receipt of receipts) {
497
+ hash2.update(JSON.stringify(receiptIdentity(receipt)));
498
+ hash2.update("\n");
499
+ }
500
+ return `sha256:${hash2.digest("hex")}`;
501
+ }
502
+ function receiptSpanId(parentSpanId, receipt) {
503
+ const digest = createHash2("sha256").update(JSON.stringify(receiptIdentity(receipt))).digest("hex");
504
+ return `${parentSpanId}:corruption:${digest}`;
505
+ }
506
+ function stampSessionIntegrity(ref, spans) {
507
+ if (!ref.integrity) return;
508
+ const parent = spans.find((item) => item.parent_span_id === null) ?? spans[0];
509
+ if (!parent) return;
510
+ const sessionId = parent.trace_id;
511
+ for (const receipt of ref.integrity.corruptions) {
512
+ receipt.harness = ref.harness;
513
+ receipt.sessionId = sessionId;
514
+ }
515
+ const attributes = sessionIntegrityAttributes(ref);
516
+ Object.assign(parent.attributes, attributes);
517
+ const existingSpanIds = new Set(spans.map((item) => item.span_id));
518
+ for (const receipt of ref.integrity.corruptions) {
519
+ const spanId = receiptSpanId(parent.span_id, receipt);
520
+ if (existingSpanIds.has(spanId)) continue;
521
+ spans.push(span({
522
+ traceId: parent.trace_id,
523
+ spanId,
524
+ parentSpanId: parent.span_id,
525
+ name: "source.corruption.receipt",
526
+ kind: "CHAIN",
527
+ startTime: parent.start_time,
528
+ endTime: parent.start_time,
529
+ status: "ERROR",
530
+ service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
531
+ extra: {
532
+ [ATTR.SESSION_INTEGRITY]: ref.integrity.status,
533
+ [ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
534
+ [ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
535
+ [ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
536
+ [ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
537
+ [ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
538
+ [ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
539
+ [ATTR.CORRUPTION_SHA256]: receipt.sha256,
540
+ [ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
541
+ }
542
+ }));
543
+ existingSpanIds.add(spanId);
544
+ }
545
+ }
546
+
547
+ // src/json.ts
548
+ import { readdir, readFile } from "fs/promises";
549
+ var JsonSourceError = class extends Error {
550
+ sourcePath;
551
+ kind;
552
+ code;
553
+ constructor(sourcePath, kind, cause) {
554
+ super(
555
+ kind === "parse" ? `Invalid JSON at ${sourcePath}` : `Unable to read JSON source at ${sourcePath}`,
556
+ { cause }
557
+ );
558
+ this.name = "JsonSourceError";
559
+ this.sourcePath = sourcePath;
560
+ this.kind = kind;
561
+ this.code = errorCode(cause);
562
+ }
563
+ };
564
+ function errorCode(error) {
565
+ return error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code : void 0;
566
+ }
567
+ function pathSafeParseCause(cause) {
568
+ if (!(cause instanceof Error)) return cause;
569
+ const location = cause.message.match(/(?:at position \d+(?: \(line \d+ column \d+\))?|line \d+ column \d+)/i)?.[0];
570
+ cause.message = location ? `JSON parse failed ${location}` : "JSON parse failed";
571
+ cause.stack = `${cause.name}: ${cause.message}`;
572
+ return cause;
573
+ }
574
+ function isMissingPathError(error) {
575
+ return errorCode(error) === "ENOENT";
576
+ }
577
+ function isMissingJsonSource(error) {
578
+ return error instanceof JsonSourceError && error.kind === "read" && isMissingPathError(error);
579
+ }
580
+ async function readJsonFile(path) {
581
+ let raw;
582
+ try {
583
+ raw = await readFile(path, "utf8");
584
+ } catch (error) {
585
+ throw new JsonSourceError(path, "read", error);
586
+ }
587
+ try {
588
+ return JSON.parse(raw);
589
+ } catch (error) {
590
+ throw new JsonSourceError(path, "parse", pathSafeParseCause(error));
591
+ }
592
+ }
593
+ async function listJsonFiles(path) {
594
+ try {
595
+ return (await readdir(path)).filter((file) => file.endsWith(".json"));
596
+ } catch (error) {
597
+ throw new JsonSourceError(path, "read", error);
598
+ }
346
599
  }
347
600
 
348
601
  // src/adapters/amp.ts
349
- import { readdir, readFile as readFile2, stat } from "fs/promises";
602
+ import { readdir as readdir2, stat } from "fs/promises";
350
603
  import { homedir } from "os";
351
- import { basename, join as join2 } from "path";
604
+ import { basename, join } from "path";
352
605
  var SERVICE = "amp";
353
606
  function textOf(content) {
354
607
  if (typeof content === "string") return content.slice(0, CONTENT_CAP);
@@ -359,25 +612,27 @@ function textOf(content) {
359
612
  var AmpAdapter = class {
360
613
  harness = "amp";
361
614
  threadsRoot() {
362
- const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
363
- return join2(base, "amp", "threads");
615
+ const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
616
+ return join(base, "amp", "threads");
364
617
  }
365
618
  async locate(opts = {}) {
366
619
  let files;
367
620
  try {
368
- files = await readdir(this.threadsRoot());
369
- } catch {
370
- return [];
621
+ files = await readdir2(this.threadsRoot());
622
+ } catch (error) {
623
+ if (isMissingPathError(error)) return [];
624
+ throw error;
371
625
  }
372
626
  const refs = [];
373
627
  for (const f of files) {
374
628
  if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
375
- const path = join2(this.threadsRoot(), f);
629
+ const path = join(this.threadsRoot(), f);
376
630
  let st;
377
631
  try {
378
632
  st = await stat(path);
379
- } catch {
380
- continue;
633
+ } catch (error) {
634
+ if (isMissingPathError(error)) continue;
635
+ throw error;
381
636
  }
382
637
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
383
638
  if (opts.cwd) continue;
@@ -386,12 +641,7 @@ var AmpAdapter = class {
386
641
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
387
642
  }
388
643
  async parse(ref) {
389
- let thread;
390
- try {
391
- thread = JSON.parse(await readFile2(ref.path, "utf8"));
392
- } catch {
393
- return [];
394
- }
644
+ const thread = await readJsonFile(ref.path);
395
645
  const traceId = thread.id ?? ref.sessionId;
396
646
  const rootId = `root:${traceId}`;
397
647
  const start = new Date(thread.created ?? 0).toISOString();
@@ -423,7 +673,6 @@ var AmpAdapter = class {
423
673
  }
424
674
  } else {
425
675
  const u = m.usage;
426
- const inputTokens2 = u ? (u.inputTokens ?? 0) + (u.cacheReadInputTokens ?? 0) + (u.cacheCreationInputTokens ?? 0) || null : null;
427
676
  spans.push(
428
677
  span({
429
678
  traceId,
@@ -435,8 +684,10 @@ var AmpAdapter = class {
435
684
  service: SERVICE,
436
685
  agent: SERVICE,
437
686
  model: u?.model ?? null,
438
- inputTokens: inputTokens2,
687
+ inputTokens: u?.inputTokens ?? null,
439
688
  outputTokens: u?.outputTokens ?? null,
689
+ cachedInputTokens: u?.cacheReadInputTokens ?? null,
690
+ cacheWriteInputTokens: u?.cacheCreationInputTokens ?? null,
440
691
  step,
441
692
  content: textOf(m.content) || null
442
693
  })
@@ -456,14 +707,17 @@ var AmpAdapter = class {
456
707
  agent: SERVICE,
457
708
  tool: b.name,
458
709
  step,
459
- content: b.input != null ? JSON.stringify(b.input) : null
710
+ extra: toolIoAttributes({ input: b.input })
460
711
  });
461
712
  spans.push(t);
462
713
  if (b.id) toolByUseId.set(b.id, t);
463
714
  step += 1;
464
715
  } else if (b.type === "tool_result" && b.tool_use_id) {
465
716
  const t = toolByUseId.get(b.tool_use_id);
466
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
717
+ if (t) {
718
+ t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
719
+ recordToolOutput(t, b.content ?? b.output);
720
+ }
467
721
  }
468
722
  }
469
723
  }
@@ -471,6 +725,108 @@ var AmpAdapter = class {
471
725
  }
472
726
  };
473
727
 
728
+ // src/jsonl.ts
729
+ import { isUtf8 } from "buffer";
730
+ import { createHash as createHash3 } from "crypto";
731
+ import { createReadStream } from "fs";
732
+ var JsonlParseError = class extends SyntaxError {
733
+ sourcePath;
734
+ lineNumber;
735
+ byteOffset;
736
+ byteLength;
737
+ sha256;
738
+ receipt;
739
+ constructor(receipt) {
740
+ super(`Invalid JSONL at ${receipt.sourcePath}:${receipt.lineNumber}`);
741
+ this.name = "JsonlParseError";
742
+ this.sourcePath = receipt.sourcePath;
743
+ this.lineNumber = receipt.lineNumber;
744
+ this.byteOffset = receipt.byteOffset;
745
+ this.byteLength = receipt.byteLength;
746
+ this.sha256 = receipt.sha256;
747
+ this.receipt = receipt;
748
+ }
749
+ };
750
+ function corruptionReceipt(rawLine, path, lineNumber, byteOffset) {
751
+ return {
752
+ receiptVersion: 1,
753
+ kind: "jsonl_corruption",
754
+ status: "degraded_not_lossless",
755
+ sourcePath: path,
756
+ lineNumber,
757
+ byteOffset,
758
+ byteLength: rawLine.length,
759
+ sha256: createHash3("sha256").update(rawLine).digest("hex"),
760
+ rawBytes: "local_source_only"
761
+ };
762
+ }
763
+ function parseLine(rawLine, path, lineNumber, byteOffset, options) {
764
+ const jsonBytes = rawLine.at(-1) === 13 ? rawLine.subarray(0, -1) : rawLine;
765
+ if (jsonBytes.length === 0) return void 0;
766
+ if (!isUtf8(jsonBytes)) return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
767
+ const json2 = jsonBytes.toString("utf8");
768
+ if (json2.trim().length === 0) return void 0;
769
+ try {
770
+ return JSON.parse(json2);
771
+ } catch {
772
+ return handleCorruption(rawLine, path, lineNumber, byteOffset, options);
773
+ }
774
+ }
775
+ function handleCorruption(rawLine, path, lineNumber, byteOffset, options) {
776
+ const receipt = corruptionReceipt(rawLine, path, lineNumber, byteOffset);
777
+ if (options.mode === "recover") {
778
+ options.onCorruption(receipt);
779
+ return void 0;
780
+ }
781
+ throw new JsonlParseError(receipt);
782
+ }
783
+ async function* readJsonl(path, options = { mode: "strict" }) {
784
+ if (options.mode === "recover" && typeof options.onCorruption !== "function") {
785
+ throw new TypeError("JSONL recover mode requires an onCorruption callback");
786
+ }
787
+ const input = createReadStream(path);
788
+ let lineNumber = 0;
789
+ let byteOffset = 0;
790
+ let fragments = [];
791
+ try {
792
+ for await (const chunk of input) {
793
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
794
+ let start = 0;
795
+ let newline = bytes.indexOf(10);
796
+ while (newline !== -1) {
797
+ fragments.push(bytes.subarray(start, newline));
798
+ const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
799
+ fragments = [];
800
+ lineNumber += 1;
801
+ const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
802
+ if (parsed !== void 0) yield parsed;
803
+ byteOffset += rawLine.length + 1;
804
+ start = newline + 1;
805
+ newline = bytes.indexOf(10, start);
806
+ }
807
+ if (start < bytes.length) fragments.push(bytes.subarray(start));
808
+ }
809
+ if (fragments.length > 0) {
810
+ const rawLine = fragments.length === 1 ? fragments[0] : Buffer.concat(fragments);
811
+ lineNumber += 1;
812
+ const parsed = parseLine(rawLine, path, lineNumber, byteOffset, options);
813
+ if (parsed !== void 0) yield parsed;
814
+ }
815
+ } finally {
816
+ input.destroy();
817
+ }
818
+ }
819
+ async function takeJsonl(path, limit, options = { mode: "strict" }) {
820
+ if (!Number.isInteger(limit) || limit < 0) throw new RangeError("limit must be a non-negative integer");
821
+ const rows = [];
822
+ if (limit === 0) return rows;
823
+ for await (const row of readJsonl(path, options)) {
824
+ rows.push(row);
825
+ if (rows.length === limit) break;
826
+ }
827
+ return rows;
828
+ }
829
+
474
830
  // src/adapters/actor.ts
475
831
  var CMD_MARKERS = [
476
832
  "<command-name>",
@@ -491,7 +847,7 @@ var INJECT_MARKERS = [
491
847
  // Prefix match: Codex adds attributes such as source="goal" to this tag.
492
848
  "<codex_internal_context",
493
849
  "<subagent_notification>",
494
- "# AGENTS.md instructions for ",
850
+ "# AGENTS.md instructions",
495
851
  "<local-command-stdout>",
496
852
  "This session is being continued from a previous conversation",
497
853
  // Slash-command skill bodies are expanded into a user turn (no <command-name>
@@ -564,26 +920,10 @@ function classifyReaction(text) {
564
920
  var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
565
921
 
566
922
  // src/adapters/claude.ts
567
- import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
923
+ import { readdir as readdir3, stat as stat2 } from "fs/promises";
568
924
  import { homedir as homedir2 } from "os";
569
- import { basename as basename2, join as join3 } from "path";
925
+ import { basename as basename2, join as join2 } from "path";
570
926
  var SERVICE2 = "claude-code";
571
- function parseLines(raw) {
572
- const out = [];
573
- for (const line of raw.split("\n")) {
574
- if (!line) continue;
575
- try {
576
- out.push(JSON.parse(line));
577
- } catch {
578
- }
579
- }
580
- return out;
581
- }
582
- function inputTokens(u) {
583
- if (!u) return null;
584
- const v = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
585
- return v > 0 ? v : null;
586
- }
587
927
  function asBlocks(content) {
588
928
  return Array.isArray(content) ? content : [];
589
929
  }
@@ -600,129 +940,140 @@ function stringifyToolResult(content) {
600
940
  }
601
941
  return "";
602
942
  }
603
- function parseClaudeStream(events, ctx) {
604
- const spans = [];
605
- const toolSpanByUseId = /* @__PURE__ */ new Map();
606
- let step = ctx.startStep;
607
- let sawUserTurn = false;
608
- for (const ev of events) {
609
- const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
610
- const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
611
- if (ev.type === "assistant" && ev.message) {
612
- const llmId = `${ctx.idPrefix}${uid}`;
613
- spans.push(
614
- span({
943
+ function createClaudeStream(startStep) {
944
+ return { spans: [], toolSpanByUseId: /* @__PURE__ */ new Map(), step: startStep, sawUserTurn: false };
945
+ }
946
+ function consumeClaudeEvent(ev, ctx, state) {
947
+ const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
948
+ const uid = ev.uuid ?? `${ctx.idPrefix}step${state.step}`;
949
+ if (ev.type === "assistant" && ev.message) {
950
+ const llmId = `${ctx.idPrefix}${uid}`;
951
+ state.spans.push(
952
+ span({
953
+ traceId: ctx.traceId,
954
+ spanId: llmId,
955
+ parentSpanId: ctx.rootParent,
956
+ name: "llm.turn",
957
+ kind: "LLM",
958
+ startTime: ts,
959
+ service: SERVICE2,
960
+ agent: ctx.agent,
961
+ model: ev.message.model ?? null,
962
+ inputTokens: ev.message.usage?.input_tokens ?? null,
963
+ outputTokens: ev.message.usage?.output_tokens ?? null,
964
+ cachedInputTokens: ev.message.usage?.cache_read_input_tokens ?? null,
965
+ cacheWriteInputTokens: ev.message.usage?.cache_creation_input_tokens ?? null,
966
+ step: state.step,
967
+ content: textOf2(ev.message.content) || null
968
+ })
969
+ );
970
+ state.step += 1;
971
+ let toolIdx = 0;
972
+ for (const block of asBlocks(ev.message.content)) {
973
+ if (block.type !== "tool_use" || !block.name) continue;
974
+ const toolSpan = span({
975
+ traceId: ctx.traceId,
976
+ spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
977
+ parentSpanId: llmId,
978
+ name: `tool.${block.name}`,
979
+ kind: "TOOL",
980
+ startTime: ts,
981
+ service: SERVICE2,
982
+ agent: ctx.agent,
983
+ tool: block.name,
984
+ step: state.step,
985
+ extra: toolIoAttributes({ input: block.input })
986
+ });
987
+ state.spans.push(toolSpan);
988
+ if (block.id) state.toolSpanByUseId.set(block.id, toolSpan);
989
+ toolIdx += 1;
990
+ state.step += 1;
991
+ }
992
+ } else if (ev.type === "user" && ev.message) {
993
+ const prompt = textOf2(ev.message.content);
994
+ if (prompt) {
995
+ const actor = claudeActor({
996
+ text: prompt,
997
+ isSidechain: ev.isSidechain,
998
+ userType: ev.userType ?? null,
999
+ isFirstUserTurn: !state.sawUserTurn
1000
+ });
1001
+ state.sawUserTurn = true;
1002
+ state.spans.push(
1003
+ userPromptSpan({
615
1004
  traceId: ctx.traceId,
616
- spanId: llmId,
1005
+ spanId: `${ctx.idPrefix}${uid}:user`,
617
1006
  parentSpanId: ctx.rootParent,
618
- name: "llm.turn",
619
- kind: "LLM",
620
1007
  startTime: ts,
621
1008
  service: SERVICE2,
622
1009
  agent: ctx.agent,
623
- model: ev.message.model ?? null,
624
- inputTokens: inputTokens(ev.message.usage),
625
- outputTokens: ev.message.usage?.output_tokens ?? null,
626
- step,
627
- content: textOf2(ev.message.content) || null
1010
+ step: state.step,
1011
+ content: prompt,
1012
+ actor
628
1013
  })
629
1014
  );
630
- step += 1;
631
- let toolIdx = 0;
632
- for (const block of asBlocks(ev.message.content)) {
633
- if (block.type !== "tool_use" || !block.name) continue;
634
- const toolSpan = span({
635
- traceId: ctx.traceId,
636
- spanId: `${ctx.idPrefix}${uid}:tool:${toolIdx}`,
637
- parentSpanId: llmId,
638
- name: `tool.${block.name}`,
639
- kind: "TOOL",
640
- startTime: ts,
641
- service: SERVICE2,
642
- agent: ctx.agent,
643
- tool: block.name,
644
- step,
645
- content: block.input != null ? JSON.stringify(block.input) : null
646
- });
647
- spans.push(toolSpan);
648
- if (block.id) toolSpanByUseId.set(block.id, toolSpan);
649
- toolIdx += 1;
650
- step += 1;
651
- }
652
- } else if (ev.type === "user" && ev.message) {
653
- const prompt = textOf2(ev.message.content);
654
- if (prompt) {
655
- const actor = claudeActor({
656
- text: prompt,
657
- isSidechain: ev.isSidechain,
658
- userType: ev.userType ?? null,
659
- isFirstUserTurn: !sawUserTurn
660
- });
661
- sawUserTurn = true;
662
- spans.push(
663
- userPromptSpan({
664
- traceId: ctx.traceId,
665
- spanId: `${ctx.idPrefix}${uid}:user`,
666
- parentSpanId: ctx.rootParent,
667
- startTime: ts,
668
- service: SERVICE2,
669
- agent: ctx.agent,
670
- step,
671
- content: prompt,
672
- actor
673
- })
674
- );
675
- step += 1;
676
- }
677
- for (const block of asBlocks(ev.message.content)) {
678
- if (block.type !== "tool_result" || !block.tool_use_id) continue;
679
- backfillResult(toolSpanByUseId.get(block.tool_use_id), ts, block.is_error === true, stringifyToolResult(block.content));
680
- }
681
- } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
682
- const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
683
- backfillResult(toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
1015
+ state.step += 1;
1016
+ }
1017
+ for (const block of asBlocks(ev.message.content)) {
1018
+ if (block.type !== "tool_result" || !block.tool_use_id) continue;
1019
+ backfillResult(
1020
+ state.toolSpanByUseId.get(block.tool_use_id),
1021
+ ts,
1022
+ block.is_error === true,
1023
+ block.content
1024
+ );
684
1025
  }
1026
+ } else if (ev.type === "attachment" && ev.attachment?.type === "tool_result" && ev.attachment.toolUseID) {
1027
+ const err = typeof ev.attachment.exitCode === "number" && ev.attachment.exitCode !== 0;
1028
+ backfillResult(state.toolSpanByUseId.get(ev.attachment.toolUseID), ts, err, ev.attachment.stderr ?? "");
685
1029
  }
686
- return { spans, toolSpanByUseId, nextStep: step };
687
1030
  }
688
- function backfillResult(s, endTime, isError, message) {
1031
+ function finishClaudeStream(state) {
1032
+ return { spans: state.spans, toolSpanByUseId: state.toolSpanByUseId, nextStep: state.step };
1033
+ }
1034
+ function backfillResult(s, endTime, isError, output) {
689
1035
  if (!s) return;
690
1036
  s.end_time = endTime;
691
1037
  const code = isError ? "ERROR" : "OK";
692
1038
  s.status = { code };
1039
+ recordToolOutput(s, output);
1040
+ const message = stringifyToolResult(output);
693
1041
  if (isError && message) s.status.message = message.slice(0, 500);
694
1042
  }
695
1043
  var ClaudeAdapter = class {
696
1044
  harness = "claude-code";
697
1045
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
698
1046
  root() {
699
- return join3(homedir2(), ".claude", "projects");
1047
+ return join2(homedir2(), ".claude", "projects");
700
1048
  }
701
1049
  async locate(opts = {}) {
702
1050
  const root = this.root();
703
1051
  let projectDirs;
704
1052
  try {
705
- projectDirs = await readdir2(root);
706
- } catch {
707
- return [];
1053
+ projectDirs = await readdir3(root);
1054
+ } catch (error) {
1055
+ if (isMissingPathError(error)) return [];
1056
+ throw error;
708
1057
  }
709
1058
  const refs = [];
710
1059
  for (const dir of projectDirs) {
711
- const dirPath = join3(root, dir);
1060
+ const dirPath = join2(root, dir);
712
1061
  let files;
713
1062
  try {
714
- files = await readdir2(dirPath);
715
- } catch {
716
- continue;
1063
+ files = await readdir3(dirPath);
1064
+ } catch (error) {
1065
+ if (isMissingPathError(error)) continue;
1066
+ throw error;
717
1067
  }
718
1068
  for (const f of files) {
719
1069
  if (!f.endsWith(".jsonl")) continue;
720
- const path = join3(dirPath, f);
1070
+ const path = join2(dirPath, f);
721
1071
  let st;
722
1072
  try {
723
1073
  st = await stat2(path);
724
- } catch {
725
- continue;
1074
+ } catch (error) {
1075
+ if (isMissingPathError(error)) continue;
1076
+ throw error;
726
1077
  }
727
1078
  if (!st.isFile()) continue;
728
1079
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -733,11 +1084,37 @@ var ClaudeAdapter = class {
733
1084
  }
734
1085
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
735
1086
  }
736
- async parse(ref) {
737
- const raw = await readFile3(ref.path, "utf8");
738
- const events = parseLines(raw);
739
- const traceId = events.find((e) => e.sessionId)?.sessionId ?? ref.sessionId;
1087
+ async parse(ref, options = {}) {
1088
+ const sourceTraceId = ref.sessionId;
1089
+ const sourceRootId = `root:${sourceTraceId}`;
1090
+ const ctx = {
1091
+ traceId: sourceTraceId,
1092
+ agent: SERVICE2,
1093
+ startStep: 0,
1094
+ idPrefix: "",
1095
+ rootParent: sourceRootId
1096
+ };
1097
+ const state = createClaudeStream(ctx.startStep);
1098
+ let discoveredTraceId;
1099
+ let firstTimestamp;
1100
+ let lastTimestamp;
1101
+ let sawEvent = false;
1102
+ for await (const event of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1103
+ if (!sawEvent) {
1104
+ firstTimestamp = event.timestamp;
1105
+ sawEvent = true;
1106
+ }
1107
+ lastTimestamp = event.timestamp;
1108
+ if (!discoveredTraceId && event.sessionId) discoveredTraceId = event.sessionId;
1109
+ consumeClaudeEvent(event, ctx, state);
1110
+ }
1111
+ const main = finishClaudeStream(state);
1112
+ const traceId = discoveredTraceId ?? sourceTraceId;
740
1113
  const rootId = `root:${traceId}`;
1114
+ for (const item of main.spans) {
1115
+ item.trace_id = traceId;
1116
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
1117
+ }
741
1118
  const spans = [
742
1119
  span({
743
1120
  traceId,
@@ -745,77 +1122,65 @@ var ClaudeAdapter = class {
745
1122
  parentSpanId: null,
746
1123
  name: "session",
747
1124
  kind: "AGENT",
748
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
749
- endTime: events.at(-1)?.timestamp,
1125
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1126
+ endTime: lastTimestamp,
750
1127
  service: SERVICE2,
751
1128
  agent: SERVICE2
752
1129
  })
753
1130
  ];
754
- const main = parseClaudeStream(events, {
755
- traceId,
756
- agent: SERVICE2,
757
- startStep: 0,
758
- idPrefix: "",
759
- rootParent: rootId
760
- });
761
- spans.push(...main.spans);
762
- await this.foldSubagents(ref, traceId, main, spans);
1131
+ appendAll(spans, main.spans);
1132
+ await this.foldSubagents(ref, traceId, main, spans, options);
763
1133
  return spans;
764
1134
  }
765
1135
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
766
- async foldSubagents(ref, traceId, main, out) {
767
- const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
1136
+ async foldSubagents(ref, traceId, main, out, options) {
1137
+ const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
768
1138
  let files;
769
1139
  try {
770
- files = await readdir2(subDir);
771
- } catch {
772
- return;
1140
+ files = await readdir3(subDir);
1141
+ } catch (error) {
1142
+ if (isMissingPathError(error)) return;
1143
+ throw error;
773
1144
  }
774
1145
  let step = main.nextStep;
775
- for (const f of files) {
776
- if (!f.endsWith(".jsonl")) continue;
777
- const hash2 = basename2(f, ".jsonl");
1146
+ for (const file of files) {
1147
+ if (!file.endsWith(".jsonl")) continue;
1148
+ const hash2 = basename2(file, ".jsonl");
1149
+ const metaPath = join2(subDir, `${hash2}.meta.json`);
778
1150
  let meta = {};
779
1151
  try {
780
- meta = JSON.parse(await readFile3(join3(subDir, `${hash2}.meta.json`), "utf8"));
781
- } catch {
1152
+ meta = await readJsonFile(metaPath);
1153
+ } catch (error) {
1154
+ if (!isMissingJsonSource(error)) throw error;
782
1155
  }
783
1156
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
784
- let raw;
785
- try {
786
- raw = await readFile3(join3(subDir, f), "utf8");
787
- } catch {
788
- continue;
789
- }
790
- const parsed = parseClaudeStream(parseLines(raw), {
1157
+ const ctx = {
791
1158
  traceId,
792
1159
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
793
1160
  startStep: step,
794
1161
  idPrefix: `${hash2}:`,
795
1162
  rootParent: parent
796
- });
797
- out.push(...parsed.spans);
1163
+ };
1164
+ const state = createClaudeStream(ctx.startStep);
1165
+ for await (const event of readJsonl(
1166
+ join2(subDir, file),
1167
+ sessionJsonlOptions(ref, options)
1168
+ )) {
1169
+ consumeClaudeEvent(event, ctx, state);
1170
+ }
1171
+ const parsed = finishClaudeStream(state);
1172
+ appendAll(out, parsed.spans);
798
1173
  step = parsed.nextStep;
799
1174
  }
800
1175
  }
801
1176
  };
802
1177
 
803
1178
  // src/adapters/codex.ts
804
- import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
1179
+ import { readdir as readdir4, stat as stat3 } from "fs/promises";
805
1180
  import { homedir as homedir3 } from "os";
806
- import { basename as basename3, join as join4 } from "path";
1181
+ import { basename as basename3, join as join3 } from "path";
807
1182
  var SERVICE3 = "codex";
808
- function parseLines2(raw) {
809
- const out = [];
810
- for (const line of raw.split("\n")) {
811
- if (!line) continue;
812
- try {
813
- out.push(JSON.parse(line));
814
- } catch {
815
- }
816
- }
817
- return out;
818
- }
1183
+ var SESSION_HEAD_LINES = 40;
819
1184
  function contentToString(content) {
820
1185
  if (typeof content === "string") return content;
821
1186
  if (Array.isArray(content)) {
@@ -826,10 +1191,82 @@ function contentToString(content) {
826
1191
  function textOf3(content) {
827
1192
  return capText(contentToString(content));
828
1193
  }
1194
+ function currentTaskStartedAt(meta) {
1195
+ const timestamp = Date.parse(meta?.payload?.timestamp ?? meta?.timestamp ?? "");
1196
+ return Number.isFinite(timestamp) ? Math.floor(timestamp / 1e3) : void 0;
1197
+ }
1198
+ function isCurrentTaskStart(line, startedAt) {
1199
+ return line.type === "event_msg" && line.payload?.type === "task_started" && typeof line.payload.started_at === "number" && Math.abs(line.payload.started_at - startedAt) <= 2;
1200
+ }
1201
+ async function hasCurrentTaskStart(path, options, startedAt) {
1202
+ for await (const line of readJsonl(path, options)) {
1203
+ if (isCurrentTaskStart(line, startedAt)) return true;
1204
+ }
1205
+ return false;
1206
+ }
1207
+ function numericStatus(value) {
1208
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1209
+ if (typeof value === "string" && /^-?\d+$/.test(value.trim())) return Number(value);
1210
+ return void 0;
1211
+ }
1212
+ function explicitOutputError(value) {
1213
+ if (Array.isArray(value)) {
1214
+ let observedSuccess = false;
1215
+ for (const item of value) {
1216
+ const status = explicitOutputError(item);
1217
+ if (status === true) return true;
1218
+ if (status === false) observedSuccess = true;
1219
+ }
1220
+ return observedSuccess ? false : void 0;
1221
+ }
1222
+ if (value && typeof value === "object") {
1223
+ const row = value;
1224
+ for (const key of ["exit_code", "exitCode"]) {
1225
+ const code = numericStatus(row[key]);
1226
+ if (code !== void 0 && ("output" in row || "chunk_id" in row || "wall_time_seconds" in row)) {
1227
+ return code !== 0;
1228
+ }
1229
+ }
1230
+ for (const key of ["timed_out", "timedOut"]) {
1231
+ if (row[key] === true) return true;
1232
+ }
1233
+ if (typeof row.succeeded === "boolean" && ("value" in row || "error" in row)) {
1234
+ return !row.succeeded;
1235
+ }
1236
+ if ((row.type === "input_text" || row.type === "text") && typeof row.text === "string") {
1237
+ return explicitOutputError(row.text);
1238
+ }
1239
+ return void 0;
1240
+ }
1241
+ if (typeof value !== "string") return void 0;
1242
+ const text = value.trim();
1243
+ if (!text) return void 0;
1244
+ if (text.startsWith("{") && text.endsWith("}") || text.startsWith("[") && text.endsWith("]")) {
1245
+ try {
1246
+ const parsedStatus = explicitOutputError(JSON.parse(text));
1247
+ if (parsedStatus !== void 0) return parsedStatus;
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ const outputStart = text.indexOf("\nOutput:\n");
1252
+ const header = outputStart >= 0 ? text.slice(0, outputStart) : text;
1253
+ if (/^Chunk ID:/i.test(header)) {
1254
+ const exitCode = header.match(/^Process exited with code\s+(-?\d+)\b/im)?.[1];
1255
+ if (exitCode !== void 0) return Number(exitCode) !== 0;
1256
+ }
1257
+ if (/^Script completed\s*\nWall time:/i.test(header)) return false;
1258
+ if (/^Script failed\s*\nWall time:/i.test(header)) return true;
1259
+ const scriptExitCode = header.match(/^Script error:\s*\nExit code:\s*(-?\d+)\b/im)?.[1];
1260
+ if (scriptExitCode !== void 0) return Number(scriptExitCode) !== 0;
1261
+ const commandExitCode = text.match(/^Command failed with exit code\s+(-?\d+)\.?$/i)?.[1];
1262
+ if (commandExitCode !== void 0) return Number(commandExitCode) !== 0;
1263
+ if (/^<tool_error>[\s\S]*<\/tool_error>$/i.test(text)) return true;
1264
+ return void 0;
1265
+ }
829
1266
  function outputIsError(output) {
830
- const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
831
- const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
832
- return { error, message: error ? s.slice(0, 500) : "" };
1267
+ const error = explicitOutputError(output) === true;
1268
+ const message = typeof output === "string" ? output : JSON.stringify(output ?? "");
1269
+ return { error, message: error ? message.slice(0, 500) : "" };
833
1270
  }
834
1271
  function singleNestedToolName(input) {
835
1272
  if (!input) return null;
@@ -842,6 +1279,28 @@ function toolInputToString(input) {
842
1279
  if (input == null) return void 0;
843
1280
  return JSON.stringify(input);
844
1281
  }
1282
+ 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;
1283
+ function sessionIds(value) {
1284
+ return [...new Set(contentToString(value).match(CODEX_SESSION_ID) ?? [])];
1285
+ }
1286
+ function spawnedSessionIds(output) {
1287
+ const text = contentToString(output);
1288
+ 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));
1289
+ return [...new Set(ids)];
1290
+ }
1291
+ function multiAgentOperation(name) {
1292
+ const prefix = "multi_agent_v1__";
1293
+ return name.startsWith(prefix) ? name.slice(prefix.length) : null;
1294
+ }
1295
+ function setAgentSessionIds(toolSpan, ids) {
1296
+ if (ids.length === 0) return;
1297
+ const unique = [...new Set(ids)];
1298
+ toolSpan.attributes["traces.codex.agent_session_ids"] = JSON.stringify(unique);
1299
+ toolSpan.attributes["traces.codex.agent_session_count"] = unique.length;
1300
+ if (toolSpan.attributes["traces.codex.agent_operation"] === "spawn_agent") {
1301
+ toolSpan.attributes["traces.child_session_ids"] = JSON.stringify(unique);
1302
+ }
1303
+ }
845
1304
  var verificationCommand = /\b(?:pnpm|npm|yarn|bun)\s+(?:run\s+)?(?:test|typecheck|lint|build|check)(?::[A-Za-z0-9:_-]+)?\b|\b(?:vitest|jest|pytest|tsc|biome|eslint|sha256sum|pdfinfo|pdftotext)\b|\bgo\s+test\b|\bcargo\s+(?:test|check|clippy|build)\b|\bgit\s+(?:status|diff|show|merge-tree)\b|\bgh-drew\s+pr\s+(?:view|checks)\b/i;
846
1305
  function hasReadOnlyCurl(input) {
847
1306
  if (!/\bcurl\b/i.test(input)) return false;
@@ -861,36 +1320,40 @@ function isExpectedBlockingTool(name, input) {
861
1320
  async function* walkRollouts(root) {
862
1321
  let years;
863
1322
  try {
864
- years = await readdir3(root);
865
- } catch {
866
- return;
1323
+ years = await readdir4(root);
1324
+ } catch (error) {
1325
+ if (isMissingPathError(error)) return;
1326
+ throw error;
867
1327
  }
868
1328
  for (const y of years) {
869
- const yp = join4(root, y);
1329
+ const yp = join3(root, y);
870
1330
  let months;
871
1331
  try {
872
- months = await readdir3(yp);
873
- } catch {
874
- continue;
1332
+ months = await readdir4(yp);
1333
+ } catch (error) {
1334
+ if (isMissingPathError(error)) continue;
1335
+ throw error;
875
1336
  }
876
1337
  for (const m of months) {
877
- const mp = join4(yp, m);
1338
+ const mp = join3(yp, m);
878
1339
  let days;
879
1340
  try {
880
- days = await readdir3(mp);
881
- } catch {
882
- continue;
1341
+ days = await readdir4(mp);
1342
+ } catch (error) {
1343
+ if (isMissingPathError(error)) continue;
1344
+ throw error;
883
1345
  }
884
1346
  for (const d of days) {
885
- const dp = join4(mp, d);
1347
+ const dp = join3(mp, d);
886
1348
  let files;
887
1349
  try {
888
- files = await readdir3(dp);
889
- } catch {
890
- continue;
1350
+ files = await readdir4(dp);
1351
+ } catch (error) {
1352
+ if (isMissingPathError(error)) continue;
1353
+ throw error;
891
1354
  }
892
1355
  for (const f of files) {
893
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
1356
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
894
1357
  }
895
1358
  }
896
1359
  }
@@ -900,7 +1363,7 @@ var CodexAdapter = class {
900
1363
  harness = "codex";
901
1364
  aliases = ["codex-acp"];
902
1365
  root() {
903
- return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
1366
+ return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
904
1367
  }
905
1368
  async locate(opts = {}) {
906
1369
  const refs = [];
@@ -908,75 +1371,113 @@ var CodexAdapter = class {
908
1371
  let st;
909
1372
  try {
910
1373
  st = await stat3(path);
911
- } catch {
912
- continue;
1374
+ } catch (error) {
1375
+ if (isMissingPathError(error)) continue;
1376
+ throw error;
913
1377
  }
914
1378
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
915
- const head = (await readFile4(path, "utf8").catch(() => "")).split("\n", 40);
916
1379
  let cwd = null;
917
1380
  let id = basename3(path).replace(/^rollout-[\dT-]+-/, "").replace(/\.jsonl$/, "");
918
- for (const line of head) {
919
- if (!line) continue;
920
- try {
921
- const l = JSON.parse(line);
922
- if (l.type === "session_meta" && l.payload?.id) id = l.payload.id;
923
- if (!cwd && l.payload?.cwd) cwd = l.payload.cwd;
924
- } catch {
925
- }
1381
+ const ref = { harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs };
1382
+ const head = await takeJsonl(path, SESSION_HEAD_LINES, sessionJsonlOptions(ref));
1383
+ for (const parsed of head) {
1384
+ if (parsed.type === "session_meta" && parsed.payload?.id) id = parsed.payload.id;
1385
+ if (!cwd && parsed.payload?.cwd) cwd = parsed.payload.cwd;
926
1386
  if (cwd) break;
927
1387
  }
928
- if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
929
- refs.push({ harness: this.harness, sessionId: id, path, cwd, mtimeMs: st.mtimeMs });
1388
+ ref.sessionId = id;
1389
+ ref.cwd = cwd;
1390
+ if (ref.integrity) {
1391
+ ref.integrity.corruptions = ref.integrity.corruptions.map((receipt) => ({ ...receipt, sessionId: id }));
1392
+ }
1393
+ if (opts.cwd && cwd && !cwd.startsWith(opts.cwd)) continue;
1394
+ if (opts.cwd && !cwd && !ref.integrity) continue;
1395
+ refs.push(ref);
930
1396
  }
931
1397
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
932
1398
  }
933
- async parse(ref) {
934
- const lines = parseLines2(await readFile4(ref.path, "utf8"));
935
- const meta = lines.find((l) => l.type === "session_meta");
1399
+ async parse(ref, options = {}) {
1400
+ const jsonl2 = sessionJsonlOptions(ref, options);
1401
+ const head = await takeJsonl(ref.path, SESSION_HEAD_LINES, jsonl2);
1402
+ let first = head[0];
1403
+ let meta = head.find((line) => line.type === "session_meta");
1404
+ const taskStartedAt = currentTaskStartedAt(meta);
1405
+ const hasTaskBoundary = taskStartedAt !== void 0 && await hasCurrentTaskStart(ref.path, jsonl2, taskStartedAt);
1406
+ let model = hasTaskBoundary ? null : head.find((line) => line.type === "turn_context")?.payload?.model ?? null;
1407
+ if (!first || !meta) {
1408
+ for await (const line of readJsonl(ref.path, jsonl2)) {
1409
+ first ??= line;
1410
+ if (!meta && line.type === "session_meta") meta = line;
1411
+ if (!model && line.type === "turn_context") model = line.payload?.model ?? null;
1412
+ if (first && meta) break;
1413
+ }
1414
+ }
936
1415
  const traceId = meta?.payload?.id ?? ref.sessionId;
937
- const model = lines.find((l) => l.type === "turn_context")?.payload?.model ?? null;
1416
+ const spawnMeta = meta?.payload?.source?.subagent?.thread_spawn;
1417
+ const parentSessionId = meta?.payload?.parent_thread_id ?? spawnMeta?.parent_thread_id;
1418
+ const sessionRole = parentSessionId || meta?.payload?.thread_source === "subagent" ? "child" : "operator";
938
1419
  const rootId = `root:${traceId}`;
939
- const spans = [
940
- span({
941
- traceId,
942
- spanId: rootId,
943
- parentSpanId: null,
944
- name: "session",
945
- kind: "AGENT",
946
- startTime: meta?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
947
- endTime: lines.at(-1)?.timestamp,
948
- service: SERVICE3,
949
- agent: SERVICE3,
950
- model
951
- })
952
- ];
1420
+ const root = span({
1421
+ traceId,
1422
+ spanId: rootId,
1423
+ parentSpanId: null,
1424
+ name: "session",
1425
+ kind: "AGENT",
1426
+ startTime: meta?.timestamp ?? first?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1427
+ service: SERVICE3,
1428
+ agent: SERVICE3,
1429
+ model,
1430
+ extra: {
1431
+ "traces.session.role": sessionRole,
1432
+ ...parentSessionId ? { "traces.parent_session_id": parentSessionId } : {},
1433
+ ...spawnMeta?.depth != null ? { "traces.codex.agent_depth": spawnMeta.depth } : {},
1434
+ ...meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname ? { "traces.codex.agent_nickname": meta?.payload?.agent_nickname ?? spawnMeta?.agent_nickname } : {},
1435
+ ...meta?.payload?.agent_role ?? spawnMeta?.agent_role ? { "traces.codex.agent_role": meta?.payload?.agent_role ?? spawnMeta?.agent_role } : {},
1436
+ ...spawnMeta?.agent_path ? { "traces.codex.agent_path": spawnMeta.agent_path } : {}
1437
+ }
1438
+ });
1439
+ const spans = [root];
953
1440
  const toolByCallId = /* @__PURE__ */ new Map();
954
1441
  const subagentByThreadId = /* @__PURE__ */ new Map();
955
1442
  let step = 0;
956
1443
  let lastLlm = rootId;
957
1444
  let sawUserTurn = false;
958
- for (const l of lines) {
1445
+ let lastTimestamp;
1446
+ const awaitingModel = model ? [] : [root];
1447
+ let reachedCurrentTask = !hasTaskBoundary;
1448
+ for await (const l of readJsonl(ref.path, jsonl2)) {
1449
+ if (!reachedCurrentTask) {
1450
+ if (isCurrentTaskStart(l, taskStartedAt)) reachedCurrentTask = true;
1451
+ else continue;
1452
+ }
1453
+ lastTimestamp = l.timestamp;
959
1454
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
960
- if (l.type === "event_msg" && l.payload?.type === "token_count") {
1455
+ if (!model && l.type === "turn_context" && l.payload?.model) {
1456
+ model = l.payload.model;
1457
+ for (const pending of awaitingModel) pending.attributes["llm.model_name"] = model;
1458
+ awaitingModel.length = 0;
1459
+ } else if (l.type === "event_msg" && l.payload?.type === "token_count") {
961
1460
  const u = l.payload.info?.last_token_usage;
962
1461
  if (u && (u.input_tokens || u.output_tokens)) {
963
1462
  const id = `llm:${step}`;
964
- spans.push(
965
- span({
966
- traceId,
967
- spanId: id,
968
- parentSpanId: rootId,
969
- name: "llm.turn",
970
- kind: "LLM",
971
- startTime: ts,
972
- service: SERVICE3,
973
- agent: SERVICE3,
974
- model,
975
- inputTokens: u.input_tokens ?? null,
976
- outputTokens: u.output_tokens ?? null,
977
- step
978
- })
979
- );
1463
+ const llm = span({
1464
+ traceId,
1465
+ spanId: id,
1466
+ parentSpanId: rootId,
1467
+ name: "llm.turn",
1468
+ kind: "LLM",
1469
+ startTime: ts,
1470
+ service: SERVICE3,
1471
+ agent: SERVICE3,
1472
+ model,
1473
+ inputTokens: u.input_tokens ?? null,
1474
+ outputTokens: u.output_tokens ?? null,
1475
+ reasoningTokens: u.reasoning_output_tokens ?? null,
1476
+ cachedInputTokens: u.cached_input_tokens ?? null,
1477
+ step
1478
+ });
1479
+ spans.push(llm);
1480
+ if (!model) awaitingModel.push(llm);
980
1481
  lastLlm = id;
981
1482
  step += 1;
982
1483
  }
@@ -988,6 +1489,7 @@ var CodexAdapter = class {
988
1489
  );
989
1490
  const nestedName = l.payload.type === "custom_tool_call" ? singleNestedToolName(input) : null;
990
1491
  const name = classifyNestedTool(nestedName ?? outerName, input);
1492
+ const agentOperation = multiAgentOperation(nestedName ?? outerName);
991
1493
  const toolSpan = span({
992
1494
  traceId,
993
1495
  spanId: `tool:${callId}`,
@@ -999,14 +1501,16 @@ var CodexAdapter = class {
999
1501
  agent: SERVICE3,
1000
1502
  tool: name,
1001
1503
  step,
1002
- content: input ?? null,
1003
1504
  extra: {
1505
+ ...toolIoAttributes({ input }),
1004
1506
  "traces.codex.call_type": l.payload.type,
1005
1507
  ...name !== outerName ? { "traces.codex.outer_tool_name": outerName } : {},
1006
1508
  ...nestedName ? { "traces.codex.nested_tool_name": nestedName } : {},
1007
- ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {}
1509
+ ...isExpectedBlockingTool(name, input) ? { "traces.expected_blocking": true } : {},
1510
+ ...agentOperation ? { "traces.codex.agent_operation": agentOperation } : {}
1008
1511
  }
1009
1512
  });
1513
+ if (agentOperation && agentOperation !== "spawn_agent") setAgentSessionIds(toolSpan, sessionIds(input));
1010
1514
  spans.push(toolSpan);
1011
1515
  toolByCallId.set(callId, toolSpan);
1012
1516
  step += 1;
@@ -1016,6 +1520,10 @@ var CodexAdapter = class {
1016
1520
  const { error, message } = outputIsError(l.payload.output);
1017
1521
  t.end_time = ts;
1018
1522
  t.status = error ? { code: "ERROR", message } : { code: "OK" };
1523
+ recordToolOutput(t, l.payload.output);
1524
+ if (t.attributes["traces.codex.agent_operation"] === "spawn_agent") {
1525
+ setAgentSessionIds(t, spawnedSessionIds(l.payload.output));
1526
+ }
1019
1527
  }
1020
1528
  } else if (l.type === "event_msg" && l.payload?.type === "sub_agent_activity") {
1021
1529
  const threadId = l.payload.agent_thread_id;
@@ -1035,12 +1543,14 @@ var CodexAdapter = class {
1035
1543
  agent: SERVICE3,
1036
1544
  tool: "Agent",
1037
1545
  step,
1038
- content: JSON.stringify({
1039
- subagent_type: subagentType,
1040
- agent_path: agentPath,
1041
- agent_thread_id: threadId
1042
- }),
1043
1546
  extra: {
1547
+ ...toolIoAttributes({
1548
+ input: {
1549
+ subagent_type: subagentType,
1550
+ agent_path: agentPath,
1551
+ agent_thread_id: threadId
1552
+ }
1553
+ }),
1044
1554
  "traces.codex.subagent_path": agentPath,
1045
1555
  "traces.codex.subagent_thread_id": threadId
1046
1556
  }
@@ -1102,47 +1612,39 @@ var CodexAdapter = class {
1102
1612
  }
1103
1613
  }
1104
1614
  }
1615
+ root.end_time = lastTimestamp ?? root.start_time;
1105
1616
  return spans;
1106
1617
  }
1107
1618
  };
1108
1619
 
1109
1620
  // src/adapters/copilot.ts
1110
- import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
1621
+ import { readdir as readdir5, stat as stat4 } from "fs/promises";
1111
1622
  import { homedir as homedir4 } from "os";
1112
- import { join as join5 } from "path";
1623
+ import { join as join4 } from "path";
1113
1624
  var SERVICE4 = "github-copilot";
1114
- function parseLines3(raw) {
1115
- const out = [];
1116
- for (const line of raw.split("\n")) {
1117
- if (!line.trim()) continue;
1118
- try {
1119
- out.push(JSON.parse(line));
1120
- } catch {
1121
- }
1122
- }
1123
- return out;
1124
- }
1125
1625
  var CopilotAdapter = class {
1126
1626
  harness = "github-copilot";
1127
1627
  aliases = ["copilot"];
1128
1628
  root() {
1129
- return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
1629
+ return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
1130
1630
  }
1131
1631
  async locate(opts = {}) {
1132
1632
  let dirs;
1133
1633
  try {
1134
- dirs = await readdir4(this.root());
1135
- } catch {
1136
- return [];
1634
+ dirs = await readdir5(this.root());
1635
+ } catch (error) {
1636
+ if (isMissingPathError(error)) return [];
1637
+ throw error;
1137
1638
  }
1138
1639
  const refs = [];
1139
1640
  for (const id of dirs) {
1140
- const path = join5(this.root(), id, "events.jsonl");
1641
+ const path = join4(this.root(), id, "events.jsonl");
1141
1642
  let st;
1142
1643
  try {
1143
1644
  st = await stat4(path);
1144
- } catch {
1145
- continue;
1645
+ } catch (error) {
1646
+ if (isMissingPathError(error)) continue;
1647
+ throw error;
1146
1648
  }
1147
1649
  if (!st.isFile()) continue;
1148
1650
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1151,28 +1653,23 @@ var CopilotAdapter = class {
1151
1653
  }
1152
1654
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1153
1655
  }
1154
- async parse(ref) {
1155
- const events = parseLines3(await readFile5(ref.path, "utf8"));
1656
+ async parse(ref, options = {}) {
1156
1657
  const traceId = ref.sessionId;
1157
1658
  const rootId = `root:${traceId}`;
1158
- const spans = [
1159
- span({
1160
- traceId,
1161
- spanId: rootId,
1162
- parentSpanId: null,
1163
- name: "session",
1164
- kind: "AGENT",
1165
- startTime: events[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1166
- endTime: events.at(-1)?.timestamp,
1167
- service: SERVICE4,
1168
- agent: SERVICE4
1169
- })
1170
- ];
1659
+ const spans = [];
1171
1660
  const toolByCallId = /* @__PURE__ */ new Map();
1661
+ let firstTimestamp;
1662
+ let lastTimestamp;
1663
+ let sawEvent = false;
1172
1664
  let step = 0;
1173
1665
  let lastLlm = rootId;
1174
1666
  let pendingInputTokens = null;
1175
- for (const ev of events) {
1667
+ for await (const ev of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1668
+ if (!sawEvent) {
1669
+ firstTimestamp = ev.timestamp;
1670
+ sawEvent = true;
1671
+ }
1672
+ lastTimestamp = ev.timestamp;
1176
1673
  const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1177
1674
  const d = ev.data ?? {};
1178
1675
  if (ev.type === "assistant.usage") {
@@ -1214,7 +1711,7 @@ var CopilotAdapter = class {
1214
1711
  agent: SERVICE4,
1215
1712
  tool: name,
1216
1713
  step,
1217
- content: d.arguments != null ? JSON.stringify(d.arguments) : null
1714
+ extra: toolIoAttributes({ input: d.arguments })
1218
1715
  });
1219
1716
  spans.push(t);
1220
1717
  toolByCallId.set(d.toolCallId, t);
@@ -1225,67 +1722,71 @@ var CopilotAdapter = class {
1225
1722
  t.end_time = ts;
1226
1723
  const err = d.success === false;
1227
1724
  t.status = err ? { code: "ERROR", message: (d.error?.message ?? "").slice(0, 500) } : { code: "OK" };
1725
+ recordToolOutput(t, d.output ?? d.result ?? d.error?.message);
1228
1726
  }
1229
1727
  }
1230
1728
  }
1231
- return spans;
1729
+ const root = span({
1730
+ traceId,
1731
+ spanId: rootId,
1732
+ parentSpanId: null,
1733
+ name: "session",
1734
+ kind: "AGENT",
1735
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1736
+ endTime: lastTimestamp,
1737
+ service: SERVICE4,
1738
+ agent: SERVICE4
1739
+ });
1740
+ return [root, ...spans];
1232
1741
  }
1233
1742
  };
1234
1743
 
1235
1744
  // src/adapters/factory.ts
1236
- import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
1745
+ import { readdir as readdir6, stat as stat5 } from "fs/promises";
1237
1746
  import { homedir as homedir5 } from "os";
1238
- import { basename as basename4, join as join6 } from "path";
1747
+ import { basename as basename4, join as join5 } from "path";
1239
1748
  var SERVICE5 = "factory";
1240
1749
  function textOf4(content) {
1241
1750
  return capText(
1242
1751
  (content ?? []).filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("")
1243
1752
  );
1244
1753
  }
1245
- function parseLines4(raw) {
1246
- const out = [];
1247
- for (const line of raw.split("\n")) {
1248
- if (!line) continue;
1249
- try {
1250
- out.push(JSON.parse(line));
1251
- } catch {
1252
- }
1253
- }
1254
- return out;
1255
- }
1256
1754
  var FactoryAdapter = class {
1257
1755
  harness = "factory";
1258
1756
  aliases = ["factory-droids", "droid"];
1259
1757
  root() {
1260
- return join6(homedir5(), ".factory", "sessions");
1758
+ return join5(homedir5(), ".factory", "sessions");
1261
1759
  }
1262
1760
  async locate(opts = {}) {
1263
1761
  const root = this.root();
1264
1762
  let dirs;
1265
1763
  try {
1266
- dirs = await readdir5(root);
1267
- } catch {
1268
- return [];
1764
+ dirs = await readdir6(root);
1765
+ } catch (error) {
1766
+ if (isMissingPathError(error)) return [];
1767
+ throw error;
1269
1768
  }
1270
1769
  const refs = [];
1271
1770
  for (const dir of dirs) {
1272
- const dp = join6(root, dir);
1771
+ const dp = join5(root, dir);
1273
1772
  let files;
1274
1773
  try {
1275
- files = await readdir5(dp);
1276
- } catch {
1277
- continue;
1774
+ files = await readdir6(dp);
1775
+ } catch (error) {
1776
+ if (isMissingPathError(error)) continue;
1777
+ throw error;
1278
1778
  }
1279
1779
  const cwd = dir.replace(/-/g, "/");
1280
1780
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1281
1781
  for (const f of files) {
1282
1782
  if (!f.endsWith(".jsonl")) continue;
1283
- const path = join6(dp, f);
1783
+ const path = join5(dp, f);
1284
1784
  let st;
1285
1785
  try {
1286
1786
  st = await stat5(path);
1287
- } catch {
1288
- continue;
1787
+ } catch (error) {
1788
+ if (isMissingPathError(error)) continue;
1789
+ throw error;
1289
1790
  }
1290
1791
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1291
1792
  refs.push({ harness: this.harness, sessionId: basename4(f, ".jsonl"), path, cwd, mtimeMs: st.mtimeMs });
@@ -1293,38 +1794,32 @@ var FactoryAdapter = class {
1293
1794
  }
1294
1795
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1295
1796
  }
1296
- async parse(ref) {
1297
- const lines = parseLines4(await readFile6(ref.path, "utf8"));
1298
- const start = lines.find((l) => l.type === "session_start");
1299
- const traceId = start?.id ?? ref.sessionId;
1300
- const rootId = `root:${traceId}`;
1797
+ async parse(ref, options = {}) {
1301
1798
  let settings = {};
1302
1799
  try {
1303
- settings = JSON.parse(await readFile6(ref.path.replace(/\.jsonl$/, ".settings.json"), "utf8"));
1304
- } catch {
1800
+ settings = await readJsonFile(ref.path.replace(/\.jsonl$/, ".settings.json"));
1801
+ } catch (error) {
1802
+ if (!isMissingJsonSource(error)) throw error;
1305
1803
  }
1306
- const spans = [
1307
- span({
1308
- traceId,
1309
- spanId: rootId,
1310
- parentSpanId: null,
1311
- name: "session",
1312
- kind: "AGENT",
1313
- startTime: start?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1314
- endTime: lines.at(-1)?.timestamp,
1315
- service: SERVICE5,
1316
- agent: SERVICE5,
1317
- model: settings.model ?? null,
1318
- extra: settings.tokenUsage ? {
1319
- "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
1320
- "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
1321
- } : void 0
1322
- })
1323
- ];
1804
+ const sourceTraceId = ref.sessionId;
1805
+ const sourceRootId = `root:${sourceTraceId}`;
1806
+ const spans = [];
1324
1807
  const toolByUseId = /* @__PURE__ */ new Map();
1808
+ let firstTimestamp;
1809
+ let lastTimestamp;
1810
+ let sessionLine;
1811
+ let sawLine = false;
1325
1812
  let step = 0;
1326
- let lastLlm = rootId;
1327
- for (const l of lines) {
1813
+ let lastLlm = sourceRootId;
1814
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
1815
+ if (!sawLine) {
1816
+ firstTimestamp = l.timestamp;
1817
+ sawLine = true;
1818
+ }
1819
+ lastTimestamp = l.timestamp;
1820
+ if (!sessionLine && l.type === "session_start") {
1821
+ sessionLine = { id: l.id, timestamp: l.timestamp };
1822
+ }
1328
1823
  if (l.type !== "message" || !l.message) continue;
1329
1824
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1330
1825
  const role = l.message.role;
@@ -1333,9 +1828,9 @@ var FactoryAdapter = class {
1333
1828
  if (text) {
1334
1829
  spans.push(
1335
1830
  userPromptSpan({
1336
- traceId,
1831
+ traceId: sourceTraceId,
1337
1832
  spanId: `user:${l.id ?? step}`,
1338
- parentSpanId: rootId,
1833
+ parentSpanId: sourceRootId,
1339
1834
  startTime: ts,
1340
1835
  content: text,
1341
1836
  service: SERVICE5,
@@ -1349,9 +1844,9 @@ var FactoryAdapter = class {
1349
1844
  const llmId = `llm:${l.id ?? step}`;
1350
1845
  spans.push(
1351
1846
  span({
1352
- traceId,
1847
+ traceId: sourceTraceId,
1353
1848
  spanId: llmId,
1354
- parentSpanId: rootId,
1849
+ parentSpanId: sourceRootId,
1355
1850
  name: `message.${role ?? "unknown"}`,
1356
1851
  kind: "LLM",
1357
1852
  startTime: ts,
@@ -1368,7 +1863,7 @@ var FactoryAdapter = class {
1368
1863
  for (const b of l.message.content ?? []) {
1369
1864
  if (b.type === "tool_use" && b.name) {
1370
1865
  const t = span({
1371
- traceId,
1866
+ traceId: sourceTraceId,
1372
1867
  spanId: `tool:${b.id ?? `${l.id}:${step}`}`,
1373
1868
  parentSpanId: lastLlm,
1374
1869
  name: `tool.${b.name}`,
@@ -1378,25 +1873,50 @@ var FactoryAdapter = class {
1378
1873
  agent: SERVICE5,
1379
1874
  tool: b.name,
1380
1875
  step,
1381
- content: b.input != null ? JSON.stringify(b.input) : null
1876
+ extra: toolIoAttributes({ input: b.input })
1382
1877
  });
1383
1878
  spans.push(t);
1384
1879
  if (b.id) toolByUseId.set(b.id, t);
1385
1880
  step += 1;
1386
1881
  } else if (b.type === "tool_result" && b.tool_use_id) {
1387
1882
  const t = toolByUseId.get(b.tool_use_id);
1388
- if (t) t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
1883
+ if (t) {
1884
+ t.status = b.is_error === true ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
1885
+ recordToolOutput(t, b.content);
1886
+ }
1389
1887
  }
1390
1888
  }
1391
1889
  }
1392
- return spans;
1890
+ const traceId = sessionLine?.id ?? sourceTraceId;
1891
+ const rootId = `root:${traceId}`;
1892
+ for (const item of spans) {
1893
+ item.trace_id = traceId;
1894
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
1895
+ }
1896
+ const root = span({
1897
+ traceId,
1898
+ spanId: rootId,
1899
+ parentSpanId: null,
1900
+ name: "session",
1901
+ kind: "AGENT",
1902
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1903
+ endTime: lastTimestamp,
1904
+ service: SERVICE5,
1905
+ agent: SERVICE5,
1906
+ model: settings.model ?? null,
1907
+ extra: settings.tokenUsage ? {
1908
+ "session.input_tokens": settings.tokenUsage.inputTokens ?? 0,
1909
+ "session.output_tokens": settings.tokenUsage.outputTokens ?? 0
1910
+ } : void 0
1911
+ });
1912
+ return [root, ...spans];
1393
1913
  }
1394
1914
  };
1395
1915
 
1396
1916
  // src/adapters/forge.ts
1397
- import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
1917
+ import { readdir as readdir7, stat as stat6 } from "fs/promises";
1398
1918
  import { homedir as homedir6 } from "os";
1399
- import { basename as basename5, join as join7 } from "path";
1919
+ import { basename as basename5, join as join6 } from "path";
1400
1920
  var SERVICE6 = "forge";
1401
1921
  function tokens(t) {
1402
1922
  if (!t) return null;
@@ -1410,7 +1930,7 @@ var ForgeAdapter = class {
1410
1930
  aliases = ["forgecode"];
1411
1931
  /** Directories scanned for `*-dump.json` exports. */
1412
1932
  dumpDirs() {
1413
- return [process.cwd(), join7(homedir6(), ".forge")];
1933
+ return [process.cwd(), join6(homedir6(), ".forge")];
1414
1934
  }
1415
1935
  async locate(opts = {}) {
1416
1936
  const refs = [];
@@ -1418,18 +1938,20 @@ var ForgeAdapter = class {
1418
1938
  for (const dir of dirs) {
1419
1939
  let files;
1420
1940
  try {
1421
- files = await readdir6(dir);
1422
- } catch {
1423
- continue;
1941
+ files = await readdir7(dir);
1942
+ } catch (error) {
1943
+ if (isMissingPathError(error)) continue;
1944
+ throw error;
1424
1945
  }
1425
1946
  for (const f of files) {
1426
1947
  if (!f.endsWith("-dump.json")) continue;
1427
- const path = join7(dir, f);
1948
+ const path = join6(dir, f);
1428
1949
  let st;
1429
1950
  try {
1430
1951
  st = await stat6(path);
1431
- } catch {
1432
- continue;
1952
+ } catch (error) {
1953
+ if (isMissingPathError(error)) continue;
1954
+ throw error;
1433
1955
  }
1434
1956
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1435
1957
  refs.push({ harness: this.harness, sessionId: basename5(f, ".json"), path, cwd: dir, mtimeMs: st.mtimeMs });
@@ -1438,12 +1960,7 @@ var ForgeAdapter = class {
1438
1960
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1439
1961
  }
1440
1962
  async parse(ref) {
1441
- let dump;
1442
- try {
1443
- dump = JSON.parse(await readFile7(ref.path, "utf8"));
1444
- } catch {
1445
- return [];
1446
- }
1963
+ const dump = await readJsonFile(ref.path);
1447
1964
  const ctx = dump.context ?? dump;
1448
1965
  const traceId = ctx.conversation_id ?? ref.sessionId;
1449
1966
  const rootId = `root:${traceId}`;
@@ -1508,7 +2025,7 @@ var ForgeAdapter = class {
1508
2025
  agent: SERVICE6,
1509
2026
  tool: tc.name ?? "tool",
1510
2027
  step,
1511
- content: tc.arguments != null ? JSON.stringify(tc.arguments) : null
2028
+ extra: toolIoAttributes({ input: tc.arguments })
1512
2029
  });
1513
2030
  spans.push(t);
1514
2031
  toolByCallId.set(id, t);
@@ -1516,7 +2033,10 @@ var ForgeAdapter = class {
1516
2033
  }
1517
2034
  } else if (entry.tool) {
1518
2035
  const t = toolByCallId.get(entry.tool.call_id ?? "");
1519
- if (t) t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2036
+ if (t) {
2037
+ t.status = entry.tool.output?.is_error ? { code: "ERROR", message: "tool reported error" } : { code: "OK" };
2038
+ recordToolOutput(t, entry.tool.output?.values ?? entry.tool.output);
2039
+ }
1520
2040
  }
1521
2041
  }
1522
2042
  return spans;
@@ -1524,9 +2044,9 @@ var ForgeAdapter = class {
1524
2044
  };
1525
2045
 
1526
2046
  // src/adapters/gemini.ts
1527
- import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
2047
+ import { readdir as readdir8, stat as stat7 } from "fs/promises";
1528
2048
  import { homedir as homedir7 } from "os";
1529
- import { join as join8 } from "path";
2049
+ import { join as join7 } from "path";
1530
2050
  function iso(ts, fallback) {
1531
2051
  if (typeof ts === "number") return new Date(ts).toISOString();
1532
2052
  if (typeof ts === "string" && ts.length > 0) return ts;
@@ -1551,7 +2071,7 @@ var GeminiFamilyAdapter = class {
1551
2071
  this.homeDirName = cfg.homeDirName;
1552
2072
  }
1553
2073
  chatsRoot() {
1554
- return join8(homedir7(), this.homeDirName, "tmp");
2074
+ return join7(homedir7(), this.homeDirName, "tmp");
1555
2075
  }
1556
2076
  /** `<home>/projects.json` maps `{ <absolute-cwd>: <projectName> }`. The session
1557
2077
  * dir name (`tmp/<projectHash>/`) is that projectName for registered projects,
@@ -1560,10 +2080,12 @@ var GeminiFamilyAdapter = class {
1560
2080
  async loadProjectCwds() {
1561
2081
  const nameToPath = /* @__PURE__ */ new Map();
1562
2082
  try {
1563
- const raw = await readFile8(join8(homedir7(), this.homeDirName, "projects.json"), "utf8");
1564
- const projects = JSON.parse(raw).projects ?? {};
2083
+ const projects = (await readJsonFile(
2084
+ join7(homedir7(), this.homeDirName, "projects.json")
2085
+ )).projects ?? {};
1565
2086
  for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
1566
- } catch {
2087
+ } catch (error) {
2088
+ if (!isMissingJsonSource(error)) throw error;
1567
2089
  }
1568
2090
  return nameToPath;
1569
2091
  }
@@ -1571,29 +2093,32 @@ var GeminiFamilyAdapter = class {
1571
2093
  const root = this.chatsRoot();
1572
2094
  let projectDirs;
1573
2095
  try {
1574
- projectDirs = await readdir7(root);
1575
- } catch {
1576
- return [];
2096
+ projectDirs = await readdir8(root);
2097
+ } catch (error) {
2098
+ if (isMissingPathError(error)) return [];
2099
+ throw error;
1577
2100
  }
1578
2101
  const projectCwds = await this.loadProjectCwds();
1579
2102
  const refs = [];
1580
2103
  for (const pd of projectDirs) {
1581
2104
  const cwd = projectCwds.get(pd) ?? null;
1582
- const chatsDir = join8(root, pd, "chats");
2105
+ const chatsDir = join7(root, pd, "chats");
1583
2106
  let files;
1584
2107
  try {
1585
- files = await readdir7(chatsDir);
1586
- } catch {
1587
- continue;
2108
+ files = await readdir8(chatsDir);
2109
+ } catch (error) {
2110
+ if (isMissingPathError(error)) continue;
2111
+ throw error;
1588
2112
  }
1589
2113
  for (const f of files) {
1590
2114
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1591
- const path = join8(chatsDir, f);
2115
+ const path = join7(chatsDir, f);
1592
2116
  let st;
1593
2117
  try {
1594
2118
  st = await stat7(path);
1595
- } catch {
1596
- continue;
2119
+ } catch (error) {
2120
+ if (isMissingPathError(error)) continue;
2121
+ throw error;
1597
2122
  }
1598
2123
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1599
2124
  if (opts.cwd && (!cwd || !cwd.startsWith(opts.cwd))) continue;
@@ -1603,12 +2128,7 @@ var GeminiFamilyAdapter = class {
1603
2128
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1604
2129
  }
1605
2130
  async parse(ref) {
1606
- let session;
1607
- try {
1608
- session = JSON.parse(await readFile8(ref.path, "utf8"));
1609
- } catch {
1610
- return [];
1611
- }
2131
+ const session = await readJsonFile(ref.path);
1612
2132
  const traceId = session.sessionId ?? ref.sessionId;
1613
2133
  const messages = session.messages ?? [];
1614
2134
  const rootId = `root:${traceId}`;
@@ -1683,7 +2203,7 @@ var GeminiFamilyAdapter = class {
1683
2203
  agent: this.service,
1684
2204
  tool: tc.name ?? "tool",
1685
2205
  step,
1686
- content: tc.args != null ? JSON.stringify(tc.args) : null
2206
+ extra: toolIoAttributes({ input: tc.args, output: tc.result })
1687
2207
  })
1688
2208
  );
1689
2209
  step += 1;
@@ -1699,9 +2219,9 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1699
2219
  };
1700
2220
 
1701
2221
  // src/adapters/opencode.ts
1702
- import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
2222
+ import { readdir as readdir9, stat as stat8 } from "fs/promises";
1703
2223
  import { homedir as homedir8 } from "os";
1704
- import { join as join9 } from "path";
2224
+ import { join as join8 } from "path";
1705
2225
  var SERVICE7 = "opencode";
1706
2226
  function isoOf(ms) {
1707
2227
  return new Date(ms ?? 0).toISOString();
@@ -1714,25 +2234,27 @@ function textOf7(parts) {
1714
2234
  var OpencodeAdapter = class {
1715
2235
  harness = "opencode";
1716
2236
  storage() {
1717
- const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1718
- return join9(base, "opencode", "storage");
2237
+ const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
2238
+ return join8(base, "opencode", "storage");
1719
2239
  }
1720
2240
  async locate(opts = {}) {
1721
- const msgRoot = join9(this.storage(), "message");
2241
+ const msgRoot = join8(this.storage(), "message");
1722
2242
  let sessionDirs;
1723
2243
  try {
1724
- sessionDirs = await readdir8(msgRoot);
1725
- } catch {
1726
- return [];
2244
+ sessionDirs = await readdir9(msgRoot);
2245
+ } catch (error) {
2246
+ if (isMissingPathError(error)) return [];
2247
+ throw error;
1727
2248
  }
1728
2249
  const refs = [];
1729
2250
  for (const sid of sessionDirs) {
1730
- const dir = join9(msgRoot, sid);
2251
+ const dir = join8(msgRoot, sid);
1731
2252
  let st;
1732
2253
  try {
1733
2254
  st = await stat8(dir);
1734
- } catch {
1735
- continue;
2255
+ } catch (error) {
2256
+ if (isMissingPathError(error)) continue;
2257
+ throw error;
1736
2258
  }
1737
2259
  if (!st.isDirectory()) continue;
1738
2260
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
@@ -1744,21 +2266,13 @@ var OpencodeAdapter = class {
1744
2266
  async parse(ref) {
1745
2267
  const traceId = ref.sessionId;
1746
2268
  const rootId = `root:${traceId}`;
1747
- const partRoot = join9(this.storage(), "part");
1748
- let msgFiles;
1749
- try {
1750
- msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
1751
- } catch {
1752
- return [];
1753
- }
1754
- const messages = [];
1755
- for (const f of msgFiles) {
1756
- try {
1757
- messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
1758
- } catch {
1759
- }
2269
+ const partRoot = join8(this.storage(), "part");
2270
+ const msgFiles = await listJsonFiles(ref.path);
2271
+ const messages = [];
2272
+ for (const f of msgFiles) {
2273
+ messages.push(await readJsonFile(join8(ref.path, f)));
1760
2274
  }
1761
- messages.sort((a, b) => (a.msg.time?.created ?? 0) - (b.msg.time?.created ?? 0));
2275
+ messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
1762
2276
  const spans = [
1763
2277
  span({
1764
2278
  traceId,
@@ -1766,59 +2280,65 @@ var OpencodeAdapter = class {
1766
2280
  parentSpanId: null,
1767
2281
  name: "session",
1768
2282
  kind: "AGENT",
1769
- startTime: isoOf(messages[0]?.msg.time?.created),
1770
- endTime: isoOf(messages.at(-1)?.msg.time?.completed ?? messages.at(-1)?.msg.time?.created),
2283
+ startTime: isoOf(messages[0]?.time?.created),
2284
+ endTime: isoOf(messages.at(-1)?.time?.completed ?? messages.at(-1)?.time?.created),
1771
2285
  service: SERVICE7,
1772
2286
  agent: SERVICE7
1773
2287
  })
1774
2288
  ];
1775
2289
  let step = 0;
1776
- for (const { msg } of messages) {
2290
+ for (const msg of messages) {
1777
2291
  const mid = msg.id ?? `m${step}`;
1778
2292
  const llmId = `llm:${mid}`;
1779
- const pdir = join9(partRoot, mid);
2293
+ const pdir = join8(partRoot, mid);
1780
2294
  const parts = [];
2295
+ let partFiles = [];
1781
2296
  try {
1782
- const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1783
- for (const pf of partFiles) {
1784
- try {
1785
- parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
1786
- } catch {
1787
- }
1788
- }
1789
- } catch {
2297
+ partFiles = await listJsonFiles(pdir);
2298
+ } catch (error) {
2299
+ if (!isMissingJsonSource(error)) throw error;
2300
+ }
2301
+ for (const pf of partFiles) {
2302
+ parts.push(await readJsonFile(join8(pdir, pf)));
1790
2303
  }
1791
2304
  const turnText = textOf7(parts);
1792
- spans.push(
1793
- span({
1794
- traceId,
1795
- spanId: llmId,
1796
- parentSpanId: rootId,
1797
- name: `message.${msg.role ?? "unknown"}`,
1798
- kind: "LLM",
1799
- startTime: isoOf(msg.time?.created),
1800
- endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1801
- service: SERVICE7,
1802
- agent: SERVICE7,
1803
- model: msg.modelID ?? null,
1804
- inputTokens: msg.tokens?.input ?? null,
1805
- outputTokens: msg.tokens?.output ?? null,
1806
- step,
1807
- content: turnText || null
1808
- })
1809
- );
1810
- step += 1;
1811
- if (msg.role === "user" && turnText) {
2305
+ const isUser = msg.role === "user";
2306
+ if (isUser) {
2307
+ if (turnText) {
2308
+ spans.push(
2309
+ userPromptSpan({
2310
+ traceId,
2311
+ spanId: `${llmId}:user`,
2312
+ parentSpanId: rootId,
2313
+ startTime: isoOf(msg.time?.created),
2314
+ service: SERVICE7,
2315
+ agent: SERVICE7,
2316
+ step,
2317
+ content: turnText
2318
+ })
2319
+ );
2320
+ step += 1;
2321
+ }
2322
+ } else {
1812
2323
  spans.push(
1813
- userPromptSpan({
2324
+ span({
1814
2325
  traceId,
1815
- spanId: `${llmId}:user`,
2326
+ spanId: llmId,
1816
2327
  parentSpanId: rootId,
2328
+ name: `message.${msg.role ?? "unknown"}`,
2329
+ kind: "LLM",
1817
2330
  startTime: isoOf(msg.time?.created),
2331
+ endTime: isoOf(msg.time?.completed ?? msg.time?.created),
1818
2332
  service: SERVICE7,
1819
2333
  agent: SERVICE7,
2334
+ model: msg.modelID ?? null,
2335
+ inputTokens: msg.tokens?.input ?? null,
2336
+ outputTokens: msg.tokens?.output ?? null,
2337
+ reasoningTokens: msg.tokens?.reasoning ?? null,
2338
+ cachedInputTokens: msg.tokens?.cache?.read ?? null,
2339
+ cacheWriteInputTokens: msg.tokens?.cache?.write ?? null,
1820
2340
  step,
1821
- content: turnText
2341
+ content: turnText || null
1822
2342
  })
1823
2343
  );
1824
2344
  step += 1;
@@ -1830,7 +2350,7 @@ var OpencodeAdapter = class {
1830
2350
  span({
1831
2351
  traceId,
1832
2352
  spanId: `tool:${part.callID ?? `${mid}:${step}`}`,
1833
- parentSpanId: llmId,
2353
+ parentSpanId: isUser ? rootId : llmId,
1834
2354
  name: `tool.${part.tool}`,
1835
2355
  kind: "TOOL",
1836
2356
  startTime: isoOf(msg.time?.created),
@@ -1841,7 +2361,10 @@ var OpencodeAdapter = class {
1841
2361
  agent: SERVICE7,
1842
2362
  tool: part.tool,
1843
2363
  step,
1844
- content: part.state?.input != null ? JSON.stringify(part.state.input) : null
2364
+ extra: toolIoAttributes({
2365
+ input: part.state?.input,
2366
+ output: part.state?.output ?? part.state?.error
2367
+ })
1845
2368
  })
1846
2369
  );
1847
2370
  step += 1;
@@ -1852,21 +2375,10 @@ var OpencodeAdapter = class {
1852
2375
  };
1853
2376
 
1854
2377
  // src/adapters/pi.ts
1855
- import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
2378
+ import { readdir as readdir10, stat as stat9 } from "fs/promises";
1856
2379
  import { homedir as homedir9 } from "os";
1857
- import { basename as basename6, join as join10 } from "path";
2380
+ import { basename as basename6, join as join9 } from "path";
1858
2381
  var SERVICE8 = "pi";
1859
- function parseLines5(raw) {
1860
- const out = [];
1861
- for (const line of raw.split("\n")) {
1862
- if (!line) continue;
1863
- try {
1864
- out.push(JSON.parse(line));
1865
- } catch {
1866
- }
1867
- }
1868
- return out;
1869
- }
1870
2382
  function isToolBlock(b) {
1871
2383
  return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
1872
2384
  }
@@ -1882,35 +2394,38 @@ function textOf8(content) {
1882
2394
  var PiAdapter = class {
1883
2395
  harness = "pi";
1884
2396
  root() {
1885
- return join10(homedir9(), ".pi", "agent", "sessions");
2397
+ return join9(homedir9(), ".pi", "agent", "sessions");
1886
2398
  }
1887
2399
  async locate(opts = {}) {
1888
2400
  const root = this.root();
1889
2401
  let dirs;
1890
2402
  try {
1891
- dirs = await readdir9(root);
1892
- } catch {
1893
- return [];
2403
+ dirs = await readdir10(root);
2404
+ } catch (error) {
2405
+ if (isMissingPathError(error)) return [];
2406
+ throw error;
1894
2407
  }
1895
2408
  const refs = [];
1896
2409
  for (const dir of dirs) {
1897
- const dp = join10(root, dir);
2410
+ const dp = join9(root, dir);
1898
2411
  let files;
1899
2412
  try {
1900
- files = await readdir9(dp);
1901
- } catch {
1902
- continue;
2413
+ files = await readdir10(dp);
2414
+ } catch (error) {
2415
+ if (isMissingPathError(error)) continue;
2416
+ throw error;
1903
2417
  }
1904
2418
  const cwd = `/${dir.replace(/^-+/, "").replace(/-+$/, "").replace(/-/g, "/")}`;
1905
2419
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1906
2420
  for (const f of files) {
1907
2421
  if (!f.endsWith(".jsonl")) continue;
1908
- const path = join10(dp, f);
2422
+ const path = join9(dp, f);
1909
2423
  let st;
1910
2424
  try {
1911
2425
  st = await stat9(path);
1912
- } catch {
1913
- continue;
2426
+ } catch (error) {
2427
+ if (isMissingPathError(error)) continue;
2428
+ throw error;
1914
2429
  }
1915
2430
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
1916
2431
  const id = basename6(f, ".jsonl").replace(/^[\dTZ.-]+_/, "");
@@ -1919,27 +2434,25 @@ var PiAdapter = class {
1919
2434
  }
1920
2435
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
1921
2436
  }
1922
- async parse(ref) {
1923
- const lines = parseLines5(await readFile10(ref.path, "utf8"));
1924
- const sessionLine = lines.find((l) => l.type === "session");
1925
- const traceId = sessionLine?.id ?? ref.sessionId;
1926
- const rootId = `root:${traceId}`;
1927
- const spans = [
1928
- span({
1929
- traceId,
1930
- spanId: rootId,
1931
- parentSpanId: null,
1932
- name: "session",
1933
- kind: "AGENT",
1934
- startTime: sessionLine?.timestamp ?? lines[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
1935
- endTime: lines.at(-1)?.timestamp,
1936
- service: SERVICE8,
1937
- agent: SERVICE8
1938
- })
1939
- ];
2437
+ async parse(ref, options = {}) {
2438
+ const sourceTraceId = ref.sessionId;
2439
+ const sourceRootId = `root:${sourceTraceId}`;
2440
+ const spans = [];
1940
2441
  const toolByCallId = /* @__PURE__ */ new Map();
2442
+ let firstTimestamp;
2443
+ let lastTimestamp;
2444
+ let sessionLine;
2445
+ let sawLine = false;
1941
2446
  let step = 0;
1942
- for (const l of lines) {
2447
+ for await (const l of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
2448
+ if (!sawLine) {
2449
+ firstTimestamp = l.timestamp;
2450
+ sawLine = true;
2451
+ }
2452
+ lastTimestamp = l.timestamp;
2453
+ if (!sessionLine && l.type === "session") {
2454
+ sessionLine = { id: l.id, timestamp: l.timestamp };
2455
+ }
1943
2456
  if (l.type !== "message" || !l.message) continue;
1944
2457
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1945
2458
  const mid = l.id ?? `m${step}`;
@@ -1950,9 +2463,9 @@ var PiAdapter = class {
1950
2463
  if (prompt) {
1951
2464
  spans.push(
1952
2465
  userPromptSpan({
1953
- traceId,
2466
+ traceId: sourceTraceId,
1954
2467
  spanId: `${mid}:user`,
1955
- parentSpanId: rootId,
2468
+ parentSpanId: sourceRootId,
1956
2469
  startTime: ts,
1957
2470
  service: SERVICE8,
1958
2471
  agent: SERVICE8,
@@ -1966,9 +2479,9 @@ var PiAdapter = class {
1966
2479
  const errored = !!msg.errorMessage;
1967
2480
  spans.push(
1968
2481
  span({
1969
- traceId,
2482
+ traceId: sourceTraceId,
1970
2483
  spanId: llmId,
1971
- parentSpanId: rootId,
2484
+ parentSpanId: sourceRootId,
1972
2485
  name: `message.${msg.role ?? "unknown"}`,
1973
2486
  kind: "LLM",
1974
2487
  startTime: ts,
@@ -1990,7 +2503,7 @@ var PiAdapter = class {
1990
2503
  const name = b.toolName ?? b.name ?? "tool";
1991
2504
  const callId = b.id ?? b.callId ?? b.toolCallId ?? `${mid}:${step}`;
1992
2505
  const toolSpan = span({
1993
- traceId,
2506
+ traceId: sourceTraceId,
1994
2507
  spanId: `tool:${callId}`,
1995
2508
  parentSpanId: llmId,
1996
2509
  name: `tool.${name}`,
@@ -2000,7 +2513,7 @@ var PiAdapter = class {
2000
2513
  agent: SERVICE8,
2001
2514
  tool: name,
2002
2515
  step,
2003
- content: (b.input ?? b.args) != null ? JSON.stringify(b.input ?? b.args) : null
2516
+ extra: toolIoAttributes({ input: b.input ?? b.args })
2004
2517
  });
2005
2518
  spans.push(toolSpan);
2006
2519
  toolByCallId.set(callId, toolSpan);
@@ -2012,30 +2525,37 @@ var PiAdapter = class {
2012
2525
  const err = b.isError === true || b.is_error === true;
2013
2526
  t.end_time = ts;
2014
2527
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2528
+ recordToolOutput(t, b.output ?? b.result ?? b.content ?? b.text);
2015
2529
  }
2016
2530
  }
2017
2531
  }
2018
2532
  }
2019
- return spans;
2533
+ const traceId = sessionLine?.id ?? sourceTraceId;
2534
+ const rootId = `root:${traceId}`;
2535
+ for (const item of spans) {
2536
+ item.trace_id = traceId;
2537
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
2538
+ }
2539
+ const root = span({
2540
+ traceId,
2541
+ spanId: rootId,
2542
+ parentSpanId: null,
2543
+ name: "session",
2544
+ kind: "AGENT",
2545
+ startTime: sessionLine?.timestamp ?? firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2546
+ endTime: lastTimestamp,
2547
+ service: SERVICE8,
2548
+ agent: SERVICE8
2549
+ });
2550
+ return [root, ...spans];
2020
2551
  }
2021
2552
  };
2022
2553
 
2023
2554
  // src/adapters/qwen.ts
2024
- import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
2555
+ import { readdir as readdir11, stat as stat10 } from "fs/promises";
2025
2556
  import { homedir as homedir10 } from "os";
2026
- import { basename as basename7, join as join11 } from "path";
2557
+ import { basename as basename7, join as join10 } from "path";
2027
2558
  var SERVICE9 = "qwen";
2028
- function parseLines6(raw) {
2029
- const out = [];
2030
- for (const line of raw.split("\n")) {
2031
- if (!line) continue;
2032
- try {
2033
- out.push(JSON.parse(line));
2034
- } catch {
2035
- }
2036
- }
2037
- return out;
2038
- }
2039
2559
  function textOf9(parts) {
2040
2560
  if (!parts) return "";
2041
2561
  return parts.filter((p) => typeof p.text === "string").map((p) => p.text).join("");
@@ -2044,34 +2564,37 @@ var QwenAdapter = class {
2044
2564
  harness = "qwen";
2045
2565
  aliases = ["qwen-code"];
2046
2566
  root() {
2047
- return join11(homedir10(), ".qwen", "projects");
2567
+ return join10(homedir10(), ".qwen", "projects");
2048
2568
  }
2049
2569
  async locate(opts = {}) {
2050
2570
  const root = this.root();
2051
2571
  let projectDirs;
2052
2572
  try {
2053
- projectDirs = await readdir10(root);
2054
- } catch {
2055
- return [];
2573
+ projectDirs = await readdir11(root);
2574
+ } catch (error) {
2575
+ if (isMissingPathError(error)) return [];
2576
+ throw error;
2056
2577
  }
2057
2578
  const refs = [];
2058
2579
  for (const pd of projectDirs) {
2059
- const chatsDir = join11(root, pd, "chats");
2580
+ const chatsDir = join10(root, pd, "chats");
2060
2581
  let files;
2061
2582
  try {
2062
- files = await readdir10(chatsDir);
2063
- } catch {
2064
- continue;
2583
+ files = await readdir11(chatsDir);
2584
+ } catch (error) {
2585
+ if (isMissingPathError(error)) continue;
2586
+ throw error;
2065
2587
  }
2066
2588
  if (opts.cwd) continue;
2067
2589
  for (const f of files) {
2068
2590
  if (!f.endsWith(".jsonl")) continue;
2069
- const path = join11(chatsDir, f);
2591
+ const path = join10(chatsDir, f);
2070
2592
  let st;
2071
2593
  try {
2072
2594
  st = await stat10(path);
2073
- } catch {
2074
- continue;
2595
+ } catch (error) {
2596
+ if (isMissingPathError(error)) continue;
2597
+ throw error;
2075
2598
  }
2076
2599
  if (opts.sinceMs && st.mtimeMs < opts.sinceMs) continue;
2077
2600
  refs.push({ harness: this.harness, sessionId: basename7(f, ".jsonl"), path, cwd: null, mtimeMs: st.mtimeMs });
@@ -2079,36 +2602,32 @@ var QwenAdapter = class {
2079
2602
  }
2080
2603
  return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
2081
2604
  }
2082
- async parse(ref) {
2083
- const records = parseLines6(await readFile11(ref.path, "utf8"));
2084
- const traceId = records.find((r) => r.sessionId)?.sessionId ?? ref.sessionId;
2085
- const rootId = `root:${traceId}`;
2086
- const spans = [
2087
- span({
2088
- traceId,
2089
- spanId: rootId,
2090
- parentSpanId: null,
2091
- name: "session",
2092
- kind: "AGENT",
2093
- startTime: records[0]?.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2094
- endTime: records.at(-1)?.timestamp,
2095
- service: SERVICE9,
2096
- agent: SERVICE9
2097
- })
2098
- ];
2605
+ async parse(ref, options = {}) {
2606
+ const sourceTraceId = ref.sessionId;
2607
+ const sourceRootId = `root:${sourceTraceId}`;
2608
+ const spans = [];
2099
2609
  const openToolsByName = /* @__PURE__ */ new Map();
2610
+ let discoveredTraceId;
2611
+ let firstTimestamp;
2612
+ let lastTimestamp;
2613
+ let sawRecord = false;
2100
2614
  let step = 0;
2101
- let lastLlm = rootId;
2102
- for (const r of records) {
2615
+ for await (const r of readJsonl(ref.path, sessionJsonlOptions(ref, options))) {
2616
+ if (!sawRecord) {
2617
+ firstTimestamp = r.timestamp;
2618
+ sawRecord = true;
2619
+ }
2620
+ lastTimestamp = r.timestamp;
2621
+ if (!discoveredTraceId && r.sessionId) discoveredTraceId = r.sessionId;
2103
2622
  const ts = r.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
2104
2623
  const role = r.message?.role;
2105
2624
  if (r.type === "assistant" || role === "model") {
2106
2625
  const llmId = `llm:${r.uuid ?? step}`;
2107
2626
  spans.push(
2108
2627
  span({
2109
- traceId,
2628
+ traceId: sourceTraceId,
2110
2629
  spanId: llmId,
2111
- parentSpanId: rootId,
2630
+ parentSpanId: sourceRootId,
2112
2631
  name: "llm.turn",
2113
2632
  kind: "LLM",
2114
2633
  startTime: ts,
@@ -2121,13 +2640,12 @@ var QwenAdapter = class {
2121
2640
  content: capText(textOf9(r.message?.parts)) || null
2122
2641
  })
2123
2642
  );
2124
- lastLlm = llmId;
2125
2643
  step += 1;
2126
2644
  for (const p of r.message?.parts ?? []) {
2127
2645
  if (!p.functionCall?.name) continue;
2128
2646
  const name = p.functionCall.name;
2129
2647
  const t = span({
2130
- traceId,
2648
+ traceId: sourceTraceId,
2131
2649
  spanId: `tool:${name}:${step}`,
2132
2650
  parentSpanId: llmId,
2133
2651
  name: `tool.${name}`,
@@ -2137,7 +2655,7 @@ var QwenAdapter = class {
2137
2655
  agent: SERVICE9,
2138
2656
  tool: name,
2139
2657
  step,
2140
- content: p.functionCall.args != null ? JSON.stringify(p.functionCall.args) : null
2658
+ extra: toolIoAttributes({ input: p.functionCall.args })
2141
2659
  });
2142
2660
  spans.push(t);
2143
2661
  const q = openToolsByName.get(name) ?? [];
@@ -2150,9 +2668,9 @@ var QwenAdapter = class {
2150
2668
  if (prompt) {
2151
2669
  spans.push(
2152
2670
  userPromptSpan({
2153
- traceId,
2671
+ traceId: sourceTraceId,
2154
2672
  spanId: `user:${r.uuid ?? step}`,
2155
- parentSpanId: rootId,
2673
+ parentSpanId: sourceRootId,
2156
2674
  startTime: ts,
2157
2675
  service: SERVICE9,
2158
2676
  agent: SERVICE9,
@@ -2171,11 +2689,29 @@ var QwenAdapter = class {
2171
2689
  if (t) {
2172
2690
  t.end_time = ts;
2173
2691
  t.status = err ? { code: "ERROR", message: "tool result reported error" } : { code: "OK" };
2692
+ recordToolOutput(t, p.functionResponse?.response ?? r.toolCallResult?.error);
2174
2693
  }
2175
2694
  }
2176
2695
  }
2177
2696
  }
2178
- return spans;
2697
+ const traceId = discoveredTraceId ?? sourceTraceId;
2698
+ const rootId = `root:${traceId}`;
2699
+ for (const item of spans) {
2700
+ item.trace_id = traceId;
2701
+ if (item.parent_span_id === sourceRootId) item.parent_span_id = rootId;
2702
+ }
2703
+ const root = span({
2704
+ traceId,
2705
+ spanId: rootId,
2706
+ parentSpanId: null,
2707
+ name: "session",
2708
+ kind: "AGENT",
2709
+ startTime: firstTimestamp ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2710
+ endTime: lastTimestamp,
2711
+ service: SERVICE9,
2712
+ agent: SERVICE9
2713
+ });
2714
+ return [root, ...spans];
2179
2715
  }
2180
2716
  };
2181
2717
 
@@ -2218,7 +2754,7 @@ function selectAdapters(opts) {
2218
2754
 
2219
2755
  // src/repo.ts
2220
2756
  import { stat as stat11 } from "fs/promises";
2221
- import { dirname, isAbsolute, join as join12, normalize, parse as parsePath } from "path";
2757
+ import { dirname, isAbsolute, join as join11, normalize, parse as parsePath } from "path";
2222
2758
  function normalizeRemote(url) {
2223
2759
  const raw = url.trim();
2224
2760
  if (!raw) return null;
@@ -2339,7 +2875,7 @@ async function nearestExistingDir(path) {
2339
2875
  async function findGitRoot(path) {
2340
2876
  let cur = await nearestExistingDir(path);
2341
2877
  while (cur) {
2342
- if (await pathStat(join12(cur, ".git"))) return cur;
2878
+ if (await pathStat(join11(cur, ".git"))) return cur;
2343
2879
  const parent = dirname(cur);
2344
2880
  if (parent === cur) return null;
2345
2881
  cur = parent;
@@ -2357,7 +2893,7 @@ async function repairDashedPath(cwd) {
2357
2893
  while (index < parts.length) {
2358
2894
  const nextPart = parts[index];
2359
2895
  if (!nextPart) break;
2360
- const exact = join12(current, nextPart);
2896
+ const exact = join11(current, nextPart);
2361
2897
  if (!await directoryExists(exact)) break;
2362
2898
  current = exact;
2363
2899
  index += 1;
@@ -2367,7 +2903,7 @@ async function repairDashedPath(cwd) {
2367
2903
  let consumed = 0;
2368
2904
  for (let n = parts.length - index; n >= 1; n -= 1) {
2369
2905
  const segment = parts.slice(index, index + n).join("-");
2370
- const candidate = join12(current, segment);
2906
+ const candidate = join11(current, segment);
2371
2907
  if (await directoryExists(candidate)) {
2372
2908
  match = candidate;
2373
2909
  consumed = n;
@@ -2575,14 +3111,25 @@ async function stampSpanWorkdirRepoAttrs(spans) {
2575
3111
  }
2576
3112
 
2577
3113
  // src/session-source.ts
2578
- async function parseSession(adapter, ref) {
2579
- const spans = await adapter.parse(ref);
3114
+ async function parseSession(adapter, ref, options = {}) {
3115
+ const spans = await adapter.parse(ref, options);
3116
+ if (spans.length === 0) throw new EmptySessionError(ref.path);
3117
+ stampSessionIntegrity(ref, spans);
3118
+ stampSessionIdentity(spans, ref.sessionId);
2580
3119
  const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
2581
3120
  if (repo.cwd) ref.cwd = repo.cwd;
2582
3121
  stampRepoAttrs(spans, repo.attrs);
2583
3122
  await stampSpanWorkdirRepoAttrs(spans);
2584
3123
  return spans;
2585
3124
  }
3125
+ var EmptySessionError = class extends Error {
3126
+ sourcePath;
3127
+ constructor(sourcePath) {
3128
+ super(`Session parser produced no spans at ${sourcePath}`);
3129
+ this.name = "EmptySessionError";
3130
+ this.sourcePath = sourcePath;
3131
+ }
3132
+ };
2586
3133
  function refKey(ref) {
2587
3134
  return `${ref.harness}\0${ref.path}\0${ref.sessionId}`;
2588
3135
  }
@@ -2592,7 +3139,7 @@ async function locateSessions(adapter, opts = {}) {
2592
3139
  const refs = await adapter.locate({ sinceMs: opts.sinceMs });
2593
3140
  const byKey = /* @__PURE__ */ new Map();
2594
3141
  for (const ref of refs) {
2595
- if (!cwdMatchesSelection(ref.cwd, cwdSelections)) continue;
3142
+ if (!cwdMatchesSelection(ref.cwd, cwdSelections) && !(ref.cwd === null && ref.integrity)) continue;
2596
3143
  byKey.set(refKey(ref), ref);
2597
3144
  }
2598
3145
  return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
@@ -2604,7 +3151,8 @@ async function* scanSessions(opts) {
2604
3151
  try {
2605
3152
  refs = await locateSessions(adapter, { cwd: opts.cwd, sinceMs: opts.sinceMs });
2606
3153
  } catch (err) {
2607
- opts.onError?.(err);
3154
+ if (!opts.onError) throw err;
3155
+ opts.onError(err);
2608
3156
  continue;
2609
3157
  }
2610
3158
  if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
@@ -2612,12 +3160,12 @@ async function* scanSessions(opts) {
2612
3160
  if (opts.signal?.aborted) return;
2613
3161
  let spans;
2614
3162
  try {
2615
- spans = await parseSession(adapter, ref);
3163
+ spans = await parseSession(adapter, ref, { corruptionMode: opts.corruptionMode });
2616
3164
  } catch (err) {
2617
- opts.onError?.(err, ref);
3165
+ if (!opts.onError) throw err;
3166
+ opts.onError(err, ref);
2618
3167
  continue;
2619
3168
  }
2620
- if (spans.length === 0) continue;
2621
3169
  yield { adapter, ref, spans };
2622
3170
  }
2623
3171
  }
@@ -2626,17 +3174,17 @@ async function* scanSessions(opts) {
2626
3174
  // src/evidence.ts
2627
3175
  import { mkdtemp, writeFile } from "fs/promises";
2628
3176
  import { tmpdir } from "os";
2629
- import { join as join13 } from "path";
3177
+ import { join as join12 } from "path";
3178
+ import {
3179
+ OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
3180
+ TOOL_NAME as TOOL_NAME2
3181
+ } from "@tangle-network/agent-eval/trace-attributes";
2630
3182
  function stringAttr(span2, key) {
2631
3183
  const value = span2.attributes[key];
2632
3184
  return typeof value === "string" && value.length > 0 ? value : void 0;
2633
3185
  }
2634
- function numberAttr(span2, key) {
2635
- const value = span2.attributes[key];
2636
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
2637
- }
2638
3186
  function spanKind(span2) {
2639
- return stringAttr(span2, "openinference.span.kind");
3187
+ return stringAttr(span2, OPENINFERENCE_SPAN_KIND2);
2640
3188
  }
2641
3189
  function repoFromSpans(spans) {
2642
3190
  const attrs = {};
@@ -2662,7 +3210,7 @@ function summarizeTools(spans) {
2662
3210
  const byTool = /* @__PURE__ */ new Map();
2663
3211
  for (const span2 of spans) {
2664
3212
  if (spanKind(span2) !== "TOOL") continue;
2665
- const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
3213
+ const name = stringAttr(span2, TOOL_NAME2) ?? span2.name.replace(/^tool\./, "");
2666
3214
  const current = byTool.get(name) ?? { calls: 0, errors: 0 };
2667
3215
  current.calls += 1;
2668
3216
  if (span2.status.code === "ERROR") current.errors += 1;
@@ -2671,7 +3219,6 @@ function summarizeTools(spans) {
2671
3219
  return [...byTool.entries()].map(([name, row]) => ({ name, calls: row.calls, errors: row.errors })).sort((a, b) => b.calls - a.calls || a.name.localeCompare(b.name));
2672
3220
  }
2673
3221
  async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2674
- const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
2675
3222
  const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
2676
3223
  const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
2677
3224
  const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
@@ -2679,10 +3226,14 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2679
3226
  const loopFindings = pipelines.stuckLoops.findings;
2680
3227
  const { firstSpanAt, lastSpanAt } = timeBounds(spans);
2681
3228
  const repo = repoFromSpans(spans);
3229
+ const execution = summarizeSpanExecution(spans, {
3230
+ experimentId: `traces-policy-evidence:${ref.sessionId}`
3231
+ });
2682
3232
  return {
2683
3233
  schemaVersion: 1,
2684
3234
  kind: "traces.policy_evidence.session",
2685
3235
  generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
3236
+ execution,
2686
3237
  session: {
2687
3238
  harness: ref.harness,
2688
3239
  sessionId: ref.sessionId,
@@ -2693,12 +3244,12 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2693
3244
  repo,
2694
3245
  metrics: {
2695
3246
  spanCount: spans.length,
2696
- llmTurnCount: llmSpans.length,
3247
+ llmTurnCount: execution.execution.modelCalls.events,
2697
3248
  toolCallCount: toolSpans.length,
2698
3249
  erroredToolCallCount,
2699
- inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
2700
- outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
2701
- models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
3250
+ inputTokens: execution.execution.tokenUsage.totals.input,
3251
+ outputTokens: execution.execution.tokenUsage.totals.output,
3252
+ models: execution.execution.models.map(({ model }) => model),
2702
3253
  tools: summarizeTools(spans),
2703
3254
  firstSpanAt,
2704
3255
  lastSpanAt
@@ -2735,7 +3286,7 @@ function serializePolicyEvidence(records) {
2735
3286
  `;
2736
3287
  }
2737
3288
  async function writePolicyEvidenceFile(records, outPath) {
2738
- const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
3289
+ const path = outPath ?? join12(await mkdtemp(join12(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
2739
3290
  await writeFile(path, serializePolicyEvidence(records), "utf8");
2740
3291
  return path;
2741
3292
  }
@@ -2880,6 +3431,7 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2880
3431
  const report = { redactionCount: 0, byRule: {} };
2881
3432
  const out = spans.map((s) => {
2882
3433
  const attributes = redactValue(s.attributes, rules, report).value;
3434
+ normalizeToolIoAttributes(attributes);
2883
3435
  let status = s.status;
2884
3436
  if (status.message) {
2885
3437
  const m = redactValue(status.message, rules, report).value;
@@ -2890,33 +3442,66 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
2890
3442
  return { spans: out, report };
2891
3443
  }
2892
3444
  async function applyRedactor(spans, redactor) {
2893
- const idx = [];
3445
+ const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
3446
+ const fields = [];
2894
3447
  const texts = [];
2895
3448
  spans.forEach((s, i) => {
2896
- const c = s.attributes["content"];
2897
- if (typeof c === "string" && c.length > 0) {
2898
- idx.push(i);
2899
- texts.push(c);
3449
+ for (const key of capturedKeys) {
3450
+ const value = s.attributes[key];
3451
+ if (typeof value === "string" && value.length > 0) {
3452
+ fields.push({ spanIndex: i, key });
3453
+ texts.push(value);
3454
+ }
3455
+ }
3456
+ if (s.status.message) {
3457
+ fields.push({ spanIndex: i, key: "status.message" });
3458
+ texts.push(s.status.message);
2900
3459
  }
2901
3460
  });
2902
3461
  const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
2903
- if (texts.length === 0) return { spans: out, changed: 0 };
3462
+ if (texts.length === 0) {
3463
+ for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
3464
+ return { spans: out, changed: 0 };
3465
+ }
2904
3466
  const scrubbed = await redactor.redactText(texts);
3467
+ if (scrubbed.length !== texts.length) {
3468
+ throw new Error(`redactor ${redactor.name}: expected ${texts.length} values, received ${scrubbed.length}`);
3469
+ }
2905
3470
  let changed = 0;
2906
- idx.forEach((spanI, k) => {
3471
+ fields.forEach(({ spanIndex, key }, k) => {
2907
3472
  if (scrubbed[k] !== texts[k]) {
2908
- out[spanI].attributes["content"] = scrubbed[k];
3473
+ if (key === "status.message") {
3474
+ out[spanIndex].status = { ...out[spanIndex].status, message: scrubbed[k] };
3475
+ } else {
3476
+ out[spanIndex].attributes[key] = scrubbed[k];
3477
+ }
2909
3478
  changed += 1;
2910
3479
  }
2911
3480
  });
3481
+ for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
2912
3482
  return { spans: out, changed };
2913
3483
  }
2914
3484
 
2915
3485
  // src/file-export.ts
2916
- import { createHash } from "crypto";
2917
- import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
3486
+ import { createHash as createHash4 } from "crypto";
3487
+ import { mkdtemp as mkdtemp2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
2918
3488
  import { tmpdir as tmpdir2 } from "os";
2919
- import { join as join14 } from "path";
3489
+ import { join as join13 } from "path";
3490
+ import {
3491
+ firstNumberAttr,
3492
+ LLM_CACHED_TOKEN_ATTR_KEYS,
3493
+ LLM_CACHED_TOKENS,
3494
+ LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
3495
+ LLM_CACHE_WRITE_TOKENS,
3496
+ LLM_COST_ATTR_KEYS,
3497
+ LLM_COST_USD,
3498
+ LLM_INPUT_TOKEN_ATTR_KEYS,
3499
+ LLM_INPUT_TOKENS,
3500
+ LLM_OUTPUT_TOKEN_ATTR_KEYS,
3501
+ LLM_OUTPUT_TOKENS,
3502
+ LLM_REASONING_TOKEN_ATTR_KEYS,
3503
+ LLM_REASONING_TOKENS
3504
+ } from "@tangle-network/agent-eval/trace-attributes";
2920
3505
  function isObject(value) {
2921
3506
  return value != null && typeof value === "object" && !Array.isArray(value);
2922
3507
  }
@@ -2936,7 +3521,7 @@ function stableJson(value) {
2936
3521
  });
2937
3522
  }
2938
3523
  function hashId(value, chars = 16) {
2939
- return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
3524
+ return createHash4("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
2940
3525
  }
2941
3526
  function isoTime(value) {
2942
3527
  if (typeof value === "string") {
@@ -3039,6 +3624,11 @@ function requireOpenInferenceRows(rows) {
3039
3624
  return rows;
3040
3625
  }
3041
3626
  function requireIntelligenceSpanRows(rows) {
3627
+ if (rows.length > 0 && rows.every(isOpenInferenceRow)) {
3628
+ throw new Error(
3629
+ "intelligence-spans input is already complete OpenInference; use --format auto or --format openinference to preserve it losslessly"
3630
+ );
3631
+ }
3042
3632
  if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
3043
3633
  return rows;
3044
3634
  }
@@ -3070,6 +3660,7 @@ function policyEvidenceToSpans(records) {
3070
3660
  "traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
3071
3661
  "traces.metrics.input_tokens": record.metrics.inputTokens,
3072
3662
  "traces.metrics.output_tokens": record.metrics.outputTokens,
3663
+ "traces.execution": stableJson(record.execution),
3073
3664
  "traces.metrics.models": record.metrics.models,
3074
3665
  "traces.metrics.tools": stableJson(record.metrics.tools),
3075
3666
  "traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
@@ -3097,6 +3688,11 @@ function policyEvidenceToSpans(records) {
3097
3688
  endTime: end,
3098
3689
  service: record.session.harness,
3099
3690
  agent: "traces",
3691
+ inputTokens: record.execution.execution.tokenUsage.totals.input,
3692
+ outputTokens: record.execution.execution.tokenUsage.totals.output,
3693
+ reasoningTokens: record.execution.execution.tokenUsage.totals.reasoning,
3694
+ cachedInputTokens: record.execution.execution.tokenUsage.totals.cached,
3695
+ cacheWriteInputTokens: record.execution.execution.tokenUsage.totals.cacheWrite,
3100
3696
  content: capText(stableJson({
3101
3697
  session: record.session,
3102
3698
  repo: record.repo,
@@ -3127,6 +3723,16 @@ function findStringKey(value, keys, depth = 4) {
3127
3723
  }
3128
3724
  return void 0;
3129
3725
  }
3726
+ function eventValue(row, keys) {
3727
+ const scopes = [row, objectValue(row.data), objectValue(row.payload)];
3728
+ for (const scope of scopes) {
3729
+ if (!scope) continue;
3730
+ for (const key of keys) {
3731
+ if (scope[key] !== void 0) return scope[key];
3732
+ }
3733
+ }
3734
+ return void 0;
3735
+ }
3130
3736
  function eventTimestamp(row) {
3131
3737
  return firstTimeDeep(row, [
3132
3738
  "timestamp",
@@ -3169,7 +3775,8 @@ function eventKind(row, type) {
3169
3775
  }
3170
3776
  function sandboxEventsToSpans(rows, wrapper) {
3171
3777
  const context = wrapper ?? {};
3172
- const traceId = findStringKey(context, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 3) ?? findStringKey(rows, ["trace_id", "traceId", "session_id", "sessionId", "run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
3778
+ const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
3779
+ const traceId = findStringKey(context, ["trace_id", "traceId"], 3) ?? findStringKey(rows, ["trace_id", "traceId"], 4) ?? sessionId ?? findStringKey(context, ["run_id", "runId"], 3) ?? findStringKey(rows, ["run_id", "runId"], 4) ?? `sandbox:${hashId(rows, 32)}`;
3173
3780
  const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
3174
3781
  const times = rows.flatMap((row) => {
3175
3782
  const time = eventTimestamp(row);
@@ -3205,10 +3812,16 @@ function sandboxEventsToSpans(rows, wrapper) {
3205
3812
  const { status, message } = eventStatus(row, type);
3206
3813
  const { kind, tool } = eventKind(row, type);
3207
3814
  const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
3815
+ const toolInput = kind === "TOOL" ? eventValue(row, ["toolInput", "tool_input", "input", "args", "arguments", "full_command"]) : void 0;
3208
3816
  const extra = {
3209
3817
  "traces.source_format": "sandbox-events",
3210
3818
  "traces.event.type": type,
3211
- "traces.event.index": index
3819
+ "traces.event.index": index,
3820
+ ...kind === "TOOL" ? toolIoAttributes({
3821
+ input: toolInput,
3822
+ output: eventValue(row, ["toolOutput", "tool_output", "output", "result"]),
3823
+ argsCaptured: toolInput !== void 0
3824
+ }) : {}
3212
3825
  };
3213
3826
  copyDefined(extra, {
3214
3827
  "traces.event.id": stringValue(row.id),
@@ -3233,7 +3846,11 @@ function sandboxEventsToSpans(rows, wrapper) {
3233
3846
  extra
3234
3847
  }));
3235
3848
  });
3236
- return spans;
3849
+ if (!sessionId) return spans;
3850
+ return spans.map((item) => ({
3851
+ ...item,
3852
+ attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
3853
+ }));
3237
3854
  }
3238
3855
  function statusCode(value) {
3239
3856
  return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
@@ -3257,8 +3874,8 @@ function unixNanoTime(value) {
3257
3874
  }
3258
3875
  function firstNumber(row, attrs, keys) {
3259
3876
  for (const key of keys) {
3260
- const value = numberValue(row[key]) ?? numberValue(attrs[key]);
3261
- if (value != null) return value;
3877
+ const value = firstNumberAttr(row, [key]) ?? firstNumberAttr(attrs, [key]);
3878
+ if (value !== null) return value;
3262
3879
  }
3263
3880
  return void 0;
3264
3881
  }
@@ -3270,6 +3887,12 @@ var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
3270
3887
  "tool.name",
3271
3888
  "llm.input_tokens",
3272
3889
  "llm.output_tokens",
3890
+ LLM_INPUT_TOKENS,
3891
+ LLM_OUTPUT_TOKENS,
3892
+ LLM_REASONING_TOKENS,
3893
+ LLM_CACHED_TOKENS,
3894
+ LLM_CACHE_WRITE_TOKENS,
3895
+ LLM_COST_USD,
3273
3896
  "step",
3274
3897
  "content"
3275
3898
  ]);
@@ -3284,10 +3907,27 @@ function preserveRawAttributes(attrs) {
3284
3907
  }
3285
3908
  return preserved;
3286
3909
  }
3910
+ function capturedToolIo(attrs) {
3911
+ const input = attrs["input.value"] ?? attrs["tool.input"] ?? attrs.tool_input ?? attrs["tool.arguments"] ?? attrs.tool_arguments ?? attrs.arguments ?? attrs.args ?? attrs.full_command;
3912
+ const capture = toolArgumentsFromAttributes({ ...attrs, "input.value": input });
3913
+ return toolIoAttributes({
3914
+ input: capture.args,
3915
+ output: attrs["output.value"] ?? attrs["tool.output"] ?? attrs.tool_output,
3916
+ argsCaptured: capture.argsCaptured
3917
+ });
3918
+ }
3919
+ function isToolTelemetrySpan(attrs) {
3920
+ const spanType = stringValue(attrs["span.type"])?.toLowerCase();
3921
+ return spanType === "tool.execution" || spanType === "tool.blocked_on_user";
3922
+ }
3923
+ function otlpSpanKind(value) {
3924
+ return value === "AGENT" || value === "LLM" || value === "TOOL" || value === "CHAIN" || value === "SPAN" ? value : void 0;
3925
+ }
3287
3926
  function intelligenceSpanKind(name, attrs) {
3288
3927
  const lowered = name.toLowerCase();
3289
3928
  const spanType = stringValue(attrs["span.type"])?.toLowerCase();
3290
3929
  if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
3930
+ if (isToolTelemetrySpan(attrs)) return "CHAIN";
3291
3931
  if (spanType === "tool" || lowered.includes("tool") || attrs["tool.name"] != null || attrs.tool_name != null || attrs["mcp.tool.name"] != null || attrs["tool.input"] != null || attrs["tool.output"] != null) {
3292
3932
  return "TOOL";
3293
3933
  }
@@ -3302,9 +3942,12 @@ function intelligenceSpansToSpans(rows) {
3302
3942
  const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
3303
3943
  const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
3304
3944
  const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
3305
- const sessionId = stringValue(row.session_id) ?? stringValue(attrs["session.id"]);
3945
+ const kind = intelligenceSpanKind(name, attrs);
3946
+ const sourceStatus = objectValue(row.status);
3947
+ const sessionId = stringValue(row.session_id) ?? sessionIdFromAttributes(attrs);
3306
3948
  const extra = {
3307
3949
  ...preserveRawAttributes(attrs),
3950
+ ...kind === "TOOL" ? capturedToolIo(attrs) : {},
3308
3951
  "traces.source_format": "intelligence-spans",
3309
3952
  "traces.row.index": index
3310
3953
  };
@@ -3318,7 +3961,6 @@ function intelligenceSpansToSpans(rows) {
3318
3961
  "generation.id": stringValue(row.generation),
3319
3962
  "cell.id": stringValue(row.cell_id),
3320
3963
  "project.key": stringValue(row.project_key),
3321
- "llm.cost_usd": numberValue(row.cost_usd),
3322
3964
  "redaction.version": stringValue(row.redaction_version),
3323
3965
  "traces.received_at": isoTime(row.received_at)
3324
3966
  });
@@ -3327,17 +3969,21 @@ function intelligenceSpansToSpans(rows) {
3327
3969
  spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
3328
3970
  parentSpanId: stringValue(row.parent_span_id) ?? null,
3329
3971
  name,
3330
- kind: intelligenceSpanKind(name, attrs),
3972
+ kind,
3331
3973
  startTime,
3332
3974
  endTime,
3333
- status: statusCode(row.status_code),
3334
- statusMessage: stringValue(row.status_message),
3975
+ status: statusCode(row.status_code ?? sourceStatus?.code),
3976
+ statusMessage: stringValue(row.status_message) ?? stringValue(sourceStatus?.message),
3335
3977
  service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
3336
3978
  agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
3337
3979
  model,
3338
3980
  tool,
3339
- inputTokens: firstNumber(row, attrs, ["input_tokens", "llm.input_tokens"]),
3340
- outputTokens: firstNumber(row, attrs, ["output_tokens", "llm.output_tokens"]),
3981
+ inputTokens: firstNumber(row, attrs, ["input_tokens", ...LLM_INPUT_TOKEN_ATTR_KEYS]),
3982
+ outputTokens: firstNumber(row, attrs, ["output_tokens", ...LLM_OUTPUT_TOKEN_ATTR_KEYS]),
3983
+ reasoningTokens: firstNumber(row, attrs, LLM_REASONING_TOKEN_ATTR_KEYS),
3984
+ cachedInputTokens: firstNumber(row, attrs, LLM_CACHED_TOKEN_ATTR_KEYS),
3985
+ cacheWriteInputTokens: firstNumber(row, attrs, LLM_CACHE_WRITE_TOKEN_ATTR_KEYS),
3986
+ costUsd: firstNumber(row, attrs, ["cost_usd", ...LLM_COST_ATTR_KEYS]),
3341
3987
  step: numberValue(attrs["interaction.sequence"]) ?? index,
3342
3988
  content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
3343
3989
  extra
@@ -3350,15 +3996,21 @@ function openInferenceToSpans(rows) {
3350
3996
  const resource = objectValue(row.resource);
3351
3997
  const resourceAttrs = objectValue(resource?.attributes);
3352
3998
  if (resourceAttrs) copyDefined(attrs, resourceAttrs);
3353
- const kind = stringValue(row.kind);
3354
- if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
3999
+ const name = stringValue(row.name) ?? "span";
4000
+ const declaredKind = otlpSpanKind(attrs["openinference.span.kind"]) ?? otlpSpanKind(row.kind);
4001
+ const kind = isToolTelemetrySpan(attrs) ? "CHAIN" : declaredKind ?? intelligenceSpanKind(name, attrs);
4002
+ if (declaredKind && declaredKind !== kind) {
4003
+ attrs["traces.raw_attribute.openinference.span.kind"] = declaredKind;
4004
+ }
4005
+ attrs["openinference.span.kind"] = kind;
4006
+ if (kind === "TOOL") Object.assign(attrs, capturedToolIo(attrs));
3355
4007
  const status = objectValue(row.status);
3356
4008
  const message = stringValue(status?.message);
3357
4009
  return {
3358
4010
  trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
3359
4011
  span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
3360
4012
  parent_span_id: stringValue(row.parent_span_id) || null,
3361
- name: stringValue(row.name) ?? "span",
4013
+ name,
3362
4014
  start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3363
4015
  end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
3364
4016
  status: { code: statusCode(status?.code), ...message ? { message } : {} },
@@ -3405,20 +4057,53 @@ function exportTraceEvidenceText(text, opts = {}) {
3405
4057
  return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
3406
4058
  }
3407
4059
  async function exportTraceEvidenceFile(inputPath, opts = {}) {
3408
- const text = await readFile12(inputPath, "utf8");
4060
+ if (/\.(?:jsonl|ndjson)$/i.test(inputPath)) {
4061
+ let format;
4062
+ const spans = [];
4063
+ const sandboxRows = [];
4064
+ let redactionCount = 0;
4065
+ const redactionsByRule = {};
4066
+ for await (const row of readJsonl(inputPath)) {
4067
+ format ??= detectFormat([row], opts.format ?? "auto");
4068
+ if (format === "sandbox-events") {
4069
+ sandboxRows.push(row);
4070
+ continue;
4071
+ }
4072
+ const converted = exportTraceEvidenceRows([row], {
4073
+ ...opts,
4074
+ sourcePath: inputPath,
4075
+ format
4076
+ });
4077
+ appendAll(spans, converted.spans);
4078
+ redactionCount += converted.redactionCount;
4079
+ for (const [rule, count2] of Object.entries(converted.redactionsByRule)) {
4080
+ redactionsByRule[rule] = (redactionsByRule[rule] ?? 0) + count2;
4081
+ }
4082
+ }
4083
+ if (!format) throw new Error("input file is empty");
4084
+ if (format === "sandbox-events") {
4085
+ return exportTraceEvidenceRows(sandboxRows, {
4086
+ ...opts,
4087
+ sourcePath: inputPath,
4088
+ format
4089
+ });
4090
+ }
4091
+ return { format, spans, redactionCount, redactionsByRule };
4092
+ }
4093
+ const text = await readFile2(inputPath, "utf8");
3409
4094
  return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
3410
4095
  }
3411
4096
  async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
3412
4097
  const result = await exportTraceEvidenceFile(inputPath, opts);
3413
- const path = outPath ?? join14(await mkdtemp2(join14(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
4098
+ const path = outPath ?? join13(await mkdtemp2(join13(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
3414
4099
  await writeFile2(path, serializeSpans(result.spans), "utf8");
3415
4100
  return { ...result, path };
3416
4101
  }
3417
4102
 
3418
4103
  // src/inspect.ts
3419
- import { mkdtemp as mkdtemp3, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
4104
+ import { mkdtemp as mkdtemp3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
3420
4105
  import { tmpdir as tmpdir3 } from "os";
3421
- import { basename as basename8, join as join15 } from "path";
4106
+ import { basename as basename8, join as join14 } from "path";
3422
4107
  var severityRank = {
3423
4108
  high: 0,
3424
4109
  medium: 1,
@@ -3681,35 +4366,308 @@ function renderInspectionReport(report) {
3681
4366
  lines.push("Refs:");
3682
4367
  for (const ref of finding2.refs.slice(0, 5)) lines.push(`- ${ref}`);
3683
4368
  }
3684
- });
3685
- return `${lines.join("\n")}
3686
- `;
3687
- }
3688
- function serializeInspectionReport(report) {
3689
- return `${JSON.stringify(report, null, 2)}
3690
- `;
3691
- }
3692
- function assertSessionIndex(value) {
3693
- if (!value || typeof value !== "object" || Array.isArray(value)) {
3694
- throw new Error("session index file must contain a JSON object");
4369
+ });
4370
+ return `${lines.join("\n")}
4371
+ `;
4372
+ }
4373
+ function serializeInspectionReport(report) {
4374
+ return `${JSON.stringify(report, null, 2)}
4375
+ `;
4376
+ }
4377
+ function assertSessionIndex(value) {
4378
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4379
+ throw new Error("session index file must contain a JSON object");
4380
+ }
4381
+ const candidate = value;
4382
+ if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
4383
+ throw new Error("input is not a traces.session_index file with schemaVersion 1");
4384
+ }
4385
+ if (!candidate.totals || typeof candidate.totals !== "object") {
4386
+ throw new Error("session index is missing totals");
4387
+ }
4388
+ }
4389
+ async function readSessionIndexFile(path) {
4390
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
4391
+ assertSessionIndex(parsed);
4392
+ return parsed;
4393
+ }
4394
+ async function writeInspectionReportFile(report, outPath) {
4395
+ const path = outPath ?? join14(await mkdtemp3(join14(tmpdir3(), "traces-inspect-")), "inspection-report.md");
4396
+ await writeFile3(path, renderInspectionReport(report), "utf8");
4397
+ return path;
4398
+ }
4399
+
4400
+ // src/adoption.ts
4401
+ import { readFile as readFile4 } from "fs/promises";
4402
+ import { join as join15 } from "path";
4403
+ function toolName(s) {
4404
+ const n = s.attributes["tool.name"];
4405
+ if (typeof n === "string") return n;
4406
+ if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
4407
+ return null;
4408
+ }
4409
+ function parseInput(s) {
4410
+ const captured = toolArgumentsFromAttributes(s.attributes);
4411
+ if (!captured.argsCaptured) return {};
4412
+ if (captured.args && typeof captured.args === "object") {
4413
+ return captured.args;
4414
+ }
4415
+ if (typeof captured.args !== "string") return {};
4416
+ try {
4417
+ const v = JSON.parse(captured.args);
4418
+ return v && typeof v === "object" ? v : {};
4419
+ } catch {
4420
+ return {};
4421
+ }
4422
+ }
4423
+ function skillNameOf(input) {
4424
+ const v = input.skill ?? input.command;
4425
+ return typeof v === "string" && v.length > 0 ? v : "?";
4426
+ }
4427
+ function subagentTypeOf(input) {
4428
+ const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
4429
+ return typeof v === "string" && v.length > 0 ? v : "?";
4430
+ }
4431
+ function isSpawnAgentTool(name) {
4432
+ return name === "spawn_agent" || name.endsWith("__spawn_agent");
4433
+ }
4434
+ function skillTelemetryCapability(service) {
4435
+ if (service === "claude-code") return "supported";
4436
+ if (service === "codex") return "unsupported";
4437
+ return "unknown";
4438
+ }
4439
+ var MATERIALIZED_SKILL_RE = /<skills_instructions>|(?:^|\n)#{1,3}\s+(?:available\s+)?skills\b|base directory for this skill:/i;
4440
+ function hasMaterializedSkillEvidence(s) {
4441
+ if (s.name !== "message.developer" && s.attributes["tangle.actor"] !== "injected") return false;
4442
+ const content = s.attributes.content;
4443
+ return typeof content === "string" && MATERIALIZED_SKILL_RE.test(content);
4444
+ }
4445
+ var SKILL_DOCUMENT_READ_COMMAND = /\b(?:cat|sed|head|tail|less|more|awk)\b/;
4446
+ function skillDocumentNames(s) {
4447
+ if (s.attributes["openinference.span.kind"] !== "TOOL") return [];
4448
+ if (s.status.code !== "OK" || !SKILL_DOCUMENT_READ_COMMAND.test(String(s.attributes["input.value"] ?? ""))) return [];
4449
+ const input = s.attributes["input.value"];
4450
+ if (typeof input !== "string") return [];
4451
+ const names = /* @__PURE__ */ new Set();
4452
+ for (const match of input.matchAll(/(?:^|[\\/'"])([^\\/'"\s]+)[\\/]SKILL\.md\b/g)) {
4453
+ const name = match[1];
4454
+ if (/^[A-Za-z][A-Za-z0-9-]*$/.test(name)) names.add(name);
4455
+ }
4456
+ return [...names];
4457
+ }
4458
+ function telemetryStatus(measured, unsupported, unknown) {
4459
+ if (measured > 0 && unsupported === 0 && unknown === 0) return "measured";
4460
+ if (measured > 0) return "partial";
4461
+ if (unsupported > 0 && unknown === 0) return "unsupported";
4462
+ return "unknown";
4463
+ }
4464
+ function skillNamesFromRun(record) {
4465
+ const names = [];
4466
+ if (Array.isArray(record.skills)) {
4467
+ for (const skill of record.skills) if (typeof skill === "string" && skill) names.push(skill);
4468
+ }
4469
+ if (typeof record.skill === "string" && record.skill) names.push(record.skill);
4470
+ return names.length > 0 ? names : ["?"];
4471
+ }
4472
+ function sessionIdFromRun(record) {
4473
+ for (const key of ["sessionId", "session_id", "traceId", "trace_id"]) {
4474
+ if (typeof record[key] === "string" && record[key]) return record[key];
4475
+ }
4476
+ return void 0;
4477
+ }
4478
+ function addCounts(target, names) {
4479
+ for (const name of names) target[name] = (target[name] ?? 0) + 1;
4480
+ }
4481
+ function countSkillRunsJsonl(raw) {
4482
+ const out = {};
4483
+ for (const line of raw.split("\n")) {
4484
+ const t = line.trim();
4485
+ if (!t) continue;
4486
+ let o;
4487
+ try {
4488
+ o = JSON.parse(t);
4489
+ } catch {
4490
+ continue;
4491
+ }
4492
+ if (!o || typeof o !== "object") continue;
4493
+ const rec = o;
4494
+ addCounts(out, skillNamesFromRun(rec));
4495
+ }
4496
+ return out;
4497
+ }
4498
+ async function readLoopRuns(cwds, sessionIds2) {
4499
+ const linkedCounts = {};
4500
+ const unlinkedCounts = {};
4501
+ let filesRead = 0;
4502
+ const seen = /* @__PURE__ */ new Set();
4503
+ for (const cwd of cwds) {
4504
+ if (!cwd || seen.has(cwd)) continue;
4505
+ seen.add(cwd);
4506
+ const path = join15(cwd, ".evolve", "skill-runs.jsonl");
4507
+ let raw;
4508
+ try {
4509
+ raw = await readFile4(path, "utf8");
4510
+ } catch {
4511
+ continue;
4512
+ }
4513
+ filesRead += 1;
4514
+ for (const line of raw.split("\n")) {
4515
+ if (!line.trim()) continue;
4516
+ let value;
4517
+ try {
4518
+ value = JSON.parse(line);
4519
+ } catch {
4520
+ continue;
4521
+ }
4522
+ if (!value || typeof value !== "object") continue;
4523
+ const record = value;
4524
+ const target = sessionIds2.has(sessionIdFromRun(record) ?? "") ? linkedCounts : unlinkedCounts;
4525
+ addCounts(target, skillNamesFromRun(record));
4526
+ }
4527
+ }
4528
+ return { linkedCounts, unlinkedCounts, filesRead };
4529
+ }
4530
+ async function analyzeAdoption(spans, opts = {}) {
4531
+ const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
4532
+ const identifiedSessions = new Set(sessionByTrace.values());
4533
+ const unassignedTraceIds = new Set(
4534
+ spans.map((span2) => span2.trace_id).filter((traceId) => !sessionByTrace.has(traceId))
4535
+ );
4536
+ const executionGroup = (span2) => sessionByTrace.get(span2.trace_id) ?? `trace:${span2.trace_id}`;
4537
+ const sessions = /* @__PURE__ */ new Set();
4538
+ const sessionsWithSkill = /* @__PURE__ */ new Set();
4539
+ const sessionsWithSubagent = /* @__PURE__ */ new Set();
4540
+ const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
4541
+ const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
4542
+ const skillInvocations = {};
4543
+ const skillDocumentReads = {};
4544
+ const subagentSpawns = {};
4545
+ const canonicalSubagentSessions = /* @__PURE__ */ new Set();
4546
+ const fallbackSubagents = /* @__PURE__ */ new Map();
4547
+ const sessionCapabilities = /* @__PURE__ */ new Map();
4548
+ for (const s of spans) {
4549
+ const group = executionGroup(s);
4550
+ sessions.add(group);
4551
+ const observedCapability = skillTelemetryCapability(s.attributes["service.name"]);
4552
+ const currentCapability = sessionCapabilities.get(group) ?? "unknown";
4553
+ if (observedCapability === "supported" || currentCapability === "unknown") {
4554
+ sessionCapabilities.set(group, observedCapability);
4555
+ }
4556
+ if (hasMaterializedSkillEvidence(s)) sessionsWithMaterializedSkills.add(group);
4557
+ const skillDocuments = skillDocumentNames(s);
4558
+ if (skillDocuments.length > 0) {
4559
+ sessionsWithSkillFileReference.add(group);
4560
+ addCounts(skillDocumentReads, skillDocuments);
4561
+ }
4562
+ const tn = toolName(s);
4563
+ if (tn === "Skill") {
4564
+ sessionCapabilities.set(group, "supported");
4565
+ const name = skillNameOf(parseInput(s));
4566
+ skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
4567
+ sessionsWithSkill.add(group);
4568
+ } else if (tn === "Task" || tn === "Agent") {
4569
+ const type = subagentTypeOf(parseInput(s));
4570
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
4571
+ sessionsWithSubagent.add(group);
4572
+ canonicalSubagentSessions.add(group);
4573
+ } else if (tn && isSpawnAgentTool(tn)) {
4574
+ const types = fallbackSubagents.get(group) ?? [];
4575
+ types.push(subagentTypeOf(parseInput(s)));
4576
+ fallbackSubagents.set(group, types);
4577
+ }
3695
4578
  }
3696
- const candidate = value;
3697
- if (candidate.kind !== "traces.session_index" || candidate.schemaVersion !== 1 || !Array.isArray(candidate.sessions)) {
3698
- throw new Error("input is not a traces.session_index v1 file");
4579
+ for (const [traceId, types] of fallbackSubagents) {
4580
+ if (canonicalSubagentSessions.has(traceId)) continue;
4581
+ sessionsWithSubagent.add(traceId);
4582
+ for (const type of types) {
4583
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
4584
+ }
3699
4585
  }
3700
- if (!candidate.totals || typeof candidate.totals !== "object") {
3701
- throw new Error("session index is missing totals");
4586
+ const {
4587
+ linkedCounts: loopDispatchedRuns,
4588
+ unlinkedCounts: unlinkedLoopDispatchedRuns,
4589
+ filesRead
4590
+ } = await readLoopRuns(opts.cwds ?? [], identifiedSessions);
4591
+ const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
4592
+ const totalSkillDocumentReads = Object.values(skillDocumentReads).reduce((a, b) => a + b, 0);
4593
+ const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
4594
+ const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
4595
+ const totalUnlinkedLoopDispatchedRuns = Object.values(unlinkedLoopDispatchedRuns).reduce((a, b) => a + b, 0);
4596
+ let skillTelemetrySessions = 0;
4597
+ let skillTelemetryUnsupportedSessions = 0;
4598
+ let skillTelemetryUnknownSessions = 0;
4599
+ for (const traceId of sessions) {
4600
+ const capability = sessionCapabilities.get(traceId) ?? "unknown";
4601
+ if (capability === "supported") skillTelemetrySessions += 1;
4602
+ else if (capability === "unsupported") skillTelemetryUnsupportedSessions += 1;
4603
+ else skillTelemetryUnknownSessions += 1;
3702
4604
  }
4605
+ return {
4606
+ executionGroupCount: sessions.size,
4607
+ identifiedSessionCount: identifiedSessions.size,
4608
+ unassignedTraceCount: unassignedTraceIds.size,
4609
+ sessionIdentityConflicts: conflicts,
4610
+ sessionsWithSkill: sessionsWithSkill.size,
4611
+ skillPenetration: skillTelemetrySessions === 0 ? null : sessionsWithSkill.size / skillTelemetrySessions,
4612
+ skillTelemetryStatus: telemetryStatus(
4613
+ skillTelemetrySessions,
4614
+ skillTelemetryUnsupportedSessions,
4615
+ skillTelemetryUnknownSessions
4616
+ ),
4617
+ skillTelemetrySessions,
4618
+ sessionsWithMaterializedSkills: sessionsWithMaterializedSkills.size,
4619
+ sessionsWithSkillFileReference: sessionsWithSkillFileReference.size,
4620
+ skillDocumentReads,
4621
+ totalSkillDocumentReads,
4622
+ skillInvocations,
4623
+ totalSkillInvocations,
4624
+ subagentSpawns,
4625
+ totalSubagentSpawns,
4626
+ sessionsWithSubagent: sessionsWithSubagent.size,
4627
+ loopDispatchedRuns,
4628
+ totalLoopDispatchedRuns,
4629
+ skillRunFilesRead: filesRead,
4630
+ unlinkedLoopDispatchedRuns,
4631
+ totalUnlinkedLoopDispatchedRuns
4632
+ };
3703
4633
  }
3704
- async function readSessionIndexFile(path) {
3705
- const parsed = JSON.parse(await readFile13(path, "utf8"));
3706
- assertSessionIndex(parsed);
3707
- return parsed;
3708
- }
3709
- async function writeInspectionReportFile(report, outPath) {
3710
- const path = outPath ?? join15(await mkdtemp3(join15(tmpdir3(), "traces-inspect-")), "inspection-report.md");
3711
- await writeFile3(path, renderInspectionReport(report), "utf8");
3712
- return path;
4634
+
4635
+ // src/analyze.ts
4636
+ import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
4637
+ import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
4638
+ var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
4639
+ async function analyzeSpans(spans, opts = {}) {
4640
+ if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
4641
+ const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
4642
+ const runId = opts.runId ?? `traces-${Date.now()}`;
4643
+ const execution = summarizeSpanExecution(spans, {
4644
+ experimentId: runId
4645
+ });
4646
+ const detStore = new OtlpFileTraceStore({
4647
+ path: otlpPath,
4648
+ maxFileBytes: GENERATED_TRACE_FILE_CEILING,
4649
+ perCallByteCeiling: GENERATED_TRACE_FILE_CEILING
4650
+ });
4651
+ await detStore.ensureIndexed();
4652
+ const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
4653
+ const result = await detRegistry.run(runId, { traceStore: detStore });
4654
+ if (opts.ai) {
4655
+ const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
4656
+ await agStore.ensureIndexed();
4657
+ const agRegistry = buildDefaultAnalystRegistry({
4658
+ ai: opts.ai,
4659
+ model: opts.model,
4660
+ includeBehavioral: false,
4661
+ registry: { log: opts.log }
4662
+ });
4663
+ const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
4664
+ budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
4665
+ });
4666
+ result.findings.push(...agResult.findings);
4667
+ result.per_analyst.push(...agResult.per_analyst);
4668
+ result.total_cost_usd += agResult.total_cost_usd;
4669
+ }
4670
+ return { otlpPath, execution, result };
3713
4671
  }
3714
4672
 
3715
4673
  // src/reactions.ts
@@ -3800,6 +4758,8 @@ function analyzeReactions(spans) {
3800
4758
 
3801
4759
  // src/report.ts
3802
4760
  var SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
4761
+ var CORRUPTION_RECEIPT_DISPLAY_LIMIT = 100;
4762
+ var SOURCE_DISPLAY_LIMIT = 20;
3803
4763
  var SEVERITY_BADGE = {
3804
4764
  critical: "\u{1F534} CRITICAL",
3805
4765
  high: "\u{1F7E0} HIGH",
@@ -3811,23 +4771,107 @@ function deterministicSummaryText(summary) {
3811
4771
  const parts = [];
3812
4772
  if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
3813
4773
  if (summary.reactionSignals > 0) parts.push(`${summary.reactionSignals} human reaction signal(s)`);
3814
- if (summary.toolErrorRuns > 0) parts.push(`${summary.toolErrorRuns} tool-error run(s)`);
4774
+ if (summary.failedRuns > 0) parts.push(`${summary.failedRuns} failed run(s)`);
3815
4775
  if (parts.length === 0) return "0 deterministic signals";
3816
4776
  if (parts.length === 1) return parts[0];
3817
4777
  return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
3818
4778
  }
3819
- function plural(count, singular, pluralForm = `${singular}s`) {
3820
- return count === 1 ? singular : pluralForm;
4779
+ function plural(count2, singular, pluralForm = `${singular}s`) {
4780
+ return count2 === 1 ? singular : pluralForm;
4781
+ }
4782
+ function tableCell(value) {
4783
+ return value.replace(/\s+/g, " ").replaceAll("|", "\\|").trim();
4784
+ }
4785
+ function count(value) {
4786
+ return Math.round(value).toLocaleString("en-US");
4787
+ }
4788
+ function decimal(value, digits = 1) {
4789
+ return value.toLocaleString("en-US", {
4790
+ minimumFractionDigits: digits,
4791
+ maximumFractionDigits: digits
4792
+ });
4793
+ }
4794
+ function distributionRow(label, distribution, unit = "") {
4795
+ if (distribution.n === 0) return `| ${label} | 0 | not captured | not captured | not captured | not captured | not captured |`;
4796
+ const value = (number) => `${decimal(number)}${unit}`;
4797
+ return `| ${label} | ${count(distribution.n)} | ${value(distribution.min)} | ${value(distribution.mean)} | ${value(distribution.p50)} | ${value(distribution.p95)} | ${value(distribution.max)} |`;
4798
+ }
4799
+ function tokenRows(usage) {
4800
+ const rows = [
4801
+ ["Input", usage.totals.input, usage.input],
4802
+ ["Output", usage.totals.output, usage.output],
4803
+ ["Reasoning (output subset)", usage.totals.reasoning, usage.reasoning],
4804
+ ["Cache read", usage.totals.cached, usage.cached],
4805
+ ["Cache write", usage.totals.cacheWrite, usage.cacheWrite]
4806
+ ];
4807
+ return rows.map(
4808
+ ([label, total, values]) => `| ${label} | ${count(total)} | ${count(values.n)} | ${decimal(values.mean)} | ${decimal(values.p50)} | ${decimal(values.p95)} | ${count(values.max)} |`
4809
+ );
4810
+ }
4811
+ function renderExecution(report) {
4812
+ const execution = report.execution;
4813
+ const lines = ["## execution facts", ""];
4814
+ lines.push(
4815
+ `- **Sessions:** ${count(execution.durationMs.n)} | **Model-call runs:** ${count(execution.modelCalls.runs)} | **Model-call events:** ${count(execution.modelCalls.events)} | **Sessions with tool errors:** ${count(execution.failures.runs)}/${count(execution.failures.reportingRuns)} (${decimal(execution.failures.fraction * 100, 2)}%)`
4816
+ );
4817
+ lines.push("- **Task quality:** not measured; these traces do not include comparable task outcome labels.");
4818
+ lines.push("");
4819
+ lines.push("| Time | Sessions measured | Min | Mean | p50 | p95 | Max |");
4820
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4821
+ lines.push(distributionRow("Recorded session interval", execution.durationMs, "ms"));
4822
+ lines.push(distributionRow("Queue", execution.queueMs, "ms"));
4823
+ lines.push("");
4824
+ lines.push("Recorded session interval is first-to-last trace time and can include idle time; it is not active agent runtime.");
4825
+ lines.push("");
4826
+ lines.push("### Direct model usage");
4827
+ lines.push("");
4828
+ lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
4829
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4830
+ lines.push(...tokenRows(execution.tokenUsage));
4831
+ lines.push("");
4832
+ lines.push("| Model | Runs |");
4833
+ lines.push("|---|---:|");
4834
+ for (const model of execution.models) {
4835
+ lines.push(`| \`${tableCell(model.model)}\` | ${count(model.runs)} |`);
4836
+ }
4837
+ lines.push("");
4838
+ lines.push("### Cost coverage");
4839
+ lines.push("");
4840
+ lines.push("| Source | Runs | Total USD |");
4841
+ lines.push("|---|---:|---:|");
4842
+ lines.push(`| Observed | ${count(report.costProvenance.observed.n)} | $${report.costProvenance.observed.totalUsd.toFixed(6)} |`);
4843
+ lines.push(`| Estimated | ${count(report.costProvenance.estimated.n)} | $${report.costProvenance.estimated.totalUsd.toFixed(6)} |`);
4844
+ lines.push(`| Uncaptured | ${count(report.costProvenance.uncaptured.n)} | not available |`);
4845
+ lines.push("");
4846
+ lines.push(`Known cost coverage: ${decimal(report.costProvenance.knownFraction * 100, 2)}%.`);
4847
+ lines.push("");
4848
+ if (execution.aggregateUsage.runs > 0) {
4849
+ lines.push("### Orchestration-reported usage");
4850
+ lines.push("");
4851
+ lines.push(
4852
+ `Kept separate from direct model usage because orchestration totals can overlap model-call telemetry. ${count(execution.aggregateUsage.runs)} run(s) reported aggregate values.`
4853
+ );
4854
+ lines.push("");
4855
+ lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
4856
+ lines.push("|---|---:|---:|---:|---:|---:|---:|");
4857
+ lines.push(...tokenRows(execution.aggregateUsage.tokenUsage));
4858
+ lines.push("");
4859
+ lines.push(
4860
+ `Aggregate cost: $${execution.aggregateUsage.totalCostUsd.toFixed(6)} across ${count(execution.aggregateUsage.costUsd.n)} reporting run(s).`
4861
+ );
4862
+ lines.push("");
4863
+ }
4864
+ return lines.join("\n");
3821
4865
  }
3822
4866
  function summarizeDeterministicSignals(pipelines, reactions) {
3823
4867
  const stuckLoops = pipelines.stuckLoops.findings.length;
3824
- const reactionSignals = Object.values(reactions.signals).reduce((total, count) => total + count, 0);
3825
- const toolErrorRuns2 = pipelines.toolUse.filter((run) => run.errorRate > 0).length;
4868
+ const reactionSignals = Object.values(reactions.signals).reduce((total, count2) => total + count2, 0);
4869
+ const failedRuns = pipelines.failureClusters.totalFailures;
3826
4870
  return {
3827
4871
  stuckLoops,
3828
4872
  reactionSignals,
3829
- toolErrorRuns: toolErrorRuns2,
3830
- totalSignals: stuckLoops + reactionSignals + toolErrorRuns2
4873
+ failedRuns,
4874
+ totalSignals: stuckLoops + reactionSignals + failedRuns
3831
4875
  };
3832
4876
  }
3833
4877
  function renderReport(result, meta) {
@@ -3835,14 +4879,93 @@ function renderReport(result, meta) {
3835
4879
  const findings = [...result.findings].sort(
3836
4880
  (a, b) => (SEVERITY_RANK[a.severity] ?? 9) - (SEVERITY_RANK[b.severity] ?? 9)
3837
4881
  );
3838
- const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, toolErrorRuns: 0, totalSignals: 0 };
3839
- const findingSummary = deterministic.totalSignals > 0 ? `${findings.length} analyst ${plural(findings.length, "finding")} + ${deterministic.totalSignals} deterministic ${plural(deterministic.totalSignals, "signal")}` : `${findings.length} ${plural(findings.length, "finding")}`;
4882
+ const deterministic = meta.deterministic ?? { stuckLoops: 0, reactionSignals: 0, failedRuns: 0, totalSignals: 0 };
4883
+ const findingSummary = deterministic.totalSignals > 0 ? `${findings.length} ${plural(findings.length, "finding")}; ${deterministic.totalSignals} raw deterministic ${plural(deterministic.totalSignals, "signal")}` : `${findings.length} ${plural(findings.length, "finding")}`;
3840
4884
  lines.push(`# Trace analysis \u2014 ${meta.harness}`);
3841
4885
  lines.push("");
4886
+ const sessionSummary = meta.unassignedTraceCount ? `${meta.sessionCount} identified session(s) + ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} without a single stable session identity` : `${meta.sessionCount} session(s)`;
3842
4887
  lines.push(
3843
- `${meta.sessionCount} session(s), ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Cost: $${result.total_cost_usd.toFixed(4)}.`
4888
+ `${sessionSummary}, ${meta.spanCount} spans \u2192 **${findingSummary}** across ${result.per_analyst.length} analyst(s). Analysis cost: $${result.total_cost_usd.toFixed(4)}.`
3844
4889
  );
3845
4890
  lines.push("");
4891
+ if (meta.unassignedTraceCount) {
4892
+ lines.push(
4893
+ `> ${meta.unassignedTraceCount} ${plural(meta.unassignedTraceCount, "trace")} lacked a single stable session identity. Their spans were analyzed but were not relabeled as sessions.`
4894
+ );
4895
+ lines.push("");
4896
+ }
4897
+ lines.push(renderExecution(meta.execution));
4898
+ lines.push("");
4899
+ if (meta.sources && meta.sources.length > 0) {
4900
+ lines.push("## Selected sessions");
4901
+ lines.push("");
4902
+ lines.push("| Role | Session ID | Parent session | Integrity | Subject (first prompt line) | Source path |");
4903
+ lines.push("|---|---|---|---|---|---|");
4904
+ for (const source of meta.sources.slice(0, SOURCE_DISPLAY_LIMIT)) {
4905
+ const corruptionCount = source.corruptionCount ?? source.corruptions?.length ?? 0;
4906
+ const integrity = source.integrity === "degraded_not_lossless" ? `degraded, not lossless (${corruptionCount} corrupt ${plural(corruptionCount, "record")})` : "complete";
4907
+ lines.push(
4908
+ `| ${source.role} | \`${tableCell(source.sessionId)}\` | ${source.parentSessionId ? `\`${tableCell(source.parentSessionId)}\`` : "\u2014"} | ${integrity} | ${tableCell(source.subject) || "(no prompt captured)"} | \`${tableCell(source.path)}\` |`
4909
+ );
4910
+ }
4911
+ if (meta.sources.length > SOURCE_DISPLAY_LIMIT) {
4912
+ lines.push(
4913
+ `| \u2026 | ${meta.sources.length - SOURCE_DISPLAY_LIMIT} additional sessions omitted | \u2014 | \u2014 | \u2014 | \u2014 |`
4914
+ );
4915
+ }
4916
+ lines.push("");
4917
+ const childCount = meta.sources.filter((source) => source.role === "child").length;
4918
+ if (childCount > 0) {
4919
+ lines.push(
4920
+ `> Scope: ${childCount}/${meta.sources.length} selected session(s) are children. Counts below describe only the selected files, not their parent operator sessions.`
4921
+ );
4922
+ lines.push("");
4923
+ }
4924
+ const totalCorruptionCount = meta.sources.reduce(
4925
+ (total, source) => total + (source.corruptionCount ?? source.corruptions?.length ?? 0),
4926
+ 0
4927
+ );
4928
+ const corruptions = [];
4929
+ for (const source of meta.sources) {
4930
+ for (const receipt of source.corruptions ?? []) {
4931
+ if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
4932
+ corruptions.push(receipt);
4933
+ }
4934
+ if (corruptions.length === CORRUPTION_RECEIPT_DISPLAY_LIMIT) break;
4935
+ }
4936
+ if (totalCorruptionCount > 0) {
4937
+ lines.push("## Source corruption receipts");
4938
+ lines.push("");
4939
+ lines.push(
4940
+ "> 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."
4941
+ );
4942
+ lines.push("");
4943
+ for (const source of meta.sources) {
4944
+ const count2 = source.corruptionCount ?? source.corruptions?.length ?? 0;
4945
+ if (count2 === 0) continue;
4946
+ const digest = source.corruptionDigest ? `, digest \`${source.corruptionDigest}\`` : "";
4947
+ lines.push(`- Session \`${tableCell(source.sessionId)}\`: ${count2} ${plural(count2, "receipt")}${digest}.`);
4948
+ }
4949
+ lines.push("");
4950
+ if (corruptions.length > 0) {
4951
+ lines.push("| Session ID | Source path | Line | Byte offset | Byte length | SHA-256 | Raw bytes |");
4952
+ lines.push("|---|---|---:|---:|---:|---|---|");
4953
+ for (const receipt of corruptions) {
4954
+ lines.push(
4955
+ `| \`${tableCell(receipt.sessionId)}\` | \`${tableCell(receipt.sourcePath)}\` | ${receipt.lineNumber} | ${receipt.byteOffset} | ${receipt.byteLength} | \`${receipt.sha256}\` | local source only |`
4956
+ );
4957
+ }
4958
+ lines.push("");
4959
+ }
4960
+ const omitted = totalCorruptionCount - corruptions.length;
4961
+ if (omitted > 0) {
4962
+ lines.push(
4963
+ `_${omitted} additional ${plural(omitted, "receipt")} omitted from this report; all receipts remain in \`source.corruption.receipt\` child spans._`
4964
+ );
4965
+ lines.push("");
4966
+ }
4967
+ }
4968
+ }
3846
4969
  lines.push("| Analyst | Status | Findings | Latency |");
3847
4970
  lines.push("|---|---|---|---|");
3848
4971
  for (const s of result.per_analyst) {
@@ -3850,7 +4973,7 @@ function renderReport(result, meta) {
3850
4973
  }
3851
4974
  lines.push("");
3852
4975
  if (findings.length === 0 && deterministic.totalSignals === 0) {
3853
- lines.push("_No findings \u2014 no behavioral inefficiencies or failure modes detected._");
4976
+ lines.push("_No supported behavioral findings in the captured fields._");
3854
4977
  } else if (findings.length === 0) {
3855
4978
  lines.push(
3856
4979
  `_No analyst findings. Deterministic checks found ${deterministicSummaryText(deterministic)}; see sections below._`
@@ -3871,6 +4994,7 @@ function renderReport(result, meta) {
3871
4994
  if (f.subject) lines.push(`- **Subject:** \`${f.subject}\``);
3872
4995
  lines.push(`- **Confidence:** ${f.confidence}`);
3873
4996
  if (f.recommended_action) lines.push(`- **Fix:** ${f.recommended_action}`);
4997
+ if (f.validation_plan) lines.push(`- **Check:** ${f.validation_plan}`);
3874
4998
  for (const ev of f.evidence_refs.slice(0, 3)) {
3875
4999
  const excerpt = ev.excerpt ? ` \u2014 \`${ev.excerpt.slice(0, 200)}\`` : "";
3876
5000
  lines.push(`- **Evidence:** ${ev.kind} ${ev.uri}${excerpt}`);
@@ -3885,20 +5009,79 @@ function renderReport(result, meta) {
3885
5009
  return lines.join("\n");
3886
5010
  }
3887
5011
  function renderPipelines(pr) {
3888
- const lines = ["## loops & waste (deterministic)", ""];
5012
+ const lines = ["## reliability, loops & waste (deterministic)", ""];
5013
+ lines.push(
5014
+ `- **Execution failures:** ${pr.failureClusters.totalFailures}/${pr.failureClusters.totalRuns} run(s)`
5015
+ );
5016
+ if (pr.failureClusters.clusters.length > 0) {
5017
+ lines.push("");
5018
+ lines.push("| Failure class | Runs | Tool | Example trace | Example error |");
5019
+ lines.push("|---|---:|---|---|---|");
5020
+ for (const cluster of pr.failureClusters.clusters.slice(0, 10)) {
5021
+ lines.push(
5022
+ `| ${tableCell(cluster.failureClass)} | ${cluster.runCount} | ${cluster.toolName ? `\`${tableCell(cluster.toolName)}\`` : "not captured"} | ${cluster.exampleRunId ? `\`${tableCell(cluster.exampleRunId)}\`` : "not captured"} | ${tableCell(cluster.exampleError ?? "not captured")} |`
5023
+ );
5024
+ }
5025
+ lines.push("");
5026
+ }
3889
5027
  if (pr.stuckLoops.findings.length === 0) {
3890
- lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args).");
5028
+ lines.push("- **Stuck loops:** none (no tool called \u22653\xD7 with identical args in a short interval).");
3891
5029
  } else {
3892
5030
  lines.push(`- **Stuck loops:** ${pr.stuckLoops.findings.length} (${(pr.stuckLoops.affectedRunRatio * 100).toFixed(0)}% of runs affected)`);
3893
5031
  for (const f of pr.stuckLoops.findings.sort((a, b) => b.occurrences - a.occurrences).slice(0, 10)) {
3894
5032
  lines.push(` - \u{1F501} \`${f.toolName}\` \xD7${f.occurrences} with identical args over ${(f.windowMs / 1e3).toFixed(1)}s`);
3895
5033
  }
3896
5034
  }
3897
- for (const m of pr.toolUse) {
3898
- if (m.totalCalls === 0) continue;
5035
+ const runsWithTools = pr.toolUse.filter((metrics) => metrics.totalCalls > 0);
5036
+ if (runsWithTools.length > 0) {
5037
+ let totalCalls = 0;
5038
+ let callsWithCapturedArgs = 0;
5039
+ let duplicateCalls = 0;
5040
+ let errorCalls = 0;
5041
+ let followedFailures = 0;
5042
+ const byTool = /* @__PURE__ */ new Map();
5043
+ for (const metrics of runsWithTools) {
5044
+ const toolStats = Object.values(metrics.byTool);
5045
+ const duplicates = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.duplicates, 0) : Math.round(metrics.duplicateRate * metrics.totalCalls);
5046
+ const errors = toolStats.length > 0 ? toolStats.reduce((total, stats) => total + stats.errors, 0) : Math.round(metrics.errorRate * metrics.totalCalls);
5047
+ totalCalls += metrics.totalCalls;
5048
+ callsWithCapturedArgs += metrics.callsWithCapturedArgs;
5049
+ duplicateCalls += duplicates;
5050
+ errorCalls += errors;
5051
+ followedFailures += Math.round(metrics.retryRate * errors);
5052
+ for (const [name, stats] of Object.entries(metrics.byTool)) {
5053
+ const aggregate = byTool.get(name) ?? {
5054
+ calls: 0,
5055
+ captured: 0,
5056
+ duplicates: 0,
5057
+ errors: 0,
5058
+ latencyMs: 0
5059
+ };
5060
+ aggregate.calls += stats.calls;
5061
+ aggregate.captured += stats.callsWithCapturedArgs;
5062
+ aggregate.duplicates += stats.duplicates;
5063
+ aggregate.errors += stats.errors;
5064
+ aggregate.latencyMs += stats.avgLatencyMs * stats.calls;
5065
+ byTool.set(name, aggregate);
5066
+ }
5067
+ }
5068
+ const failureFollowUp = errorCalls > 0 ? `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)` : "";
5069
+ const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
3899
5070
  lines.push(
3900
- `- **Tool use** (${m.totalCalls} calls): ${(m.duplicateRate * 100).toFixed(0)}% duplicate, ${(m.retryRate * 100).toFixed(0)}% retry, ${(m.errorRate * 100).toFixed(0)}% error`
5071
+ `- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
3901
5072
  );
5073
+ if (byTool.size > 0) {
5074
+ lines.push("");
5075
+ lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
5076
+ lines.push("|---|---:|---:|---:|---:|---:|");
5077
+ for (const [name, stats] of [...byTool].sort(
5078
+ (left, right) => right[1].calls - left[1].calls || left[0].localeCompare(right[0])
5079
+ )) {
5080
+ lines.push(
5081
+ `| \`${tableCell(name)}\` | ${stats.calls} | ${stats.captured}/${stats.calls} | ${stats.duplicates}/${stats.captured} | ${stats.errors}/${stats.calls} | ${(stats.latencyMs / stats.calls).toFixed(1)}ms |`
5082
+ );
5083
+ }
5084
+ }
3902
5085
  }
3903
5086
  lines.push("");
3904
5087
  return lines.join("\n");
@@ -3952,26 +5135,66 @@ function renderReactions(rr) {
3952
5135
  }
3953
5136
  function renderAdoption(ar) {
3954
5137
  const lines = ["## skill & subagent adoption (deterministic)", ""];
5138
+ const scopeParts = [`${ar.identifiedSessionCount} identified session(s)`];
5139
+ if (ar.unassignedTraceCount > 0) {
5140
+ scopeParts.push(`${ar.unassignedTraceCount} trace(s) without a single stable session identity`);
5141
+ }
5142
+ lines.push(`- **Scope:** ${scopeParts.join(" + ")}`);
5143
+ if (ar.sessionIdentityConflicts.length > 0) {
5144
+ const displayed = ar.sessionIdentityConflicts.slice(0, SOURCE_DISPLAY_LIMIT);
5145
+ const details = displayed.map((conflict) => `\`${conflict.traceId}\` (${conflict.sessionIds.map((id) => `\`${id}\``).join(", ")})`).join("; ");
5146
+ const omitted = ar.sessionIdentityConflicts.length - displayed.length;
5147
+ lines.push(
5148
+ `- **Conflicting session identity:** ${ar.sessionIdentityConflicts.length} trace(s): ${details}${omitted > 0 ? `; ${omitted} more omitted from this report` : ""}`
5149
+ );
5150
+ }
5151
+ lines.push(
5152
+ "- **Prompt, tools, MCP, hooks, and full agent profile:** not assessed; no baseline profile was supplied."
5153
+ );
5154
+ if (ar.skillPenetration === null) {
5155
+ const label = ar.skillTelemetryStatus === "unsupported" ? "uncaptured/unsupported" : "uncaptured/unknown";
5156
+ lines.push(
5157
+ `- **Explicit skill invocation rate:** ${label} (${ar.skillTelemetrySessions}/${ar.executionGroupCount} observed group(s) expose a dedicated Skill event)`
5158
+ );
5159
+ } else {
5160
+ const excluded = ar.executionGroupCount - ar.skillTelemetrySessions;
5161
+ lines.push(
5162
+ `- **Explicit skill invocation rate:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.skillTelemetrySessions} measurable group(s)${excluded > 0 ? `; ${excluded} unmeasurable group(s) excluded` : ""})`
5163
+ );
5164
+ }
3955
5165
  lines.push(
3956
- `- **Skill penetration:** ${(ar.skillPenetration * 100).toFixed(0)}% (${ar.sessionsWithSkill}/${ar.sessionCount} session(s) invoked a skill explicitly)`
5166
+ `- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **Sessions with successful skill-document reads:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount}`
3957
5167
  );
3958
5168
  lines.push(
3959
- `- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Loop-dispatched runs:** ${ar.totalLoopDispatchedRuns} (from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s))`
5169
+ `- **Explicit skill invocations:** ${ar.totalSkillInvocations} (Skill tool spans) \xB7 **Session-linked loop runs:** ${ar.totalLoopDispatchedRuns}`
3960
5170
  );
5171
+ if (ar.totalUnlinkedLoopDispatchedRuns > 0) {
5172
+ lines.push(
5173
+ `- **Unlinked repository history:** ${ar.totalUnlinkedLoopDispatchedRuns} run(s) from ${ar.skillRunFilesRead} \`.evolve/skill-runs.jsonl\` file(s); not attributed to these sessions.`
5174
+ );
5175
+ }
5176
+ if (ar.totalSkillDocumentReads > 0) {
5177
+ lines.push(`- **Successful skill-document reads:** ${ar.totalSkillDocumentReads}; inspection is not outcome evidence.`);
5178
+ }
3961
5179
  lines.push(
3962
- `- **Subagent spawns:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} session(s)`
5180
+ `- **Subagent spawns observed:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} observed group(s); capture completeness depends on source telemetry.`
3963
5181
  );
3964
5182
  lines.push("");
3965
5183
  const skillRows = Object.entries(ar.skillInvocations).sort((a, b) => b[1] - a[1]);
5184
+ const documentRows = Object.entries(ar.skillDocumentReads).sort((a, b) => b[1] - a[1]);
3966
5185
  const loopRows = Object.entries(ar.loopDispatchedRuns).sort((a, b) => b[1] - a[1]);
3967
- if (skillRows.length > 0 || loopRows.length > 0) {
3968
- lines.push("| Skill | Explicit invocations | Loop-dispatched runs |");
3969
- lines.push("|---|---|---|");
3970
- const names = [.../* @__PURE__ */ new Set([...skillRows.map((r) => r[0]), ...loopRows.map((r) => r[0])])].sort(
3971
- (a, b) => (ar.skillInvocations[b] ?? 0) + (ar.loopDispatchedRuns[b] ?? 0) - (ar.skillInvocations[a] ?? 0) - (ar.loopDispatchedRuns[a] ?? 0)
5186
+ if (skillRows.length > 0 || documentRows.length > 0 || loopRows.length > 0) {
5187
+ lines.push("| Skill | Explicit invocations | Documents read | Session-linked loop runs |");
5188
+ lines.push("|---|---|---|---|");
5189
+ const names = [.../* @__PURE__ */ new Set([
5190
+ ...skillRows.map((r) => r[0]),
5191
+ ...documentRows.map((r) => r[0]),
5192
+ ...loopRows.map((r) => r[0])
5193
+ ])].sort(
5194
+ (a, b) => (ar.skillInvocations[b] ?? 0) + (ar.skillDocumentReads[b] ?? 0) + (ar.loopDispatchedRuns[b] ?? 0) - (ar.skillInvocations[a] ?? 0) - (ar.skillDocumentReads[a] ?? 0) - (ar.loopDispatchedRuns[a] ?? 0)
3972
5195
  );
3973
5196
  for (const n of names) {
3974
- lines.push(`| \`${n}\` | ${ar.skillInvocations[n] ?? 0} | ${ar.loopDispatchedRuns[n] ?? 0} |`);
5197
+ lines.push(`| \`${n}\` | ${ar.skillInvocations[n] ?? 0} | ${ar.skillDocumentReads[n] ?? 0} | ${ar.loopDispatchedRuns[n] ?? 0} |`);
3975
5198
  }
3976
5199
  lines.push("");
3977
5200
  }
@@ -4002,9 +5225,6 @@ var severityRank2 = {
4002
5225
  low: 3,
4003
5226
  info: 4
4004
5227
  };
4005
- function slug(input) {
4006
- return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 72) || "item";
4007
- }
4008
5228
  function json(value) {
4009
5229
  return `${JSON.stringify(value, null, 2)}
4010
5230
  `;
@@ -4017,13 +5237,76 @@ function evidence(kind, uri, excerpt) {
4017
5237
  return excerpt ? { kind, uri, excerpt } : { kind, uri };
4018
5238
  }
4019
5239
  function totalReactionSignals(reactions) {
4020
- return Object.values(reactions.signals).reduce((sum, count) => sum + count, 0);
5240
+ return Object.values(reactions.signals).reduce((sum, count2) => sum + count2, 0);
4021
5241
  }
4022
- function toolErrorRuns(pipelines) {
4023
- return pipelines.toolUse.filter((run) => run.errorRate > 0).length;
5242
+ function traceCoverageFinding(pipelines, adoption) {
5243
+ const toolCalls = pipelines.toolUse.reduce((total, run) => total + run.totalCalls, 0);
5244
+ const capturedToolCalls = pipelines.toolUse.reduce(
5245
+ (total, run) => total + run.callsWithCapturedArgs,
5246
+ 0
5247
+ );
5248
+ const missingToolArgs = toolCalls - capturedToolCalls;
5249
+ const traces = pipelines.toolUse.length;
5250
+ const missingSessionIds = adoption.unassignedTraceCount;
5251
+ const sessionIdentityConflicts = adoption.sessionIdentityConflicts;
5252
+ const unmeasurableSkillGroups = adoption.executionGroupCount - adoption.skillTelemetrySessions;
5253
+ if (missingToolArgs === 0 && missingSessionIds === 0 && unmeasurableSkillGroups === 0) {
5254
+ return void 0;
5255
+ }
5256
+ const parts = [];
5257
+ const refs = [];
5258
+ if (missingToolArgs > 0) {
5259
+ parts.push(`${missingToolArgs}/${toolCalls} tool call(s) lacked comparable arguments`);
5260
+ refs.push(evidence("metric", "coverage.tool_arguments", `${capturedToolCalls}/${toolCalls} captured`));
5261
+ }
5262
+ if (missingSessionIds > 0) {
5263
+ parts.push(`${missingSessionIds}/${traces} trace(s) lacked a single stable session identity`);
5264
+ refs.push(evidence("metric", "coverage.session_identity", `${traces - missingSessionIds}/${traces} identified`));
5265
+ }
5266
+ if (sessionIdentityConflicts.length > 0) {
5267
+ refs.push(evidence(
5268
+ "span",
5269
+ "coverage.session_identity_conflicts",
5270
+ sessionIdentityConflicts.map((conflict) => conflict.traceId).join(", ")
5271
+ ));
5272
+ }
5273
+ if (unmeasurableSkillGroups > 0) {
5274
+ parts.push(`${unmeasurableSkillGroups}/${adoption.executionGroupCount} execution group(s) lacked measurable skill events`);
5275
+ refs.push(evidence(
5276
+ "metric",
5277
+ "coverage.skill_events",
5278
+ `${adoption.skillTelemetrySessions}/${adoption.executionGroupCount} measurable`
5279
+ ));
5280
+ }
5281
+ const argumentCoverage = toolCalls === 0 ? 1 : capturedToolCalls / toolCalls;
5282
+ const sessionCoverage = traces === 0 ? 1 : (traces - missingSessionIds) / traces;
5283
+ const severity = argumentCoverage < 0.5 || sessionCoverage < 0.5 ? "high" : argumentCoverage < 0.9 || sessionCoverage < 0.9 || unmeasurableSkillGroups > 0 ? "medium" : "low";
5284
+ return makeFinding({
5285
+ analyst_id: "traces-deterministic",
5286
+ area: "instrumentation",
5287
+ claim: `Trace capture is incomplete: ${parts.join("; ")}`,
5288
+ severity,
5289
+ rationale: "Missing identity or agent-profile evidence prevents reliable cross-run comparison and can hide tool, prompt, skill, or subagent failures.",
5290
+ evidence_refs: refs,
5291
+ 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.",
5292
+ 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.",
5293
+ confidence: 1,
5294
+ metadata: {
5295
+ source: "traces.coverage",
5296
+ toolCalls,
5297
+ capturedToolCalls,
5298
+ traces,
5299
+ missingSessionIds,
5300
+ sessionIdentityConflictCount: sessionIdentityConflicts.length,
5301
+ skillTelemetryGroups: adoption.skillTelemetrySessions,
5302
+ executionGroups: adoption.executionGroupCount
5303
+ }
5304
+ });
4024
5305
  }
4025
5306
  function deterministicFindings(pipelines, reactions, adoption) {
4026
5307
  const findings = [];
5308
+ const coverage = traceCoverageFinding(pipelines, adoption);
5309
+ if (coverage) findings.push(coverage);
4027
5310
  const loops = pipelines.stuckLoops.findings;
4028
5311
  if (loops.length > 0) {
4029
5312
  const top = [...loops].sort((a, b) => b.occurrences - a.occurrences)[0];
@@ -4043,19 +5326,34 @@ function deterministicFindings(pipelines, reactions, adoption) {
4043
5326
  metadata: { source: "traces.pipeline.stuckLoopView", loopCount: loops.length }
4044
5327
  }));
4045
5328
  }
4046
- const erroredRuns = toolErrorRuns(pipelines);
4047
- if (erroredRuns > 0) {
5329
+ const failedRuns = pipelines.failureClusters.totalFailures;
5330
+ if (failedRuns > 0) {
5331
+ const top = pipelines.failureClusters.clusters[0];
5332
+ const failureRate = failedRuns / Math.max(1, pipelines.failureClusters.totalRuns);
4048
5333
  findings.push(makeFinding({
4049
5334
  analyst_id: "traces-deterministic",
4050
- area: "tool-use",
4051
- claim: `${erroredRuns} run(s) included tool errors`,
4052
- severity: erroredRuns >= 5 ? "high" : "medium",
4053
- rationale: "Tool errors are expensive when the agent keeps planning as if the call succeeded.",
4054
- evidence_refs: [evidence("metric", "pipelines.tool_error_runs", `${erroredRuns} run(s) with errorRate > 0`)],
4055
- recommended_action: "Fix the highest-frequency failing tool path first, then make the agent inspect failure output before retrying.",
4056
- validation_plan: "Rerun the same trace window and require the tool-error run count and repeated retries to decrease.",
4057
- confidence: 0.9,
4058
- metadata: { source: "traces.pipeline.computeToolUseMetrics", erroredRuns }
5335
+ area: "reliability",
5336
+ claim: `${failedRuns}/${pipelines.failureClusters.totalRuns} run(s) had execution errors`,
5337
+ severity: failureRate >= 0.1 ? "high" : "medium",
5338
+ rationale: "Execution errors consume time and tokens, and sparse error telemetry can hide whether the agent recovered or repeated the same failure.",
5339
+ evidence_refs: [
5340
+ evidence(
5341
+ "metric",
5342
+ `pipelines.failure_cluster.${top?.failureClass ?? "unknown"}`,
5343
+ top ? `${top.runCount} run(s); example: ${top.exampleError ?? "error details not captured"}` : `${failedRuns} failed run(s)`
5344
+ ),
5345
+ ...top?.exampleRunId ? [evidence("span", `trace:${top.exampleRunId}`, top.exampleError)] : []
5346
+ ],
5347
+ 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.",
5348
+ validation_plan: "Rerun a comparable trace window and require failed runs and repeated failures in the top cluster to decrease.",
5349
+ confidence: 1,
5350
+ metadata: {
5351
+ source: "agent-eval.failureClusterView",
5352
+ failedRuns,
5353
+ totalRuns: pipelines.failureClusters.totalRuns,
5354
+ topFailureClass: top?.failureClass,
5355
+ exampleRunId: top?.exampleRunId
5356
+ }
4059
5357
  }));
4060
5358
  }
4061
5359
  const corrective = reactions.signals.correction + reactions.signals.frustration + reactions.signals.jargon + reactions.signals.structure;
@@ -4074,7 +5372,8 @@ function deterministicFindings(pipelines, reactions, adoption) {
4074
5372
  }));
4075
5373
  }
4076
5374
  const skillRuns = adoption.totalSkillInvocations + adoption.totalLoopDispatchedRuns;
4077
- if (adoption.sessionCount > 0 && skillRuns === 0) {
5375
+ const allSessionsMeasurable = adoption.skillTelemetrySessions === adoption.executionGroupCount;
5376
+ if (allSessionsMeasurable && adoption.executionGroupCount > 0 && skillRuns === 0) {
4078
5377
  findings.push(makeFinding({
4079
5378
  analyst_id: "traces-deterministic",
4080
5379
  area: "workflow",
@@ -4085,7 +5384,7 @@ function deterministicFindings(pipelines, reactions, adoption) {
4085
5384
  recommended_action: "Create or invoke a narrow skill for the repeated workflow, then let traces compare future sessions against this baseline.",
4086
5385
  validation_plan: "Rerun traces and require skill-run attribution to appear for repeated workflows.",
4087
5386
  confidence: 0.75,
4088
- metadata: { source: "traces.adoption", sessionCount: adoption.sessionCount }
5387
+ metadata: { source: "traces.adoption", executionGroupCount: adoption.executionGroupCount }
4089
5388
  }));
4090
5389
  }
4091
5390
  return findings;
@@ -4146,41 +5445,6 @@ function normalizeExternalFindings(results) {
4146
5445
  }
4147
5446
  return out;
4148
5447
  }
4149
- function sourceOf(finding2) {
4150
- if (finding2.analyst_id.startsWith("external:")) return "external";
4151
- if (finding2.analyst_id === "traces-deterministic") return "deterministic";
4152
- return "analyst";
4153
- }
4154
- function recommendationTitle(finding2) {
4155
- if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
4156
- return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
4157
- }
4158
- function buildRecommendations(findings) {
4159
- return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
4160
- schemaVersion: 1,
4161
- id: `rec-${index + 1}-${slug(finding2.area)}`,
4162
- severity: finding2.severity,
4163
- title: recommendationTitle(finding2),
4164
- action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
4165
- rationale: finding2.rationale ?? finding2.claim,
4166
- evidenceRefs: finding2.evidence_refs,
4167
- findingIds: [finding2.finding_id],
4168
- validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
4169
- source: sourceOf(finding2)
4170
- }));
4171
- }
4172
- function buildClaims(findings) {
4173
- return findings.map((finding2, index) => ({
4174
- schemaVersion: 1,
4175
- id: `claim-${index + 1}-${slug(finding2.area)}`,
4176
- severity: finding2.severity,
4177
- text: finding2.claim,
4178
- area: finding2.area,
4179
- sourceFindingId: finding2.finding_id,
4180
- evidenceRefs: finding2.evidence_refs,
4181
- validationPlan: finding2.validation_plan
4182
- }));
4183
- }
4184
5448
  function buildEvidenceRows(findings) {
4185
5449
  return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
4186
5450
  schemaVersion: 1,
@@ -4192,31 +5456,10 @@ function buildEvidenceRows(findings) {
4192
5456
  evidence: ref
4193
5457
  })));
4194
5458
  }
4195
- function renderRecommendations(recommendations) {
4196
- const lines = ["## recommendations", ""];
4197
- if (recommendations.length === 0) {
4198
- lines.push("- No recommendations emitted.");
4199
- lines.push("");
4200
- return lines.join("\n");
4201
- }
4202
- for (const rec of recommendations.slice(0, 10)) {
4203
- lines.push(`### ${rec.severity.toUpperCase()} \u2014 ${rec.title}`);
4204
- lines.push("");
4205
- lines.push(`- **Action:** ${rec.action}`);
4206
- lines.push(`- **Why:** ${rec.rationale}`);
4207
- lines.push(`- **Check:** ${rec.validationPlan}`);
4208
- for (const ref of rec.evidenceRefs.slice(0, 3)) {
4209
- lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
4210
- }
4211
- lines.push("");
4212
- }
4213
- return lines.join("\n");
4214
- }
4215
5459
  function renderFindingPacket(packet, title = "Trace finding packet") {
4216
5460
  const lines = [`# ${title}`, ""];
4217
- lines.push(`${packet.findings.length} finding(s), ${packet.recommendations.length} recommendation(s), ${packet.claims.length} claim(s).`);
5461
+ lines.push(`${packet.findings.length} evidence-backed finding(s) with actions and checks.`);
4218
5462
  lines.push("");
4219
- lines.push(renderRecommendations(packet.recommendations));
4220
5463
  if (packet.findings.length > 0) {
4221
5464
  lines.push("## findings");
4222
5465
  lines.push("");
@@ -4242,33 +5485,10 @@ function buildTraceFindingPacket(opts) {
4242
5485
  kind: "traces.finding_packet",
4243
5486
  generatedAt,
4244
5487
  source: opts.source ?? "traces",
4245
- findings: opts.findings,
4246
- recommendations: buildRecommendations(opts.findings),
4247
- claims: buildClaims(opts.findings)
5488
+ findings: opts.findings
4248
5489
  };
4249
5490
  return { ...packet, report: renderFindingPacket(packet, opts.title) };
4250
5491
  }
4251
- function buildDefaultImprovementProposals(input) {
4252
- return input.recommendations.slice(0, 5).map((recommendation, index) => {
4253
- const matchingFindings = input.findings.filter((finding2) => recommendation.findingIds.includes(finding2.finding_id));
4254
- const evidenceRefs = recommendation.evidenceRefs.length > 0 ? recommendation.evidenceRefs : matchingFindings.flatMap((finding2) => finding2.evidence_refs);
4255
- return {
4256
- id: `proposal-${index + 1}-${slug(recommendation.title)}`,
4257
- title: recommendation.title,
4258
- description: [
4259
- recommendation.action,
4260
- "",
4261
- `Why: ${recommendation.rationale}`,
4262
- `Validation: ${recommendation.validationPlan}`,
4263
- "",
4264
- "This is a proposal-only artifact. It does not mutate code, prompts, skills, tools, MCP config, hooks, subagents, or external systems until a human applies it and reruns validation."
4265
- ].join("\n"),
4266
- recommendationIds: [recommendation.id],
4267
- validationCommand: "traces improve --last 1 --dir .traces/improvement",
4268
- evidenceRefs
4269
- };
4270
- });
4271
- }
4272
5492
  function renderExternal(results) {
4273
5493
  if (results.length === 0) return "";
4274
5494
  const lines = ["## external analyzers", ""];
@@ -4284,11 +5504,13 @@ function renderInvestigationReport(result, analystResult) {
4284
5504
  const base = `${renderReport({ ...analystResult, findings: [...result.findings] }, {
4285
5505
  harness: result.harness,
4286
5506
  sessionCount: result.sessionCount,
5507
+ unassignedTraceCount: result.unassignedTraceCount,
4287
5508
  spanCount: result.spanCount,
4288
5509
  otlpPath: result.otlpPath,
4289
- deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions)
5510
+ execution: result.execution,
5511
+ deterministic: summarizeDeterministicSignals(result.pipelines, result.reactions),
5512
+ sources: result.sources
4290
5513
  })}
4291
- ${renderRecommendations(result.recommendations)}
4292
5514
  ${renderPipelines(result.pipelines)}
4293
5515
  ${renderReactions(result.reactions)}
4294
5516
  ${renderAdoption(result.adoption)}`;
@@ -4365,19 +5587,18 @@ async function runTraceInvestigation(opts) {
4365
5587
  ...deterministicFindings(pipelines, reactions, adoption),
4366
5588
  ...normalizeExternalFindings(external)
4367
5589
  ];
4368
- const recommendations = buildRecommendations(findings);
4369
- const claims = buildClaims(findings);
4370
5590
  const partial = {
4371
5591
  schemaVersion: 1,
4372
5592
  kind: "traces.investigation",
4373
5593
  generatedAt,
4374
5594
  harness: opts.harness,
4375
- sessionCount: opts.sessionCount,
5595
+ sessionCount: adoption.identifiedSessionCount,
5596
+ unassignedTraceCount: adoption.unassignedTraceCount,
5597
+ sources: opts.sources,
4376
5598
  spanCount: opts.spans.length,
4377
5599
  otlpPath: analysis.otlpPath,
5600
+ execution: analysis.execution,
4378
5601
  findings,
4379
- recommendations,
4380
- claims,
4381
5602
  pipelines,
4382
5603
  reactions,
4383
5604
  adoption,
@@ -4409,70 +5630,38 @@ async function runTraceStoreInvestigation(opts) {
4409
5630
  analystResult
4410
5631
  };
4411
5632
  }
4412
- function replayProof(result, proposals) {
4413
- const deterministic = summarizeDeterministicSignals(result.pipelines, result.reactions);
4414
- return {
4415
- schemaVersion: 1,
4416
- kind: "traces.improvement_replay",
4417
- generatedAt: result.generatedAt,
4418
- status: proposals.length > 0 ? "proposal-only" : "not-run",
4419
- candidateApplied: false,
4420
- baseline: {
4421
- spanCount: result.spanCount,
4422
- findingCount: result.findings.length,
4423
- recommendationCount: result.recommendations.length,
4424
- stuckLoops: deterministic.stuckLoops,
4425
- reactionSignals: deterministic.reactionSignals,
4426
- toolErrorRuns: deterministic.toolErrorRuns
4427
- },
4428
- proposals,
4429
- note: proposals.length > 0 ? "traces produced reviewable proposals only; apply one and rerun traces on a comparable window to populate after metrics." : "no proposal adapter was configured, so no candidate was applied; recommendations remain reviewable guidance."
4430
- };
4431
- }
4432
5633
  async function writeTraceImprovementArtifacts(result, outDir) {
4433
5634
  const directory = outDir ? resolve(outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
4434
5635
  await mkdir(directory, { recursive: true });
4435
5636
  const paths = {
4436
5637
  directory,
4437
- findings: join16(directory, "findings.json"),
4438
- recommendations: join16(directory, "recommendations.json"),
4439
- proposals: join16(directory, "proposals.json"),
5638
+ result: join16(directory, "result.json"),
4440
5639
  evidence: join16(directory, "evidence.jsonl"),
4441
- claims: join16(directory, "claims.json"),
4442
5640
  report: join16(directory, "report.md"),
4443
- replay: join16(directory, "replay-before-after.json")
5641
+ traces: result.otlpPath
4444
5642
  };
5643
+ const { report, ...machineResult } = result;
4445
5644
  await Promise.all([
4446
- writeFile4(paths.findings, json(result.findings), "utf8"),
4447
- writeFile4(paths.recommendations, json(result.recommendations), "utf8"),
4448
- writeFile4(paths.proposals, json(result.proposals), "utf8"),
5645
+ writeFile4(paths.result, json(machineResult), "utf8"),
4449
5646
  writeFile4(paths.evidence, jsonl(buildEvidenceRows(result.findings)), "utf8"),
4450
- writeFile4(paths.claims, json(result.claims), "utf8"),
4451
- writeFile4(paths.report, result.report, "utf8"),
4452
- writeFile4(paths.replay, json(result.replay), "utf8")
5647
+ writeFile4(paths.report, report, "utf8")
4453
5648
  ]);
4454
5649
  return paths;
4455
5650
  }
4456
- async function runTraceImprovementLoop(opts) {
4457
- const investigation = await runTraceInvestigation(opts);
4458
- const adapterInput = {
4459
- findings: investigation.findings,
4460
- recommendations: investigation.recommendations,
4461
- claims: investigation.claims,
4462
- spans: opts.spans,
4463
- otlpPath: investigation.otlpPath
4464
- };
4465
- const proposals = opts.adapter ? await opts.adapter.propose(adapterInput) : buildDefaultImprovementProposals(adapterInput);
4466
- const replay = replayProof(investigation, proposals);
5651
+ async function runTraceImprovement(opts) {
5652
+ const directory = opts.outDir ? resolve(opts.outDir) : await mkdtemp4(join16(tmpdir4(), "traces-improvement-"));
5653
+ await mkdir(directory, { recursive: true });
5654
+ const investigation = await runTraceInvestigation({
5655
+ ...opts,
5656
+ otlpOutPath: opts.otlpOutPath ?? join16(directory, "traces.otlp.jsonl")
5657
+ });
4467
5658
  const result = {
4468
5659
  ...investigation,
4469
- kind: "traces.improvement",
4470
- proposals,
4471
- replay
5660
+ kind: "traces.improvement"
4472
5661
  };
4473
5662
  return {
4474
5663
  ...result,
4475
- artifacts: await writeTraceImprovementArtifacts(result, opts.outDir)
5664
+ artifacts: await writeTraceImprovementArtifacts(result, directory)
4476
5665
  };
4477
5666
  }
4478
5667
  async function saveReport(path, report) {
@@ -4481,7 +5670,7 @@ async function saveReport(path, report) {
4481
5670
  }
4482
5671
 
4483
5672
  // src/live.ts
4484
- import { createHash as createHash2 } from "crypto";
5673
+ import { createHash as createHash5 } from "crypto";
4485
5674
  var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
4486
5675
  var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
4487
5676
  var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
@@ -4501,7 +5690,7 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
4501
5690
  signal?.addEventListener("abort", onAbort, { once: true });
4502
5691
  });
4503
5692
  function hash(parts) {
4504
- return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
5693
+ return createHash5("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
4505
5694
  }
4506
5695
  function asString(value) {
4507
5696
  if (typeof value === "string" && value.trim()) return value;
@@ -4955,7 +6144,7 @@ async function watchSessions(opts) {
4955
6144
  }
4956
6145
 
4957
6146
  // src/session-index.ts
4958
- import { mkdtemp as mkdtemp5, readdir as readdir11, readFile as readFile14, stat as stat12, writeFile as writeFile5 } from "fs/promises";
6147
+ import { mkdtemp as mkdtemp5, readdir as readdir12, readFile as readFile5, stat as stat12, writeFile as writeFile5 } from "fs/promises";
4959
6148
  import { tmpdir as tmpdir5 } from "os";
4960
6149
  import { dirname as dirname3, join as join17 } from "path";
4961
6150
  function indexRow(record) {
@@ -4977,7 +6166,8 @@ function indexRow(record) {
4977
6166
  },
4978
6167
  models: record.metrics.models,
4979
6168
  tools: record.metrics.tools,
4980
- signals: record.signals
6169
+ signals: record.signals,
6170
+ execution: record.execution
4981
6171
  };
4982
6172
  }
4983
6173
  function uniqueSorted(values) {
@@ -5072,7 +6262,7 @@ async function summarizeFile(path, kind) {
5072
6262
  const base = { kind, path, bytes: Number(s.size), mtimeMs: Number(s.mtimeMs) };
5073
6263
  if (!path.endsWith(".md") && !path.endsWith(".jsonl")) return base;
5074
6264
  try {
5075
- const text = await readFile14(path, "utf8");
6265
+ const text = await readFile5(path, "utf8");
5076
6266
  const lines = countLines(text);
5077
6267
  const jsonl2 = path.endsWith(".jsonl") ? summarizeJsonl(text) : void 0;
5078
6268
  return {
@@ -5091,7 +6281,7 @@ async function walkMarkdown(root, kind, maxFiles = 100) {
5091
6281
  if (out.length >= maxFiles) return;
5092
6282
  let entries;
5093
6283
  try {
5094
- entries = await readdir11(dir, { withFileTypes: true });
6284
+ entries = await readdir12(dir, { withFileTypes: true });
5095
6285
  } catch {
5096
6286
  return;
5097
6287
  }
@@ -5177,10 +6367,10 @@ async function writeSessionIndexFile(index, outPath) {
5177
6367
  }
5178
6368
 
5179
6369
  // src/upload-state.ts
5180
- import { createHash as createHash3 } from "crypto";
5181
- import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
6370
+ import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
5182
6371
  import { homedir as homedir11 } from "os";
5183
6372
  import { dirname as dirname4, join as join18 } from "path";
6373
+ import { contentHash } from "@tangle-network/agent-eval";
5184
6374
  function stateFile() {
5185
6375
  const base = process.env.XDG_STATE_HOME ?? join18(homedir11(), ".local", "state");
5186
6376
  return join18(base, "tangle-traces", "uploaded.json");
@@ -5189,16 +6379,14 @@ function uploadKey(harness, sessionId) {
5189
6379
  return `${harness}:${sessionId}`;
5190
6380
  }
5191
6381
  function sessionHash(spans) {
5192
- const h = createHash3("sha256");
5193
- for (const s of spans) {
5194
- h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
5195
- `);
5196
- }
5197
- return h.digest("hex").slice(0, 32);
6382
+ return contentHash(spans).slice(0, 32);
6383
+ }
6384
+ function outboundHash(events, identity) {
6385
+ return contentHash({ events, identity }).slice(0, 32);
5198
6386
  }
5199
6387
  async function loadState() {
5200
6388
  try {
5201
- return JSON.parse(await readFile15(stateFile(), "utf8"));
6389
+ return JSON.parse(await readFile6(stateFile(), "utf8"));
5202
6390
  } catch {
5203
6391
  return {};
5204
6392
  }
@@ -5214,7 +6402,7 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
5214
6402
  }
5215
6403
 
5216
6404
  // src/upload.ts
5217
- import { readFile as readFile16 } from "fs/promises";
6405
+ import { readFile as readFile7 } from "fs/promises";
5218
6406
  import { createRequire } from "module";
5219
6407
  import { hostname } from "os";
5220
6408
  import { basename as basename9, join as join19 } from "path";
@@ -5233,7 +6421,7 @@ async function planUpload(opts = {}) {
5233
6421
  spans,
5234
6422
  redaction: report,
5235
6423
  hash: hash2,
5236
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
6424
+ isNew: true
5237
6425
  });
5238
6426
  }
5239
6427
  return { items, state, sinceMs: opts.sinceMs };
@@ -5241,19 +6429,18 @@ async function planUpload(opts = {}) {
5241
6429
  async function gitBranch(cwd) {
5242
6430
  if (!cwd) return void 0;
5243
6431
  try {
5244
- const head = await readFile16(join19(cwd, ".git", "HEAD"), "utf8");
6432
+ const head = await readFile7(join19(cwd, ".git", "HEAD"), "utf8");
5245
6433
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
5246
6434
  return m?.[1]?.trim();
5247
6435
  } catch {
5248
6436
  return void 0;
5249
6437
  }
5250
6438
  }
5251
- async function sessionMeta(item, uploadedAt) {
6439
+ async function sessionMeta(item) {
5252
6440
  const meta = {
5253
6441
  [ATTR.HARNESS]: item.ref.harness,
5254
6442
  [ATTR.SESSION_FILE]: basename9(item.ref.path),
5255
6443
  [ATTR.HOST]: hostname(),
5256
- [ATTR.UPLOADED_AT]: uploadedAt,
5257
6444
  [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
5258
6445
  [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
5259
6446
  [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
@@ -5287,25 +6474,69 @@ function toTraceSpanEvents(spans, rootMeta) {
5287
6474
  };
5288
6475
  });
5289
6476
  }
6477
+ var PartialUploadError = class extends Error {
6478
+ sessionKey;
6479
+ accepted;
6480
+ expected;
6481
+ constructor(sessionKey, accepted, expected) {
6482
+ super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
6483
+ this.name = "PartialUploadError";
6484
+ this.sessionKey = sessionKey;
6485
+ this.accepted = accepted;
6486
+ this.expected = expected;
6487
+ }
6488
+ };
5290
6489
  function stripSpanContent(spans) {
5291
6490
  return spans.map((s) => {
5292
- if (s.attributes["content"] == null) return s;
5293
- const { content: _drop, ...attributes } = s.attributes;
5294
- return { ...s, attributes };
6491
+ const attributes = { ...s.attributes };
6492
+ delete attributes.content;
6493
+ for (const key of TOOL_IO_VALUE_KEYS) delete attributes[key];
6494
+ normalizeToolIoAttributes(attributes);
6495
+ const status = s.status.message ? { code: s.status.code } : s.status;
6496
+ return { ...s, attributes, status };
5295
6497
  });
5296
6498
  }
5297
- async function executeUpload(plan, opts = {}) {
5298
- let newItems = plan.items.filter((i) => i.isNew);
6499
+ async function prepareOutbound(plan, opts, uploadedAt) {
6500
+ let candidates = plan.items;
5299
6501
  if (opts.redactor && !opts.stripContent) {
5300
- const r = opts.redactor;
5301
- newItems = await Promise.all(newItems.map(async (i) => ({ ...i, spans: (await applyRedactor(i.spans, r)).spans })));
6502
+ const redactor = opts.redactor;
6503
+ candidates = await Promise.all(
6504
+ candidates.map(async (item) => ({ ...item, spans: (await applyRedactor(item.spans, redactor)).spans }))
6505
+ );
5302
6506
  }
5303
- if (opts.stripContent) newItems = newItems.map((i) => ({ ...i, spans: stripSpanContent(i.spans) }));
5304
- const skipped = plan.items.length - newItems.length;
5305
- const redactionCount = newItems.reduce((n, i) => n + i.redaction.redactionCount, 0);
6507
+ if (opts.stripContent) {
6508
+ candidates = candidates.map((item) => ({ ...item, spans: stripSpanContent(item.spans) }));
6509
+ }
6510
+ const identity = {
6511
+ stripContent: opts.stripContent === true,
6512
+ redactor: opts.stripContent ? null : opts.redactor?.name ?? null,
6513
+ redactionVersion: REDACTION_VERSION
6514
+ };
6515
+ const items = [];
6516
+ for (const item of candidates) {
6517
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
6518
+ const previous = plan.state[key];
6519
+ const meta = await sessionMeta(item);
6520
+ const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
6521
+ let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
6522
+ let events = eventsAt(eventTimestamp2);
6523
+ let hash2 = outboundHash(events, identity);
6524
+ if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash2)) continue;
6525
+ if (eventTimestamp2 !== uploadedAt) {
6526
+ eventTimestamp2 = uploadedAt;
6527
+ events = eventsAt(eventTimestamp2);
6528
+ hash2 = outboundHash(events, identity);
6529
+ }
6530
+ items.push({ item, events, hash: hash2 });
6531
+ }
6532
+ return { items, skipped: plan.items.length - items.length };
6533
+ }
6534
+ async function executeUpload(plan, opts = {}) {
5306
6535
  const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
6536
+ const { items, skipped } = await prepareOutbound(plan, opts, uploadedAt);
6537
+ const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
5307
6538
  if (opts.dryRun) {
5308
- const allSpans = newItems.flatMap((i) => i.spans);
6539
+ const allSpans = items.flatMap(({ item }) => item.spans);
5309
6540
  const path = await writeOtlpFile(allSpans, opts.otlpOut);
5310
6541
  return {
5311
6542
  uploadedSessions: 0,
@@ -5316,6 +6547,15 @@ async function executeUpload(plan, opts = {}) {
5316
6547
  otlpPath: path
5317
6548
  };
5318
6549
  }
6550
+ if (items.length === 0) {
6551
+ return {
6552
+ uploadedSessions: 0,
6553
+ skippedSessions: skipped,
6554
+ acceptedSpans: 0,
6555
+ redactionCount,
6556
+ dryRun: false
6557
+ };
6558
+ }
5319
6559
  const client = opts.backend ?? hostedClientFromEnv();
5320
6560
  if (!client) {
5321
6561
  throw new Error(
@@ -5324,23 +6564,28 @@ async function executeUpload(plan, opts = {}) {
5324
6564
  }
5325
6565
  const state = plan.state;
5326
6566
  let acceptedSpans = 0;
5327
- for (const item of newItems) {
5328
- const meta = await sessionMeta(item, uploadedAt);
5329
- const events = toTraceSpanEvents(item.spans, meta);
5330
- const idempotencyKey = `${uploadKey(item.ref.harness, item.ref.sessionId)}:${item.hash}`;
6567
+ const completed = [];
6568
+ for (const outbound of items) {
6569
+ const { item, events, hash: hash2 } = outbound;
6570
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
6571
+ const idempotencyKey = `${key}:${hash2}`;
5331
6572
  const res = await client.ingestTraces(events, idempotencyKey);
6573
+ if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
5332
6574
  acceptedSpans += res.accepted;
5333
- state[uploadKey(item.ref.harness, item.ref.sessionId)] = {
5334
- hash: item.hash,
6575
+ completed.push({ key, item, hash: hash2, spanCount: events.length });
6576
+ opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
6577
+ }
6578
+ for (const { key, item, hash: hash2, spanCount } of completed) {
6579
+ state[key] = {
6580
+ hash: hash2,
5335
6581
  uploadedAt,
5336
6582
  harness: item.ref.harness,
5337
- spanCount: item.spans.length
6583
+ spanCount
5338
6584
  };
5339
- opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
5340
6585
  }
5341
6586
  await saveState(state);
5342
6587
  return {
5343
- uploadedSessions: newItems.length,
6588
+ uploadedSessions: items.length,
5344
6589
  skippedSessions: skipped,
5345
6590
  acceptedSpans,
5346
6591
  redactionCount,
@@ -5349,25 +6594,36 @@ async function executeUpload(plan, opts = {}) {
5349
6594
  }
5350
6595
 
5351
6596
  export {
5352
- countSkillRunsJsonl,
5353
- analyzeAdoption,
5354
6597
  span,
5355
6598
  toOpenInferenceSpan,
5356
6599
  serializeSpans,
5357
6600
  writeOtlpFile,
5358
- analyzeSpans,
6601
+ CONTENT_CAP,
6602
+ capText,
6603
+ ACTOR_ATTR,
6604
+ userPromptSpan,
6605
+ appendAll,
5359
6606
  ATTR,
5360
6607
  INGEST_SOURCE_CLI,
5361
6608
  DEFAULT_HARNESS,
6609
+ sessionIdFromAttributes,
6610
+ indexSessionIdsByTrace,
6611
+ stampSessionIdentity,
6612
+ summarizeOtlpExecution,
6613
+ summarizeSpanExecution,
5362
6614
  parseIsoToEpochMs,
5363
6615
  parseSince,
5364
6616
  toRuntimeStore,
5365
6617
  runPipelines,
5366
- CONTENT_CAP,
5367
- capText,
5368
- ACTOR_ATTR,
5369
- userPromptSpan,
6618
+ recordSessionCorruption,
6619
+ sessionIntegrityAttributes,
6620
+ stampSessionIntegrity,
6621
+ JsonSourceError,
6622
+ readJsonFile,
5370
6623
  AmpAdapter,
6624
+ JsonlParseError,
6625
+ readJsonl,
6626
+ takeJsonl,
5371
6627
  textIsSynthetic,
5372
6628
  looksLikeAgentPrompt,
5373
6629
  claudeActor,
@@ -5395,6 +6651,7 @@ export {
5395
6651
  stampRepoAttrs,
5396
6652
  stampSpanWorkdirRepoAttrs,
5397
6653
  parseSession,
6654
+ EmptySessionError,
5398
6655
  locateSessions,
5399
6656
  scanSessions,
5400
6657
  buildPolicyEvidenceRecord,
@@ -5419,20 +6676,19 @@ export {
5419
6676
  serializeInspectionReport,
5420
6677
  readSessionIndexFile,
5421
6678
  writeInspectionReportFile,
6679
+ countSkillRunsJsonl,
6680
+ analyzeAdoption,
6681
+ analyzeSpans,
5422
6682
  REACTIONS,
5423
6683
  analyzeReactions,
5424
- summarizeDeterministicSignals,
5425
- renderReport,
5426
- renderPipelines,
5427
- renderReactions,
5428
- renderAdoption,
6684
+ CORRUPTION_RECEIPT_DISPLAY_LIMIT,
5429
6685
  buildTraceFindingPacket,
5430
6686
  loadTracesConfig,
5431
6687
  mergeTracesConfig,
5432
6688
  runTraceInvestigation,
5433
6689
  runTraceStoreInvestigation,
5434
6690
  writeTraceImprovementArtifacts,
5435
- runTraceImprovementLoop,
6691
+ runTraceImprovement,
5436
6692
  saveReport,
5437
6693
  classifyLiveActions,
5438
6694
  defaultTraceLiveAnalysts,
@@ -5448,10 +6704,12 @@ export {
5448
6704
  writeSessionIndexFile,
5449
6705
  uploadKey,
5450
6706
  sessionHash,
6707
+ outboundHash,
5451
6708
  loadState,
5452
6709
  saveState,
5453
6710
  alreadyUploaded,
5454
6711
  planUpload,
5455
6712
  toTraceSpanEvents,
6713
+ PartialUploadError,
5456
6714
  executeUpload
5457
6715
  };