@tangle-network/traces 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -1,28 +1,44 @@
1
1
  # traces
2
2
 
3
- Point it at your coding-agent session traces and get **failure-mode + efficiency findings** — without instrumenting anything. It reads the logs your harness already writes to disk.
3
+ Analyze your coding-agent session logs for failure modes — stuck loops, token bloat, dropped self-verification — with zero instrumentation. It reads the logs your harness (Claude Code, Codex, OpenCode, …) already writes to disk.
4
4
 
5
- Built for the recurring problem: agents loop, re-send their whole history, stop verifying their own work, and get stuck. `traces` surfaces that from the trace, deterministically.
5
+ `traces` is a CLI and an SDK: run it ad-hoc, or build on it.
6
+
7
+ ## Install
6
8
 
7
9
  ```bash
8
- npx @tangle-network/traces analyze --harness claude-code --last 1
10
+ npm i -g @tangle-network/traces # the `traces` CLI
11
+ npx @tangle-network/traces analyze # or run without installing
12
+ npm i @tangle-network/traces # or use it as a library
9
13
  ```
10
14
 
11
- ## What it does
15
+ ## Quick start
12
16
 
13
- 1. Reads a harness's native session log (no OpenTelemetry setup required).
14
- 2. Normalizes it to OTLP-JSONL — the shape the [`@tangle-network/agent-eval`](https://www.npmjs.com/package/@tangle-network/agent-eval) analyst suite *and* [HALO](https://github.com/context-labs/halo) consume.
15
- 3. Runs the analysts and writes a markdown report.
17
+ ```bash
18
+ traces analyze --harness claude-code --last 1
19
+ ```
16
20
 
17
- The deterministic pass (token-growth, output-decay, tool-monoculture, missing self-verification) needs **no API key and costs nothing**. Add `--llm` for the agentic RLM analysts (failure-mode / knowledge-gap / knowledge-poisoning / improvement).
21
+ ```
22
+ # Trace report — claude-code · 1 session · 2,429 spans
23
+
24
+ ## Stuck loops
25
+ - `bash` ×7 with identical args in 48s (same git status retried, no state change)
26
+
27
+ ## Tool use
28
+ - 1,204 tool calls · duplicate-call 22% · retry 6% · error 8%
29
+ ```
30
+
31
+ The deterministic pass (stuck loops, token growth, tool monoculture, missing self-verification) needs no API key and costs nothing. Add `--llm` for the agentic analysts (failure-mode / knowledge-gap / knowledge-poisoning / improvement); they call OpenAI and respect `--budget`.
32
+
33
+ The written OTLP-JSONL artifact also feeds [HALO](https://github.com/context-labs/halo) (`halo spans.jsonl -p "diagnose"`) — analysis is never locked to one engine.
18
34
 
19
35
  ## Supported harnesses
20
36
 
21
- Covers the coding-agent CLIs in the agent-dev-container nix profile. "Verified" = parser exercised against real on-disk sessions; "fixture" = parser exercised against a schema-accurate fixture (no local sessions to test); see the test suite.
37
+ "Verified" = tested against real sessions; "fixture" = tested against schema-accurate fixtures (no real sessions available).
22
38
 
23
39
  | Harness (aliases) | Reads from | Status |
24
40
  |---|---|---|
25
- | `claude-code` (`claudish`, `openclaw`, `nanoclaw`) | `~/.claude/projects/<cwd>/*.jsonl` (+ subagent sidechains) | verified |
41
+ | `claude-code` (`claude`, `claudish`, `openclaw`, `nanoclaw`) | `~/.claude/projects/<cwd>/*.jsonl` (+ subagent sidechains) | verified |
26
42
  | `codex` (`codex-acp`) | `~/.codex/sessions/**/rollout-*.jsonl` | verified |
27
43
  | `opencode` | `~/.local/share/opencode/storage/` | verified |
28
44
  | `gemini` (`gemini-cli`) | `~/.gemini/tmp/<hash>/chats/session-*.json` | verified |
@@ -31,97 +47,68 @@ Covers the coding-agent CLIs in the agent-dev-container nix profile. "Verified"
31
47
  | `qwen` (`qwen-code`) | `~/.qwen/projects/<cwd>/chats/*.jsonl` | fixture |
32
48
  | `amp` | `~/.local/share/amp/threads/T-*.json` | fixture |
33
49
  | `github-copilot` (`copilot`) | `~/.copilot/session-state/<id>/events.jsonl` | fixture |
34
- | `forge` (`forgecode`) | `/dump` JSON exports (live `~/.forge/.forge.db` SQLite is a follow-up) | fixture |
35
-
36
- Notes: Factory's token totals live in the `.settings.json` sidecar (no per-turn usage). Forge's primary store is SQLite; v1 reads its dependency-free `/dump` exports. ACP-only bridges (`claude-agent-acp`) may not persist a local transcript at all.
50
+ | `forge` (`forgecode`) | `/dump` JSON exports | fixture |
37
51
 
38
- Adding a harness is one file implementing `HarnessTraceAdapter` (`locate` + `parse`) in `src/adapters/`.
52
+ Factory stores token totals in `.settings.json`, not per turn. Forge reads `/dump` JSON exports (live SQLite is a follow-up). ACP-only bridges may not persist a local transcript.
39
53
 
40
54
  ## Commands
41
55
 
42
56
  ```bash
43
- traces list --harness claude-code --last 20 # discover sessions
44
- traces analyze --harness codex --last 1 # $0 deterministic report
57
+ traces list --harness claude-code --last 20 # discover sessions
58
+ traces analyze --harness codex --last 1 # $0 deterministic report
45
59
  traces analyze --all --since 2026-06-18 --out report.md
46
- traces convert --harness claude-code --last 1 --otlp spans.jsonl # OTLP only → HALO
47
- traces analyze --harness claude-code --last 1 --llm # +agentic (needs OPENAI_API_KEY)
48
- traces watch --all # live observer: notify on stuck loops (read-only)
49
- traces upload --since 1h --dry-run # redact + dedup + preview, no network
50
- traces upload --since 24h # upload last day to the Intelligence Platform
60
+ traces convert --harness claude-code --last 1 --otlp spans.jsonl # OTLP only
61
+ traces watch --all # live observer; notify on stuck loops
62
+ traces upload --since 1h --dry-run # redact + dedup + preview, no network
63
+ traces upload --since 24h # upload last day to the Intelligence Platform
51
64
  ```
52
65
 
53
- ### Options
54
-
55
66
  | Flag | Meaning |
56
67
  |---|---|
57
68
  | `--harness <id>` | Harness or alias (default: `claude-code`) |
58
- | `--all` | Sweep every known harness |
69
+ | `--all` | Every known harness |
59
70
  | `--last <n>` | Most-recent N sessions |
60
- | `--session <path>` | Analyze one explicit session file |
61
- | `--cwd <dir>` | Filter sessions by working directory |
71
+ | `--session <path>` | One explicit session file |
72
+ | `--cwd <dir>` | Filter by working directory |
62
73
  | `--since <t>` | `upload`: window — `30m`/`2h`/`7d` or ISO (default 24h); `analyze`: ISO cutoff |
63
- | `--out <path>` | Write report to a file |
74
+ | `--out <path>` | Write the report to a file |
64
75
  | `--otlp <path>` | OTLP artifact path (also the dry-run upload preview) |
65
- | `--llm` | Enable agentic RLM analysts (needs `OPENAI_API_KEY`) |
66
- | `--budget <usd>` | USD cap for agentic analysts |
67
- | `--dry-run` | `upload`: redact + dedup + preview, do not send |
68
- | `--yes`, `-y` | `upload`: skip the confirmation prompt |
69
- | `--interval <s>` / `--window <m>` | `watch`: poll interval / active-session window |
76
+ | `--llm` / `--budget <usd>` | Enable agentic analysts (needs `OPENAI_API_KEY`) / cap their spend |
77
+ | `--interval <s>` / `--window <m>` | `watch`: poll seconds (default 5) / active-session window minutes (default 30) |
70
78
  | `--min-loop <n>` | Identical repeated calls before flagging a loop (default 3) |
79
+ | `--dry-run` / `--yes` | `upload`: preview without sending / skip the confirm prompt |
71
80
 
72
- **Upload** redacts PII/secrets *before anything leaves the machine*, dedups against already-uploaded sessions, and tags each session with metadata (harness, cwd, git branch, host). It needs the platform env: `TANGLE_INGEST_URL` (or `TANGLE_ORCHESTRATOR_URL`), `TANGLE_INGEST_API_KEY` (or `TANGLE_API_KEY`), `TANGLE_TENANT_ID`. Without them, `--dry-run` still works fully.
81
+ `upload` redacts PII/secrets locally before anything leaves the machine, dedups against already-uploaded sessions, and tags each with metadata (harness, cwd, git branch, host). It needs `TANGLE_INGEST_URL` (or `TANGLE_ORCHESTRATOR_URL`), `TANGLE_INGEST_API_KEY` (or `TANGLE_API_KEY`), and `TANGLE_TENANT_ID`; without them, `--dry-run` still works.
73
82
 
74
83
  ## Use as a library
75
84
 
76
- The CLI is a thin layer over an exported SDK — build your own tooling on top:
85
+ The CLI is a thin consumer of these exports. Full runnable examples in [`examples/`](./examples).
77
86
 
78
- ```ts
79
- import {
80
- watchSessions, collectSessions, analyzeSpans, executeUpload,
81
- AnalystRegistry, makeFinding, listAdapters,
82
- } from '@tangle-network/traces'
83
- ```
84
-
85
- **Observe live sessions** and feed findings into your own system (read-only, cancellable):
87
+ | Export | Signature | Use |
88
+ |---|---|---|
89
+ | `analyzeSpans` | `(spans, { registry?, ai?, budgetUsd? }) → AnalyzeResult` | run analysts — built-in, or **your own** via `registry` |
90
+ | `watchSessions` | `(ObserverOptions) → Promise<void>` | live observer; `onLoop` / `onReport` / `signal` / `adapters` |
91
+ | `collectSessions` | `(CollectOptions) → SessionBatch[]` | redacted per-session batches for your own pipeline |
92
+ | `redactSpans` | `(spans, rules?) → { spans, report }` | PII/secret redaction (`TRACES_REDACTION_RULES`) |
93
+ | `planUpload` / `executeUpload` | `(…, { backend? }) → …` | redact + dedup + send to any sink |
94
+ | `listAdapters` / `resolveAdapter` | `() → […]` / `(id) → adapter` | the harness registry |
95
+ | `HarnessTraceAdapter` | interface (`locate` + `parse`) | implement to add a harness |
86
96
 
87
97
  ```ts
88
- const controller = new AbortController()
89
- await watchSessions({
90
- all: true,
91
- signal: controller.signal,
92
- onLoop: (loop) => alertSlack(`stuck loop: ${loop.toolName} ×${loop.occurrences} in ${loop.sessionId}`),
93
- onReport: (ref, report) => metrics.record(ref.harness, report.toolUse),
94
- })
95
- ```
98
+ import { watchSessions, analyzeSpans, AnalystRegistry, makeFinding } from '@tangle-network/traces'
96
99
 
97
- **Run your OWN analysts** (any agent/detector) instead of the built-ins — register against the
98
- [`@tangle-network/agent-eval`](https://www.npmjs.com/package/@tangle-network/agent-eval) `AnalystRegistry`:
100
+ // Observe live sessions, feed findings anywhere (read-only, cancellable):
101
+ const c = new AbortController()
102
+ await watchSessions({ all: true, signal: c.signal, onLoop: (l) => alert(l.toolName, l.occurrences) })
99
103
 
100
- ```ts
104
+ // Run your own analyst instead of the built-ins:
101
105
  const registry = new AnalystRegistry()
102
- registry.register({
103
- id: 'my-detector', description: '…', inputKind: 'trace-store',
104
- cost: { kind: 'deterministic' }, version: '1.0.0',
105
- async analyze(store, ctx) { /* your logic */ return [makeFinding({ /* … */ })] },
106
- })
107
- const { result } = await analyzeSpans(spans, { registry })
108
- ```
109
-
110
- **Collect redacted batches** to feed a vector store / fine-tune corpus / another pipeline:
111
-
112
- ```ts
113
- const batches = await collectSessions({ all: true, sinceMs: Date.now() - 3_600_000 })
114
- // → [{ ref, spans /* redacted */, redaction }]
115
- ```
116
-
117
- **Upload to your own backend** (anything implementing `ingestTraces`), not just Tangle:
118
-
119
- ```ts
120
- await executeUpload(plan, { backend: { async ingestTraces(spans, key) { /* POST anywhere */ return { accepted: spans.length } } } })
106
+ registry.register({ id: 'mine', description: '…', inputKind: 'trace-store', cost: { kind: 'deterministic' }, version: '1.0.0',
107
+ async analyze() { return [makeFinding({ analyst_id: 'mine', area: 'custom', claim: '…', severity: 'info', evidence_refs: [], confidence: 0.9 })] } })
108
+ await analyzeSpans(spans, { registry })
121
109
  ```
122
110
 
123
- **Register a new harness** — implement `HarnessTraceAdapter` (`locate` + `parse`) and pass it via
124
- `adapters: [...]` to `watchSessions`/`collectSessions`, or drop a file in `src/adapters/`.
111
+ Add a new harness: implement `HarnessTraceAdapter` and pass it via `adapters: [...]` to `watchSessions`/`collectSessions`, or drop a file in `src/adapters/`. See [`examples/register-harness.ts`](./examples/register-harness.ts).
125
112
 
126
113
  ## Develop
127
114
 
@@ -31,29 +31,21 @@ function serializeSpans(spans) {
31
31
  `;
32
32
  }
33
33
  async function writeOtlpFile(spans, outPath) {
34
- const { mkdtemp: mkdtemp2, writeFile: writeFile3 } = await import("fs/promises");
35
- const { tmpdir: tmpdir2 } = await import("os");
36
- const { join: join14 } = await import("path");
37
- const path = outPath ?? join14(await mkdtemp2(join14(tmpdir2(), "tangle-traces-")), "spans.otlp.jsonl");
38
- await writeFile3(path, serializeSpans(spans), "utf8");
34
+ const { mkdtemp, writeFile: writeFile2 } = 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 writeFile2(path, serializeSpans(spans), "utf8");
39
39
  return path;
40
40
  }
41
41
 
42
42
  // src/analyze.ts
43
- import { mkdtemp, writeFile } from "fs/promises";
44
- import { tmpdir } from "os";
45
- import { join } from "path";
46
43
  import { buildDefaultAnalystRegistry } from "@tangle-network/agent-eval/analyst";
47
44
  import { OtlpFileTraceStore } from "@tangle-network/agent-eval/traces";
48
- async function writeOtlp(spans, outPath) {
49
- const path = outPath ?? join(await mkdtemp(join(tmpdir(), "traces-")), "spans.otlp.jsonl");
50
- await writeFile(path, serializeSpans(spans), "utf8");
51
- return path;
52
- }
53
45
  var DETERMINISTIC_VIEW_CEILING = 256 * 1024 * 1024;
54
46
  async function analyzeSpans(spans, opts = {}) {
55
47
  if (spans.length === 0) throw new Error("analyzeSpans: no spans to analyze");
56
- const otlpPath = await writeOtlp(spans, opts.otlpOutPath);
48
+ const otlpPath = await writeOtlpFile(spans, opts.otlpOutPath);
57
49
  const runId = opts.runId ?? `traces-${Date.now()}`;
58
50
  const detStore = new OtlpFileTraceStore({ path: otlpPath, perCallByteCeiling: DETERMINISTIC_VIEW_CEILING });
59
51
  await detStore.ensureIndexed();
@@ -135,13 +127,13 @@ async function runPipelines(spans, opts = {}) {
135
127
  // src/adapters/amp.ts
136
128
  import { readdir, readFile, stat } from "fs/promises";
137
129
  import { homedir } from "os";
138
- import { basename, join as join2 } from "path";
130
+ import { basename, join } from "path";
139
131
  var SERVICE = "amp";
140
132
  var AmpAdapter = class {
141
133
  harness = "amp";
142
134
  threadsRoot() {
143
- const base = process.env.XDG_DATA_HOME ?? join2(homedir(), ".local", "share");
144
- return join2(base, "amp", "threads");
135
+ const base = process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share");
136
+ return join(base, "amp", "threads");
145
137
  }
146
138
  async locate(opts = {}) {
147
139
  let files;
@@ -153,7 +145,7 @@ var AmpAdapter = class {
153
145
  const refs = [];
154
146
  for (const f of files) {
155
147
  if (!f.startsWith("T-") || !f.endsWith(".json")) continue;
156
- const path = join2(this.threadsRoot(), f);
148
+ const path = join(this.threadsRoot(), f);
157
149
  let st;
158
150
  try {
159
151
  st = await stat(path);
@@ -235,7 +227,7 @@ var AmpAdapter = class {
235
227
  // src/adapters/claude.ts
236
228
  import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
237
229
  import { homedir as homedir2 } from "os";
238
- import { basename as basename2, join as join3 } from "path";
230
+ import { basename as basename2, join as join2 } from "path";
239
231
  var SERVICE2 = "claude-code";
240
232
  function parseLines(raw) {
241
233
  const out = [];
@@ -333,7 +325,7 @@ var ClaudeAdapter = class {
333
325
  harness = "claude-code";
334
326
  aliases = ["claude", "claudish", "openclaw", "nanoclaw"];
335
327
  root() {
336
- return join3(homedir2(), ".claude", "projects");
328
+ return join2(homedir2(), ".claude", "projects");
337
329
  }
338
330
  async locate(opts = {}) {
339
331
  const root = this.root();
@@ -345,7 +337,7 @@ var ClaudeAdapter = class {
345
337
  }
346
338
  const refs = [];
347
339
  for (const dir of projectDirs) {
348
- const dirPath = join3(root, dir);
340
+ const dirPath = join2(root, dir);
349
341
  let files;
350
342
  try {
351
343
  files = await readdir2(dirPath);
@@ -354,7 +346,7 @@ var ClaudeAdapter = class {
354
346
  }
355
347
  for (const f of files) {
356
348
  if (!f.endsWith(".jsonl")) continue;
357
- const path = join3(dirPath, f);
349
+ const path = join2(dirPath, f);
358
350
  let st;
359
351
  try {
360
352
  st = await stat2(path);
@@ -401,7 +393,7 @@ var ClaudeAdapter = class {
401
393
  }
402
394
  /** Parse `<session>/subagents/agent-*.jsonl`, parenting each under its Agent call. */
403
395
  async foldSubagents(ref, traceId, main, out) {
404
- const subDir = join3(ref.path.replace(/\.jsonl$/, ""), "subagents");
396
+ const subDir = join2(ref.path.replace(/\.jsonl$/, ""), "subagents");
405
397
  let files;
406
398
  try {
407
399
  files = await readdir2(subDir);
@@ -414,13 +406,13 @@ var ClaudeAdapter = class {
414
406
  const hash = basename2(f, ".jsonl");
415
407
  let meta = {};
416
408
  try {
417
- meta = JSON.parse(await readFile2(join3(subDir, `${hash}.meta.json`), "utf8"));
409
+ meta = JSON.parse(await readFile2(join2(subDir, `${hash}.meta.json`), "utf8"));
418
410
  } catch {
419
411
  }
420
412
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
421
413
  let raw;
422
414
  try {
423
- raw = await readFile2(join3(subDir, f), "utf8");
415
+ raw = await readFile2(join2(subDir, f), "utf8");
424
416
  } catch {
425
417
  continue;
426
418
  }
@@ -440,7 +432,7 @@ var ClaudeAdapter = class {
440
432
  // src/adapters/codex.ts
441
433
  import { readdir as readdir3, readFile as readFile3, stat as stat3 } from "fs/promises";
442
434
  import { homedir as homedir3 } from "os";
443
- import { basename as basename3, join as join4 } from "path";
435
+ import { basename as basename3, join as join3 } from "path";
444
436
  var SERVICE3 = "codex";
445
437
  function parseLines2(raw) {
446
438
  const out = [];
@@ -473,7 +465,7 @@ async function* walkRollouts(root) {
473
465
  return;
474
466
  }
475
467
  for (const y of years) {
476
- const yp = join4(root, y);
468
+ const yp = join3(root, y);
477
469
  let months;
478
470
  try {
479
471
  months = await readdir3(yp);
@@ -481,7 +473,7 @@ async function* walkRollouts(root) {
481
473
  continue;
482
474
  }
483
475
  for (const m of months) {
484
- const mp = join4(yp, m);
476
+ const mp = join3(yp, m);
485
477
  let days;
486
478
  try {
487
479
  days = await readdir3(mp);
@@ -489,7 +481,7 @@ async function* walkRollouts(root) {
489
481
  continue;
490
482
  }
491
483
  for (const d of days) {
492
- const dp = join4(mp, d);
484
+ const dp = join3(mp, d);
493
485
  let files;
494
486
  try {
495
487
  files = await readdir3(dp);
@@ -497,7 +489,7 @@ async function* walkRollouts(root) {
497
489
  continue;
498
490
  }
499
491
  for (const f of files) {
500
- if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join4(dp, f);
492
+ if (f.startsWith("rollout-") && f.endsWith(".jsonl")) yield join3(dp, f);
501
493
  }
502
494
  }
503
495
  }
@@ -507,7 +499,7 @@ var CodexAdapter = class {
507
499
  harness = "codex";
508
500
  aliases = ["codex-acp"];
509
501
  root() {
510
- return process.env.CODEX_HOME ? join4(process.env.CODEX_HOME, "sessions") : join4(homedir3(), ".codex", "sessions");
502
+ return process.env.CODEX_HOME ? join3(process.env.CODEX_HOME, "sessions") : join3(homedir3(), ".codex", "sessions");
511
503
  }
512
504
  async locate(opts = {}) {
513
505
  const refs = [];
@@ -635,7 +627,7 @@ var CodexAdapter = class {
635
627
  // src/adapters/copilot.ts
636
628
  import { readdir as readdir4, readFile as readFile4, stat as stat4 } from "fs/promises";
637
629
  import { homedir as homedir4 } from "os";
638
- import { join as join5 } from "path";
630
+ import { join as join4 } from "path";
639
631
  var SERVICE4 = "github-copilot";
640
632
  function parseLines3(raw) {
641
633
  const out = [];
@@ -652,7 +644,7 @@ var CopilotAdapter = class {
652
644
  harness = "github-copilot";
653
645
  aliases = ["copilot"];
654
646
  root() {
655
- return join5(process.env.COPILOT_HOME ?? join5(homedir4(), ".copilot"), "session-state");
647
+ return join4(process.env.COPILOT_HOME ?? join4(homedir4(), ".copilot"), "session-state");
656
648
  }
657
649
  async locate(opts = {}) {
658
650
  let dirs;
@@ -663,7 +655,7 @@ var CopilotAdapter = class {
663
655
  }
664
656
  const refs = [];
665
657
  for (const id of dirs) {
666
- const path = join5(this.root(), id, "events.jsonl");
658
+ const path = join4(this.root(), id, "events.jsonl");
667
659
  let st;
668
660
  try {
669
661
  st = await stat4(path);
@@ -761,7 +753,7 @@ var CopilotAdapter = class {
761
753
  // src/adapters/factory.ts
762
754
  import { readdir as readdir5, readFile as readFile5, stat as stat5 } from "fs/promises";
763
755
  import { homedir as homedir5 } from "os";
764
- import { basename as basename4, join as join6 } from "path";
756
+ import { basename as basename4, join as join5 } from "path";
765
757
  var SERVICE5 = "factory";
766
758
  function parseLines4(raw) {
767
759
  const out = [];
@@ -778,7 +770,7 @@ var FactoryAdapter = class {
778
770
  harness = "factory";
779
771
  aliases = ["factory-droids", "droid"];
780
772
  root() {
781
- return join6(homedir5(), ".factory", "sessions");
773
+ return join5(homedir5(), ".factory", "sessions");
782
774
  }
783
775
  async locate(opts = {}) {
784
776
  const root = this.root();
@@ -790,7 +782,7 @@ var FactoryAdapter = class {
790
782
  }
791
783
  const refs = [];
792
784
  for (const dir of dirs) {
793
- const dp = join6(root, dir);
785
+ const dp = join5(root, dir);
794
786
  let files;
795
787
  try {
796
788
  files = await readdir5(dp);
@@ -801,7 +793,7 @@ var FactoryAdapter = class {
801
793
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
802
794
  for (const f of files) {
803
795
  if (!f.endsWith(".jsonl")) continue;
804
- const path = join6(dp, f);
796
+ const path = join5(dp, f);
805
797
  let st;
806
798
  try {
807
799
  st = await stat5(path);
@@ -899,7 +891,7 @@ var FactoryAdapter = class {
899
891
  // src/adapters/forge.ts
900
892
  import { readdir as readdir6, readFile as readFile6, stat as stat6 } from "fs/promises";
901
893
  import { homedir as homedir6 } from "os";
902
- import { basename as basename5, join as join7 } from "path";
894
+ import { basename as basename5, join as join6 } from "path";
903
895
  var SERVICE6 = "forge";
904
896
  function tokens(t) {
905
897
  if (!t) return null;
@@ -910,7 +902,7 @@ var ForgeAdapter = class {
910
902
  aliases = ["forgecode"];
911
903
  /** Directories scanned for `*-dump.json` exports. */
912
904
  dumpDirs() {
913
- return [process.cwd(), join7(homedir6(), ".forge")];
905
+ return [process.cwd(), join6(homedir6(), ".forge")];
914
906
  }
915
907
  async locate(opts = {}) {
916
908
  const refs = [];
@@ -924,7 +916,7 @@ var ForgeAdapter = class {
924
916
  }
925
917
  for (const f of files) {
926
918
  if (!f.endsWith("-dump.json")) continue;
927
- const path = join7(dir, f);
919
+ const path = join6(dir, f);
928
920
  let st;
929
921
  try {
930
922
  st = await stat6(path);
@@ -1009,7 +1001,7 @@ var ForgeAdapter = class {
1009
1001
  // src/adapters/gemini.ts
1010
1002
  import { readdir as readdir7, readFile as readFile7, stat as stat7 } from "fs/promises";
1011
1003
  import { homedir as homedir7 } from "os";
1012
- import { join as join8 } from "path";
1004
+ import { join as join7 } from "path";
1013
1005
  function iso(ts, fallback) {
1014
1006
  if (typeof ts === "number") return new Date(ts).toISOString();
1015
1007
  if (typeof ts === "string" && ts.length > 0) return ts;
@@ -1030,7 +1022,7 @@ var GeminiFamilyAdapter = class {
1030
1022
  this.homeDirName = cfg.homeDirName;
1031
1023
  }
1032
1024
  chatsRoot() {
1033
- return join8(homedir7(), this.homeDirName, "tmp");
1025
+ return join7(homedir7(), this.homeDirName, "tmp");
1034
1026
  }
1035
1027
  async locate(opts = {}) {
1036
1028
  const root = this.chatsRoot();
@@ -1042,7 +1034,7 @@ var GeminiFamilyAdapter = class {
1042
1034
  }
1043
1035
  const refs = [];
1044
1036
  for (const pd of projectDirs) {
1045
- const chatsDir = join8(root, pd, "chats");
1037
+ const chatsDir = join7(root, pd, "chats");
1046
1038
  let files;
1047
1039
  try {
1048
1040
  files = await readdir7(chatsDir);
@@ -1051,7 +1043,7 @@ var GeminiFamilyAdapter = class {
1051
1043
  }
1052
1044
  for (const f of files) {
1053
1045
  if (!f.startsWith("session-") || !f.endsWith(".json")) continue;
1054
- const path = join8(chatsDir, f);
1046
+ const path = join7(chatsDir, f);
1055
1047
  let st;
1056
1048
  try {
1057
1049
  st = await stat7(path);
@@ -1147,7 +1139,7 @@ var GeminiAdapter = class extends GeminiFamilyAdapter {
1147
1139
  // src/adapters/opencode.ts
1148
1140
  import { readdir as readdir8, readFile as readFile8, stat as stat8 } from "fs/promises";
1149
1141
  import { homedir as homedir8 } from "os";
1150
- import { join as join9 } from "path";
1142
+ import { join as join8 } from "path";
1151
1143
  var SERVICE7 = "opencode";
1152
1144
  function isoOf(ms2) {
1153
1145
  return new Date(ms2 ?? 0).toISOString();
@@ -1155,11 +1147,11 @@ function isoOf(ms2) {
1155
1147
  var OpencodeAdapter = class {
1156
1148
  harness = "opencode";
1157
1149
  storage() {
1158
- const base = process.env.XDG_DATA_HOME ?? join9(homedir8(), ".local", "share");
1159
- return join9(base, "opencode", "storage");
1150
+ const base = process.env.XDG_DATA_HOME ?? join8(homedir8(), ".local", "share");
1151
+ return join8(base, "opencode", "storage");
1160
1152
  }
1161
1153
  async locate(opts = {}) {
1162
- const msgRoot = join9(this.storage(), "message");
1154
+ const msgRoot = join8(this.storage(), "message");
1163
1155
  let sessionDirs;
1164
1156
  try {
1165
1157
  sessionDirs = await readdir8(msgRoot);
@@ -1168,7 +1160,7 @@ var OpencodeAdapter = class {
1168
1160
  }
1169
1161
  const refs = [];
1170
1162
  for (const sid of sessionDirs) {
1171
- const dir = join9(msgRoot, sid);
1163
+ const dir = join8(msgRoot, sid);
1172
1164
  let st;
1173
1165
  try {
1174
1166
  st = await stat8(dir);
@@ -1185,7 +1177,7 @@ var OpencodeAdapter = class {
1185
1177
  async parse(ref) {
1186
1178
  const traceId = ref.sessionId;
1187
1179
  const rootId = `root:${traceId}`;
1188
- const partRoot = join9(this.storage(), "part");
1180
+ const partRoot = join8(this.storage(), "part");
1189
1181
  let msgFiles;
1190
1182
  try {
1191
1183
  msgFiles = (await readdir8(ref.path)).filter((f) => f.endsWith(".json"));
@@ -1195,7 +1187,7 @@ var OpencodeAdapter = class {
1195
1187
  const messages = [];
1196
1188
  for (const f of msgFiles) {
1197
1189
  try {
1198
- messages.push({ msg: JSON.parse(await readFile8(join9(ref.path, f), "utf8")), file: f });
1190
+ messages.push({ msg: JSON.parse(await readFile8(join8(ref.path, f), "utf8")), file: f });
1199
1191
  } catch {
1200
1192
  }
1201
1193
  }
@@ -1235,7 +1227,7 @@ var OpencodeAdapter = class {
1235
1227
  })
1236
1228
  );
1237
1229
  step += 1;
1238
- const pdir = join9(partRoot, mid);
1230
+ const pdir = join8(partRoot, mid);
1239
1231
  let partFiles;
1240
1232
  try {
1241
1233
  partFiles = (await readdir8(pdir)).filter((f) => f.endsWith(".json"));
@@ -1245,7 +1237,7 @@ var OpencodeAdapter = class {
1245
1237
  for (const pf of partFiles) {
1246
1238
  let part;
1247
1239
  try {
1248
- part = JSON.parse(await readFile8(join9(pdir, pf), "utf8"));
1240
+ part = JSON.parse(await readFile8(join8(pdir, pf), "utf8"));
1249
1241
  } catch {
1250
1242
  continue;
1251
1243
  }
@@ -1279,7 +1271,7 @@ var OpencodeAdapter = class {
1279
1271
  // src/adapters/pi.ts
1280
1272
  import { readdir as readdir9, readFile as readFile9, stat as stat9 } from "fs/promises";
1281
1273
  import { homedir as homedir9 } from "os";
1282
- import { basename as basename6, join as join10 } from "path";
1274
+ import { basename as basename6, join as join9 } from "path";
1283
1275
  var SERVICE8 = "pi";
1284
1276
  function parseLines5(raw) {
1285
1277
  const out = [];
@@ -1305,7 +1297,7 @@ function textOf(content) {
1305
1297
  var PiAdapter = class {
1306
1298
  harness = "pi";
1307
1299
  root() {
1308
- return join10(homedir9(), ".pi", "agent", "sessions");
1300
+ return join9(homedir9(), ".pi", "agent", "sessions");
1309
1301
  }
1310
1302
  async locate(opts = {}) {
1311
1303
  const root = this.root();
@@ -1317,7 +1309,7 @@ var PiAdapter = class {
1317
1309
  }
1318
1310
  const refs = [];
1319
1311
  for (const dir of dirs) {
1320
- const dp = join10(root, dir);
1312
+ const dp = join9(root, dir);
1321
1313
  let files;
1322
1314
  try {
1323
1315
  files = await readdir9(dp);
@@ -1328,7 +1320,7 @@ var PiAdapter = class {
1328
1320
  if (opts.cwd && !cwd.startsWith(opts.cwd)) continue;
1329
1321
  for (const f of files) {
1330
1322
  if (!f.endsWith(".jsonl")) continue;
1331
- const path = join10(dp, f);
1323
+ const path = join9(dp, f);
1332
1324
  let st;
1333
1325
  try {
1334
1326
  st = await stat9(path);
@@ -1427,7 +1419,7 @@ var PiAdapter = class {
1427
1419
  // src/adapters/qwen.ts
1428
1420
  import { readdir as readdir10, readFile as readFile10, stat as stat10 } from "fs/promises";
1429
1421
  import { homedir as homedir10 } from "os";
1430
- import { basename as basename7, join as join11 } from "path";
1422
+ import { basename as basename7, join as join10 } from "path";
1431
1423
  var SERVICE9 = "qwen";
1432
1424
  function parseLines6(raw) {
1433
1425
  const out = [];
@@ -1448,7 +1440,7 @@ var QwenAdapter = class {
1448
1440
  harness = "qwen";
1449
1441
  aliases = ["qwen-code"];
1450
1442
  root() {
1451
- return join11(homedir10(), ".qwen", "projects");
1443
+ return join10(homedir10(), ".qwen", "projects");
1452
1444
  }
1453
1445
  async locate(opts = {}) {
1454
1446
  const root = this.root();
@@ -1460,7 +1452,7 @@ var QwenAdapter = class {
1460
1452
  }
1461
1453
  const refs = [];
1462
1454
  for (const pd of projectDirs) {
1463
- const chatsDir = join11(root, pd, "chats");
1455
+ const chatsDir = join10(root, pd, "chats");
1464
1456
  let files;
1465
1457
  try {
1466
1458
  files = await readdir10(chatsDir);
@@ -1470,7 +1462,7 @@ var QwenAdapter = class {
1470
1462
  if (opts.cwd) continue;
1471
1463
  for (const f of files) {
1472
1464
  if (!f.endsWith(".jsonl")) continue;
1473
- const path = join11(chatsDir, f);
1465
+ const path = join10(chatsDir, f);
1474
1466
  let st;
1475
1467
  try {
1476
1468
  st = await stat10(path);
@@ -1596,11 +1588,15 @@ function knownHarnesses() {
1596
1588
 
1597
1589
  // src/observer.ts
1598
1590
  var sleep = (ms2, signal) => new Promise((resolve) => {
1599
- const t = setTimeout(resolve, ms2);
1600
- signal?.addEventListener("abort", () => {
1591
+ const onAbort = () => {
1601
1592
  clearTimeout(t);
1602
1593
  resolve();
1603
- }, { once: true });
1594
+ };
1595
+ const t = setTimeout(() => {
1596
+ signal?.removeEventListener("abort", onAbort);
1597
+ resolve();
1598
+ }, ms2);
1599
+ signal?.addEventListener("abort", onAbort, { once: true });
1604
1600
  });
1605
1601
  function adaptersFor(opts) {
1606
1602
  if (opts.adapters && opts.adapters.length > 0) return opts.adapters;
@@ -1645,27 +1641,41 @@ async function watchSessions(opts) {
1645
1641
  continue;
1646
1642
  }
1647
1643
  sessionCount += 1;
1648
- if (opts.onReport) await opts.onReport(ref, report);
1644
+ if (opts.onReport) {
1645
+ try {
1646
+ await opts.onReport(ref, report);
1647
+ } catch (err) {
1648
+ opts.onError?.(err, ref);
1649
+ }
1650
+ }
1649
1651
  for (const f of report.stuckLoops.findings) {
1650
1652
  const key = loopKey(ref.sessionId, f.toolName, f.argHash);
1651
1653
  if (f.occurrences > (seen.get(key) ?? 0)) {
1652
1654
  seen.set(key, f.occurrences);
1653
1655
  if (opts.onLoop) {
1654
- await opts.onLoop({
1655
- harness: adapter.harness,
1656
- sessionId: ref.sessionId,
1657
- cwd: ref.cwd,
1658
- toolName: f.toolName,
1659
- argHash: f.argHash,
1660
- occurrences: f.occurrences,
1661
- windowMs: f.windowMs
1662
- });
1656
+ try {
1657
+ await opts.onLoop({
1658
+ harness: adapter.harness,
1659
+ sessionId: ref.sessionId,
1660
+ cwd: ref.cwd,
1661
+ toolName: f.toolName,
1662
+ argHash: f.argHash,
1663
+ occurrences: f.occurrences,
1664
+ windowMs: f.windowMs
1665
+ });
1666
+ } catch (err) {
1667
+ opts.onError?.(err, ref);
1668
+ }
1663
1669
  }
1664
1670
  }
1665
1671
  }
1666
1672
  }
1667
1673
  }
1668
- opts.onTick?.({ sessions: sessionCount });
1674
+ try {
1675
+ opts.onTick?.({ sessions: sessionCount });
1676
+ } catch (err) {
1677
+ opts.onError?.(err);
1678
+ }
1669
1679
  if (opts.signal?.aborted) break;
1670
1680
  await sleep(intervalMs, opts.signal);
1671
1681
  }
@@ -1705,12 +1715,12 @@ function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
1705
1715
 
1706
1716
  // src/upload-state.ts
1707
1717
  import { createHash } from "crypto";
1708
- import { mkdir, readFile as readFile11, writeFile as writeFile2 } from "fs/promises";
1718
+ import { mkdir, readFile as readFile11, writeFile } from "fs/promises";
1709
1719
  import { homedir as homedir11 } from "os";
1710
- import { dirname, join as join12 } from "path";
1720
+ import { dirname, join as join11 } from "path";
1711
1721
  function stateFile() {
1712
- const base = process.env.XDG_STATE_HOME ?? join12(homedir11(), ".local", "state");
1713
- return join12(base, "tangle-traces", "uploaded.json");
1722
+ const base = process.env.XDG_STATE_HOME ?? join11(homedir11(), ".local", "state");
1723
+ return join11(base, "tangle-traces", "uploaded.json");
1714
1724
  }
1715
1725
  function uploadKey(harness, sessionId) {
1716
1726
  return `${harness}:${sessionId}`;
@@ -1733,7 +1743,7 @@ async function loadState() {
1733
1743
  async function saveState(state) {
1734
1744
  const f = stateFile();
1735
1745
  await mkdir(dirname(f), { recursive: true });
1736
- await writeFile2(f, `${JSON.stringify(state, null, 2)}
1746
+ await writeFile(f, `${JSON.stringify(state, null, 2)}
1737
1747
  `, "utf8");
1738
1748
  }
1739
1749
  function alreadyUploaded(state, harness, sessionId, hash) {
@@ -1744,7 +1754,7 @@ function alreadyUploaded(state, harness, sessionId, hash) {
1744
1754
  import { readFile as readFile12 } from "fs/promises";
1745
1755
  import { createRequire } from "module";
1746
1756
  import { hostname } from "os";
1747
- import { basename as basename8, join as join13 } from "path";
1757
+ import { basename as basename8, join as join12 } from "path";
1748
1758
  import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
1749
1759
  import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
1750
1760
  var require2 = createRequire(import.meta.url);
@@ -1762,14 +1772,16 @@ async function planUpload(opts) {
1762
1772
  let refs;
1763
1773
  try {
1764
1774
  refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
1765
- } catch {
1775
+ } catch (err) {
1776
+ opts.onError?.(err);
1766
1777
  continue;
1767
1778
  }
1768
1779
  for (const ref of refs) {
1769
1780
  let raw;
1770
1781
  try {
1771
1782
  raw = await adapter.parse(ref);
1772
- } catch {
1783
+ } catch (err) {
1784
+ opts.onError?.(err, ref);
1773
1785
  continue;
1774
1786
  }
1775
1787
  if (raw.length === 0) continue;
@@ -1789,7 +1801,7 @@ async function planUpload(opts) {
1789
1801
  async function gitBranch(cwd) {
1790
1802
  if (!cwd) return void 0;
1791
1803
  try {
1792
- const head = await readFile12(join13(cwd, ".git", "HEAD"), "utf8");
1804
+ const head = await readFile12(join12(cwd, ".git", "HEAD"), "utf8");
1793
1805
  const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
1794
1806
  return m?.[1]?.trim();
1795
1807
  } catch {
@@ -1892,7 +1904,6 @@ export {
1892
1904
  span,
1893
1905
  serializeSpans,
1894
1906
  writeOtlpFile,
1895
- writeOtlp,
1896
1907
  analyzeSpans,
1897
1908
  toRuntimeStore,
1898
1909
  runPipelines,
package/dist/cli.js CHANGED
@@ -8,8 +8,8 @@ import {
8
8
  resolveAdapter,
9
9
  runPipelines,
10
10
  watchSessions,
11
- writeOtlp
12
- } from "./chunk-HPBKRTTP.js";
11
+ writeOtlpFile
12
+ } from "./chunk-YYVPAOOD.js";
13
13
 
14
14
  // src/cli.ts
15
15
  import { stat, writeFile } from "fs/promises";
@@ -254,7 +254,7 @@ async function collectSpans(args) {
254
254
  async function cmdConvert(args) {
255
255
  const { spans } = await collectSpans(args);
256
256
  if (spans.length === 0) throw new Error("no spans found for the given selection");
257
- const path = await writeOtlp(spans, args.otlp);
257
+ const path = await writeOtlpFile(spans, args.otlp);
258
258
  console.log(`wrote ${spans.length} spans \u2192 ${path}`);
259
259
  }
260
260
  async function cmdAnalyze(args) {
package/dist/index.d.ts CHANGED
@@ -442,8 +442,6 @@ interface AnalyzeResult {
442
442
  otlpPath: string;
443
443
  result: Awaited<ReturnType<ReturnType<typeof buildDefaultAnalystRegistry>['run']>>;
444
444
  }
445
- /** Write spans to an OTLP-JSONL file and return its path. */
446
- declare function writeOtlp(spans: readonly OtlpSpan[], outPath?: string): Promise<string>;
447
445
  declare function analyzeSpans(spans: readonly OtlpSpan[], opts?: AnalyzeOptions): Promise<AnalyzeResult>;
448
446
 
449
447
  /**
@@ -553,6 +551,8 @@ interface CollectOptions {
553
551
  last?: number;
554
552
  /** Redact PII/secrets (default true). Set false to get raw spans. */
555
553
  redact?: boolean;
554
+ /** Called on a per-adapter locate/parse failure (collection continues). */
555
+ onError?: (error: unknown, ref?: SessionRef) => void;
556
556
  }
557
557
  interface SessionBatch {
558
558
  ref: SessionRef;
@@ -622,6 +622,8 @@ interface PlanOptions {
622
622
  all?: boolean;
623
623
  cwd?: string;
624
624
  sinceMs?: number;
625
+ /** Called on a per-adapter locate/parse failure (planning continues). */
626
+ onError?: (error: unknown, ref?: SessionRef) => void;
625
627
  }
626
628
  /** Read-only: select sessions in the window, redact, and mark which are new. */
627
629
  declare function planUpload(opts: PlanOptions): Promise<UploadPlan>;
@@ -654,4 +656,4 @@ interface ExecuteOptions {
654
656
  /** Send the NEW items (or, on dryRun, write the redacted OTLP that would send). */
655
657
  declare function executeUpload(plan: UploadPlan, opts?: ExecuteOptions): Promise<UploadResult>;
656
658
 
657
- export { AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, type ExecuteOptions, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type RuntimeTrace, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, planUpload, redactSpans, resolveAdapter, runPipelines, saveState, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, watchSessions, writeOtlp, writeOtlpFile };
659
+ export { AmpAdapter, type AnalyzeOptions, type AnalyzeResult, CODING_REDACTION_RULES, ClaudeAdapter, CodexAdapter, type CollectOptions, CopilotAdapter, type ExecuteOptions, FactoryAdapter, ForgeAdapter, GeminiAdapter, type HarnessTraceAdapter, type LocateOptions, type ObservedLoop, type ObserverOptions, OpencodeAdapter, type OtlpSpan, type OtlpSpanKind, type OtlpStatusCode, PiAdapter, type PipelineOptions, type PipelineReport, type PlanOptions, QwenAdapter, type RuntimeTrace, type SessionBatch, type SessionRef, type SpanInput, type SpanRedaction, TRACES_REDACTION_RULES, type UploadBackend, type UploadItem, type UploadPlan, type UploadRecord, type UploadResult, type UploadState, alreadyUploaded, analyzeSpans, collectSessions, executeUpload, knownHarnesses, listAdapters, loadState, planUpload, redactSpans, resolveAdapter, runPipelines, saveState, serializeSpans, sessionHash, span, toRuntimeStore, toTraceSpanEvents, uploadKey, watchSessions, writeOtlpFile };
package/dist/index.js CHANGED
@@ -29,9 +29,8 @@ import {
29
29
  toTraceSpanEvents,
30
30
  uploadKey,
31
31
  watchSessions,
32
- writeOtlp,
33
32
  writeOtlpFile
34
- } from "./chunk-HPBKRTTP.js";
33
+ } from "./chunk-YYVPAOOD.js";
35
34
 
36
35
  // src/collect.ts
37
36
  function adaptersFor(opts) {
@@ -50,7 +49,8 @@ async function collectSessions(opts = {}) {
50
49
  let refs;
51
50
  try {
52
51
  refs = await adapter.locate({ cwd: opts.cwd, sinceMs: opts.sinceMs });
53
- } catch {
52
+ } catch (err) {
53
+ opts.onError?.(err);
54
54
  continue;
55
55
  }
56
56
  if (opts.last && opts.last > 0) refs = refs.slice(0, opts.last);
@@ -58,7 +58,8 @@ async function collectSessions(opts = {}) {
58
58
  let spans;
59
59
  try {
60
60
  spans = await adapter.parse(ref);
61
- } catch {
61
+ } catch (err) {
62
+ opts.onError?.(err, ref);
62
63
  continue;
63
64
  }
64
65
  if (spans.length === 0) continue;
@@ -121,6 +122,5 @@ export {
121
122
  toTraceSpanEvents,
122
123
  uploadKey,
123
124
  watchSessions,
124
- writeOtlp,
125
125
  writeOtlpFile
126
126
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/traces",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Point it at your coding-agent session traces (Claude Code, Codex, OpenCode, Gemini, Pi, …) and get failure-mode + efficiency findings. CLI + SDK over the @tangle-network/agent-eval analyst suite — observe live sessions, run your own analysts, redact, and upload to the Tangle Intelligence Platform.",
5
5
  "type": "module",
6
6
  "license": "MIT",