@tangle-network/traces 0.8.16 → 0.8.18
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 +35 -4
- package/dist/{chunk-EOQY2RIK.js → chunk-G3OBZSWY.js} +553 -83
- package/dist/cli.js +153 -14
- package/dist/index.d.ts +115 -2
- package/dist/index.js +9 -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
|
|
@@ -2726,7 +2726,9 @@ function hashId(value, chars = 16) {
|
|
|
2726
2726
|
}
|
|
2727
2727
|
function isoTime(value) {
|
|
2728
2728
|
if (typeof value === "string") {
|
|
2729
|
-
const
|
|
2729
|
+
const trimmed = value.trim();
|
|
2730
|
+
const normalized = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/.test(trimmed) ? `${trimmed.replace(" ", "T")}Z` : trimmed;
|
|
2731
|
+
const ms = Date.parse(normalized);
|
|
2730
2732
|
return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
|
|
2731
2733
|
}
|
|
2732
2734
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
@@ -2768,14 +2770,19 @@ function parseJsonRows(text) {
|
|
|
2768
2770
|
const trimmed = text.trim();
|
|
2769
2771
|
if (!trimmed) throw new Error("input file is empty");
|
|
2770
2772
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2773
|
+
try {
|
|
2774
|
+
const parsed = JSON.parse(trimmed);
|
|
2775
|
+
if (Array.isArray(parsed)) return { rows: parsed };
|
|
2776
|
+
if (isObject(parsed)) {
|
|
2777
|
+
const events = parsed.events;
|
|
2778
|
+
if (Array.isArray(events)) return { rows: events, wrapper: parsed };
|
|
2779
|
+
return { rows: [parsed] };
|
|
2780
|
+
}
|
|
2781
|
+
throw new Error("input JSON must be an object, array, or JSONL rows");
|
|
2782
|
+
} catch (error) {
|
|
2783
|
+
const hasMultipleRows = /\r?\n/.test(trimmed);
|
|
2784
|
+
if (!hasMultipleRows) throw error;
|
|
2777
2785
|
}
|
|
2778
|
-
throw new Error("input JSON must be an object, array, or JSONL rows");
|
|
2779
2786
|
}
|
|
2780
2787
|
return {
|
|
2781
2788
|
rows: trimmed.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line))
|
|
@@ -2788,6 +2795,10 @@ function isOpenInferenceRow(row) {
|
|
|
2788
2795
|
if (!isObject(row)) return false;
|
|
2789
2796
|
return typeof row.trace_id === "string" && typeof row.span_id === "string" && typeof row.name === "string" && typeof row.start_time === "string" && typeof row.end_time === "string" && isObject(row.status) && isObject(row.attributes) && isObject(row.resource) && isObject(row.scope);
|
|
2790
2797
|
}
|
|
2798
|
+
function isIntelligenceSpanRow(row) {
|
|
2799
|
+
if (!isObject(row)) return false;
|
|
2800
|
+
return typeof row.trace_id === "string" && typeof row.name === "string" && isObject(row.attributes) && (row.start_unix_nano != null || row.start_time != null || row.received_at != null);
|
|
2801
|
+
}
|
|
2791
2802
|
function eventType(row) {
|
|
2792
2803
|
return stringValue(row.type) ?? stringValue(row.event) ?? stringValue(row.name) ?? stringValue(row.kind);
|
|
2793
2804
|
}
|
|
@@ -2801,8 +2812,9 @@ function detectFormat(rows, requested) {
|
|
|
2801
2812
|
if (rows.length === 0) throw new Error("input contains no rows");
|
|
2802
2813
|
if (rows.every(isPolicyEvidenceRow)) return "policy-evidence";
|
|
2803
2814
|
if (rows.every(isOpenInferenceRow)) return "openinference";
|
|
2815
|
+
if (rows.every(isIntelligenceSpanRow)) return "intelligence-spans";
|
|
2804
2816
|
if (rows.some(looksLikeSandboxEvent)) return "sandbox-events";
|
|
2805
|
-
throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, or
|
|
2817
|
+
throw new Error("could not detect input format; use --format policy-evidence, sandbox-events, openinference, or intelligence-spans");
|
|
2806
2818
|
}
|
|
2807
2819
|
function requirePolicyEvidenceRows(rows) {
|
|
2808
2820
|
if (!rows.every(isPolicyEvidenceRow)) throw new Error("policy-evidence input must contain only traces.policy_evidence.session rows");
|
|
@@ -2812,6 +2824,10 @@ function requireOpenInferenceRows(rows) {
|
|
|
2812
2824
|
if (!rows.every(isOpenInferenceRow)) throw new Error("openinference input must contain only complete OpenInference span rows");
|
|
2813
2825
|
return rows;
|
|
2814
2826
|
}
|
|
2827
|
+
function requireIntelligenceSpanRows(rows) {
|
|
2828
|
+
if (!rows.every(isIntelligenceSpanRow)) throw new Error("intelligence-spans input must contain only Intelligence span rows");
|
|
2829
|
+
return rows;
|
|
2830
|
+
}
|
|
2815
2831
|
function requireObjectRows(rows) {
|
|
2816
2832
|
if (!rows.every(isObject)) throw new Error("sandbox-events input must contain only JSON object events");
|
|
2817
2833
|
return rows;
|
|
@@ -3008,6 +3024,112 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
3008
3024
|
function statusCode(value) {
|
|
3009
3025
|
return value === "OK" || value === "ERROR" || value === "UNSET" ? value : "UNSET";
|
|
3010
3026
|
}
|
|
3027
|
+
function numberValue(value) {
|
|
3028
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
3029
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
3030
|
+
const parsed = Number(value);
|
|
3031
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3032
|
+
}
|
|
3033
|
+
return void 0;
|
|
3034
|
+
}
|
|
3035
|
+
function unixNanoTime(value) {
|
|
3036
|
+
if (value == null) return void 0;
|
|
3037
|
+
try {
|
|
3038
|
+
const ns = typeof value === "bigint" ? value : BigInt(String(value));
|
|
3039
|
+
return new Date(Number(ns / 1000000n)).toISOString();
|
|
3040
|
+
} catch {
|
|
3041
|
+
return void 0;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
function firstNumber(row, attrs, keys) {
|
|
3045
|
+
for (const key of keys) {
|
|
3046
|
+
const value = numberValue(row[key]) ?? numberValue(attrs[key]);
|
|
3047
|
+
if (value != null) return value;
|
|
3048
|
+
}
|
|
3049
|
+
return void 0;
|
|
3050
|
+
}
|
|
3051
|
+
var COMPUTED_SPAN_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
3052
|
+
"openinference.span.kind",
|
|
3053
|
+
"service.name",
|
|
3054
|
+
"agent.name",
|
|
3055
|
+
"llm.model_name",
|
|
3056
|
+
"tool.name",
|
|
3057
|
+
"llm.input_tokens",
|
|
3058
|
+
"llm.output_tokens",
|
|
3059
|
+
"step",
|
|
3060
|
+
"content"
|
|
3061
|
+
]);
|
|
3062
|
+
function preserveRawAttributes(attrs) {
|
|
3063
|
+
const preserved = {};
|
|
3064
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
3065
|
+
if (COMPUTED_SPAN_ATTRIBUTES.has(key)) {
|
|
3066
|
+
preserved[`traces.raw_attribute.${key}`] = value;
|
|
3067
|
+
} else {
|
|
3068
|
+
preserved[key] = value;
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
return preserved;
|
|
3072
|
+
}
|
|
3073
|
+
function intelligenceSpanKind(name, attrs) {
|
|
3074
|
+
const lowered = name.toLowerCase();
|
|
3075
|
+
const spanType = stringValue(attrs["span.type"])?.toLowerCase();
|
|
3076
|
+
if (spanType === "llm_request" || lowered.includes("llm")) return "LLM";
|
|
3077
|
+
if (spanType === "tool" || lowered.includes("tool") || attrs["tool.name"] != null || attrs.tool_name != null || attrs["mcp.tool.name"] != null || attrs["tool.input"] != null || attrs["tool.output"] != null) {
|
|
3078
|
+
return "TOOL";
|
|
3079
|
+
}
|
|
3080
|
+
if (spanType === "interaction" || lowered.includes("interaction") || lowered.includes("agent")) return "AGENT";
|
|
3081
|
+
return "CHAIN";
|
|
3082
|
+
}
|
|
3083
|
+
function intelligenceSpansToSpans(rows) {
|
|
3084
|
+
return rows.map((row, index) => {
|
|
3085
|
+
const attrs = { ...objectValue(row.attributes) ?? {} };
|
|
3086
|
+
const name = stringValue(row.name) ?? "intelligence.span";
|
|
3087
|
+
const startTime = unixNanoTime(row.start_unix_nano) ?? isoTime(row.start_time) ?? stringValue(row.start_time) ?? isoTime(row.received_at) ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
3088
|
+
const endTime = unixNanoTime(row.end_unix_nano) ?? isoTime(row.end_time) ?? stringValue(row.end_time) ?? startTime;
|
|
3089
|
+
const model = stringValue(row.model) ?? stringValue(attrs["llm.model_name"]) ?? stringValue(attrs.model) ?? stringValue(attrs["gen_ai.request.model"]);
|
|
3090
|
+
const tool = stringValue(attrs["tool.name"]) ?? stringValue(attrs.tool_name) ?? stringValue(attrs["mcp.tool.name"]);
|
|
3091
|
+
const sessionId = stringValue(row.session_id) ?? stringValue(attrs["session.id"]);
|
|
3092
|
+
const extra = {
|
|
3093
|
+
...preserveRawAttributes(attrs),
|
|
3094
|
+
"traces.source_format": "intelligence-spans",
|
|
3095
|
+
"traces.row.index": index
|
|
3096
|
+
};
|
|
3097
|
+
copyDefined(extra, {
|
|
3098
|
+
"traces.row.id": stringValue(row.id),
|
|
3099
|
+
[ATTR.SESSION_ID]: sessionId,
|
|
3100
|
+
"session.id": sessionId,
|
|
3101
|
+
"run.id": stringValue(row.run_id),
|
|
3102
|
+
"thread.id": stringValue(row.thread_id),
|
|
3103
|
+
"scenario.id": stringValue(row.scenario_id),
|
|
3104
|
+
"generation.id": stringValue(row.generation),
|
|
3105
|
+
"cell.id": stringValue(row.cell_id),
|
|
3106
|
+
"project.key": stringValue(row.project_key),
|
|
3107
|
+
"llm.cost_usd": numberValue(row.cost_usd),
|
|
3108
|
+
"redaction.version": stringValue(row.redaction_version),
|
|
3109
|
+
"traces.received_at": isoTime(row.received_at)
|
|
3110
|
+
});
|
|
3111
|
+
return span({
|
|
3112
|
+
traceId: stringValue(row.trace_id) ?? hashId(row, 32),
|
|
3113
|
+
spanId: stringValue(row.id) ?? hashId({ row, field: "span" }),
|
|
3114
|
+
parentSpanId: stringValue(row.parent_span_id) ?? null,
|
|
3115
|
+
name,
|
|
3116
|
+
kind: intelligenceSpanKind(name, attrs),
|
|
3117
|
+
startTime,
|
|
3118
|
+
endTime,
|
|
3119
|
+
status: statusCode(row.status_code),
|
|
3120
|
+
statusMessage: stringValue(row.status_message),
|
|
3121
|
+
service: stringValue(attrs["service.name"]) ?? stringValue(attrs.service_name) ?? "tangle-intelligence",
|
|
3122
|
+
agent: stringValue(attrs["agent.name"]) ?? stringValue(attrs["service.name"]) ?? null,
|
|
3123
|
+
model,
|
|
3124
|
+
tool,
|
|
3125
|
+
inputTokens: firstNumber(row, attrs, ["input_tokens", "llm.input_tokens"]),
|
|
3126
|
+
outputTokens: firstNumber(row, attrs, ["output_tokens", "llm.output_tokens"]),
|
|
3127
|
+
step: numberValue(attrs["interaction.sequence"]) ?? index,
|
|
3128
|
+
content: stringValue(attrs.user_prompt) ?? stringValue(attrs["gen_ai.prompt.messages"]) ?? stringValue(attrs["log.content.task"]) ?? null,
|
|
3129
|
+
extra
|
|
3130
|
+
});
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3011
3133
|
function openInferenceToSpans(rows) {
|
|
3012
3134
|
return rows.map((row) => {
|
|
3013
3135
|
const attrs = isObject(row.attributes) ? { ...row.attributes } : {};
|
|
@@ -3053,7 +3175,7 @@ function withExportAttributes(spans, attributes) {
|
|
|
3053
3175
|
}
|
|
3054
3176
|
function exportTraceEvidenceRows(rows, opts = {}, wrapper) {
|
|
3055
3177
|
const format = detectFormat(rows, opts.format ?? "auto");
|
|
3056
|
-
const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
|
|
3178
|
+
const converted = format === "policy-evidence" ? policyEvidenceToSpans(requirePolicyEvidenceRows(rows)) : format === "openinference" ? openInferenceToSpans(requireOpenInferenceRows(rows)) : format === "intelligence-spans" ? intelligenceSpansToSpans(requireIntelligenceSpanRows(rows)) : sandboxEventsToSpans(requireObjectRows(rows), wrapper);
|
|
3057
3179
|
const spans = withExportAttributes(converted, opts.attributes);
|
|
3058
3180
|
if (spans.length === 0) throw new Error(`no spans exported from ${format} input`);
|
|
3059
3181
|
const redacted = redactSpans(spans);
|
|
@@ -3113,12 +3235,12 @@ function topLoopTools(row) {
|
|
|
3113
3235
|
current.maxOccurrences = Math.max(current.maxOccurrences, loop.occurrences);
|
|
3114
3236
|
byTool.set(loop.toolName, current);
|
|
3115
3237
|
}
|
|
3116
|
-
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}`);
|
|
3117
3239
|
const suffix = row.signals.stuckLoopsOmitted > 0 ? `, +${row.signals.stuckLoopsOmitted} omitted` : "";
|
|
3118
3240
|
return loops.length > 0 ? `${loops.join(", ")}${suffix}` : `${row.signals.stuckLoopCount} loop(s)`;
|
|
3119
3241
|
}
|
|
3120
3242
|
function sortFindings(findings) {
|
|
3121
|
-
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 }));
|
|
3122
3244
|
}
|
|
3123
3245
|
function contextSourceCounts(index) {
|
|
3124
3246
|
return { contextFiles: allContextFiles(index).length };
|
|
@@ -3313,9 +3435,9 @@ function inspectSessionIndex(index, opts = {}) {
|
|
|
3313
3435
|
},
|
|
3314
3436
|
totals: {
|
|
3315
3437
|
findings: ranked.length,
|
|
3316
|
-
high: ranked.filter((
|
|
3317
|
-
medium: ranked.filter((
|
|
3318
|
-
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
|
|
3319
3441
|
},
|
|
3320
3442
|
findings: ranked
|
|
3321
3443
|
};
|
|
@@ -3334,16 +3456,16 @@ function renderInspectionReport(report) {
|
|
|
3334
3456
|
return `${lines.join("\n")}
|
|
3335
3457
|
`;
|
|
3336
3458
|
}
|
|
3337
|
-
report.findings.forEach((
|
|
3459
|
+
report.findings.forEach((finding2, i) => {
|
|
3338
3460
|
lines.push("");
|
|
3339
|
-
lines.push(`## ${i + 1}. [${
|
|
3340
|
-
lines.push(`Area: ${
|
|
3461
|
+
lines.push(`## ${i + 1}. [${finding2.severity}] ${finding2.title}`);
|
|
3462
|
+
lines.push(`Area: ${finding2.area} Impact: ${finding2.impact}`);
|
|
3341
3463
|
lines.push("Evidence:");
|
|
3342
|
-
for (const item of
|
|
3343
|
-
lines.push(`Next: ${
|
|
3344
|
-
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) {
|
|
3345
3467
|
lines.push("Refs:");
|
|
3346
|
-
for (const ref of
|
|
3468
|
+
for (const ref of finding2.refs.slice(0, 5)) lines.push(`- ${ref}`);
|
|
3347
3469
|
}
|
|
3348
3470
|
});
|
|
3349
3471
|
return `${lines.join("\n")}
|
|
@@ -3804,55 +3926,55 @@ function normalizeExternalFindings(results) {
|
|
|
3804
3926
|
for (const result of results) {
|
|
3805
3927
|
if (result.findings) out.push(...result.findings);
|
|
3806
3928
|
externalRows(result.output).forEach((row, index) => {
|
|
3807
|
-
const
|
|
3808
|
-
if (
|
|
3929
|
+
const finding2 = coerceExternalFinding(result.analyzer, row, index);
|
|
3930
|
+
if (finding2) out.push(finding2);
|
|
3809
3931
|
});
|
|
3810
3932
|
}
|
|
3811
3933
|
return out;
|
|
3812
3934
|
}
|
|
3813
|
-
function sourceOf(
|
|
3814
|
-
if (
|
|
3815
|
-
if (
|
|
3935
|
+
function sourceOf(finding2) {
|
|
3936
|
+
if (finding2.analyst_id.startsWith("external:")) return "external";
|
|
3937
|
+
if (finding2.analyst_id === "traces-deterministic") return "deterministic";
|
|
3816
3938
|
return "analyst";
|
|
3817
3939
|
}
|
|
3818
|
-
function recommendationTitle(
|
|
3819
|
-
if (
|
|
3820
|
-
return `Investigate ${
|
|
3940
|
+
function recommendationTitle(finding2) {
|
|
3941
|
+
if (finding2.recommended_action) return finding2.recommended_action.replace(/\.$/, "");
|
|
3942
|
+
return `Investigate ${finding2.area}: ${finding2.claim}`.replace(/\.$/, "");
|
|
3821
3943
|
}
|
|
3822
3944
|
function buildRecommendations(findings) {
|
|
3823
|
-
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) => ({
|
|
3824
3946
|
schemaVersion: 1,
|
|
3825
|
-
id: `rec-${index + 1}-${slug(
|
|
3826
|
-
severity:
|
|
3827
|
-
title: recommendationTitle(
|
|
3828
|
-
action:
|
|
3829
|
-
rationale:
|
|
3830
|
-
evidenceRefs:
|
|
3831
|
-
findingIds: [
|
|
3832
|
-
validationPlan:
|
|
3833
|
-
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)
|
|
3834
3956
|
}));
|
|
3835
3957
|
}
|
|
3836
3958
|
function buildClaims(findings) {
|
|
3837
|
-
return findings.map((
|
|
3959
|
+
return findings.map((finding2, index) => ({
|
|
3838
3960
|
schemaVersion: 1,
|
|
3839
|
-
id: `claim-${index + 1}-${slug(
|
|
3840
|
-
severity:
|
|
3841
|
-
text:
|
|
3842
|
-
area:
|
|
3843
|
-
sourceFindingId:
|
|
3844
|
-
evidenceRefs:
|
|
3845
|
-
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
|
|
3846
3968
|
}));
|
|
3847
3969
|
}
|
|
3848
3970
|
function buildEvidenceRows(findings) {
|
|
3849
|
-
return findings.flatMap((
|
|
3971
|
+
return findings.flatMap((finding2) => finding2.evidence_refs.map((ref) => ({
|
|
3850
3972
|
schemaVersion: 1,
|
|
3851
3973
|
kind: "traces.improvement_evidence",
|
|
3852
|
-
findingId:
|
|
3853
|
-
severity:
|
|
3854
|
-
area:
|
|
3855
|
-
claim:
|
|
3974
|
+
findingId: finding2.finding_id,
|
|
3975
|
+
severity: finding2.severity,
|
|
3976
|
+
area: finding2.area,
|
|
3977
|
+
claim: finding2.claim,
|
|
3856
3978
|
evidence: ref
|
|
3857
3979
|
})));
|
|
3858
3980
|
}
|
|
@@ -3884,14 +4006,14 @@ function renderFindingPacket(packet, title = "Trace finding packet") {
|
|
|
3884
4006
|
if (packet.findings.length > 0) {
|
|
3885
4007
|
lines.push("## findings");
|
|
3886
4008
|
lines.push("");
|
|
3887
|
-
for (const
|
|
3888
|
-
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}`);
|
|
3889
4011
|
lines.push("");
|
|
3890
|
-
lines.push(`- **Area:** ${
|
|
3891
|
-
lines.push(`- **Analyst:** ${
|
|
3892
|
-
if (
|
|
3893
|
-
if (
|
|
3894
|
-
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)) {
|
|
3895
4017
|
lines.push(`- **Evidence:** ${ref.kind} ${ref.uri}${ref.excerpt ? ` \u2014 \`${ref.excerpt.slice(0, 180)}\`` : ""}`);
|
|
3896
4018
|
}
|
|
3897
4019
|
lines.push("");
|
|
@@ -4120,8 +4242,16 @@ async function saveReport(path, report) {
|
|
|
4120
4242
|
await writeFile4(path, report, "utf8");
|
|
4121
4243
|
}
|
|
4122
4244
|
|
|
4123
|
-
// 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;
|
|
4124
4250
|
var sleep = (ms, signal) => new Promise((resolve2) => {
|
|
4251
|
+
if (signal?.aborted) {
|
|
4252
|
+
resolve2();
|
|
4253
|
+
return;
|
|
4254
|
+
}
|
|
4125
4255
|
const onAbort = () => {
|
|
4126
4256
|
clearTimeout(t);
|
|
4127
4257
|
resolve2();
|
|
@@ -4132,14 +4262,329 @@ var sleep = (ms, signal) => new Promise((resolve2) => {
|
|
|
4132
4262
|
}, ms);
|
|
4133
4263
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4134
4264
|
});
|
|
4135
|
-
function
|
|
4136
|
-
return
|
|
4265
|
+
function hash(parts) {
|
|
4266
|
+
return createHash2("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
4267
|
+
}
|
|
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 timeOf(span2) {
|
|
4301
|
+
const parsed = Date.parse(span2.start_time);
|
|
4302
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
4303
|
+
}
|
|
4304
|
+
function orderSpans(spans) {
|
|
4305
|
+
return [...spans].sort((a, b) => {
|
|
4306
|
+
const stepA = typeof a.attributes.step === "number" ? a.attributes.step : void 0;
|
|
4307
|
+
const stepB = typeof b.attributes.step === "number" ? b.attributes.step : void 0;
|
|
4308
|
+
if (stepA != null && stepB != null && stepA !== stepB) return stepA - stepB;
|
|
4309
|
+
return timeOf(a) - timeOf(b);
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
function sessionFrom(spans, opts) {
|
|
4313
|
+
const first = spans[0];
|
|
4314
|
+
return {
|
|
4315
|
+
harness: opts.session?.harness ?? asString(first?.attributes["service.name"]) ?? "unknown",
|
|
4316
|
+
sessionId: opts.session?.sessionId ?? first?.trace_id ?? "unknown",
|
|
4317
|
+
cwd: opts.session?.cwd ?? asString(first?.attributes["tangle.cwd"]) ?? null,
|
|
4318
|
+
...opts.session?.path ? { path: opts.session.path } : {}
|
|
4319
|
+
};
|
|
4320
|
+
}
|
|
4321
|
+
function evidence2(kind, label, value, spanIds) {
|
|
4322
|
+
return { kind, label, value, ...spanIds && spanIds.length > 0 ? { spanIds } : {} };
|
|
4323
|
+
}
|
|
4324
|
+
function finding(input) {
|
|
4325
|
+
const fingerprint = hash([input.ruleId, ...input.signature]);
|
|
4326
|
+
return {
|
|
4327
|
+
schemaVersion: 1,
|
|
4328
|
+
kind: "traces.live_finding",
|
|
4329
|
+
id: `live.${input.ruleId}.${fingerprint}`,
|
|
4330
|
+
fingerprint,
|
|
4331
|
+
ruleId: input.ruleId,
|
|
4332
|
+
severity: input.severity,
|
|
4333
|
+
title: input.title,
|
|
4334
|
+
claim: input.claim,
|
|
4335
|
+
action: input.action,
|
|
4336
|
+
check: input.check,
|
|
4337
|
+
evidence: input.evidence,
|
|
4338
|
+
session: input.session,
|
|
4339
|
+
observedAt: input.observedAt
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
function analyzeLiveBatch(spans, opts = {}) {
|
|
4343
|
+
const ordered2 = orderSpans(spans);
|
|
4344
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4345
|
+
const session = sessionFrom(ordered2, opts);
|
|
4346
|
+
const tools = ordered2.filter(isTool);
|
|
4347
|
+
const erroredTools = tools.filter((span2) => span2.status.code === "ERROR");
|
|
4348
|
+
const verificationCalls = tools.filter(isVerification);
|
|
4349
|
+
const changeCalls = tools.filter(isChange);
|
|
4350
|
+
const findings = [];
|
|
4351
|
+
const failedBySignature = /* @__PURE__ */ new Map();
|
|
4352
|
+
for (const span2 of erroredTools) {
|
|
4353
|
+
const signature = toolSignature(span2);
|
|
4354
|
+
failedBySignature.set(signature, [...failedBySignature.get(signature) ?? [], span2]);
|
|
4355
|
+
}
|
|
4356
|
+
for (const [signature, rows] of failedBySignature) {
|
|
4357
|
+
if (rows.length < 2) continue;
|
|
4358
|
+
findings.push(finding({
|
|
4359
|
+
ruleId: "same-failing-command",
|
|
4360
|
+
severity: rows.length >= 3 ? "high" : "medium",
|
|
4361
|
+
title: "Same failing command is repeating",
|
|
4362
|
+
claim: `${rows.length} failed tool call(s) repeated the same command or arguments.`,
|
|
4363
|
+
action: "Stop rerunning it; inspect the first failure, change state, or choose a different diagnostic path before another retry.",
|
|
4364
|
+
check: "The next action should read the failing file, inspect the stack/output, or edit code before this command runs again.",
|
|
4365
|
+
evidence: [
|
|
4366
|
+
evidence2("pattern", "command", signature, rows.map((span2) => span2.span_id)),
|
|
4367
|
+
evidence2("metric", "failed_repeats", String(rows.length))
|
|
4368
|
+
],
|
|
4369
|
+
session,
|
|
4370
|
+
observedAt: generatedAt,
|
|
4371
|
+
signature: [signature, String(rows.length)]
|
|
4372
|
+
}));
|
|
4373
|
+
}
|
|
4374
|
+
let lastChangeIndex = -1;
|
|
4375
|
+
let verificationSinceChange = 0;
|
|
4376
|
+
const verificationSpanIds = [];
|
|
4377
|
+
for (let i = 0; i < ordered2.length; i++) {
|
|
4378
|
+
const span2 = ordered2[i];
|
|
4379
|
+
if (isChange(span2)) {
|
|
4380
|
+
lastChangeIndex = i;
|
|
4381
|
+
verificationSinceChange = 0;
|
|
4382
|
+
verificationSpanIds.length = 0;
|
|
4383
|
+
} else if (isVerification(span2)) {
|
|
4384
|
+
verificationSinceChange += 1;
|
|
4385
|
+
verificationSpanIds.push(span2.span_id);
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
if (verificationSinceChange >= 2) {
|
|
4389
|
+
findings.push(finding({
|
|
4390
|
+
ruleId: "verification-without-change",
|
|
4391
|
+
severity: verificationSinceChange >= 3 ? "high" : "medium",
|
|
4392
|
+
title: "Verification is repeating without a state change",
|
|
4393
|
+
claim: `${verificationSinceChange} verification command(s) ran after the last detected code/config change.`,
|
|
4394
|
+
action: "Do not spend another verification run until the agent either edits state or explains what new signal the rerun will reveal.",
|
|
4395
|
+
check: "The next action should be a code/config edit, targeted file read, or a different narrow diagnostic command.",
|
|
4396
|
+
evidence: [
|
|
4397
|
+
evidence2("metric", "verification_calls_since_change", String(verificationSinceChange), verificationSpanIds),
|
|
4398
|
+
evidence2("metric", "change_calls", String(changeCalls.length))
|
|
4399
|
+
],
|
|
4400
|
+
session,
|
|
4401
|
+
observedAt: generatedAt,
|
|
4402
|
+
signature: [String(verificationSinceChange), verificationSpanIds.join(",")]
|
|
4403
|
+
}));
|
|
4404
|
+
}
|
|
4405
|
+
const lastClaim = [...ordered2].reverse().find((span2) => isTextSpan(span2) && CLAIM_DONE_RE.test(spanContent(span2)));
|
|
4406
|
+
if (lastClaim) {
|
|
4407
|
+
const claimTime = timeOf(lastClaim);
|
|
4408
|
+
const verificationAfterClaim = verificationCalls.some((span2) => timeOf(span2) > claimTime);
|
|
4409
|
+
if (!verificationAfterClaim) {
|
|
4410
|
+
findings.push(finding({
|
|
4411
|
+
ruleId: "completion-claim-without-verification",
|
|
4412
|
+
severity: "medium",
|
|
4413
|
+
title: "Completion claim has no later verification",
|
|
4414
|
+
claim: "The agent claimed completion, correctness, or verification without a later verification tool call in the observed trace.",
|
|
4415
|
+
action: "Require a concrete check after the claim, or downgrade the claim to an unverified hypothesis.",
|
|
4416
|
+
check: "A later test/build/typecheck/lint/smoke command should appear after the claim.",
|
|
4417
|
+
evidence: [
|
|
4418
|
+
evidence2("span", "claim_span", spanContent(lastClaim).slice(0, 240), [lastClaim.span_id]),
|
|
4419
|
+
evidence2("metric", "verification_after_claim", "0")
|
|
4420
|
+
],
|
|
4421
|
+
session,
|
|
4422
|
+
observedAt: generatedAt,
|
|
4423
|
+
signature: [lastClaim.span_id, spanContent(lastClaim).slice(0, 120)]
|
|
4424
|
+
}));
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
if (tools.length >= 4 && erroredTools.length / tools.length >= 0.5) {
|
|
4428
|
+
findings.push(finding({
|
|
4429
|
+
ruleId: "high-tool-error-rate",
|
|
4430
|
+
severity: erroredTools.length / tools.length >= 0.75 ? "high" : "medium",
|
|
4431
|
+
title: "Tool calls are mostly failing",
|
|
4432
|
+
claim: `${erroredTools.length}/${tools.length} tool call(s) in this live batch ended with ERROR.`,
|
|
4433
|
+
action: "Switch from broad execution to diagnosis: classify the dominant error, then run one targeted command to validate the fix path.",
|
|
4434
|
+
check: "The next batch should reduce the error ratio or show a new targeted diagnostic result.",
|
|
4435
|
+
evidence: [
|
|
4436
|
+
evidence2("metric", "errored_tool_calls", `${erroredTools.length}/${tools.length}`, erroredTools.map((span2) => span2.span_id))
|
|
4437
|
+
],
|
|
4438
|
+
session,
|
|
4439
|
+
observedAt: generatedAt,
|
|
4440
|
+
signature: [String(erroredTools.length), String(tools.length)]
|
|
4441
|
+
}));
|
|
4442
|
+
}
|
|
4443
|
+
return {
|
|
4444
|
+
schemaVersion: 1,
|
|
4445
|
+
kind: "traces.live_batch",
|
|
4446
|
+
generatedAt,
|
|
4447
|
+
session,
|
|
4448
|
+
spanCount: ordered2.length,
|
|
4449
|
+
newSpanCount: opts.newSpanCount ?? ordered2.length,
|
|
4450
|
+
toolCallCount: tools.length,
|
|
4451
|
+
erroredToolCallCount: erroredTools.length,
|
|
4452
|
+
verificationCallCount: verificationCalls.length,
|
|
4453
|
+
changeCallCount: changeCalls.length,
|
|
4454
|
+
findingCount: findings.length,
|
|
4455
|
+
findings
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
function sessionEvent(session, spans, generatedAt) {
|
|
4459
|
+
const ordered2 = orderSpans(spans);
|
|
4460
|
+
return {
|
|
4461
|
+
schemaVersion: 1,
|
|
4462
|
+
kind: "traces.stream.session",
|
|
4463
|
+
event: "session",
|
|
4464
|
+
generatedAt,
|
|
4465
|
+
session,
|
|
4466
|
+
spanCount: ordered2.length,
|
|
4467
|
+
startedAt: ordered2[0]?.start_time ?? null,
|
|
4468
|
+
endedAt: ordered2.at(-1)?.end_time ?? null
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
function traceStreamEventsFromSpans(spans, opts = {}) {
|
|
4472
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4473
|
+
const session = sessionFrom(spans, {
|
|
4474
|
+
...opts,
|
|
4475
|
+
session: opts.ref ? { harness: opts.ref.harness, sessionId: opts.ref.sessionId, cwd: opts.ref.cwd, path: opts.ref.path } : opts.session
|
|
4476
|
+
});
|
|
4477
|
+
const ordered2 = orderSpans(spans);
|
|
4478
|
+
const batch = analyzeLiveBatch(ordered2, { generatedAt, session, newSpanCount: ordered2.length });
|
|
4479
|
+
const events = [sessionEvent(session, ordered2, generatedAt)];
|
|
4480
|
+
if (opts.includeSpans !== false) {
|
|
4481
|
+
for (const span2 of ordered2) {
|
|
4482
|
+
events.push({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
events.push({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, batch });
|
|
4486
|
+
if (opts.includeFindings !== false) {
|
|
4487
|
+
for (const liveFinding of batch.findings) {
|
|
4488
|
+
events.push({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
|
|
4489
|
+
}
|
|
4490
|
+
}
|
|
4491
|
+
return events;
|
|
4492
|
+
}
|
|
4493
|
+
function serializeTraceStreamEvent(event) {
|
|
4494
|
+
return `${JSON.stringify(event)}
|
|
4495
|
+
`;
|
|
4496
|
+
}
|
|
4497
|
+
async function streamSessions(opts) {
|
|
4498
|
+
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
4499
|
+
const windowMs = opts.windowMs ?? 30 * 6e4;
|
|
4500
|
+
const includeSpans = opts.includeSpans !== false;
|
|
4501
|
+
const includeFindings = opts.includeFindings !== false;
|
|
4502
|
+
const includeBatches = opts.includeBatches !== false;
|
|
4503
|
+
const seenSessions = /* @__PURE__ */ new Set();
|
|
4504
|
+
const seenSpans = /* @__PURE__ */ new Set();
|
|
4505
|
+
const seenFindings = /* @__PURE__ */ new Set();
|
|
4506
|
+
while (!opts.signal?.aborted) {
|
|
4507
|
+
let sessions = 0;
|
|
4508
|
+
let newSpans = 0;
|
|
4509
|
+
let findings = 0;
|
|
4510
|
+
for await (const { adapter, ref, spans } of scanSessions({
|
|
4511
|
+
...opts,
|
|
4512
|
+
sinceMs: opts.sinceMs ?? Date.now() - windowMs,
|
|
4513
|
+
signal: opts.signal
|
|
4514
|
+
})) {
|
|
4515
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4516
|
+
const session = { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path };
|
|
4517
|
+
const ordered2 = orderSpans(spans);
|
|
4518
|
+
const sessionKey = `${session.harness}:${session.sessionId}`;
|
|
4519
|
+
const unseen = ordered2.filter((span2) => {
|
|
4520
|
+
const key = `${sessionKey}:${span2.span_id}`;
|
|
4521
|
+
if (seenSpans.has(key)) return false;
|
|
4522
|
+
seenSpans.add(key);
|
|
4523
|
+
return true;
|
|
4524
|
+
});
|
|
4525
|
+
sessions += 1;
|
|
4526
|
+
newSpans += unseen.length;
|
|
4527
|
+
if (!seenSessions.has(sessionKey)) {
|
|
4528
|
+
seenSessions.add(sessionKey);
|
|
4529
|
+
await opts.onEvent(sessionEvent(session, ordered2, generatedAt));
|
|
4530
|
+
}
|
|
4531
|
+
if (includeSpans) {
|
|
4532
|
+
for (const span2 of unseen) {
|
|
4533
|
+
await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.span", event: "span", generatedAt, session, span: span2 });
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
const batch = analyzeLiveBatch(ordered2, { generatedAt, session, newSpanCount: unseen.length });
|
|
4537
|
+
if (includeBatches) {
|
|
4538
|
+
await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.batch", event: "analysis_batch", generatedAt, batch });
|
|
4539
|
+
}
|
|
4540
|
+
if (includeFindings) {
|
|
4541
|
+
for (const liveFinding of batch.findings) {
|
|
4542
|
+
if (seenFindings.has(liveFinding.id)) continue;
|
|
4543
|
+
seenFindings.add(liveFinding.id);
|
|
4544
|
+
findings += 1;
|
|
4545
|
+
await opts.onEvent({ schemaVersion: 1, kind: "traces.stream.finding", event: "finding", generatedAt, finding: liveFinding });
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
await opts.onEvent({
|
|
4550
|
+
schemaVersion: 1,
|
|
4551
|
+
kind: "traces.stream.tick",
|
|
4552
|
+
event: "tick",
|
|
4553
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4554
|
+
sessions,
|
|
4555
|
+
newSpans,
|
|
4556
|
+
findings
|
|
4557
|
+
});
|
|
4558
|
+
if (opts.once || opts.signal?.aborted) break;
|
|
4559
|
+
await sleep(intervalMs, opts.signal);
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
// src/observer.ts
|
|
4564
|
+
var sleep2 = (ms, signal) => new Promise((resolve2) => {
|
|
4565
|
+
if (signal?.aborted) {
|
|
4566
|
+
resolve2();
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
const onAbort = () => {
|
|
4570
|
+
clearTimeout(t);
|
|
4571
|
+
resolve2();
|
|
4572
|
+
};
|
|
4573
|
+
const t = setTimeout(() => {
|
|
4574
|
+
signal?.removeEventListener("abort", onAbort);
|
|
4575
|
+
resolve2();
|
|
4576
|
+
}, ms);
|
|
4577
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
4578
|
+
});
|
|
4579
|
+
function loopKey(sessionId, toolName3, argHash) {
|
|
4580
|
+
return `${sessionId}:${toolName3}:${argHash}`;
|
|
4137
4581
|
}
|
|
4138
4582
|
async function watchSessions(opts) {
|
|
4139
4583
|
const windowMs = opts.windowMs ?? 30 * 6e4;
|
|
4140
4584
|
const intervalMs = Math.max(250, opts.intervalMs ?? 5e3);
|
|
4141
4585
|
const minLoop = opts.minLoopOccurrences ?? 3;
|
|
4142
4586
|
const seen = /* @__PURE__ */ new Map();
|
|
4587
|
+
const seenFindings = /* @__PURE__ */ new Set();
|
|
4143
4588
|
while (!opts.signal?.aborted) {
|
|
4144
4589
|
let sessionCount = 0;
|
|
4145
4590
|
for await (const { adapter, ref, spans } of scanSessions({
|
|
@@ -4166,6 +4611,27 @@ async function watchSessions(opts) {
|
|
|
4166
4611
|
opts.onError?.(err, ref);
|
|
4167
4612
|
}
|
|
4168
4613
|
}
|
|
4614
|
+
const batch = analyzeLiveBatch(spans, {
|
|
4615
|
+
session: { harness: adapter.harness, sessionId: ref.sessionId, cwd: ref.cwd, path: ref.path }
|
|
4616
|
+
});
|
|
4617
|
+
if (opts.onBatch) {
|
|
4618
|
+
try {
|
|
4619
|
+
await opts.onBatch(ref, batch);
|
|
4620
|
+
} catch (err) {
|
|
4621
|
+
opts.onError?.(err, ref);
|
|
4622
|
+
}
|
|
4623
|
+
}
|
|
4624
|
+
if (opts.onFinding) {
|
|
4625
|
+
for (const finding2 of batch.findings) {
|
|
4626
|
+
if (seenFindings.has(finding2.id)) continue;
|
|
4627
|
+
seenFindings.add(finding2.id);
|
|
4628
|
+
try {
|
|
4629
|
+
await opts.onFinding(finding2);
|
|
4630
|
+
} catch (err) {
|
|
4631
|
+
opts.onError?.(err, ref);
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4169
4635
|
for (const f of report.stuckLoops.findings) {
|
|
4170
4636
|
const key = loopKey(ref.sessionId, f.toolName, f.argHash);
|
|
4171
4637
|
if (f.occurrences > (seen.get(key) ?? 0)) {
|
|
@@ -4194,7 +4660,7 @@ async function watchSessions(opts) {
|
|
|
4194
4660
|
opts.onError?.(err);
|
|
4195
4661
|
}
|
|
4196
4662
|
if (opts.signal?.aborted) break;
|
|
4197
|
-
await
|
|
4663
|
+
await sleep2(intervalMs, opts.signal);
|
|
4198
4664
|
}
|
|
4199
4665
|
}
|
|
4200
4666
|
|
|
@@ -4421,7 +4887,7 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
4421
4887
|
}
|
|
4422
4888
|
|
|
4423
4889
|
// src/upload-state.ts
|
|
4424
|
-
import { createHash as
|
|
4890
|
+
import { createHash as createHash3 } from "crypto";
|
|
4425
4891
|
import { mkdir as mkdir2, readFile as readFile15, writeFile as writeFile6 } from "fs/promises";
|
|
4426
4892
|
import { homedir as homedir11 } from "os";
|
|
4427
4893
|
import { dirname as dirname4, join as join18 } from "path";
|
|
@@ -4433,7 +4899,7 @@ function uploadKey(harness, sessionId) {
|
|
|
4433
4899
|
return `${harness}:${sessionId}`;
|
|
4434
4900
|
}
|
|
4435
4901
|
function sessionHash(spans) {
|
|
4436
|
-
const h =
|
|
4902
|
+
const h = createHash3("sha256");
|
|
4437
4903
|
for (const s of spans) {
|
|
4438
4904
|
h.update(`${s.span_id}\0${s.name}\0${s.start_time}\0${s.status.code}
|
|
4439
4905
|
`);
|
|
@@ -4453,8 +4919,8 @@ async function saveState(state) {
|
|
|
4453
4919
|
await writeFile6(f, `${JSON.stringify(state, null, 2)}
|
|
4454
4920
|
`, "utf8");
|
|
4455
4921
|
}
|
|
4456
|
-
function alreadyUploaded(state, harness, sessionId,
|
|
4457
|
-
return state[uploadKey(harness, sessionId)]?.hash ===
|
|
4922
|
+
function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
4923
|
+
return state[uploadKey(harness, sessionId)]?.hash === hash2;
|
|
4458
4924
|
}
|
|
4459
4925
|
|
|
4460
4926
|
// src/upload.ts
|
|
@@ -4471,13 +4937,13 @@ async function planUpload(opts = {}) {
|
|
|
4471
4937
|
const items = [];
|
|
4472
4938
|
for await (const { ref, spans: raw } of scanSessions(opts)) {
|
|
4473
4939
|
const { spans, report } = redactSpans(raw);
|
|
4474
|
-
const
|
|
4940
|
+
const hash2 = sessionHash(spans);
|
|
4475
4941
|
items.push({
|
|
4476
4942
|
ref,
|
|
4477
4943
|
spans,
|
|
4478
4944
|
redaction: report,
|
|
4479
|
-
hash,
|
|
4480
|
-
isNew: !alreadyUploaded(state, ref.harness, ref.sessionId,
|
|
4945
|
+
hash: hash2,
|
|
4946
|
+
isNew: !alreadyUploaded(state, ref.harness, ref.sessionId, hash2)
|
|
4481
4947
|
});
|
|
4482
4948
|
}
|
|
4483
4949
|
return { items, state, sinceMs: opts.sinceMs };
|
|
@@ -4674,6 +5140,10 @@ export {
|
|
|
4674
5140
|
writeTraceImprovementArtifacts,
|
|
4675
5141
|
runTraceImprovementLoop,
|
|
4676
5142
|
saveReport,
|
|
5143
|
+
analyzeLiveBatch,
|
|
5144
|
+
traceStreamEventsFromSpans,
|
|
5145
|
+
serializeTraceStreamEvent,
|
|
5146
|
+
streamSessions,
|
|
4677
5147
|
watchSessions,
|
|
4678
5148
|
buildSessionIndex,
|
|
4679
5149
|
buildSessionIndexFromRows,
|