@tangle-network/traces 0.9.0 → 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,399 +1,3 @@
1
- // src/attributes.ts
2
- var ATTR = {
3
- /** = the trace/session id. Wire contract (dedup linkage). */
4
- SESSION_ID: "tangle.sessionId",
5
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
6
- * that also streamed live. Wire contract. */
7
- INGEST_SOURCE: "tangle.ingest_source",
8
- HARNESS: "tangle.harness",
9
- CWD: "tangle.cwd",
10
- /** THE per-session grouping key the spine derives subjects from
11
- * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
12
- * remote (host/owner/repo) when readable, else the cwd path basename. */
13
- SUBJECT_KEY: "tangle.subject.key",
14
- /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
15
- GIT_REPOSITORY: "git.repository",
16
- /** Current branch at conversion time. */
17
- GIT_BRANCH_NAME: "git.branch",
18
- /** HEAD short sha at conversion time. */
19
- GIT_COMMIT: "git.commit",
20
- /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
21
- REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
22
- GIT_BRANCH: "tangle.git_branch",
23
- HOST: "tangle.host",
24
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
25
- * which collided with `tangle.ingest_source`.) */
26
- SESSION_FILE: "tangle.session_file",
27
- UPLOADED_AT: "tangle.uploaded_at",
28
- UPLOADER: "tangle.uploader",
29
- REDACTION_VERSION: "redaction.version",
30
- REDACTION_COUNT: "redaction.count",
31
- /** Upload contract: bounded root summary plus location-only receipt child spans.
32
- * Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
33
- SESSION_INTEGRITY: "traces.session.integrity",
34
- CORRUPTION_COUNT: "traces.session.corruption_count",
35
- CORRUPTION_DIGEST: "traces.session.corruption_digest",
36
- CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
37
- CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
38
- CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
39
- CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
40
- CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
41
- CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
42
- CORRUPTION_SHA256: "traces.session.corruption.sha256",
43
- RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
44
- };
45
- var INGEST_SOURCE_CLI = "cli";
46
- var DEFAULT_HARNESS = "claude-code";
47
- var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
48
- function sessionIdFromAttributes(attributes) {
49
- return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes[key]).find(
50
- (value) => typeof value === "string" && value.length > 0
51
- );
52
- }
53
- function indexSessionIdsByTrace(spans) {
54
- const candidatesByTrace = /* @__PURE__ */ new Map();
55
- for (const span2 of spans) {
56
- const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
57
- for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
58
- const value = span2.attributes[key];
59
- if (typeof value === "string" && value.length > 0) candidates.add(value);
60
- }
61
- if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
62
- }
63
- const sessionByTrace = /* @__PURE__ */ new Map();
64
- const conflicts = [];
65
- for (const [traceId, candidates] of candidatesByTrace) {
66
- const sessionIds2 = [...candidates].sort();
67
- if (sessionIds2.length === 1) {
68
- sessionByTrace.set(traceId, sessionIds2[0]);
69
- } else {
70
- conflicts.push({ traceId, sessionIds: sessionIds2 });
71
- }
72
- }
73
- conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
74
- return { sessionByTrace, conflicts };
75
- }
76
- function stampSessionIdentity(spans, fallbackSessionId) {
77
- const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
78
- const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
79
- const traceIds = new Set(spans.map((span2) => span2.trace_id));
80
- const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
81
- for (const span2 of spans) {
82
- if (conflictingTraceIds.has(span2.trace_id)) continue;
83
- const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
84
- if (!sessionIdFromAttributes(span2.attributes)) {
85
- span2.attributes[ATTR.SESSION_ID] = sessionId;
86
- }
87
- }
88
- }
89
-
90
- // src/adoption.ts
91
- import { readFile } from "fs/promises";
92
- import { join } from "path";
93
-
94
- // src/adapters/tool-io.ts
95
- import { createHash } from "crypto";
96
- var TOOL_IO_VALUE_MAX_BYTES = 16 * 1024;
97
- var TOOL_IO_VALUE_KEYS = ["input.value", "output.value"];
98
- var TOOL_IO_SIDES = ["input", "output"];
99
- var LEGACY_TRUNCATION_MARKER = /\n\[truncated (\d+) bytes; sha256=[a-f0-9]{64}\]$/;
100
- var COUNTED_TRUNCATION_MARKER = /\n\[truncated \d+ bytes\]$/;
101
- var TRUNCATION_MARKER = /\n\[truncated\]$/;
102
- function canonicalJson(value) {
103
- const sort = (item) => {
104
- if (Array.isArray(item)) return item.map(sort);
105
- if (!item || typeof item !== "object") return item;
106
- return Object.fromEntries(
107
- Object.entries(item).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, entry]) => [key, sort(entry)])
108
- );
109
- };
110
- const json2 = JSON.stringify(sort(value));
111
- if (json2 === void 0) throw new TypeError("tool I/O value is not JSON-serializable");
112
- return json2;
113
- }
114
- function serialize(value) {
115
- if (value === void 0) return void 0;
116
- if (typeof value === "string") {
117
- const trimmed = value.trim();
118
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
119
- try {
120
- return { value: canonicalJson(JSON.parse(trimmed)), mimeType: "application/json" };
121
- } catch {
122
- }
123
- }
124
- return { value, mimeType: "text/plain" };
125
- }
126
- return { value: canonicalJson(value), mimeType: "application/json" };
127
- }
128
- function utf8Prefix(value, maxBytes) {
129
- let bytes = 0;
130
- let end = 0;
131
- for (const char of value) {
132
- const charBytes = Buffer.byteLength(char);
133
- if (bytes + charBytes > maxBytes) break;
134
- bytes += charBytes;
135
- end += char.length;
136
- }
137
- return Buffer.from(value.slice(0, end), "utf8").toString("utf8");
138
- }
139
- function truncatedValue(value, bytes) {
140
- const longestMarker = `
141
- [truncated ${bytes} bytes]`;
142
- const prefix = utf8Prefix(value, TOOL_IO_VALUE_MAX_BYTES - Buffer.byteLength(longestMarker));
143
- const omittedBytes = bytes - Buffer.byteLength(prefix);
144
- return `${prefix}
145
- [truncated ${omittedBytes} bytes]`;
146
- }
147
- function valueAttributes(side, raw) {
148
- const serialized = serialize(raw);
149
- if (!serialized) return {};
150
- const bytes = Buffer.byteLength(serialized.value);
151
- const sha256 = createHash("sha256").update(serialized.value).digest("hex");
152
- const truncated = bytes > TOOL_IO_VALUE_MAX_BYTES;
153
- const value = truncated ? truncatedValue(serialized.value, bytes) : serialized.value;
154
- return {
155
- [`${side}.value`]: value,
156
- [`${side}.mime_type`]: serialized.mimeType,
157
- [`traces.${side}.bytes`]: bytes,
158
- [`traces.${side}.sha256`]: sha256,
159
- [`traces.${side}.truncated`]: truncated
160
- };
161
- }
162
- function toolIoAttributes(io) {
163
- return {
164
- ...valueAttributes("input", io.input),
165
- ...valueAttributes("output", io.output),
166
- ...io.argsCaptured === void 0 ? {} : { "tool.args_captured": io.argsCaptured }
167
- };
168
- }
169
- function toolArgumentsFromAttributes(attributes) {
170
- const declared = attributes["tool.args_captured"];
171
- const explicit = typeof declared === "boolean" ? declared : declared === "true" ? true : declared === "false" ? false : void 0;
172
- const argsCaptured = explicit ?? attributes["input.value"] !== void 0;
173
- return {
174
- args: argsCaptured ? attributes["input.value"] : void 0,
175
- argsCaptured
176
- };
177
- }
178
- function normalizeToolIoAttributes(attributes) {
179
- for (const side of TOOL_IO_SIDES) {
180
- const valueKey = `${side}.value`;
181
- const mimeKey = `${side}.mime_type`;
182
- const bytesKey = `traces.${side}.bytes`;
183
- const digestKey = `traces.${side}.sha256`;
184
- const truncatedKey = `traces.${side}.truncated`;
185
- const rawValue = attributes[valueKey];
186
- if (typeof rawValue !== "string") {
187
- delete attributes[mimeKey];
188
- delete attributes[bytesKey];
189
- delete attributes[digestKey];
190
- delete attributes[truncatedKey];
191
- continue;
192
- }
193
- let value = rawValue.replace(LEGACY_TRUNCATION_MARKER, "\n[truncated]").replace(COUNTED_TRUNCATION_MARKER, "\n[truncated]");
194
- let truncated = TRUNCATION_MARKER.test(value);
195
- const unboundedBytes = Buffer.byteLength(value);
196
- if (unboundedBytes > TOOL_IO_VALUE_MAX_BYTES) {
197
- value = truncatedValue(value, unboundedBytes);
198
- truncated = true;
199
- }
200
- attributes[valueKey] = value;
201
- attributes[bytesKey] = Buffer.byteLength(value);
202
- attributes[digestKey] = createHash("sha256").update(value).digest("hex");
203
- attributes[truncatedKey] = truncated;
204
- }
205
- }
206
- function recordToolOutput(toolSpan, output) {
207
- if (!toolSpan) return;
208
- Object.assign(toolSpan.attributes, valueAttributes("output", output));
209
- }
210
-
211
- // src/adoption.ts
212
- function toolName(s) {
213
- const n = s.attributes["tool.name"];
214
- if (typeof n === "string") return n;
215
- if (s.name.startsWith("tool.")) return s.name.slice("tool.".length);
216
- return null;
217
- }
218
- function parseInput(s) {
219
- const captured = toolArgumentsFromAttributes(s.attributes);
220
- if (!captured.argsCaptured) return {};
221
- if (captured.args && typeof captured.args === "object") {
222
- return captured.args;
223
- }
224
- if (typeof captured.args !== "string") return {};
225
- try {
226
- const v = JSON.parse(captured.args);
227
- return v && typeof v === "object" ? v : {};
228
- } catch {
229
- return {};
230
- }
231
- }
232
- function skillNameOf(input) {
233
- const v = input.skill ?? input.command;
234
- return typeof v === "string" && v.length > 0 ? v : "?";
235
- }
236
- function subagentTypeOf(input) {
237
- const v = input.subagent_type ?? input.agent_type ?? input.type ?? input.name;
238
- return typeof v === "string" && v.length > 0 ? v : "?";
239
- }
240
- function isSpawnAgentTool(name) {
241
- return name === "spawn_agent" || name.endsWith("__spawn_agent");
242
- }
243
- function skillTelemetryCapability(service) {
244
- if (service === "claude-code") return "supported";
245
- if (service === "codex") return "unsupported";
246
- return "unknown";
247
- }
248
- var MATERIALIZED_SKILL_RE = /<skills_instructions>|(?:^|\n)#{1,3}\s+(?:available\s+)?skills\b|base directory for this skill:/i;
249
- function hasMaterializedSkillEvidence(s) {
250
- if (s.name !== "message.developer" && s.attributes["tangle.actor"] !== "injected") return false;
251
- const content = s.attributes.content;
252
- return typeof content === "string" && MATERIALIZED_SKILL_RE.test(content);
253
- }
254
- function hasSkillFileReference(s) {
255
- if (s.attributes["openinference.span.kind"] !== "TOOL") return false;
256
- const input = s.attributes["input.value"];
257
- return typeof input === "string" && input.includes("SKILL.md");
258
- }
259
- function telemetryStatus(measured, unsupported, unknown) {
260
- if (measured > 0 && unsupported === 0 && unknown === 0) return "measured";
261
- if (measured > 0) return "partial";
262
- if (unsupported > 0 && unknown === 0) return "unsupported";
263
- return "unknown";
264
- }
265
- function countSkillRunsJsonl(raw) {
266
- const out = {};
267
- for (const line of raw.split("\n")) {
268
- const t = line.trim();
269
- if (!t) continue;
270
- let o;
271
- try {
272
- o = JSON.parse(t);
273
- } catch {
274
- continue;
275
- }
276
- if (!o || typeof o !== "object") continue;
277
- const rec = o;
278
- const names = [];
279
- if (Array.isArray(rec.skills)) {
280
- for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
281
- }
282
- if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
283
- if (names.length === 0) names.push("?");
284
- for (const n of names) out[n] = (out[n] ?? 0) + 1;
285
- }
286
- return out;
287
- }
288
- async function readLoopRuns(cwds) {
289
- const counts = {};
290
- let filesRead = 0;
291
- const seen = /* @__PURE__ */ new Set();
292
- for (const cwd of cwds) {
293
- if (!cwd || seen.has(cwd)) continue;
294
- seen.add(cwd);
295
- const path = join(cwd, ".evolve", "skill-runs.jsonl");
296
- let raw;
297
- try {
298
- raw = await readFile(path, "utf8");
299
- } catch {
300
- continue;
301
- }
302
- filesRead += 1;
303
- for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
304
- }
305
- return { counts, filesRead };
306
- }
307
- async function analyzeAdoption(spans, opts = {}) {
308
- const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
309
- const identifiedSessions = new Set(sessionByTrace.values());
310
- const unassignedTraceIds = new Set(
311
- spans.map((span2) => span2.trace_id).filter((traceId) => !sessionByTrace.has(traceId))
312
- );
313
- const executionGroup = (span2) => sessionByTrace.get(span2.trace_id) ?? `trace:${span2.trace_id}`;
314
- const sessions = /* @__PURE__ */ new Set();
315
- const sessionsWithSkill = /* @__PURE__ */ new Set();
316
- const sessionsWithSubagent = /* @__PURE__ */ new Set();
317
- const sessionsWithMaterializedSkills = /* @__PURE__ */ new Set();
318
- const sessionsWithSkillFileReference = /* @__PURE__ */ new Set();
319
- const skillInvocations = {};
320
- const subagentSpawns = {};
321
- const canonicalSubagentSessions = /* @__PURE__ */ new Set();
322
- const fallbackSubagents = /* @__PURE__ */ new Map();
323
- const sessionCapabilities = /* @__PURE__ */ new Map();
324
- for (const s of spans) {
325
- const group = executionGroup(s);
326
- sessions.add(group);
327
- const observedCapability = skillTelemetryCapability(s.attributes["service.name"]);
328
- const currentCapability = sessionCapabilities.get(group) ?? "unknown";
329
- if (observedCapability === "supported" || currentCapability === "unknown") {
330
- sessionCapabilities.set(group, observedCapability);
331
- }
332
- if (hasMaterializedSkillEvidence(s)) sessionsWithMaterializedSkills.add(group);
333
- if (hasSkillFileReference(s)) sessionsWithSkillFileReference.add(group);
334
- const tn = toolName(s);
335
- if (tn === "Skill") {
336
- sessionCapabilities.set(group, "supported");
337
- const name = skillNameOf(parseInput(s));
338
- skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
339
- sessionsWithSkill.add(group);
340
- } else if (tn === "Task" || tn === "Agent") {
341
- const type = subagentTypeOf(parseInput(s));
342
- subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
343
- sessionsWithSubagent.add(group);
344
- canonicalSubagentSessions.add(group);
345
- } else if (tn && isSpawnAgentTool(tn)) {
346
- const types = fallbackSubagents.get(group) ?? [];
347
- types.push(subagentTypeOf(parseInput(s)));
348
- fallbackSubagents.set(group, types);
349
- }
350
- }
351
- for (const [traceId, types] of fallbackSubagents) {
352
- if (canonicalSubagentSessions.has(traceId)) continue;
353
- sessionsWithSubagent.add(traceId);
354
- for (const type of types) {
355
- subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
356
- }
357
- }
358
- const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
359
- const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
360
- const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
361
- const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
362
- let skillTelemetrySessions = 0;
363
- let skillTelemetryUnsupportedSessions = 0;
364
- let skillTelemetryUnknownSessions = 0;
365
- for (const traceId of sessions) {
366
- const capability = sessionCapabilities.get(traceId) ?? "unknown";
367
- if (capability === "supported") skillTelemetrySessions += 1;
368
- else if (capability === "unsupported") skillTelemetryUnsupportedSessions += 1;
369
- else skillTelemetryUnknownSessions += 1;
370
- }
371
- return {
372
- executionGroupCount: sessions.size,
373
- identifiedSessionCount: identifiedSessions.size,
374
- unassignedTraceCount: unassignedTraceIds.size,
375
- sessionIdentityConflicts: conflicts,
376
- sessionsWithSkill: sessionsWithSkill.size,
377
- skillPenetration: skillTelemetrySessions === 0 ? null : sessionsWithSkill.size / skillTelemetrySessions,
378
- skillTelemetryStatus: telemetryStatus(
379
- skillTelemetrySessions,
380
- skillTelemetryUnsupportedSessions,
381
- skillTelemetryUnknownSessions
382
- ),
383
- skillTelemetrySessions,
384
- sessionsWithMaterializedSkills: sessionsWithMaterializedSkills.size,
385
- sessionsWithSkillFileReference: sessionsWithSkillFileReference.size,
386
- skillInvocations,
387
- totalSkillInvocations,
388
- subagentSpawns,
389
- totalSubagentSpawns,
390
- sessionsWithSubagent: sessionsWithSubagent.size,
391
- loopDispatchedRuns,
392
- totalLoopDispatchedRuns,
393
- skillRunFilesRead: filesRead
394
- };
395
- }
396
-
397
1
  // src/otlp.ts
398
2
  import {
399
3
  applyLlmSpanOtlpAttributes,
@@ -454,18 +58,136 @@ function toOpenInferenceSpan(s) {
454
58
  attributes: a
455
59
  };
456
60
  }
457
- function serializeSpans(spans) {
458
- if (spans.length === 0) return "";
459
- return `${spans.map((s) => JSON.stringify(toOpenInferenceSpan(s))).join("\n")}
460
- `;
61
+ function serializeSpans(spans) {
62
+ if (spans.length === 0) return "";
63
+ return `${spans.map((s) => JSON.stringify(toOpenInferenceSpan(s))).join("\n")}
64
+ `;
65
+ }
66
+ async function writeOtlpFile(spans, outPath) {
67
+ const { mkdtemp: mkdtemp6, writeFile: writeFile7 } = await import("fs/promises");
68
+ const { tmpdir: tmpdir6 } = await import("os");
69
+ const { join: join20 } = await import("path");
70
+ const path = outPath ?? join20(await mkdtemp6(join20(tmpdir6(), "traces-")), "spans.otlp.jsonl");
71
+ await writeFile7(path, serializeSpans(spans), "utf8");
72
+ return path;
73
+ }
74
+
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);
102
+ }
103
+
104
+ // src/attributes.ts
105
+ var ATTR = {
106
+ /** = the trace/session id. Wire contract (dedup linkage). */
107
+ SESSION_ID: "tangle.sessionId",
108
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
109
+ * that also streamed live. Wire contract. */
110
+ INGEST_SOURCE: "tangle.ingest_source",
111
+ HARNESS: "tangle.harness",
112
+ CWD: "tangle.cwd",
113
+ /** THE per-session grouping key the spine derives subjects from
114
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
115
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
116
+ SUBJECT_KEY: "tangle.subject.key",
117
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
118
+ GIT_REPOSITORY: "git.repository",
119
+ /** Current branch at conversion time. */
120
+ GIT_BRANCH_NAME: "git.branch",
121
+ /** HEAD short sha at conversion time. */
122
+ GIT_COMMIT: "git.commit",
123
+ /** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
124
+ REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
125
+ GIT_BRANCH: "tangle.git_branch",
126
+ HOST: "tangle.host",
127
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
128
+ * which collided with `tangle.ingest_source`.) */
129
+ SESSION_FILE: "tangle.session_file",
130
+ UPLOADED_AT: "tangle.uploaded_at",
131
+ UPLOADER: "tangle.uploader",
132
+ REDACTION_VERSION: "redaction.version",
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"
147
+ };
148
+ var INGEST_SOURCE_CLI = "cli";
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 };
461
178
  }
462
- async function writeOtlpFile(spans, outPath) {
463
- const { mkdtemp: mkdtemp6, writeFile: writeFile7 } = await import("fs/promises");
464
- const { tmpdir: tmpdir6 } = await import("os");
465
- const { join: join20 } = await import("path");
466
- const path = outPath ?? join20(await mkdtemp6(join20(tmpdir6(), "traces-")), "spans.otlp.jsonl");
467
- await writeFile7(path, serializeSpans(spans), "utf8");
468
- return path;
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
+ }
469
191
  }
470
192
 
471
193
  // src/execution.ts
@@ -497,68 +219,6 @@ function summarizeSpanExecution(spans, opts = {}) {
497
219
  });
498
220
  }
499
221
 
500
- // src/analyze.ts
501
- import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
502
- import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
503
- var GENERATED_TRACE_FILE_CEILING = 512 * 1024 * 1024;
504
- async function analyzeSpans(spans, opts = {}) {
505
- if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
506
- const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
507
- const runId = opts.runId ?? `traces-${Date.now()}`;
508
- const execution = summarizeSpanExecution(spans, {
509
- experimentId: runId
510
- });
511
- const detStore = new OtlpFileTraceStore({
512
- path: otlpPath,
513
- maxFileBytes: GENERATED_TRACE_FILE_CEILING,
514
- perCallByteCeiling: GENERATED_TRACE_FILE_CEILING
515
- });
516
- await detStore.ensureIndexed();
517
- const detRegistry = opts.registry ?? buildDefaultAnalystRegistry({ registry: { log: opts.log } });
518
- const result = await detRegistry.run(runId, { traceStore: detStore });
519
- if (opts.ai) {
520
- const agStore = new OtlpFileTraceStore({ path: otlpPath, maxFileBytes: GENERATED_TRACE_FILE_CEILING });
521
- await agStore.ensureIndexed();
522
- const agRegistry = buildDefaultAnalystRegistry({
523
- ai: opts.ai,
524
- model: opts.model,
525
- includeBehavioral: false,
526
- registry: { log: opts.log }
527
- });
528
- const agResult = await agRegistry.run(runId, { traceStore: agStore }, {
529
- budget: opts.budgetUsd != null ? { totalUsd: opts.budgetUsd } : void 0
530
- });
531
- result.findings.push(...agResult.findings);
532
- result.per_analyst.push(...agResult.per_analyst);
533
- result.total_cost_usd += agResult.total_cost_usd;
534
- }
535
- return { otlpPath, execution, result };
536
- }
537
-
538
- // src/adapters/conversation.ts
539
- var CONTENT_CAP = 8e3;
540
- function capText(raw) {
541
- const t = raw.trim();
542
- if (t.length <= CONTENT_CAP) return t;
543
- return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
544
- }
545
- var ACTOR_ATTR = "tangle.actor";
546
- function userPromptSpan(o) {
547
- return span({
548
- traceId: o.traceId,
549
- spanId: o.spanId,
550
- parentSpanId: o.parentSpanId,
551
- name: "user.prompt",
552
- kind: "CHAIN",
553
- startTime: o.startTime,
554
- service: o.service ?? null,
555
- agent: o.agent ?? null,
556
- step: o.step,
557
- content: o.content,
558
- extra: { [ACTOR_ATTR]: o.actor ?? "human" }
559
- });
560
- }
561
-
562
222
  // src/time.ts
563
223
  function parseIsoToEpochMs(ts) {
564
224
  if (!ts) return 0;
@@ -580,6 +240,125 @@ function parseSince(value) {
580
240
 
581
241
  // src/runtime-store.ts
582
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
583
362
  var KIND = {
584
363
  AGENT: "agent",
585
364
  LLM: "llm",
@@ -766,7 +545,7 @@ function stampSessionIntegrity(ref, spans) {
766
545
  }
767
546
 
768
547
  // src/json.ts
769
- import { readdir, readFile as readFile2 } from "fs/promises";
548
+ import { readdir, readFile } from "fs/promises";
770
549
  var JsonSourceError = class extends Error {
771
550
  sourcePath;
772
551
  kind;
@@ -801,7 +580,7 @@ function isMissingJsonSource(error) {
801
580
  async function readJsonFile(path) {
802
581
  let raw;
803
582
  try {
804
- raw = await readFile2(path, "utf8");
583
+ raw = await readFile(path, "utf8");
805
584
  } catch (error) {
806
585
  throw new JsonSourceError(path, "read", error);
807
586
  }
@@ -822,7 +601,7 @@ async function listJsonFiles(path) {
822
601
  // src/adapters/amp.ts
823
602
  import { readdir as readdir2, stat } from "fs/promises";
824
603
  import { homedir } from "os";
825
- import { basename, join as join2 } from "path";
604
+ import { basename, join } from "path";
826
605
  var SERVICE = "amp";
827
606
  function textOf(content) {
828
607
  if (typeof content === "string") return content.slice(0, CONTENT_CAP);
@@ -833,8 +612,8 @@ function textOf(content) {
833
612
  var AmpAdapter = class {
834
613
  harness = "amp";
835
614
  threadsRoot() {
836
- const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
837
- return join2(base, "amp", "threads");
615
+ const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
616
+ return join(base, "amp", "threads");
838
617
  }
839
618
  async locate(opts = {}) {
840
619
  let files;
@@ -847,7 +626,7 @@ var AmpAdapter = class {
847
626
  const refs = [];
848
627
  for (const f of files) {
849
628
  if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
850
- const path = join2(this.threadsRoot(), f);
629
+ const path = join(this.threadsRoot(), f);
851
630
  let st;
852
631
  try {
853
632
  st = await stat(path);
@@ -1068,7 +847,7 @@ var INJECT_MARKERS = [
1068
847
  // Prefix match: Codex adds attributes such as source="goal" to this tag.
1069
848
  "<codex_internal_context",
1070
849
  "<subagent_notification>",
1071
- "# AGENTS.md instructions for ",
850
+ "# AGENTS.md instructions",
1072
851
  "<local-command-stdout>",
1073
852
  "This session is being continued from a previous conversation",
1074
853
  // Slash-command skill bodies are expanded into a user turn (no <command-name>
@@ -1143,7 +922,7 @@ var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
1143
922
  // src/adapters/claude.ts
1144
923
  import { readdir as readdir3, stat as stat2 } from "fs/promises";
1145
924
  import { homedir as homedir2 } from "os";
1146
- import { basename as basename2, join as join3 } from "path";
925
+ import { basename as basename2, join as join2 } from "path";
1147
926
  var SERVICE2 = "claude-code";
1148
927
  function asBlocks(content) {
1149
928
  return Array.isArray(content) ? content : [];
@@ -1265,7 +1044,7 @@ var ClaudeAdapter = class {
1265
1044
  harness = "claude-code";
1266
1045
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
1267
1046
  root() {
1268
- return join3(homedir2(), ".claude", "projects");
1047
+ return join2(homedir2(), ".claude", "projects");
1269
1048
  }
1270
1049
  async locate(opts = {}) {
1271
1050
  const root = this.root();
@@ -1278,7 +1057,7 @@ var ClaudeAdapter = class {
1278
1057
  }
1279
1058
  const refs = [];
1280
1059
  for (const dir of projectDirs) {
1281
- const dirPath = join3(root, dir);
1060
+ const dirPath = join2(root, dir);
1282
1061
  let files;
1283
1062
  try {
1284
1063
  files = await readdir3(dirPath);
@@ -1288,7 +1067,7 @@ var ClaudeAdapter = class {
1288
1067
  }
1289
1068
  for (const f of files) {
1290
1069
  if (!f.endsWith(".jsonl")) continue;
1291
- const path = join3(dirPath, f);
1070
+ const path = join2(dirPath, f);
1292
1071
  let st;
1293
1072
  try {
1294
1073
  st = await stat2(path);
@@ -1347,15 +1126,15 @@ var ClaudeAdapter = class {
1347
1126
  endTime: lastTimestamp,
1348
1127
  service: SERVICE2,
1349
1128
  agent: SERVICE2
1350
- }),
1351
- ...main.spans
1129
+ })
1352
1130
  ];
1131
+ appendAll(spans, main.spans);
1353
1132
  await this.foldSubagents(ref, traceId, main, spans, options);
1354
1133
  return spans;
1355
1134
  }
1356
1135
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
1357
1136
  async foldSubagents(ref, traceId, main, out, options) {
1358
- const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
1137
+ const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
1359
1138
  let files;
1360
1139
  try {
1361
1140
  files = await readdir3(subDir);
@@ -1367,7 +1146,7 @@ var ClaudeAdapter = class {
1367
1146
  for (const file of files) {
1368
1147
  if (!file.endsWith(".jsonl")) continue;
1369
1148
  const hash2 = basename2(file, ".jsonl");
1370
- const metaPath = join3(subDir, `${hash2}.meta.json`);
1149
+ const metaPath = join2(subDir, `${hash2}.meta.json`);
1371
1150
  let meta = {};
1372
1151
  try {
1373
1152
  meta = await readJsonFile(metaPath);
@@ -1384,13 +1163,13 @@ var ClaudeAdapter = class {
1384
1163
  };
1385
1164
  const state = createClaudeStream(ctx.startStep);
1386
1165
  for await (const event of readJsonl(
1387
- join3(subDir, file),
1166
+ join2(subDir, file),
1388
1167
  sessionJsonlOptions(ref, options)
1389
1168
  )) {
1390
1169
  consumeClaudeEvent(event, ctx, state);
1391
1170
  }
1392
1171
  const parsed = finishClaudeStream(state);
1393
- out.push(...parsed.spans);
1172
+ appendAll(out, parsed.spans);
1394
1173
  step = parsed.nextStep;
1395
1174
  }
1396
1175
  }
@@ -1399,7 +1178,7 @@ var ClaudeAdapter = class {
1399
1178
  // src/adapters/codex.ts
1400
1179
  import { readdir as readdir4, stat as stat3 } from "fs/promises";
1401
1180
  import { homedir as homedir3 } from "os";
1402
- import { basename as basename3, join as join4 } from "path";
1181
+ import { basename as basename3, join as join3 } from "path";
1403
1182
  var SERVICE3 = "codex";
1404
1183
  var SESSION_HEAD_LINES = 40;
1405
1184
  function contentToString(content) {
@@ -1407,15 +1186,87 @@ function contentToString(content) {
1407
1186
  if (Array.isArray(content)) {
1408
1187
  return content.map((c) => c && typeof c === "object" && "text" in c ? String(c.text ?? "") : "").join("");
1409
1188
  }
1410
- return "";
1411
- }
1412
- function textOf3(content) {
1413
- return capText(contentToString(content));
1189
+ return "";
1190
+ }
1191
+ function textOf3(content) {
1192
+ return capText(contentToString(content));
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;
1414
1265
  }
1415
1266
  function outputIsError(output) {
1416
- const s = typeof output === "string" ? output : JSON.stringify(output ?? "");
1417
- const error = /"success"\s*:\s*false|exit(?:_| )code["\s:]+[1-9]|\bcommand failed\b|\bENOENT\b|\berror:/i.test(s);
1418
- 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) : "" };
1419
1270
  }
1420
1271
  function singleNestedToolName(input) {
1421
1272
  if (!input) return null;
@@ -1475,7 +1326,7 @@ async function* walkRollouts(root) {
1475
1326
  throw error;
1476
1327
  }
1477
1328
  for (const y of years) {
1478
- const yp = join4(root, y);
1329
+ const yp = join3(root, y);
1479
1330
  let months;
1480
1331
  try {
1481
1332
  months = await readdir4(yp);
@@ -1484,7 +1335,7 @@ async function* walkRollouts(root) {
1484
1335
  throw error;
1485
1336
  }
1486
1337
  for (const m of months) {
1487
- const mp = join4(yp, m);
1338
+ const mp = join3(yp, m);
1488
1339
  let days;
1489
1340
  try {
1490
1341
  days = await readdir4(mp);
@@ -1493,7 +1344,7 @@ async function* walkRollouts(root) {
1493
1344
  throw error;
1494
1345
  }
1495
1346
  for (const d of days) {
1496
- const dp = join4(mp, d);
1347
+ const dp = join3(mp, d);
1497
1348
  let files;
1498
1349
  try {
1499
1350
  files = await readdir4(dp);
@@ -1502,7 +1353,7 @@ async function* walkRollouts(root) {
1502
1353
  throw error;
1503
1354
  }
1504
1355
  for (const f of files) {
1505
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
1356
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
1506
1357
  }
1507
1358
  }
1508
1359
  }
@@ -1512,7 +1363,7 @@ var CodexAdapter = class {
1512
1363
  harness = "codex";
1513
1364
  aliases = ["codex-acp"];
1514
1365
  root() {
1515
- 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");
1516
1367
  }
1517
1368
  async locate(opts = {}) {
1518
1369
  const refs = [];
@@ -1550,7 +1401,9 @@ var CodexAdapter = class {
1550
1401
  const head = await takeJsonl(ref.path, SESSION_HEAD_LINES, jsonl2);
1551
1402
  let first = head[0];
1552
1403
  let meta = head.find((line) => line.type === "session_meta");
1553
- let model = head.find((line) => line.type === "turn_context")?.payload?.model ?? null;
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;
1554
1407
  if (!first || !meta) {
1555
1408
  for await (const line of readJsonl(ref.path, jsonl2)) {
1556
1409
  first ??= line;
@@ -1591,7 +1444,12 @@ var CodexAdapter = class {
1591
1444
  let sawUserTurn = false;
1592
1445
  let lastTimestamp;
1593
1446
  const awaitingModel = model ? [] : [root];
1447
+ let reachedCurrentTask = !hasTaskBoundary;
1594
1448
  for await (const l of readJsonl(ref.path, jsonl2)) {
1449
+ if (!reachedCurrentTask) {
1450
+ if (isCurrentTaskStart(l, taskStartedAt)) reachedCurrentTask = true;
1451
+ else continue;
1452
+ }
1595
1453
  lastTimestamp = l.timestamp;
1596
1454
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
1597
1455
  if (!model && l.type === "turn_context" && l.payload?.model) {
@@ -1762,13 +1620,13 @@ var CodexAdapter = class {
1762
1620
  // src/adapters/copilot.ts
1763
1621
  import { readdir as readdir5, stat as stat4 } from "fs/promises";
1764
1622
  import { homedir as homedir4 } from "os";
1765
- import { join as join5 } from "path";
1623
+ import { join as join4 } from "path";
1766
1624
  var SERVICE4 = "github-copilot";
1767
1625
  var CopilotAdapter = class {
1768
1626
  harness = "github-copilot";
1769
1627
  aliases = ["copilot"];
1770
1628
  root() {
1771
- return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
1629
+ return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
1772
1630
  }
1773
1631
  async locate(opts = {}) {
1774
1632
  let dirs;
@@ -1780,7 +1638,7 @@ var CopilotAdapter = class {
1780
1638
  }
1781
1639
  const refs = [];
1782
1640
  for (const id of dirs) {
1783
- const path = join5(this.root(), id, "events.jsonl");
1641
+ const path = join4(this.root(), id, "events.jsonl");
1784
1642
  let st;
1785
1643
  try {
1786
1644
  st = await stat4(path);
@@ -1886,7 +1744,7 @@ var CopilotAdapter = class {
1886
1744
  // src/adapters/factory.ts
1887
1745
  import { readdir as readdir6, stat as stat5 } from "fs/promises";
1888
1746
  import { homedir as homedir5 } from "os";
1889
- import { basename as basename4, join as join6 } from "path";
1747
+ import { basename as basename4, join as join5 } from "path";
1890
1748
  var SERVICE5 = "factory";
1891
1749
  function textOf4(content) {
1892
1750
  return capText(
@@ -1897,7 +1755,7 @@ var FactoryAdapter = class {
1897
1755
  harness = "factory";
1898
1756
  aliases = ["factory-droids", "droid"];
1899
1757
  root() {
1900
- return join6(homedir5(), ".factory", "sessions");
1758
+ return join5(homedir5(), ".factory", "sessions");
1901
1759
  }
1902
1760
  async locate(opts = {}) {
1903
1761
  const root = this.root();
@@ -1910,7 +1768,7 @@ var FactoryAdapter = class {
1910
1768
  }
1911
1769
  const refs = [];
1912
1770
  for (const dir of dirs) {
1913
- const dp = join6(root, dir);
1771
+ const dp = join5(root, dir);
1914
1772
  let files;
1915
1773
  try {
1916
1774
  files = await readdir6(dp);
@@ -1922,7 +1780,7 @@ var FactoryAdapter = class {
1922
1780
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1923
1781
  for (const f of files) {
1924
1782
  if (!f.endsWith(".jsonl")) continue;
1925
- const path = join6(dp, f);
1783
+ const path = join5(dp, f);
1926
1784
  let st;
1927
1785
  try {
1928
1786
  st = await stat5(path);
@@ -2058,7 +1916,7 @@ var FactoryAdapter = class {
2058
1916
  // src/adapters/forge.ts
2059
1917
  import { readdir as readdir7, stat as stat6 } from "fs/promises";
2060
1918
  import { homedir as homedir6 } from "os";
2061
- import { basename as basename5, join as join7 } from "path";
1919
+ import { basename as basename5, join as join6 } from "path";
2062
1920
  var SERVICE6 = "forge";
2063
1921
  function tokens(t) {
2064
1922
  if (!t) return null;
@@ -2072,7 +1930,7 @@ var ForgeAdapter = class {
2072
1930
  aliases = ["forgecode"];
2073
1931
  /** Directories scanned for `*-dump.json` exports. */
2074
1932
  dumpDirs() {
2075
- return [process.cwd(), join7(homedir6(), ".forge")];
1933
+ return [process.cwd(), join6(homedir6(), ".forge")];
2076
1934
  }
2077
1935
  async locate(opts = {}) {
2078
1936
  const refs = [];
@@ -2087,7 +1945,7 @@ var ForgeAdapter = class {
2087
1945
  }
2088
1946
  for (const f of files) {
2089
1947
  if (!f.endsWith("-dump.json")) continue;
2090
- const path = join7(dir, f);
1948
+ const path = join6(dir, f);
2091
1949
  let st;
2092
1950
  try {
2093
1951
  st = await stat6(path);
@@ -2188,7 +2046,7 @@ var ForgeAdapter = class {
2188
2046
  // src/adapters/gemini.ts
2189
2047
  import { readdir as readdir8, stat as stat7 } from "fs/promises";
2190
2048
  import { homedir as homedir7 } from "os";
2191
- import { join as join8 } from "path";
2049
+ import { join as join7 } from "path";
2192
2050
  function iso(ts, fallback) {
2193
2051
  if (typeof ts === "number") return new Date(ts).toISOString();
2194
2052
  if (typeof ts === "string" && ts.length > 0) return ts;
@@ -2213,7 +2071,7 @@ var GeminiFamilyAdapter = class {
2213
2071
  this.homeDirName = cfg.homeDirName;
2214
2072
  }
2215
2073
  chatsRoot() {
2216
- return join8(homedir7(), this.homeDirName, "tmp");
2074
+ return join7(homedir7(), this.homeDirName, "tmp");
2217
2075
  }
2218
2076
  /** `<home>/projects.json` maps `{ <absolute-cwd>: <projectName> }`. The session
2219
2077
  * dir name (`tmp/<projectHash>/`) is that projectName for registered projects,
@@ -2223,7 +2081,7 @@ var GeminiFamilyAdapter = class {
2223
2081
  const nameToPath = /* @__PURE__ */ new Map();
2224
2082
  try {
2225
2083
  const projects = (await readJsonFile(
2226
- join8(homedir7(), this.homeDirName, "projects.json")
2084
+ join7(homedir7(), this.homeDirName, "projects.json")
2227
2085
  )).projects ?? {};
2228
2086
  for (const [path, name] of Object.entries(projects)) nameToPath.set(name, path);
2229
2087
  } catch (error) {
@@ -2244,7 +2102,7 @@ var GeminiFamilyAdapter = class {
2244
2102
  const refs = [];
2245
2103
  for (const pd of projectDirs) {
2246
2104
  const cwd = projectCwds.get(pd) ?? null;
2247
- const chatsDir = join8(root, pd, "chats");
2105
+ const chatsDir = join7(root, pd, "chats");
2248
2106
  let files;
2249
2107
  try {
2250
2108
  files = await readdir8(chatsDir);
@@ -2254,7 +2112,7 @@ var GeminiFamilyAdapter = class {
2254
2112
  }
2255
2113
  for (const f of files) {
2256
2114
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
2257
- const path = join8(chatsDir, f);
2115
+ const path = join7(chatsDir, f);
2258
2116
  let st;
2259
2117
  try {
2260
2118
  st = await stat7(path);
@@ -2363,7 +2221,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
2363
2221
  // src/adapters/opencode.ts
2364
2222
  import { readdir as readdir9, stat as stat8 } from "fs/promises";
2365
2223
  import { homedir as homedir8 } from "os";
2366
- import { join as join9 } from "path";
2224
+ import { join as join8 } from "path";
2367
2225
  var SERVICE7 = "opencode";
2368
2226
  function isoOf(ms) {
2369
2227
  return new Date(ms ?? 0).toISOString();
@@ -2376,11 +2234,11 @@ function textOf7(parts) {
2376
2234
  var OpencodeAdapter = class {
2377
2235
  harness = "opencode";
2378
2236
  storage() {
2379
- const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
2380
- return join9(base, "opencode", "storage");
2237
+ const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
2238
+ return join8(base, "opencode", "storage");
2381
2239
  }
2382
2240
  async locate(opts = {}) {
2383
- const msgRoot = join9(this.storage(), "message");
2241
+ const msgRoot = join8(this.storage(), "message");
2384
2242
  let sessionDirs;
2385
2243
  try {
2386
2244
  sessionDirs = await readdir9(msgRoot);
@@ -2390,7 +2248,7 @@ var OpencodeAdapter = class {
2390
2248
  }
2391
2249
  const refs = [];
2392
2250
  for (const sid of sessionDirs) {
2393
- const dir = join9(msgRoot, sid);
2251
+ const dir = join8(msgRoot, sid);
2394
2252
  let st;
2395
2253
  try {
2396
2254
  st = await stat8(dir);
@@ -2408,11 +2266,11 @@ var OpencodeAdapter = class {
2408
2266
  async parse(ref) {
2409
2267
  const traceId = ref.sessionId;
2410
2268
  const rootId = `root:${traceId}`;
2411
- const partRoot = join9(this.storage(), "part");
2269
+ const partRoot = join8(this.storage(), "part");
2412
2270
  const msgFiles = await listJsonFiles(ref.path);
2413
2271
  const messages = [];
2414
2272
  for (const f of msgFiles) {
2415
- messages.push(await readJsonFile(join9(ref.path, f)));
2273
+ messages.push(await readJsonFile(join8(ref.path, f)));
2416
2274
  }
2417
2275
  messages.sort((a, b) => (a.time?.created ?? 0) - (b.time?.created ?? 0));
2418
2276
  const spans = [
@@ -2432,7 +2290,7 @@ var OpencodeAdapter = class {
2432
2290
  for (const msg of messages) {
2433
2291
  const mid = msg.id ?? `m${step}`;
2434
2292
  const llmId = `llm:${mid}`;
2435
- const pdir = join9(partRoot, mid);
2293
+ const pdir = join8(partRoot, mid);
2436
2294
  const parts = [];
2437
2295
  let partFiles = [];
2438
2296
  try {
@@ -2441,7 +2299,7 @@ var OpencodeAdapter = class {
2441
2299
  if (!isMissingJsonSource(error)) throw error;
2442
2300
  }
2443
2301
  for (const pf of partFiles) {
2444
- parts.push(await readJsonFile(join9(pdir, pf)));
2302
+ parts.push(await readJsonFile(join8(pdir, pf)));
2445
2303
  }
2446
2304
  const turnText = textOf7(parts);
2447
2305
  const isUser = msg.role === "user";
@@ -2519,7 +2377,7 @@ var OpencodeAdapter = class {
2519
2377
  // src/adapters/pi.ts
2520
2378
  import { readdir as readdir10, stat as stat9 } from "fs/promises";
2521
2379
  import { homedir as homedir9 } from "os";
2522
- import { basename as basename6, join as join10 } from "path";
2380
+ import { basename as basename6, join as join9 } from "path";
2523
2381
  var SERVICE8 = "pi";
2524
2382
  function isToolBlock(b) {
2525
2383
  return typeof b.type === "string" && /tool[_-]?call|tool[_-]?use|^tool$/i.test(b.type);
@@ -2536,7 +2394,7 @@ function textOf8(content) {
2536
2394
  var PiAdapter = class {
2537
2395
  harness = "pi";
2538
2396
  root() {
2539
- return join10(homedir9(), ".pi", "agent", "sessions");
2397
+ return join9(homedir9(), ".pi", "agent", "sessions");
2540
2398
  }
2541
2399
  async locate(opts = {}) {
2542
2400
  const root = this.root();
@@ -2549,7 +2407,7 @@ var PiAdapter = class {
2549
2407
  }
2550
2408
  const refs = [];
2551
2409
  for (const dir of dirs) {
2552
- const dp = join10(root, dir);
2410
+ const dp = join9(root, dir);
2553
2411
  let files;
2554
2412
  try {
2555
2413
  files = await readdir10(dp);
@@ -2561,7 +2419,7 @@ var PiAdapter = class {
2561
2419
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
2562
2420
  for (const f of files) {
2563
2421
  if (!f.endsWith(".jsonl")) continue;
2564
- const path = join10(dp, f);
2422
+ const path = join9(dp, f);
2565
2423
  let st;
2566
2424
  try {
2567
2425
  st = await stat9(path);
@@ -2696,7 +2554,7 @@ var PiAdapter = class {
2696
2554
  // src/adapters/qwen.ts
2697
2555
  import { readdir as readdir11, stat as stat10 } from "fs/promises";
2698
2556
  import { homedir as homedir10 } from "os";
2699
- import { basename as basename7, join as join11 } from "path";
2557
+ import { basename as basename7, join as join10 } from "path";
2700
2558
  var SERVICE9 = "qwen";
2701
2559
  function textOf9(parts) {
2702
2560
  if (!parts) return "";
@@ -2706,7 +2564,7 @@ var QwenAdapter = class {
2706
2564
  harness = "qwen";
2707
2565
  aliases = ["qwen-code"];
2708
2566
  root() {
2709
- return join11(homedir10(), ".qwen", "projects");
2567
+ return join10(homedir10(), ".qwen", "projects");
2710
2568
  }
2711
2569
  async locate(opts = {}) {
2712
2570
  const root = this.root();
@@ -2719,7 +2577,7 @@ var QwenAdapter = class {
2719
2577
  }
2720
2578
  const refs = [];
2721
2579
  for (const pd of projectDirs) {
2722
- const chatsDir = join11(root, pd, "chats");
2580
+ const chatsDir = join10(root, pd, "chats");
2723
2581
  let files;
2724
2582
  try {
2725
2583
  files = await readdir11(chatsDir);
@@ -2730,7 +2588,7 @@ var QwenAdapter = class {
2730
2588
  if (opts.cwd) continue;
2731
2589
  for (const f of files) {
2732
2590
  if (!f.endsWith(".jsonl")) continue;
2733
- const path = join11(chatsDir, f);
2591
+ const path = join10(chatsDir, f);
2734
2592
  let st;
2735
2593
  try {
2736
2594
  st = await stat10(path);
@@ -2896,7 +2754,7 @@ function selectAdapters(opts) {
2896
2754
 
2897
2755
  // src/repo.ts
2898
2756
  import { stat as stat11 } from "fs/promises";
2899
- 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";
2900
2758
  function normalizeRemote(url) {
2901
2759
  const raw = url.trim();
2902
2760
  if (!raw) return null;
@@ -3017,7 +2875,7 @@ async function nearestExistingDir(path) {
3017
2875
  async function findGitRoot(path) {
3018
2876
  let cur = await nearestExistingDir(path);
3019
2877
  while (cur) {
3020
- if (await pathStat(join12(cur, ".git"))) return cur;
2878
+ if (await pathStat(join11(cur, ".git"))) return cur;
3021
2879
  const parent = dirname(cur);
3022
2880
  if (parent === cur) return null;
3023
2881
  cur = parent;
@@ -3035,7 +2893,7 @@ async function repairDashedPath(cwd) {
3035
2893
  while (index < parts.length) {
3036
2894
  const nextPart = parts[index];
3037
2895
  if (!nextPart) break;
3038
- const exact = join12(current, nextPart);
2896
+ const exact = join11(current, nextPart);
3039
2897
  if (!await directoryExists(exact)) break;
3040
2898
  current = exact;
3041
2899
  index += 1;
@@ -3045,7 +2903,7 @@ async function repairDashedPath(cwd) {
3045
2903
  let consumed = 0;
3046
2904
  for (let n = parts.length - index; n >= 1; n -= 1) {
3047
2905
  const segment = parts.slice(index, index + n).join("-");
3048
- const candidate = join12(current, segment);
2906
+ const candidate = join11(current, segment);
3049
2907
  if (await directoryExists(candidate)) {
3050
2908
  match = candidate;
3051
2909
  consumed = n;
@@ -3316,7 +3174,7 @@ async function* scanSessions(opts) {
3316
3174
  // src/evidence.ts
3317
3175
  import { mkdtemp, writeFile } from "fs/promises";
3318
3176
  import { tmpdir } from "os";
3319
- import { join as join13 } from "path";
3177
+ import { join as join12 } from "path";
3320
3178
  import {
3321
3179
  OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
3322
3180
  TOOL_NAME as TOOL_NAME2
@@ -3428,7 +3286,7 @@ function serializePolicyEvidence(records) {
3428
3286
  `;
3429
3287
  }
3430
3288
  async function writePolicyEvidenceFile(records, outPath) {
3431
- 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");
3432
3290
  await writeFile(path, serializePolicyEvidence(records), "utf8");
3433
3291
  return path;
3434
3292
  }
@@ -3626,9 +3484,9 @@ async function applyRedactor(spans, redactor) {
3626
3484
 
3627
3485
  // src/file-export.ts
3628
3486
  import { createHash as createHash4 } from "crypto";
3629
- import { mkdtemp as mkdtemp2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
3487
+ import { mkdtemp as mkdtemp2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
3630
3488
  import { tmpdir as tmpdir2 } from "os";
3631
- import { join as join14 } from "path";
3489
+ import { join as join13 } from "path";
3632
3490
  import {
3633
3491
  firstNumberAttr,
3634
3492
  LLM_CACHED_TOKEN_ATTR_KEYS,
@@ -4216,7 +4074,7 @@ async function exportTraceEvidenceFile(inputPath, opts = {}) {
4216
4074
  sourcePath: inputPath,
4217
4075
  format
4218
4076
  });
4219
- spans.push(...converted.spans);
4077
+ appendAll(spans, converted.spans);
4220
4078
  redactionCount += converted.redactionCount;
4221
4079
  for (const [rule, count2] of Object.entries(converted.redactionsByRule)) {
4222
4080
  redactionsByRule[rule] = (redactionsByRule[rule] ?? 0) + count2;
@@ -4232,20 +4090,20 @@ async function exportTraceEvidenceFile(inputPath, opts = {}) {
4232
4090
  }
4233
4091
  return { format, spans, redactionCount, redactionsByRule };
4234
4092
  }
4235
- const text = await readFile3(inputPath, "utf8");
4093
+ const text = await readFile2(inputPath, "utf8");
4236
4094
  return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
4237
4095
  }
4238
4096
  async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
4239
4097
  const result = await exportTraceEvidenceFile(inputPath, opts);
4240
- 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");
4241
4099
  await writeFile2(path, serializeSpans(result.spans), "utf8");
4242
4100
  return { ...result, path };
4243
4101
  }
4244
4102
 
4245
4103
  // src/inspect.ts
4246
- import { mkdtemp as mkdtemp3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
4104
+ import { mkdtemp as mkdtemp3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
4247
4105
  import { tmpdir as tmpdir3 } from "os";
4248
- import { basename as basename8, join as join15 } from "path";
4106
+ import { basename as basename8, join as join14 } from "path";
4249
4107
  var severityRank = {
4250
4108
  high: 0,
4251
4109
  medium: 1,
@@ -4529,16 +4387,289 @@ function assertSessionIndex(value) {
4529
4387
  }
4530
4388
  }
4531
4389
  async function readSessionIndexFile(path) {
4532
- const parsed = JSON.parse(await readFile4(path, "utf8"));
4390
+ const parsed = JSON.parse(await readFile3(path, "utf8"));
4533
4391
  assertSessionIndex(parsed);
4534
4392
  return parsed;
4535
4393
  }
4536
4394
  async function writeInspectionReportFile(report, outPath) {
4537
- const path = outPath ?? join15(await mkdtemp3(join15(tmpdir3(), "traces-inspect-")), "inspection-report.md");
4395
+ const path = outPath ?? join14(await mkdtemp3(join14(tmpdir3(), "traces-inspect-")), "inspection-report.md");
4538
4396
  await writeFile3(path, renderInspectionReport(report), "utf8");
4539
4397
  return path;
4540
4398
  }
4541
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
+ }
4578
+ }
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
+ }
4585
+ }
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;
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
+ };
4633
+ }
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 };
4671
+ }
4672
+
4542
4673
  // src/reactions.ts
4543
4674
  var REACTIONS = ["correction", "frustration", "jargon", "structure", "praise"];
4544
4675
  function emptySignals() {
@@ -4681,15 +4812,17 @@ function renderExecution(report) {
4681
4812
  const execution = report.execution;
4682
4813
  const lines = ["## execution facts", ""];
4683
4814
  lines.push(
4684
- `- **Runs:** ${count(execution.durationMs.n)} | **Model-call runs:** ${count(execution.modelCalls.runs)} | **Model-call events:** ${count(execution.modelCalls.events)} | **Failed runs:** ${count(execution.failures.runs)}/${count(execution.failures.reportingRuns)} (${decimal(execution.failures.fraction * 100, 2)}%)`
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)}%)`
4685
4816
  );
4686
4817
  lines.push("- **Task quality:** not measured; these traces do not include comparable task outcome labels.");
4687
4818
  lines.push("");
4688
- lines.push("| Time | Runs measured | Min | Mean | p50 | p95 | Max |");
4819
+ lines.push("| Time | Sessions measured | Min | Mean | p50 | p95 | Max |");
4689
4820
  lines.push("|---|---:|---:|---:|---:|---:|---:|");
4690
- lines.push(distributionRow("Duration", execution.durationMs, "ms"));
4821
+ lines.push(distributionRow("Recorded session interval", execution.durationMs, "ms"));
4691
4822
  lines.push(distributionRow("Queue", execution.queueMs, "ms"));
4692
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("");
4693
4826
  lines.push("### Direct model usage");
4694
4827
  lines.push("");
4695
4828
  lines.push("| Token category | Total | Runs measured | Mean | p50 | p95 | Max |");
@@ -5030,25 +5163,38 @@ function renderAdoption(ar) {
5030
5163
  );
5031
5164
  }
5032
5165
  lines.push(
5033
- `- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **SKILL.md tool references:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount} observed group(s)`
5166
+ `- **Materialized skill catalogs/instructions:** ${ar.sessionsWithMaterializedSkills}/${ar.executionGroupCount} observed group(s) \xB7 **Sessions with successful skill-document reads:** ${ar.sessionsWithSkillFileReference}/${ar.executionGroupCount}`
5034
5167
  );
5035
5168
  lines.push(
5036
- `- **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}`
5037
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
+ }
5038
5179
  lines.push(
5039
5180
  `- **Subagent spawns observed:** ${ar.totalSubagentSpawns} across ${ar.sessionsWithSubagent} observed group(s); capture completeness depends on source telemetry.`
5040
5181
  );
5041
5182
  lines.push("");
5042
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]);
5043
5185
  const loopRows = Object.entries(ar.loopDispatchedRuns).sort((a, b) => b[1] - a[1]);
5044
- if (skillRows.length > 0 || loopRows.length > 0) {
5045
- lines.push("| Skill | Explicit invocations | Loop-dispatched runs |");
5046
- lines.push("|---|---|---|");
5047
- const names = [.../* @__PURE__ */ new Set([...skillRows.map((r) => r[0]), ...loopRows.map((r) => r[0])])].sort(
5048
- (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)
5049
5195
  );
5050
5196
  for (const n of names) {
5051
- 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} |`);
5052
5198
  }
5053
5199
  lines.push("");
5054
5200
  }
@@ -6448,25 +6594,23 @@ async function executeUpload(plan, opts = {}) {
6448
6594
  }
6449
6595
 
6450
6596
  export {
6451
- ATTR,
6452
- INGEST_SOURCE_CLI,
6453
- DEFAULT_HARNESS,
6454
- sessionIdFromAttributes,
6455
- indexSessionIdsByTrace,
6456
- stampSessionIdentity,
6457
- countSkillRunsJsonl,
6458
- analyzeAdoption,
6459
6597
  span,
6460
6598
  toOpenInferenceSpan,
6461
6599
  serializeSpans,
6462
6600
  writeOtlpFile,
6463
- summarizeOtlpExecution,
6464
- summarizeSpanExecution,
6465
- analyzeSpans,
6466
6601
  CONTENT_CAP,
6467
6602
  capText,
6468
6603
  ACTOR_ATTR,
6469
6604
  userPromptSpan,
6605
+ appendAll,
6606
+ ATTR,
6607
+ INGEST_SOURCE_CLI,
6608
+ DEFAULT_HARNESS,
6609
+ sessionIdFromAttributes,
6610
+ indexSessionIdsByTrace,
6611
+ stampSessionIdentity,
6612
+ summarizeOtlpExecution,
6613
+ summarizeSpanExecution,
6470
6614
  parseIsoToEpochMs,
6471
6615
  parseSince,
6472
6616
  toRuntimeStore,
@@ -6532,14 +6676,12 @@ export {
6532
6676
  serializeInspectionReport,
6533
6677
  readSessionIndexFile,
6534
6678
  writeInspectionReportFile,
6679
+ countSkillRunsJsonl,
6680
+ analyzeAdoption,
6681
+ analyzeSpans,
6535
6682
  REACTIONS,
6536
6683
  analyzeReactions,
6537
6684
  CORRUPTION_RECEIPT_DISPLAY_LIMIT,
6538
- summarizeDeterministicSignals,
6539
- renderReport,
6540
- renderPipelines,
6541
- renderReactions,
6542
- renderAdoption,
6543
6685
  buildTraceFindingPacket,
6544
6686
  loadTracesConfig,
6545
6687
  mergeTracesConfig,