@tangle-network/traces 0.8.17 → 0.8.19

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.
@@ -751,10 +751,10 @@ var ClaudeAdapter = class {
751
751
  let step = main.nextStep;
752
752
  for (const f of files) {
753
753
  if (!f.endsWith(".jsonl")) continue;
754
- const hash = basename2(f, ".jsonl");
754
+ const hash2 = basename2(f, ".jsonl");
755
755
  let meta = {};
756
756
  try {
757
- meta = JSON.parse(await readFile3(join3(subDir, `${hash}.meta.json`), "utf8"));
757
+ meta = JSON.parse(await readFile3(join3(subDir, `${hash2}.meta.json`), "utf8"));
758
758
  } catch {
759
759
  }
760
760
  const parent = meta.toolUseId && main.toolSpanByUseId.get(meta.toolUseId)?.span_id || `root:${traceId}`;
@@ -768,7 +768,7 @@ var ClaudeAdapter = class {
768
768
  traceId,
769
769
  agent: meta.agentType ? `subagent:${meta.agentType}` : "subagent",
770
770
  startStep: step,
771
- idPrefix: `${hash}:`,
771
+ idPrefix: `${hash2}:`,
772
772
  rootParent: parent
773
773
  });
774
774
  out.push(...parsed.spans);
@@ -2286,13 +2286,13 @@ function extractAbsolutePaths(spans) {
2286
2286
  let scannedChars = 0;
2287
2287
  for (const span2 of spans) {
2288
2288
  for (const value of [span2.name, span2.status.message, ...Object.values(span2.attributes)]) {
2289
- for (const evidence2 of pathsFromUnknown(value)) {
2290
- scannedChars += evidence2.path.length;
2289
+ for (const evidence3 of pathsFromUnknown(value)) {
2290
+ scannedChars += evidence3.path.length;
2291
2291
  if (scannedChars > MAX_SPAN_TEXT_CHARS) return paths;
2292
- const key = `${evidence2.source}:${evidence2.path}`;
2292
+ const key = `${evidence3.source}:${evidence3.path}`;
2293
2293
  if (seen.has(key)) continue;
2294
2294
  seen.add(key);
2295
- paths.push(evidence2);
2295
+ paths.push(evidence3);
2296
2296
  if (paths.length >= MAX_SPAN_PATH_CANDIDATES) return paths;
2297
2297
  }
2298
2298
  }
@@ -2346,8 +2346,8 @@ async function resolveSessionRepoAttrs(cwd, spans) {
2346
2346
  add(await repairDashedPath(cwd), "repaired-cwd");
2347
2347
  add(cwd, "ref-cwd");
2348
2348
  }
2349
- for (const evidence2 of extractAbsolutePaths(spans)) {
2350
- add(await repoCandidateForPath(evidence2.path), evidence2.source, evidence2.weight);
2349
+ for (const evidence3 of extractAbsolutePaths(spans)) {
2350
+ add(await repoCandidateForPath(evidence3.path), evidence3.source, evidence3.weight);
2351
2351
  }
2352
2352
  const resolved = [];
2353
2353
  for (const candidate of candidates.values()) {
@@ -2492,9 +2492,9 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
2492
2492
  signals: {
2493
2493
  stuckLoopCount: loopFindings.length,
2494
2494
  affectedRunRatio: pipelines.stuckLoops.affectedRunRatio,
2495
- stuckLoops: loopFindings.slice(0, loopLimit).map((finding) => ({
2496
- toolName: finding.toolName,
2497
- occurrences: finding.occurrences
2495
+ stuckLoops: loopFindings.slice(0, loopLimit).map((finding2) => ({
2496
+ toolName: finding2.toolName,
2497
+ occurrences: finding2.occurrences
2498
2498
  })),
2499
2499
  stuckLoopsOmitted: Math.max(0, loopFindings.length - loopLimit),
2500
2500
  toolErrorRate: toolSpans.length === 0 ? 0 : erroredToolCallCount / toolSpans.length
@@ -3235,12 +3235,12 @@ function topLoopTools(row) {
3235
3235
  current.maxOccurrences = Math.max(current.maxOccurrences, loop.occurrences);
3236
3236
  byTool.set(loop.toolName, current);
3237
3237
  }
3238
- const loops = [...byTool.entries()].sort(([, a], [, b]) => b.groups - a.groups || b.maxOccurrences - a.maxOccurrences).slice(0, 3).map(([toolName2, stats]) => `${toolName2} ${stats.groups} group(s), max x${stats.maxOccurrences}`);
3238
+ const loops = [...byTool.entries()].sort(([, a], [, b]) => b.groups - a.groups || b.maxOccurrences - a.maxOccurrences).slice(0, 3).map(([toolName3, stats]) => `${toolName3} ${stats.groups} group(s), max x${stats.maxOccurrences}`);
3239
3239
  const suffix = row.signals.stuckLoopsOmitted > 0 ? `, +${row.signals.stuckLoopsOmitted} omitted` : "";
3240
3240
  return loops.length > 0 ? `${loops.join(", ")}${suffix}` : `${row.signals.stuckLoopCount} loop(s)`;
3241
3241
  }
3242
3242
  function sortFindings(findings) {
3243
- return findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || b.impact - a.impact || a.id.localeCompare(b.id)).map((finding) => ({ ...finding }));
3243
+ return findings.sort((a, b) => severityRank[a.severity] - severityRank[b.severity] || b.impact - a.impact || a.id.localeCompare(b.id)).map((finding2) => ({ ...finding2 }));
3244
3244
  }
3245
3245
  function contextSourceCounts(index) {
3246
3246
  return { contextFiles: allContextFiles(index).length };
@@ -3435,9 +3435,9 @@ function inspectSessionIndex(index, opts = {}) {
3435
3435
  },
3436
3436
  totals: {
3437
3437
  findings: ranked.length,
3438
- high: ranked.filter((finding) => finding.severity === "high").length,
3439
- medium: ranked.filter((finding) => finding.severity === "medium").length,
3440
- low: ranked.filter((finding) => finding.severity === "low").length
3438
+ high: ranked.filter((finding2) => finding2.severity === "high").length,
3439
+ medium: ranked.filter((finding2) => finding2.severity === "medium").length,
3440
+ low: ranked.filter((finding2) => finding2.severity === "low").length
3441
3441
  },
3442
3442
  findings: ranked
3443
3443
  };
@@ -3456,16 +3456,16 @@ function renderInspectionReport(report) {
3456
3456
  return `${lines.join("\n")}
3457
3457
  `;
3458
3458
  }
3459
- report.findings.forEach((finding, i) => {
3459
+ report.findings.forEach((finding2, i) => {
3460
3460
  lines.push("");
3461
- lines.push(`## ${i + 1}. [${finding.severity}] ${finding.title}`);
3462
- lines.push(`Area: ${finding.area} Impact: ${finding.impact}`);
3461
+ lines.push(`## ${i + 1}. [${finding2.severity}] ${finding2.title}`);
3462
+ lines.push(`Area: ${finding2.area} Impact: ${finding2.impact}`);
3463
3463
  lines.push("Evidence:");
3464
- for (const item of finding.evidence) lines.push(`- ${item}`);
3465
- lines.push(`Next: ${finding.next}`);
3466
- if (finding.refs && finding.refs.length > 0) {
3464
+ for (const item of finding2.evidence) lines.push(`- ${item}`);
3465
+ lines.push(`Next: ${finding2.next}`);
3466
+ if (finding2.refs && finding2.refs.length > 0) {
3467
3467
  lines.push("Refs:");
3468
- for (const ref of finding.refs.slice(0, 5)) lines.push(`- ${ref}`);
3468
+ for (const ref of finding2.refs.slice(0, 5)) lines.push(`- ${ref}`);
3469
3469
  }
3470
3470
  });
3471
3471
  return `${lines.join("\n")}
@@ -3926,55 +3926,55 @@ function normalizeExternalFindings(results) {
3926
3926
  for (const result of results) {
3927
3927
  if (result.findings) out.push(...result.findings);
3928
3928
  externalRows(result.output).forEach((row, index) => {
3929
- const finding = coerceExternalFinding(result.analyzer, row, index);
3930
- if (finding) out.push(finding);
3929
+ const finding2 = coerceExternalFinding(result.analyzer, row, index);
3930
+ if (finding2) out.push(finding2);
3931
3931
  });
3932
3932
  }
3933
3933
  return out;
3934
3934
  }
3935
- function sourceOf(finding) {
3936
- if (finding.analyst_id.startsWith("external:")) return "external";
3937
- if (finding.analyst_id === "traces-deterministic") return "deterministic";
3935
+ function sourceOf(finding2) {
3936
+ if (finding2.analyst_id.startsWith("external:")) return "external";
3937
+ if (finding2.analyst_id === "traces-deterministic") return "deterministic";
3938
3938
  return "analyst";
3939
3939
  }
3940
- function recommendationTitle(finding) {
3941
- if (finding.recommended_action) return finding.recommended_action.replace(/\.$/, "");
3942
- return `Investigate ${finding.area}: ${finding.claim}`.replace(/\.$/, "");
3940
+ function recommendationTitle(finding2) {
3941
+ if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
3942
+ return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
3943
3943
  }
3944
3944
  function buildRecommendations(findings) {
3945
- return [...findings].filter((finding) => finding.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding, index) => ({
3945
+ return [...findings].filter((finding2) => finding2.severity !== "info").sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || b.confidence - a.confidence || a.finding_id.localeCompare(b.finding_id)).slice(0, 20).map((finding2, index) => ({
3946
3946
  schemaVersion: 1,
3947
- id: `rec-${index + 1}-${slug(finding.area)}`,
3948
- severity: finding.severity,
3949
- title: recommendationTitle(finding),
3950
- action: finding.recommended_action ?? `Review and fix: ${finding.claim}`,
3951
- rationale: finding.rationale ?? finding.claim,
3952
- evidenceRefs: finding.evidence_refs,
3953
- findingIds: [finding.finding_id],
3954
- validationPlan: finding.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
3955
- source: sourceOf(finding)
3947
+ id: `rec-${index + 1}-${slug(finding2.area)}`,
3948
+ severity: finding2.severity,
3949
+ title: recommendationTitle(finding2),
3950
+ action: finding2.recommended_action ?? `Review and fix: ${finding2.claim}`,
3951
+ rationale: finding2.rationale ?? finding2.claim,
3952
+ evidenceRefs: finding2.evidence_refs,
3953
+ findingIds: [finding2.finding_id],
3954
+ validationPlan: finding2.validation_plan ?? "Rerun `traces improve` on a fresh comparable session window and confirm this finding disappears or drops in severity.",
3955
+ source: sourceOf(finding2)
3956
3956
  }));
3957
3957
  }
3958
3958
  function buildClaims(findings) {
3959
- return findings.map((finding, index) => ({
3959
+ return findings.map((finding2, index) => ({
3960
3960
  schemaVersion: 1,
3961
- id: `claim-${index + 1}-${slug(finding.area)}`,
3962
- severity: finding.severity,
3963
- text: finding.claim,
3964
- area: finding.area,
3965
- sourceFindingId: finding.finding_id,
3966
- evidenceRefs: finding.evidence_refs,
3967
- validationPlan: finding.validation_plan
3961
+ id: `claim-${index + 1}-${slug(finding2.area)}`,
3962
+ severity: finding2.severity,
3963
+ text: finding2.claim,
3964
+ area: finding2.area,
3965
+ sourceFindingId: finding2.finding_id,
3966
+ evidenceRefs: finding2.evidence_refs,
3967
+ validationPlan: finding2.validation_plan
3968
3968
  }));
3969
3969
  }
3970
3970
  function buildEvidenceRows(findings) {
3971
- return findings.flatMap((finding) => finding.evidence_refs.map((ref) => ({
3971
+ return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
3972
3972
  schemaVersion: 1,
3973
3973
  kind: "traces.improvement_evidence",
3974
- findingId: finding.finding_id,
3975
- severity: finding.severity,
3976
- area: finding.area,
3977
- claim: finding.claim,
3974
+ findingId: finding2.finding_id,
3975
+ severity: finding2.severity,
3976
+ area: finding2.area,
3977
+ claim: finding2.claim,
3978
3978
  evidence: ref
3979
3979
  })));
3980
3980
  }
@@ -4006,14 +4006,14 @@ function renderFindingPacket(packet, title = "Trace finding packet") {
4006
4006
  if (packet.findings.length > 0) {
4007
4007
  lines.push("## findings");
4008
4008
  lines.push("");
4009
- for (const finding of [...packet.findings].sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || a.area.localeCompare(b.area) || a.claim.localeCompare(b.claim))) {
4010
- lines.push(`### ${finding.severity.toUpperCase()} \u2014 ${finding.claim}`);
4009
+ for (const finding2 of [...packet.findings].sort((a, b) => severityRank2[a.severity] - severityRank2[b.severity] || a.area.localeCompare(b.area) || a.claim.localeCompare(b.claim))) {
4010
+ lines.push(`### ${finding2.severity.toUpperCase()} \u2014 ${finding2.claim}`);
4011
4011
  lines.push("");
4012
- lines.push(`- **Area:** ${finding.area}`);
4013
- lines.push(`- **Analyst:** ${finding.analyst_id}`);
4014
- if (finding.recommended_action) lines.push(`- **Action:** ${finding.recommended_action}`);
4015
- if (finding.validation_plan) lines.push(`- **Check:** ${finding.validation_plan}`);
4016
- for (const ref of finding.evidence_refs.slice(0, 3)) {
4012
+ lines.push(`- **Area:** ${finding2.area}`);
4013
+ lines.push(`- **Analyst:** ${finding2.analyst_id}`);
4014
+ if (finding2.recommended_action) lines.push(`- **Action:** ${finding2.recommended_action}`);
4015
+ if (finding2.validation_plan) lines.push(`- **Check:** ${finding2.validation_plan}`);
4016
+ for (const ref of finding2.evidence_refs.slice(0, 3)) {
4017
4017
  lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
4018
4018
  }
4019
4019
  lines.push("");
@@ -4242,8 +4242,16 @@ async function saveReport(path, report) {
4242
4242
  await writeFile4(path, report, "utf8");
4243
4243
  }
4244
4244
 
4245
- // src/observer.ts
4245
+ // src/live.ts
4246
+ import { createHash as createHash2 } from "crypto";
4247
+ var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
4248
+ var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
4249
+ var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
4246
4250
  var sleep = (ms, signal) => new Promise((resolve2) => {
4251
+ if (signal?.aborted) {
4252
+ resolve2();
4253
+ return;
4254
+ }
4247
4255
  const onAbort = () => {
4248
4256
  clearTimeout(t);
4249
4257
  resolve2();
@@ -4254,72 +4262,460 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
4254
4262
  }, ms);
4255
4263
  signal?.addEventListener("abort", onAbort, { once: true });
4256
4264
  });
4257
- function loopKey(sessionId, toolName2, argHash) {
4258
- return `${sessionId}\0${toolName2}\0${argHash}`;
4265
+ function hash(parts) {
4266
+ return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
4259
4267
  }
4260
- async function watchSessions(opts) {
4261
- const windowMs = opts.windowMs ?? 30 * 6e4;
4268
+ function asString(value) {
4269
+ if (typeof value === "string" && value.trim()) return value;
4270
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4271
+ return void 0;
4272
+ }
4273
+ function spanKind2(span2) {
4274
+ return asString(span2.attributes["openinference.span.kind"])?.toUpperCase() ?? "SPAN";
4275
+ }
4276
+ function spanContent(span2) {
4277
+ return asString(span2.attributes.content) ?? asString(span2.attributes["input.value"]) ?? asString(span2.attributes["llm.input"]) ?? asString(span2.attributes["gen_ai.prompt"]) ?? "";
4278
+ }
4279
+ function toolName2(span2) {
4280
+ return asString(span2.attributes["tool.name"]) ?? span2.name.replace(/^tool\./, "");
4281
+ }
4282
+ function toolSignature(span2) {
4283
+ const content = spanContent(span2).replace(/\s+/g, " ").trim();
4284
+ return `${toolName2(span2)}:${content || span2.name}`;
4285
+ }
4286
+ function isTool(span2) {
4287
+ return spanKind2(span2) === "TOOL" || span2.attributes["tool.name"] != null || span2.name.startsWith("tool.");
4288
+ }
4289
+ function isTextSpan(span2) {
4290
+ return !isTool(span2) && spanContent(span2).length > 0;
4291
+ }
4292
+ function isVerification(span2) {
4293
+ if (!isTool(span2)) return false;
4294
+ return VERIFY_RE.test(`${toolName2(span2)} ${span2.name} ${spanContent(span2)}`);
4295
+ }
4296
+ function isChange(span2) {
4297
+ if (!isTool(span2)) return false;
4298
+ return CHANGE_RE.test(`${toolName2(span2)} ${span2.name} ${spanContent(span2)}`);
4299
+ }
4300
+ function isRead(span2) {
4301
+ if (!isTool(span2)) return false;
4302
+ return /\b(read|cat|less|grep|rg|sed -n|nl|ls|find|git show|git diff)\b/i.test(`${toolName2(span2)} ${span2.name} ${spanContent(span2)}`);
4303
+ }
4304
+ function actionKind(span2) {
4305
+ if (isVerification(span2)) return "verify";
4306
+ if (isChange(span2)) return "edit";
4307
+ if (isRead(span2)) return "read";
4308
+ if (isTool(span2)) return "tool";
4309
+ if (isTextSpan(span2) && CLAIM_DONE_RE.test(spanContent(span2))) return "claim";
4310
+ return "other";
4311
+ }
4312
+ function classifyLiveActions(spans) {
4313
+ return orderSpans(spans).map((span2) => {
4314
+ const tool = isTool(span2) ? toolName2(span2) : void 0;
4315
+ return {
4316
+ kind: actionKind(span2),
4317
+ spanId: span2.span_id,
4318
+ span: span2,
4319
+ ...tool ? { toolName: tool } : {},
4320
+ signature: toolSignature(span2),
4321
+ errored: span2.status.code === "ERROR"
4322
+ };
4323
+ });
4324
+ }
4325
+ function actionCounts(actions) {
4326
+ const counts = { tool: 0, read: 0, edit: 0, verify: 0, claim: 0, other: 0 };
4327
+ for (const action of actions) counts[action.kind] += 1;
4328
+ return counts;
4329
+ }
4330
+ function timeOf(span2) {
4331
+ const parsed = Date.parse(span2.start_time);
4332
+ return Number.isFinite(parsed) ? parsed : 0;
4333
+ }
4334
+ function orderSpans(spans) {
4335
+ return [...spans].sort((a, b) => {
4336
+ const stepA = typeof a.attributes.step === "number" ? a.attributes.step : void 0;
4337
+ const stepB = typeof b.attributes.step === "number" ? b.attributes.step : void 0;
4338
+ if (stepA != null && stepB != null && stepA !== stepB) return stepA - stepB;
4339
+ return timeOf(a) - timeOf(b);
4340
+ });
4341
+ }
4342
+ function sessionFrom(spans, opts) {
4343
+ const first = spans[0];
4344
+ return {
4345
+ harness: opts.session?.harness ?? asString(first?.attributes["service.name"]) ?? "unknown",
4346
+ sessionId: opts.session?.sessionId ?? first?.trace_id ?? "unknown",
4347
+ cwd: opts.session?.cwd ?? asString(first?.attributes["tangle.cwd"]) ?? null,
4348
+ ...opts.session?.path ? { path: opts.session.path } : {}
4349
+ };
4350
+ }
4351
+ function evidence2(kind, label, value, spanIds) {
4352
+ return { kind, label, value, ...spanIds && spanIds.length > 0 ? { spanIds } : {} };
4353
+ }
4354
+ function finding(input) {
4355
+ const fingerprint = hash([input.ruleId, ...input.signature]);
4356
+ return {
4357
+ schemaVersion: 1,
4358
+ kind: "traces.live_finding",
4359
+ id: `live.${input.ruleId}.${fingerprint}`,
4360
+ fingerprint,
4361
+ ruleId: input.ruleId,
4362
+ severity: input.severity,
4363
+ title: input.title,
4364
+ claim: input.claim,
4365
+ action: input.action,
4366
+ check: input.check,
4367
+ evidence: input.evidence,
4368
+ session: input.session,
4369
+ observedAt: input.observedAt
4370
+ };
4371
+ }
4372
+ var sameFailingCommandAnalyst = {
4373
+ id: "same-failing-command",
4374
+ analyze(context) {
4375
+ const findings = [];
4376
+ const failedBySignature = /* @__PURE__ */ new Map();
4377
+ for (const action of context.actions) {
4378
+ if (!action.toolName || !action.errored) continue;
4379
+ failedBySignature.set(action.signature, [...failedBySignature.get(action.signature) ?? [], action.span]);
4380
+ }
4381
+ for (const [signature, rows] of failedBySignature) {
4382
+ if (rows.length < 2) continue;
4383
+ findings.push(finding({
4384
+ ruleId: "same-failing-command",
4385
+ severity: rows.length >= 3 ? "high" : "medium",
4386
+ title: "Same failing command is repeating",
4387
+ claim: `${rows.length} failed tool call(s) repeated the same command or arguments.`,
4388
+ action: "Stop rerunning it; inspect the first failure, change state, or choose a different diagnostic path before another retry.",
4389
+ check: "The next action should read the failing file, inspect the stack/output, or edit code before this command runs again.",
4390
+ evidence: [
4391
+ evidence2("pattern", "command", signature, rows.map((span2) => span2.span_id)),
4392
+ evidence2("metric", "failed_repeats", String(rows.length))
4393
+ ],
4394
+ session: context.session,
4395
+ observedAt: context.generatedAt,
4396
+ signature: [signature, String(rows.length)]
4397
+ }));
4398
+ }
4399
+ return findings;
4400
+ }
4401
+ };
4402
+ var verificationWithoutChangeAnalyst = {
4403
+ id: "verification-without-change",
4404
+ analyze(context) {
4405
+ let verificationSinceChange = 0;
4406
+ const verificationSpanIds = [];
4407
+ for (const action of context.actions) {
4408
+ if (action.kind === "edit") {
4409
+ verificationSinceChange = 0;
4410
+ verificationSpanIds.length = 0;
4411
+ } else if (action.kind === "verify") {
4412
+ verificationSinceChange += 1;
4413
+ verificationSpanIds.push(action.spanId);
4414
+ }
4415
+ }
4416
+ if (verificationSinceChange < 2) return [];
4417
+ return [finding({
4418
+ ruleId: "verification-without-change",
4419
+ severity: verificationSinceChange >= 3 ? "high" : "medium",
4420
+ title: "Verification is repeating without a state change",
4421
+ claim: `${verificationSinceChange} verification command(s) ran after the last detected code/config change.`,
4422
+ action: "Do not spend another verification run until the agent either edits state or explains what new signal the rerun will reveal.",
4423
+ check: "The next action should be a code/config edit, targeted file read, or a different narrow diagnostic command.",
4424
+ evidence: [
4425
+ evidence2("metric", "verification_calls_since_change", String(verificationSinceChange), verificationSpanIds),
4426
+ evidence2("metric", "change_calls", String(context.changeActions.length))
4427
+ ],
4428
+ session: context.session,
4429
+ observedAt: context.generatedAt,
4430
+ signature: [String(verificationSinceChange), verificationSpanIds.join(",")]
4431
+ })];
4432
+ }
4433
+ };
4434
+ var completionClaimWithoutVerificationAnalyst = {
4435
+ id: "completion-claim-without-verification",
4436
+ analyze(context) {
4437
+ const lastClaim = [...context.actions].reverse().find((action) => action.kind === "claim");
4438
+ if (!lastClaim) return [];
4439
+ const claimTime = timeOf(lastClaim.span);
4440
+ const verificationAfterClaim = context.verificationActions.some((action) => timeOf(action.span) > claimTime);
4441
+ if (verificationAfterClaim) return [];
4442
+ return [finding({
4443
+ ruleId: "completion-claim-without-verification",
4444
+ severity: "medium",
4445
+ title: "Completion claim has no later verification",
4446
+ claim: "The agent claimed completion, correctness, or verification without a later verification tool call in the observed trace.",
4447
+ action: "Require a concrete check after the claim, or downgrade the claim to an unverified hypothesis.",
4448
+ check: "A later test/build/typecheck/lint/smoke command should appear after the claim.",
4449
+ evidence: [
4450
+ evidence2("span", "claim_span", spanContent(lastClaim.span).slice(0, 240), [lastClaim.spanId]),
4451
+ evidence2("metric", "verification_after_claim", "0")
4452
+ ],
4453
+ session: context.session,
4454
+ observedAt: context.generatedAt,
4455
+ signature: [lastClaim.spanId, spanContent(lastClaim.span).slice(0, 120)]
4456
+ })];
4457
+ }
4458
+ };
4459
+ var highToolErrorRateAnalyst = {
4460
+ id: "high-tool-error-rate",
4461
+ analyze(context) {
4462
+ if (context.tools.length < 4 || context.erroredTools.length / context.tools.length < 0.5) return [];
4463
+ return [finding({
4464
+ ruleId: "high-tool-error-rate",
4465
+ severity: context.erroredTools.length / context.tools.length >= 0.75 ? "high" : "medium",
4466
+ title: "Tool calls are mostly failing",
4467
+ claim: `${context.erroredTools.length}/${context.tools.length} tool call(s) in this live batch ended with ERROR.`,
4468
+ action: "Switch from broad execution to diagnosis: classify the dominant error, then run one targeted command to validate the fix path.",
4469
+ check: "The next batch should reduce the error ratio or show a new targeted diagnostic result.",
4470
+ evidence: [
4471
+ evidence2("metric", "errored_tool_calls", `${context.erroredTools.length}/${context.tools.length}`, context.erroredTools.map((span2) => span2.span_id))
4472
+ ],
4473
+ session: context.session,
4474
+ observedAt: context.generatedAt,
4475
+ signature: [String(context.erroredTools.length), String(context.tools.length)]
4476
+ })];
4477
+ }
4478
+ };
4479
+ var defaultTraceLiveAnalysts = [
4480
+ sameFailingCommandAnalyst,
4481
+ verificationWithoutChangeAnalyst,
4482
+ completionClaimWithoutVerificationAnalyst,
4483
+ highToolErrorRateAnalyst
4484
+ ];
4485
+ function liveAnalysts(opts) {
4486
+ return [...opts.analysts ?? defaultTraceLiveAnalysts, ...opts.extraAnalysts ?? []];
4487
+ }
4488
+ function analyzeLiveBatch(spans, opts = {}) {
4489
+ const ordered2 = orderSpans(spans);
4490
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4491
+ const session = sessionFrom(ordered2, opts);
4492
+ const actions = classifyLiveActions(ordered2);
4493
+ const tools = ordered2.filter(isTool);
4494
+ const erroredTools = tools.filter((span2) => span2.status.code === "ERROR");
4495
+ const verificationActions = actions.filter((action) => action.kind === "verify");
4496
+ const changeActions = actions.filter((action) => action.kind === "edit");
4497
+ const context = {
4498
+ spans: ordered2,
4499
+ actions,
4500
+ tools,
4501
+ erroredTools,
4502
+ verificationActions,
4503
+ changeActions,
4504
+ session,
4505
+ generatedAt,
4506
+ newSpanCount: opts.newSpanCount ?? ordered2.length
4507
+ };
4508
+ const findings = liveAnalysts(opts).flatMap((analyst) => [...analyst.analyze(context)]);
4509
+ return {
4510
+ schemaVersion: 1,
4511
+ kind: "traces.live_batch",
4512
+ generatedAt,
4513
+ session,
4514
+ spanCount: ordered2.length,
4515
+ newSpanCount: opts.newSpanCount ?? ordered2.length,
4516
+ toolCallCount: tools.length,
4517
+ erroredToolCallCount: erroredTools.length,
4518
+ verificationCallCount: verificationActions.length,
4519
+ changeCallCount: changeActions.length,
4520
+ actionCounts: actionCounts(actions),
4521
+ findingCount: findings.length,
4522
+ findings
4523
+ };
4524
+ }
4525
+ function sessionEvent(session, spans, generatedAt) {
4526
+ const ordered2 = orderSpans(spans);
4527
+ return {
4528
+ schemaVersion: 1,
4529
+ kind: "traces.stream.session",
4530
+ event: "session",
4531
+ generatedAt,
4532
+ session,
4533
+ spanCount: ordered2.length,
4534
+ startedAt: ordered2[0]?.start_time ?? null,
4535
+ endedAt: ordered2.at(-1)?.end_time ?? null
4536
+ };
4537
+ }
4538
+ function traceStreamEventsFromSpans(spans, opts = {}) {
4539
+ const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
4540
+ const session = sessionFrom(spans, {
4541
+ ...opts,
4542
+ session: opts.ref ? { harness: opts.ref.harness, sessionId: opts.ref.sessionId, cwd: opts.ref.cwd, path: opts.ref.path } : opts.session
4543
+ });
4544
+ const ordered2 = orderSpans(spans);
4545
+ const batch = analyzeLiveBatch(ordered2, {
4546
+ generatedAt,
4547
+ session,
4548
+ newSpanCount: ordered2.length,
4549
+ analysts: opts.analysts,
4550
+ extraAnalysts: opts.extraAnalysts
4551
+ });
4552
+ const events = [sessionEvent(session, ordered2, generatedAt)];
4553
+ if (opts.includeSpans !== false) {
4554
+ for (const span2 of ordered2) {
4555
+ events.push({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
4556
+ }
4557
+ }
4558
+ events.push({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, ref: opts.ref, batch });
4559
+ if (opts.includeFindings !== false) {
4560
+ for (const liveFinding of batch.findings) {
4561
+ events.push({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
4562
+ }
4563
+ }
4564
+ return events;
4565
+ }
4566
+ function serializeTraceStreamEvent(event) {
4567
+ return `${JSON.stringify(event)}
4568
+ `;
4569
+ }
4570
+ function loopsFromReport(session, report) {
4571
+ return report.stuckLoops.findings.map((loop) => ({
4572
+ harness: session.harness,
4573
+ sessionId: session.sessionId,
4574
+ cwd: session.cwd,
4575
+ toolName: loop.toolName,
4576
+ argHash: loop.argHash,
4577
+ occurrences: loop.occurrences,
4578
+ windowMs: loop.windowMs
4579
+ }));
4580
+ }
4581
+ async function streamSessions(opts) {
4262
4582
  const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
4263
- const minLoop = opts.minLoopOccurrences ?? 3;
4264
- const seen = /* @__PURE__ */ new Map();
4583
+ const windowMs = opts.windowMs ?? 30 * 6e4;
4584
+ const includeSpans = opts.includeSpans !== false;
4585
+ const includeFindings = opts.includeFindings !== false;
4586
+ const includeBatches = opts.includeBatches !== false;
4587
+ const includeReports = opts.includeReports === true;
4588
+ const seenSessions = /* @__PURE__ */ new Set();
4589
+ const seenSpans = /* @__PURE__ */ new Set();
4590
+ const seenFindings = /* @__PURE__ */ new Set();
4265
4591
  while (!opts.signal?.aborted) {
4266
- let sessionCount = 0;
4592
+ let sessions = 0;
4593
+ let newSpans = 0;
4594
+ let findings = 0;
4267
4595
  for await (const { adapter, ref, spans } of scanSessions({
4268
- adapters: opts.adapters,
4269
- all: opts.all,
4270
- harnesses: opts.harnesses,
4271
- cwd: opts.cwd,
4272
- sinceMs: Date.now() - windowMs,
4273
- signal: opts.signal,
4274
- onError: opts.onError
4596
+ ...opts,
4597
+ sinceMs: opts.sinceMs ?? Date.now() - windowMs,
4598
+ signal: opts.signal
4275
4599
  })) {
4276
- let report;
4277
- try {
4278
- report = await runPipelines(spans, { minLoopOccurrences: minLoop });
4279
- } catch (err) {
4280
- opts.onError?.(err, ref);
4281
- continue;
4600
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4601
+ const session = { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path };
4602
+ const ordered2 = orderSpans(spans);
4603
+ const sessionKey = `${session.harness}:${session.sessionId}`;
4604
+ const unseen = ordered2.filter((span2) => {
4605
+ const key = `${sessionKey}:${span2.span_id}`;
4606
+ if (seenSpans.has(key)) return false;
4607
+ seenSpans.add(key);
4608
+ return true;
4609
+ });
4610
+ sessions += 1;
4611
+ newSpans += unseen.length;
4612
+ if (!seenSessions.has(sessionKey)) {
4613
+ seenSessions.add(sessionKey);
4614
+ await opts.onEvent(sessionEvent(session, ordered2, generatedAt));
4615
+ }
4616
+ if (includeSpans) {
4617
+ for (const span2 of unseen) {
4618
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
4619
+ }
4620
+ }
4621
+ const batch = analyzeLiveBatch(ordered2, {
4622
+ generatedAt,
4623
+ session,
4624
+ newSpanCount: unseen.length,
4625
+ extraAnalysts: opts.extraAnalysts
4626
+ });
4627
+ if (includeBatches) {
4628
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, ref, batch });
4282
4629
  }
4283
- sessionCount += 1;
4284
- if (opts.onReport) {
4630
+ if (includeReports) {
4285
4631
  try {
4286
- await opts.onReport(ref, report);
4632
+ const report = await runPipelines(ordered2, { minLoopOccurrences: opts.minLoopOccurrences });
4633
+ await opts.onEvent({
4634
+ schemaVersion: 1,
4635
+ kind: "traces.stream.report",
4636
+ event: "report",
4637
+ generatedAt,
4638
+ session,
4639
+ ref,
4640
+ report,
4641
+ loops: loopsFromReport(session, report)
4642
+ });
4287
4643
  } catch (err) {
4288
4644
  opts.onError?.(err, ref);
4289
4645
  }
4290
4646
  }
4291
- for (const f of report.stuckLoops.findings) {
4292
- const key = loopKey(ref.sessionId, f.toolName, f.argHash);
4293
- if (f.occurrences > (seen.get(key) ?? 0)) {
4294
- seen.set(key, f.occurrences);
4295
- if (opts.onLoop) {
4296
- try {
4297
- await opts.onLoop({
4298
- harness: adapter.harness,
4299
- sessionId: ref.sessionId,
4300
- cwd: ref.cwd,
4301
- toolName: f.toolName,
4302
- argHash: f.argHash,
4303
- occurrences: f.occurrences,
4304
- windowMs: f.windowMs
4305
- });
4306
- } catch (err) {
4307
- opts.onError?.(err, ref);
4308
- }
4309
- }
4647
+ if (includeFindings) {
4648
+ for (const liveFinding of batch.findings) {
4649
+ if (seenFindings.has(liveFinding.id)) continue;
4650
+ seenFindings.add(liveFinding.id);
4651
+ findings += 1;
4652
+ await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
4310
4653
  }
4311
4654
  }
4312
4655
  }
4313
- try {
4314
- opts.onTick?.({ sessions: sessionCount });
4315
- } catch (err) {
4316
- opts.onError?.(err);
4317
- }
4318
- if (opts.signal?.aborted) break;
4656
+ await opts.onEvent({
4657
+ schemaVersion: 1,
4658
+ kind: "traces.stream.tick",
4659
+ event: "tick",
4660
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4661
+ sessions,
4662
+ newSpans,
4663
+ findings
4664
+ });
4665
+ if (opts.once || opts.signal?.aborted) break;
4319
4666
  await sleep(intervalMs, opts.signal);
4320
4667
  }
4321
4668
  }
4322
4669
 
4670
+ // src/observer.ts
4671
+ function loopKey(loop) {
4672
+ return `${loop.sessionId}:${loop.toolName}:${loop.argHash}`;
4673
+ }
4674
+ async function callSafely(ref, onError, fn) {
4675
+ try {
4676
+ await fn();
4677
+ } catch (err) {
4678
+ onError?.(err, ref);
4679
+ }
4680
+ }
4681
+ async function watchSessions(opts) {
4682
+ const seenLoops = /* @__PURE__ */ new Map();
4683
+ await streamSessions({
4684
+ adapters: opts.adapters,
4685
+ all: opts.all,
4686
+ harnesses: opts.harnesses,
4687
+ cwd: opts.cwd,
4688
+ windowMs: opts.windowMs,
4689
+ intervalMs: opts.intervalMs,
4690
+ minLoopOccurrences: opts.minLoopOccurrences,
4691
+ signal: opts.signal,
4692
+ includeSpans: false,
4693
+ includeBatches: Boolean(opts.onBatch),
4694
+ includeFindings: Boolean(opts.onFinding),
4695
+ includeReports: Boolean(opts.onReport || opts.onLoop),
4696
+ onError: opts.onError,
4697
+ onEvent: async (event) => {
4698
+ if (event.event === "analysis_batch") {
4699
+ if (event.ref) {
4700
+ await callSafely(event.ref, opts.onError, () => opts.onBatch?.(event.ref, event.batch));
4701
+ }
4702
+ } else if (event.event === "finding") {
4703
+ await callSafely(void 0, opts.onError, () => opts.onFinding?.(event.finding));
4704
+ } else if (event.event === "report") {
4705
+ await callSafely(event.ref, opts.onError, () => opts.onReport?.(event.ref, event.report));
4706
+ for (const loop of event.loops) {
4707
+ const key = loopKey(loop);
4708
+ if (loop.occurrences <= (seenLoops.get(key) ?? 0)) continue;
4709
+ seenLoops.set(key, loop.occurrences);
4710
+ await callSafely(event.ref, opts.onError, () => opts.onLoop?.(loop));
4711
+ }
4712
+ } else if (event.event === "tick") {
4713
+ await callSafely(void 0, opts.onError, () => opts.onTick?.({ sessions: event.sessions }));
4714
+ }
4715
+ }
4716
+ });
4717
+ }
4718
+
4323
4719
  // src/session-index.ts
4324
4720
  import { mkdtemp as mkdtemp5, readdir as readdir11, readFile as readFile14, stat as stat12, writeFile as writeFile5 } from "fs/promises";
4325
4721
  import { tmpdir as tmpdir5 } from "os";
@@ -4543,7 +4939,7 @@ async function writeSessionIndexFile(index, outPath) {
4543
4939
  }
4544
4940
 
4545
4941
  // src/upload-state.ts
4546
- import { createHash as createHash2 } from "crypto";
4942
+ import { createHash as createHash3 } from "crypto";
4547
4943
  import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
4548
4944
  import { homedir as homedir11 } from "os";
4549
4945
  import { dirname as dirname4, join as join18 } from "path";
@@ -4555,7 +4951,7 @@ function uploadKey(harness, sessionId) {
4555
4951
  return `${harness}:${sessionId}`;
4556
4952
  }
4557
4953
  function sessionHash(spans) {
4558
- const h = createHash2("sha256");
4954
+ const h = createHash3("sha256");
4559
4955
  for (const s of spans) {
4560
4956
  h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
4561
4957
  `);
@@ -4575,8 +4971,8 @@ async function saveState(state) {
4575
4971
  await writeFile6(f, `${JSON.stringify(state, null, 2)}
4576
4972
  `, "utf8");
4577
4973
  }
4578
- function alreadyUploaded(state, harness, sessionId, hash) {
4579
- return state[uploadKey(harness, sessionId)]?.hash === hash;
4974
+ function alreadyUploaded(state, harness, sessionId, hash2) {
4975
+ return state[uploadKey(harness, sessionId)]?.hash === hash2;
4580
4976
  }
4581
4977
 
4582
4978
  // src/upload.ts
@@ -4593,13 +4989,13 @@ async function planUpload(opts = {}) {
4593
4989
  const items = [];
4594
4990
  for await (const { ref, spans: raw } of scanSessions(opts)) {
4595
4991
  const { spans, report } = redactSpans(raw);
4596
- const hash = sessionHash(spans);
4992
+ const hash2 = sessionHash(spans);
4597
4993
  items.push({
4598
4994
  ref,
4599
4995
  spans,
4600
4996
  redaction: report,
4601
- hash,
4602
- isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash)
4997
+ hash: hash2,
4998
+ isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
4603
4999
  });
4604
5000
  }
4605
5001
  return { items, state, sinceMs: opts.sinceMs };
@@ -4796,6 +5192,12 @@ export {
4796
5192
  writeTraceImprovementArtifacts,
4797
5193
  runTraceImprovementLoop,
4798
5194
  saveReport,
5195
+ classifyLiveActions,
5196
+ defaultTraceLiveAnalysts,
5197
+ analyzeLiveBatch,
5198
+ traceStreamEventsFromSpans,
5199
+ serializeTraceStreamEvent,
5200
+ streamSessions,
4799
5201
  watchSessions,
4800
5202
  buildSessionIndex,
4801
5203
  buildSessionIndexFromRows,