@tangle-network/traces 0.23.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +61 -1078
  2. package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
  3. package/dist/ask-XLS3VANY.js +39 -0
  4. package/dist/check-YF6ZHPGG.js +322 -0
  5. package/dist/chunk-3I4PJ5Q7.js +25 -0
  6. package/dist/chunk-3T62R7ON.js +5695 -0
  7. package/dist/chunk-4VKPN432.js +138 -0
  8. package/dist/chunk-6CLC7DKP.js +492 -0
  9. package/dist/chunk-7CBV5RJ2.js +535 -0
  10. package/dist/chunk-B62DOFGI.js +273 -0
  11. package/dist/chunk-CL3OQOWX.js +407 -0
  12. package/dist/chunk-DLGAKCQJ.js +782 -0
  13. package/dist/chunk-DU534HAX.js +263 -0
  14. package/dist/chunk-GLUDGUGJ.js +232 -0
  15. package/dist/chunk-J2PM5SVN.js +503 -0
  16. package/dist/chunk-M3W6TZHQ.js +381 -0
  17. package/dist/chunk-NH7HCDLB.js +759 -0
  18. package/dist/chunk-NZ5Y7UEL.js +1217 -0
  19. package/dist/chunk-O52XK7OB.js +19 -0
  20. package/dist/chunk-PYH3PL3N.js +62 -0
  21. package/dist/chunk-RZL5GAWH.js +60 -0
  22. package/dist/chunk-TR6O4S4T.js +208 -0
  23. package/dist/chunk-TW43MR5D.js +259 -0
  24. package/dist/chunk-TYLFUX42.js +228 -0
  25. package/dist/chunk-UOLRCQHC.js +120 -0
  26. package/dist/chunk-UVE5ANVV.js +3921 -0
  27. package/dist/chunk-V2OCGX57.js +53 -0
  28. package/dist/cli.js +2086 -2050
  29. package/dist/diff-ZZ6KYBPP.js +16 -0
  30. package/dist/improvement-OJQ6365E.js +25 -0
  31. package/dist/index.d.ts +1235 -4357
  32. package/dist/index.js +53 -1204
  33. package/dist/live-Y64AJ6MA.js +14 -0
  34. package/dist/mcp-ZPGTJQYQ.js +85 -0
  35. package/dist/report-JAMXRTCW.js +338 -0
  36. package/dist/run-watch-PLWTIRSV.js +649 -0
  37. package/dist/supervisor-report-RMPLLBGZ.js +29 -0
  38. package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
  39. package/dist/upload-R6NMKSGQ.js +20 -0
  40. package/dist/verify-safe-C5ZXZOZ2.js +8 -0
  41. package/docs/analyze.md +76 -0
  42. package/docs/ask.md +88 -0
  43. package/docs/bundle.md +40 -0
  44. package/docs/check.md +77 -0
  45. package/docs/convert.md +44 -0
  46. package/docs/develop.md +32 -0
  47. package/docs/diff.md +38 -0
  48. package/docs/facts.md +114 -0
  49. package/docs/mcp.md +36 -0
  50. package/docs/migrate-1.0.md +33 -0
  51. package/docs/parsers.md +66 -0
  52. package/docs/release-1.0.0.md +147 -0
  53. package/docs/sdk.md +77 -0
  54. package/docs/select.md +70 -0
  55. package/docs/skills.md +46 -0
  56. package/docs/upload.md +47 -0
  57. package/docs/watch-jsonl.v1.example.jsonl +5 -0
  58. package/docs/watch.md +66 -0
  59. package/examples/README.md +9 -0
  60. package/examples/custom-analyst.ts +65 -74
  61. package/install.sh +22 -2
  62. package/package.json +8 -7
  63. package/skills/inspect-agent-traces/SKILL.md +47 -110
  64. package/dist/chunk-COSWFESO.js +0 -20979
  65. package/docs/replay-verify.md +0 -123
  66. package/docs/stream-findings.svg +0 -39
  67. package/docs/trace-analysts.md +0 -643
  68. package/examples/custom-backend.ts +0 -21
  69. package/examples/external-engines.ts +0 -62
  70. package/examples/improvement-config.mjs +0 -63
  71. package/examples/observe-and-alert.ts +0 -19
  72. package/examples/register-harness.ts +0 -26
  73. package/skills/build-trace-analyst/SKILL.md +0 -76
  74. package/skills/build-trace-analyst/agents/openai.yaml +0 -4
@@ -0,0 +1,273 @@
1
+ import {
2
+ isMetadataAttribute,
3
+ metadataOnly
4
+ } from "./chunk-V2OCGX57.js";
5
+ import {
6
+ scanSessions
7
+ } from "./chunk-3T62R7ON.js";
8
+ import {
9
+ ATTR,
10
+ INGEST_SOURCE_CLI
11
+ } from "./chunk-TR6O4S4T.js";
12
+ import {
13
+ writeOtlpFile
14
+ } from "./chunk-NZ5Y7UEL.js";
15
+ import {
16
+ redactSpans
17
+ } from "./chunk-RZL5GAWH.js";
18
+ import {
19
+ parseIsoToEpochMs
20
+ } from "./chunk-DU534HAX.js";
21
+
22
+ // src/upload.ts
23
+ import { readFile as readFile2 } from "fs/promises";
24
+ import { createRequire } from "module";
25
+ import { hostname } from "os";
26
+ import { basename, join as join2 } from "path";
27
+ import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
28
+ import { assessShareSafety, combineVerdicts, REDACTION_VERSION, redact, shareAllowed } from "@tangle-network/agent-eval/traces";
29
+
30
+ // src/upload-state.ts
31
+ import { mkdir, readFile, writeFile } from "fs/promises";
32
+ import { homedir } from "os";
33
+ import { dirname, join } from "path";
34
+ import { contentHash } from "@tangle-network/agent-eval";
35
+ function stateHome() {
36
+ return process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
37
+ }
38
+ function stateFile() {
39
+ return join(stateHome(), "traces", "uploaded.json");
40
+ }
41
+ function legacyStateFile() {
42
+ return join(stateHome(), "tangle-traces", "uploaded.json");
43
+ }
44
+ function uploadKey(harness, sessionId) {
45
+ return `${harness}:${sessionId}`;
46
+ }
47
+ function sessionHash(spans) {
48
+ return contentHash(spans).slice(0, 32);
49
+ }
50
+ function outboundHash(events, identity) {
51
+ return contentHash({ events, identity }).slice(0, 32);
52
+ }
53
+ async function loadState() {
54
+ for (const file of [stateFile(), legacyStateFile()]) {
55
+ try {
56
+ return JSON.parse(await readFile(file, "utf8"));
57
+ } catch {
58
+ }
59
+ }
60
+ return {};
61
+ }
62
+ async function saveState(state) {
63
+ const f = stateFile();
64
+ await mkdir(dirname(f), { recursive: true });
65
+ await writeFile(f, `${JSON.stringify(state, null, 2)}
66
+ `, "utf8");
67
+ }
68
+ function alreadyUploaded(state, harness, sessionId, hash) {
69
+ return state[uploadKey(harness, sessionId)]?.hash === hash;
70
+ }
71
+
72
+ // src/upload.ts
73
+ var require2 = createRequire(import.meta.url);
74
+ var TRACES_VERSION = require2("../package.json").version;
75
+ async function planUpload(opts = {}) {
76
+ return { ...await planUploadFrom(scanSessions(opts)), sinceMs: opts.sinceMs };
77
+ }
78
+ async function planUploadFrom(sessions) {
79
+ const state = await loadState();
80
+ const items = [];
81
+ for await (const { ref, spans: raw } of sessions) {
82
+ const { spans, report } = redactSpans(raw);
83
+ const hash = sessionHash(spans);
84
+ items.push({
85
+ ref,
86
+ spans,
87
+ redaction: report,
88
+ hash,
89
+ isNew: true
90
+ });
91
+ }
92
+ return { items, state };
93
+ }
94
+ async function gitBranch(cwd) {
95
+ if (!cwd) return void 0;
96
+ try {
97
+ const head = await readFile2(join2(cwd, ".git", "HEAD"), "utf8");
98
+ const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
99
+ return m?.[1]?.trim();
100
+ } catch {
101
+ return void 0;
102
+ }
103
+ }
104
+ async function sessionMeta(item) {
105
+ const meta = {
106
+ [ATTR.HARNESS]: item.ref.harness,
107
+ [ATTR.SESSION_FILE]: basename(item.ref.path),
108
+ [ATTR.HOST]: hostname(),
109
+ [ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
110
+ [ATTR.REDACTION_VERSION]: REDACTION_VERSION,
111
+ [ATTR.REDACTION_COUNT]: item.redaction.redactionCount
112
+ };
113
+ if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
114
+ const branch = await gitBranch(item.ref.cwd);
115
+ if (branch) meta[ATTR.GIT_BRANCH] = branch;
116
+ return redact(meta).value;
117
+ }
118
+ var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
119
+ function toTraceSpanEvents(spans, rootMeta) {
120
+ return spans.map((s) => {
121
+ const attributes = {};
122
+ for (const [k, v] of Object.entries(s.attributes)) {
123
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes[k] = v;
124
+ else if (v != null) attributes[k] = JSON.stringify(v);
125
+ }
126
+ attributes[ATTR.SESSION_ID] = s.trace_id;
127
+ attributes[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
128
+ if (s.parent_span_id === null) Object.assign(attributes, rootMeta);
129
+ const startMs = parseIsoToEpochMs(s.start_time);
130
+ const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
131
+ return {
132
+ traceId: s.trace_id,
133
+ spanId: s.span_id,
134
+ ...s.parent_span_id ? { parentSpanId: s.parent_span_id } : {},
135
+ name: s.name,
136
+ startTimeUnixNano: msToNano(startMs),
137
+ endTimeUnixNano: msToNano(endMs),
138
+ attributes,
139
+ ...s.status.code === "UNSET" ? {} : { status: { code: s.status.code, ...s.status.message ? { message: s.status.message } : {} } }
140
+ };
141
+ });
142
+ }
143
+ var PartialUploadError = class extends Error {
144
+ sessionKey;
145
+ accepted;
146
+ expected;
147
+ constructor(sessionKey, accepted, expected) {
148
+ super(`upload: backend accepted ${accepted} of ${expected} spans for ${sessionKey}`);
149
+ this.name = "PartialUploadError";
150
+ this.sessionKey = sessionKey;
151
+ this.accepted = accepted;
152
+ this.expected = expected;
153
+ }
154
+ };
155
+ async function prepareOutbound(plan, opts, uploadedAt) {
156
+ let candidates = plan.items;
157
+ if (opts.stripContent) {
158
+ candidates = candidates.map((item) => ({ ...item, spans: metadataOnly(item.spans).spans }));
159
+ }
160
+ const identity = {
161
+ stripContent: opts.stripContent === true,
162
+ redactor: null,
163
+ redactionVersion: REDACTION_VERSION
164
+ };
165
+ const items = [];
166
+ const refused = [];
167
+ const verdicts = [];
168
+ for (const item of candidates) {
169
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
170
+ const previous = plan.state[key];
171
+ const meta = await sessionMeta(item);
172
+ if (opts.stripContent) {
173
+ for (const [key2, value] of Object.entries(meta)) if (!isMetadataAttribute(key2, value)) delete meta[key2];
174
+ }
175
+ const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
176
+ let eventTimestamp = previous?.uploadedAt ?? uploadedAt;
177
+ let events = eventsAt(eventTimestamp);
178
+ let hash = outboundHash(events, identity);
179
+ if (alreadyUploaded(plan.state, item.ref.harness, item.ref.sessionId, hash)) continue;
180
+ if (eventTimestamp !== uploadedAt) {
181
+ eventTimestamp = uploadedAt;
182
+ events = eventsAt(eventTimestamp);
183
+ hash = outboundHash(events, identity);
184
+ }
185
+ const verdict = assessShareSafety(events);
186
+ verdicts.push(verdict);
187
+ if (!shareAllowed(verdict)) {
188
+ refused.push({ harness: item.ref.harness, sessionId: item.ref.sessionId, verdict });
189
+ continue;
190
+ }
191
+ items.push({ item, events, hash });
192
+ }
193
+ return {
194
+ items,
195
+ skipped: plan.items.length - items.length - refused.length,
196
+ refused,
197
+ ...verdicts.length > 0 ? { verdict: combineVerdicts("default", verdicts) } : {}
198
+ };
199
+ }
200
+ async function executeUpload(plan, opts = {}) {
201
+ const uploadedAt = (/* @__PURE__ */ new Date()).toISOString();
202
+ const { items, skipped, refused, verdict } = await prepareOutbound(plan, opts, uploadedAt);
203
+ const redactionCount = items.reduce((n, { item }) => n + item.redaction.redactionCount, 0);
204
+ if (opts.dryRun) {
205
+ const path = opts.otlpOut === void 0 ? void 0 : await writeOtlpFile(items.flatMap(({ item }) => item.spans), opts.otlpOut);
206
+ return {
207
+ uploadedSessions: 0,
208
+ skippedSessions: skipped,
209
+ acceptedSpans: 0,
210
+ redactionCount,
211
+ dryRun: true,
212
+ ...path === void 0 ? {} : { otlpPath: path },
213
+ refused,
214
+ ...verdict ? { verdict } : {}
215
+ };
216
+ }
217
+ if (items.length === 0) {
218
+ return {
219
+ uploadedSessions: 0,
220
+ skippedSessions: skipped,
221
+ acceptedSpans: 0,
222
+ redactionCount,
223
+ dryRun: false,
224
+ refused,
225
+ ...verdict ? { verdict } : {}
226
+ };
227
+ }
228
+ const client = opts.backend ?? hostedClientFromEnv();
229
+ if (!client) {
230
+ throw new Error(
231
+ "upload: no backend. Pass opts.backend, or configure the hosted Tangle Intelligence client via TANGLE_INGEST_URL (or TANGLE_ORCHESTRATOR_URL), TANGLE_INGEST_API_KEY (or TANGLE_API_KEY), and TANGLE_TENANT_ID. Use --dry-run to preview without uploading."
232
+ );
233
+ }
234
+ const state = plan.state;
235
+ let acceptedSpans = 0;
236
+ const completed = [];
237
+ for (const outbound of items) {
238
+ const { item, events, hash } = outbound;
239
+ const key = uploadKey(item.ref.harness, item.ref.sessionId);
240
+ const idempotencyKey = `${key}:${hash}`;
241
+ const res = await client.ingestTraces(events, idempotencyKey);
242
+ if (res.accepted !== events.length) throw new PartialUploadError(key, res.accepted, events.length);
243
+ acceptedSpans += res.accepted;
244
+ completed.push({ key, item, hash, spanCount: events.length });
245
+ opts.log?.(`uploaded ${item.ref.harness} ${item.ref.sessionId.slice(0, 8)} \u2014 ${res.accepted} spans`);
246
+ }
247
+ for (const { key, item, hash, spanCount } of completed) {
248
+ state[key] = {
249
+ hash,
250
+ uploadedAt,
251
+ harness: item.ref.harness,
252
+ spanCount
253
+ };
254
+ }
255
+ await saveState(state);
256
+ return {
257
+ uploadedSessions: items.length,
258
+ skippedSessions: skipped,
259
+ acceptedSpans,
260
+ redactionCount,
261
+ dryRun: false,
262
+ refused,
263
+ ...verdict ? { verdict } : {}
264
+ };
265
+ }
266
+
267
+ export {
268
+ planUpload,
269
+ planUploadFrom,
270
+ toTraceSpanEvents,
271
+ PartialUploadError,
272
+ executeUpload
273
+ };
@@ -0,0 +1,407 @@
1
+ import {
2
+ detectEvidenceFormat,
3
+ exportTraceEvidenceFile
4
+ } from "./chunk-6CLC7DKP.js";
5
+ import {
6
+ isCodexExecRecord,
7
+ listAdapters,
8
+ parseSession,
9
+ readsStoreRoots,
10
+ resolveAdapter
11
+ } from "./chunk-3T62R7ON.js";
12
+ import {
13
+ isOtlpJsonFile,
14
+ isStandardOtlpRow,
15
+ readJsonl,
16
+ readOtlpInput
17
+ } from "./chunk-NZ5Y7UEL.js";
18
+
19
+ // src/refs.ts
20
+ import { readFile, stat } from "fs/promises";
21
+ import { homedir } from "os";
22
+ import { join, resolve, sep } from "path";
23
+ import { CODEX_HOME_RULE, STORES, codexHomes, isCodexHome } from "@tangle-network/harness-sessions";
24
+ import { ATTR } from "@tangle-network/agent-trace-contract";
25
+ var MIN_REF_CHARS = 8;
26
+ var RefError = class extends Error {
27
+ code;
28
+ candidates;
29
+ constructor(message, code, candidates = []) {
30
+ super(message);
31
+ this.name = "RefError";
32
+ this.code = code;
33
+ this.candidates = candidates;
34
+ }
35
+ };
36
+ function isObject(value) {
37
+ return value != null && typeof value === "object" && !Array.isArray(value);
38
+ }
39
+ function errorText(error) {
40
+ return error instanceof Error ? error.message : String(error);
41
+ }
42
+ async function statOf(path) {
43
+ try {
44
+ return await stat(path);
45
+ } catch (error) {
46
+ const code = error.code;
47
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
48
+ throw error;
49
+ }
50
+ }
51
+ async function isDirectory(path) {
52
+ return (await statOf(path))?.isDirectory() ?? false;
53
+ }
54
+ async function isFile(path) {
55
+ return (await statOf(path))?.isFile() ?? false;
56
+ }
57
+ function ownHome() {
58
+ return process.env.HOME || homedir();
59
+ }
60
+ var HEAD_ROWS = 20;
61
+ var MAX_WHOLE_JSON_BYTES = 256 * 1024 * 1024;
62
+ var CODEX_ROLLOUT_TYPES = /* @__PURE__ */ new Set(["session_meta", "response_item", "event_msg", "turn_context"]);
63
+ var STORE_PATHS = [
64
+ { harness: "claude-code", root: STORES["claude-code.projects-jsonl"].root },
65
+ { harness: "pi", root: STORES["pi.session-jsonl"].root },
66
+ { harness: "kimi", root: STORES["kimi.session-dir"].root },
67
+ { harness: "factory", root: STORES["factory.session-jsonl"].root }
68
+ ];
69
+ function harnessOfStorePath(path) {
70
+ const absolute = resolve(path);
71
+ for (const { harness, root } of STORE_PATHS) {
72
+ if (absolute.includes(`${sep}${root.split("/").join(sep)}${sep}`)) return harness;
73
+ }
74
+ const segments = absolute.split(sep);
75
+ for (let i = 1; i < segments.length - 1; i += 1) {
76
+ if (!CODEX_HOME_RULE.sessionDirs.includes(segments[i + 1])) continue;
77
+ if (isCodexHome(segments.slice(0, i + 1).join(sep))) return "codex";
78
+ }
79
+ return void 0;
80
+ }
81
+ async function headRows(path) {
82
+ const rows = [];
83
+ let unparsed = 0;
84
+ for await (const row of readJsonl(path, { mode: "recover", onCorruption: () => {
85
+ unparsed += 1;
86
+ } })) {
87
+ rows.push(row);
88
+ if (rows.length >= HEAD_ROWS) return rows;
89
+ if (unparsed > HEAD_ROWS && rows.length === 0) break;
90
+ }
91
+ if (rows.length > 0 && unparsed === 0) {
92
+ const only = rows.length === 1 ? rows[0] : void 0;
93
+ if (Array.isArray(only)) return only;
94
+ if (isObject(only) && Array.isArray(only.events)) return only.events;
95
+ return rows;
96
+ }
97
+ const info = await stat(path);
98
+ if (info.size > MAX_WHOLE_JSON_BYTES) return rows;
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(await readFile(path, "utf8"));
102
+ } catch {
103
+ return rows;
104
+ }
105
+ if (Array.isArray(parsed)) return parsed;
106
+ if (isObject(parsed) && Array.isArray(parsed.events)) return parsed.events;
107
+ return [parsed];
108
+ }
109
+ function isClaudeRecord(row) {
110
+ return isObject(row) && (typeof row.sessionId === "string" || typeof row.session_id === "string") && typeof row.type === "string" && ("message" in row || "uuid" in row || "subtype" in row);
111
+ }
112
+ async function sniffTraceFile(path) {
113
+ const stored = harnessOfStorePath(path);
114
+ if (stored) return { kind: "session", harness: stored };
115
+ if (await isOtlpJsonFile(path)) return { kind: "spans", reader: "otlp" };
116
+ const rows = await headRows(path);
117
+ if (rows.some(isCodexExecRecord)) return { kind: "session", harness: "codex-exec" };
118
+ if (rows.length > 0 && rows.every((row) => isObject(row) && typeof row.trace_id === "string" && typeof row.span_id === "string")) {
119
+ return { kind: "spans", reader: "otlp" };
120
+ }
121
+ if (rows.length > 0 && rows.every(isStandardOtlpRow)) return { kind: "spans", reader: "otlp" };
122
+ if (!rows.some((row) => isObject(row) && typeof row.span_id === "string")) {
123
+ if (rows.some(isClaudeRecord)) return { kind: "session", harness: "claude-code" };
124
+ if (rows.some((row) => isObject(row) && CODEX_ROLLOUT_TYPES.has(String(row.type)) && isObject(row.payload))) {
125
+ return { kind: "session", harness: "codex" };
126
+ }
127
+ const first = rows[0];
128
+ if (isObject(first) && first.type === "session" && typeof first.id === "string") return { kind: "session", harness: "pi" };
129
+ if (rows.some((row) => isObject(row) && row.type === "session_start")) return { kind: "session", harness: "factory" };
130
+ }
131
+ const evidence = detectEvidenceFormat(rows);
132
+ if (evidence === "sandbox-events") {
133
+ for await (const row of readJsonl(path, { mode: "recover", onCorruption: () => {
134
+ } })) {
135
+ if (isCodexExecRecord(row)) return { kind: "session", harness: "codex-exec" };
136
+ }
137
+ }
138
+ if (evidence) return { kind: "spans", reader: evidence };
139
+ return { kind: "spans", reader: "otlp" };
140
+ }
141
+ function adapterFor(harness) {
142
+ const adapter = resolveAdapter(harness);
143
+ if (!adapter) {
144
+ throw new RefError(`unknown harness "${harness}"; known: ${listAdapters().map((a) => a.harness).join(", ")}`, "usage");
145
+ }
146
+ return adapter;
147
+ }
148
+ async function fileRef(path, ref, selector, options) {
149
+ if (options.format) return { kind: "spans", ref, path, reader: options.format, ...selector ? { selector } : {} };
150
+ const sniffed = options.harness ? { kind: "session", harness: options.harness } : await sniffTraceFile(path);
151
+ if (sniffed.kind === "spans") return { kind: "spans", ref, path, reader: sniffed.reader, ...selector ? { selector } : {} };
152
+ if (selector) throw new RefError(`${ref}: a #selector picks a run from a span file; ${path} is a ${sniffed.harness} session`, "usage");
153
+ const adapter = adapterFor(sniffed.harness);
154
+ const info = await stat(path);
155
+ return {
156
+ kind: "session",
157
+ ref,
158
+ adapter,
159
+ session: { harness: adapter.harness, sessionId: ref, path, cwd: null, mtimeMs: info.mtimeMs }
160
+ };
161
+ }
162
+ var HOME_STORES = [
163
+ STORES["claude-code.projects-jsonl"].root,
164
+ STORES["opencode.sqlite"].root,
165
+ STORES["pi.session-jsonl"].root,
166
+ STORES["kimi.session-dir"].root,
167
+ STORES["factory.session-jsonl"].root
168
+ ];
169
+ async function isStoreRoot(dir) {
170
+ for (const root of HOME_STORES) if (await isDirectory(join(dir, root))) return true;
171
+ for (const home of await codexHomes(dir)) {
172
+ for (const sessions of CODEX_HOME_RULE.sessionDirs) if (await isDirectory(join(home, sessions))) return true;
173
+ }
174
+ return false;
175
+ }
176
+ var BUNDLE_SPAN_FILES = ["spans.otlp.jsonl", join("derived", "trace.otlp.jsonl")];
177
+ async function directoryRef(path, ref) {
178
+ if (await isFile(join(path, "manifest.json"))) {
179
+ for (const file of BUNDLE_SPAN_FILES) {
180
+ if (await isFile(join(path, file))) return { kind: "spans", ref, path: join(path, file), reader: "otlp" };
181
+ }
182
+ throw new RefError(`${ref} has a manifest.json but no spans.otlp.jsonl; it is not a traces bundle`, "unreadable");
183
+ }
184
+ const { findSupervisorRunDirs, isRuntimeSupervisorRunDir } = await import("@tangle-network/agent-eval/supervisor-run");
185
+ if (await isRuntimeSupervisorRunDir(path)) return { kind: "run-dir", ref, path };
186
+ if (await isStoreRoot(path)) return { kind: "store-root", ref, path: resolve(path) };
187
+ if ((await findSupervisorRunDirs(path)).length > 0) return { kind: "run-dir", ref, path };
188
+ return { kind: "spans", ref, path, reader: "otlp" };
189
+ }
190
+ function looksLikePath(ref) {
191
+ return ref.includes("/") || ref.startsWith(".") || ref.startsWith("~") || /\.(?:jsonl|ndjson|json|db)$/iu.test(ref);
192
+ }
193
+ function candidate({ adapter, session }) {
194
+ return { harness: adapter.harness, sessionId: session.sessionId, path: session.path };
195
+ }
196
+ var COMPLETE_ID_CHARS = 20;
197
+ async function lookupSession(query, options, storeRoot) {
198
+ const short = query.length < MIN_REF_CHARS;
199
+ const home = storeRoot ?? ownHome();
200
+ const selected = options.harness ? [adapterFor(options.harness)] : listAdapters();
201
+ const adapters = storeRoot === void 0 ? selected : selected.filter(readsStoreRoots);
202
+ const failures = /* @__PURE__ */ new Map();
203
+ const listed = /* @__PURE__ */ new Map();
204
+ const sessionsOf = (adapter, exact) => {
205
+ if (readsStoreRoots(adapter) && exact) return adapter.locateIn(home, { nativeSessionId: query });
206
+ let all = listed.get(adapter);
207
+ if (!all) {
208
+ all = readsStoreRoots(adapter) ? adapter.locateIn(home) : adapter.locate();
209
+ listed.set(adapter, all);
210
+ }
211
+ return all;
212
+ };
213
+ const search = async (exact) => {
214
+ const found2 = /* @__PURE__ */ new Map();
215
+ for (const adapter of adapters) {
216
+ let sessions;
217
+ try {
218
+ sessions = await sessionsOf(adapter, exact);
219
+ } catch (error) {
220
+ failures.set(adapter.harness, errorText(error));
221
+ continue;
222
+ }
223
+ for (const session of sessions) {
224
+ const id = session.sessionId;
225
+ if (id === query || !exact && (id.startsWith(query) || id.endsWith(query))) {
226
+ found2.set(`${adapter.harness}\0${session.path}\0${id}`, { adapter, session });
227
+ }
228
+ }
229
+ }
230
+ return [...found2.values()].sort((a, b) => b.session.mtimeMs - a.session.mtimeMs);
231
+ };
232
+ let found = query.length >= COMPLETE_ID_CHARS || short ? await search(true) : [];
233
+ if (found.length === 0 && short) {
234
+ throw new RefError(
235
+ `"${query}" is not a file, a directory or a session id, and a session id prefix or suffix needs at least ${MIN_REF_CHARS} characters`,
236
+ "usage"
237
+ );
238
+ }
239
+ if (found.length === 0) {
240
+ const matched = await search(false);
241
+ const exact = matched.filter(({ session }) => session.sessionId === query);
242
+ found = exact.length > 0 ? exact : matched;
243
+ }
244
+ if (found.length === 1) return { kind: "session", ref: query, adapter: found[0].adapter, session: found[0].session };
245
+ const where = storeRoot === void 0 ? "" : ` under ${storeRoot}`;
246
+ if (found.length === 0) {
247
+ const unread = failures.size > 0 ? `; could not read ${[...failures].map(([harness, reason]) => `${harness}: ${reason}`).join("; ")}` : "";
248
+ throw new RefError(
249
+ `no session id is, starts with or ends with "${query}"${where} (${adapters.map((a) => a.harness).join(", ")})${unread}; \`traces list\` shows ids`,
250
+ "unreadable"
251
+ );
252
+ }
253
+ const shown = found.slice(0, 5).map((item) => `${item.adapter.harness} ${item.session.sessionId}`);
254
+ const more = found.length > shown.length ? ` and ${found.length - shown.length} more` : "";
255
+ throw new RefError(
256
+ `"${query}" matches ${found.length} sessions${where} (${shown.join(", ")}${more}); pass more of the id`,
257
+ "ambiguous",
258
+ found.map(candidate)
259
+ );
260
+ }
261
+ function shortestRefs(ids) {
262
+ const unique = [...new Set(ids)];
263
+ const reverse = (text) => [...text].reverse().join("");
264
+ const forward = [...unique].sort();
265
+ const backward = unique.map(reverse).sort();
266
+ const starting = (sorted, prefix) => {
267
+ let low = 0;
268
+ let high = sorted.length;
269
+ while (low < high) {
270
+ const mid = low + high >> 1;
271
+ if (sorted[mid] < prefix) low = mid + 1;
272
+ else high = mid;
273
+ }
274
+ const found = [];
275
+ for (let i = low; i < sorted.length && found.length < 2 && sorted[i].startsWith(prefix); i += 1) found.push(sorted[i]);
276
+ return found;
277
+ };
278
+ const alone = (id, form) => {
279
+ const matches = /* @__PURE__ */ new Set([...starting(forward, form), ...starting(backward, reverse(form)).map(reverse)]);
280
+ return matches.size === 1 && matches.has(id);
281
+ };
282
+ const shortest = /* @__PURE__ */ new Map();
283
+ for (const id of unique) {
284
+ let best = id;
285
+ for (let length = MIN_REF_CHARS; length < id.length; length += 1) {
286
+ const prefix = id.slice(0, length);
287
+ const suffix = id.slice(-length);
288
+ if (alone(id, prefix)) {
289
+ best = prefix;
290
+ break;
291
+ }
292
+ if (alone(id, suffix)) {
293
+ best = suffix;
294
+ break;
295
+ }
296
+ }
297
+ shortest.set(id, best);
298
+ }
299
+ return shortest;
300
+ }
301
+ async function splitSelector(ref) {
302
+ const hash = ref.lastIndexOf("#");
303
+ if (hash <= 0 || hash === ref.length - 1 || await statOf(ref) !== null) return { path: ref };
304
+ const path = ref.slice(0, hash);
305
+ return await statOf(path) === null ? { path: ref } : { path, selector: ref.slice(hash + 1) };
306
+ }
307
+ async function pathRef(ref, options) {
308
+ const { path, selector } = await splitSelector(ref);
309
+ const info = await statOf(path);
310
+ if (info === null) {
311
+ if (looksLikePath(ref)) throw new RefError(`no such file or directory: ${ref}`, "unreadable");
312
+ return void 0;
313
+ }
314
+ if (info.isDirectory()) {
315
+ if (selector) throw new RefError(`${ref}: a #selector picks a run from a span file, not a directory`, "usage");
316
+ return directoryRef(path, ref);
317
+ }
318
+ if (!info.isFile()) throw new RefError(`${ref} is neither a file nor a directory`, "unreadable");
319
+ return fileRef(path, ref, selector, options);
320
+ }
321
+ async function resolveRefs(refs, options = {}) {
322
+ const byPath = await Promise.all(refs.map((ref) => pathRef(ref, options)));
323
+ const roots = byPath.filter((ref) => ref?.kind === "store-root");
324
+ if (roots.length > 1) {
325
+ throw new RefError(`name one store root per command, not ${roots.length}: ${roots.map((root) => root.ref).join(", ")}`, "usage");
326
+ }
327
+ const resolved = [];
328
+ for (const [index, ref] of refs.entries()) {
329
+ resolved.push(byPath[index] ?? await lookupSession(ref, options, roots[0]?.path));
330
+ }
331
+ return resolved;
332
+ }
333
+ function branchSpans(spans, branch) {
334
+ const byId = /* @__PURE__ */ new Map();
335
+ for (const span of spans) byId.set(span.span_id, span);
336
+ const children = /* @__PURE__ */ new Map();
337
+ for (const span of spans) {
338
+ if (!span.parent_span_id) continue;
339
+ const list = children.get(span.parent_span_id);
340
+ if (list) list.push(span);
341
+ else children.set(span.parent_span_id, [span]);
342
+ }
343
+ const matched = spans.filter(
344
+ (span) => span.attributes[ATTR.branchId] === branch || span.attributes[ATTR.branchArm] === branch
345
+ );
346
+ const matchedIds = new Set(matched.map((span) => span.span_id));
347
+ const hasMatchedAncestor = (span) => {
348
+ let current = span;
349
+ while (current.parent_span_id) {
350
+ const parent = byId.get(current.parent_span_id);
351
+ if (!parent) return false;
352
+ if (matchedIds.has(parent.span_id)) return true;
353
+ current = parent;
354
+ }
355
+ return false;
356
+ };
357
+ const roots = matched.filter((span) => !hasMatchedAncestor(span));
358
+ if (roots.length > 1) {
359
+ const named = roots.map((span) => `${span.span_id} (${ATTR.branchId}=${JSON.stringify(span.attributes[ATTR.branchId] ?? null)})`).join(", ");
360
+ throw new Error(
361
+ `"${branch}" matches ${roots.length} separate subtrees, not one arm: ${named} \u2014 sibling arms can share a branch id; select by span id, or narrow the input to one arm first`
362
+ );
363
+ }
364
+ const selected = /* @__PURE__ */ new Set();
365
+ const stack = [...matched];
366
+ while (stack.length > 0) {
367
+ const span = stack.pop();
368
+ if (selected.has(span)) continue;
369
+ selected.add(span);
370
+ stack.push(...children.get(span.span_id) ?? []);
371
+ }
372
+ return spans.filter((span) => selected.has(span));
373
+ }
374
+ function selectSpans(spans, selector, path) {
375
+ if (selector.startsWith("branch=")) {
376
+ const branch = selector.slice("branch=".length);
377
+ const picked2 = branchSpans(spans, branch);
378
+ if (picked2.length === 0) throw new RefError(`${path} holds no span with ${ATTR.branchId} or ${ATTR.branchArm} "${branch}"`, "unreadable");
379
+ return picked2;
380
+ }
381
+ const picked = spans.filter((span) => span.trace_id === selector);
382
+ if (picked.length === 0) throw new RefError(`${path} holds no run with trace id ${selector}`, "unreadable");
383
+ return picked;
384
+ }
385
+ async function readRefSpans(ref, options = {}) {
386
+ const resolved = (await resolveRefs([ref], options))[0];
387
+ switch (resolved.kind) {
388
+ case "session":
389
+ return parseSession(resolved.adapter, resolved.session);
390
+ case "run-dir":
391
+ return [...(await readOtlpInput(resolved.path)).spans];
392
+ case "store-root":
393
+ throw new RefError(`${ref} is a store root, which holds many sessions; name one session in it`, "usage");
394
+ case "spans": {
395
+ const spans = resolved.reader === "otlp" ? [...(await readOtlpInput(resolved.path)).spans] : (await exportTraceEvidenceFile(resolved.path, { format: resolved.reader })).spans;
396
+ return resolved.selector ? selectSpans(spans, resolved.selector, resolved.path) : spans;
397
+ }
398
+ }
399
+ }
400
+
401
+ export {
402
+ RefError,
403
+ shortestRefs,
404
+ resolveRefs,
405
+ selectSpans,
406
+ readRefSpans
407
+ };