@tangle-network/traces 0.7.0 → 0.8.5

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,3 +1,110 @@
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;
26
+ return typeof v === "string" && v.length > 0 ? v : "?";
27
+ }
28
+ function countSkillRunsJsonl(raw) {
29
+ const out = {};
30
+ for (const line of raw.split("\n")) {
31
+ const t = line.trim();
32
+ if (!t) continue;
33
+ let o;
34
+ try {
35
+ o = JSON.parse(t);
36
+ } catch {
37
+ continue;
38
+ }
39
+ if (!o || typeof o !== "object") continue;
40
+ const rec = o;
41
+ const names = [];
42
+ if (Array.isArray(rec.skills)) {
43
+ for (const s of rec.skills) if (typeof s === "string" && s) names.push(s);
44
+ }
45
+ if (typeof rec.skill === "string" && rec.skill) names.push(rec.skill);
46
+ if (names.length === 0) names.push("?");
47
+ for (const n of names) out[n] = (out[n] ?? 0) + 1;
48
+ }
49
+ return out;
50
+ }
51
+ async function readLoopRuns(cwds) {
52
+ const counts = {};
53
+ let filesRead = 0;
54
+ const seen = /* @__PURE__ */ new Set();
55
+ for (const cwd of cwds) {
56
+ if (!cwd || seen.has(cwd)) continue;
57
+ seen.add(cwd);
58
+ const path = join(cwd, ".evolve", "skill-runs.jsonl");
59
+ let raw;
60
+ try {
61
+ raw = await readFile(path, "utf8");
62
+ } catch {
63
+ continue;
64
+ }
65
+ filesRead += 1;
66
+ for (const [k, v] of Object.entries(countSkillRunsJsonl(raw))) counts[k] = (counts[k] ?? 0) + v;
67
+ }
68
+ return { counts, filesRead };
69
+ }
70
+ async function analyzeAdoption(spans, opts = {}) {
71
+ const sessions = /* @__PURE__ */ new Set();
72
+ const sessionsWithSkill = /* @__PURE__ */ new Set();
73
+ const sessionsWithSubagent = /* @__PURE__ */ new Set();
74
+ const skillInvocations = {};
75
+ const subagentSpawns = {};
76
+ for (const s of spans) {
77
+ sessions.add(s.trace_id);
78
+ const tn = toolName(s);
79
+ if (tn === "Skill") {
80
+ const name = skillNameOf(parseInput(s));
81
+ skillInvocations[name] = (skillInvocations[name] ?? 0) + 1;
82
+ sessionsWithSkill.add(s.trace_id);
83
+ } else if (tn === "Task" || tn === "Agent") {
84
+ const type = subagentTypeOf(parseInput(s));
85
+ subagentSpawns[type] = (subagentSpawns[type] ?? 0) + 1;
86
+ sessionsWithSubagent.add(s.trace_id);
87
+ }
88
+ }
89
+ const { counts: loopDispatchedRuns, filesRead } = await readLoopRuns(opts.cwds ?? []);
90
+ const totalSkillInvocations = Object.values(skillInvocations).reduce((a, b) => a + b, 0);
91
+ const totalSubagentSpawns = Object.values(subagentSpawns).reduce((a, b) => a + b, 0);
92
+ const totalLoopDispatchedRuns = Object.values(loopDispatchedRuns).reduce((a, b) => a + b, 0);
93
+ return {
94
+ sessionCount: sessions.size,
95
+ sessionsWithSkill: sessionsWithSkill.size,
96
+ skillPenetration: sessions.size === 0 ? 0 : sessionsWithSkill.size / sessions.size,
97
+ skillInvocations,
98
+ totalSkillInvocations,
99
+ subagentSpawns,
100
+ totalSubagentSpawns,
101
+ sessionsWithSubagent: sessionsWithSubagent.size,
102
+ loopDispatchedRuns,
103
+ totalLoopDispatchedRuns,
104
+ skillRunFilesRead: filesRead
105
+ };
106
+ }
107
+
1
108
  // src/otlp.ts
2
109
  function span(input) {
3
110
  const attributes = {
@@ -25,17 +132,39 @@ function span(input) {
25
132
  attributes
26
133
  };
27
134
  }
135
+ function toOpenInferenceSpan(s) {
136
+ const a = s.attributes;
137
+ const resourceAttrs = {};
138
+ if (a["service.name"] != null) resourceAttrs["service.name"] = a["service.name"];
139
+ if (a["agent.name"] != null) resourceAttrs["agent.name"] = a["agent.name"];
140
+ for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd"]) {
141
+ if (a[k] != null) resourceAttrs[k] = a[k];
142
+ }
143
+ return {
144
+ trace_id: s.trace_id,
145
+ span_id: s.span_id,
146
+ parent_span_id: s.parent_span_id ?? "",
147
+ name: s.name,
148
+ kind: typeof a["openinference.span.kind"] === "string" ? a["openinference.span.kind"] : "CHAIN",
149
+ start_time: s.start_time,
150
+ end_time: s.end_time,
151
+ status: { code: s.status.code, message: s.status.message ?? "" },
152
+ resource: { attributes: resourceAttrs },
153
+ scope: { name: "tangle-traces", version: "" },
154
+ attributes: a
155
+ };
156
+ }
28
157
  function serializeSpans(spans) {
29
158
  if (spans.length === 0) return "";
30
- return `${spans.map((s) => JSON.stringify(s)).join("\n")}
159
+ return `${spans.map((s) => JSON.stringify(toOpenInferenceSpan(s))).join("\n")}
31
160
  `;
32
161
  }
33
162
  async function writeOtlpFile(spans, outPath) {
34
- const { mkdtemp, writeFile: writeFile3 } = await import("fs/promises");
35
- const { tmpdir } = await import("os");
36
- const { join: join13 } = await import("path");
37
- const path = outPath ?? join13(await mkdtemp(join13(tmpdir(), "traces-")), "spans.otlp.jsonl");
38
- await writeFile3(path, serializeSpans(spans), "utf8");
163
+ const { mkdtemp: mkdtemp3, writeFile: writeFile4 } = await import("fs/promises");
164
+ const { tmpdir: tmpdir3 } = await import("os");
165
+ const { join: join16 } = await import("path");
166
+ const path = outPath ?? join16(await mkdtemp3(join16(tmpdir3(), "traces-")), "spans.otlp.jsonl");
167
+ await writeFile4(path, serializeSpans(spans), "utf8");
39
168
  return path;
40
169
  }
41
170
 
@@ -70,150 +199,37 @@ async function analyzeSpans(spans, opts = {}) {
70
199
  return { otlpPath, result };
71
200
  }
72
201
 
73
- // src/external.ts
74
- import { spawn } from "child_process";
75
- import { readFile, writeFile } from "fs/promises";
76
- function runCommand(command, args = [], opts = {}) {
77
- const timeoutMs = opts.timeoutMs ?? 12e4;
78
- const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
79
- return new Promise((resolve, reject) => {
80
- let child;
81
- try {
82
- child = spawn(command, [...args], { signal: opts.signal });
83
- } catch (err2) {
84
- reject(err2 instanceof Error ? err2 : new Error(String(err2)));
85
- return;
86
- }
87
- let out = "";
88
- let err = "";
89
- let killed = false;
90
- const timer = setTimeout(() => {
91
- killed = true;
92
- child.kill("SIGKILL");
93
- reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
94
- }, timeoutMs);
95
- const cap = (add, buf) => {
96
- const next = buf + add;
97
- if (next.length > maxBuffer) {
98
- killed = true;
99
- child.kill("SIGKILL");
100
- clearTimeout(timer);
101
- reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
102
- }
103
- return next;
104
- };
105
- child.stdout?.on("data", (d) => {
106
- out = cap(String(d), out);
107
- });
108
- child.stderr?.on("data", (d) => {
109
- err = cap(String(d), err);
110
- });
111
- child.on("error", (e) => {
112
- clearTimeout(timer);
113
- if (!killed) reject(e);
114
- });
115
- child.on("close", (code) => {
116
- clearTimeout(timer);
117
- if (!killed) resolve({ stdout: out, stderr: err, code });
118
- });
119
- if (opts.input !== void 0) {
120
- child.stdin?.on("error", () => {
121
- });
122
- child.stdin?.end(opts.input);
123
- }
124
- });
125
- }
126
- function commandAnalyzer(spec) {
127
- return {
128
- name: spec.name,
129
- async analyze(otlpPath, opts = {}) {
130
- try {
131
- const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
132
- signal: opts.signal,
133
- timeoutMs: spec.timeoutMs
134
- });
135
- if (res.code !== 0) {
136
- return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
137
- }
138
- return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
139
- } catch (e) {
140
- return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
141
- }
142
- }
143
- };
144
- }
145
- function toCanonicalOpenInference(otlpJsonl, opts = {}) {
146
- return otlpJsonl.split("\n").filter((l) => l.trim()).map((line) => {
147
- const s = JSON.parse(line);
148
- const a = s.attributes ?? {};
149
- const resourceAttrs = { "service.name": a["service.name"] ?? opts.service ?? "traces" };
150
- if (a["agent.name"]) resourceAttrs["agent.name"] = a["agent.name"];
151
- return JSON.stringify({
152
- trace_id: s.trace_id,
153
- span_id: s.span_id,
154
- parent_span_id: s.parent_span_id ?? "",
155
- trace_state: "",
156
- name: s.name,
157
- kind: a["openinference.span.kind"] ?? "CHAIN",
158
- start_time: s.start_time,
159
- end_time: s.end_time,
160
- status: { code: s.status?.code ?? "OK", message: s.status?.message ?? "" },
161
- resource: { attributes: resourceAttrs },
162
- scope: { name: opts.scopeName ?? "tangle-traces", version: "" },
163
- attributes: a
164
- });
165
- }).join("\n");
166
- }
167
- function haloAnalyzer(opts = {}) {
168
- const command = opts.command ?? "halo";
169
- const defaultPrompt = opts.defaultPrompt ?? "diagnose";
170
- return {
171
- name: "halo",
172
- async analyze(otlpPath, o = {}) {
173
- try {
174
- const canonical = toCanonicalOpenInference(await readFile(otlpPath, "utf8"));
175
- const canonPath = `${otlpPath}.halo.jsonl`;
176
- await writeFile(canonPath, canonical);
177
- const args = [canonPath, "-p", o.prompt ?? defaultPrompt];
178
- if (opts.model) args.push("-m", opts.model);
179
- const res = await runCommand(command, args, { signal: o.signal, timeoutMs: opts.timeoutMs });
180
- if (res.code !== 0) {
181
- return { analyzer: "halo", ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
182
- }
183
- return { analyzer: "halo", ok: true, output: res.stdout.trim() };
184
- } catch (e) {
185
- return { analyzer: "halo", ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
186
- }
187
- }
188
- };
189
- }
190
- function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
191
- return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
192
- }
193
- function commandRedactor(spec) {
194
- const name = spec.name ?? spec.command;
195
- return {
196
- name,
197
- async redactText(texts) {
198
- if (texts.length === 0) return [];
199
- const res = await runCommand(spec.command, spec.args ?? [], {
200
- input: JSON.stringify(texts),
201
- timeoutMs: spec.timeoutMs
202
- });
203
- if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
204
- let parsed;
205
- try {
206
- parsed = JSON.parse(res.stdout);
207
- } catch {
208
- throw new Error(`redactor ${name}: stdout was not JSON`);
209
- }
210
- if (!Array.isArray(parsed) || parsed.length !== texts.length) {
211
- throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
212
- }
213
- return parsed.map(String);
214
- }
215
- };
216
- }
202
+ // src/attributes.ts
203
+ var ATTR = {
204
+ /** = the trace/session id. Wire contract (dedup linkage). */
205
+ SESSION_ID: "tangle.sessionId",
206
+ /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
207
+ * that also streamed live. Wire contract. */
208
+ INGEST_SOURCE: "tangle.ingest_source",
209
+ HARNESS: "tangle.harness",
210
+ CWD: "tangle.cwd",
211
+ /** THE per-session grouping key the spine derives subjects from
212
+ * (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
213
+ * remote (host/owner/repo) when readable, else the cwd path basename. */
214
+ SUBJECT_KEY: "tangle.subject.key",
215
+ /** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
216
+ GIT_REPOSITORY: "git.repository",
217
+ /** Current branch at conversion time. */
218
+ GIT_BRANCH_NAME: "git.branch",
219
+ /** HEAD short sha at conversion time. */
220
+ GIT_COMMIT: "git.commit",
221
+ GIT_BRANCH: "tangle.git_branch",
222
+ HOST: "tangle.host",
223
+ /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
224
+ * which collided with `tangle.ingest_source`.) */
225
+ SESSION_FILE: "tangle.session_file",
226
+ UPLOADED_AT: "tangle.uploaded_at",
227
+ UPLOADER: "tangle.uploader",
228
+ REDACTION_VERSION: "redaction.version",
229
+ REDACTION_COUNT: "redaction.count"
230
+ };
231
+ var INGEST_SOURCE_CLI = "cli";
232
+ var DEFAULT_HARNESS = "claude-code";
217
233
 
218
234
  // src/time.ts
219
235
  function parseIsoToEpochMs(ts) {
@@ -291,6 +307,7 @@ function capText(raw) {
291
307
  if (t.length <= CONTENT_CAP) return t;
292
308
  return `${t.slice(0, CONTENT_CAP)}\u2026 [+${t.length - CONTENT_CAP} chars]`;
293
309
  }
310
+ var ACTOR_ATTR = "tangle.actor";
294
311
  function userPromptSpan(o) {
295
312
  return span({
296
313
  traceId: o.traceId,
@@ -302,14 +319,15 @@ function userPromptSpan(o) {
302
319
  service: o.service ?? null,
303
320
  agent: o.agent ?? null,
304
321
  step: o.step,
305
- content: o.content
322
+ content: o.content,
323
+ extra: { [ACTOR_ATTR]: o.actor ?? "human" }
306
324
  });
307
325
  }
308
326
 
309
327
  // src/adapters/amp.ts
310
328
  import { readdir, readFile as readFile2, stat } from "fs/promises";
311
329
  import { homedir } from "os";
312
- import { basename, join } from "path";
330
+ import { basename, join as join2 } from "path";
313
331
  var SERVICE = "amp";
314
332
  function textOf(content) {
315
333
  if (typeof content === "string") return content.slice(0, CONTENT_CAP);
@@ -320,8 +338,8 @@ function textOf(content) {
320
338
  var AmpAdapter = class {
321
339
  harness = "amp";
322
340
  threadsRoot() {
323
- const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
324
- return join(base, "amp", "threads");
341
+ const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
342
+ return join2(base, "amp", "threads");
325
343
  }
326
344
  async locate(opts = {}) {
327
345
  let files;
@@ -333,7 +351,7 @@ var AmpAdapter = class {
333
351
  const refs = [];
334
352
  for (const f of files) {
335
353
  if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
336
- const path = join(this.threadsRoot(), f);
354
+ const path = join2(this.threadsRoot(), f);
337
355
  let st;
338
356
  try {
339
357
  st = await stat(path);
@@ -432,10 +450,98 @@ var AmpAdapter = class {
432
450
  }
433
451
  };
434
452
 
453
+ // src/adapters/actor.ts
454
+ var CMD_MARKERS = [
455
+ "<command-name>",
456
+ "<local-command-caveat>",
457
+ "<command-message>",
458
+ "<command-args>",
459
+ "<command-stdout>"
460
+ ];
461
+ var INJECT_MARKERS = [
462
+ "<system-reminder>",
463
+ "Caveat: The messages below",
464
+ "<user-memory-input>",
465
+ // Harness-injected wrappers that arrive as `userType: external` user turns —
466
+ // they pass the structural human test but are the harness feeding the model,
467
+ // not a person. (Observed in Claude Code task/loop sessions.)
468
+ "<task-notification>",
469
+ "<task-prompt>",
470
+ "<local-command-stdout>",
471
+ "This session is being continued from a previous conversation",
472
+ // Slash-command skill bodies are expanded into a user turn (no <command-name>
473
+ // wrapper) — the skill prompt, not the human.
474
+ "Base directory for this skill:"
475
+ ];
476
+ var SYNTHETIC_MARKERS = [
477
+ "You are in a development environment with full file system access",
478
+ "## What the user asked for",
479
+ "Build the project described below",
480
+ "Filesystem sandboxing defines",
481
+ "<permissions instructions>",
482
+ "You are an autonomous",
483
+ "You are a coding agent",
484
+ "Your task is to build",
485
+ "## Deliverable",
486
+ "## Acceptance criteria",
487
+ "Return ONLY",
488
+ "respond with JSON",
489
+ "<environment_details>",
490
+ "<task>",
491
+ "Review this change before it is pushed",
492
+ "Complete a security review",
493
+ "Focus on correctness, regressions, security issues",
494
+ "Read-only critical audit",
495
+ "Staff-engineer code review"
496
+ ];
497
+ var AGENT_PROMPT = /^(you are\b|you're\b|read[- ]only\b|stop now\b|your task\b|your job\b|review the\b|audit\b|analyze the\b|return (only|a |the )|here is the\b|context:|task:|do not (revert|edit|touch)|work only in\b|you have been\b|act as\b|operate the\b|goal:|objective:)/i;
498
+ function textIsCmdOrInject(s) {
499
+ return CMD_MARKERS.some((m) => s.includes(m)) || INJECT_MARKERS.some((m) => s.includes(m));
500
+ }
501
+ function textIsSynthetic(s) {
502
+ const head = s.slice(0, 600);
503
+ return SYNTHETIC_MARKERS.some((m) => head.includes(m));
504
+ }
505
+ function looksLikeAgentPrompt(s) {
506
+ const t = s.trim();
507
+ if (t.length > 1500) return true;
508
+ return AGENT_PROMPT.test(t);
509
+ }
510
+ function claudeActor(args) {
511
+ if (args.isSidechain === true) return "subagent-spawn";
512
+ if (args.userType != null && args.userType !== "external") return "injected";
513
+ if (textIsCmdOrInject(args.text)) return "injected";
514
+ if (textIsSynthetic(args.text)) return "injected";
515
+ if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "subagent-spawn";
516
+ return "human";
517
+ }
518
+ function codexActor(args) {
519
+ if (textIsCmdOrInject(args.text)) return "injected";
520
+ if (textIsSynthetic(args.text)) return "injected";
521
+ if (args.isFirstUserTurn && looksLikeAgentPrompt(args.text)) return "injected";
522
+ return "human";
523
+ }
524
+ var JARGON_COMPLAINT = /\b(jargon|plain english|plain language|eli5|explain like|in plain|what does that mean|what does .* mean|i don'?t (understand|get)|gibberish|english please|too technical|speak (plainly|english)|dumb(ed)? it down|simpler|simplify your|no buzzwords?)\b/;
525
+ var STRUCTURE_COMPLAINT = /\b(too long|tl;?dr|get to the point|bluf|lead with|bullet|bulletpoint|wall of text|too verbose|shorter|be concise|stop (yapping|rambling)|less words?|fewer words?|just (the|tell me)|cut the)\b/;
526
+ var CORRECTION = /(\bno[,. ]|^no\b|\bstop\b|\bdon'?t\b|you keep|i (said|told you|asked)|that'?s not (what|right|it)|not what i|didn'?t ask|incorrect|that'?s wrong|you'?re wrong|why are you|why did you|that'?s not the|missed the|not the point|ur wrong|thats not)/;
527
+ var FRUSTRATION = /(ugh|wtf|wth|come on|seriously|again\?|for the (last|nth) time|are you kidding|jesus|christ|frustrat|annoying|wasting|waste of)/;
528
+ var PRAISE = /\b(perfect|exactly|nailed it|love (it|this)|beautiful|great work|ship it|clean|nice work|excellent|that'?s it|yes!+|amazing|chef'?s kiss|brilliant)\b/;
529
+ function classifyReaction(text) {
530
+ const t = text.toLowerCase();
531
+ const hits = [];
532
+ if (JARGON_COMPLAINT.test(t)) hits.push("jargon");
533
+ if (STRUCTURE_COMPLAINT.test(t)) hits.push("structure");
534
+ if (CORRECTION.test(t)) hits.push("correction");
535
+ if (FRUSTRATION.test(t)) hits.push("frustration");
536
+ if (PRAISE.test(t)) hits.push("praise");
537
+ return hits;
538
+ }
539
+ var CORRECTIVE_REACTIONS = ["correction", "frustration", "jargon", "structure"];
540
+
435
541
  // src/adapters/claude.ts
436
542
  import { readdir as readdir2, readFile as readFile3, stat as stat2 } from "fs/promises";
437
543
  import { homedir as homedir2 } from "os";
438
- import { basename as basename2, join as join2 } from "path";
544
+ import { basename as basename2, join as join3 } from "path";
439
545
  var SERVICE2 = "claude-code";
440
546
  function parseLines(raw) {
441
547
  const out = [];
@@ -473,6 +579,7 @@ function parseClaudeStream(events, ctx) {
473
579
  const spans = [];
474
580
  const toolSpanByUseId = /* @__PURE__ */ new Map();
475
581
  let step = ctx.startStep;
582
+ let sawUserTurn = false;
476
583
  for (const ev of events) {
477
584
  const ts = ev.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
478
585
  const uid = ev.uuid ?? `${ctx.idPrefix}step${step}`;
@@ -520,6 +627,13 @@ function parseClaudeStream(events, ctx) {
520
627
  } else if (ev.type === "user" && ev.message) {
521
628
  const prompt = textOf2(ev.message.content);
522
629
  if (prompt) {
630
+ const actor = claudeActor({
631
+ text: prompt,
632
+ isSidechain: ev.isSidechain,
633
+ userType: ev.userType ?? null,
634
+ isFirstUserTurn: !sawUserTurn
635
+ });
636
+ sawUserTurn = true;
523
637
  spans.push(
524
638
  userPromptSpan({
525
639
  traceId: ctx.traceId,
@@ -529,7 +643,8 @@ function parseClaudeStream(events, ctx) {
529
643
  service: SERVICE2,
530
644
  agent: ctx.agent,
531
645
  step,
532
- content: prompt
646
+ content: prompt,
647
+ actor
533
648
  })
534
649
  );
535
650
  step += 1;
@@ -556,7 +671,7 @@ var ClaudeAdapter = class {
556
671
  harness = "claude-code";
557
672
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
558
673
  root() {
559
- return join2(homedir2(), ".claude", "projects");
674
+ return join3(homedir2(), ".claude", "projects");
560
675
  }
561
676
  async locate(opts = {}) {
562
677
  const root = this.root();
@@ -568,7 +683,7 @@ var ClaudeAdapter = class {
568
683
  }
569
684
  const refs = [];
570
685
  for (const dir of projectDirs) {
571
- const dirPath = join2(root, dir);
686
+ const dirPath = join3(root, dir);
572
687
  let files;
573
688
  try {
574
689
  files = await readdir2(dirPath);
@@ -577,7 +692,7 @@ var ClaudeAdapter = class {
577
692
  }
578
693
  for (const f of files) {
579
694
  if (!f.endsWith(".jsonl")) continue;
580
- const path = join2(dirPath, f);
695
+ const path = join3(dirPath, f);
581
696
  let st;
582
697
  try {
583
698
  st = await stat2(path);
@@ -624,7 +739,7 @@ var ClaudeAdapter = class {
624
739
  }
625
740
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
626
741
  async foldSubagents(ref, traceId, main, out) {
627
- const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
742
+ const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
628
743
  let files;
629
744
  try {
630
745
  files = await readdir2(subDir);
@@ -637,13 +752,13 @@ var ClaudeAdapter = class {
637
752
  const hash = basename2(f, ".jsonl");
638
753
  let meta = {};
639
754
  try {
640
- meta = JSON.parse(await readFile3(join2(subDir, `${hash}.meta.json`), "utf8"));
755
+ meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
641
756
  } catch {
642
757
  }
643
758
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
644
759
  let raw;
645
760
  try {
646
- raw = await readFile3(join2(subDir, f), "utf8");
761
+ raw = await readFile3(join3(subDir, f), "utf8");
647
762
  } catch {
648
763
  continue;
649
764
  }
@@ -663,7 +778,7 @@ var ClaudeAdapter = class {
663
778
  // src/adapters/codex.ts
664
779
  import { readdir as readdir3, readFile as readFile4, stat as stat3 } from "fs/promises";
665
780
  import { homedir as homedir3 } from "os";
666
- import { basename as basename3, join as join3 } from "path";
781
+ import { basename as basename3, join as join4 } from "path";
667
782
  var SERVICE3 = "codex";
668
783
  function parseLines2(raw) {
669
784
  const out = [];
@@ -699,7 +814,7 @@ async function* walkRollouts(root) {
699
814
  return;
700
815
  }
701
816
  for (const y of years) {
702
- const yp = join3(root, y);
817
+ const yp = join4(root, y);
703
818
  let months;
704
819
  try {
705
820
  months = await readdir3(yp);
@@ -707,7 +822,7 @@ async function* walkRollouts(root) {
707
822
  continue;
708
823
  }
709
824
  for (const m of months) {
710
- const mp = join3(yp, m);
825
+ const mp = join4(yp, m);
711
826
  let days;
712
827
  try {
713
828
  days = await readdir3(mp);
@@ -715,7 +830,7 @@ async function* walkRollouts(root) {
715
830
  continue;
716
831
  }
717
832
  for (const d of days) {
718
- const dp = join3(mp, d);
833
+ const dp = join4(mp, d);
719
834
  let files;
720
835
  try {
721
836
  files = await readdir3(dp);
@@ -723,7 +838,7 @@ async function* walkRollouts(root) {
723
838
  continue;
724
839
  }
725
840
  for (const f of files) {
726
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
841
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
727
842
  }
728
843
  }
729
844
  }
@@ -733,7 +848,7 @@ var CodexAdapter = class {
733
848
  harness = "codex";
734
849
  aliases = ["codex-acp"];
735
850
  root() {
736
- return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
851
+ return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
737
852
  }
738
853
  async locate(opts = {}) {
739
854
  const refs = [];
@@ -782,6 +897,7 @@ var CodexAdapter = class {
782
897
  const toolByCallId = /* @__PURE__ */ new Map();
783
898
  let step = 0;
784
899
  let lastLlm = rootId;
900
+ let sawUserTurn = false;
785
901
  for (const l of lines) {
786
902
  const ts = l.timestamp ?? (/* @__PURE__ */ new Date(0)).toISOString();
787
903
  if (l.type === "event_msg" && l.payload?.type === "token_count") {
@@ -836,6 +952,8 @@ var CodexAdapter = class {
836
952
  } else if (l.type === "response_item" && l.payload?.type === "message" && l.payload.role === "user") {
837
953
  const prompt = textOf3(l.payload.content);
838
954
  if (prompt) {
955
+ const actor = codexActor({ text: prompt, isFirstUserTurn: !sawUserTurn });
956
+ sawUserTurn = true;
839
957
  spans.push(
840
958
  userPromptSpan({
841
959
  traceId,
@@ -845,7 +963,8 @@ var CodexAdapter = class {
845
963
  content: prompt,
846
964
  service: SERVICE3,
847
965
  agent: SERVICE3,
848
- step
966
+ step,
967
+ actor
849
968
  })
850
969
  );
851
970
  step += 1;
@@ -878,7 +997,7 @@ var CodexAdapter = class {
878
997
  // src/adapters/copilot.ts
879
998
  import { readdir as readdir4, readFile as readFile5, stat as stat4 } from "fs/promises";
880
999
  import { homedir as homedir4 } from "os";
881
- import { join as join4 } from "path";
1000
+ import { join as join5 } from "path";
882
1001
  var SERVICE4 = "github-copilot";
883
1002
  function parseLines3(raw) {
884
1003
  const out = [];
@@ -895,7 +1014,7 @@ var CopilotAdapter = class {
895
1014
  harness = "github-copilot";
896
1015
  aliases = ["copilot"];
897
1016
  root() {
898
- return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
1017
+ return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
899
1018
  }
900
1019
  async locate(opts = {}) {
901
1020
  let dirs;
@@ -906,7 +1025,7 @@ var CopilotAdapter = class {
906
1025
  }
907
1026
  const refs = [];
908
1027
  for (const id of dirs) {
909
- const path = join4(this.root(), id, "events.jsonl");
1028
+ const path = join5(this.root(), id, "events.jsonl");
910
1029
  let st;
911
1030
  try {
912
1031
  st = await stat4(path);
@@ -1004,7 +1123,7 @@ var CopilotAdapter = class {
1004
1123
  // src/adapters/factory.ts
1005
1124
  import { readdir as readdir5, readFile as readFile6, stat as stat5 } from "fs/promises";
1006
1125
  import { homedir as homedir5 } from "os";
1007
- import { basename as basename4, join as join5 } from "path";
1126
+ import { basename as basename4, join as join6 } from "path";
1008
1127
  var SERVICE5 = "factory";
1009
1128
  function textOf4(content) {
1010
1129
  return capText(
@@ -1026,7 +1145,7 @@ var FactoryAdapter = class {
1026
1145
  harness = "factory";
1027
1146
  aliases = ["factory-droids", "droid"];
1028
1147
  root() {
1029
- return join5(homedir5(), ".factory", "sessions");
1148
+ return join6(homedir5(), ".factory", "sessions");
1030
1149
  }
1031
1150
  async locate(opts = {}) {
1032
1151
  const root = this.root();
@@ -1038,7 +1157,7 @@ var FactoryAdapter = class {
1038
1157
  }
1039
1158
  const refs = [];
1040
1159
  for (const dir of dirs) {
1041
- const dp = join5(root, dir);
1160
+ const dp = join6(root, dir);
1042
1161
  let files;
1043
1162
  try {
1044
1163
  files = await readdir5(dp);
@@ -1049,7 +1168,7 @@ var FactoryAdapter = class {
1049
1168
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1050
1169
  for (const f of files) {
1051
1170
  if (!f.endsWith(".jsonl")) continue;
1052
- const path = join5(dp, f);
1171
+ const path = join6(dp, f);
1053
1172
  let st;
1054
1173
  try {
1055
1174
  st = await stat5(path);
@@ -1165,7 +1284,7 @@ var FactoryAdapter = class {
1165
1284
  // src/adapters/forge.ts
1166
1285
  import { readdir as readdir6, readFile as readFile7, stat as stat6 } from "fs/promises";
1167
1286
  import { homedir as homedir6 } from "os";
1168
- import { basename as basename5, join as join6 } from "path";
1287
+ import { basename as basename5, join as join7 } from "path";
1169
1288
  var SERVICE6 = "forge";
1170
1289
  function tokens(t) {
1171
1290
  if (!t) return null;
@@ -1179,7 +1298,7 @@ var ForgeAdapter = class {
1179
1298
  aliases = ["forgecode"];
1180
1299
  /** Directories scanned for `*-dump.json` exports. */
1181
1300
  dumpDirs() {
1182
- return [process.cwd(), join6(homedir6(), ".forge")];
1301
+ return [process.cwd(), join7(homedir6(), ".forge")];
1183
1302
  }
1184
1303
  async locate(opts = {}) {
1185
1304
  const refs = [];
@@ -1193,7 +1312,7 @@ var ForgeAdapter = class {
1193
1312
  }
1194
1313
  for (const f of files) {
1195
1314
  if (!f.endsWith("-dump.json")) continue;
1196
- const path = join6(dir, f);
1315
+ const path = join7(dir, f);
1197
1316
  let st;
1198
1317
  try {
1199
1318
  st = await stat6(path);
@@ -1295,7 +1414,7 @@ var ForgeAdapter = class {
1295
1414
  // src/adapters/gemini.ts
1296
1415
  import { readdir as readdir7, readFile as readFile8, stat as stat7 } from "fs/promises";
1297
1416
  import { homedir as homedir7 } from "os";
1298
- import { join as join7 } from "path";
1417
+ import { join as join8 } from "path";
1299
1418
  function iso(ts, fallback) {
1300
1419
  if (typeof ts === "number") return new Date(ts).toISOString();
1301
1420
  if (typeof ts === "string" && ts.length > 0) return ts;
@@ -1320,7 +1439,7 @@ var GeminiFamilyAdapter = class {
1320
1439
  this.homeDirName = cfg.homeDirName;
1321
1440
  }
1322
1441
  chatsRoot() {
1323
- return join7(homedir7(), this.homeDirName, "tmp");
1442
+ return join8(homedir7(), this.homeDirName, "tmp");
1324
1443
  }
1325
1444
  async locate(opts = {}) {
1326
1445
  const root = this.chatsRoot();
@@ -1332,7 +1451,7 @@ var GeminiFamilyAdapter = class {
1332
1451
  }
1333
1452
  const refs = [];
1334
1453
  for (const pd of projectDirs) {
1335
- const chatsDir = join7(root, pd, "chats");
1454
+ const chatsDir = join8(root, pd, "chats");
1336
1455
  let files;
1337
1456
  try {
1338
1457
  files = await readdir7(chatsDir);
@@ -1341,7 +1460,7 @@ var GeminiFamilyAdapter = class {
1341
1460
  }
1342
1461
  for (const f of files) {
1343
1462
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1344
- const path = join7(chatsDir, f);
1463
+ const path = join8(chatsDir, f);
1345
1464
  let st;
1346
1465
  try {
1347
1466
  st = await stat7(path);
@@ -1454,7 +1573,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1454
1573
  // src/adapters/opencode.ts
1455
1574
  import { readdir as readdir8, readFile as readFile9, stat as stat8 } from "fs/promises";
1456
1575
  import { homedir as homedir8 } from "os";
1457
- import { join as join8 } from "path";
1576
+ import { join as join9 } from "path";
1458
1577
  var SERVICE7 = "opencode";
1459
1578
  function isoOf(ms) {
1460
1579
  return new Date(ms ?? 0).toISOString();
@@ -1467,11 +1586,11 @@ function textOf7(parts) {
1467
1586
  var OpencodeAdapter = class {
1468
1587
  harness = "opencode";
1469
1588
  storage() {
1470
- const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
1471
- return join8(base, "opencode", "storage");
1589
+ const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1590
+ return join9(base, "opencode", "storage");
1472
1591
  }
1473
1592
  async locate(opts = {}) {
1474
- const msgRoot = join8(this.storage(), "message");
1593
+ const msgRoot = join9(this.storage(), "message");
1475
1594
  let sessionDirs;
1476
1595
  try {
1477
1596
  sessionDirs = await readdir8(msgRoot);
@@ -1480,7 +1599,7 @@ var OpencodeAdapter = class {
1480
1599
  }
1481
1600
  const refs = [];
1482
1601
  for (const sid of sessionDirs) {
1483
- const dir = join8(msgRoot, sid);
1602
+ const dir = join9(msgRoot, sid);
1484
1603
  let st;
1485
1604
  try {
1486
1605
  st = await stat8(dir);
@@ -1497,7 +1616,7 @@ var OpencodeAdapter = class {
1497
1616
  async parse(ref) {
1498
1617
  const traceId = ref.sessionId;
1499
1618
  const rootId = `root:${traceId}`;
1500
- const partRoot = join8(this.storage(), "part");
1619
+ const partRoot = join9(this.storage(), "part");
1501
1620
  let msgFiles;
1502
1621
  try {
1503
1622
  msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
@@ -1507,7 +1626,7 @@ var OpencodeAdapter = class {
1507
1626
  const messages = [];
1508
1627
  for (const f of msgFiles) {
1509
1628
  try {
1510
- messages.push({ msg: JSON.parse(await readFile9(join8(ref.path, f), "utf8")), file: f });
1629
+ messages.push({ msg: JSON.parse(await readFile9(join9(ref.path, f), "utf8")), file: f });
1511
1630
  } catch {
1512
1631
  }
1513
1632
  }
@@ -1529,13 +1648,13 @@ var OpencodeAdapter = class {
1529
1648
  for (const { msg } of messages) {
1530
1649
  const mid = msg.id ?? `m${step}`;
1531
1650
  const llmId = `llm:${mid}`;
1532
- const pdir = join8(partRoot, mid);
1651
+ const pdir = join9(partRoot, mid);
1533
1652
  const parts = [];
1534
1653
  try {
1535
1654
  const partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
1536
1655
  for (const pf of partFiles) {
1537
1656
  try {
1538
- parts.push(JSON.parse(await readFile9(join8(pdir, pf), "utf8")));
1657
+ parts.push(JSON.parse(await readFile9(join9(pdir, pf), "utf8")));
1539
1658
  } catch {
1540
1659
  }
1541
1660
  }
@@ -1607,7 +1726,7 @@ var OpencodeAdapter = class {
1607
1726
  // src/adapters/pi.ts
1608
1727
  import { readdir as readdir9, readFile as readFile10, stat as stat9 } from "fs/promises";
1609
1728
  import { homedir as homedir9 } from "os";
1610
- import { basename as basename6, join as join9 } from "path";
1729
+ import { basename as basename6, join as join10 } from "path";
1611
1730
  var SERVICE8 = "pi";
1612
1731
  function parseLines5(raw) {
1613
1732
  const out = [];
@@ -1635,7 +1754,7 @@ function textOf8(content) {
1635
1754
  var PiAdapter = class {
1636
1755
  harness = "pi";
1637
1756
  root() {
1638
- return join9(homedir9(), ".pi", "agent", "sessions");
1757
+ return join10(homedir9(), ".pi", "agent", "sessions");
1639
1758
  }
1640
1759
  async locate(opts = {}) {
1641
1760
  const root = this.root();
@@ -1647,7 +1766,7 @@ var PiAdapter = class {
1647
1766
  }
1648
1767
  const refs = [];
1649
1768
  for (const dir of dirs) {
1650
- const dp = join9(root, dir);
1769
+ const dp = join10(root, dir);
1651
1770
  let files;
1652
1771
  try {
1653
1772
  files = await readdir9(dp);
@@ -1658,7 +1777,7 @@ var PiAdapter = class {
1658
1777
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1659
1778
  for (const f of files) {
1660
1779
  if (!f.endsWith(".jsonl")) continue;
1661
- const path = join9(dp, f);
1780
+ const path = join10(dp, f);
1662
1781
  let st;
1663
1782
  try {
1664
1783
  st = await stat9(path);
@@ -1776,7 +1895,7 @@ var PiAdapter = class {
1776
1895
  // src/adapters/qwen.ts
1777
1896
  import { readdir as readdir10, readFile as readFile11, stat as stat10 } from "fs/promises";
1778
1897
  import { homedir as homedir10 } from "os";
1779
- import { basename as basename7, join as join10 } from "path";
1898
+ import { basename as basename7, join as join11 } from "path";
1780
1899
  var SERVICE9 = "qwen";
1781
1900
  function parseLines6(raw) {
1782
1901
  const out = [];
@@ -1797,7 +1916,7 @@ var QwenAdapter = class {
1797
1916
  harness = "qwen";
1798
1917
  aliases = ["qwen-code"];
1799
1918
  root() {
1800
- return join10(homedir10(), ".qwen", "projects");
1919
+ return join11(homedir10(), ".qwen", "projects");
1801
1920
  }
1802
1921
  async locate(opts = {}) {
1803
1922
  const root = this.root();
@@ -1809,7 +1928,7 @@ var QwenAdapter = class {
1809
1928
  }
1810
1929
  const refs = [];
1811
1930
  for (const pd of projectDirs) {
1812
- const chatsDir = join10(root, pd, "chats");
1931
+ const chatsDir = join11(root, pd, "chats");
1813
1932
  let files;
1814
1933
  try {
1815
1934
  files = await readdir10(chatsDir);
@@ -1819,7 +1938,7 @@ var QwenAdapter = class {
1819
1938
  if (opts.cwd) continue;
1820
1939
  for (const f of files) {
1821
1940
  if (!f.endsWith(".jsonl")) continue;
1822
- const path = join10(chatsDir, f);
1941
+ const path = join11(chatsDir, f);
1823
1942
  let st;
1824
1943
  try {
1825
1944
  st = await stat10(path);
@@ -1969,7 +2088,103 @@ function selectAdapters(opts) {
1969
2088
  });
1970
2089
  }
1971
2090
 
2091
+ // src/repo.ts
2092
+ function normalizeRemote(url) {
2093
+ const raw = url.trim();
2094
+ if (!raw) return null;
2095
+ const url2 = raw.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(.+)$/);
2096
+ if (url2) {
2097
+ let rest = url2[1];
2098
+ const at = rest.lastIndexOf("@");
2099
+ if (at !== -1) rest = rest.slice(at + 1);
2100
+ rest = rest.replace(/\.git$/, "").replace(/\/+$/g, "");
2101
+ const slash = rest.indexOf("/");
2102
+ if (slash === -1) return null;
2103
+ const host = rest.slice(0, slash).replace(/:\d+$/, "");
2104
+ const path = rest.slice(slash + 1).replace(/^\/+/, "");
2105
+ return host && path ? `${host}/${path}` : null;
2106
+ }
2107
+ const scp = raw.match(/^[^@/]+@([^:/]+):(.+)$/);
2108
+ if (scp) {
2109
+ const path = scp[2].replace(/\.git$/, "").replace(/^\/+|\/+$/g, "");
2110
+ return path ? `${scp[1]}/${path}` : null;
2111
+ }
2112
+ return null;
2113
+ }
2114
+ async function readGit(cwd) {
2115
+ const { stat: stat11 } = await import("fs/promises");
2116
+ await stat11(`${cwd}/.git`);
2117
+ const { execFile } = await import("child_process");
2118
+ const { promisify } = await import("util");
2119
+ const run = promisify(execFile);
2120
+ const git = async (args) => {
2121
+ try {
2122
+ const { stdout } = await run("git", ["-C", cwd, ...args], { timeout: 5e3 });
2123
+ const v = stdout.trim();
2124
+ return v.length > 0 ? v : null;
2125
+ } catch {
2126
+ return null;
2127
+ }
2128
+ };
2129
+ let remote = await git(["remote", "get-url", "origin"]);
2130
+ if (!remote) {
2131
+ const first = await git(["remote"]);
2132
+ const name = first?.split("\n")[0]?.trim();
2133
+ if (name) remote = await git(["remote", "get-url", name]);
2134
+ }
2135
+ const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"]);
2136
+ const commit = await git(["rev-parse", "--short", "HEAD"]);
2137
+ return {
2138
+ remote,
2139
+ branch: branch === "HEAD" ? null : branch,
2140
+ // detached HEAD → no branch name
2141
+ commit
2142
+ };
2143
+ }
2144
+ async function resolveRepoAttrs(cwd) {
2145
+ if (!cwd) return {};
2146
+ const attrs = { [ATTR.CWD]: cwd };
2147
+ const basename9 = (() => {
2148
+ const trimmed = cwd.replace(/\/+$/g, "");
2149
+ const idx = trimmed.lastIndexOf("/");
2150
+ const name = idx === -1 ? trimmed : trimmed.slice(idx + 1);
2151
+ return name || trimmed;
2152
+ })();
2153
+ try {
2154
+ const { remote, branch, commit } = await readGit(cwd);
2155
+ const normalized = remote ? normalizeRemote(remote) : null;
2156
+ if (normalized) {
2157
+ attrs[ATTR.SUBJECT_KEY] = normalized;
2158
+ attrs[ATTR.GIT_REPOSITORY] = normalized;
2159
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
2160
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
2161
+ return attrs;
2162
+ }
2163
+ if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
2164
+ if (commit) attrs[ATTR.GIT_COMMIT] = commit;
2165
+ } catch {
2166
+ }
2167
+ attrs[ATTR.SUBJECT_KEY] = basename9;
2168
+ return attrs;
2169
+ }
2170
+ function stampRepoAttrs(spans, attrs) {
2171
+ const keys = Object.keys(attrs);
2172
+ if (keys.length === 0) return spans;
2173
+ for (const s of spans) {
2174
+ for (const k of keys) {
2175
+ if (s.attributes[k] === void 0) s.attributes[k] = attrs[k];
2176
+ }
2177
+ }
2178
+ return spans;
2179
+ }
2180
+
1972
2181
  // src/session-source.ts
2182
+ async function parseSession(adapter, ref) {
2183
+ const spans = await adapter.parse(ref);
2184
+ const repoAttrs = await resolveRepoAttrs(ref.cwd);
2185
+ stampRepoAttrs(spans, repoAttrs);
2186
+ return spans;
2187
+ }
1973
2188
  async function* scanSessions(opts) {
1974
2189
  for (const adapter of selectAdapters(opts)) {
1975
2190
  if (opts.signal?.aborted) return;
@@ -1985,7 +2200,7 @@ async function* scanSessions(opts) {
1985
2200
  if (opts.signal?.aborted) return;
1986
2201
  let spans;
1987
2202
  try {
1988
- spans = await adapter.parse(ref);
2203
+ spans = await parseSession(adapter, ref);
1989
2204
  } catch (err) {
1990
2205
  opts.onError?.(err, ref);
1991
2206
  continue;
@@ -1996,106 +2211,233 @@ async function* scanSessions(opts) {
1996
2211
  }
1997
2212
  }
1998
2213
 
1999
- // src/observer.ts
2000
- var sleep = (ms, signal) => new Promise((resolve) => {
2001
- const onAbort = () => {
2002
- clearTimeout(t);
2003
- resolve();
2214
+ // src/evidence.ts
2215
+ import { mkdtemp, writeFile } from "fs/promises";
2216
+ import { tmpdir } from "os";
2217
+ import { join as join12 } from "path";
2218
+ function stringAttr(span2, key) {
2219
+ const value = span2.attributes[key];
2220
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2221
+ }
2222
+ function numberAttr(span2, key) {
2223
+ const value = span2.attributes[key];
2224
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
2225
+ }
2226
+ function spanKind(span2) {
2227
+ return stringAttr(span2, "openinference.span.kind");
2228
+ }
2229
+ function repoFromSpans(spans) {
2230
+ const attrs = {};
2231
+ for (const span2 of spans) {
2232
+ attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
2233
+ attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
2234
+ attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
2235
+ attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
2236
+ attrs.cwd ??= stringAttr(span2, ATTR.CWD);
2237
+ if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd) break;
2238
+ }
2239
+ return attrs;
2240
+ }
2241
+ function timeBounds(spans) {
2242
+ const times = spans.flatMap((span2) => [span2.start_time, span2.end_time]).filter((value) => value && value !== "now").sort();
2243
+ return {
2244
+ firstSpanAt: times[0] ?? null,
2245
+ lastSpanAt: times[times.length - 1] ?? null
2004
2246
  };
2005
- const t = setTimeout(() => {
2006
- signal?.removeEventListener("abort", onAbort);
2007
- resolve();
2008
- }, ms);
2009
- signal?.addEventListener("abort", onAbort, { once: true });
2010
- });
2011
- function loopKey(sessionId, toolName, argHash) {
2012
- return `${sessionId}\0${toolName}\0${argHash}`;
2013
2247
  }
2014
- async function watchSessions(opts) {
2015
- const windowMs = opts.windowMs ?? 30 * 6e4;
2016
- const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
2017
- const minLoop = opts.minLoopOccurrences ?? 3;
2018
- const seen = /* @__PURE__ */ new Map();
2019
- while (!opts.signal?.aborted) {
2020
- let sessionCount = 0;
2021
- for await (const { adapter, ref, spans } of scanSessions({
2022
- adapters: opts.adapters,
2023
- all: opts.all,
2024
- harnesses: opts.harnesses,
2025
- cwd: opts.cwd,
2026
- sinceMs: Date.now() - windowMs,
2027
- signal: opts.signal,
2028
- onError: opts.onError
2029
- })) {
2030
- let report;
2031
- try {
2032
- report = await runPipelines(spans, { minLoopOccurrences: minLoop });
2033
- } catch (err) {
2034
- opts.onError?.(err, ref);
2035
- continue;
2036
- }
2037
- sessionCount += 1;
2038
- if (opts.onReport) {
2039
- try {
2040
- await opts.onReport(ref, report);
2041
- } catch (err) {
2042
- opts.onError?.(err, ref);
2043
- }
2044
- }
2045
- for (const f of report.stuckLoops.findings) {
2046
- const key = loopKey(ref.sessionId, f.toolName, f.argHash);
2047
- if (f.occurrences > (seen.get(key) ?? 0)) {
2048
- seen.set(key, f.occurrences);
2049
- if (opts.onLoop) {
2050
- try {
2051
- await opts.onLoop({
2052
- harness: adapter.harness,
2053
- sessionId: ref.sessionId,
2054
- cwd: ref.cwd,
2055
- toolName: f.toolName,
2056
- argHash: f.argHash,
2057
- occurrences: f.occurrences,
2058
- windowMs: f.windowMs
2059
- });
2060
- } catch (err) {
2061
- opts.onError?.(err, ref);
2062
- }
2063
- }
2248
+ function summarizeTools(spans) {
2249
+ const byTool = /* @__PURE__ */ new Map();
2250
+ for (const span2 of spans) {
2251
+ if (spanKind(span2) !== "TOOL") continue;
2252
+ const name = stringAttr(span2, "tool.name") ?? span2.name.replace(/^tool\./, "");
2253
+ const current = byTool.get(name) ?? { calls: 0, errors: 0 };
2254
+ current.calls += 1;
2255
+ if (span2.status.code === "ERROR") current.errors += 1;
2256
+ byTool.set(name, current);
2257
+ }
2258
+ 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));
2259
+ }
2260
+ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2261
+ const llmSpans = spans.filter((span2) => spanKind(span2) === "LLM");
2262
+ const toolSpans = spans.filter((span2) => spanKind(span2) === "TOOL");
2263
+ const erroredToolCallCount = toolSpans.filter((span2) => span2.status.code === "ERROR").length;
2264
+ const pipelines = await runPipelines(spans, { minLoopOccurrences: opts.minLoopOccurrences });
2265
+ const loopLimit = opts.maxLoopExamples ?? 25;
2266
+ const loopFindings = pipelines.stuckLoops.findings;
2267
+ const { firstSpanAt, lastSpanAt } = timeBounds(spans);
2268
+ return {
2269
+ schemaVersion: 1,
2270
+ kind: "traces.policy_evidence.session",
2271
+ generatedAt: opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
2272
+ session: {
2273
+ harness: ref.harness,
2274
+ sessionId: ref.sessionId,
2275
+ path: ref.path,
2276
+ cwd: ref.cwd,
2277
+ mtimeMs: ref.mtimeMs
2278
+ },
2279
+ repo: repoFromSpans(spans),
2280
+ metrics: {
2281
+ spanCount: spans.length,
2282
+ llmTurnCount: llmSpans.length,
2283
+ toolCallCount: toolSpans.length,
2284
+ erroredToolCallCount,
2285
+ inputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.input_tokens"), 0),
2286
+ outputTokens: llmSpans.reduce((sum, span2) => sum + numberAttr(span2, "llm.output_tokens"), 0),
2287
+ models: [...new Set(llmSpans.map((span2) => stringAttr(span2, "llm.model_name")).filter((value) => Boolean(value)))].sort(),
2288
+ tools: summarizeTools(spans),
2289
+ firstSpanAt,
2290
+ lastSpanAt
2291
+ },
2292
+ signals: {
2293
+ stuckLoopCount: loopFindings.length,
2294
+ affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
2295
+ stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
2296
+ toolName: finding.toolName,
2297
+ occurrences: finding.occurrences
2298
+ })),
2299
+ stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
2300
+ toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
2301
+ },
2302
+ provenance: {
2303
+ source: "traces",
2304
+ evidenceKind: "session-summary",
2305
+ ...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
2306
+ notCampaignCell: true,
2307
+ note: "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell."
2308
+ }
2309
+ };
2310
+ }
2311
+ async function collectPolicyEvidence(opts) {
2312
+ const records = [];
2313
+ for await (const session of scanSessions(opts)) {
2314
+ records.push(await buildPolicyEvidenceRecord(session.ref, session.spans, opts));
2315
+ }
2316
+ return records;
2317
+ }
2318
+ function serializePolicyEvidence(records) {
2319
+ if (records.length === 0) return "";
2320
+ return `${records.map((record) => JSON.stringify(record)).join("\n")}
2321
+ `;
2322
+ }
2323
+ async function writePolicyEvidenceFile(records, outPath) {
2324
+ const path = outPath ?? join12(await mkdtemp(join12(tmpdir(), "traces-evidence-")), "policy-evidence.jsonl");
2325
+ await writeFile(path, serializePolicyEvidence(records), "utf8");
2326
+ return path;
2327
+ }
2328
+
2329
+ // src/external.ts
2330
+ import { spawn } from "child_process";
2331
+ function runCommand(command, args = [], opts = {}) {
2332
+ const timeoutMs = opts.timeoutMs ?? 12e4;
2333
+ const maxBuffer = opts.maxBuffer ?? 32 * 1024 * 1024;
2334
+ return new Promise((resolve, reject) => {
2335
+ let child;
2336
+ try {
2337
+ child = spawn(command, [...args], { signal: opts.signal });
2338
+ } catch (err2) {
2339
+ reject(err2 instanceof Error ? err2 : new Error(String(err2)));
2340
+ return;
2341
+ }
2342
+ let out = "";
2343
+ let err = "";
2344
+ let killed = false;
2345
+ const timer = setTimeout(() => {
2346
+ killed = true;
2347
+ child.kill("SIGKILL");
2348
+ reject(new Error(`${command}: timed out after ${timeoutMs}ms`));
2349
+ }, timeoutMs);
2350
+ const cap = (add, buf) => {
2351
+ const next = buf + add;
2352
+ if (next.length > maxBuffer) {
2353
+ killed = true;
2354
+ child.kill("SIGKILL");
2355
+ clearTimeout(timer);
2356
+ reject(new Error(`${command}: output exceeded ${maxBuffer} bytes`));
2357
+ }
2358
+ return next;
2359
+ };
2360
+ child.stdout?.on("data", (d) => {
2361
+ out = cap(String(d), out);
2362
+ });
2363
+ child.stderr?.on("data", (d) => {
2364
+ err = cap(String(d), err);
2365
+ });
2366
+ child.on("error", (e) => {
2367
+ clearTimeout(timer);
2368
+ if (!killed) reject(e);
2369
+ });
2370
+ child.on("close", (code) => {
2371
+ clearTimeout(timer);
2372
+ if (!killed) resolve({ stdout: out, stderr: err, code });
2373
+ });
2374
+ if (opts.input !== void 0) {
2375
+ child.stdin?.on("error", () => {
2376
+ });
2377
+ child.stdin?.end(opts.input);
2378
+ }
2379
+ });
2380
+ }
2381
+ function commandAnalyzer(spec) {
2382
+ return {
2383
+ name: spec.name,
2384
+ async analyze(otlpPath, opts = {}) {
2385
+ try {
2386
+ const res = await runCommand(spec.command, spec.args(otlpPath, opts.prompt), {
2387
+ signal: opts.signal,
2388
+ timeoutMs: spec.timeoutMs
2389
+ });
2390
+ if (res.code !== 0) {
2391
+ return { analyzer: spec.name, ok: false, output: res.stdout, error: res.stderr.trim() || `exit ${res.code}` };
2064
2392
  }
2393
+ return { analyzer: spec.name, ok: true, output: res.stdout.trim(), findings: spec.parse?.(res.stdout) };
2394
+ } catch (e) {
2395
+ return { analyzer: spec.name, ok: false, output: "", error: e instanceof Error ? e.message : String(e) };
2065
2396
  }
2066
2397
  }
2067
- try {
2068
- opts.onTick?.({ sessions: sessionCount });
2069
- } catch (err) {
2070
- opts.onError?.(err);
2398
+ };
2399
+ }
2400
+ function haloAnalyzer(opts = {}) {
2401
+ const defaultPrompt = opts.defaultPrompt ?? "diagnose";
2402
+ return commandAnalyzer({
2403
+ name: "halo",
2404
+ command: opts.command ?? "halo",
2405
+ args: (otlpPath, prompt) => {
2406
+ const args = [otlpPath, "-p", prompt ?? defaultPrompt];
2407
+ if (opts.model) args.push("-m", opts.model);
2408
+ return args;
2409
+ },
2410
+ timeoutMs: opts.timeoutMs
2411
+ });
2412
+ }
2413
+ function runExternalAnalyzers(otlpPath, analyzers, opts = {}) {
2414
+ return Promise.all(analyzers.map((a) => a.analyze(otlpPath, opts)));
2415
+ }
2416
+ function commandRedactor(spec) {
2417
+ const name = spec.name ?? spec.command;
2418
+ return {
2419
+ name,
2420
+ async redactText(texts) {
2421
+ if (texts.length === 0) return [];
2422
+ const res = await runCommand(spec.command, spec.args ?? [], {
2423
+ input: JSON.stringify(texts),
2424
+ timeoutMs: spec.timeoutMs
2425
+ });
2426
+ if (res.code !== 0) throw new Error(`redactor ${name}: exit ${res.code}: ${res.stderr.trim()}`);
2427
+ let parsed;
2428
+ try {
2429
+ parsed = JSON.parse(res.stdout);
2430
+ } catch {
2431
+ throw new Error(`redactor ${name}: stdout was not JSON`);
2432
+ }
2433
+ if (!Array.isArray(parsed) || parsed.length !== texts.length) {
2434
+ throw new Error(`redactor ${name}: expected a JSON array of ${texts.length} strings`);
2435
+ }
2436
+ return parsed.map(String);
2071
2437
  }
2072
- if (opts.signal?.aborted) break;
2073
- await sleep(intervalMs, opts.signal);
2074
- }
2438
+ };
2075
2439
  }
2076
2440
 
2077
- // src/attributes.ts
2078
- var ATTR = {
2079
- /** = the trace/session id. Wire contract (dedup linkage). */
2080
- SESSION_ID: "tangle.sessionId",
2081
- /** "cli" marks a CLI upload so the platform can drop a re-upload of a session
2082
- * that also streamed live. Wire contract. */
2083
- INGEST_SOURCE: "tangle.ingest_source",
2084
- HARNESS: "tangle.harness",
2085
- CWD: "tangle.cwd",
2086
- GIT_BRANCH: "tangle.git_branch",
2087
- HOST: "tangle.host",
2088
- /** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
2089
- * which collided with `tangle.ingest_source`.) */
2090
- SESSION_FILE: "tangle.session_file",
2091
- UPLOADED_AT: "tangle.uploaded_at",
2092
- UPLOADER: "tangle.uploader",
2093
- REDACTION_VERSION: "redaction.version",
2094
- REDACTION_COUNT: "redaction.count"
2095
- };
2096
- var INGEST_SOURCE_CLI = "cli";
2097
- var DEFAULT_HARNESS = "claude-code";
2098
-
2099
2441
  // src/redact.ts
2100
2442
  import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
2101
2443
  var CODING_REDACTION_RULES = [
@@ -2156,20 +2498,564 @@ async function applyRedactor(spans, redactor) {
2156
2498
  return { spans: out, changed };
2157
2499
  }
2158
2500
 
2159
- // src/upload-state.ts
2501
+ // src/file-export.ts
2160
2502
  import { createHash } from "crypto";
2161
- import { mkdir, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
2503
+ import { mkdtemp as mkdtemp2, readFile as readFile12, writeFile as writeFile2 } from "fs/promises";
2504
+ import { tmpdir as tmpdir2 } from "os";
2505
+ import { join as join13 } from "path";
2506
+ function isObject(value) {
2507
+ return value != null && typeof value === "object" && !Array.isArray(value);
2508
+ }
2509
+ function stringValue(value) {
2510
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2511
+ }
2512
+ function objectValue(value) {
2513
+ return isObject(value) ? value : void 0;
2514
+ }
2515
+ function stableJson(value) {
2516
+ return JSON.stringify(value, (_key, v) => {
2517
+ if (!isObject(v)) return v;
2518
+ return Object.keys(v).sort().reduce((acc, key) => {
2519
+ acc[key] = v[key];
2520
+ return acc;
2521
+ }, {});
2522
+ });
2523
+ }
2524
+ function hashId(value, chars = 16) {
2525
+ return createHash("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
2526
+ }
2527
+ function isoTime(value) {
2528
+ if (typeof value === "string") {
2529
+ const ms = Date.parse(value);
2530
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
2531
+ }
2532
+ if (typeof value === "number" && Number.isFinite(value)) {
2533
+ const ms = value < 1e10 ? value * 1e3 : value;
2534
+ return new Date(ms).toISOString();
2535
+ }
2536
+ return void 0;
2537
+ }
2538
+ function firstTime(obj, keys) {
2539
+ for (const key of keys) {
2540
+ const value = isoTime(obj[key]);
2541
+ if (value) return value;
2542
+ }
2543
+ return void 0;
2544
+ }
2545
+ function firstTimeDeep(value, keys, depth = 5) {
2546
+ if (depth < 0) return void 0;
2547
+ if (isObject(value)) {
2548
+ const direct = firstTime(value, keys);
2549
+ if (direct) return direct;
2550
+ for (const child of Object.values(value)) {
2551
+ const found = firstTimeDeep(child, keys, depth - 1);
2552
+ if (found) return found;
2553
+ }
2554
+ } else if (Array.isArray(value)) {
2555
+ for (const child of value) {
2556
+ const found = firstTimeDeep(child, keys, depth - 1);
2557
+ if (found) return found;
2558
+ }
2559
+ }
2560
+ return void 0;
2561
+ }
2562
+ function copyDefined(target, entries) {
2563
+ for (const [key, value] of Object.entries(entries)) {
2564
+ if (value != null && value !== "") target[key] = value;
2565
+ }
2566
+ }
2567
+ function parseJsonRows(text) {
2568
+ const trimmed = text.trim();
2569
+ if (!trimmed) throw new Error("input file is empty");
2570
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
2571
+ const parsed = JSON.parse(trimmed);
2572
+ if (Array.isArray(parsed)) return { rows: parsed };
2573
+ if (isObject(parsed)) {
2574
+ const events = parsed.events;
2575
+ if (Array.isArray(events)) return { rows: events, wrapper: parsed };
2576
+ return { rows: [parsed] };
2577
+ }
2578
+ throw new Error("input JSON must be an object, array, or JSONL rows");
2579
+ }
2580
+ return {
2581
+ rows: trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line))
2582
+ };
2583
+ }
2584
+ function isPolicyEvidenceRow(row) {
2585
+ return isObject(row) && row.kind === "traces.policy_evidence.session";
2586
+ }
2587
+ function isOpenInferenceRow(row) {
2588
+ if (!isObject(row)) return false;
2589
+ return typeof row.trace_id === "string" && typeof row.span_id === "string" && typeof row.name === "string" && typeof row.start_time === "string" && typeof row.end_time === "string" && isObject(row.status) && isObject(row.attributes) && isObject(row.resource) && isObject(row.scope);
2590
+ }
2591
+ function eventType(row) {
2592
+ return stringValue(row.type) ?? stringValue(row.event) ?? stringValue(row.name) ?? stringValue(row.kind);
2593
+ }
2594
+ function looksLikeSandboxEvent(row) {
2595
+ if (!isObject(row)) return false;
2596
+ const type = eventType(row)?.toLowerCase();
2597
+ return type === "start" || type === "raw" || type === "result" || type === "done" || type === "error";
2598
+ }
2599
+ function detectFormat(rows, requested) {
2600
+ if (requested !== "auto") return requested;
2601
+ if (rows.length === 0) throw new Error("input contains no rows");
2602
+ if (rows.every(isPolicyEvidenceRow)) return "policy-evidence";
2603
+ if (rows.every(isOpenInferenceRow)) return "openinference";
2604
+ if (rows.some(looksLikeSandboxEvent)) return "sandbox-events";
2605
+ throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, or openinference");
2606
+ }
2607
+ function requirePolicyEvidenceRows(rows) {
2608
+ if (!rows.every(isPolicyEvidenceRow)) throw new Error("policy-evidence input must contain only traces.policy_evidence.session rows");
2609
+ return rows;
2610
+ }
2611
+ function requireOpenInferenceRows(rows) {
2612
+ if (!rows.every(isOpenInferenceRow)) throw new Error("openinference input must contain only complete OpenInference span rows");
2613
+ return rows;
2614
+ }
2615
+ function requireObjectRows(rows) {
2616
+ if (!rows.every(isObject)) throw new Error("sandbox-events input must contain only JSON object events");
2617
+ return rows;
2618
+ }
2619
+ function evidenceTimeBounds(record) {
2620
+ const sessionTime = new Date(record.session.mtimeMs).toISOString();
2621
+ const start = record.metrics.firstSpanAt ?? record.generatedAt ?? sessionTime;
2622
+ const end = record.metrics.lastSpanAt ?? start;
2623
+ return { start, end };
2624
+ }
2625
+ function policyEvidenceToSpans(records) {
2626
+ return records.map((record) => {
2627
+ const { start, end } = evidenceTimeBounds(record);
2628
+ const traceId = record.session.sessionId || hashId(record, 32);
2629
+ const extra = {
2630
+ "traces.source_format": "policy-evidence",
2631
+ "traces.policy_evidence.schema_version": record.schemaVersion,
2632
+ "traces.policy_evidence.kind": record.kind,
2633
+ "traces.session.id": record.session.sessionId,
2634
+ "traces.session.path": record.session.path,
2635
+ "traces.session.cwd": record.session.cwd,
2636
+ "traces.session.mtime_ms": record.session.mtimeMs,
2637
+ "traces.metrics.span_count": record.metrics.spanCount,
2638
+ "traces.metrics.llm_turn_count": record.metrics.llmTurnCount,
2639
+ "traces.metrics.tool_call_count": record.metrics.toolCallCount,
2640
+ "traces.metrics.errored_tool_call_count": record.metrics.erroredToolCallCount,
2641
+ "traces.metrics.input_tokens": record.metrics.inputTokens,
2642
+ "traces.metrics.output_tokens": record.metrics.outputTokens,
2643
+ "traces.metrics.models": record.metrics.models,
2644
+ "traces.metrics.tools": stableJson(record.metrics.tools),
2645
+ "traces.signals.stuck_loop_count": record.signals.stuckLoopCount,
2646
+ "traces.signals.affected_run_ratio": record.signals.affectedRunRatio,
2647
+ "traces.signals.stuck_loops": stableJson(record.signals.stuckLoops),
2648
+ "traces.signals.stuck_loops_omitted": record.signals.stuckLoopsOmitted,
2649
+ "traces.signals.tool_error_rate": record.signals.toolErrorRate,
2650
+ "traces.provenance.source": record.provenance.source,
2651
+ "traces.provenance.evidence_kind": record.provenance.evidenceKind,
2652
+ "traces.provenance.otlp_path": record.provenance.otlpPath,
2653
+ [ATTR.SUBJECT_KEY]: record.repo.subjectKey,
2654
+ [ATTR.GIT_REPOSITORY]: record.repo.repository,
2655
+ [ATTR.GIT_BRANCH_NAME]: record.repo.branch,
2656
+ [ATTR.GIT_COMMIT]: record.repo.commit,
2657
+ [ATTR.CWD]: record.repo.cwd ?? record.session.cwd
2658
+ };
2659
+ return span({
2660
+ traceId,
2661
+ spanId: `policy:${hashId(record)}`,
2662
+ parentSpanId: null,
2663
+ name: "policy_evidence.session",
2664
+ kind: "AGENT",
2665
+ startTime: start,
2666
+ endTime: end,
2667
+ service: record.session.harness,
2668
+ agent: "traces",
2669
+ content: capText(stableJson({
2670
+ session: record.session,
2671
+ repo: record.repo,
2672
+ metrics: record.metrics,
2673
+ signals: record.signals,
2674
+ provenance: record.provenance
2675
+ })),
2676
+ extra
2677
+ });
2678
+ });
2679
+ }
2680
+ function findStringKey(value, keys, depth = 4) {
2681
+ if (depth < 0) return void 0;
2682
+ if (isObject(value)) {
2683
+ for (const key of keys) {
2684
+ const found = stringValue(value[key]);
2685
+ if (found) return found;
2686
+ }
2687
+ for (const child of Object.values(value)) {
2688
+ const found = findStringKey(child, keys, depth - 1);
2689
+ if (found) return found;
2690
+ }
2691
+ } else if (Array.isArray(value)) {
2692
+ for (const child of value) {
2693
+ const found = findStringKey(child, keys, depth - 1);
2694
+ if (found) return found;
2695
+ }
2696
+ }
2697
+ return void 0;
2698
+ }
2699
+ function eventTimestamp(row) {
2700
+ return firstTimeDeep(row, [
2701
+ "timestamp",
2702
+ "start_time",
2703
+ "startTime",
2704
+ "startedAt",
2705
+ "createdAt",
2706
+ "created_at",
2707
+ "created",
2708
+ "time"
2709
+ ]);
2710
+ }
2711
+ function eventEndTimestamp(row, fallback) {
2712
+ return firstTimeDeep(row, [
2713
+ "end_time",
2714
+ "endTime",
2715
+ "completedAt",
2716
+ "completed_at",
2717
+ "finishedAt",
2718
+ "updatedAt",
2719
+ "updated_at",
2720
+ "updated"
2721
+ ]) ?? fallback;
2722
+ }
2723
+ function eventStatus(row, type) {
2724
+ const lowered = type.toLowerCase();
2725
+ const errorMessage = stringValue(row.error) ?? findStringKey(row, ["error", "errorMessage", "error_message"], 3);
2726
+ if (lowered.includes("error") || errorMessage) {
2727
+ const message = errorMessage ?? stringValue(row.message);
2728
+ return { status: "ERROR", ...message ? { message: capText(message) } : {} };
2729
+ }
2730
+ return { status: "OK" };
2731
+ }
2732
+ function eventKind(row, type) {
2733
+ const lowered = type.toLowerCase();
2734
+ const tool = findStringKey(row, ["tool", "toolName", "tool_name"], 4);
2735
+ if (tool || lowered.includes("tool") || stableJson(row).includes("tool-invocation")) return { kind: "TOOL", tool };
2736
+ if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
2737
+ return { kind: "CHAIN" };
2738
+ }
2739
+ function sandboxEventsToSpans(rows, wrapper) {
2740
+ const context = wrapper ?? {};
2741
+ 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)}`;
2742
+ const service = findStringKey(context, ["service", "harness", "source"], 2) ?? "sandbox-opencode";
2743
+ const times = rows.flatMap((row) => {
2744
+ const time = eventTimestamp(row);
2745
+ return time ? [time] : [];
2746
+ }).sort();
2747
+ const rootId = `events:${hashId({ traceId, rows: rows.length })}`;
2748
+ const hasError = rows.some((row) => eventStatus(row, eventType(row) ?? "event").status === "ERROR");
2749
+ const rootStart = times[0] ?? (/* @__PURE__ */ new Date(0)).toISOString();
2750
+ const rootEnd = times[times.length - 1] ?? rootStart;
2751
+ const spans = [
2752
+ span({
2753
+ traceId,
2754
+ spanId: rootId,
2755
+ parentSpanId: null,
2756
+ name: "sandbox.events",
2757
+ kind: "AGENT",
2758
+ startTime: rootStart,
2759
+ endTime: rootEnd,
2760
+ status: hasError ? "ERROR" : "OK",
2761
+ service,
2762
+ agent: service,
2763
+ extra: {
2764
+ "traces.source_format": "sandbox-events",
2765
+ "traces.event_count": rows.length
2766
+ }
2767
+ })
2768
+ ];
2769
+ let previousTime = rootStart;
2770
+ rows.forEach((row, index) => {
2771
+ const type = eventType(row) ?? "event";
2772
+ const time = eventTimestamp(row) ?? previousTime;
2773
+ previousTime = time;
2774
+ const { status, message } = eventStatus(row, type);
2775
+ const { kind, tool } = eventKind(row, type);
2776
+ const name = kind === "TOOL" && tool ? `tool.${tool}` : `event.${type}`;
2777
+ const extra = {
2778
+ "traces.source_format": "sandbox-events",
2779
+ "traces.event.type": type,
2780
+ "traces.event.index": index
2781
+ };
2782
+ copyDefined(extra, {
2783
+ "traces.event.id": stringValue(row.id),
2784
+ "traces.event.raw_type": stringValue(row.type),
2785
+ "traces.event.raw_event": stringValue(row.event)
2786
+ });
2787
+ spans.push(span({
2788
+ traceId,
2789
+ spanId: `event:${index}:${hashId(row)}`,
2790
+ parentSpanId: rootId,
2791
+ name,
2792
+ kind,
2793
+ startTime: time,
2794
+ endTime: eventEndTimestamp(row, time),
2795
+ status,
2796
+ statusMessage: message,
2797
+ service,
2798
+ agent: service,
2799
+ tool: kind === "TOOL" ? tool ?? type : null,
2800
+ step: index,
2801
+ content: capText(stableJson(row)),
2802
+ extra
2803
+ }));
2804
+ });
2805
+ return spans;
2806
+ }
2807
+ function statusCode(value) {
2808
+ return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
2809
+ }
2810
+ function openInferenceToSpans(rows) {
2811
+ return rows.map((row) => {
2812
+ const attrs = isObject(row.attributes) ? { ...row.attributes } : {};
2813
+ const resource = objectValue(row.resource);
2814
+ const resourceAttrs = objectValue(resource?.attributes);
2815
+ if (resourceAttrs) copyDefined(attrs, resourceAttrs);
2816
+ const kind = stringValue(row.kind);
2817
+ if (kind && attrs["openinference.span.kind"] == null) attrs["openinference.span.kind"] = kind;
2818
+ const status = objectValue(row.status);
2819
+ const message = stringValue(status?.message);
2820
+ return {
2821
+ trace_id: stringValue(row.trace_id) ?? hashId(row, 32),
2822
+ span_id: stringValue(row.span_id) ?? hashId({ row, field: "span" }),
2823
+ parent_span_id: stringValue(row.parent_span_id) || null,
2824
+ name: stringValue(row.name) ?? "span",
2825
+ start_time: stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2826
+ end_time: stringValue(row.end_time) ?? stringValue(row.start_time) ?? (/* @__PURE__ */ new Date(0)).toISOString(),
2827
+ status: { code: statusCode(status?.code), ...message ? { message } : {} },
2828
+ attributes: attrs
2829
+ };
2830
+ });
2831
+ }
2832
+ function normalizeAttributes(attributes) {
2833
+ const normalized = {};
2834
+ for (const [key, value] of Object.entries(attributes ?? {})) {
2835
+ if (!key || value == null) continue;
2836
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
2837
+ normalized[key] = value;
2838
+ } else if (Array.isArray(value) && value.every(
2839
+ (item) => typeof item === "string" || typeof item === "number" || typeof item === "boolean"
2840
+ )) {
2841
+ normalized[key] = value;
2842
+ } else {
2843
+ normalized[key] = stableJson(value);
2844
+ }
2845
+ }
2846
+ return normalized;
2847
+ }
2848
+ function withExportAttributes(spans, attributes) {
2849
+ const normalized = normalizeAttributes(attributes);
2850
+ if (Object.keys(normalized).length === 0) return [...spans];
2851
+ return spans.map((s) => ({ ...s, attributes: { ...s.attributes, ...normalized } }));
2852
+ }
2853
+ function exportTraceEvidenceRows(rows, opts = {}, wrapper) {
2854
+ const format = detectFormat(rows, opts.format ?? "auto");
2855
+ const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
2856
+ const spans = withExportAttributes(converted, opts.attributes);
2857
+ if (spans.length === 0) throw new Error(`no spans exported from ${format} input`);
2858
+ const redacted = redactSpans(spans);
2859
+ return {
2860
+ format,
2861
+ spans: redacted.spans,
2862
+ redactionCount: redacted.report.redactionCount,
2863
+ redactionsByRule: redacted.report.byRule
2864
+ };
2865
+ }
2866
+ function exportTraceEvidenceText(text, opts = {}) {
2867
+ const parsed = parseJsonRows(text);
2868
+ return exportTraceEvidenceRows(parsed.rows, opts, parsed.wrapper);
2869
+ }
2870
+ async function exportTraceEvidenceFile(inputPath, opts = {}) {
2871
+ const text = await readFile12(inputPath, "utf8");
2872
+ return exportTraceEvidenceText(text, { ...opts, sourcePath: inputPath });
2873
+ }
2874
+ async function writeTraceEvidenceExportFile(inputPath, outPath, opts = {}) {
2875
+ const result = await exportTraceEvidenceFile(inputPath, opts);
2876
+ const path = outPath ?? join13(await mkdtemp2(join13(tmpdir2(), "traces-export-")), "spans.openinference.jsonl");
2877
+ await writeFile2(path, serializeSpans(result.spans), "utf8");
2878
+ return { ...result, path };
2879
+ }
2880
+
2881
+ // src/observer.ts
2882
+ var sleep = (ms, signal) => new Promise((resolve) => {
2883
+ const onAbort = () => {
2884
+ clearTimeout(t);
2885
+ resolve();
2886
+ };
2887
+ const t = setTimeout(() => {
2888
+ signal?.removeEventListener("abort", onAbort);
2889
+ resolve();
2890
+ }, ms);
2891
+ signal?.addEventListener("abort", onAbort, { once: true });
2892
+ });
2893
+ function loopKey(sessionId, toolName2, argHash) {
2894
+ return `${sessionId}\0${toolName2}\0${argHash}`;
2895
+ }
2896
+ async function watchSessions(opts) {
2897
+ const windowMs = opts.windowMs ?? 30 * 6e4;
2898
+ const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
2899
+ const minLoop = opts.minLoopOccurrences ?? 3;
2900
+ const seen = /* @__PURE__ */ new Map();
2901
+ while (!opts.signal?.aborted) {
2902
+ let sessionCount = 0;
2903
+ for await (const { adapter, ref, spans } of scanSessions({
2904
+ adapters: opts.adapters,
2905
+ all: opts.all,
2906
+ harnesses: opts.harnesses,
2907
+ cwd: opts.cwd,
2908
+ sinceMs: Date.now() - windowMs,
2909
+ signal: opts.signal,
2910
+ onError: opts.onError
2911
+ })) {
2912
+ let report;
2913
+ try {
2914
+ report = await runPipelines(spans, { minLoopOccurrences: minLoop });
2915
+ } catch (err) {
2916
+ opts.onError?.(err, ref);
2917
+ continue;
2918
+ }
2919
+ sessionCount += 1;
2920
+ if (opts.onReport) {
2921
+ try {
2922
+ await opts.onReport(ref, report);
2923
+ } catch (err) {
2924
+ opts.onError?.(err, ref);
2925
+ }
2926
+ }
2927
+ for (const f of report.stuckLoops.findings) {
2928
+ const key = loopKey(ref.sessionId, f.toolName, f.argHash);
2929
+ if (f.occurrences > (seen.get(key) ?? 0)) {
2930
+ seen.set(key, f.occurrences);
2931
+ if (opts.onLoop) {
2932
+ try {
2933
+ await opts.onLoop({
2934
+ harness: adapter.harness,
2935
+ sessionId: ref.sessionId,
2936
+ cwd: ref.cwd,
2937
+ toolName: f.toolName,
2938
+ argHash: f.argHash,
2939
+ occurrences: f.occurrences,
2940
+ windowMs: f.windowMs
2941
+ });
2942
+ } catch (err) {
2943
+ opts.onError?.(err, ref);
2944
+ }
2945
+ }
2946
+ }
2947
+ }
2948
+ }
2949
+ try {
2950
+ opts.onTick?.({ sessions: sessionCount });
2951
+ } catch (err) {
2952
+ opts.onError?.(err);
2953
+ }
2954
+ if (opts.signal?.aborted) break;
2955
+ await sleep(intervalMs, opts.signal);
2956
+ }
2957
+ }
2958
+
2959
+ // src/reactions.ts
2960
+ var REACTIONS = ["correction", "frustration", "jargon", "structure", "praise"];
2961
+ function emptySignals() {
2962
+ return { correction: 0, frustration: 0, jargon: 0, structure: 0, praise: 0 };
2963
+ }
2964
+ function spanStep(s) {
2965
+ const v = s.attributes.step;
2966
+ return typeof v === "number" ? v : Number.MAX_SAFE_INTEGER;
2967
+ }
2968
+ function ordered(spans) {
2969
+ return [...spans].sort((a, b) => {
2970
+ const ds = spanStep(a) - spanStep(b);
2971
+ if (ds !== 0) return ds;
2972
+ return a.start_time.localeCompare(b.start_time);
2973
+ });
2974
+ }
2975
+ function isAssistant(s) {
2976
+ const kind = s.attributes["openinference.span.kind"];
2977
+ if (s.name === "user.prompt") return false;
2978
+ return kind === "LLM" || s.name.startsWith("message.assistant");
2979
+ }
2980
+ function isHumanPrompt(s) {
2981
+ return s.name === "user.prompt" && s.attributes[ACTOR_ATTR] === "human";
2982
+ }
2983
+ function contentOf(s) {
2984
+ const c = s.attributes.content;
2985
+ return typeof c === "string" ? c : "";
2986
+ }
2987
+ var MAX_PAIRS = 10;
2988
+ function pairStrength(reactions) {
2989
+ let n = 0;
2990
+ for (const r of reactions) n += CORRECTIVE_REACTIONS.includes(r) ? 2 : 1;
2991
+ return n;
2992
+ }
2993
+ function analyzeReactions(spans) {
2994
+ const byTrace = /* @__PURE__ */ new Map();
2995
+ for (const s of spans) {
2996
+ const arr = byTrace.get(s.trace_id) ?? [];
2997
+ arr.push(s);
2998
+ byTrace.set(s.trace_id, arr);
2999
+ }
3000
+ const sessions = [];
3001
+ const corpus = emptySignals();
3002
+ let corpusTurns = 0;
3003
+ const pairs = [];
3004
+ for (const [sessionId, traceSpans] of byTrace) {
3005
+ const seq = ordered(traceSpans);
3006
+ const sessionSignals = emptySignals();
3007
+ let humanReactionTurns = 0;
3008
+ let lastAssistant = null;
3009
+ for (const s of seq) {
3010
+ if (isAssistant(s)) {
3011
+ const text = contentOf(s);
3012
+ if (text) lastAssistant = text;
3013
+ continue;
3014
+ }
3015
+ if (!isHumanPrompt(s)) continue;
3016
+ if (lastAssistant === null) continue;
3017
+ humanReactionTurns += 1;
3018
+ const reactions = classifyReaction(contentOf(s));
3019
+ if (reactions.length > 0) {
3020
+ for (const r of reactions) {
3021
+ sessionSignals[r] += 1;
3022
+ corpus[r] += 1;
3023
+ }
3024
+ pairs.push({ assistant: lastAssistant, human: contentOf(s), reactions, sessionId });
3025
+ }
3026
+ lastAssistant = null;
3027
+ }
3028
+ corpusTurns += humanReactionTurns;
3029
+ sessions.push({ sessionId, humanReactionTurns, signals: sessionSignals });
3030
+ }
3031
+ const corrective = CORRECTIVE_REACTIONS.reduce((n, r) => n + corpus[r], 0);
3032
+ const positive = corpus.praise;
3033
+ const totalSignals = corrective + positive;
3034
+ const ratio = totalSignals === 0 ? null : positive === 0 ? Number.POSITIVE_INFINITY : corrective / positive;
3035
+ pairs.sort((a, b) => pairStrength(b.reactions) - pairStrength(a.reactions));
3036
+ return {
3037
+ sessions,
3038
+ signals: corpus,
3039
+ humanReactionTurns: corpusTurns,
3040
+ correctiveToPositiveRatio: ratio,
3041
+ triggerPairs: pairs.slice(0, MAX_PAIRS)
3042
+ };
3043
+ }
3044
+
3045
+ // src/upload-state.ts
3046
+ import { createHash as createHash2 } from "crypto";
3047
+ import { mkdir, readFile as readFile13, writeFile as writeFile3 } from "fs/promises";
2162
3048
  import { homedir as homedir11 } from "os";
2163
- import { dirname, join as join11 } from "path";
3049
+ import { dirname, join as join14 } from "path";
2164
3050
  function stateFile() {
2165
- const base = process.env.XDG_STATE_HOME ?? join11(homedir11(), ".local", "state");
2166
- return join11(base, "tangle-traces", "uploaded.json");
3051
+ const base = process.env.XDG_STATE_HOME ?? join14(homedir11(), ".local", "state");
3052
+ return join14(base, "tangle-traces", "uploaded.json");
2167
3053
  }
2168
3054
  function uploadKey(harness, sessionId) {
2169
3055
  return `${harness}:${sessionId}`;
2170
3056
  }
2171
3057
  function sessionHash(spans) {
2172
- const h = createHash("sha256");
3058
+ const h = createHash2("sha256");
2173
3059
  for (const s of spans) {
2174
3060
  h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
2175
3061
  `);
@@ -2178,7 +3064,7 @@ function sessionHash(spans) {
2178
3064
  }
2179
3065
  async function loadState() {
2180
3066
  try {
2181
- return JSON.parse(await readFile12(stateFile(), "utf8"));
3067
+ return JSON.parse(await readFile13(stateFile(), "utf8"));
2182
3068
  } catch {
2183
3069
  return {};
2184
3070
  }
@@ -2186,7 +3072,7 @@ async function loadState() {
2186
3072
  async function saveState(state) {
2187
3073
  const f = stateFile();
2188
3074
  await mkdir(dirname(f), { recursive: true });
2189
- await writeFile2(f, `${JSON.stringify(state, null, 2)}
3075
+ await writeFile3(f, `${JSON.stringify(state, null, 2)}
2190
3076
  `, "utf8");
2191
3077
  }
2192
3078
  function alreadyUploaded(state, harness, sessionId, hash) {
@@ -2194,10 +3080,10 @@ function alreadyUploaded(state, harness, sessionId, hash) {
2194
3080
  }
2195
3081
 
2196
3082
  // src/upload.ts
2197
- import { readFile as readFile13 } from "fs/promises";
3083
+ import { readFile as readFile14 } from "fs/promises";
2198
3084
  import { createRequire } from "module";
2199
3085
  import { hostname } from "os";
2200
- import { basename as basename8, join as join12 } from "path";
3086
+ import { basename as basename8, join as join15 } from "path";
2201
3087
  import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
2202
3088
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
2203
3089
  var require2 = createRequire(import.meta.url);
@@ -2221,7 +3107,7 @@ async function planUpload(opts = {}) {
2221
3107
  async function gitBranch(cwd) {
2222
3108
  if (!cwd) return void 0;
2223
3109
  try {
2224
- const head = await readFile13(join12(cwd, ".git", "HEAD"), "utf8");
3110
+ const head = await readFile14(join15(cwd, ".git", "HEAD"), "utf8");
2225
3111
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
2226
3112
  return m?.[1]?.trim();
2227
3113
  } catch {
@@ -2329,24 +3215,31 @@ async function executeUpload(plan, opts = {}) {
2329
3215
  }
2330
3216
 
2331
3217
  export {
3218
+ countSkillRunsJsonl,
3219
+ analyzeAdoption,
2332
3220
  span,
3221
+ toOpenInferenceSpan,
2333
3222
  serializeSpans,
2334
3223
  writeOtlpFile,
2335
3224
  analyzeSpans,
2336
- runCommand,
2337
- commandAnalyzer,
2338
- toCanonicalOpenInference,
2339
- haloAnalyzer,
2340
- runExternalAnalyzers,
2341
- commandRedactor,
3225
+ ATTR,
3226
+ INGEST_SOURCE_CLI,
3227
+ DEFAULT_HARNESS,
2342
3228
  parseIsoToEpochMs,
2343
3229
  parseSince,
2344
3230
  toRuntimeStore,
2345
3231
  runPipelines,
2346
3232
  CONTENT_CAP,
2347
3233
  capText,
3234
+ ACTOR_ATTR,
2348
3235
  userPromptSpan,
2349
3236
  AmpAdapter,
3237
+ textIsSynthetic,
3238
+ looksLikeAgentPrompt,
3239
+ claudeActor,
3240
+ codexActor,
3241
+ classifyReaction,
3242
+ CORRECTIVE_REACTIONS,
2350
3243
  ClaudeAdapter,
2351
3244
  CodexAdapter,
2352
3245
  CopilotAdapter,
@@ -2360,15 +3253,31 @@ export {
2360
3253
  resolveAdapter,
2361
3254
  knownHarnesses,
2362
3255
  selectAdapters,
3256
+ normalizeRemote,
3257
+ resolveRepoAttrs,
3258
+ stampRepoAttrs,
3259
+ parseSession,
2363
3260
  scanSessions,
2364
- watchSessions,
2365
- ATTR,
2366
- INGEST_SOURCE_CLI,
2367
- DEFAULT_HARNESS,
3261
+ buildPolicyEvidenceRecord,
3262
+ collectPolicyEvidence,
3263
+ serializePolicyEvidence,
3264
+ writePolicyEvidenceFile,
3265
+ runCommand,
3266
+ commandAnalyzer,
3267
+ haloAnalyzer,
3268
+ runExternalAnalyzers,
3269
+ commandRedactor,
2368
3270
  CODING_REDACTION_RULES,
2369
3271
  TRACES_REDACTION_RULES,
2370
3272
  redactSpans,
2371
3273
  applyRedactor,
3274
+ exportTraceEvidenceRows,
3275
+ exportTraceEvidenceText,
3276
+ exportTraceEvidenceFile,
3277
+ writeTraceEvidenceExportFile,
3278
+ watchSessions,
3279
+ REACTIONS,
3280
+ analyzeReactions,
2372
3281
  uploadKey,
2373
3282
  sessionHash,
2374
3283
  loadState,