@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.
- package/README.md +43 -5
- package/dist/{chunk-QM4QAG3V.js → chunk-FCTZLWP7.js} +520 -118
- package/dist/cli.js +151 -6
- package/dist/index.d.ts +176 -26
- package/dist/index.js +13 -1
- package/package.json +1 -1
|
@@ -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
|
|
754
|
+
const hash2 = basename2(f, ".jsonl");
|
|
755
755
|
let meta = {};
|
|
756
756
|
try {
|
|
757
|
-
meta = JSON.parse(await readFile3(join3(subDir, `${
|
|
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: `${
|
|
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
|
|
2290
|
-
scannedChars +=
|
|
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 = `${
|
|
2292
|
+
const key = `${evidence3.source}:${evidence3.path}`;
|
|
2293
2293
|
if (seen.has(key)) continue;
|
|
2294
2294
|
seen.add(key);
|
|
2295
|
-
paths.push(
|
|
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
|
|
2350
|
-
add(await repoCandidateForPath(
|
|
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((
|
|
2496
|
-
toolName:
|
|
2497
|
-
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(([
|
|
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((
|
|
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((
|
|
3439
|
-
medium: ranked.filter((
|
|
3440
|
-
low: ranked.filter((
|
|
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((
|
|
3459
|
+
report.findings.forEach((finding2, i) => {
|
|
3460
3460
|
lines.push("");
|
|
3461
|
-
lines.push(`## ${i + 1}. [${
|
|
3462
|
-
lines.push(`Area: ${
|
|
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
|
|
3465
|
-
lines.push(`Next: ${
|
|
3466
|
-
if (
|
|
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
|
|
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
|
|
3930
|
-
if (
|
|
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(
|
|
3936
|
-
if (
|
|
3937
|
-
if (
|
|
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(
|
|
3941
|
-
if (
|
|
3942
|
-
return `Investigate ${
|
|
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((
|
|
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(
|
|
3948
|
-
severity:
|
|
3949
|
-
title: recommendationTitle(
|
|
3950
|
-
action:
|
|
3951
|
-
rationale:
|
|
3952
|
-
evidenceRefs:
|
|
3953
|
-
findingIds: [
|
|
3954
|
-
validationPlan:
|
|
3955
|
-
source: sourceOf(
|
|
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((
|
|
3959
|
+
return findings.map((finding2, index) => ({
|
|
3960
3960
|
schemaVersion: 1,
|
|
3961
|
-
id: `claim-${index + 1}-${slug(
|
|
3962
|
-
severity:
|
|
3963
|
-
text:
|
|
3964
|
-
area:
|
|
3965
|
-
sourceFindingId:
|
|
3966
|
-
evidenceRefs:
|
|
3967
|
-
validationPlan:
|
|
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((
|
|
3971
|
+
return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
|
|
3972
3972
|
schemaVersion: 1,
|
|
3973
3973
|
kind: "traces.improvement_evidence",
|
|
3974
|
-
findingId:
|
|
3975
|
-
severity:
|
|
3976
|
-
area:
|
|
3977
|
-
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
|
|
4010
|
-
lines.push(`### ${
|
|
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:** ${
|
|
4013
|
-
lines.push(`- **Analyst:** ${
|
|
4014
|
-
if (
|
|
4015
|
-
if (
|
|
4016
|
-
for (const ref of
|
|
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/
|
|
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
|
|
4258
|
-
return
|
|
4265
|
+
function hash(parts) {
|
|
4266
|
+
return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
4259
4267
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
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
|
|
4264
|
-
const
|
|
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
|
|
4592
|
+
let sessions = 0;
|
|
4593
|
+
let newSpans = 0;
|
|
4594
|
+
let findings = 0;
|
|
4267
4595
|
for await (const { adapter, ref, spans } of scanSessions({
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
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
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
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
|
-
|
|
4284
|
-
if (opts.onReport) {
|
|
4630
|
+
if (includeReports) {
|
|
4285
4631
|
try {
|
|
4286
|
-
await
|
|
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
|
-
|
|
4292
|
-
const
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
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
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
4579
|
-
return state[uploadKey(harness, sessionId)]?.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
|
|
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,
|
|
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,
|