@tangle-network/traces 0.11.5 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -2
- package/dist/{chunk-VTYOCFGF.js → chunk-CV22VGCK.js} +1594 -254
- package/dist/cli.js +163 -61
- package/dist/index.d.ts +518 -2
- package/dist/index.js +37 -1
- package/docs/trace-analysts.md +39 -1
- package/examples/external-engines.ts +14 -1
- package/package.json +6 -5
- package/skills/build-trace-analyst/SKILL.md +13 -0
- package/skills/inspect-agent-traces/SKILL.md +30 -4
|
@@ -3,6 +3,145 @@ function appendAll(target, values) {
|
|
|
3
3
|
for (const value of values) target.push(value);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
// src/attributes.ts
|
|
7
|
+
var ATTR = {
|
|
8
|
+
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
9
|
+
SESSION_ID: "tangle.sessionId",
|
|
10
|
+
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
11
|
+
* that also streamed live. Wire contract. */
|
|
12
|
+
INGEST_SOURCE: "tangle.ingest_source",
|
|
13
|
+
HARNESS: "tangle.harness",
|
|
14
|
+
CWD: "tangle.cwd",
|
|
15
|
+
/** THE per-session grouping key the spine derives subjects from
|
|
16
|
+
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
17
|
+
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
18
|
+
SUBJECT_KEY: "tangle.subject.key",
|
|
19
|
+
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
|
|
20
|
+
GIT_REPOSITORY: "git.repository",
|
|
21
|
+
/** Current branch at conversion time. */
|
|
22
|
+
GIT_BRANCH_NAME: "git.branch",
|
|
23
|
+
/** HEAD short sha at conversion time. */
|
|
24
|
+
GIT_COMMIT: "git.commit",
|
|
25
|
+
/** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
|
|
26
|
+
REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
|
|
27
|
+
GIT_BRANCH: "tangle.git_branch",
|
|
28
|
+
HOST: "tangle.host",
|
|
29
|
+
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
30
|
+
* which collided with `tangle.ingest_source`.) */
|
|
31
|
+
SESSION_FILE: "tangle.session_file",
|
|
32
|
+
UPLOADED_AT: "tangle.uploaded_at",
|
|
33
|
+
UPLOADER: "tangle.uploader",
|
|
34
|
+
REDACTION_VERSION: "redaction.version",
|
|
35
|
+
REDACTION_COUNT: "redaction.count",
|
|
36
|
+
/** Upload contract: bounded root summary plus location-only receipt child spans.
|
|
37
|
+
* Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
|
|
38
|
+
SESSION_INTEGRITY: "traces.session.integrity",
|
|
39
|
+
CORRUPTION_COUNT: "traces.session.corruption_count",
|
|
40
|
+
CORRUPTION_DIGEST: "traces.session.corruption_digest",
|
|
41
|
+
CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
|
|
42
|
+
CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
|
|
43
|
+
CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
|
|
44
|
+
CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
|
|
45
|
+
CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
|
|
46
|
+
CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
|
|
47
|
+
CORRUPTION_SHA256: "traces.session.corruption.sha256",
|
|
48
|
+
RAW_SOURCE_RETENTION: "traces.session.raw_source_retention",
|
|
49
|
+
/** Container image reference the session executed in (OTel semconv key). */
|
|
50
|
+
CONTAINER_IMAGE: "container.image.name",
|
|
51
|
+
/** Immutable image digest (`sha256:…`) — the replay-grade environment pin. */
|
|
52
|
+
CONTAINER_IMAGE_DIGEST: "container.image.digest",
|
|
53
|
+
/** Sandbox/container instance id that executed the session. */
|
|
54
|
+
SANDBOX_ID: "tangle.sandbox.id",
|
|
55
|
+
/** Recorded execution cwd, verbatim. Unlike `tangle.cwd` it is never
|
|
56
|
+
* repaired against the host filesystem — replay verification needs the
|
|
57
|
+
* in-container path even when no such path exists on this host. */
|
|
58
|
+
ENVIRONMENT_CWD: "tangle.environment.cwd"
|
|
59
|
+
};
|
|
60
|
+
var INGEST_SOURCE_CLI = "cli";
|
|
61
|
+
function environmentAttributes(environment) {
|
|
62
|
+
if (!environment) return {};
|
|
63
|
+
const attrs = {};
|
|
64
|
+
if (environment.image) attrs[ATTR.CONTAINER_IMAGE] = environment.image;
|
|
65
|
+
if (environment.imageDigest) attrs[ATTR.CONTAINER_IMAGE_DIGEST] = environment.imageDigest;
|
|
66
|
+
if (environment.sandboxId) attrs[ATTR.SANDBOX_ID] = environment.sandboxId;
|
|
67
|
+
if (environment.cwd) attrs[ATTR.ENVIRONMENT_CWD] = environment.cwd;
|
|
68
|
+
if (environment.gitCommit) attrs[ATTR.GIT_COMMIT] = environment.gitCommit;
|
|
69
|
+
return attrs;
|
|
70
|
+
}
|
|
71
|
+
function stampEnvironmentAttrs(spans, environment) {
|
|
72
|
+
const attrs = environmentAttributes(environment);
|
|
73
|
+
const keys = Object.keys(attrs);
|
|
74
|
+
if (keys.length === 0) return;
|
|
75
|
+
for (const span2 of spans) {
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
if (span2.attributes[key] === void 0) span2.attributes[key] = attrs[key];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function environmentFromSpanAttributes(spans) {
|
|
82
|
+
const first = (key) => {
|
|
83
|
+
for (const span2 of spans) {
|
|
84
|
+
const value = span2.attributes[key];
|
|
85
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
};
|
|
89
|
+
const image = first(ATTR.CONTAINER_IMAGE);
|
|
90
|
+
const imageDigest = first(ATTR.CONTAINER_IMAGE_DIGEST);
|
|
91
|
+
const sandboxId = first(ATTR.SANDBOX_ID);
|
|
92
|
+
const cwd = first(ATTR.ENVIRONMENT_CWD);
|
|
93
|
+
if (!image && !imageDigest && !sandboxId && !cwd) return null;
|
|
94
|
+
return {
|
|
95
|
+
...image ? { image } : {},
|
|
96
|
+
...imageDigest ? { imageDigest } : {},
|
|
97
|
+
...sandboxId ? { sandboxId } : {},
|
|
98
|
+
cwd: cwd ?? null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
var DEFAULT_HARNESS = "claude-code";
|
|
102
|
+
var SESSION_ID_ATTRIBUTE_KEYS = [ATTR.SESSION_ID, "session.id", "traces.session.id"];
|
|
103
|
+
function sessionIdFromAttributes(attributes2) {
|
|
104
|
+
return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes2[key]).find(
|
|
105
|
+
(value) => typeof value === "string" && value.length > 0
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function indexSessionIdsByTrace(spans) {
|
|
109
|
+
const candidatesByTrace = /* @__PURE__ */ new Map();
|
|
110
|
+
for (const span2 of spans) {
|
|
111
|
+
const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
|
|
112
|
+
for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
|
|
113
|
+
const value = span2.attributes[key];
|
|
114
|
+
if (typeof value === "string" && value.length > 0) candidates.add(value);
|
|
115
|
+
}
|
|
116
|
+
if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
|
|
117
|
+
}
|
|
118
|
+
const sessionByTrace = /* @__PURE__ */ new Map();
|
|
119
|
+
const conflicts = [];
|
|
120
|
+
for (const [traceId, candidates] of candidatesByTrace) {
|
|
121
|
+
const sessionIds = [...candidates].sort();
|
|
122
|
+
if (sessionIds.length === 1) {
|
|
123
|
+
sessionByTrace.set(traceId, sessionIds[0]);
|
|
124
|
+
} else {
|
|
125
|
+
conflicts.push({ traceId, sessionIds });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
129
|
+
return { sessionByTrace, conflicts };
|
|
130
|
+
}
|
|
131
|
+
function stampSessionIdentity(spans, fallbackSessionId) {
|
|
132
|
+
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
133
|
+
const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
|
|
134
|
+
const traceIds = new Set(spans.map((span2) => span2.trace_id));
|
|
135
|
+
const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
|
|
136
|
+
for (const span2 of spans) {
|
|
137
|
+
if (conflictingTraceIds.has(span2.trace_id)) continue;
|
|
138
|
+
const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
|
|
139
|
+
if (!sessionIdFromAttributes(span2.attributes)) {
|
|
140
|
+
span2.attributes[ATTR.SESSION_ID] = sessionId;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
6
145
|
// src/time.ts
|
|
7
146
|
function parseIsoToEpochMs(ts) {
|
|
8
147
|
if (typeof ts !== "string" || ts.trim().length === 0) {
|
|
@@ -31,7 +170,7 @@ function parseSince(value) {
|
|
|
31
170
|
import { readdir, readFile, stat } from "fs/promises";
|
|
32
171
|
import { basename, join } from "path";
|
|
33
172
|
import {
|
|
34
|
-
ATTR,
|
|
173
|
+
ATTR as ATTR2,
|
|
35
174
|
declaredSpanKind,
|
|
36
175
|
resolveSpanKind,
|
|
37
176
|
SPAN_KIND_ATTR_KEYS,
|
|
@@ -325,7 +464,7 @@ function readLinks(value, traceId) {
|
|
|
325
464
|
return links;
|
|
326
465
|
}
|
|
327
466
|
function bridgeContractAttributes(attributes2) {
|
|
328
|
-
const toolName4 = nonEmptyString(attributes2[
|
|
467
|
+
const toolName4 = nonEmptyString(attributes2[ATTR2.toolName]);
|
|
329
468
|
if (toolName4 !== null && nonEmptyString(attributes2[TOOL_NAME]) === null) {
|
|
330
469
|
attributes2[TOOL_NAME] = toolName4;
|
|
331
470
|
}
|
|
@@ -385,7 +524,7 @@ function otlpRowToSpan(row, fallbackTraceId) {
|
|
|
385
524
|
bridgeContractAttributes(attributes2);
|
|
386
525
|
const { kind, declaredRaw, substituted: kindSubstituted } = resolveRowKind(row, attributes2, name);
|
|
387
526
|
if (kindSubstituted) substitute("kind", declaredRaw ?? void 0);
|
|
388
|
-
attributes2[
|
|
527
|
+
attributes2[ATTR2.spanKind] = kind;
|
|
389
528
|
if (kind === "TOOL") Object.assign(attributes2, capturedToolIo(attributes2));
|
|
390
529
|
const status = isObject(row.status) ? row.status : void 0;
|
|
391
530
|
const statusMessage = nonEmptyString(status?.message);
|
|
@@ -725,6 +864,9 @@ function toOpenInferenceSpan(s) {
|
|
|
725
864
|
for (const k of ["tangle.subject.key", "git.repository", "git.branch", "git.commit", "tangle.cwd", "traces.repo_resolution_source"]) {
|
|
726
865
|
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
727
866
|
}
|
|
867
|
+
for (const k of ["container.image.name", "container.image.digest", "tangle.sandbox.id", "tangle.environment.cwd"]) {
|
|
868
|
+
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
869
|
+
}
|
|
728
870
|
for (const k of ["traces.source.unreadable_rows", "traces.source.unreadable_row_kinds"]) {
|
|
729
871
|
if (a[k] != null) resourceAttrs[k] = a[k];
|
|
730
872
|
}
|
|
@@ -764,11 +906,11 @@ function serializeSpans(spans) {
|
|
|
764
906
|
`;
|
|
765
907
|
}
|
|
766
908
|
async function writeOtlpFile(spans, outPath) {
|
|
767
|
-
const { mkdtemp: mkdtemp8, writeFile:
|
|
909
|
+
const { mkdtemp: mkdtemp8, writeFile: writeFile12 } = await import("fs/promises");
|
|
768
910
|
const { tmpdir: tmpdir9 } = await import("os");
|
|
769
|
-
const { join:
|
|
770
|
-
const path = outPath ??
|
|
771
|
-
await
|
|
911
|
+
const { join: join36 } = await import("path");
|
|
912
|
+
const path = outPath ?? join36(await mkdtemp8(join36(tmpdir9(), "traces-")), "spans.otlp.jsonl");
|
|
913
|
+
await writeFile12(path, serializeSpans(spans), "utf8");
|
|
772
914
|
return path;
|
|
773
915
|
}
|
|
774
916
|
|
|
@@ -796,95 +938,6 @@ function userPromptSpan(o) {
|
|
|
796
938
|
});
|
|
797
939
|
}
|
|
798
940
|
|
|
799
|
-
// src/attributes.ts
|
|
800
|
-
var ATTR2 = {
|
|
801
|
-
/** = the trace/session id. Wire contract (dedup linkage). */
|
|
802
|
-
SESSION_ID: "tangle.sessionId",
|
|
803
|
-
/** "cli" marks a CLI upload so the platform can drop a re-upload of a session
|
|
804
|
-
* that also streamed live. Wire contract. */
|
|
805
|
-
INGEST_SOURCE: "tangle.ingest_source",
|
|
806
|
-
HARNESS: "tangle.harness",
|
|
807
|
-
CWD: "tangle.cwd",
|
|
808
|
-
/** THE per-session grouping key the spine derives subjects from
|
|
809
|
-
* (`deriveSubjectKey` reads `tangle.subject.key` first). Resolves to the git
|
|
810
|
-
* remote (host/owner/repo) when readable, else the cwd path basename. */
|
|
811
|
-
SUBJECT_KEY: "tangle.subject.key",
|
|
812
|
-
/** Normalized git remote (e.g. `github.com/tangle-network/agent-dev-container`). */
|
|
813
|
-
GIT_REPOSITORY: "git.repository",
|
|
814
|
-
/** Current branch at conversion time. */
|
|
815
|
-
GIT_BRANCH_NAME: "git.branch",
|
|
816
|
-
/** HEAD short sha at conversion time. */
|
|
817
|
-
GIT_COMMIT: "git.commit",
|
|
818
|
-
/** How traces resolved the session cwd: recorded cwd, repaired cwd, or span path. */
|
|
819
|
-
REPO_RESOLUTION_SOURCE: "traces.repo_resolution_source",
|
|
820
|
-
GIT_BRANCH: "tangle.git_branch",
|
|
821
|
-
HOST: "tangle.host",
|
|
822
|
-
/** Basename of the session file. (Renamed from the ambiguous `tangle.source`,
|
|
823
|
-
* which collided with `tangle.ingest_source`.) */
|
|
824
|
-
SESSION_FILE: "tangle.session_file",
|
|
825
|
-
UPLOADED_AT: "tangle.uploaded_at",
|
|
826
|
-
UPLOADER: "tangle.uploader",
|
|
827
|
-
REDACTION_VERSION: "redaction.version",
|
|
828
|
-
REDACTION_COUNT: "redaction.count",
|
|
829
|
-
/** Upload contract: bounded root summary plus location-only receipt child spans.
|
|
830
|
-
* Receipt hashes are deliberate integrity evidence; raw malformed bytes never upload. */
|
|
831
|
-
SESSION_INTEGRITY: "traces.session.integrity",
|
|
832
|
-
CORRUPTION_COUNT: "traces.session.corruption_count",
|
|
833
|
-
CORRUPTION_DIGEST: "traces.session.corruption_digest",
|
|
834
|
-
CORRUPTION_RECEIPT_VERSION: "traces.session.corruption.receipt_version",
|
|
835
|
-
CORRUPTION_RECEIPT_KIND: "traces.session.corruption.kind",
|
|
836
|
-
CORRUPTION_SOURCE_PATH: "traces.session.corruption.source_path",
|
|
837
|
-
CORRUPTION_LINE_NUMBER: "traces.session.corruption.line_number",
|
|
838
|
-
CORRUPTION_BYTE_OFFSET: "traces.session.corruption.byte_offset",
|
|
839
|
-
CORRUPTION_BYTE_LENGTH: "traces.session.corruption.byte_length",
|
|
840
|
-
CORRUPTION_SHA256: "traces.session.corruption.sha256",
|
|
841
|
-
RAW_SOURCE_RETENTION: "traces.session.raw_source_retention"
|
|
842
|
-
};
|
|
843
|
-
var INGEST_SOURCE_CLI = "cli";
|
|
844
|
-
var DEFAULT_HARNESS = "claude-code";
|
|
845
|
-
var SESSION_ID_ATTRIBUTE_KEYS = [ATTR2.SESSION_ID, "session.id", "traces.session.id"];
|
|
846
|
-
function sessionIdFromAttributes(attributes2) {
|
|
847
|
-
return SESSION_ID_ATTRIBUTE_KEYS.map((key) => attributes2[key]).find(
|
|
848
|
-
(value) => typeof value === "string" && value.length > 0
|
|
849
|
-
);
|
|
850
|
-
}
|
|
851
|
-
function indexSessionIdsByTrace(spans) {
|
|
852
|
-
const candidatesByTrace = /* @__PURE__ */ new Map();
|
|
853
|
-
for (const span2 of spans) {
|
|
854
|
-
const candidates = candidatesByTrace.get(span2.trace_id) ?? /* @__PURE__ */ new Set();
|
|
855
|
-
for (const key of SESSION_ID_ATTRIBUTE_KEYS) {
|
|
856
|
-
const value = span2.attributes[key];
|
|
857
|
-
if (typeof value === "string" && value.length > 0) candidates.add(value);
|
|
858
|
-
}
|
|
859
|
-
if (candidates.size > 0) candidatesByTrace.set(span2.trace_id, candidates);
|
|
860
|
-
}
|
|
861
|
-
const sessionByTrace = /* @__PURE__ */ new Map();
|
|
862
|
-
const conflicts = [];
|
|
863
|
-
for (const [traceId, candidates] of candidatesByTrace) {
|
|
864
|
-
const sessionIds = [...candidates].sort();
|
|
865
|
-
if (sessionIds.length === 1) {
|
|
866
|
-
sessionByTrace.set(traceId, sessionIds[0]);
|
|
867
|
-
} else {
|
|
868
|
-
conflicts.push({ traceId, sessionIds });
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
conflicts.sort((a, b) => a.traceId.localeCompare(b.traceId));
|
|
872
|
-
return { sessionByTrace, conflicts };
|
|
873
|
-
}
|
|
874
|
-
function stampSessionIdentity(spans, fallbackSessionId) {
|
|
875
|
-
const { sessionByTrace, conflicts } = indexSessionIdsByTrace(spans);
|
|
876
|
-
const conflictingTraceIds = new Set(conflicts.map((conflict) => conflict.traceId));
|
|
877
|
-
const traceIds = new Set(spans.map((span2) => span2.trace_id));
|
|
878
|
-
const singleTraceId = traceIds.size === 1 ? traceIds.values().next().value : void 0;
|
|
879
|
-
for (const span2 of spans) {
|
|
880
|
-
if (conflictingTraceIds.has(span2.trace_id)) continue;
|
|
881
|
-
const sessionId = sessionByTrace.get(span2.trace_id) ?? singleTraceId ?? fallbackSessionId;
|
|
882
|
-
if (!sessionIdFromAttributes(span2.attributes)) {
|
|
883
|
-
span2.attributes[ATTR2.SESSION_ID] = sessionId;
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
|
|
888
941
|
// src/chat-trajectory.ts
|
|
889
942
|
import { createHash as createHash3 } from "crypto";
|
|
890
943
|
function isChatTrajectoryMessage(value) {
|
|
@@ -1763,6 +1816,76 @@ function summarizeSpanExecution(spans, opts = {}) {
|
|
|
1763
1816
|
});
|
|
1764
1817
|
}
|
|
1765
1818
|
|
|
1819
|
+
// src/failure-followup.ts
|
|
1820
|
+
import { OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2, TOOL_NAME as TOOL_NAME3 } from "@tangle-network/agent-eval/trace-attributes";
|
|
1821
|
+
function argKeyOf(span2) {
|
|
1822
|
+
const sha = span2.attributes["traces.input.sha256"];
|
|
1823
|
+
if (typeof sha === "string" && sha.length > 0) return `sha:${sha}`;
|
|
1824
|
+
const value = span2.attributes["input.value"];
|
|
1825
|
+
if (typeof value === "string" && value.length > 0) return `raw:${value}`;
|
|
1826
|
+
return null;
|
|
1827
|
+
}
|
|
1828
|
+
function schemeOf(key) {
|
|
1829
|
+
return key.slice(0, key.indexOf(":"));
|
|
1830
|
+
}
|
|
1831
|
+
function classifyFailureFollowUps(spans) {
|
|
1832
|
+
const calls = [];
|
|
1833
|
+
for (const s of spans) {
|
|
1834
|
+
if (s.attributes[OPENINFERENCE_SPAN_KIND2] !== "TOOL") continue;
|
|
1835
|
+
const toolName4 = s.attributes[TOOL_NAME3];
|
|
1836
|
+
if (typeof toolName4 !== "string" || toolName4.length === 0) continue;
|
|
1837
|
+
calls.push({ span: s, toolName: toolName4, argKey: argKeyOf(s) });
|
|
1838
|
+
}
|
|
1839
|
+
calls.sort(
|
|
1840
|
+
(a, b) => a.span.start_time.localeCompare(b.span.start_time) || (Number(a.span.attributes.step) || 0) - (Number(b.span.attributes.step) || 0)
|
|
1841
|
+
);
|
|
1842
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
1843
|
+
for (const call of calls) {
|
|
1844
|
+
const key = `${call.span.trace_id}|${call.toolName}`;
|
|
1845
|
+
const seq = sequences.get(key);
|
|
1846
|
+
if (seq) seq.push(call);
|
|
1847
|
+
else sequences.set(key, [call]);
|
|
1848
|
+
}
|
|
1849
|
+
const items = [];
|
|
1850
|
+
const blindByTool = {};
|
|
1851
|
+
for (const seq of sequences.values()) {
|
|
1852
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1853
|
+
const call = seq[i];
|
|
1854
|
+
if (call.span.status.code !== "ERROR") continue;
|
|
1855
|
+
if (call.span.attributes["traces.expected_blocking"] === true) continue;
|
|
1856
|
+
const followUp = seq[i + 1] ?? null;
|
|
1857
|
+
let kind;
|
|
1858
|
+
if (!followUp) {
|
|
1859
|
+
kind = "none";
|
|
1860
|
+
} else if (call.argKey !== null && followUp.argKey !== null && schemeOf(call.argKey) === schemeOf(followUp.argKey)) {
|
|
1861
|
+
kind = call.argKey === followUp.argKey ? "blind" : "adapted";
|
|
1862
|
+
} else {
|
|
1863
|
+
kind = "args-unknown";
|
|
1864
|
+
}
|
|
1865
|
+
if (kind === "blind") blindByTool[call.toolName] = (blindByTool[call.toolName] ?? 0) + 1;
|
|
1866
|
+
items.push({
|
|
1867
|
+
traceId: call.span.trace_id,
|
|
1868
|
+
toolName: call.toolName,
|
|
1869
|
+
failedSpanId: call.span.span_id,
|
|
1870
|
+
followUpSpanId: followUp?.span.span_id ?? null,
|
|
1871
|
+
kind,
|
|
1872
|
+
followUpSucceeded: followUp ? followUp.span.status.code !== "ERROR" : null
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const followedItems = items.filter((item) => item.kind !== "none");
|
|
1877
|
+
return {
|
|
1878
|
+
failures: items.length,
|
|
1879
|
+
followed: followedItems.length,
|
|
1880
|
+
blind: items.filter((item) => item.kind === "blind").length,
|
|
1881
|
+
adapted: items.filter((item) => item.kind === "adapted").length,
|
|
1882
|
+
argsUnknown: items.filter((item) => item.kind === "args-unknown").length,
|
|
1883
|
+
followUpSucceeded: followedItems.filter((item) => item.followUpSucceeded === true).length,
|
|
1884
|
+
blindByTool,
|
|
1885
|
+
items
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1766
1889
|
// src/runtime-store.ts
|
|
1767
1890
|
import { InMemoryTraceStore } from "@tangle-network/agent-eval";
|
|
1768
1891
|
var KIND = {
|
|
@@ -1851,7 +1974,7 @@ async function runPipelines(spans, opts = {}) {
|
|
|
1851
1974
|
failureClusterView(store),
|
|
1852
1975
|
Promise.all(runIds.map((runId) => computeToolUseMetrics(store, runId)))
|
|
1853
1976
|
]);
|
|
1854
|
-
return { stuckLoops, failureClusters, toolUse };
|
|
1977
|
+
return { stuckLoops, failureClusters, toolUse, failureFollowUps: classifyFailureFollowUps(spans) };
|
|
1855
1978
|
}
|
|
1856
1979
|
|
|
1857
1980
|
// src/integrity.ts
|
|
@@ -1907,10 +2030,10 @@ function sessionJsonlOptions(ref, options = {}) {
|
|
|
1907
2030
|
function sessionIntegrityAttributes(ref) {
|
|
1908
2031
|
if (!ref.integrity) return {};
|
|
1909
2032
|
return {
|
|
1910
|
-
[
|
|
1911
|
-
[
|
|
1912
|
-
[
|
|
1913
|
-
[
|
|
2033
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
2034
|
+
[ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
|
|
2035
|
+
[ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
|
|
2036
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
1914
2037
|
};
|
|
1915
2038
|
}
|
|
1916
2039
|
function corruptionDigest(receipts) {
|
|
@@ -1951,15 +2074,15 @@ function stampSessionIntegrity(ref, spans) {
|
|
|
1951
2074
|
status: "ERROR",
|
|
1952
2075
|
service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
|
|
1953
2076
|
extra: {
|
|
1954
|
-
[
|
|
1955
|
-
[
|
|
1956
|
-
[
|
|
1957
|
-
[
|
|
1958
|
-
[
|
|
1959
|
-
[
|
|
1960
|
-
[
|
|
1961
|
-
[
|
|
1962
|
-
[
|
|
2077
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
2078
|
+
[ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
|
|
2079
|
+
[ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
|
|
2080
|
+
[ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
|
|
2081
|
+
[ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
|
|
2082
|
+
[ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
|
|
2083
|
+
[ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
|
|
2084
|
+
[ATTR.CORRUPTION_SHA256]: receipt.sha256,
|
|
2085
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
1963
2086
|
}
|
|
1964
2087
|
}));
|
|
1965
2088
|
existingSpanIds.add(spanId);
|
|
@@ -6003,7 +6126,7 @@ function selectAdapters(opts) {
|
|
|
6003
6126
|
}
|
|
6004
6127
|
|
|
6005
6128
|
// src/repo.ts
|
|
6006
|
-
import { stat as stat13 } from "fs/promises";
|
|
6129
|
+
import { realpath as realpath3, stat as stat13 } from "fs/promises";
|
|
6007
6130
|
import { dirname as dirname6, isAbsolute as isAbsolute4, join as join17, normalize, parse as parsePath } from "path";
|
|
6008
6131
|
function normalizeRemote(url) {
|
|
6009
6132
|
const raw = url.trim();
|
|
@@ -6082,6 +6205,13 @@ function uniqNormalized(paths) {
|
|
|
6082
6205
|
function parseWorktreePorcelain(output) {
|
|
6083
6206
|
return output.split("\n").filter((line) => line.startsWith("worktree ")).map((line) => line.slice("worktree ".length).trim()).filter(Boolean);
|
|
6084
6207
|
}
|
|
6208
|
+
async function realpathOrNull(path) {
|
|
6209
|
+
try {
|
|
6210
|
+
return await realpath3(path);
|
|
6211
|
+
} catch {
|
|
6212
|
+
return null;
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6085
6215
|
async function equivalentGitCwds(cwd) {
|
|
6086
6216
|
if (!cwd) return [];
|
|
6087
6217
|
const selected = normalize(cwd);
|
|
@@ -6089,7 +6219,20 @@ async function equivalentGitCwds(cwd) {
|
|
|
6089
6219
|
const top = await gitOutput(selected, ["rev-parse", "--show-toplevel"]);
|
|
6090
6220
|
if (!top) return [selected];
|
|
6091
6221
|
const worktrees = await gitOutput(top, ["worktree", "list", "--porcelain"]);
|
|
6092
|
-
|
|
6222
|
+
const candidates = [selected, top, ...worktrees ? parseWorktreePorcelain(worktrees) : []];
|
|
6223
|
+
const expanded = [];
|
|
6224
|
+
for (const candidate of candidates) {
|
|
6225
|
+
expanded.push(candidate);
|
|
6226
|
+
const resolved = await realpathOrNull(candidate);
|
|
6227
|
+
if (resolved) expanded.push(resolved);
|
|
6228
|
+
}
|
|
6229
|
+
return uniqNormalized(expanded);
|
|
6230
|
+
}
|
|
6231
|
+
async function cwdMatchesSelectionResolved(cwd, selections) {
|
|
6232
|
+
if (cwdMatchesSelection(cwd, selections)) return true;
|
|
6233
|
+
if (!cwd || !isAbsolute4(cwd)) return false;
|
|
6234
|
+
const resolved = await realpathOrNull(normalize(cwd));
|
|
6235
|
+
return resolved !== null && resolved !== normalize(cwd) && cwdMatchesSelection(resolved, selections);
|
|
6093
6236
|
}
|
|
6094
6237
|
function cwdMatchesSelection(cwd, selections) {
|
|
6095
6238
|
if (!cwd) return false;
|
|
@@ -6236,7 +6379,7 @@ function extractAbsolutePaths(spans) {
|
|
|
6236
6379
|
}
|
|
6237
6380
|
async function resolveRepoAttrs(cwd) {
|
|
6238
6381
|
if (!cwd) return {};
|
|
6239
|
-
const attrs = { [
|
|
6382
|
+
const attrs = { [ATTR.CWD]: cwd };
|
|
6240
6383
|
const basename16 = (() => {
|
|
6241
6384
|
const trimmed = cwd.replace(/\/+$/g, "");
|
|
6242
6385
|
const idx = trimmed.lastIndexOf("/");
|
|
@@ -6249,17 +6392,17 @@ async function resolveRepoAttrs(cwd) {
|
|
|
6249
6392
|
const { remote, branch, commit } = await readGit(gitRoot);
|
|
6250
6393
|
const normalized = remote ? normalizeRemote(remote) : null;
|
|
6251
6394
|
if (normalized) {
|
|
6252
|
-
attrs[
|
|
6253
|
-
attrs[
|
|
6254
|
-
if (branch) attrs[
|
|
6255
|
-
if (commit) attrs[
|
|
6395
|
+
attrs[ATTR.SUBJECT_KEY] = normalized;
|
|
6396
|
+
attrs[ATTR.GIT_REPOSITORY] = normalized;
|
|
6397
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
6398
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
6256
6399
|
return attrs;
|
|
6257
6400
|
}
|
|
6258
|
-
if (branch) attrs[
|
|
6259
|
-
if (commit) attrs[
|
|
6401
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
6402
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
6260
6403
|
} catch {
|
|
6261
6404
|
}
|
|
6262
|
-
attrs[
|
|
6405
|
+
attrs[ATTR.SUBJECT_KEY] = basename16;
|
|
6263
6406
|
return attrs;
|
|
6264
6407
|
}
|
|
6265
6408
|
async function repoCandidateForPath(path) {
|
|
@@ -6290,12 +6433,12 @@ async function resolveSessionRepoAttrs(cwd, spans) {
|
|
|
6290
6433
|
resolved.push({ ...candidate, attrs });
|
|
6291
6434
|
}
|
|
6292
6435
|
const byScore = (a, b) => b.score - a.score || a.order - b.order;
|
|
6293
|
-
const withGit = resolved.filter((candidate) => candidate.attrs[
|
|
6294
|
-
const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[
|
|
6436
|
+
const withGit = resolved.filter((candidate) => candidate.attrs[ATTR.GIT_REPOSITORY]).sort(byScore);
|
|
6437
|
+
const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
|
|
6295
6438
|
if (!selected) return { attrs: {}, cwd: null, source: "none" };
|
|
6296
6439
|
return {
|
|
6297
|
-
attrs: { ...selected.attrs, [
|
|
6298
|
-
cwd: selected.attrs[
|
|
6440
|
+
attrs: { ...selected.attrs, [ATTR.REPO_RESOLUTION_SOURCE]: selected.source },
|
|
6441
|
+
cwd: selected.attrs[ATTR.CWD] ?? selected.cwd,
|
|
6299
6442
|
source: selected.source
|
|
6300
6443
|
};
|
|
6301
6444
|
}
|
|
@@ -6323,9 +6466,9 @@ async function resolutionForWorkdirEvidence(evidence3, cache) {
|
|
|
6323
6466
|
return null;
|
|
6324
6467
|
}
|
|
6325
6468
|
const attrs = await resolveRepoAttrs(cwd);
|
|
6326
|
-
const resolved = attrs[
|
|
6327
|
-
attrs: { ...attrs, [
|
|
6328
|
-
cwd: attrs[
|
|
6469
|
+
const resolved = attrs[ATTR.SUBJECT_KEY] ? {
|
|
6470
|
+
attrs: { ...attrs, [ATTR.REPO_RESOLUTION_SOURCE]: evidence3.source },
|
|
6471
|
+
cwd: attrs[ATTR.CWD] ?? cwd,
|
|
6329
6472
|
source: evidence3.source
|
|
6330
6473
|
} : null;
|
|
6331
6474
|
cache.set(evidence3.path, resolved);
|
|
@@ -6348,7 +6491,7 @@ async function resolveSpanWorkdirRepoAttrs(span2, cache) {
|
|
|
6348
6491
|
}
|
|
6349
6492
|
const byScore = (a, b) => b.score - a.score || a.order - b.order;
|
|
6350
6493
|
const resolved = [...candidates.values()];
|
|
6351
|
-
const selected = resolved.filter((candidate) => candidate.repo.attrs[
|
|
6494
|
+
const selected = resolved.filter((candidate) => candidate.repo.attrs[ATTR.GIT_REPOSITORY]).sort(byScore)[0] ?? resolved.filter((candidate) => candidate.repo.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
|
|
6352
6495
|
if (!selected) return null;
|
|
6353
6496
|
return selected.repo;
|
|
6354
6497
|
}
|
|
@@ -6364,7 +6507,7 @@ async function stampSpanWorkdirRepoAttrs(spans) {
|
|
|
6364
6507
|
async function awaitWithAbort(operation, signal) {
|
|
6365
6508
|
if (!signal) return operation;
|
|
6366
6509
|
signal.throwIfAborted();
|
|
6367
|
-
return new Promise((
|
|
6510
|
+
return new Promise((resolve10, reject) => {
|
|
6368
6511
|
const stop = () => {
|
|
6369
6512
|
signal.removeEventListener("abort", stop);
|
|
6370
6513
|
reject(signal.reason);
|
|
@@ -6373,7 +6516,7 @@ async function awaitWithAbort(operation, signal) {
|
|
|
6373
6516
|
operation.then(
|
|
6374
6517
|
(value) => {
|
|
6375
6518
|
signal.removeEventListener("abort", stop);
|
|
6376
|
-
|
|
6519
|
+
resolve10(value);
|
|
6377
6520
|
},
|
|
6378
6521
|
(error) => {
|
|
6379
6522
|
signal.removeEventListener("abort", stop);
|
|
@@ -6392,6 +6535,8 @@ async function parseSession(adapter, ref, options = {}) {
|
|
|
6392
6535
|
if (spans.length === 0) throw new EmptySessionError(ref.path);
|
|
6393
6536
|
stampSessionIntegrity(ref, spans);
|
|
6394
6537
|
stampSessionIdentity(spans, ref.sessionId);
|
|
6538
|
+
stampEnvironmentAttrs(spans, ref.environment);
|
|
6539
|
+
if (ref.cwd === null && ref.environment?.cwd) ref.cwd = ref.environment.cwd;
|
|
6395
6540
|
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
6396
6541
|
if (repo.cwd) ref.cwd = repo.cwd;
|
|
6397
6542
|
stampRepoAttrs(spans, repo.attrs);
|
|
@@ -6414,8 +6559,15 @@ async function locateSessions(adapter, opts = {}) {
|
|
|
6414
6559
|
const cwdSelections = await equivalentGitCwds(opts.cwd);
|
|
6415
6560
|
const refs = await adapter.locate({ sinceMs: opts.sinceMs });
|
|
6416
6561
|
const byKey = /* @__PURE__ */ new Map();
|
|
6562
|
+
const resolvedMatch = /* @__PURE__ */ new Map();
|
|
6417
6563
|
for (const ref of refs) {
|
|
6418
|
-
|
|
6564
|
+
let matched = cwdMatchesSelection(ref.cwd, cwdSelections);
|
|
6565
|
+
if (!matched && ref.cwd !== null) {
|
|
6566
|
+
const cached = resolvedMatch.get(ref.cwd);
|
|
6567
|
+
matched = cached ?? await cwdMatchesSelectionResolved(ref.cwd, cwdSelections);
|
|
6568
|
+
if (cached === void 0) resolvedMatch.set(ref.cwd, matched);
|
|
6569
|
+
}
|
|
6570
|
+
if (!matched && !(ref.cwd === null && ref.integrity)) continue;
|
|
6419
6571
|
byKey.set(refKey(ref), ref);
|
|
6420
6572
|
}
|
|
6421
6573
|
return [...byKey.values()].sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
@@ -6550,25 +6702,26 @@ import { mkdtemp as mkdtemp2, writeFile as writeFile3 } from "fs/promises";
|
|
|
6550
6702
|
import { tmpdir as tmpdir2 } from "os";
|
|
6551
6703
|
import { join as join18 } from "path";
|
|
6552
6704
|
import {
|
|
6553
|
-
OPENINFERENCE_SPAN_KIND as
|
|
6554
|
-
TOOL_NAME as
|
|
6705
|
+
OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND3,
|
|
6706
|
+
TOOL_NAME as TOOL_NAME4
|
|
6555
6707
|
} from "@tangle-network/agent-eval/trace-attributes";
|
|
6708
|
+
var POLICY_EVIDENCE_NOTE = "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell.";
|
|
6556
6709
|
function stringAttr(span2, key) {
|
|
6557
6710
|
const value = span2.attributes[key];
|
|
6558
6711
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
6559
6712
|
}
|
|
6560
6713
|
function spanKind(span2) {
|
|
6561
|
-
return stringAttr(span2,
|
|
6714
|
+
return stringAttr(span2, OPENINFERENCE_SPAN_KIND3);
|
|
6562
6715
|
}
|
|
6563
6716
|
function repoFromSpans(spans) {
|
|
6564
6717
|
const attrs = {};
|
|
6565
6718
|
for (const span2 of spans) {
|
|
6566
|
-
attrs.subjectKey ??= stringAttr(span2,
|
|
6567
|
-
attrs.repository ??= stringAttr(span2,
|
|
6568
|
-
attrs.branch ??= stringAttr(span2,
|
|
6569
|
-
attrs.commit ??= stringAttr(span2,
|
|
6570
|
-
attrs.cwd ??= stringAttr(span2,
|
|
6571
|
-
attrs.resolutionSource ??= stringAttr(span2,
|
|
6719
|
+
attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
|
|
6720
|
+
attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
|
|
6721
|
+
attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
|
|
6722
|
+
attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
|
|
6723
|
+
attrs.cwd ??= stringAttr(span2, ATTR.CWD);
|
|
6724
|
+
attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
|
|
6572
6725
|
if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
|
|
6573
6726
|
}
|
|
6574
6727
|
return attrs;
|
|
@@ -6584,7 +6737,7 @@ function summarizeTools(spans) {
|
|
|
6584
6737
|
const byTool = /* @__PURE__ */ new Map();
|
|
6585
6738
|
for (const span2 of spans) {
|
|
6586
6739
|
if (spanKind(span2) !== "TOOL") continue;
|
|
6587
|
-
const name = stringAttr(span2,
|
|
6740
|
+
const name = stringAttr(span2, TOOL_NAME4) ?? span2.name.replace(/^tool\./, "");
|
|
6588
6741
|
const current = byTool.get(name) ?? { calls: 0, errors: 0 };
|
|
6589
6742
|
current.calls += 1;
|
|
6590
6743
|
if (span2.status.code === "ERROR") current.errors += 1;
|
|
@@ -6657,7 +6810,7 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
6657
6810
|
...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
|
|
6658
6811
|
...opts.sourceSha256 ? { sourceSha256: opts.sourceSha256 } : {},
|
|
6659
6812
|
notCampaignCell: true,
|
|
6660
|
-
note:
|
|
6813
|
+
note: POLICY_EVIDENCE_NOTE
|
|
6661
6814
|
}
|
|
6662
6815
|
};
|
|
6663
6816
|
}
|
|
@@ -7181,11 +7334,11 @@ function parseCorpusFlag(value) {
|
|
|
7181
7334
|
if (eq <= 0) throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
|
|
7182
7335
|
const name = value.slice(0, eq);
|
|
7183
7336
|
const rest = value.slice(eq + 1);
|
|
7184
|
-
const
|
|
7185
|
-
if (
|
|
7337
|
+
const sep3 = rest.indexOf("::");
|
|
7338
|
+
if (sep3 <= 0 || sep3 === rest.length - 2) {
|
|
7186
7339
|
throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
|
|
7187
7340
|
}
|
|
7188
|
-
return { name, labelsPath: rest.slice(0,
|
|
7341
|
+
return { name, labelsPath: rest.slice(0, sep3), preparedDir: rest.slice(sep3 + 2) };
|
|
7189
7342
|
}
|
|
7190
7343
|
var SUBMIT_ACTION_SIGNATURE = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT";
|
|
7191
7344
|
function isSubmitAction(action) {
|
|
@@ -7211,7 +7364,7 @@ function goldIncorrectSteps(entry) {
|
|
|
7211
7364
|
function findRawTrajFiles(sweRawDir) {
|
|
7212
7365
|
if (!existsSync2(sweRawDir)) return [];
|
|
7213
7366
|
const entries = readdirSync2(sweRawDir, { recursive: true, encoding: "utf8" });
|
|
7214
|
-
return entries.filter((
|
|
7367
|
+
return entries.filter((relative6) => relative6.endsWith(".traj.json")).map((relative6) => join20(sweRawDir, relative6)).sort();
|
|
7215
7368
|
}
|
|
7216
7369
|
function cwdFromPwdObservation(steps) {
|
|
7217
7370
|
for (const step of steps) {
|
|
@@ -8376,9 +8529,9 @@ function renderBatchReport(report) {
|
|
|
8376
8529
|
lines.push("");
|
|
8377
8530
|
lines.push("| corpus | trajectory | image | error |");
|
|
8378
8531
|
lines.push("| --- | --- | --- | --- |");
|
|
8379
|
-
for (const
|
|
8532
|
+
for (const failure2 of report.pullFailures) {
|
|
8380
8533
|
lines.push(
|
|
8381
|
-
`| ${
|
|
8534
|
+
`| ${failure2.corpus} | ${failure2.trajId} | \`${failure2.image}\` | ${failure2.error.replaceAll("|", "\\|")} |`
|
|
8382
8535
|
);
|
|
8383
8536
|
}
|
|
8384
8537
|
}
|
|
@@ -9444,13 +9597,13 @@ function terminateWindowsProcessTree(child) {
|
|
|
9444
9597
|
forceKill(child);
|
|
9445
9598
|
return Promise.resolve();
|
|
9446
9599
|
}
|
|
9447
|
-
return new Promise((
|
|
9600
|
+
return new Promise((resolve10) => {
|
|
9448
9601
|
let finished = false;
|
|
9449
9602
|
const finish = () => {
|
|
9450
9603
|
if (finished) return;
|
|
9451
9604
|
finished = true;
|
|
9452
9605
|
forceKill(child);
|
|
9453
|
-
|
|
9606
|
+
resolve10();
|
|
9454
9607
|
};
|
|
9455
9608
|
let killer;
|
|
9456
9609
|
try {
|
|
@@ -9477,7 +9630,7 @@ function signalProcess(pid, signal) {
|
|
|
9477
9630
|
}
|
|
9478
9631
|
}
|
|
9479
9632
|
function readProcessTableWithPs() {
|
|
9480
|
-
return new Promise((
|
|
9633
|
+
return new Promise((resolve10, reject) => {
|
|
9481
9634
|
let child;
|
|
9482
9635
|
try {
|
|
9483
9636
|
child = spawn("ps", ["-A", "-o", "pid=", "-o", "ppid="], {
|
|
@@ -9531,7 +9684,7 @@ function readProcessTableWithPs() {
|
|
|
9531
9684
|
reject(new Error("ps: returned no process records"));
|
|
9532
9685
|
return;
|
|
9533
9686
|
}
|
|
9534
|
-
|
|
9687
|
+
resolve10(rows);
|
|
9535
9688
|
});
|
|
9536
9689
|
});
|
|
9537
9690
|
});
|
|
@@ -9541,10 +9694,10 @@ async function readLinuxProcProcessTable() {
|
|
|
9541
9694
|
const rows = await Promise.all(
|
|
9542
9695
|
entries.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)).map(async (entry) => {
|
|
9543
9696
|
try {
|
|
9544
|
-
const
|
|
9545
|
-
const commandEnd =
|
|
9697
|
+
const stat18 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
|
|
9698
|
+
const commandEnd = stat18.lastIndexOf(")");
|
|
9546
9699
|
if (commandEnd < 0) return void 0;
|
|
9547
|
-
const fields =
|
|
9700
|
+
const fields = stat18.slice(commandEnd + 1).trim().split(/\s+/);
|
|
9548
9701
|
const pid = Number(entry.name);
|
|
9549
9702
|
const parentPid = Number(fields[1]);
|
|
9550
9703
|
if (!Number.isSafeInteger(pid) || pid < 1 || !Number.isSafeInteger(parentPid) || parentPid < 0) {
|
|
@@ -9606,7 +9759,7 @@ async function terminatePosixProcessTree(child) {
|
|
|
9606
9759
|
}
|
|
9607
9760
|
const rootPid = child.pid;
|
|
9608
9761
|
const descendants = /* @__PURE__ */ new Map();
|
|
9609
|
-
let
|
|
9762
|
+
let failure2;
|
|
9610
9763
|
try {
|
|
9611
9764
|
signalProcess(-rootPid, "SIGSTOP");
|
|
9612
9765
|
signalProcess(rootPid, "SIGSTOP");
|
|
@@ -9623,7 +9776,7 @@ async function terminatePosixProcessTree(child) {
|
|
|
9623
9776
|
previous = signature;
|
|
9624
9777
|
}
|
|
9625
9778
|
} catch (error) {
|
|
9626
|
-
|
|
9779
|
+
failure2 = error;
|
|
9627
9780
|
} finally {
|
|
9628
9781
|
for (const processEntry of [...descendants.values()].sort(
|
|
9629
9782
|
(left, right) => right.depth - left.depth
|
|
@@ -9631,18 +9784,18 @@ async function terminatePosixProcessTree(child) {
|
|
|
9631
9784
|
try {
|
|
9632
9785
|
signalProcess(processEntry.pid, "SIGKILL");
|
|
9633
9786
|
} catch (error) {
|
|
9634
|
-
|
|
9787
|
+
failure2 ??= error;
|
|
9635
9788
|
}
|
|
9636
9789
|
}
|
|
9637
9790
|
try {
|
|
9638
9791
|
signalProcess(-rootPid, "SIGKILL");
|
|
9639
9792
|
} catch (error) {
|
|
9640
|
-
|
|
9793
|
+
failure2 ??= error;
|
|
9641
9794
|
}
|
|
9642
9795
|
forceKill(child);
|
|
9643
9796
|
}
|
|
9644
|
-
if (
|
|
9645
|
-
throw
|
|
9797
|
+
if (failure2) {
|
|
9798
|
+
throw failure2 instanceof Error ? failure2 : new Error(String(failure2));
|
|
9646
9799
|
}
|
|
9647
9800
|
}
|
|
9648
9801
|
function terminateProcessTree(child) {
|
|
@@ -9666,7 +9819,7 @@ function runCommand(command, args = [], opts = {}) {
|
|
|
9666
9819
|
throw new RangeError("maxBuffer must be a positive safe integer");
|
|
9667
9820
|
}
|
|
9668
9821
|
if (opts.signal?.aborted) return Promise.reject(abortError(opts.signal));
|
|
9669
|
-
return new Promise((
|
|
9822
|
+
return new Promise((resolve10, reject) => {
|
|
9670
9823
|
let child;
|
|
9671
9824
|
try {
|
|
9672
9825
|
child = spawn(command, [...args], { detached: process.platform !== "win32" });
|
|
@@ -9698,7 +9851,7 @@ function runCommand(command, args = [], opts = {}) {
|
|
|
9698
9851
|
} else if (spawnError) {
|
|
9699
9852
|
reject(spawnError);
|
|
9700
9853
|
} else {
|
|
9701
|
-
|
|
9854
|
+
resolve10({ stdout: out, stderr: err, code: closeCode });
|
|
9702
9855
|
}
|
|
9703
9856
|
};
|
|
9704
9857
|
function requestTermination(error) {
|
|
@@ -10613,44 +10766,463 @@ function assertPositiveInteger(name, value) {
|
|
|
10613
10766
|
}
|
|
10614
10767
|
}
|
|
10615
10768
|
|
|
10616
|
-
// src/
|
|
10617
|
-
import {
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10632
|
-
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
// start their line, prose colons never do; JSON keys are safe anywhere because both
|
|
10637
|
-
// sides are quoted. Identifiers must END in a secret word, same as the shell rule.
|
|
10638
|
-
{
|
|
10639
|
-
id: "config-secret",
|
|
10640
|
-
pattern: /(?:^[ \t]*["']?(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])["']?[ \t]*[:=][ \t]*|["'](?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)["']\s*:\s*)["']?[A-Za-z0-9._\-]{12,}["']?/gim
|
|
10641
|
-
},
|
|
10642
|
-
// Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
|
|
10643
|
-
{ id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
|
|
10644
|
-
{
|
|
10645
|
-
id: "url-secret-param",
|
|
10646
|
-
pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
|
|
10769
|
+
// src/analyst-engine-prime.ts
|
|
10770
|
+
import { createHash as createHash10 } from "crypto";
|
|
10771
|
+
import { request as httpRequest } from "http";
|
|
10772
|
+
import { request as httpsRequest } from "https";
|
|
10773
|
+
var DEFAULT_BRIDGE_URL = "http://localhost:4181";
|
|
10774
|
+
var DEFAULT_MODEL = "prime/zai/glm-5.2";
|
|
10775
|
+
var DEFAULT_TIMEOUT_MS = 12e5;
|
|
10776
|
+
var DEFAULT_MAX_INLINE_CHARS = 36e4;
|
|
10777
|
+
var DEFAULT_PER_ATTRIBUTE_CHAR_CAP = 1200;
|
|
10778
|
+
var DEFAULT_QUESTION = "Diagnose this trajectory: identify concrete agent failures, wasted work, and unsupported claims.";
|
|
10779
|
+
var MAX_FINDINGS = 10;
|
|
10780
|
+
var MAX_SPAN_IDS_PER_FINDING = 8;
|
|
10781
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
|
|
10782
|
+
var ANALYST_ID = "prime";
|
|
10783
|
+
function positiveIntegerEnv(name) {
|
|
10784
|
+
const raw = process.env[name];
|
|
10785
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
10786
|
+
const value = Number(raw);
|
|
10787
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
10788
|
+
throw new TypeError(`${name} must be a positive integer, got '${raw}'`);
|
|
10647
10789
|
}
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
function
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10790
|
+
return value;
|
|
10791
|
+
}
|
|
10792
|
+
function finiteOrNull(value) {
|
|
10793
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
10794
|
+
}
|
|
10795
|
+
function normalizeUsage(raw) {
|
|
10796
|
+
if (!raw || typeof raw !== "object") return { calls: null, tokens: null, estimated: false };
|
|
10797
|
+
const usage = raw;
|
|
10798
|
+
const input = finiteOrNull(usage.prompt_tokens);
|
|
10799
|
+
const output = finiteOrNull(usage.completion_tokens);
|
|
10800
|
+
return {
|
|
10801
|
+
calls: finiteOrNull(usage.model_requests),
|
|
10802
|
+
tokens: input === null && output === null ? null : { input, output },
|
|
10803
|
+
estimated: usage.estimated === true
|
|
10804
|
+
};
|
|
10805
|
+
}
|
|
10806
|
+
function mergeUsage(a, b) {
|
|
10807
|
+
const calls = a.calls !== null && b.calls !== null ? a.calls + b.calls : null;
|
|
10808
|
+
const tokens3 = a.tokens !== null && b.tokens !== null && a.tokens.input !== null && b.tokens.input !== null && a.tokens.output !== null && b.tokens.output !== null ? { input: a.tokens.input + b.tokens.input, output: a.tokens.output + b.tokens.output } : null;
|
|
10809
|
+
return { calls, tokens: tokens3, estimated: a.estimated || b.estimated };
|
|
10810
|
+
}
|
|
10811
|
+
function renderUsage(usage) {
|
|
10812
|
+
const calls = usage.calls === null ? "uncaptured" : String(usage.calls);
|
|
10813
|
+
const input = usage.tokens?.input ?? "uncaptured";
|
|
10814
|
+
const output = usage.tokens?.output ?? "uncaptured";
|
|
10815
|
+
const estimated = usage.estimated ? " (bridge-estimated)" : "";
|
|
10816
|
+
return `usage: calls=${calls} input_tokens=${input} output_tokens=${output}${estimated}; cost=uncaptured (no pricing table)`;
|
|
10817
|
+
}
|
|
10818
|
+
function capAttributeValue(value, cap) {
|
|
10819
|
+
const rendered = JSON.stringify(value);
|
|
10820
|
+
if (rendered === void 0 || rendered.length <= cap) return value;
|
|
10821
|
+
if (typeof value === "string") return `${value.slice(0, cap)}\u2026[truncated ${value.length - cap} chars]`;
|
|
10822
|
+
return `[omitted non-string attribute: ${rendered.length} JSON chars]`;
|
|
10823
|
+
}
|
|
10824
|
+
function capRowAttributes(row, cap) {
|
|
10825
|
+
const attributes2 = row.attributes;
|
|
10826
|
+
if (!attributes2 || typeof attributes2 !== "object" || Array.isArray(attributes2)) return row;
|
|
10827
|
+
const capped = {};
|
|
10828
|
+
for (const [key, value] of Object.entries(attributes2)) {
|
|
10829
|
+
capped[key] = capAttributeValue(value, cap);
|
|
10830
|
+
}
|
|
10831
|
+
return { ...row, attributes: capped };
|
|
10832
|
+
}
|
|
10833
|
+
async function projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, signal) {
|
|
10834
|
+
const rows = [];
|
|
10835
|
+
const spanTraces = /* @__PURE__ */ new Map();
|
|
10836
|
+
let index = 0;
|
|
10837
|
+
for await (const value of readJsonl(otlpPath, { signal })) {
|
|
10838
|
+
const label = `${otlpPath}:${index + 1}`;
|
|
10839
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10840
|
+
throw new TypeError(`${label} must contain an object`);
|
|
10841
|
+
}
|
|
10842
|
+
const row = value;
|
|
10843
|
+
if (typeof row.trace_id !== "string" || row.trace_id.length === 0) {
|
|
10844
|
+
throw new TypeError(`${label}.trace_id must be a non-empty string`);
|
|
10845
|
+
}
|
|
10846
|
+
if (typeof row.span_id !== "string" || row.span_id.length === 0) {
|
|
10847
|
+
throw new TypeError(`${label}.span_id must be a non-empty string`);
|
|
10848
|
+
}
|
|
10849
|
+
rows.push(row);
|
|
10850
|
+
const traces = spanTraces.get(row.span_id) ?? /* @__PURE__ */ new Set();
|
|
10851
|
+
traces.add(row.trace_id);
|
|
10852
|
+
spanTraces.set(row.span_id, traces);
|
|
10853
|
+
index += 1;
|
|
10854
|
+
}
|
|
10855
|
+
if (rows.length === 0) throw new Error(`${otlpPath} contains no spans`);
|
|
10856
|
+
const traceCount = new Set(rows.map((row) => row.trace_id)).size;
|
|
10857
|
+
let projected = rows;
|
|
10858
|
+
let rendered = JSON.stringify(projected);
|
|
10859
|
+
const delivery = {
|
|
10860
|
+
mode: "inline-json",
|
|
10861
|
+
perAttributeCharCap: null,
|
|
10862
|
+
renderedChars: rendered.length
|
|
10863
|
+
};
|
|
10864
|
+
if (rendered.length > maxInlineChars) {
|
|
10865
|
+
projected = rows.map((row) => capRowAttributes(row, perAttributeCharCap));
|
|
10866
|
+
rendered = JSON.stringify(projected);
|
|
10867
|
+
delivery.perAttributeCharCap = perAttributeCharCap;
|
|
10868
|
+
delivery.renderedChars = rendered.length;
|
|
10869
|
+
}
|
|
10870
|
+
if (rendered.length > maxInlineChars) {
|
|
10871
|
+
throw new Error(
|
|
10872
|
+
`trajectory renders to ${rendered.length} chars even at per-attribute cap ${perAttributeCharCap} (budget ${maxInlineChars}); inline delivery impossible`
|
|
10873
|
+
);
|
|
10874
|
+
}
|
|
10875
|
+
return { rows: projected, rendered, delivery, traceCount, spanTraces };
|
|
10876
|
+
}
|
|
10877
|
+
var OUTPUT_CONTRACT_LINES = [
|
|
10878
|
+
"OUTPUT CONTRACT (you have no trace tools and no REPL):",
|
|
10879
|
+
"You are a one-shot analyst. Every fact you need is in the TRAJECTORY JSON below.",
|
|
10880
|
+
"Do not run shell commands, do not read or write files, do not use any tools.",
|
|
10881
|
+
"Reply with EXACTLY one fenced ```json code block and no other fenced block. The JSON object has exactly two fields:",
|
|
10882
|
+
' "answer": string \u2014 ONE short sentence (max 300 chars) summarizing your verdict.',
|
|
10883
|
+
' "findings": array (possibly empty) of finding rows, each exactly:',
|
|
10884
|
+
' {"span_ids": [string, ...],',
|
|
10885
|
+
' "severity": "critical"|"high"|"medium"|"low"|"info",',
|
|
10886
|
+
' "area": string (short kebab-case topic, max 40 chars),',
|
|
10887
|
+
' "claim": string (ONE short sentence, max 200 chars),',
|
|
10888
|
+
' "action": string (optional, ONE short imperative sentence, max 200 chars),',
|
|
10889
|
+
' "confidence": number 0..1}',
|
|
10890
|
+
"Do NOT include a rationale field. Keep every string SHORT \u2014 long strings get corrupted in transport and void your work.",
|
|
10891
|
+
`Report at most ${MAX_FINDINGS} findings; a finding cites 1..${MAX_SPAN_IDS_PER_FINDING} span_ids, each copied VERBATIM from a span in the trajectory below.`,
|
|
10892
|
+
'"findings" is [] only for a clean trajectory.'
|
|
10893
|
+
];
|
|
10894
|
+
function buildPrompt(question, projection) {
|
|
10895
|
+
return [
|
|
10896
|
+
`QUESTION: ${question}`,
|
|
10897
|
+
"",
|
|
10898
|
+
...OUTPUT_CONTRACT_LINES,
|
|
10899
|
+
"",
|
|
10900
|
+
`TRAJECTORY (${projection.traceCount} trace(s); ${projection.rows.length} spans; full OpenInference span projection as JSON):`,
|
|
10901
|
+
projection.rendered
|
|
10902
|
+
].join("\n");
|
|
10903
|
+
}
|
|
10904
|
+
function buildRepairPrompt(parseDefect, previousReply) {
|
|
10905
|
+
return [
|
|
10906
|
+
"Your previous reply to a trace-analysis task was structurally malformed and could not be parsed",
|
|
10907
|
+
`(${parseDefect}). Below is your previous reply verbatim. Re-emit ONLY the corrected JSON \u2014 one`,
|
|
10908
|
+
"fenced ```json block, no other text, no tools. The JSON object has exactly two fields:",
|
|
10909
|
+
' "answer": string (ONE short sentence, max 300 chars)',
|
|
10910
|
+
' "findings": array (possibly empty) of {"span_ids": [string, ...],',
|
|
10911
|
+
' "severity": "critical"|"high"|"medium"|"low"|"info", "area": string (max 40 chars),',
|
|
10912
|
+
' "claim": string (max 200 chars), "action": string (optional, max 200 chars), "confidence": number 0..1}',
|
|
10913
|
+
"No rationale field. Keep every string SHORT. Preserve the span ids and verdicts of your previous reply exactly; shorten prose freely.",
|
|
10914
|
+
"",
|
|
10915
|
+
"PREVIOUS REPLY:",
|
|
10916
|
+
previousReply
|
|
10917
|
+
].join("\n");
|
|
10918
|
+
}
|
|
10919
|
+
function tryParseObject(text2) {
|
|
10920
|
+
try {
|
|
10921
|
+
const value = JSON.parse(text2.trim());
|
|
10922
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10923
|
+
} catch {
|
|
10924
|
+
return null;
|
|
10925
|
+
}
|
|
10926
|
+
}
|
|
10927
|
+
function extractJsonObject(text2) {
|
|
10928
|
+
const direct = tryParseObject(text2);
|
|
10929
|
+
if (direct) return direct;
|
|
10930
|
+
const fenced = [...text2.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
|
|
10931
|
+
for (let i = fenced.length - 1; i >= 0; i -= 1) {
|
|
10932
|
+
const candidate = tryParseObject(fenced[i][1]);
|
|
10933
|
+
if (candidate) return candidate;
|
|
10934
|
+
}
|
|
10935
|
+
const start = text2.indexOf("{");
|
|
10936
|
+
const end = text2.lastIndexOf("}");
|
|
10937
|
+
if (start >= 0 && end > start) {
|
|
10938
|
+
const candidate = tryParseObject(text2.slice(start, end + 1));
|
|
10939
|
+
if (candidate) return candidate;
|
|
10940
|
+
}
|
|
10941
|
+
return null;
|
|
10942
|
+
}
|
|
10943
|
+
function parseDefectOf(parsed) {
|
|
10944
|
+
if (parsed === null) return "no parseable JSON object";
|
|
10945
|
+
if (!Array.isArray(parsed.findings)) return 'JSON has no "findings" array';
|
|
10946
|
+
return null;
|
|
10947
|
+
}
|
|
10948
|
+
function findingRowDefect(row, spanTraces) {
|
|
10949
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return "row is not an object";
|
|
10950
|
+
const record2 = row;
|
|
10951
|
+
if (!Array.isArray(record2.span_ids) || record2.span_ids.length === 0) {
|
|
10952
|
+
return "span_ids must be a non-empty array";
|
|
10953
|
+
}
|
|
10954
|
+
const spanIds = [];
|
|
10955
|
+
for (const id of record2.span_ids) {
|
|
10956
|
+
if (typeof id !== "string" || id.length === 0) return "span_ids must contain non-empty strings";
|
|
10957
|
+
const traces = spanTraces.get(id);
|
|
10958
|
+
if (!traces) return `span_id '${id}' is not in the trajectory`;
|
|
10959
|
+
if (traces.size > 1) return `span_id '${id}' is ambiguous across ${traces.size} traces`;
|
|
10960
|
+
if (!spanIds.includes(id)) spanIds.push(id);
|
|
10961
|
+
}
|
|
10962
|
+
if (spanIds.length > MAX_SPAN_IDS_PER_FINDING) {
|
|
10963
|
+
return `span_ids cites ${spanIds.length} distinct spans (cap ${MAX_SPAN_IDS_PER_FINDING})`;
|
|
10964
|
+
}
|
|
10965
|
+
if (typeof record2.severity !== "string" || !SEVERITIES.has(record2.severity)) {
|
|
10966
|
+
return "severity outside the analyst severity enum";
|
|
10967
|
+
}
|
|
10968
|
+
if (typeof record2.area !== "string" || record2.area.trim().length === 0 || record2.area.length > 200) {
|
|
10969
|
+
return "area must be a 1-200 char string";
|
|
10970
|
+
}
|
|
10971
|
+
if (typeof record2.claim !== "string" || record2.claim.trim().length === 0 || record2.claim.length > 2e3) {
|
|
10972
|
+
return "claim must be a 1-2000 char string";
|
|
10973
|
+
}
|
|
10974
|
+
if (record2.action !== void 0 && (typeof record2.action !== "string" || record2.action.trim().length === 0 || record2.action.length > 2e3)) {
|
|
10975
|
+
return "action must be a 1-2000 char string when present";
|
|
10976
|
+
}
|
|
10977
|
+
if (typeof record2.confidence !== "number" || !Number.isFinite(record2.confidence) || record2.confidence < 0 || record2.confidence > 1) {
|
|
10978
|
+
return "confidence must be 0..1";
|
|
10979
|
+
}
|
|
10980
|
+
return {
|
|
10981
|
+
spanIds,
|
|
10982
|
+
severity: record2.severity,
|
|
10983
|
+
area: record2.area.trim(),
|
|
10984
|
+
claim: record2.claim.trim(),
|
|
10985
|
+
...typeof record2.action === "string" ? { action: record2.action.trim() } : {},
|
|
10986
|
+
confidence: record2.confidence
|
|
10987
|
+
};
|
|
10988
|
+
}
|
|
10989
|
+
function toAnalystFinding(row, index, spanTraces, model, producedAt) {
|
|
10990
|
+
const uris = row.spanIds.map((spanId) => {
|
|
10991
|
+
const traces = spanTraces.get(spanId);
|
|
10992
|
+
return spanEvidenceUri([...traces][0], spanId);
|
|
10993
|
+
});
|
|
10994
|
+
const findingId = `prime-${createHash10("sha256").update(`${index}
|
|
10995
|
+
${row.claim}
|
|
10996
|
+
${uris.join("\n")}`).digest("hex").slice(0, 12)}`;
|
|
10997
|
+
return {
|
|
10998
|
+
schema_version: "1.0.0",
|
|
10999
|
+
finding_id: findingId,
|
|
11000
|
+
analyst_id: ANALYST_ID,
|
|
11001
|
+
produced_at: producedAt,
|
|
11002
|
+
severity: row.severity,
|
|
11003
|
+
area: row.area,
|
|
11004
|
+
claim: row.claim,
|
|
11005
|
+
evidence_refs: uris.map((uri) => ({ kind: "span", uri })),
|
|
11006
|
+
confidence: row.confidence,
|
|
11007
|
+
...row.action ? { recommended_action: row.action } : {},
|
|
11008
|
+
metadata: { engine: ANALYST_ID, model }
|
|
11009
|
+
};
|
|
11010
|
+
}
|
|
11011
|
+
var httpJsonTransport = ({ url, body, signal }) => {
|
|
11012
|
+
const target = new URL(url);
|
|
11013
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
11014
|
+
throw new TypeError(`bridge URL must be http: or https:, got ${target.protocol}`);
|
|
11015
|
+
}
|
|
11016
|
+
const requestFn = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
11017
|
+
const payload = JSON.stringify(body);
|
|
11018
|
+
return new Promise((resolve10, reject) => {
|
|
11019
|
+
const req = requestFn(
|
|
11020
|
+
{
|
|
11021
|
+
hostname: target.hostname,
|
|
11022
|
+
port: target.port,
|
|
11023
|
+
path: `${target.pathname}${target.search}`,
|
|
11024
|
+
method: "POST",
|
|
11025
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
|
|
11026
|
+
signal
|
|
11027
|
+
},
|
|
11028
|
+
(res) => {
|
|
11029
|
+
const chunks = [];
|
|
11030
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
11031
|
+
res.on("end", () => resolve10({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
|
|
11032
|
+
res.on("error", reject);
|
|
11033
|
+
}
|
|
11034
|
+
);
|
|
11035
|
+
req.on("error", reject);
|
|
11036
|
+
req.end(payload);
|
|
11037
|
+
});
|
|
11038
|
+
};
|
|
11039
|
+
var PrimeBridgeError = class extends Error {
|
|
11040
|
+
};
|
|
11041
|
+
async function callBridge(transport, url, model, content, timeoutMs, callerSignal, turn) {
|
|
11042
|
+
const controller = new AbortController();
|
|
11043
|
+
const timer = setTimeout(
|
|
11044
|
+
() => controller.abort(new Error(`${turn}bridge call exceeded ${timeoutMs}ms`)),
|
|
11045
|
+
timeoutMs
|
|
11046
|
+
);
|
|
11047
|
+
const onCallerAbort = () => controller.abort(callerSignal.reason);
|
|
11048
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
11049
|
+
if (callerSignal?.aborted) onCallerAbort();
|
|
11050
|
+
let response;
|
|
11051
|
+
try {
|
|
11052
|
+
response = await transport({
|
|
11053
|
+
url,
|
|
11054
|
+
body: { model, messages: [{ role: "user", content }] },
|
|
11055
|
+
signal: controller.signal
|
|
11056
|
+
});
|
|
11057
|
+
} catch (error) {
|
|
11058
|
+
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
11059
|
+
throw new PrimeBridgeError(
|
|
11060
|
+
`${turn}bridge transport failure: ${reason instanceof Error ? reason.message : String(reason)}`
|
|
11061
|
+
);
|
|
11062
|
+
} finally {
|
|
11063
|
+
clearTimeout(timer);
|
|
11064
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
11065
|
+
}
|
|
11066
|
+
if (response.status !== 200) {
|
|
11067
|
+
throw new PrimeBridgeError(`${turn}bridge HTTP ${response.status}: ${response.text.slice(0, 500)}`);
|
|
11068
|
+
}
|
|
11069
|
+
let parsed;
|
|
11070
|
+
try {
|
|
11071
|
+
parsed = JSON.parse(response.text);
|
|
11072
|
+
} catch {
|
|
11073
|
+
throw new PrimeBridgeError(`${turn}bridge returned unparseable JSON (${response.text.length} bytes)`);
|
|
11074
|
+
}
|
|
11075
|
+
const record2 = parsed;
|
|
11076
|
+
const replyContent = record2.choices?.[0]?.message?.content;
|
|
11077
|
+
if (typeof replyContent !== "string" || replyContent.length === 0) {
|
|
11078
|
+
throw new PrimeBridgeError(`${turn}bridge reply carries no message content`);
|
|
11079
|
+
}
|
|
11080
|
+
return { content: replyContent, usage: normalizeUsage(record2.usage) };
|
|
11081
|
+
}
|
|
11082
|
+
function failure(output, error) {
|
|
11083
|
+
return { analyzer: ANALYST_ID, kind: "report", ok: false, output, error };
|
|
11084
|
+
}
|
|
11085
|
+
function primeAnalyzer(opts = {}) {
|
|
11086
|
+
const bridgeUrl = (opts.bridgeUrl ?? process.env.TRACES_PRIME_BRIDGE_URL ?? DEFAULT_BRIDGE_URL).replace(/\/+$/, "");
|
|
11087
|
+
const model = opts.model ?? process.env.TRACES_PRIME_MODEL ?? DEFAULT_MODEL;
|
|
11088
|
+
const timeoutMs = opts.timeoutMs ?? positiveIntegerEnv("TRACES_PRIME_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
|
|
11089
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
11090
|
+
throw new RangeError("timeoutMs must be a positive safe integer");
|
|
11091
|
+
}
|
|
11092
|
+
const maxInlineChars = opts.maxInlineChars ?? DEFAULT_MAX_INLINE_CHARS;
|
|
11093
|
+
const perAttributeCharCap = opts.perAttributeCharCap ?? DEFAULT_PER_ATTRIBUTE_CHAR_CAP;
|
|
11094
|
+
if (!Number.isSafeInteger(maxInlineChars) || maxInlineChars < 1) {
|
|
11095
|
+
throw new RangeError("maxInlineChars must be a positive safe integer");
|
|
11096
|
+
}
|
|
11097
|
+
if (!Number.isSafeInteger(perAttributeCharCap) || perAttributeCharCap < 1) {
|
|
11098
|
+
throw new RangeError("perAttributeCharCap must be a positive safe integer");
|
|
11099
|
+
}
|
|
11100
|
+
const repairEnabled = opts.repair ?? true;
|
|
11101
|
+
const transport = opts.transport ?? httpJsonTransport;
|
|
11102
|
+
const url = `${bridgeUrl}/v1/chat/completions`;
|
|
11103
|
+
return {
|
|
11104
|
+
name: ANALYST_ID,
|
|
11105
|
+
async analyze(otlpPath, analyzeOpts = {}) {
|
|
11106
|
+
let projection;
|
|
11107
|
+
try {
|
|
11108
|
+
projection = await projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, analyzeOpts.signal);
|
|
11109
|
+
} catch (error) {
|
|
11110
|
+
return failure("", error instanceof Error ? error.message : String(error));
|
|
11111
|
+
}
|
|
11112
|
+
const question = analyzeOpts.prompt ?? opts.defaultPrompt ?? DEFAULT_QUESTION;
|
|
11113
|
+
const deliveryLine = `delivery: ${projection.delivery.mode} (${projection.delivery.renderedChars} chars, per-attribute cap ${projection.delivery.perAttributeCharCap ?? "none"})`;
|
|
11114
|
+
let reply;
|
|
11115
|
+
try {
|
|
11116
|
+
reply = await callBridge(
|
|
11117
|
+
transport,
|
|
11118
|
+
url,
|
|
11119
|
+
model,
|
|
11120
|
+
buildPrompt(question, projection),
|
|
11121
|
+
timeoutMs,
|
|
11122
|
+
analyzeOpts.signal,
|
|
11123
|
+
""
|
|
11124
|
+
);
|
|
11125
|
+
} catch (error) {
|
|
11126
|
+
if (!(error instanceof PrimeBridgeError)) throw error;
|
|
11127
|
+
return failure("", `${error.message}; ${deliveryLine}`);
|
|
11128
|
+
}
|
|
11129
|
+
let usage = reply.usage;
|
|
11130
|
+
let parsed = extractJsonObject(reply.content);
|
|
11131
|
+
let parseDefect = parseDefectOf(parsed);
|
|
11132
|
+
let repairAttempted = false;
|
|
11133
|
+
if (parseDefect !== null && repairEnabled) {
|
|
11134
|
+
repairAttempted = true;
|
|
11135
|
+
let repairReply;
|
|
11136
|
+
try {
|
|
11137
|
+
repairReply = await callBridge(
|
|
11138
|
+
transport,
|
|
11139
|
+
url,
|
|
11140
|
+
model,
|
|
11141
|
+
buildRepairPrompt(parseDefect, reply.content),
|
|
11142
|
+
timeoutMs,
|
|
11143
|
+
analyzeOpts.signal,
|
|
11144
|
+
"repair-turn "
|
|
11145
|
+
);
|
|
11146
|
+
} catch (error) {
|
|
11147
|
+
if (!(error instanceof PrimeBridgeError)) throw error;
|
|
11148
|
+
return failure(reply.content, `${error.message}; ${renderUsage(usage)}`);
|
|
11149
|
+
}
|
|
11150
|
+
usage = mergeUsage(usage, repairReply.usage);
|
|
11151
|
+
parsed = extractJsonObject(repairReply.content);
|
|
11152
|
+
parseDefect = parseDefectOf(parsed);
|
|
11153
|
+
}
|
|
11154
|
+
if (parseDefect !== null) {
|
|
11155
|
+
return failure(
|
|
11156
|
+
reply.content,
|
|
11157
|
+
`${parseDefect} in prime reply${repairAttempted ? " even after the bounded repair turn" : ""}; ` + renderUsage(usage)
|
|
11158
|
+
);
|
|
11159
|
+
}
|
|
11160
|
+
const rows = parsed.findings.slice(0, MAX_FINDINGS);
|
|
11161
|
+
const overflow = parsed.findings.length - rows.length;
|
|
11162
|
+
const producedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11163
|
+
const rejected = [];
|
|
11164
|
+
const findings = [];
|
|
11165
|
+
rows.forEach((row, index) => {
|
|
11166
|
+
const decoded = findingRowDefect(row, projection.spanTraces);
|
|
11167
|
+
if (typeof decoded === "string") {
|
|
11168
|
+
rejected.push(`rejected[${index}]: ${decoded}`);
|
|
11169
|
+
return;
|
|
11170
|
+
}
|
|
11171
|
+
findings.push(toAnalystFinding(decoded, index, projection.spanTraces, model, producedAt));
|
|
11172
|
+
});
|
|
11173
|
+
const answer = typeof parsed.answer === "string" ? parsed.answer : null;
|
|
11174
|
+
const output = [
|
|
11175
|
+
...answer ? [`answer: ${answer}`] : [],
|
|
11176
|
+
`findings: ${findings.length} mapped, ${rejected.length} rejected` + (overflow > 0 ? `, ${overflow} over the ${MAX_FINDINGS}-finding cap dropped` : ""),
|
|
11177
|
+
...rejected,
|
|
11178
|
+
deliveryLine,
|
|
11179
|
+
`repair: ${repairAttempted ? "attempted (succeeded)" : "not needed"}`,
|
|
11180
|
+
renderUsage(usage),
|
|
11181
|
+
...findings.length === 0 ? ["zero findings \u2014 an honest null, not a failure"] : []
|
|
11182
|
+
].join("\n");
|
|
11183
|
+
return { analyzer: ANALYST_ID, kind: "findings", ok: true, output, findings };
|
|
11184
|
+
}
|
|
11185
|
+
};
|
|
11186
|
+
}
|
|
11187
|
+
|
|
11188
|
+
// src/redact.ts
|
|
11189
|
+
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
11190
|
+
var CODING_REDACTION_RULES = [
|
|
11191
|
+
{ id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
|
|
11192
|
+
{ id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
11193
|
+
{ id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
11194
|
+
{ id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
11195
|
+
{ id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
|
|
11196
|
+
{
|
|
11197
|
+
id: "private-key",
|
|
11198
|
+
pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
|
|
11199
|
+
},
|
|
11200
|
+
// Match the whole identifier because underscores prevent a word boundary before a secret suffix.
|
|
11201
|
+
{
|
|
11202
|
+
id: "assigned-secret",
|
|
11203
|
+
pattern: /(?<![A-Za-z0-9_-])(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])\+?=(?:\$\((?:\\.|[^\\\r\n])*\)|\$\{(?:\\.|[^}\\\r\n])*\}|`(?:\\.|[^`\\\r\n])*`|\$?'(?:\\.|[^'\\\r\n])*'|\$?"(?:\\.|[^"\\\r\n])*"|\\[^\r\n]|[^\s;&|<>()"'`\\\r\n])+/gi
|
|
11204
|
+
},
|
|
11205
|
+
// Config-file secrets (YAML/INI line keys, JSON quoted keys) — the coverage the shell
|
|
11206
|
+
// rule above deliberately gave up to spare prose ("Narrative token: x") and type
|
|
11207
|
+
// annotations ("(token: string)"). The line anchor is the discriminator: config keys
|
|
11208
|
+
// start their line, prose colons never do; JSON keys are safe anywhere because both
|
|
11209
|
+
// sides are quoted. Identifiers must END in a secret word, same as the shell rule.
|
|
11210
|
+
{
|
|
11211
|
+
id: "config-secret",
|
|
11212
|
+
pattern: /(?:^[ \t]*["']?(?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)(?![A-Za-z0-9_-])["']?[ \t]*[:=][ \t]*|["'](?:[A-Za-z_][A-Za-z0-9_-]*)?(?:api[_-]?key|secret|token|password|passwd|access[_-]?token|client[_-]?secret)["']\s*:\s*)["']?[A-Za-z0-9._\-]{12,}["']?/gim
|
|
11213
|
+
},
|
|
11214
|
+
// Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
|
|
11215
|
+
{ id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
|
|
11216
|
+
{
|
|
11217
|
+
id: "url-secret-param",
|
|
11218
|
+
pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
|
|
11219
|
+
}
|
|
11220
|
+
];
|
|
11221
|
+
var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
|
|
11222
|
+
function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
11223
|
+
const report = { redactionCount: 0, byRule: {} };
|
|
11224
|
+
const out = spans.map((s) => {
|
|
11225
|
+
const attributes2 = redactValue(s.attributes, rules, report).value;
|
|
10654
11226
|
normalizeToolIoAttributes(attributes2);
|
|
10655
11227
|
let status = s.status;
|
|
10656
11228
|
if (status.message) {
|
|
@@ -10703,7 +11275,7 @@ async function applyRedactor(spans, redactor) {
|
|
|
10703
11275
|
}
|
|
10704
11276
|
|
|
10705
11277
|
// src/file-export.ts
|
|
10706
|
-
import { createHash as
|
|
11278
|
+
import { createHash as createHash11 } from "crypto";
|
|
10707
11279
|
import { mkdtemp as mkdtemp4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
10708
11280
|
import { tmpdir as tmpdir5 } from "os";
|
|
10709
11281
|
import { join as join24 } from "path";
|
|
@@ -10741,7 +11313,7 @@ function stableJson(value) {
|
|
|
10741
11313
|
});
|
|
10742
11314
|
}
|
|
10743
11315
|
function hashId(value, chars = 16) {
|
|
10744
|
-
return
|
|
11316
|
+
return createHash11("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
10745
11317
|
}
|
|
10746
11318
|
function isoTime2(value) {
|
|
10747
11319
|
if (typeof value === "string") {
|
|
@@ -10902,12 +11474,12 @@ function policyEvidenceToSpans(records) {
|
|
|
10902
11474
|
"traces.provenance.evidence_kind": record2.provenance.evidenceKind,
|
|
10903
11475
|
"traces.provenance.otlp_path": record2.provenance.otlpPath,
|
|
10904
11476
|
"traces.provenance.source_sha256": record2.provenance.sourceSha256,
|
|
10905
|
-
[
|
|
10906
|
-
[
|
|
10907
|
-
[
|
|
10908
|
-
[
|
|
10909
|
-
[
|
|
10910
|
-
[
|
|
11477
|
+
[ATTR.SUBJECT_KEY]: record2.repo.subjectKey,
|
|
11478
|
+
[ATTR.GIT_REPOSITORY]: record2.repo.repository,
|
|
11479
|
+
[ATTR.GIT_BRANCH_NAME]: record2.repo.branch,
|
|
11480
|
+
[ATTR.GIT_COMMIT]: record2.repo.commit,
|
|
11481
|
+
[ATTR.CWD]: record2.repo.cwd ?? record2.session.cwd,
|
|
11482
|
+
[ATTR.REPO_RESOLUTION_SOURCE]: record2.repo.resolutionSource
|
|
10911
11483
|
};
|
|
10912
11484
|
return span({
|
|
10913
11485
|
traceId,
|
|
@@ -11004,6 +11576,54 @@ function eventKind(row, type) {
|
|
|
11004
11576
|
if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
|
|
11005
11577
|
return { kind: "CHAIN" };
|
|
11006
11578
|
}
|
|
11579
|
+
function sandboxEventsEnvironment(context, rows) {
|
|
11580
|
+
const ready = rows.find((row) => eventType(row) === "runtime.ready");
|
|
11581
|
+
const readyData = ready && isObject5(ready.data) ? ready.data : void 0;
|
|
11582
|
+
const pick = (keys) => findStringKey(context, keys, 2) ?? (readyData ? findStringKey(readyData, keys, 2) : void 0);
|
|
11583
|
+
const image = pick(["image"]);
|
|
11584
|
+
const imageDigest = pick(["imageDigest", "image_digest"]);
|
|
11585
|
+
const sandboxId = pick(["sandboxId", "sandbox_id"]);
|
|
11586
|
+
const cwd = pick(["cwd", "workspaceRoot", "workspace_root"]);
|
|
11587
|
+
if (!image && !imageDigest && !sandboxId && !cwd) return null;
|
|
11588
|
+
return {
|
|
11589
|
+
...image ? { image } : {},
|
|
11590
|
+
...imageDigest ? { imageDigest } : {},
|
|
11591
|
+
...sandboxId ? { sandboxId } : {},
|
|
11592
|
+
cwd: cwd ?? null
|
|
11593
|
+
};
|
|
11594
|
+
}
|
|
11595
|
+
function llmEventUsage(row) {
|
|
11596
|
+
const num = (keys) => {
|
|
11597
|
+
for (const key of keys) {
|
|
11598
|
+
const direct = numberValue(row[key]);
|
|
11599
|
+
if (direct !== void 0) return direct;
|
|
11600
|
+
for (const nest of ["data", "usage", "tokenUsage"]) {
|
|
11601
|
+
const container = row[nest];
|
|
11602
|
+
if (!isObject5(container)) continue;
|
|
11603
|
+
const value = numberValue(container[key]);
|
|
11604
|
+
if (value !== void 0) return value;
|
|
11605
|
+
for (const inner of ["usage", "tokenUsage"]) {
|
|
11606
|
+
const deep = container[inner];
|
|
11607
|
+
if (isObject5(deep)) {
|
|
11608
|
+
const deepValue = numberValue(deep[key]);
|
|
11609
|
+
if (deepValue !== void 0) return deepValue;
|
|
11610
|
+
}
|
|
11611
|
+
}
|
|
11612
|
+
}
|
|
11613
|
+
}
|
|
11614
|
+
return void 0;
|
|
11615
|
+
};
|
|
11616
|
+
const model = findStringKey(row, ["model"], 3);
|
|
11617
|
+
const inputTokens = num(["prompt_tokens", "input_tokens", "inputTokens"]);
|
|
11618
|
+
const outputTokens = num(["completion_tokens", "output_tokens", "outputTokens"]);
|
|
11619
|
+
const costUsd = num(["totalCostUsd", "total_cost_usd", "costUsd", "cost_usd"]);
|
|
11620
|
+
return {
|
|
11621
|
+
...model ? { model } : {},
|
|
11622
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
11623
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
11624
|
+
...costUsd !== void 0 ? { costUsd } : {}
|
|
11625
|
+
};
|
|
11626
|
+
}
|
|
11007
11627
|
function sandboxEventsToSpans(rows, wrapper) {
|
|
11008
11628
|
const context = wrapper ?? {};
|
|
11009
11629
|
const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
|
|
@@ -11074,13 +11694,15 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
11074
11694
|
tool: kind === "TOOL" ? tool ?? type : null,
|
|
11075
11695
|
step: index,
|
|
11076
11696
|
content: capText(stableJson(row)),
|
|
11077
|
-
extra
|
|
11697
|
+
extra,
|
|
11698
|
+
...kind === "LLM" ? llmEventUsage(row) : {}
|
|
11078
11699
|
}));
|
|
11079
11700
|
});
|
|
11701
|
+
stampEnvironmentAttrs(spans, sandboxEventsEnvironment(context, rows) ?? void 0);
|
|
11080
11702
|
if (!sessionId) return spans;
|
|
11081
11703
|
return spans.map((item) => ({
|
|
11082
11704
|
...item,
|
|
11083
|
-
attributes: { ...item.attributes, [
|
|
11705
|
+
attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
|
|
11084
11706
|
}));
|
|
11085
11707
|
}
|
|
11086
11708
|
function statusCode2(value) {
|
|
@@ -11159,7 +11781,7 @@ function intelligenceSpansToSpans(rows) {
|
|
|
11159
11781
|
};
|
|
11160
11782
|
copyDefined(extra, {
|
|
11161
11783
|
"traces.row.id": stringValue3(row.id),
|
|
11162
|
-
[
|
|
11784
|
+
[ATTR.SESSION_ID]: sessionId,
|
|
11163
11785
|
"session.id": sessionId,
|
|
11164
11786
|
"run.id": stringValue3(row.run_id),
|
|
11165
11787
|
"thread.id": stringValue3(row.thread_id),
|
|
@@ -12534,6 +13156,47 @@ var SEVERITY_BADGE2 = {
|
|
|
12534
13156
|
low: "\u{1F535} LOW",
|
|
12535
13157
|
info: "\u2139\uFE0F INFO"
|
|
12536
13158
|
};
|
|
13159
|
+
function sessionReportSource(ref, spans, sessionIdOverride) {
|
|
13160
|
+
const root = spans.find((item) => item.parent_span_id === null) ?? spans[0];
|
|
13161
|
+
const prompt = spans.find(
|
|
13162
|
+
(item) => item.name === "user.prompt" && item.attributes[ACTOR_ATTR] === "human"
|
|
13163
|
+
) ?? spans.find((item) => item.name === "user.prompt") ?? spans.find(
|
|
13164
|
+
(item) => item.attributes["span.type"] === "interaction" && typeof item.attributes.content === "string"
|
|
13165
|
+
);
|
|
13166
|
+
const content = typeof prompt?.attributes.content === "string" ? prompt.attributes.content : "";
|
|
13167
|
+
const firstLine = content.split(/\r?\n/, 1)[0].trim();
|
|
13168
|
+
const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
|
|
13169
|
+
const role = root?.attributes["traces.session.role"];
|
|
13170
|
+
const parentSessionId = root?.attributes["traces.parent_session_id"];
|
|
13171
|
+
const relationship = describeSessionRelationship(ref, spans);
|
|
13172
|
+
const corruptionDigest2 = root?.attributes[ATTR.CORRUPTION_DIGEST];
|
|
13173
|
+
const sessionId = [
|
|
13174
|
+
sessionIdOverride,
|
|
13175
|
+
root ? sessionIdFromAttributes(root.attributes) : void 0,
|
|
13176
|
+
root?.trace_id,
|
|
13177
|
+
ref.sessionId
|
|
13178
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
13179
|
+
return {
|
|
13180
|
+
sessionId,
|
|
13181
|
+
path: ref.path,
|
|
13182
|
+
subject,
|
|
13183
|
+
role: role === "operator" || role === "child" ? role : "unknown",
|
|
13184
|
+
...typeof parentSessionId === "string" ? { parentSessionId } : {},
|
|
13185
|
+
childSessionIds: relationship.childSessionIds,
|
|
13186
|
+
...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
|
|
13187
|
+
...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
|
|
13188
|
+
...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
|
|
13189
|
+
...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
|
|
13190
|
+
...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
|
|
13191
|
+
...relationship.turnId ? { turnId: relationship.turnId } : {},
|
|
13192
|
+
integrity: ref.integrity?.status ?? "complete",
|
|
13193
|
+
...ref.integrity ? {
|
|
13194
|
+
corruptionCount: ref.integrity.corruptions.length,
|
|
13195
|
+
...typeof corruptionDigest2 === "string" ? { corruptionDigest: corruptionDigest2 } : {},
|
|
13196
|
+
corruptions: ref.integrity.corruptions.slice(0, CORRUPTION_RECEIPT_DISPLAY_LIMIT)
|
|
13197
|
+
} : {}
|
|
13198
|
+
};
|
|
13199
|
+
}
|
|
12537
13200
|
function deterministicSummaryText(summary) {
|
|
12538
13201
|
const parts = [];
|
|
12539
13202
|
if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
|
|
@@ -12912,11 +13575,27 @@ function renderPipelines(pr, unavailable) {
|
|
|
12912
13575
|
byTool.set(name, aggregate);
|
|
12913
13576
|
}
|
|
12914
13577
|
}
|
|
12915
|
-
const
|
|
13578
|
+
const followUps = pr.failureFollowUps;
|
|
13579
|
+
let failureFollowUp = "";
|
|
13580
|
+
if (followUps && followUps.failures > 0) {
|
|
13581
|
+
const split = [
|
|
13582
|
+
`${followUps.blind} blind (identical args)`,
|
|
13583
|
+
`${followUps.adapted} adapted (changed args)`
|
|
13584
|
+
];
|
|
13585
|
+
if (followUps.argsUnknown > 0) split.push(`${followUps.argsUnknown} not comparable`);
|
|
13586
|
+
const succeeded = followUps.followed > 0 ? `; ${followUps.followUpSucceeded}/${followUps.followed} follow-ups succeeded` : "";
|
|
13587
|
+
failureFollowUp = `; ${followUps.followed}/${followUps.failures} failed calls followed by another same-tool call (${(followUps.followed / followUps.failures * 100).toFixed(0)}%) \u2014 ${split.join(", ")}${succeeded}`;
|
|
13588
|
+
} else if (errorCalls > 0) {
|
|
13589
|
+
failureFollowUp = `; ${followedFailures}/${errorCalls} failed calls followed by another same-tool call (${(followedFailures / errorCalls * 100).toFixed(0)}%)`;
|
|
13590
|
+
}
|
|
12916
13591
|
const duplicateSummary = callsWithCapturedArgs > 0 ? `${duplicateCalls}/${callsWithCapturedArgs} captured calls repeated exactly` : "exact repeats unavailable";
|
|
12917
13592
|
lines.push(
|
|
12918
13593
|
`- **Tool use:** ${totalCalls} calls across ${runsWithTools.length}/${pr.toolUse.length} traces; ${callsWithCapturedArgs}/${totalCalls} arguments captured; ${duplicateSummary}; ${errorCalls}/${totalCalls} failed${failureFollowUp}`
|
|
12919
13594
|
);
|
|
13595
|
+
if (followUps && followUps.blind > 0) {
|
|
13596
|
+
const byTool2 = Object.entries(followUps.blindByTool).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])).map(([name, count2]) => `\`${tableCell2(name)}\` \xD7${count2}`);
|
|
13597
|
+
lines.push(` - \u{1F501} Blind retries (same args re-sent after a failure): ${byTool2.join(", ")}`);
|
|
13598
|
+
}
|
|
12920
13599
|
if (byTool.size > 0) {
|
|
12921
13600
|
lines.push("");
|
|
12922
13601
|
lines.push("| Tool | Calls | Args captured | Exact repeats | Errors | Mean latency |");
|
|
@@ -13684,27 +14363,27 @@ async function saveReport(path, report) {
|
|
|
13684
14363
|
}
|
|
13685
14364
|
|
|
13686
14365
|
// src/live.ts
|
|
13687
|
-
import { createHash as
|
|
14366
|
+
import { createHash as createHash12 } from "crypto";
|
|
13688
14367
|
var VERIFY_RE = /\b(test|tests|vitest|jest|pytest|go test|cargo test|tsc|typecheck|lint|biome|eslint|build|check:invariants|preflight)\b/i;
|
|
13689
14368
|
var CHANGE_RE = /\b(apply_patch|edit|write|create|delete|rename|replace|update|git apply|sed -i|perl -pi|tee\s|cat\s+>)/i;
|
|
13690
14369
|
var CLAIM_DONE_RE = /\b(done|fixed|complete|completed|implemented|merged|verified|all green|tests pass|ready|works now)\b/i;
|
|
13691
|
-
var sleep = (ms2, signal) => new Promise((
|
|
14370
|
+
var sleep = (ms2, signal) => new Promise((resolve10) => {
|
|
13692
14371
|
if (signal?.aborted) {
|
|
13693
|
-
|
|
14372
|
+
resolve10();
|
|
13694
14373
|
return;
|
|
13695
14374
|
}
|
|
13696
14375
|
const onAbort = () => {
|
|
13697
14376
|
clearTimeout(t);
|
|
13698
|
-
|
|
14377
|
+
resolve10();
|
|
13699
14378
|
};
|
|
13700
14379
|
const t = setTimeout(() => {
|
|
13701
14380
|
signal?.removeEventListener("abort", onAbort);
|
|
13702
|
-
|
|
14381
|
+
resolve10();
|
|
13703
14382
|
}, ms2);
|
|
13704
14383
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
13705
14384
|
});
|
|
13706
14385
|
function hash(parts) {
|
|
13707
|
-
return
|
|
14386
|
+
return createHash12("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
13708
14387
|
}
|
|
13709
14388
|
function asString(value) {
|
|
13710
14389
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -15174,13 +15853,13 @@ function aborted(signal) {
|
|
|
15174
15853
|
return signal !== void 0 && signal.aborted;
|
|
15175
15854
|
}
|
|
15176
15855
|
function sleep2(msDelay, signal) {
|
|
15177
|
-
return new Promise((
|
|
15178
|
-
const timer = setTimeout(
|
|
15856
|
+
return new Promise((resolve10) => {
|
|
15857
|
+
const timer = setTimeout(resolve10, msDelay);
|
|
15179
15858
|
signal?.addEventListener(
|
|
15180
15859
|
"abort",
|
|
15181
15860
|
() => {
|
|
15182
15861
|
clearTimeout(timer);
|
|
15183
|
-
|
|
15862
|
+
resolve10();
|
|
15184
15863
|
},
|
|
15185
15864
|
{ once: true }
|
|
15186
15865
|
);
|
|
@@ -15520,11 +16199,11 @@ async function collectSessionWorkflow(adapter, seeds, options = {}) {
|
|
|
15520
16199
|
}
|
|
15521
16200
|
|
|
15522
16201
|
// src/session-selection.ts
|
|
15523
|
-
import { createHash as
|
|
16202
|
+
import { createHash as createHash13 } from "crypto";
|
|
15524
16203
|
import { createReadStream as createReadStream2 } from "fs";
|
|
15525
16204
|
async function fileSha256(path, signal) {
|
|
15526
16205
|
signal?.throwIfAborted();
|
|
15527
|
-
const hash2 =
|
|
16206
|
+
const hash2 = createHash13("sha256");
|
|
15528
16207
|
try {
|
|
15529
16208
|
for await (const chunk of createReadStream2(path, { signal })) {
|
|
15530
16209
|
signal?.throwIfAborted();
|
|
@@ -15552,7 +16231,7 @@ async function sourceSnapshot(adapter, ref, signal) {
|
|
|
15552
16231
|
signal?.throwIfAborted();
|
|
15553
16232
|
files.push({ path, sha256: await fileSha256(path, signal) });
|
|
15554
16233
|
}
|
|
15555
|
-
const sha2562 = files.length === 1 ? files[0].sha256 :
|
|
16234
|
+
const sha2562 = files.length === 1 ? files[0].sha256 : createHash13("sha256").update(JSON.stringify(files)).digest("hex");
|
|
15556
16235
|
return { sha256: sha2562, files };
|
|
15557
16236
|
}
|
|
15558
16237
|
function summarizeWorkflows(workflows) {
|
|
@@ -15813,14 +16492,30 @@ async function collectContextRoot(root) {
|
|
|
15813
16492
|
const summary = await summarizeFile(join31(evolve, name), "evolve-jsonl");
|
|
15814
16493
|
if (summary) files.push(summary);
|
|
15815
16494
|
}
|
|
15816
|
-
for (const name of ["scorecard.json"]) {
|
|
16495
|
+
for (const name of ["scorecard.json", "current.json"]) {
|
|
15817
16496
|
const summary = await summarizeFile(join31(evolve, name), "evolve-json");
|
|
15818
16497
|
if (summary) files.push(summary);
|
|
15819
16498
|
}
|
|
15820
16499
|
files.push(...await walkMarkdown(join31(evolve, "reflections"), "reflection"));
|
|
15821
16500
|
files.push(...await walkMarkdown(join31(evolve, "handoffs"), "handoff"));
|
|
16501
|
+
for (const name of await listEvolveRootFiles(evolve)) {
|
|
16502
|
+
if (/^handoff-.*\.md$/.test(name)) {
|
|
16503
|
+
const summary = await summarizeFile(join31(evolve, name), "handoff");
|
|
16504
|
+
if (summary) files.push(summary);
|
|
16505
|
+
}
|
|
16506
|
+
}
|
|
16507
|
+
const progress = await summarizeFile(join31(evolve, "progress.md"), "other");
|
|
16508
|
+
if (progress) files.push(progress);
|
|
15822
16509
|
return { root, files: files.sort((a, b) => a.path.localeCompare(b.path)) };
|
|
15823
16510
|
}
|
|
16511
|
+
async function listEvolveRootFiles(evolve) {
|
|
16512
|
+
try {
|
|
16513
|
+
const entries = await readdir18(evolve, { withFileTypes: true });
|
|
16514
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
16515
|
+
} catch {
|
|
16516
|
+
return [];
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
15824
16519
|
async function collectContextIndex(records) {
|
|
15825
16520
|
const rootSet = /* @__PURE__ */ new Set();
|
|
15826
16521
|
for (const record2 of records) {
|
|
@@ -15869,14 +16564,641 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
15869
16564
|
return path;
|
|
15870
16565
|
}
|
|
15871
16566
|
|
|
16567
|
+
// src/bundle.ts
|
|
16568
|
+
import { createHash as createHash14 } from "crypto";
|
|
16569
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
16570
|
+
import { cp, mkdir as mkdir5, readdir as readdir19, readFile as readFile14, stat as stat17, writeFile as writeFile9 } from "fs/promises";
|
|
16571
|
+
import { join as join32, relative as relative5, resolve as resolve8, sep as sep2 } from "path";
|
|
16572
|
+
var SESSION_WINDOW_PAD_MS = 15 * 60 * 1e3;
|
|
16573
|
+
var KNOWN_LIMITS = [
|
|
16574
|
+
"'what was decided' is prose-only: handoff, progress, and reflection files are copied as markdown with no structured decision extraction",
|
|
16575
|
+
"'what was measured' is split between experiments.jsonl free-text fields and progress/handoff markdown tables",
|
|
16576
|
+
"session-to-ledger join is by timestamp/cwd inference: ledger rows are window-sliced, not linked by transcript path",
|
|
16577
|
+
"progress.md is copied whole; no session-dated section extraction is performed",
|
|
16578
|
+
"open pull-request state is not captured: the bundler makes no network calls"
|
|
16579
|
+
];
|
|
16580
|
+
function sha256Hex(bytes) {
|
|
16581
|
+
return createHash14("sha256").update(bytes).digest("hex");
|
|
16582
|
+
}
|
|
16583
|
+
function bundlePath(...parts) {
|
|
16584
|
+
return parts.join("/");
|
|
16585
|
+
}
|
|
16586
|
+
async function statOrNull(path) {
|
|
16587
|
+
try {
|
|
16588
|
+
return await stat17(path);
|
|
16589
|
+
} catch (error) {
|
|
16590
|
+
if (isMissingPathError(error)) return null;
|
|
16591
|
+
if (error.code === "ENOTDIR") return null;
|
|
16592
|
+
throw error;
|
|
16593
|
+
}
|
|
16594
|
+
}
|
|
16595
|
+
function sliceJsonlByWindow(text2, windowStartMs, windowEndMs) {
|
|
16596
|
+
const kept = [];
|
|
16597
|
+
let totalRows = 0;
|
|
16598
|
+
let unparseableTsRows = 0;
|
|
16599
|
+
for (const line of text2.split("\n")) {
|
|
16600
|
+
if (!line.trim()) continue;
|
|
16601
|
+
totalRows += 1;
|
|
16602
|
+
let tsMs = Number.NaN;
|
|
16603
|
+
try {
|
|
16604
|
+
const row = JSON.parse(line);
|
|
16605
|
+
const ts = row !== null && typeof row === "object" && !Array.isArray(row) ? row.ts : void 0;
|
|
16606
|
+
if (typeof ts === "string") tsMs = Date.parse(ts);
|
|
16607
|
+
} catch {
|
|
16608
|
+
}
|
|
16609
|
+
if (Number.isNaN(tsMs)) {
|
|
16610
|
+
unparseableTsRows += 1;
|
|
16611
|
+
continue;
|
|
16612
|
+
}
|
|
16613
|
+
if (tsMs >= windowStartMs && tsMs <= windowEndMs) kept.push(line);
|
|
16614
|
+
}
|
|
16615
|
+
return { kept, totalRows, unparseableTsRows };
|
|
16616
|
+
}
|
|
16617
|
+
async function runGitLog(cwd, sinceIso, untilIso) {
|
|
16618
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
16619
|
+
const { promisify: promisify2 } = await import("util");
|
|
16620
|
+
const run = promisify2(execFile2);
|
|
16621
|
+
try {
|
|
16622
|
+
const { stdout } = await run(
|
|
16623
|
+
"git",
|
|
16624
|
+
["-C", cwd, "log", `--since=${sinceIso}`, `--until=${untilIso}`, "--date=iso-strict", "--pretty=format:%H %ad %an %s"],
|
|
16625
|
+
{ timeout: 5e3 }
|
|
16626
|
+
);
|
|
16627
|
+
return { succeeded: true, stdout };
|
|
16628
|
+
} catch (error) {
|
|
16629
|
+
return { succeeded: false, error: error instanceof Error ? error.message.split("\n", 1)[0] : String(error) };
|
|
16630
|
+
}
|
|
16631
|
+
}
|
|
16632
|
+
async function listSessionBundleFiles(root) {
|
|
16633
|
+
const out = [];
|
|
16634
|
+
const pending = [resolve8(root)];
|
|
16635
|
+
while (pending.length > 0) {
|
|
16636
|
+
const dir = pending.pop();
|
|
16637
|
+
for (const entry of await readdir19(dir, { withFileTypes: true })) {
|
|
16638
|
+
const path = join32(dir, entry.name);
|
|
16639
|
+
if (entry.isDirectory()) pending.push(path);
|
|
16640
|
+
else if (entry.isFile()) out.push(relative5(resolve8(root), path).split(sep2).join("/"));
|
|
16641
|
+
}
|
|
16642
|
+
}
|
|
16643
|
+
return out.sort();
|
|
16644
|
+
}
|
|
16645
|
+
async function sliceLedgerJsonl(ctx, name) {
|
|
16646
|
+
const source = join32(ctx.evolveDir, name);
|
|
16647
|
+
const s = await statOrNull(source);
|
|
16648
|
+
if (!s?.isFile()) {
|
|
16649
|
+
ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
|
|
16650
|
+
return;
|
|
16651
|
+
}
|
|
16652
|
+
const text2 = await readFile14(source, "utf8");
|
|
16653
|
+
const target = join32(ctx.ledgerDir, name);
|
|
16654
|
+
if (ctx.windowStartMs === null || ctx.windowEndMs === null) {
|
|
16655
|
+
await writeFile9(target, text2, "utf8");
|
|
16656
|
+
ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16659
|
+
const slice = sliceJsonlByWindow(text2, ctx.windowStartMs, ctx.windowEndMs);
|
|
16660
|
+
await writeFile9(target, slice.kept.length > 0 ? `${slice.kept.join("\n")}
|
|
16661
|
+
` : "", "utf8");
|
|
16662
|
+
ctx.slices.push({
|
|
16663
|
+
path: bundlePath("ledger", name),
|
|
16664
|
+
source,
|
|
16665
|
+
rule: "session-window-rows",
|
|
16666
|
+
totalRows: slice.totalRows,
|
|
16667
|
+
keptRows: slice.kept.length,
|
|
16668
|
+
unparseableTsRows: slice.unparseableTsRows
|
|
16669
|
+
});
|
|
16670
|
+
}
|
|
16671
|
+
async function copyLedgerWhole(ctx, name) {
|
|
16672
|
+
const source = join32(ctx.evolveDir, name);
|
|
16673
|
+
const s = await statOrNull(source);
|
|
16674
|
+
if (!s?.isFile()) {
|
|
16675
|
+
ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
|
|
16676
|
+
return;
|
|
16677
|
+
}
|
|
16678
|
+
await cp(source, join32(ctx.ledgerDir, name));
|
|
16679
|
+
ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
|
|
16680
|
+
}
|
|
16681
|
+
async function copyLatestHandoff(ctx) {
|
|
16682
|
+
let names;
|
|
16683
|
+
try {
|
|
16684
|
+
names = (await readdir19(ctx.evolveDir, { withFileTypes: true })).filter((entry) => entry.isFile() && /^handoff-.*\.md$/.test(entry.name)).map((entry) => entry.name).sort();
|
|
16685
|
+
} catch {
|
|
16686
|
+
names = [];
|
|
16687
|
+
}
|
|
16688
|
+
const latest = names[names.length - 1];
|
|
16689
|
+
if (!latest) {
|
|
16690
|
+
ctx.absent.push({
|
|
16691
|
+
path: bundlePath("ledger", "handoff-*.md"),
|
|
16692
|
+
reason: `no flat handoff-*.md files in ${ctx.evolveDir}`
|
|
16693
|
+
});
|
|
16694
|
+
return;
|
|
16695
|
+
}
|
|
16696
|
+
const source = join32(ctx.evolveDir, latest);
|
|
16697
|
+
await cp(source, join32(ctx.ledgerDir, latest));
|
|
16698
|
+
ctx.slices.push({ path: bundlePath("ledger", latest), source, rule: "latest-by-name" });
|
|
16699
|
+
}
|
|
16700
|
+
async function copyWindowReflections(ctx) {
|
|
16701
|
+
const reflectionsDir = join32(ctx.evolveDir, "reflections");
|
|
16702
|
+
const s = await statOrNull(reflectionsDir);
|
|
16703
|
+
if (!s?.isDirectory()) {
|
|
16704
|
+
ctx.absent.push({
|
|
16705
|
+
path: bundlePath("ledger", "reflections"),
|
|
16706
|
+
reason: `no reflections directory at ${reflectionsDir}`
|
|
16707
|
+
});
|
|
16708
|
+
return;
|
|
16709
|
+
}
|
|
16710
|
+
const names = (await readdir19(reflectionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
16711
|
+
const windowed = ctx.windowStartMs === null || ctx.windowEndMs === null ? names : names.filter((name) => {
|
|
16712
|
+
const date = name.match(/(\d{4}-\d{2}-\d{2})/)?.[1];
|
|
16713
|
+
if (!date) return false;
|
|
16714
|
+
const startDate = new Date(ctx.windowStartMs).toISOString().slice(0, 10);
|
|
16715
|
+
const endDate = new Date(ctx.windowEndMs).toISOString().slice(0, 10);
|
|
16716
|
+
return date >= startDate && date <= endDate;
|
|
16717
|
+
});
|
|
16718
|
+
if (windowed.length === 0) {
|
|
16719
|
+
ctx.absent.push({
|
|
16720
|
+
path: bundlePath("ledger", "reflections"),
|
|
16721
|
+
reason: `no reflections dated inside the session window in ${reflectionsDir}`
|
|
16722
|
+
});
|
|
16723
|
+
return;
|
|
16724
|
+
}
|
|
16725
|
+
await mkdir5(join32(ctx.ledgerDir, "reflections"), { recursive: true });
|
|
16726
|
+
const rule = ctx.windowStartMs === null ? "copied-whole" : "session-window-dated-files";
|
|
16727
|
+
for (const name of windowed) {
|
|
16728
|
+
const source = join32(reflectionsDir, name);
|
|
16729
|
+
await cp(source, join32(ctx.ledgerDir, "reflections", name));
|
|
16730
|
+
ctx.slices.push({ path: bundlePath("ledger", "reflections", name), source, rule });
|
|
16731
|
+
}
|
|
16732
|
+
}
|
|
16733
|
+
async function assembleSessionBundle(opts) {
|
|
16734
|
+
const { adapter, ref } = opts;
|
|
16735
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16736
|
+
let transcriptBytes;
|
|
16737
|
+
try {
|
|
16738
|
+
transcriptBytes = await readFile14(ref.path);
|
|
16739
|
+
} catch (error) {
|
|
16740
|
+
if (isMissingPathError(error)) {
|
|
16741
|
+
throw new Error(
|
|
16742
|
+
`session transcript not found at ${ref.path} \u2014 a bundle cannot be assembled without its transcript`
|
|
16743
|
+
);
|
|
16744
|
+
}
|
|
16745
|
+
throw error;
|
|
16746
|
+
}
|
|
16747
|
+
const transcriptSha256 = sha256Hex(transcriptBytes);
|
|
16748
|
+
const spans = await parseSession(adapter, ref, { signal: opts.signal });
|
|
16749
|
+
const outDir = resolve8(opts.outDir);
|
|
16750
|
+
await mkdir5(outDir, { recursive: true });
|
|
16751
|
+
if ((await readdir19(outDir)).length > 0) {
|
|
16752
|
+
throw new Error(
|
|
16753
|
+
`bundle output directory ${outDir} is not empty \u2014 pass a new directory so one bundle holds exactly one session`
|
|
16754
|
+
);
|
|
16755
|
+
}
|
|
16756
|
+
const absent = [];
|
|
16757
|
+
const slices = [];
|
|
16758
|
+
await mkdir5(join32(outDir, "session"), { recursive: true });
|
|
16759
|
+
await writeFile9(join32(outDir, "session", "transcript.jsonl"), transcriptBytes);
|
|
16760
|
+
const subagentsSource = join32(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
16761
|
+
if ((await statOrNull(subagentsSource))?.isDirectory()) {
|
|
16762
|
+
await cp(subagentsSource, join32(outDir, "session", "subagents"), { recursive: true });
|
|
16763
|
+
} else {
|
|
16764
|
+
absent.push({ path: bundlePath("session", "subagents"), reason: `no subagents directory at ${subagentsSource}` });
|
|
16765
|
+
}
|
|
16766
|
+
await mkdir5(join32(outDir, "derived"), { recursive: true });
|
|
16767
|
+
const otlpPath = join32(outDir, "derived", "trace.otlp.jsonl");
|
|
16768
|
+
const investigation = await runTraceInvestigation({
|
|
16769
|
+
spans,
|
|
16770
|
+
harness: ref.harness,
|
|
16771
|
+
sources: [sessionReportSource(ref, spans)],
|
|
16772
|
+
cwds: ref.cwd ? [ref.cwd] : [],
|
|
16773
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16774
|
+
otlpOutPath: otlpPath,
|
|
16775
|
+
generatedAt,
|
|
16776
|
+
signal: opts.signal,
|
|
16777
|
+
log: opts.log
|
|
16778
|
+
});
|
|
16779
|
+
await writeFile9(join32(outDir, "derived", "report.md"), investigation.report, "utf8");
|
|
16780
|
+
const evidence3 = await buildPolicyEvidenceRecord(ref, spans, {
|
|
16781
|
+
generatedAt,
|
|
16782
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16783
|
+
// Bundle-relative pointer: the bundle must stay internally valid when moved.
|
|
16784
|
+
otlpPath: bundlePath("derived", "trace.otlp.jsonl"),
|
|
16785
|
+
sourceSha256: transcriptSha256
|
|
16786
|
+
});
|
|
16787
|
+
await writeFile9(join32(outDir, "derived", "evidence.jsonl"), serializePolicyEvidence([evidence3]), "utf8");
|
|
16788
|
+
const index = await buildSessionIndexFromRows([{ ref, spans }], {
|
|
16789
|
+
generatedAt,
|
|
16790
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16791
|
+
selection: { command: "bundle", harness: ref.harness, session: ref.sessionId }
|
|
16792
|
+
});
|
|
16793
|
+
await writeFile9(join32(outDir, "derived", "session-index.json"), serializeSessionIndex(index), "utf8");
|
|
16794
|
+
const { firstSpanAt, lastSpanAt } = evidence3.metrics;
|
|
16795
|
+
const windowStartMs = firstSpanAt !== null ? Date.parse(firstSpanAt) - SESSION_WINDOW_PAD_MS : null;
|
|
16796
|
+
const windowEndMs = lastSpanAt !== null ? Date.parse(lastSpanAt) + SESSION_WINDOW_PAD_MS : null;
|
|
16797
|
+
const contextRoot = await findContextRoot(ref.cwd);
|
|
16798
|
+
if (!contextRoot) {
|
|
16799
|
+
absent.push({ path: "ledger", reason: `no context root found from session cwd ${ref.cwd ?? "(unknown)"}` });
|
|
16800
|
+
} else {
|
|
16801
|
+
const evolveDir = join32(contextRoot, ".evolve");
|
|
16802
|
+
if (!(await statOrNull(evolveDir))?.isDirectory()) {
|
|
16803
|
+
absent.push({ path: "ledger", reason: `no .evolve ledger at ${evolveDir}` });
|
|
16804
|
+
} else {
|
|
16805
|
+
const ledgerDir = join32(outDir, "ledger");
|
|
16806
|
+
await mkdir5(ledgerDir, { recursive: true });
|
|
16807
|
+
const ctx = { evolveDir, ledgerDir, windowStartMs, windowEndMs, slices, absent };
|
|
16808
|
+
await sliceLedgerJsonl(ctx, "experiments.jsonl");
|
|
16809
|
+
await sliceLedgerJsonl(ctx, "skill-runs.jsonl");
|
|
16810
|
+
await copyLedgerWhole(ctx, "current.json");
|
|
16811
|
+
await copyLedgerWhole(ctx, "scorecard.json");
|
|
16812
|
+
await copyLedgerWhole(ctx, "progress.md");
|
|
16813
|
+
await copyLatestHandoff(ctx);
|
|
16814
|
+
await copyWindowReflections(ctx);
|
|
16815
|
+
}
|
|
16816
|
+
}
|
|
16817
|
+
const gitLogPath = bundlePath("repo", "git-log.txt");
|
|
16818
|
+
if (!ref.cwd) {
|
|
16819
|
+
absent.push({ path: gitLogPath, reason: "session has no recorded cwd to read git history from" });
|
|
16820
|
+
} else if (windowStartMs === null || windowEndMs === null) {
|
|
16821
|
+
absent.push({ path: gitLogPath, reason: "session window unavailable (spans carry no timestamps)" });
|
|
16822
|
+
} else {
|
|
16823
|
+
const log = await runGitLog(ref.cwd, new Date(windowStartMs).toISOString(), new Date(windowEndMs).toISOString());
|
|
16824
|
+
if (log.succeeded) {
|
|
16825
|
+
await mkdir5(join32(outDir, "repo"), { recursive: true });
|
|
16826
|
+
await writeFile9(join32(outDir, "repo", "git-log.txt"), log.stdout ? `${log.stdout}
|
|
16827
|
+
` : "", "utf8");
|
|
16828
|
+
} else {
|
|
16829
|
+
absent.push({ path: gitLogPath, reason: `git log failed in ${ref.cwd}: ${log.error}` });
|
|
16830
|
+
}
|
|
16831
|
+
}
|
|
16832
|
+
const files = await hashSessionBundleFiles(outDir);
|
|
16833
|
+
const manifest = {
|
|
16834
|
+
schemaVersion: 2,
|
|
16835
|
+
kind: "traces.session_bundle",
|
|
16836
|
+
view: "full",
|
|
16837
|
+
createdAt: generatedAt,
|
|
16838
|
+
provenance: {
|
|
16839
|
+
sessionId: ref.sessionId,
|
|
16840
|
+
harness: ref.harness,
|
|
16841
|
+
cwd: ref.cwd,
|
|
16842
|
+
transcriptPath: resolve8(ref.path),
|
|
16843
|
+
transcriptSha256,
|
|
16844
|
+
contextRoot,
|
|
16845
|
+
tracesVersion: tracesPackageVersion(),
|
|
16846
|
+
sessionWindow: { firstSpanAt, lastSpanAt, padMs: SESSION_WINDOW_PAD_MS }
|
|
16847
|
+
},
|
|
16848
|
+
files,
|
|
16849
|
+
absent,
|
|
16850
|
+
// The full view is the whole record: it drops nothing, and says so rather
|
|
16851
|
+
// than leaving a reader to infer completeness from a missing field.
|
|
16852
|
+
excluded: [],
|
|
16853
|
+
ledgerSlices: slices,
|
|
16854
|
+
knownLimits: KNOWN_LIMITS
|
|
16855
|
+
};
|
|
16856
|
+
const manifestPath = await writeSessionBundleManifest(outDir, manifest);
|
|
16857
|
+
return { directory: outDir, manifestPath, manifest };
|
|
16858
|
+
}
|
|
16859
|
+
async function hashSessionBundleFiles(root) {
|
|
16860
|
+
const files = [];
|
|
16861
|
+
for (const path of await listSessionBundleFiles(root)) {
|
|
16862
|
+
const bytes = await readFile14(join32(root, path));
|
|
16863
|
+
files.push({ path, bytes: bytes.length, sha256: sha256Hex(bytes) });
|
|
16864
|
+
}
|
|
16865
|
+
return files;
|
|
16866
|
+
}
|
|
16867
|
+
async function writeSessionBundleManifest(outDir, manifest) {
|
|
16868
|
+
const manifestPath = join32(outDir, "manifest.json");
|
|
16869
|
+
await writeFile9(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
16870
|
+
`, "utf8");
|
|
16871
|
+
return manifestPath;
|
|
16872
|
+
}
|
|
16873
|
+
async function readSessionBundleManifest(bundleDir) {
|
|
16874
|
+
const path = join32(resolve8(bundleDir), "manifest.json");
|
|
16875
|
+
let text2;
|
|
16876
|
+
try {
|
|
16877
|
+
text2 = await readFile14(path, "utf8");
|
|
16878
|
+
} catch (error) {
|
|
16879
|
+
if (isMissingPathError(error)) {
|
|
16880
|
+
throw new Error(`no manifest.json at ${path} \u2014 ${bundleDir} is not a traces session bundle`);
|
|
16881
|
+
}
|
|
16882
|
+
throw error;
|
|
16883
|
+
}
|
|
16884
|
+
let parsed;
|
|
16885
|
+
try {
|
|
16886
|
+
parsed = JSON.parse(text2);
|
|
16887
|
+
} catch (error) {
|
|
16888
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
16889
|
+
}
|
|
16890
|
+
const manifest = parsed;
|
|
16891
|
+
if (manifest?.kind !== "traces.session_bundle") {
|
|
16892
|
+
throw new Error(`${path} has kind ${JSON.stringify(manifest?.kind)}, expected "traces.session_bundle"`);
|
|
16893
|
+
}
|
|
16894
|
+
if (manifest.schemaVersion !== 2) {
|
|
16895
|
+
throw new Error(
|
|
16896
|
+
`${path} is bundle schemaVersion ${JSON.stringify(manifest.schemaVersion)}; this traces reads 2. Re-run \`traces bundle\` to assemble it with the view-aware manifest.`
|
|
16897
|
+
);
|
|
16898
|
+
}
|
|
16899
|
+
if (manifest.view !== "full" && manifest.view !== "evidence-only") {
|
|
16900
|
+
throw new Error(`${path} has view ${JSON.stringify(manifest.view)}, expected "full" or "evidence-only"`);
|
|
16901
|
+
}
|
|
16902
|
+
return manifest;
|
|
16903
|
+
}
|
|
16904
|
+
var cachedVersion;
|
|
16905
|
+
function tracesPackageVersion() {
|
|
16906
|
+
if (cachedVersion) return cachedVersion;
|
|
16907
|
+
const pkg = JSON.parse(
|
|
16908
|
+
readFileSync5(new URL("../package.json", import.meta.url), "utf8")
|
|
16909
|
+
);
|
|
16910
|
+
if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
|
|
16911
|
+
cachedVersion = pkg.version;
|
|
16912
|
+
return cachedVersion;
|
|
16913
|
+
}
|
|
16914
|
+
|
|
16915
|
+
// src/bundle-view.ts
|
|
16916
|
+
import { createHash as createHash15 } from "crypto";
|
|
16917
|
+
import { mkdir as mkdir6, readdir as readdir20, readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
|
|
16918
|
+
import { dirname as dirname9, join as join33, resolve as resolve9 } from "path";
|
|
16919
|
+
var EVIDENCE_ONLY_ALLOW_LIST = [
|
|
16920
|
+
{
|
|
16921
|
+
path: "derived/session-index.json",
|
|
16922
|
+
owned: true,
|
|
16923
|
+
carries: "per-session counts, tool names, models, and nearby-context file metadata"
|
|
16924
|
+
},
|
|
16925
|
+
{
|
|
16926
|
+
path: "derived/evidence.jsonl",
|
|
16927
|
+
owned: true,
|
|
16928
|
+
carries: "execution statistics, token and tool totals, loop and error counts"
|
|
16929
|
+
},
|
|
16930
|
+
{
|
|
16931
|
+
path: "ledger/current.json",
|
|
16932
|
+
owned: false,
|
|
16933
|
+
carries: "the repo ledger current-state record"
|
|
16934
|
+
},
|
|
16935
|
+
{
|
|
16936
|
+
path: "ledger/scorecard.json",
|
|
16937
|
+
owned: false,
|
|
16938
|
+
carries: "the repo ledger scorecard rows"
|
|
16939
|
+
},
|
|
16940
|
+
{
|
|
16941
|
+
path: "ledger/skill-runs.jsonl",
|
|
16942
|
+
owned: false,
|
|
16943
|
+
carries: "skill-run rows: skill name, timestamp, and outcome fields"
|
|
16944
|
+
}
|
|
16945
|
+
];
|
|
16946
|
+
var EXCLUSION_RULES = [
|
|
16947
|
+
{
|
|
16948
|
+
matches: (path) => path === "session/transcript.jsonl" || path.startsWith("session/"),
|
|
16949
|
+
rule: "session-source",
|
|
16950
|
+
reason: "raw session text: the transcript and any subagent transcript hold every word the session wrote, including the shell call that authored an earlier report"
|
|
16951
|
+
},
|
|
16952
|
+
{
|
|
16953
|
+
matches: (path) => path === "derived/report.md",
|
|
16954
|
+
rule: "session-text-derived",
|
|
16955
|
+
reason: "the deterministic report quotes the session verbatim: the first prompt line and evidence excerpts per finding"
|
|
16956
|
+
},
|
|
16957
|
+
{
|
|
16958
|
+
matches: (path) => path === "derived/trace.otlp.jsonl",
|
|
16959
|
+
rule: "session-text-derived",
|
|
16960
|
+
reason: "spans carry input.value, output.value and content: the full prompt and response text of every turn"
|
|
16961
|
+
},
|
|
16962
|
+
{
|
|
16963
|
+
matches: (path) => path === "ledger/progress.md",
|
|
16964
|
+
rule: "authored-prose",
|
|
16965
|
+
reason: "progress notes are written at session close and restate what the session concluded"
|
|
16966
|
+
},
|
|
16967
|
+
{
|
|
16968
|
+
matches: (path) => /^ledger\/handoff-.*\.md$/.test(path),
|
|
16969
|
+
rule: "authored-prose",
|
|
16970
|
+
reason: "a handoff is the session conclusion in prose"
|
|
16971
|
+
},
|
|
16972
|
+
{
|
|
16973
|
+
matches: (path) => path.startsWith("ledger/reflections/"),
|
|
16974
|
+
rule: "authored-prose",
|
|
16975
|
+
reason: "a reflection is the session conclusion in prose"
|
|
16976
|
+
},
|
|
16977
|
+
{
|
|
16978
|
+
matches: (path) => path === "ledger/experiments.jsonl",
|
|
16979
|
+
rule: "authored-prose",
|
|
16980
|
+
reason: "experiment rows carry free-text verdict and note fields that restate what the session concluded"
|
|
16981
|
+
},
|
|
16982
|
+
{
|
|
16983
|
+
matches: (path) => path === "repo/git-log.txt",
|
|
16984
|
+
rule: "authored-prose",
|
|
16985
|
+
reason: "commit subjects are prose the session authored"
|
|
16986
|
+
},
|
|
16987
|
+
{
|
|
16988
|
+
matches: () => true,
|
|
16989
|
+
rule: "not-allow-listed",
|
|
16990
|
+
reason: "the evidence-only view carries an explicit allow-list of counted artifacts, and this path is not on it"
|
|
16991
|
+
}
|
|
16992
|
+
];
|
|
16993
|
+
var SIGNATURE_WORDS = 8;
|
|
16994
|
+
var MIN_WORD_TOKENS = 7;
|
|
16995
|
+
function scrubIdentifiers(line) {
|
|
16996
|
+
return line.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, " ").replace(/\/?(?:[\w.@+~-]+\/)+[\w.@+~-]*/g, " ").replace(/\b[0-9a-f-]{8,}\b/gi, " ");
|
|
16997
|
+
}
|
|
16998
|
+
var PACKAGE_CONSTANT_PROSE = [POLICY_EVIDENCE_NOTE];
|
|
16999
|
+
function stripPackageConstants(text2) {
|
|
17000
|
+
let out = text2;
|
|
17001
|
+
for (const constant of PACKAGE_CONSTANT_PROSE) out = out.split(constant).join(" ");
|
|
17002
|
+
return out;
|
|
17003
|
+
}
|
|
17004
|
+
function collectJsonStrings(value, into) {
|
|
17005
|
+
if (typeof value === "string") into.push(value);
|
|
17006
|
+
else if (Array.isArray(value)) for (const item of value) collectJsonStrings(item, into);
|
|
17007
|
+
else if (value !== null && typeof value === "object") {
|
|
17008
|
+
for (const item of Object.values(value)) collectJsonStrings(item, into);
|
|
17009
|
+
}
|
|
17010
|
+
}
|
|
17011
|
+
function proseFragments(line) {
|
|
17012
|
+
const trimmed = line.trim();
|
|
17013
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
17014
|
+
try {
|
|
17015
|
+
const parsed = JSON.parse(trimmed);
|
|
17016
|
+
const strings = [];
|
|
17017
|
+
collectJsonStrings(parsed, strings);
|
|
17018
|
+
return strings;
|
|
17019
|
+
} catch {
|
|
17020
|
+
}
|
|
17021
|
+
}
|
|
17022
|
+
return [line];
|
|
17023
|
+
}
|
|
17024
|
+
function sessionTextSignatures(text2) {
|
|
17025
|
+
const out = /* @__PURE__ */ new Set();
|
|
17026
|
+
for (const line of stripPackageConstants(text2).split("\n")) {
|
|
17027
|
+
for (const fragment of proseFragments(line)) {
|
|
17028
|
+
const words = scrubIdentifiers(fragment).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean);
|
|
17029
|
+
for (let i = 0; i + SIGNATURE_WORDS <= words.length; i++) {
|
|
17030
|
+
const window = words.slice(i, i + SIGNATURE_WORDS);
|
|
17031
|
+
if (window.filter((word) => /^[a-z]+$/.test(word)).length < MIN_WORD_TOKENS) continue;
|
|
17032
|
+
out.add(window.join(" "));
|
|
17033
|
+
}
|
|
17034
|
+
}
|
|
17035
|
+
}
|
|
17036
|
+
return out;
|
|
17037
|
+
}
|
|
17038
|
+
async function findSessionTextLeaks(opts) {
|
|
17039
|
+
const index = /* @__PURE__ */ new Map();
|
|
17040
|
+
const carried = [];
|
|
17041
|
+
for (const path of opts.carriedPaths) {
|
|
17042
|
+
const signatures = sessionTextSignatures(await readFile15(join33(opts.viewDir, path), "utf8"));
|
|
17043
|
+
carried.push({ path, signatures: signatures.size });
|
|
17044
|
+
for (const signature of signatures) if (!index.has(signature)) index.set(signature, path);
|
|
17045
|
+
}
|
|
17046
|
+
const hits = /* @__PURE__ */ new Map();
|
|
17047
|
+
const comparedSources = [];
|
|
17048
|
+
let sourceSignatures = 0;
|
|
17049
|
+
for (const sourcePath of opts.excludedPaths) {
|
|
17050
|
+
const signatures = sessionTextSignatures(await readFile15(join33(opts.sourceDir, sourcePath), "utf8"));
|
|
17051
|
+
comparedSources.push(sourcePath);
|
|
17052
|
+
sourceSignatures += signatures.size;
|
|
17053
|
+
for (const signature of signatures) {
|
|
17054
|
+
const viewPath = index.get(signature);
|
|
17055
|
+
if (viewPath === void 0) continue;
|
|
17056
|
+
const hit = hits.get(viewPath) ?? { matched: /* @__PURE__ */ new Set(), sources: /* @__PURE__ */ new Set(), example: signature };
|
|
17057
|
+
hit.matched.add(signature);
|
|
17058
|
+
hit.sources.add(sourcePath);
|
|
17059
|
+
hits.set(viewPath, hit);
|
|
17060
|
+
}
|
|
17061
|
+
}
|
|
17062
|
+
const leaks = [...hits].map(([viewPath, hit]) => ({
|
|
17063
|
+
viewPath,
|
|
17064
|
+
sources: [...hit.sources].sort(),
|
|
17065
|
+
signatures: hit.matched.size,
|
|
17066
|
+
example: hit.example
|
|
17067
|
+
}));
|
|
17068
|
+
return { leaks, carried, comparedSources, sourceSignatures };
|
|
17069
|
+
}
|
|
17070
|
+
function classifyEvidenceOnlyPath(path) {
|
|
17071
|
+
const allowed = EVIDENCE_ONLY_ALLOW_LIST.find((entry) => entry.path === path);
|
|
17072
|
+
if (allowed) return { carried: true, owned: allowed.owned, carries: allowed.carries };
|
|
17073
|
+
const rule = EXCLUSION_RULES.find((entry) => entry.matches(path));
|
|
17074
|
+
return { carried: false, rule: rule.rule, reason: rule.reason };
|
|
17075
|
+
}
|
|
17076
|
+
var EVIDENCE_ONLY_LIMITS = [
|
|
17077
|
+
"the exclusion is verbatim-tight, not paraphrase-tight: a conclusion restated in different words inside a carried artifact is not detectable by content signatures",
|
|
17078
|
+
"ledger prose (progress, handoffs, reflections) and experiment free-text fields are excluded whole; a measured experiment result is readable only from the full bundle, under audit rules",
|
|
17079
|
+
"the view carries counts, names, identifiers, timestamps and hashes; a question that needs the words of a turn cannot be answered from it"
|
|
17080
|
+
];
|
|
17081
|
+
async function projectSessionBundle(opts) {
|
|
17082
|
+
if (opts.view !== "evidence-only") {
|
|
17083
|
+
throw new Error(
|
|
17084
|
+
`unsupported bundle view ${JSON.stringify(opts.view)}. The full view IS the bundle \`traces bundle\` assembles, so there is nothing to project into it; the projectable view is "evidence-only".`
|
|
17085
|
+
);
|
|
17086
|
+
}
|
|
17087
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
17088
|
+
const sourceDir = resolve9(opts.bundleDir);
|
|
17089
|
+
const source = await readSessionBundleManifest(sourceDir);
|
|
17090
|
+
if (source.view !== "full") {
|
|
17091
|
+
throw new Error(
|
|
17092
|
+
`${sourceDir} is already the ${source.view} view \u2014 project from the full bundle, not from a view of it`
|
|
17093
|
+
);
|
|
17094
|
+
}
|
|
17095
|
+
const outDir = resolve9(opts.outDir);
|
|
17096
|
+
await mkdir6(outDir, { recursive: true });
|
|
17097
|
+
if ((await readdir20(outDir)).length > 0) {
|
|
17098
|
+
throw new Error(
|
|
17099
|
+
`view output directory ${outDir} is not empty \u2014 pass a new directory so one directory holds exactly one view`
|
|
17100
|
+
);
|
|
17101
|
+
}
|
|
17102
|
+
const sourceHashes = new Map(source.files.map((file) => [file.path, file]));
|
|
17103
|
+
const carriedPaths = [];
|
|
17104
|
+
const carries = /* @__PURE__ */ new Map();
|
|
17105
|
+
const excluded = [];
|
|
17106
|
+
const candidates = [];
|
|
17107
|
+
for (const path of await listSessionBundleFiles(sourceDir)) {
|
|
17108
|
+
if (path === "manifest.json") continue;
|
|
17109
|
+
const bytes = await readFile15(join33(sourceDir, path));
|
|
17110
|
+
const sha2562 = createHash15("sha256").update(bytes).digest("hex");
|
|
17111
|
+
const recorded = sourceHashes.get(path);
|
|
17112
|
+
if (!recorded) {
|
|
17113
|
+
throw new Error(
|
|
17114
|
+
`${join33(sourceDir, path)} is not listed in the bundle manifest \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
|
|
17115
|
+
);
|
|
17116
|
+
}
|
|
17117
|
+
if (recorded.sha256 !== sha2562) {
|
|
17118
|
+
throw new Error(
|
|
17119
|
+
`${join33(sourceDir, path)} does not match its manifest hash (${recorded.sha256} recorded, ${sha2562} on disk) \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
|
|
17120
|
+
);
|
|
17121
|
+
}
|
|
17122
|
+
const verdict = classifyEvidenceOnlyPath(path);
|
|
17123
|
+
if (!verdict.carried) {
|
|
17124
|
+
excluded.push({ path, bytes: bytes.length, sha256: sha2562, rule: verdict.rule, reason: verdict.reason });
|
|
17125
|
+
continue;
|
|
17126
|
+
}
|
|
17127
|
+
candidates.push({ path, bytes, sha256: sha2562, carries: verdict.carries, owned: verdict.owned });
|
|
17128
|
+
}
|
|
17129
|
+
const report = await findSessionTextLeaks({
|
|
17130
|
+
viewDir: sourceDir,
|
|
17131
|
+
sourceDir,
|
|
17132
|
+
carriedPaths: candidates.map((candidate) => candidate.path),
|
|
17133
|
+
excludedPaths: excluded.map((entry) => entry.path)
|
|
17134
|
+
});
|
|
17135
|
+
const leakByPath = new Map(report.leaks.map((leak) => [leak.viewPath, leak]));
|
|
17136
|
+
const ownedLeak = report.leaks.find((leak) => candidates.some((c) => c.path === leak.viewPath && c.owned));
|
|
17137
|
+
if (ownedLeak) {
|
|
17138
|
+
throw new Error(
|
|
17139
|
+
`${ownedLeak.viewPath} repeats ${ownedLeak.signatures} ${SIGNATURE_WORDS}-word run(s) found in ${ownedLeak.sources.join(", ")}, for example "${ownedLeak.example}". That artifact is derived by traces and must carry counts only, so this is a defect in the derivation, not a property of the session. No view was written.`
|
|
17140
|
+
);
|
|
17141
|
+
}
|
|
17142
|
+
for (const candidate of candidates) {
|
|
17143
|
+
const leak = leakByPath.get(candidate.path);
|
|
17144
|
+
if (leak) {
|
|
17145
|
+
excluded.push({
|
|
17146
|
+
path: candidate.path,
|
|
17147
|
+
bytes: candidate.bytes.length,
|
|
17148
|
+
sha256: candidate.sha256,
|
|
17149
|
+
rule: "content-signature",
|
|
17150
|
+
reason: `the content check found ${leak.signatures} ${SIGNATURE_WORDS}-word run(s) this file shares with ${leak.sources.join(", ")}; carrying it would put back text the view removes`,
|
|
17151
|
+
contentMatch: { signatures: leak.signatures, sources: leak.sources }
|
|
17152
|
+
});
|
|
17153
|
+
continue;
|
|
17154
|
+
}
|
|
17155
|
+
const target = join33(outDir, candidate.path);
|
|
17156
|
+
await mkdir6(dirname9(target), { recursive: true });
|
|
17157
|
+
await writeFile10(target, candidate.bytes);
|
|
17158
|
+
carriedPaths.push(candidate.path);
|
|
17159
|
+
carries.set(candidate.path, candidate.carries);
|
|
17160
|
+
}
|
|
17161
|
+
const leakCheck = {
|
|
17162
|
+
signatureWords: SIGNATURE_WORDS,
|
|
17163
|
+
comparedSources: report.comparedSources,
|
|
17164
|
+
sourceSignatures: report.sourceSignatures,
|
|
17165
|
+
carried: report.carried.filter((entry) => carries.has(entry.path)).map((entry) => ({
|
|
17166
|
+
path: entry.path,
|
|
17167
|
+
carries: carries.get(entry.path),
|
|
17168
|
+
signatures: entry.signatures
|
|
17169
|
+
})),
|
|
17170
|
+
matches: 0
|
|
17171
|
+
};
|
|
17172
|
+
const sourceManifestBytes = await readFile15(join33(sourceDir, "manifest.json"));
|
|
17173
|
+
const manifest = {
|
|
17174
|
+
schemaVersion: 2,
|
|
17175
|
+
kind: "traces.session_bundle",
|
|
17176
|
+
view: "evidence-only",
|
|
17177
|
+
createdAt: generatedAt,
|
|
17178
|
+
provenance: source.provenance,
|
|
17179
|
+
files: await hashSessionBundleFiles(outDir),
|
|
17180
|
+
absent: source.absent,
|
|
17181
|
+
excluded,
|
|
17182
|
+
ledgerSlices: source.ledgerSlices.filter((slice) => carriedPaths.includes(slice.path)),
|
|
17183
|
+
knownLimits: [...source.knownLimits, ...EVIDENCE_ONLY_LIMITS],
|
|
17184
|
+
projection: {
|
|
17185
|
+
sourceDirectory: sourceDir,
|
|
17186
|
+
sourceManifestSha256: createHash15("sha256").update(sourceManifestBytes).digest("hex"),
|
|
17187
|
+
leakCheck
|
|
17188
|
+
}
|
|
17189
|
+
};
|
|
17190
|
+
const manifestPath = await writeSessionBundleManifest(outDir, manifest);
|
|
17191
|
+
return { directory: outDir, manifestPath, manifest, leakCheck };
|
|
17192
|
+
}
|
|
17193
|
+
|
|
15872
17194
|
// src/upload-state.ts
|
|
15873
|
-
import { mkdir as
|
|
17195
|
+
import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
15874
17196
|
import { homedir as homedir12 } from "os";
|
|
15875
|
-
import { dirname as
|
|
17197
|
+
import { dirname as dirname10, join as join34 } from "path";
|
|
15876
17198
|
import { contentHash } from "@tangle-network/agent-eval";
|
|
15877
17199
|
function stateFile() {
|
|
15878
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
15879
|
-
return
|
|
17200
|
+
const base = process.env.XDG_STATE_HOME ?? join34(homedir12(), ".local", "state");
|
|
17201
|
+
return join34(base, "tangle-traces", "uploaded.json");
|
|
15880
17202
|
}
|
|
15881
17203
|
function uploadKey(harness, sessionId) {
|
|
15882
17204
|
return `${harness}:${sessionId}`;
|
|
@@ -15889,15 +17211,15 @@ function outboundHash(events, identity) {
|
|
|
15889
17211
|
}
|
|
15890
17212
|
async function loadState() {
|
|
15891
17213
|
try {
|
|
15892
|
-
return JSON.parse(await
|
|
17214
|
+
return JSON.parse(await readFile16(stateFile(), "utf8"));
|
|
15893
17215
|
} catch {
|
|
15894
17216
|
return {};
|
|
15895
17217
|
}
|
|
15896
17218
|
}
|
|
15897
17219
|
async function saveState(state) {
|
|
15898
17220
|
const f = stateFile();
|
|
15899
|
-
await
|
|
15900
|
-
await
|
|
17221
|
+
await mkdir7(dirname10(f), { recursive: true });
|
|
17222
|
+
await writeFile11(f, `${JSON.stringify(state, null, 2)}
|
|
15901
17223
|
`, "utf8");
|
|
15902
17224
|
}
|
|
15903
17225
|
function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
@@ -15905,10 +17227,10 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
|
15905
17227
|
}
|
|
15906
17228
|
|
|
15907
17229
|
// src/upload.ts
|
|
15908
|
-
import { readFile as
|
|
17230
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
15909
17231
|
import { createRequire as createRequire2 } from "module";
|
|
15910
17232
|
import { hostname as hostname2 } from "os";
|
|
15911
|
-
import { basename as basename15, join as
|
|
17233
|
+
import { basename as basename15, join as join35 } from "path";
|
|
15912
17234
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
15913
17235
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
15914
17236
|
var require2 = createRequire2(import.meta.url);
|
|
@@ -15932,7 +17254,7 @@ async function planUpload(opts = {}) {
|
|
|
15932
17254
|
async function gitBranch(cwd) {
|
|
15933
17255
|
if (!cwd) return void 0;
|
|
15934
17256
|
try {
|
|
15935
|
-
const head = await
|
|
17257
|
+
const head = await readFile17(join35(cwd, ".git", "HEAD"), "utf8");
|
|
15936
17258
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
15937
17259
|
return m?.[1]?.trim();
|
|
15938
17260
|
} catch {
|
|
@@ -15941,16 +17263,16 @@ async function gitBranch(cwd) {
|
|
|
15941
17263
|
}
|
|
15942
17264
|
async function sessionMeta(item) {
|
|
15943
17265
|
const meta = {
|
|
15944
|
-
[
|
|
15945
|
-
[
|
|
15946
|
-
[
|
|
15947
|
-
[
|
|
15948
|
-
[
|
|
15949
|
-
[
|
|
15950
|
-
};
|
|
15951
|
-
if (item.ref.cwd) meta[
|
|
17266
|
+
[ATTR.HARNESS]: item.ref.harness,
|
|
17267
|
+
[ATTR.SESSION_FILE]: basename15(item.ref.path),
|
|
17268
|
+
[ATTR.HOST]: hostname2(),
|
|
17269
|
+
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
17270
|
+
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
17271
|
+
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
17272
|
+
};
|
|
17273
|
+
if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
|
|
15952
17274
|
const branch = await gitBranch(item.ref.cwd);
|
|
15953
|
-
if (branch) meta[
|
|
17275
|
+
if (branch) meta[ATTR.GIT_BRANCH] = branch;
|
|
15954
17276
|
return meta;
|
|
15955
17277
|
}
|
|
15956
17278
|
var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
|
|
@@ -15961,8 +17283,8 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
15961
17283
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes2[k] = v;
|
|
15962
17284
|
else if (v != null) attributes2[k] = JSON.stringify(v);
|
|
15963
17285
|
}
|
|
15964
|
-
attributes2[
|
|
15965
|
-
attributes2[
|
|
17286
|
+
attributes2[ATTR.SESSION_ID] = s.trace_id;
|
|
17287
|
+
attributes2[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
|
|
15966
17288
|
if (s.parent_span_id === null) Object.assign(attributes2, rootMeta);
|
|
15967
17289
|
const startMs = parseIsoToEpochMs(s.start_time);
|
|
15968
17290
|
const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
|
|
@@ -16021,7 +17343,7 @@ async function prepareOutbound(plan, opts, uploadedAt) {
|
|
|
16021
17343
|
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
16022
17344
|
const previous = plan.state[key];
|
|
16023
17345
|
const meta = await sessionMeta(item);
|
|
16024
|
-
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [
|
|
17346
|
+
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
|
|
16025
17347
|
let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
|
|
16026
17348
|
let events = eventsAt(eventTimestamp2);
|
|
16027
17349
|
let hash2 = outboundHash(events, identity);
|
|
@@ -16099,6 +17421,15 @@ async function executeUpload(plan, opts = {}) {
|
|
|
16099
17421
|
|
|
16100
17422
|
export {
|
|
16101
17423
|
appendAll,
|
|
17424
|
+
ATTR,
|
|
17425
|
+
INGEST_SOURCE_CLI,
|
|
17426
|
+
environmentAttributes,
|
|
17427
|
+
stampEnvironmentAttrs,
|
|
17428
|
+
environmentFromSpanAttributes,
|
|
17429
|
+
DEFAULT_HARNESS,
|
|
17430
|
+
sessionIdFromAttributes,
|
|
17431
|
+
indexSessionIdsByTrace,
|
|
17432
|
+
stampSessionIdentity,
|
|
16102
17433
|
parseIsoToEpochMs,
|
|
16103
17434
|
parseSince,
|
|
16104
17435
|
resolveOtlpInputFiles,
|
|
@@ -16115,18 +17446,13 @@ export {
|
|
|
16115
17446
|
capText,
|
|
16116
17447
|
ACTOR_ATTR,
|
|
16117
17448
|
userPromptSpan,
|
|
16118
|
-
ATTR2 as ATTR,
|
|
16119
|
-
INGEST_SOURCE_CLI,
|
|
16120
|
-
DEFAULT_HARNESS,
|
|
16121
|
-
sessionIdFromAttributes,
|
|
16122
|
-
indexSessionIdsByTrace,
|
|
16123
|
-
stampSessionIdentity,
|
|
16124
17449
|
isChatTrajectoryMessage,
|
|
16125
17450
|
isChatTrajectory,
|
|
16126
17451
|
chatTrajectoryToSpans,
|
|
16127
17452
|
importCodeTraceBench,
|
|
16128
17453
|
summarizeOtlpExecution,
|
|
16129
17454
|
summarizeSpanExecution,
|
|
17455
|
+
classifyFailureFollowUps,
|
|
16130
17456
|
toRuntimeStore,
|
|
16131
17457
|
runPipelines,
|
|
16132
17458
|
recordSessionCorruption,
|
|
@@ -16162,6 +17488,7 @@ export {
|
|
|
16162
17488
|
selectAdapters,
|
|
16163
17489
|
normalizeRemote,
|
|
16164
17490
|
equivalentGitCwds,
|
|
17491
|
+
cwdMatchesSelectionResolved,
|
|
16165
17492
|
cwdMatchesSelection,
|
|
16166
17493
|
resolveRepoAttrs,
|
|
16167
17494
|
resolveSessionRepoAttrs,
|
|
@@ -16173,6 +17500,7 @@ export {
|
|
|
16173
17500
|
scanSessions,
|
|
16174
17501
|
SessionWorkflowError,
|
|
16175
17502
|
describeSessionRelationship,
|
|
17503
|
+
POLICY_EVIDENCE_NOTE,
|
|
16176
17504
|
buildPolicyEvidenceRecord,
|
|
16177
17505
|
collectPolicyEvidence,
|
|
16178
17506
|
serializePolicyEvidence,
|
|
@@ -16233,6 +17561,8 @@ export {
|
|
|
16233
17561
|
commandRedactor,
|
|
16234
17562
|
writeHodoscopeInput,
|
|
16235
17563
|
hodoscopeAnalyzer,
|
|
17564
|
+
httpJsonTransport,
|
|
17565
|
+
primeAnalyzer,
|
|
16236
17566
|
CODING_REDACTION_RULES,
|
|
16237
17567
|
TRACES_REDACTION_RULES,
|
|
16238
17568
|
redactSpans,
|
|
@@ -16263,7 +17593,7 @@ export {
|
|
|
16263
17593
|
analyzeSteeringChain,
|
|
16264
17594
|
REACTIONS,
|
|
16265
17595
|
analyzeReactions,
|
|
16266
|
-
|
|
17596
|
+
sessionReportSource,
|
|
16267
17597
|
buildTraceFindingPacket,
|
|
16268
17598
|
loadTracesConfig,
|
|
16269
17599
|
mergeTracesConfig,
|
|
@@ -16320,10 +17650,20 @@ export {
|
|
|
16320
17650
|
collectSessionWorkflow,
|
|
16321
17651
|
collectSessionSelection,
|
|
16322
17652
|
buildSessionIndex,
|
|
17653
|
+
findContextRoot,
|
|
16323
17654
|
buildSessionIndexFromRows,
|
|
16324
17655
|
collectSessionIndex,
|
|
16325
17656
|
serializeSessionIndex,
|
|
16326
17657
|
writeSessionIndexFile,
|
|
17658
|
+
listSessionBundleFiles,
|
|
17659
|
+
assembleSessionBundle,
|
|
17660
|
+
hashSessionBundleFiles,
|
|
17661
|
+
writeSessionBundleManifest,
|
|
17662
|
+
readSessionBundleManifest,
|
|
17663
|
+
sessionTextSignatures,
|
|
17664
|
+
findSessionTextLeaks,
|
|
17665
|
+
classifyEvidenceOnlyPath,
|
|
17666
|
+
projectSessionBundle,
|
|
16327
17667
|
uploadKey,
|
|
16328
17668
|
sessionHash,
|
|
16329
17669
|
outboundHash,
|