@tangle-network/traces 0.11.5 → 0.11.6
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-MEAEXKUV.js} +1496 -271
- package/dist/cli.js +163 -61
- package/dist/index.d.ts +451 -1
- package/dist/index.js +33 -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) {
|
|
@@ -1907,10 +1960,10 @@ function sessionJsonlOptions(ref, options = {}) {
|
|
|
1907
1960
|
function sessionIntegrityAttributes(ref) {
|
|
1908
1961
|
if (!ref.integrity) return {};
|
|
1909
1962
|
return {
|
|
1910
|
-
[
|
|
1911
|
-
[
|
|
1912
|
-
[
|
|
1913
|
-
[
|
|
1963
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
1964
|
+
[ATTR.CORRUPTION_COUNT]: ref.integrity.corruptions.length,
|
|
1965
|
+
[ATTR.CORRUPTION_DIGEST]: corruptionDigest(ref.integrity.corruptions),
|
|
1966
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
1914
1967
|
};
|
|
1915
1968
|
}
|
|
1916
1969
|
function corruptionDigest(receipts) {
|
|
@@ -1951,15 +2004,15 @@ function stampSessionIntegrity(ref, spans) {
|
|
|
1951
2004
|
status: "ERROR",
|
|
1952
2005
|
service: typeof parent.attributes["service.name"] === "string" ? parent.attributes["service.name"] : ref.harness,
|
|
1953
2006
|
extra: {
|
|
1954
|
-
[
|
|
1955
|
-
[
|
|
1956
|
-
[
|
|
1957
|
-
[
|
|
1958
|
-
[
|
|
1959
|
-
[
|
|
1960
|
-
[
|
|
1961
|
-
[
|
|
1962
|
-
[
|
|
2007
|
+
[ATTR.SESSION_INTEGRITY]: ref.integrity.status,
|
|
2008
|
+
[ATTR.CORRUPTION_RECEIPT_VERSION]: receipt.receiptVersion,
|
|
2009
|
+
[ATTR.CORRUPTION_RECEIPT_KIND]: receipt.kind,
|
|
2010
|
+
[ATTR.CORRUPTION_SOURCE_PATH]: receipt.sourcePath,
|
|
2011
|
+
[ATTR.CORRUPTION_LINE_NUMBER]: receipt.lineNumber,
|
|
2012
|
+
[ATTR.CORRUPTION_BYTE_OFFSET]: receipt.byteOffset,
|
|
2013
|
+
[ATTR.CORRUPTION_BYTE_LENGTH]: receipt.byteLength,
|
|
2014
|
+
[ATTR.CORRUPTION_SHA256]: receipt.sha256,
|
|
2015
|
+
[ATTR.RAW_SOURCE_RETENTION]: "local_source_only"
|
|
1963
2016
|
}
|
|
1964
2017
|
}));
|
|
1965
2018
|
existingSpanIds.add(spanId);
|
|
@@ -6236,7 +6289,7 @@ function extractAbsolutePaths(spans) {
|
|
|
6236
6289
|
}
|
|
6237
6290
|
async function resolveRepoAttrs(cwd) {
|
|
6238
6291
|
if (!cwd) return {};
|
|
6239
|
-
const attrs = { [
|
|
6292
|
+
const attrs = { [ATTR.CWD]: cwd };
|
|
6240
6293
|
const basename16 = (() => {
|
|
6241
6294
|
const trimmed = cwd.replace(/\/+$/g, "");
|
|
6242
6295
|
const idx = trimmed.lastIndexOf("/");
|
|
@@ -6249,17 +6302,17 @@ async function resolveRepoAttrs(cwd) {
|
|
|
6249
6302
|
const { remote, branch, commit } = await readGit(gitRoot);
|
|
6250
6303
|
const normalized = remote ? normalizeRemote(remote) : null;
|
|
6251
6304
|
if (normalized) {
|
|
6252
|
-
attrs[
|
|
6253
|
-
attrs[
|
|
6254
|
-
if (branch) attrs[
|
|
6255
|
-
if (commit) attrs[
|
|
6305
|
+
attrs[ATTR.SUBJECT_KEY] = normalized;
|
|
6306
|
+
attrs[ATTR.GIT_REPOSITORY] = normalized;
|
|
6307
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
6308
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
6256
6309
|
return attrs;
|
|
6257
6310
|
}
|
|
6258
|
-
if (branch) attrs[
|
|
6259
|
-
if (commit) attrs[
|
|
6311
|
+
if (branch) attrs[ATTR.GIT_BRANCH_NAME] = branch;
|
|
6312
|
+
if (commit) attrs[ATTR.GIT_COMMIT] = commit;
|
|
6260
6313
|
} catch {
|
|
6261
6314
|
}
|
|
6262
|
-
attrs[
|
|
6315
|
+
attrs[ATTR.SUBJECT_KEY] = basename16;
|
|
6263
6316
|
return attrs;
|
|
6264
6317
|
}
|
|
6265
6318
|
async function repoCandidateForPath(path) {
|
|
@@ -6290,12 +6343,12 @@ async function resolveSessionRepoAttrs(cwd, spans) {
|
|
|
6290
6343
|
resolved.push({ ...candidate, attrs });
|
|
6291
6344
|
}
|
|
6292
6345
|
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[
|
|
6346
|
+
const withGit = resolved.filter((candidate) => candidate.attrs[ATTR.GIT_REPOSITORY]).sort(byScore);
|
|
6347
|
+
const selected = withGit[0] ?? resolved.filter((candidate) => candidate.attrs[ATTR.SUBJECT_KEY]).sort(byScore)[0];
|
|
6295
6348
|
if (!selected) return { attrs: {}, cwd: null, source: "none" };
|
|
6296
6349
|
return {
|
|
6297
|
-
attrs: { ...selected.attrs, [
|
|
6298
|
-
cwd: selected.attrs[
|
|
6350
|
+
attrs: { ...selected.attrs, [ATTR.REPO_RESOLUTION_SOURCE]: selected.source },
|
|
6351
|
+
cwd: selected.attrs[ATTR.CWD] ?? selected.cwd,
|
|
6299
6352
|
source: selected.source
|
|
6300
6353
|
};
|
|
6301
6354
|
}
|
|
@@ -6323,9 +6376,9 @@ async function resolutionForWorkdirEvidence(evidence3, cache) {
|
|
|
6323
6376
|
return null;
|
|
6324
6377
|
}
|
|
6325
6378
|
const attrs = await resolveRepoAttrs(cwd);
|
|
6326
|
-
const resolved = attrs[
|
|
6327
|
-
attrs: { ...attrs, [
|
|
6328
|
-
cwd: attrs[
|
|
6379
|
+
const resolved = attrs[ATTR.SUBJECT_KEY] ? {
|
|
6380
|
+
attrs: { ...attrs, [ATTR.REPO_RESOLUTION_SOURCE]: evidence3.source },
|
|
6381
|
+
cwd: attrs[ATTR.CWD] ?? cwd,
|
|
6329
6382
|
source: evidence3.source
|
|
6330
6383
|
} : null;
|
|
6331
6384
|
cache.set(evidence3.path, resolved);
|
|
@@ -6348,7 +6401,7 @@ async function resolveSpanWorkdirRepoAttrs(span2, cache) {
|
|
|
6348
6401
|
}
|
|
6349
6402
|
const byScore = (a, b) => b.score - a.score || a.order - b.order;
|
|
6350
6403
|
const resolved = [...candidates.values()];
|
|
6351
|
-
const selected = resolved.filter((candidate) => candidate.repo.attrs[
|
|
6404
|
+
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
6405
|
if (!selected) return null;
|
|
6353
6406
|
return selected.repo;
|
|
6354
6407
|
}
|
|
@@ -6364,7 +6417,7 @@ async function stampSpanWorkdirRepoAttrs(spans) {
|
|
|
6364
6417
|
async function awaitWithAbort(operation, signal) {
|
|
6365
6418
|
if (!signal) return operation;
|
|
6366
6419
|
signal.throwIfAborted();
|
|
6367
|
-
return new Promise((
|
|
6420
|
+
return new Promise((resolve10, reject) => {
|
|
6368
6421
|
const stop = () => {
|
|
6369
6422
|
signal.removeEventListener("abort", stop);
|
|
6370
6423
|
reject(signal.reason);
|
|
@@ -6373,7 +6426,7 @@ async function awaitWithAbort(operation, signal) {
|
|
|
6373
6426
|
operation.then(
|
|
6374
6427
|
(value) => {
|
|
6375
6428
|
signal.removeEventListener("abort", stop);
|
|
6376
|
-
|
|
6429
|
+
resolve10(value);
|
|
6377
6430
|
},
|
|
6378
6431
|
(error) => {
|
|
6379
6432
|
signal.removeEventListener("abort", stop);
|
|
@@ -6392,6 +6445,8 @@ async function parseSession(adapter, ref, options = {}) {
|
|
|
6392
6445
|
if (spans.length === 0) throw new EmptySessionError(ref.path);
|
|
6393
6446
|
stampSessionIntegrity(ref, spans);
|
|
6394
6447
|
stampSessionIdentity(spans, ref.sessionId);
|
|
6448
|
+
stampEnvironmentAttrs(spans, ref.environment);
|
|
6449
|
+
if (ref.cwd === null && ref.environment?.cwd) ref.cwd = ref.environment.cwd;
|
|
6395
6450
|
const repo = await resolveSessionRepoAttrs(ref.cwd, spans);
|
|
6396
6451
|
if (repo.cwd) ref.cwd = repo.cwd;
|
|
6397
6452
|
stampRepoAttrs(spans, repo.attrs);
|
|
@@ -6553,6 +6608,7 @@ import {
|
|
|
6553
6608
|
OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2,
|
|
6554
6609
|
TOOL_NAME as TOOL_NAME3
|
|
6555
6610
|
} from "@tangle-network/agent-eval/trace-attributes";
|
|
6611
|
+
var POLICY_EVIDENCE_NOTE = "This is normalized coding-agent session evidence for downstream policy mining; it is not an eval campaign cell.";
|
|
6556
6612
|
function stringAttr(span2, key) {
|
|
6557
6613
|
const value = span2.attributes[key];
|
|
6558
6614
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
@@ -6563,12 +6619,12 @@ function spanKind(span2) {
|
|
|
6563
6619
|
function repoFromSpans(spans) {
|
|
6564
6620
|
const attrs = {};
|
|
6565
6621
|
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,
|
|
6622
|
+
attrs.subjectKey ??= stringAttr(span2, ATTR.SUBJECT_KEY);
|
|
6623
|
+
attrs.repository ??= stringAttr(span2, ATTR.GIT_REPOSITORY);
|
|
6624
|
+
attrs.branch ??= stringAttr(span2, ATTR.GIT_BRANCH_NAME);
|
|
6625
|
+
attrs.commit ??= stringAttr(span2, ATTR.GIT_COMMIT);
|
|
6626
|
+
attrs.cwd ??= stringAttr(span2, ATTR.CWD);
|
|
6627
|
+
attrs.resolutionSource ??= stringAttr(span2, ATTR.REPO_RESOLUTION_SOURCE);
|
|
6572
6628
|
if (attrs.subjectKey && attrs.repository && attrs.branch && attrs.commit && attrs.cwd && attrs.resolutionSource) break;
|
|
6573
6629
|
}
|
|
6574
6630
|
return attrs;
|
|
@@ -6657,7 +6713,7 @@ async function buildPolicyEvidenceRecord(ref, spans, opts = {}) {
|
|
|
6657
6713
|
...opts.otlpPath ? { otlpPath: opts.otlpPath } : {},
|
|
6658
6714
|
...opts.sourceSha256 ? { sourceSha256: opts.sourceSha256 } : {},
|
|
6659
6715
|
notCampaignCell: true,
|
|
6660
|
-
note:
|
|
6716
|
+
note: POLICY_EVIDENCE_NOTE
|
|
6661
6717
|
}
|
|
6662
6718
|
};
|
|
6663
6719
|
}
|
|
@@ -7181,11 +7237,11 @@ function parseCorpusFlag(value) {
|
|
|
7181
7237
|
if (eq <= 0) throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
|
|
7182
7238
|
const name = value.slice(0, eq);
|
|
7183
7239
|
const rest = value.slice(eq + 1);
|
|
7184
|
-
const
|
|
7185
|
-
if (
|
|
7240
|
+
const sep3 = rest.indexOf("::");
|
|
7241
|
+
if (sep3 <= 0 || sep3 === rest.length - 2) {
|
|
7186
7242
|
throw new Error(`--corpus must be name=<labels>::<prepared>, got: ${value}`);
|
|
7187
7243
|
}
|
|
7188
|
-
return { name, labelsPath: rest.slice(0,
|
|
7244
|
+
return { name, labelsPath: rest.slice(0, sep3), preparedDir: rest.slice(sep3 + 2) };
|
|
7189
7245
|
}
|
|
7190
7246
|
var SUBMIT_ACTION_SIGNATURE = "COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT";
|
|
7191
7247
|
function isSubmitAction(action) {
|
|
@@ -7211,7 +7267,7 @@ function goldIncorrectSteps(entry) {
|
|
|
7211
7267
|
function findRawTrajFiles(sweRawDir) {
|
|
7212
7268
|
if (!existsSync2(sweRawDir)) return [];
|
|
7213
7269
|
const entries = readdirSync2(sweRawDir, { recursive: true, encoding: "utf8" });
|
|
7214
|
-
return entries.filter((
|
|
7270
|
+
return entries.filter((relative6) => relative6.endsWith(".traj.json")).map((relative6) => join20(sweRawDir, relative6)).sort();
|
|
7215
7271
|
}
|
|
7216
7272
|
function cwdFromPwdObservation(steps) {
|
|
7217
7273
|
for (const step of steps) {
|
|
@@ -8376,9 +8432,9 @@ function renderBatchReport(report) {
|
|
|
8376
8432
|
lines.push("");
|
|
8377
8433
|
lines.push("| corpus | trajectory | image | error |");
|
|
8378
8434
|
lines.push("| --- | --- | --- | --- |");
|
|
8379
|
-
for (const
|
|
8435
|
+
for (const failure2 of report.pullFailures) {
|
|
8380
8436
|
lines.push(
|
|
8381
|
-
`| ${
|
|
8437
|
+
`| ${failure2.corpus} | ${failure2.trajId} | \`${failure2.image}\` | ${failure2.error.replaceAll("|", "\\|")} |`
|
|
8382
8438
|
);
|
|
8383
8439
|
}
|
|
8384
8440
|
}
|
|
@@ -9444,13 +9500,13 @@ function terminateWindowsProcessTree(child) {
|
|
|
9444
9500
|
forceKill(child);
|
|
9445
9501
|
return Promise.resolve();
|
|
9446
9502
|
}
|
|
9447
|
-
return new Promise((
|
|
9503
|
+
return new Promise((resolve10) => {
|
|
9448
9504
|
let finished = false;
|
|
9449
9505
|
const finish = () => {
|
|
9450
9506
|
if (finished) return;
|
|
9451
9507
|
finished = true;
|
|
9452
9508
|
forceKill(child);
|
|
9453
|
-
|
|
9509
|
+
resolve10();
|
|
9454
9510
|
};
|
|
9455
9511
|
let killer;
|
|
9456
9512
|
try {
|
|
@@ -9477,7 +9533,7 @@ function signalProcess(pid, signal) {
|
|
|
9477
9533
|
}
|
|
9478
9534
|
}
|
|
9479
9535
|
function readProcessTableWithPs() {
|
|
9480
|
-
return new Promise((
|
|
9536
|
+
return new Promise((resolve10, reject) => {
|
|
9481
9537
|
let child;
|
|
9482
9538
|
try {
|
|
9483
9539
|
child = spawn("ps", ["-A", "-o", "pid=", "-o", "ppid="], {
|
|
@@ -9531,7 +9587,7 @@ function readProcessTableWithPs() {
|
|
|
9531
9587
|
reject(new Error("ps: returned no process records"));
|
|
9532
9588
|
return;
|
|
9533
9589
|
}
|
|
9534
|
-
|
|
9590
|
+
resolve10(rows);
|
|
9535
9591
|
});
|
|
9536
9592
|
});
|
|
9537
9593
|
});
|
|
@@ -9541,10 +9597,10 @@ async function readLinuxProcProcessTable() {
|
|
|
9541
9597
|
const rows = await Promise.all(
|
|
9542
9598
|
entries.filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name)).map(async (entry) => {
|
|
9543
9599
|
try {
|
|
9544
|
-
const
|
|
9545
|
-
const commandEnd =
|
|
9600
|
+
const stat18 = await readFile7(`/proc/${entry.name}/stat`, "utf8");
|
|
9601
|
+
const commandEnd = stat18.lastIndexOf(")");
|
|
9546
9602
|
if (commandEnd < 0) return void 0;
|
|
9547
|
-
const fields =
|
|
9603
|
+
const fields = stat18.slice(commandEnd + 1).trim().split(/\s+/);
|
|
9548
9604
|
const pid = Number(entry.name);
|
|
9549
9605
|
const parentPid = Number(fields[1]);
|
|
9550
9606
|
if (!Number.isSafeInteger(pid) || pid < 1 || !Number.isSafeInteger(parentPid) || parentPid < 0) {
|
|
@@ -9606,7 +9662,7 @@ async function terminatePosixProcessTree(child) {
|
|
|
9606
9662
|
}
|
|
9607
9663
|
const rootPid = child.pid;
|
|
9608
9664
|
const descendants = /* @__PURE__ */ new Map();
|
|
9609
|
-
let
|
|
9665
|
+
let failure2;
|
|
9610
9666
|
try {
|
|
9611
9667
|
signalProcess(-rootPid, "SIGSTOP");
|
|
9612
9668
|
signalProcess(rootPid, "SIGSTOP");
|
|
@@ -9623,7 +9679,7 @@ async function terminatePosixProcessTree(child) {
|
|
|
9623
9679
|
previous = signature;
|
|
9624
9680
|
}
|
|
9625
9681
|
} catch (error) {
|
|
9626
|
-
|
|
9682
|
+
failure2 = error;
|
|
9627
9683
|
} finally {
|
|
9628
9684
|
for (const processEntry of [...descendants.values()].sort(
|
|
9629
9685
|
(left, right) => right.depth - left.depth
|
|
@@ -9631,18 +9687,18 @@ async function terminatePosixProcessTree(child) {
|
|
|
9631
9687
|
try {
|
|
9632
9688
|
signalProcess(processEntry.pid, "SIGKILL");
|
|
9633
9689
|
} catch (error) {
|
|
9634
|
-
|
|
9690
|
+
failure2 ??= error;
|
|
9635
9691
|
}
|
|
9636
9692
|
}
|
|
9637
9693
|
try {
|
|
9638
9694
|
signalProcess(-rootPid, "SIGKILL");
|
|
9639
9695
|
} catch (error) {
|
|
9640
|
-
|
|
9696
|
+
failure2 ??= error;
|
|
9641
9697
|
}
|
|
9642
9698
|
forceKill(child);
|
|
9643
9699
|
}
|
|
9644
|
-
if (
|
|
9645
|
-
throw
|
|
9700
|
+
if (failure2) {
|
|
9701
|
+
throw failure2 instanceof Error ? failure2 : new Error(String(failure2));
|
|
9646
9702
|
}
|
|
9647
9703
|
}
|
|
9648
9704
|
function terminateProcessTree(child) {
|
|
@@ -9666,7 +9722,7 @@ function runCommand(command, args = [], opts = {}) {
|
|
|
9666
9722
|
throw new RangeError("maxBuffer must be a positive safe integer");
|
|
9667
9723
|
}
|
|
9668
9724
|
if (opts.signal?.aborted) return Promise.reject(abortError(opts.signal));
|
|
9669
|
-
return new Promise((
|
|
9725
|
+
return new Promise((resolve10, reject) => {
|
|
9670
9726
|
let child;
|
|
9671
9727
|
try {
|
|
9672
9728
|
child = spawn(command, [...args], { detached: process.platform !== "win32" });
|
|
@@ -9698,7 +9754,7 @@ function runCommand(command, args = [], opts = {}) {
|
|
|
9698
9754
|
} else if (spawnError) {
|
|
9699
9755
|
reject(spawnError);
|
|
9700
9756
|
} else {
|
|
9701
|
-
|
|
9757
|
+
resolve10({ stdout: out, stderr: err, code: closeCode });
|
|
9702
9758
|
}
|
|
9703
9759
|
};
|
|
9704
9760
|
function requestTermination(error) {
|
|
@@ -10613,71 +10669,490 @@ function assertPositiveInteger(name, value) {
|
|
|
10613
10669
|
}
|
|
10614
10670
|
}
|
|
10615
10671
|
|
|
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
|
|
10672
|
+
// src/analyst-engine-prime.ts
|
|
10673
|
+
import { createHash as createHash10 } from "crypto";
|
|
10674
|
+
import { request as httpRequest } from "http";
|
|
10675
|
+
import { request as httpsRequest } from "https";
|
|
10676
|
+
var DEFAULT_BRIDGE_URL = "http://localhost:4181";
|
|
10677
|
+
var DEFAULT_MODEL = "prime/zai/glm-5.2";
|
|
10678
|
+
var DEFAULT_TIMEOUT_MS = 12e5;
|
|
10679
|
+
var DEFAULT_MAX_INLINE_CHARS = 36e4;
|
|
10680
|
+
var DEFAULT_PER_ATTRIBUTE_CHAR_CAP = 1200;
|
|
10681
|
+
var DEFAULT_QUESTION = "Diagnose this trajectory: identify concrete agent failures, wasted work, and unsupported claims.";
|
|
10682
|
+
var MAX_FINDINGS = 10;
|
|
10683
|
+
var MAX_SPAN_IDS_PER_FINDING = 8;
|
|
10684
|
+
var SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low", "info"]);
|
|
10685
|
+
var ANALYST_ID = "prime";
|
|
10686
|
+
function positiveIntegerEnv(name) {
|
|
10687
|
+
const raw = process.env[name];
|
|
10688
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
10689
|
+
const value = Number(raw);
|
|
10690
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
10691
|
+
throw new TypeError(`${name} must be a positive integer, got '${raw}'`);
|
|
10647
10692
|
}
|
|
10648
|
-
|
|
10649
|
-
var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
|
|
10650
|
-
function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
10651
|
-
const report = { redactionCount: 0, byRule: {} };
|
|
10652
|
-
const out = spans.map((s) => {
|
|
10653
|
-
const attributes2 = redactValue(s.attributes, rules, report).value;
|
|
10654
|
-
normalizeToolIoAttributes(attributes2);
|
|
10655
|
-
let status = s.status;
|
|
10656
|
-
if (status.message) {
|
|
10657
|
-
const m = redactValue(status.message, rules, report).value;
|
|
10658
|
-
if (typeof m === "string" && m !== status.message) status = { ...status, message: m };
|
|
10659
|
-
}
|
|
10660
|
-
return { ...s, attributes: attributes2, status };
|
|
10661
|
-
});
|
|
10662
|
-
return { spans: out, report };
|
|
10693
|
+
return value;
|
|
10663
10694
|
}
|
|
10664
|
-
|
|
10665
|
-
|
|
10666
|
-
|
|
10667
|
-
|
|
10668
|
-
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10672
|
-
|
|
10673
|
-
|
|
10674
|
-
|
|
10675
|
-
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
|
|
10680
|
-
}
|
|
10695
|
+
function finiteOrNull(value) {
|
|
10696
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
10697
|
+
}
|
|
10698
|
+
function normalizeUsage(raw) {
|
|
10699
|
+
if (!raw || typeof raw !== "object") return { calls: null, tokens: null, estimated: false };
|
|
10700
|
+
const usage = raw;
|
|
10701
|
+
const input = finiteOrNull(usage.prompt_tokens);
|
|
10702
|
+
const output = finiteOrNull(usage.completion_tokens);
|
|
10703
|
+
return {
|
|
10704
|
+
calls: finiteOrNull(usage.model_requests),
|
|
10705
|
+
tokens: input === null && output === null ? null : { input, output },
|
|
10706
|
+
estimated: usage.estimated === true
|
|
10707
|
+
};
|
|
10708
|
+
}
|
|
10709
|
+
function mergeUsage(a, b) {
|
|
10710
|
+
const calls = a.calls !== null && b.calls !== null ? a.calls + b.calls : null;
|
|
10711
|
+
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;
|
|
10712
|
+
return { calls, tokens: tokens3, estimated: a.estimated || b.estimated };
|
|
10713
|
+
}
|
|
10714
|
+
function renderUsage(usage) {
|
|
10715
|
+
const calls = usage.calls === null ? "uncaptured" : String(usage.calls);
|
|
10716
|
+
const input = usage.tokens?.input ?? "uncaptured";
|
|
10717
|
+
const output = usage.tokens?.output ?? "uncaptured";
|
|
10718
|
+
const estimated = usage.estimated ? " (bridge-estimated)" : "";
|
|
10719
|
+
return `usage: calls=${calls} input_tokens=${input} output_tokens=${output}${estimated}; cost=uncaptured (no pricing table)`;
|
|
10720
|
+
}
|
|
10721
|
+
function capAttributeValue(value, cap) {
|
|
10722
|
+
const rendered = JSON.stringify(value);
|
|
10723
|
+
if (rendered === void 0 || rendered.length <= cap) return value;
|
|
10724
|
+
if (typeof value === "string") return `${value.slice(0, cap)}\u2026[truncated ${value.length - cap} chars]`;
|
|
10725
|
+
return `[omitted non-string attribute: ${rendered.length} JSON chars]`;
|
|
10726
|
+
}
|
|
10727
|
+
function capRowAttributes(row, cap) {
|
|
10728
|
+
const attributes2 = row.attributes;
|
|
10729
|
+
if (!attributes2 || typeof attributes2 !== "object" || Array.isArray(attributes2)) return row;
|
|
10730
|
+
const capped = {};
|
|
10731
|
+
for (const [key, value] of Object.entries(attributes2)) {
|
|
10732
|
+
capped[key] = capAttributeValue(value, cap);
|
|
10733
|
+
}
|
|
10734
|
+
return { ...row, attributes: capped };
|
|
10735
|
+
}
|
|
10736
|
+
async function projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, signal) {
|
|
10737
|
+
const rows = [];
|
|
10738
|
+
const spanTraces = /* @__PURE__ */ new Map();
|
|
10739
|
+
let index = 0;
|
|
10740
|
+
for await (const value of readJsonl(otlpPath, { signal })) {
|
|
10741
|
+
const label = `${otlpPath}:${index + 1}`;
|
|
10742
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
10743
|
+
throw new TypeError(`${label} must contain an object`);
|
|
10744
|
+
}
|
|
10745
|
+
const row = value;
|
|
10746
|
+
if (typeof row.trace_id !== "string" || row.trace_id.length === 0) {
|
|
10747
|
+
throw new TypeError(`${label}.trace_id must be a non-empty string`);
|
|
10748
|
+
}
|
|
10749
|
+
if (typeof row.span_id !== "string" || row.span_id.length === 0) {
|
|
10750
|
+
throw new TypeError(`${label}.span_id must be a non-empty string`);
|
|
10751
|
+
}
|
|
10752
|
+
rows.push(row);
|
|
10753
|
+
const traces = spanTraces.get(row.span_id) ?? /* @__PURE__ */ new Set();
|
|
10754
|
+
traces.add(row.trace_id);
|
|
10755
|
+
spanTraces.set(row.span_id, traces);
|
|
10756
|
+
index += 1;
|
|
10757
|
+
}
|
|
10758
|
+
if (rows.length === 0) throw new Error(`${otlpPath} contains no spans`);
|
|
10759
|
+
const traceCount = new Set(rows.map((row) => row.trace_id)).size;
|
|
10760
|
+
let projected = rows;
|
|
10761
|
+
let rendered = JSON.stringify(projected);
|
|
10762
|
+
const delivery = {
|
|
10763
|
+
mode: "inline-json",
|
|
10764
|
+
perAttributeCharCap: null,
|
|
10765
|
+
renderedChars: rendered.length
|
|
10766
|
+
};
|
|
10767
|
+
if (rendered.length > maxInlineChars) {
|
|
10768
|
+
projected = rows.map((row) => capRowAttributes(row, perAttributeCharCap));
|
|
10769
|
+
rendered = JSON.stringify(projected);
|
|
10770
|
+
delivery.perAttributeCharCap = perAttributeCharCap;
|
|
10771
|
+
delivery.renderedChars = rendered.length;
|
|
10772
|
+
}
|
|
10773
|
+
if (rendered.length > maxInlineChars) {
|
|
10774
|
+
throw new Error(
|
|
10775
|
+
`trajectory renders to ${rendered.length} chars even at per-attribute cap ${perAttributeCharCap} (budget ${maxInlineChars}); inline delivery impossible`
|
|
10776
|
+
);
|
|
10777
|
+
}
|
|
10778
|
+
return { rows: projected, rendered, delivery, traceCount, spanTraces };
|
|
10779
|
+
}
|
|
10780
|
+
var OUTPUT_CONTRACT_LINES = [
|
|
10781
|
+
"OUTPUT CONTRACT (you have no trace tools and no REPL):",
|
|
10782
|
+
"You are a one-shot analyst. Every fact you need is in the TRAJECTORY JSON below.",
|
|
10783
|
+
"Do not run shell commands, do not read or write files, do not use any tools.",
|
|
10784
|
+
"Reply with EXACTLY one fenced ```json code block and no other fenced block. The JSON object has exactly two fields:",
|
|
10785
|
+
' "answer": string \u2014 ONE short sentence (max 300 chars) summarizing your verdict.',
|
|
10786
|
+
' "findings": array (possibly empty) of finding rows, each exactly:',
|
|
10787
|
+
' {"span_ids": [string, ...],',
|
|
10788
|
+
' "severity": "critical"|"high"|"medium"|"low"|"info",',
|
|
10789
|
+
' "area": string (short kebab-case topic, max 40 chars),',
|
|
10790
|
+
' "claim": string (ONE short sentence, max 200 chars),',
|
|
10791
|
+
' "action": string (optional, ONE short imperative sentence, max 200 chars),',
|
|
10792
|
+
' "confidence": number 0..1}',
|
|
10793
|
+
"Do NOT include a rationale field. Keep every string SHORT \u2014 long strings get corrupted in transport and void your work.",
|
|
10794
|
+
`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.`,
|
|
10795
|
+
'"findings" is [] only for a clean trajectory.'
|
|
10796
|
+
];
|
|
10797
|
+
function buildPrompt(question, projection) {
|
|
10798
|
+
return [
|
|
10799
|
+
`QUESTION: ${question}`,
|
|
10800
|
+
"",
|
|
10801
|
+
...OUTPUT_CONTRACT_LINES,
|
|
10802
|
+
"",
|
|
10803
|
+
`TRAJECTORY (${projection.traceCount} trace(s); ${projection.rows.length} spans; full OpenInference span projection as JSON):`,
|
|
10804
|
+
projection.rendered
|
|
10805
|
+
].join("\n");
|
|
10806
|
+
}
|
|
10807
|
+
function buildRepairPrompt(parseDefect, previousReply) {
|
|
10808
|
+
return [
|
|
10809
|
+
"Your previous reply to a trace-analysis task was structurally malformed and could not be parsed",
|
|
10810
|
+
`(${parseDefect}). Below is your previous reply verbatim. Re-emit ONLY the corrected JSON \u2014 one`,
|
|
10811
|
+
"fenced ```json block, no other text, no tools. The JSON object has exactly two fields:",
|
|
10812
|
+
' "answer": string (ONE short sentence, max 300 chars)',
|
|
10813
|
+
' "findings": array (possibly empty) of {"span_ids": [string, ...],',
|
|
10814
|
+
' "severity": "critical"|"high"|"medium"|"low"|"info", "area": string (max 40 chars),',
|
|
10815
|
+
' "claim": string (max 200 chars), "action": string (optional, max 200 chars), "confidence": number 0..1}',
|
|
10816
|
+
"No rationale field. Keep every string SHORT. Preserve the span ids and verdicts of your previous reply exactly; shorten prose freely.",
|
|
10817
|
+
"",
|
|
10818
|
+
"PREVIOUS REPLY:",
|
|
10819
|
+
previousReply
|
|
10820
|
+
].join("\n");
|
|
10821
|
+
}
|
|
10822
|
+
function tryParseObject(text2) {
|
|
10823
|
+
try {
|
|
10824
|
+
const value = JSON.parse(text2.trim());
|
|
10825
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
10826
|
+
} catch {
|
|
10827
|
+
return null;
|
|
10828
|
+
}
|
|
10829
|
+
}
|
|
10830
|
+
function extractJsonObject(text2) {
|
|
10831
|
+
const direct = tryParseObject(text2);
|
|
10832
|
+
if (direct) return direct;
|
|
10833
|
+
const fenced = [...text2.matchAll(/```(?:json)?\s*\n?([\s\S]*?)```/g)];
|
|
10834
|
+
for (let i = fenced.length - 1; i >= 0; i -= 1) {
|
|
10835
|
+
const candidate = tryParseObject(fenced[i][1]);
|
|
10836
|
+
if (candidate) return candidate;
|
|
10837
|
+
}
|
|
10838
|
+
const start = text2.indexOf("{");
|
|
10839
|
+
const end = text2.lastIndexOf("}");
|
|
10840
|
+
if (start >= 0 && end > start) {
|
|
10841
|
+
const candidate = tryParseObject(text2.slice(start, end + 1));
|
|
10842
|
+
if (candidate) return candidate;
|
|
10843
|
+
}
|
|
10844
|
+
return null;
|
|
10845
|
+
}
|
|
10846
|
+
function parseDefectOf(parsed) {
|
|
10847
|
+
if (parsed === null) return "no parseable JSON object";
|
|
10848
|
+
if (!Array.isArray(parsed.findings)) return 'JSON has no "findings" array';
|
|
10849
|
+
return null;
|
|
10850
|
+
}
|
|
10851
|
+
function findingRowDefect(row, spanTraces) {
|
|
10852
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return "row is not an object";
|
|
10853
|
+
const record2 = row;
|
|
10854
|
+
if (!Array.isArray(record2.span_ids) || record2.span_ids.length === 0) {
|
|
10855
|
+
return "span_ids must be a non-empty array";
|
|
10856
|
+
}
|
|
10857
|
+
const spanIds = [];
|
|
10858
|
+
for (const id of record2.span_ids) {
|
|
10859
|
+
if (typeof id !== "string" || id.length === 0) return "span_ids must contain non-empty strings";
|
|
10860
|
+
const traces = spanTraces.get(id);
|
|
10861
|
+
if (!traces) return `span_id '${id}' is not in the trajectory`;
|
|
10862
|
+
if (traces.size > 1) return `span_id '${id}' is ambiguous across ${traces.size} traces`;
|
|
10863
|
+
if (!spanIds.includes(id)) spanIds.push(id);
|
|
10864
|
+
}
|
|
10865
|
+
if (spanIds.length > MAX_SPAN_IDS_PER_FINDING) {
|
|
10866
|
+
return `span_ids cites ${spanIds.length} distinct spans (cap ${MAX_SPAN_IDS_PER_FINDING})`;
|
|
10867
|
+
}
|
|
10868
|
+
if (typeof record2.severity !== "string" || !SEVERITIES.has(record2.severity)) {
|
|
10869
|
+
return "severity outside the analyst severity enum";
|
|
10870
|
+
}
|
|
10871
|
+
if (typeof record2.area !== "string" || record2.area.trim().length === 0 || record2.area.length > 200) {
|
|
10872
|
+
return "area must be a 1-200 char string";
|
|
10873
|
+
}
|
|
10874
|
+
if (typeof record2.claim !== "string" || record2.claim.trim().length === 0 || record2.claim.length > 2e3) {
|
|
10875
|
+
return "claim must be a 1-2000 char string";
|
|
10876
|
+
}
|
|
10877
|
+
if (record2.action !== void 0 && (typeof record2.action !== "string" || record2.action.trim().length === 0 || record2.action.length > 2e3)) {
|
|
10878
|
+
return "action must be a 1-2000 char string when present";
|
|
10879
|
+
}
|
|
10880
|
+
if (typeof record2.confidence !== "number" || !Number.isFinite(record2.confidence) || record2.confidence < 0 || record2.confidence > 1) {
|
|
10881
|
+
return "confidence must be 0..1";
|
|
10882
|
+
}
|
|
10883
|
+
return {
|
|
10884
|
+
spanIds,
|
|
10885
|
+
severity: record2.severity,
|
|
10886
|
+
area: record2.area.trim(),
|
|
10887
|
+
claim: record2.claim.trim(),
|
|
10888
|
+
...typeof record2.action === "string" ? { action: record2.action.trim() } : {},
|
|
10889
|
+
confidence: record2.confidence
|
|
10890
|
+
};
|
|
10891
|
+
}
|
|
10892
|
+
function toAnalystFinding(row, index, spanTraces, model, producedAt) {
|
|
10893
|
+
const uris = row.spanIds.map((spanId) => {
|
|
10894
|
+
const traces = spanTraces.get(spanId);
|
|
10895
|
+
return spanEvidenceUri([...traces][0], spanId);
|
|
10896
|
+
});
|
|
10897
|
+
const findingId = `prime-${createHash10("sha256").update(`${index}
|
|
10898
|
+
${row.claim}
|
|
10899
|
+
${uris.join("\n")}`).digest("hex").slice(0, 12)}`;
|
|
10900
|
+
return {
|
|
10901
|
+
schema_version: "1.0.0",
|
|
10902
|
+
finding_id: findingId,
|
|
10903
|
+
analyst_id: ANALYST_ID,
|
|
10904
|
+
produced_at: producedAt,
|
|
10905
|
+
severity: row.severity,
|
|
10906
|
+
area: row.area,
|
|
10907
|
+
claim: row.claim,
|
|
10908
|
+
evidence_refs: uris.map((uri) => ({ kind: "span", uri })),
|
|
10909
|
+
confidence: row.confidence,
|
|
10910
|
+
...row.action ? { recommended_action: row.action } : {},
|
|
10911
|
+
metadata: { engine: ANALYST_ID, model }
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10914
|
+
var httpJsonTransport = ({ url, body, signal }) => {
|
|
10915
|
+
const target = new URL(url);
|
|
10916
|
+
if (target.protocol !== "http:" && target.protocol !== "https:") {
|
|
10917
|
+
throw new TypeError(`bridge URL must be http: or https:, got ${target.protocol}`);
|
|
10918
|
+
}
|
|
10919
|
+
const requestFn = target.protocol === "https:" ? httpsRequest : httpRequest;
|
|
10920
|
+
const payload = JSON.stringify(body);
|
|
10921
|
+
return new Promise((resolve10, reject) => {
|
|
10922
|
+
const req = requestFn(
|
|
10923
|
+
{
|
|
10924
|
+
hostname: target.hostname,
|
|
10925
|
+
port: target.port,
|
|
10926
|
+
path: `${target.pathname}${target.search}`,
|
|
10927
|
+
method: "POST",
|
|
10928
|
+
headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload) },
|
|
10929
|
+
signal
|
|
10930
|
+
},
|
|
10931
|
+
(res) => {
|
|
10932
|
+
const chunks = [];
|
|
10933
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
10934
|
+
res.on("end", () => resolve10({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") }));
|
|
10935
|
+
res.on("error", reject);
|
|
10936
|
+
}
|
|
10937
|
+
);
|
|
10938
|
+
req.on("error", reject);
|
|
10939
|
+
req.end(payload);
|
|
10940
|
+
});
|
|
10941
|
+
};
|
|
10942
|
+
var PrimeBridgeError = class extends Error {
|
|
10943
|
+
};
|
|
10944
|
+
async function callBridge(transport, url, model, content, timeoutMs, callerSignal, turn) {
|
|
10945
|
+
const controller = new AbortController();
|
|
10946
|
+
const timer = setTimeout(
|
|
10947
|
+
() => controller.abort(new Error(`${turn}bridge call exceeded ${timeoutMs}ms`)),
|
|
10948
|
+
timeoutMs
|
|
10949
|
+
);
|
|
10950
|
+
const onCallerAbort = () => controller.abort(callerSignal.reason);
|
|
10951
|
+
callerSignal?.addEventListener("abort", onCallerAbort, { once: true });
|
|
10952
|
+
if (callerSignal?.aborted) onCallerAbort();
|
|
10953
|
+
let response;
|
|
10954
|
+
try {
|
|
10955
|
+
response = await transport({
|
|
10956
|
+
url,
|
|
10957
|
+
body: { model, messages: [{ role: "user", content }] },
|
|
10958
|
+
signal: controller.signal
|
|
10959
|
+
});
|
|
10960
|
+
} catch (error) {
|
|
10961
|
+
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
10962
|
+
throw new PrimeBridgeError(
|
|
10963
|
+
`${turn}bridge transport failure: ${reason instanceof Error ? reason.message : String(reason)}`
|
|
10964
|
+
);
|
|
10965
|
+
} finally {
|
|
10966
|
+
clearTimeout(timer);
|
|
10967
|
+
callerSignal?.removeEventListener("abort", onCallerAbort);
|
|
10968
|
+
}
|
|
10969
|
+
if (response.status !== 200) {
|
|
10970
|
+
throw new PrimeBridgeError(`${turn}bridge HTTP ${response.status}: ${response.text.slice(0, 500)}`);
|
|
10971
|
+
}
|
|
10972
|
+
let parsed;
|
|
10973
|
+
try {
|
|
10974
|
+
parsed = JSON.parse(response.text);
|
|
10975
|
+
} catch {
|
|
10976
|
+
throw new PrimeBridgeError(`${turn}bridge returned unparseable JSON (${response.text.length} bytes)`);
|
|
10977
|
+
}
|
|
10978
|
+
const record2 = parsed;
|
|
10979
|
+
const replyContent = record2.choices?.[0]?.message?.content;
|
|
10980
|
+
if (typeof replyContent !== "string" || replyContent.length === 0) {
|
|
10981
|
+
throw new PrimeBridgeError(`${turn}bridge reply carries no message content`);
|
|
10982
|
+
}
|
|
10983
|
+
return { content: replyContent, usage: normalizeUsage(record2.usage) };
|
|
10984
|
+
}
|
|
10985
|
+
function failure(output, error) {
|
|
10986
|
+
return { analyzer: ANALYST_ID, kind: "report", ok: false, output, error };
|
|
10987
|
+
}
|
|
10988
|
+
function primeAnalyzer(opts = {}) {
|
|
10989
|
+
const bridgeUrl = (opts.bridgeUrl ?? process.env.TRACES_PRIME_BRIDGE_URL ?? DEFAULT_BRIDGE_URL).replace(/\/+$/, "");
|
|
10990
|
+
const model = opts.model ?? process.env.TRACES_PRIME_MODEL ?? DEFAULT_MODEL;
|
|
10991
|
+
const timeoutMs = opts.timeoutMs ?? positiveIntegerEnv("TRACES_PRIME_TIMEOUT_MS") ?? DEFAULT_TIMEOUT_MS;
|
|
10992
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
10993
|
+
throw new RangeError("timeoutMs must be a positive safe integer");
|
|
10994
|
+
}
|
|
10995
|
+
const maxInlineChars = opts.maxInlineChars ?? DEFAULT_MAX_INLINE_CHARS;
|
|
10996
|
+
const perAttributeCharCap = opts.perAttributeCharCap ?? DEFAULT_PER_ATTRIBUTE_CHAR_CAP;
|
|
10997
|
+
if (!Number.isSafeInteger(maxInlineChars) || maxInlineChars < 1) {
|
|
10998
|
+
throw new RangeError("maxInlineChars must be a positive safe integer");
|
|
10999
|
+
}
|
|
11000
|
+
if (!Number.isSafeInteger(perAttributeCharCap) || perAttributeCharCap < 1) {
|
|
11001
|
+
throw new RangeError("perAttributeCharCap must be a positive safe integer");
|
|
11002
|
+
}
|
|
11003
|
+
const repairEnabled = opts.repair ?? true;
|
|
11004
|
+
const transport = opts.transport ?? httpJsonTransport;
|
|
11005
|
+
const url = `${bridgeUrl}/v1/chat/completions`;
|
|
11006
|
+
return {
|
|
11007
|
+
name: ANALYST_ID,
|
|
11008
|
+
async analyze(otlpPath, analyzeOpts = {}) {
|
|
11009
|
+
let projection;
|
|
11010
|
+
try {
|
|
11011
|
+
projection = await projectArtifact(otlpPath, maxInlineChars, perAttributeCharCap, analyzeOpts.signal);
|
|
11012
|
+
} catch (error) {
|
|
11013
|
+
return failure("", error instanceof Error ? error.message : String(error));
|
|
11014
|
+
}
|
|
11015
|
+
const question = analyzeOpts.prompt ?? opts.defaultPrompt ?? DEFAULT_QUESTION;
|
|
11016
|
+
const deliveryLine = `delivery: ${projection.delivery.mode} (${projection.delivery.renderedChars} chars, per-attribute cap ${projection.delivery.perAttributeCharCap ?? "none"})`;
|
|
11017
|
+
let reply;
|
|
11018
|
+
try {
|
|
11019
|
+
reply = await callBridge(
|
|
11020
|
+
transport,
|
|
11021
|
+
url,
|
|
11022
|
+
model,
|
|
11023
|
+
buildPrompt(question, projection),
|
|
11024
|
+
timeoutMs,
|
|
11025
|
+
analyzeOpts.signal,
|
|
11026
|
+
""
|
|
11027
|
+
);
|
|
11028
|
+
} catch (error) {
|
|
11029
|
+
if (!(error instanceof PrimeBridgeError)) throw error;
|
|
11030
|
+
return failure("", `${error.message}; ${deliveryLine}`);
|
|
11031
|
+
}
|
|
11032
|
+
let usage = reply.usage;
|
|
11033
|
+
let parsed = extractJsonObject(reply.content);
|
|
11034
|
+
let parseDefect = parseDefectOf(parsed);
|
|
11035
|
+
let repairAttempted = false;
|
|
11036
|
+
if (parseDefect !== null && repairEnabled) {
|
|
11037
|
+
repairAttempted = true;
|
|
11038
|
+
let repairReply;
|
|
11039
|
+
try {
|
|
11040
|
+
repairReply = await callBridge(
|
|
11041
|
+
transport,
|
|
11042
|
+
url,
|
|
11043
|
+
model,
|
|
11044
|
+
buildRepairPrompt(parseDefect, reply.content),
|
|
11045
|
+
timeoutMs,
|
|
11046
|
+
analyzeOpts.signal,
|
|
11047
|
+
"repair-turn "
|
|
11048
|
+
);
|
|
11049
|
+
} catch (error) {
|
|
11050
|
+
if (!(error instanceof PrimeBridgeError)) throw error;
|
|
11051
|
+
return failure(reply.content, `${error.message}; ${renderUsage(usage)}`);
|
|
11052
|
+
}
|
|
11053
|
+
usage = mergeUsage(usage, repairReply.usage);
|
|
11054
|
+
parsed = extractJsonObject(repairReply.content);
|
|
11055
|
+
parseDefect = parseDefectOf(parsed);
|
|
11056
|
+
}
|
|
11057
|
+
if (parseDefect !== null) {
|
|
11058
|
+
return failure(
|
|
11059
|
+
reply.content,
|
|
11060
|
+
`${parseDefect} in prime reply${repairAttempted ? " even after the bounded repair turn" : ""}; ` + renderUsage(usage)
|
|
11061
|
+
);
|
|
11062
|
+
}
|
|
11063
|
+
const rows = parsed.findings.slice(0, MAX_FINDINGS);
|
|
11064
|
+
const overflow = parsed.findings.length - rows.length;
|
|
11065
|
+
const producedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11066
|
+
const rejected = [];
|
|
11067
|
+
const findings = [];
|
|
11068
|
+
rows.forEach((row, index) => {
|
|
11069
|
+
const decoded = findingRowDefect(row, projection.spanTraces);
|
|
11070
|
+
if (typeof decoded === "string") {
|
|
11071
|
+
rejected.push(`rejected[${index}]: ${decoded}`);
|
|
11072
|
+
return;
|
|
11073
|
+
}
|
|
11074
|
+
findings.push(toAnalystFinding(decoded, index, projection.spanTraces, model, producedAt));
|
|
11075
|
+
});
|
|
11076
|
+
const answer = typeof parsed.answer === "string" ? parsed.answer : null;
|
|
11077
|
+
const output = [
|
|
11078
|
+
...answer ? [`answer: ${answer}`] : [],
|
|
11079
|
+
`findings: ${findings.length} mapped, ${rejected.length} rejected` + (overflow > 0 ? `, ${overflow} over the ${MAX_FINDINGS}-finding cap dropped` : ""),
|
|
11080
|
+
...rejected,
|
|
11081
|
+
deliveryLine,
|
|
11082
|
+
`repair: ${repairAttempted ? "attempted (succeeded)" : "not needed"}`,
|
|
11083
|
+
renderUsage(usage),
|
|
11084
|
+
...findings.length === 0 ? ["zero findings \u2014 an honest null, not a failure"] : []
|
|
11085
|
+
].join("\n");
|
|
11086
|
+
return { analyzer: ANALYST_ID, kind: "findings", ok: true, output, findings };
|
|
11087
|
+
}
|
|
11088
|
+
};
|
|
11089
|
+
}
|
|
11090
|
+
|
|
11091
|
+
// src/redact.ts
|
|
11092
|
+
import { DEFAULT_REDACTION_RULES, redactValue } from "@tangle-network/agent-eval/traces";
|
|
11093
|
+
var CODING_REDACTION_RULES = [
|
|
11094
|
+
{ id: "github-token", pattern: /\b(?:gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{22,})\b/g },
|
|
11095
|
+
{ id: "aws-akid", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
|
|
11096
|
+
{ id: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g },
|
|
11097
|
+
{ id: "jwt", pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
11098
|
+
{ id: "bearer", pattern: /\bBearer\s+[A-Za-z0-9._-]{16,}/gi },
|
|
11099
|
+
{
|
|
11100
|
+
id: "private-key",
|
|
11101
|
+
pattern: /-----BEGIN(?:[A-Z ]+)?PRIVATE KEY-----[\s\S]*?-----END(?:[A-Z ]+)?PRIVATE KEY-----/g
|
|
11102
|
+
},
|
|
11103
|
+
// Match the whole identifier because underscores prevent a word boundary before a secret suffix.
|
|
11104
|
+
{
|
|
11105
|
+
id: "assigned-secret",
|
|
11106
|
+
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
|
|
11107
|
+
},
|
|
11108
|
+
// Config-file secrets (YAML/INI line keys, JSON quoted keys) — the coverage the shell
|
|
11109
|
+
// rule above deliberately gave up to spare prose ("Narrative token: x") and type
|
|
11110
|
+
// annotations ("(token: string)"). The line anchor is the discriminator: config keys
|
|
11111
|
+
// start their line, prose colons never do; JSON keys are safe anywhere because both
|
|
11112
|
+
// sides are quoted. Identifiers must END in a secret word, same as the shell rule.
|
|
11113
|
+
{
|
|
11114
|
+
id: "config-secret",
|
|
11115
|
+
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
|
|
11116
|
+
},
|
|
11117
|
+
// Credentials embedded in URLs — common when a prompt pastes a curl/clone line.
|
|
11118
|
+
{ id: "url-userinfo", pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
|
|
11119
|
+
{
|
|
11120
|
+
id: "url-secret-param",
|
|
11121
|
+
pattern: /[?&](?:access[_-]?token|api[_-]?key|apikey|auth|token|secret|password|sig|signature)=[^&\s"'#)]+/gi
|
|
11122
|
+
}
|
|
11123
|
+
];
|
|
11124
|
+
var TRACES_REDACTION_RULES = [...DEFAULT_REDACTION_RULES, ...CODING_REDACTION_RULES];
|
|
11125
|
+
function redactSpans(spans, rules = TRACES_REDACTION_RULES) {
|
|
11126
|
+
const report = { redactionCount: 0, byRule: {} };
|
|
11127
|
+
const out = spans.map((s) => {
|
|
11128
|
+
const attributes2 = redactValue(s.attributes, rules, report).value;
|
|
11129
|
+
normalizeToolIoAttributes(attributes2);
|
|
11130
|
+
let status = s.status;
|
|
11131
|
+
if (status.message) {
|
|
11132
|
+
const m = redactValue(status.message, rules, report).value;
|
|
11133
|
+
if (typeof m === "string" && m !== status.message) status = { ...status, message: m };
|
|
11134
|
+
}
|
|
11135
|
+
return { ...s, attributes: attributes2, status };
|
|
11136
|
+
});
|
|
11137
|
+
return { spans: out, report };
|
|
11138
|
+
}
|
|
11139
|
+
async function applyRedactor(spans, redactor) {
|
|
11140
|
+
const capturedKeys = ["content", ...TOOL_IO_VALUE_KEYS];
|
|
11141
|
+
const fields = [];
|
|
11142
|
+
const texts = [];
|
|
11143
|
+
spans.forEach((s, i) => {
|
|
11144
|
+
for (const key of capturedKeys) {
|
|
11145
|
+
const value = s.attributes[key];
|
|
11146
|
+
if (typeof value === "string" && value.length > 0) {
|
|
11147
|
+
fields.push({ spanIndex: i, key });
|
|
11148
|
+
texts.push(value);
|
|
11149
|
+
}
|
|
11150
|
+
}
|
|
11151
|
+
if (s.status.message) {
|
|
11152
|
+
fields.push({ spanIndex: i, key: "status.message" });
|
|
11153
|
+
texts.push(s.status.message);
|
|
11154
|
+
}
|
|
11155
|
+
});
|
|
10681
11156
|
const out = spans.map((s) => ({ ...s, attributes: { ...s.attributes } }));
|
|
10682
11157
|
if (texts.length === 0) {
|
|
10683
11158
|
for (const span2 of out) normalizeToolIoAttributes(span2.attributes);
|
|
@@ -10703,7 +11178,7 @@ async function applyRedactor(spans, redactor) {
|
|
|
10703
11178
|
}
|
|
10704
11179
|
|
|
10705
11180
|
// src/file-export.ts
|
|
10706
|
-
import { createHash as
|
|
11181
|
+
import { createHash as createHash11 } from "crypto";
|
|
10707
11182
|
import { mkdtemp as mkdtemp4, readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
|
|
10708
11183
|
import { tmpdir as tmpdir5 } from "os";
|
|
10709
11184
|
import { join as join24 } from "path";
|
|
@@ -10741,7 +11216,7 @@ function stableJson(value) {
|
|
|
10741
11216
|
});
|
|
10742
11217
|
}
|
|
10743
11218
|
function hashId(value, chars = 16) {
|
|
10744
|
-
return
|
|
11219
|
+
return createHash11("sha256").update(stableJson(value)).digest("hex").slice(0, chars);
|
|
10745
11220
|
}
|
|
10746
11221
|
function isoTime2(value) {
|
|
10747
11222
|
if (typeof value === "string") {
|
|
@@ -10902,12 +11377,12 @@ function policyEvidenceToSpans(records) {
|
|
|
10902
11377
|
"traces.provenance.evidence_kind": record2.provenance.evidenceKind,
|
|
10903
11378
|
"traces.provenance.otlp_path": record2.provenance.otlpPath,
|
|
10904
11379
|
"traces.provenance.source_sha256": record2.provenance.sourceSha256,
|
|
10905
|
-
[
|
|
10906
|
-
[
|
|
10907
|
-
[
|
|
10908
|
-
[
|
|
10909
|
-
[
|
|
10910
|
-
[
|
|
11380
|
+
[ATTR.SUBJECT_KEY]: record2.repo.subjectKey,
|
|
11381
|
+
[ATTR.GIT_REPOSITORY]: record2.repo.repository,
|
|
11382
|
+
[ATTR.GIT_BRANCH_NAME]: record2.repo.branch,
|
|
11383
|
+
[ATTR.GIT_COMMIT]: record2.repo.commit,
|
|
11384
|
+
[ATTR.CWD]: record2.repo.cwd ?? record2.session.cwd,
|
|
11385
|
+
[ATTR.REPO_RESOLUTION_SOURCE]: record2.repo.resolutionSource
|
|
10911
11386
|
};
|
|
10912
11387
|
return span({
|
|
10913
11388
|
traceId,
|
|
@@ -11004,6 +11479,54 @@ function eventKind(row, type) {
|
|
|
11004
11479
|
if (lowered.includes("llm") || lowered === "token") return { kind: "LLM" };
|
|
11005
11480
|
return { kind: "CHAIN" };
|
|
11006
11481
|
}
|
|
11482
|
+
function sandboxEventsEnvironment(context, rows) {
|
|
11483
|
+
const ready = rows.find((row) => eventType(row) === "runtime.ready");
|
|
11484
|
+
const readyData = ready && isObject5(ready.data) ? ready.data : void 0;
|
|
11485
|
+
const pick = (keys) => findStringKey(context, keys, 2) ?? (readyData ? findStringKey(readyData, keys, 2) : void 0);
|
|
11486
|
+
const image = pick(["image"]);
|
|
11487
|
+
const imageDigest = pick(["imageDigest", "image_digest"]);
|
|
11488
|
+
const sandboxId = pick(["sandboxId", "sandbox_id"]);
|
|
11489
|
+
const cwd = pick(["cwd", "workspaceRoot", "workspace_root"]);
|
|
11490
|
+
if (!image && !imageDigest && !sandboxId && !cwd) return null;
|
|
11491
|
+
return {
|
|
11492
|
+
...image ? { image } : {},
|
|
11493
|
+
...imageDigest ? { imageDigest } : {},
|
|
11494
|
+
...sandboxId ? { sandboxId } : {},
|
|
11495
|
+
cwd: cwd ?? null
|
|
11496
|
+
};
|
|
11497
|
+
}
|
|
11498
|
+
function llmEventUsage(row) {
|
|
11499
|
+
const num = (keys) => {
|
|
11500
|
+
for (const key of keys) {
|
|
11501
|
+
const direct = numberValue(row[key]);
|
|
11502
|
+
if (direct !== void 0) return direct;
|
|
11503
|
+
for (const nest of ["data", "usage", "tokenUsage"]) {
|
|
11504
|
+
const container = row[nest];
|
|
11505
|
+
if (!isObject5(container)) continue;
|
|
11506
|
+
const value = numberValue(container[key]);
|
|
11507
|
+
if (value !== void 0) return value;
|
|
11508
|
+
for (const inner of ["usage", "tokenUsage"]) {
|
|
11509
|
+
const deep = container[inner];
|
|
11510
|
+
if (isObject5(deep)) {
|
|
11511
|
+
const deepValue = numberValue(deep[key]);
|
|
11512
|
+
if (deepValue !== void 0) return deepValue;
|
|
11513
|
+
}
|
|
11514
|
+
}
|
|
11515
|
+
}
|
|
11516
|
+
}
|
|
11517
|
+
return void 0;
|
|
11518
|
+
};
|
|
11519
|
+
const model = findStringKey(row, ["model"], 3);
|
|
11520
|
+
const inputTokens = num(["prompt_tokens", "input_tokens", "inputTokens"]);
|
|
11521
|
+
const outputTokens = num(["completion_tokens", "output_tokens", "outputTokens"]);
|
|
11522
|
+
const costUsd = num(["totalCostUsd", "total_cost_usd", "costUsd", "cost_usd"]);
|
|
11523
|
+
return {
|
|
11524
|
+
...model ? { model } : {},
|
|
11525
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
11526
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
11527
|
+
...costUsd !== void 0 ? { costUsd } : {}
|
|
11528
|
+
};
|
|
11529
|
+
}
|
|
11007
11530
|
function sandboxEventsToSpans(rows, wrapper) {
|
|
11008
11531
|
const context = wrapper ?? {};
|
|
11009
11532
|
const sessionId = findStringKey(context, ["session_id", "sessionId"], 3) ?? findStringKey(rows, ["session_id", "sessionId"], 4);
|
|
@@ -11074,13 +11597,15 @@ function sandboxEventsToSpans(rows, wrapper) {
|
|
|
11074
11597
|
tool: kind === "TOOL" ? tool ?? type : null,
|
|
11075
11598
|
step: index,
|
|
11076
11599
|
content: capText(stableJson(row)),
|
|
11077
|
-
extra
|
|
11600
|
+
extra,
|
|
11601
|
+
...kind === "LLM" ? llmEventUsage(row) : {}
|
|
11078
11602
|
}));
|
|
11079
11603
|
});
|
|
11604
|
+
stampEnvironmentAttrs(spans, sandboxEventsEnvironment(context, rows) ?? void 0);
|
|
11080
11605
|
if (!sessionId) return spans;
|
|
11081
11606
|
return spans.map((item) => ({
|
|
11082
11607
|
...item,
|
|
11083
|
-
attributes: { ...item.attributes, [
|
|
11608
|
+
attributes: { ...item.attributes, [ATTR.SESSION_ID]: sessionId }
|
|
11084
11609
|
}));
|
|
11085
11610
|
}
|
|
11086
11611
|
function statusCode2(value) {
|
|
@@ -11159,7 +11684,7 @@ function intelligenceSpansToSpans(rows) {
|
|
|
11159
11684
|
};
|
|
11160
11685
|
copyDefined(extra, {
|
|
11161
11686
|
"traces.row.id": stringValue3(row.id),
|
|
11162
|
-
[
|
|
11687
|
+
[ATTR.SESSION_ID]: sessionId,
|
|
11163
11688
|
"session.id": sessionId,
|
|
11164
11689
|
"run.id": stringValue3(row.run_id),
|
|
11165
11690
|
"thread.id": stringValue3(row.thread_id),
|
|
@@ -12534,6 +13059,47 @@ var SEVERITY_BADGE2 = {
|
|
|
12534
13059
|
low: "\u{1F535} LOW",
|
|
12535
13060
|
info: "\u2139\uFE0F INFO"
|
|
12536
13061
|
};
|
|
13062
|
+
function sessionReportSource(ref, spans, sessionIdOverride) {
|
|
13063
|
+
const root = spans.find((item) => item.parent_span_id === null) ?? spans[0];
|
|
13064
|
+
const prompt = spans.find(
|
|
13065
|
+
(item) => item.name === "user.prompt" && item.attributes[ACTOR_ATTR] === "human"
|
|
13066
|
+
) ?? spans.find((item) => item.name === "user.prompt") ?? spans.find(
|
|
13067
|
+
(item) => item.attributes["span.type"] === "interaction" && typeof item.attributes.content === "string"
|
|
13068
|
+
);
|
|
13069
|
+
const content = typeof prompt?.attributes.content === "string" ? prompt.attributes.content : "";
|
|
13070
|
+
const firstLine = content.split(/\r?\n/, 1)[0].trim();
|
|
13071
|
+
const subject = firstLine.length > 240 ? `${firstLine.slice(0, 240)}\u2026 [+${firstLine.length - 240} chars]` : firstLine;
|
|
13072
|
+
const role = root?.attributes["traces.session.role"];
|
|
13073
|
+
const parentSessionId = root?.attributes["traces.parent_session_id"];
|
|
13074
|
+
const relationship = describeSessionRelationship(ref, spans);
|
|
13075
|
+
const corruptionDigest2 = root?.attributes[ATTR.CORRUPTION_DIGEST];
|
|
13076
|
+
const sessionId = [
|
|
13077
|
+
sessionIdOverride,
|
|
13078
|
+
root ? sessionIdFromAttributes(root.attributes) : void 0,
|
|
13079
|
+
root?.trace_id,
|
|
13080
|
+
ref.sessionId
|
|
13081
|
+
].find((value) => typeof value === "string" && value.length > 0);
|
|
13082
|
+
return {
|
|
13083
|
+
sessionId,
|
|
13084
|
+
path: ref.path,
|
|
13085
|
+
subject,
|
|
13086
|
+
role: role === "operator" || role === "child" ? role : "unknown",
|
|
13087
|
+
...typeof parentSessionId === "string" ? { parentSessionId } : {},
|
|
13088
|
+
childSessionIds: relationship.childSessionIds,
|
|
13089
|
+
...relationship.depth !== void 0 ? { depth: relationship.depth } : {},
|
|
13090
|
+
...relationship.agentNickname ? { agentNickname: relationship.agentNickname } : {},
|
|
13091
|
+
...relationship.agentRole ? { agentRole: relationship.agentRole } : {},
|
|
13092
|
+
...relationship.agentPath ? { agentPath: relationship.agentPath } : {},
|
|
13093
|
+
...relationship.taskScope ? { taskScope: relationship.taskScope } : {},
|
|
13094
|
+
...relationship.turnId ? { turnId: relationship.turnId } : {},
|
|
13095
|
+
integrity: ref.integrity?.status ?? "complete",
|
|
13096
|
+
...ref.integrity ? {
|
|
13097
|
+
corruptionCount: ref.integrity.corruptions.length,
|
|
13098
|
+
...typeof corruptionDigest2 === "string" ? { corruptionDigest: corruptionDigest2 } : {},
|
|
13099
|
+
corruptions: ref.integrity.corruptions.slice(0, CORRUPTION_RECEIPT_DISPLAY_LIMIT)
|
|
13100
|
+
} : {}
|
|
13101
|
+
};
|
|
13102
|
+
}
|
|
12537
13103
|
function deterministicSummaryText(summary) {
|
|
12538
13104
|
const parts = [];
|
|
12539
13105
|
if (summary.stuckLoops > 0) parts.push(`${summary.stuckLoops} stuck loop(s)`);
|
|
@@ -13684,27 +14250,27 @@ async function saveReport(path, report) {
|
|
|
13684
14250
|
}
|
|
13685
14251
|
|
|
13686
14252
|
// src/live.ts
|
|
13687
|
-
import { createHash as
|
|
14253
|
+
import { createHash as createHash12 } from "crypto";
|
|
13688
14254
|
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
14255
|
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
14256
|
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((
|
|
14257
|
+
var sleep = (ms2, signal) => new Promise((resolve10) => {
|
|
13692
14258
|
if (signal?.aborted) {
|
|
13693
|
-
|
|
14259
|
+
resolve10();
|
|
13694
14260
|
return;
|
|
13695
14261
|
}
|
|
13696
14262
|
const onAbort = () => {
|
|
13697
14263
|
clearTimeout(t);
|
|
13698
|
-
|
|
14264
|
+
resolve10();
|
|
13699
14265
|
};
|
|
13700
14266
|
const t = setTimeout(() => {
|
|
13701
14267
|
signal?.removeEventListener("abort", onAbort);
|
|
13702
|
-
|
|
14268
|
+
resolve10();
|
|
13703
14269
|
}, ms2);
|
|
13704
14270
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
13705
14271
|
});
|
|
13706
14272
|
function hash(parts) {
|
|
13707
|
-
return
|
|
14273
|
+
return createHash12("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
|
|
13708
14274
|
}
|
|
13709
14275
|
function asString(value) {
|
|
13710
14276
|
if (typeof value === "string" && value.trim()) return value;
|
|
@@ -15174,13 +15740,13 @@ function aborted(signal) {
|
|
|
15174
15740
|
return signal !== void 0 && signal.aborted;
|
|
15175
15741
|
}
|
|
15176
15742
|
function sleep2(msDelay, signal) {
|
|
15177
|
-
return new Promise((
|
|
15178
|
-
const timer = setTimeout(
|
|
15743
|
+
return new Promise((resolve10) => {
|
|
15744
|
+
const timer = setTimeout(resolve10, msDelay);
|
|
15179
15745
|
signal?.addEventListener(
|
|
15180
15746
|
"abort",
|
|
15181
15747
|
() => {
|
|
15182
15748
|
clearTimeout(timer);
|
|
15183
|
-
|
|
15749
|
+
resolve10();
|
|
15184
15750
|
},
|
|
15185
15751
|
{ once: true }
|
|
15186
15752
|
);
|
|
@@ -15520,11 +16086,11 @@ async function collectSessionWorkflow(adapter, seeds, options = {}) {
|
|
|
15520
16086
|
}
|
|
15521
16087
|
|
|
15522
16088
|
// src/session-selection.ts
|
|
15523
|
-
import { createHash as
|
|
16089
|
+
import { createHash as createHash13 } from "crypto";
|
|
15524
16090
|
import { createReadStream as createReadStream2 } from "fs";
|
|
15525
16091
|
async function fileSha256(path, signal) {
|
|
15526
16092
|
signal?.throwIfAborted();
|
|
15527
|
-
const hash2 =
|
|
16093
|
+
const hash2 = createHash13("sha256");
|
|
15528
16094
|
try {
|
|
15529
16095
|
for await (const chunk of createReadStream2(path, { signal })) {
|
|
15530
16096
|
signal?.throwIfAborted();
|
|
@@ -15552,7 +16118,7 @@ async function sourceSnapshot(adapter, ref, signal) {
|
|
|
15552
16118
|
signal?.throwIfAborted();
|
|
15553
16119
|
files.push({ path, sha256: await fileSha256(path, signal) });
|
|
15554
16120
|
}
|
|
15555
|
-
const sha2562 = files.length === 1 ? files[0].sha256 :
|
|
16121
|
+
const sha2562 = files.length === 1 ? files[0].sha256 : createHash13("sha256").update(JSON.stringify(files)).digest("hex");
|
|
15556
16122
|
return { sha256: sha2562, files };
|
|
15557
16123
|
}
|
|
15558
16124
|
function summarizeWorkflows(workflows) {
|
|
@@ -15813,14 +16379,30 @@ async function collectContextRoot(root) {
|
|
|
15813
16379
|
const summary = await summarizeFile(join31(evolve, name), "evolve-jsonl");
|
|
15814
16380
|
if (summary) files.push(summary);
|
|
15815
16381
|
}
|
|
15816
|
-
for (const name of ["scorecard.json"]) {
|
|
16382
|
+
for (const name of ["scorecard.json", "current.json"]) {
|
|
15817
16383
|
const summary = await summarizeFile(join31(evolve, name), "evolve-json");
|
|
15818
16384
|
if (summary) files.push(summary);
|
|
15819
16385
|
}
|
|
15820
16386
|
files.push(...await walkMarkdown(join31(evolve, "reflections"), "reflection"));
|
|
15821
16387
|
files.push(...await walkMarkdown(join31(evolve, "handoffs"), "handoff"));
|
|
16388
|
+
for (const name of await listEvolveRootFiles(evolve)) {
|
|
16389
|
+
if (/^handoff-.*\.md$/.test(name)) {
|
|
16390
|
+
const summary = await summarizeFile(join31(evolve, name), "handoff");
|
|
16391
|
+
if (summary) files.push(summary);
|
|
16392
|
+
}
|
|
16393
|
+
}
|
|
16394
|
+
const progress = await summarizeFile(join31(evolve, "progress.md"), "other");
|
|
16395
|
+
if (progress) files.push(progress);
|
|
15822
16396
|
return { root, files: files.sort((a, b) => a.path.localeCompare(b.path)) };
|
|
15823
16397
|
}
|
|
16398
|
+
async function listEvolveRootFiles(evolve) {
|
|
16399
|
+
try {
|
|
16400
|
+
const entries = await readdir18(evolve, { withFileTypes: true });
|
|
16401
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => entry.name).sort();
|
|
16402
|
+
} catch {
|
|
16403
|
+
return [];
|
|
16404
|
+
}
|
|
16405
|
+
}
|
|
15824
16406
|
async function collectContextIndex(records) {
|
|
15825
16407
|
const rootSet = /* @__PURE__ */ new Set();
|
|
15826
16408
|
for (const record2 of records) {
|
|
@@ -15869,14 +16451,641 @@ async function writeSessionIndexFile(index, outPath) {
|
|
|
15869
16451
|
return path;
|
|
15870
16452
|
}
|
|
15871
16453
|
|
|
16454
|
+
// src/bundle.ts
|
|
16455
|
+
import { createHash as createHash14 } from "crypto";
|
|
16456
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
16457
|
+
import { cp, mkdir as mkdir5, readdir as readdir19, readFile as readFile14, stat as stat17, writeFile as writeFile9 } from "fs/promises";
|
|
16458
|
+
import { join as join32, relative as relative5, resolve as resolve8, sep as sep2 } from "path";
|
|
16459
|
+
var SESSION_WINDOW_PAD_MS = 15 * 60 * 1e3;
|
|
16460
|
+
var KNOWN_LIMITS = [
|
|
16461
|
+
"'what was decided' is prose-only: handoff, progress, and reflection files are copied as markdown with no structured decision extraction",
|
|
16462
|
+
"'what was measured' is split between experiments.jsonl free-text fields and progress/handoff markdown tables",
|
|
16463
|
+
"session-to-ledger join is by timestamp/cwd inference: ledger rows are window-sliced, not linked by transcript path",
|
|
16464
|
+
"progress.md is copied whole; no session-dated section extraction is performed",
|
|
16465
|
+
"open pull-request state is not captured: the bundler makes no network calls"
|
|
16466
|
+
];
|
|
16467
|
+
function sha256Hex(bytes) {
|
|
16468
|
+
return createHash14("sha256").update(bytes).digest("hex");
|
|
16469
|
+
}
|
|
16470
|
+
function bundlePath(...parts) {
|
|
16471
|
+
return parts.join("/");
|
|
16472
|
+
}
|
|
16473
|
+
async function statOrNull(path) {
|
|
16474
|
+
try {
|
|
16475
|
+
return await stat17(path);
|
|
16476
|
+
} catch (error) {
|
|
16477
|
+
if (isMissingPathError(error)) return null;
|
|
16478
|
+
if (error.code === "ENOTDIR") return null;
|
|
16479
|
+
throw error;
|
|
16480
|
+
}
|
|
16481
|
+
}
|
|
16482
|
+
function sliceJsonlByWindow(text2, windowStartMs, windowEndMs) {
|
|
16483
|
+
const kept = [];
|
|
16484
|
+
let totalRows = 0;
|
|
16485
|
+
let unparseableTsRows = 0;
|
|
16486
|
+
for (const line of text2.split("\n")) {
|
|
16487
|
+
if (!line.trim()) continue;
|
|
16488
|
+
totalRows += 1;
|
|
16489
|
+
let tsMs = Number.NaN;
|
|
16490
|
+
try {
|
|
16491
|
+
const row = JSON.parse(line);
|
|
16492
|
+
const ts = row !== null && typeof row === "object" && !Array.isArray(row) ? row.ts : void 0;
|
|
16493
|
+
if (typeof ts === "string") tsMs = Date.parse(ts);
|
|
16494
|
+
} catch {
|
|
16495
|
+
}
|
|
16496
|
+
if (Number.isNaN(tsMs)) {
|
|
16497
|
+
unparseableTsRows += 1;
|
|
16498
|
+
continue;
|
|
16499
|
+
}
|
|
16500
|
+
if (tsMs >= windowStartMs && tsMs <= windowEndMs) kept.push(line);
|
|
16501
|
+
}
|
|
16502
|
+
return { kept, totalRows, unparseableTsRows };
|
|
16503
|
+
}
|
|
16504
|
+
async function runGitLog(cwd, sinceIso, untilIso) {
|
|
16505
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
16506
|
+
const { promisify: promisify2 } = await import("util");
|
|
16507
|
+
const run = promisify2(execFile2);
|
|
16508
|
+
try {
|
|
16509
|
+
const { stdout } = await run(
|
|
16510
|
+
"git",
|
|
16511
|
+
["-C", cwd, "log", `--since=${sinceIso}`, `--until=${untilIso}`, "--date=iso-strict", "--pretty=format:%H %ad %an %s"],
|
|
16512
|
+
{ timeout: 5e3 }
|
|
16513
|
+
);
|
|
16514
|
+
return { succeeded: true, stdout };
|
|
16515
|
+
} catch (error) {
|
|
16516
|
+
return { succeeded: false, error: error instanceof Error ? error.message.split("\n", 1)[0] : String(error) };
|
|
16517
|
+
}
|
|
16518
|
+
}
|
|
16519
|
+
async function listSessionBundleFiles(root) {
|
|
16520
|
+
const out = [];
|
|
16521
|
+
const pending = [resolve8(root)];
|
|
16522
|
+
while (pending.length > 0) {
|
|
16523
|
+
const dir = pending.pop();
|
|
16524
|
+
for (const entry of await readdir19(dir, { withFileTypes: true })) {
|
|
16525
|
+
const path = join32(dir, entry.name);
|
|
16526
|
+
if (entry.isDirectory()) pending.push(path);
|
|
16527
|
+
else if (entry.isFile()) out.push(relative5(resolve8(root), path).split(sep2).join("/"));
|
|
16528
|
+
}
|
|
16529
|
+
}
|
|
16530
|
+
return out.sort();
|
|
16531
|
+
}
|
|
16532
|
+
async function sliceLedgerJsonl(ctx, name) {
|
|
16533
|
+
const source = join32(ctx.evolveDir, name);
|
|
16534
|
+
const s = await statOrNull(source);
|
|
16535
|
+
if (!s?.isFile()) {
|
|
16536
|
+
ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
|
|
16537
|
+
return;
|
|
16538
|
+
}
|
|
16539
|
+
const text2 = await readFile14(source, "utf8");
|
|
16540
|
+
const target = join32(ctx.ledgerDir, name);
|
|
16541
|
+
if (ctx.windowStartMs === null || ctx.windowEndMs === null) {
|
|
16542
|
+
await writeFile9(target, text2, "utf8");
|
|
16543
|
+
ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
|
|
16544
|
+
return;
|
|
16545
|
+
}
|
|
16546
|
+
const slice = sliceJsonlByWindow(text2, ctx.windowStartMs, ctx.windowEndMs);
|
|
16547
|
+
await writeFile9(target, slice.kept.length > 0 ? `${slice.kept.join("\n")}
|
|
16548
|
+
` : "", "utf8");
|
|
16549
|
+
ctx.slices.push({
|
|
16550
|
+
path: bundlePath("ledger", name),
|
|
16551
|
+
source,
|
|
16552
|
+
rule: "session-window-rows",
|
|
16553
|
+
totalRows: slice.totalRows,
|
|
16554
|
+
keptRows: slice.kept.length,
|
|
16555
|
+
unparseableTsRows: slice.unparseableTsRows
|
|
16556
|
+
});
|
|
16557
|
+
}
|
|
16558
|
+
async function copyLedgerWhole(ctx, name) {
|
|
16559
|
+
const source = join32(ctx.evolveDir, name);
|
|
16560
|
+
const s = await statOrNull(source);
|
|
16561
|
+
if (!s?.isFile()) {
|
|
16562
|
+
ctx.absent.push({ path: bundlePath("ledger", name), reason: `no ${name} at ${source}` });
|
|
16563
|
+
return;
|
|
16564
|
+
}
|
|
16565
|
+
await cp(source, join32(ctx.ledgerDir, name));
|
|
16566
|
+
ctx.slices.push({ path: bundlePath("ledger", name), source, rule: "copied-whole" });
|
|
16567
|
+
}
|
|
16568
|
+
async function copyLatestHandoff(ctx) {
|
|
16569
|
+
let names;
|
|
16570
|
+
try {
|
|
16571
|
+
names = (await readdir19(ctx.evolveDir, { withFileTypes: true })).filter((entry) => entry.isFile() && /^handoff-.*\.md$/.test(entry.name)).map((entry) => entry.name).sort();
|
|
16572
|
+
} catch {
|
|
16573
|
+
names = [];
|
|
16574
|
+
}
|
|
16575
|
+
const latest = names[names.length - 1];
|
|
16576
|
+
if (!latest) {
|
|
16577
|
+
ctx.absent.push({
|
|
16578
|
+
path: bundlePath("ledger", "handoff-*.md"),
|
|
16579
|
+
reason: `no flat handoff-*.md files in ${ctx.evolveDir}`
|
|
16580
|
+
});
|
|
16581
|
+
return;
|
|
16582
|
+
}
|
|
16583
|
+
const source = join32(ctx.evolveDir, latest);
|
|
16584
|
+
await cp(source, join32(ctx.ledgerDir, latest));
|
|
16585
|
+
ctx.slices.push({ path: bundlePath("ledger", latest), source, rule: "latest-by-name" });
|
|
16586
|
+
}
|
|
16587
|
+
async function copyWindowReflections(ctx) {
|
|
16588
|
+
const reflectionsDir = join32(ctx.evolveDir, "reflections");
|
|
16589
|
+
const s = await statOrNull(reflectionsDir);
|
|
16590
|
+
if (!s?.isDirectory()) {
|
|
16591
|
+
ctx.absent.push({
|
|
16592
|
+
path: bundlePath("ledger", "reflections"),
|
|
16593
|
+
reason: `no reflections directory at ${reflectionsDir}`
|
|
16594
|
+
});
|
|
16595
|
+
return;
|
|
16596
|
+
}
|
|
16597
|
+
const names = (await readdir19(reflectionsDir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort();
|
|
16598
|
+
const windowed = ctx.windowStartMs === null || ctx.windowEndMs === null ? names : names.filter((name) => {
|
|
16599
|
+
const date = name.match(/(\d{4}-\d{2}-\d{2})/)?.[1];
|
|
16600
|
+
if (!date) return false;
|
|
16601
|
+
const startDate = new Date(ctx.windowStartMs).toISOString().slice(0, 10);
|
|
16602
|
+
const endDate = new Date(ctx.windowEndMs).toISOString().slice(0, 10);
|
|
16603
|
+
return date >= startDate && date <= endDate;
|
|
16604
|
+
});
|
|
16605
|
+
if (windowed.length === 0) {
|
|
16606
|
+
ctx.absent.push({
|
|
16607
|
+
path: bundlePath("ledger", "reflections"),
|
|
16608
|
+
reason: `no reflections dated inside the session window in ${reflectionsDir}`
|
|
16609
|
+
});
|
|
16610
|
+
return;
|
|
16611
|
+
}
|
|
16612
|
+
await mkdir5(join32(ctx.ledgerDir, "reflections"), { recursive: true });
|
|
16613
|
+
const rule = ctx.windowStartMs === null ? "copied-whole" : "session-window-dated-files";
|
|
16614
|
+
for (const name of windowed) {
|
|
16615
|
+
const source = join32(reflectionsDir, name);
|
|
16616
|
+
await cp(source, join32(ctx.ledgerDir, "reflections", name));
|
|
16617
|
+
ctx.slices.push({ path: bundlePath("ledger", "reflections", name), source, rule });
|
|
16618
|
+
}
|
|
16619
|
+
}
|
|
16620
|
+
async function assembleSessionBundle(opts) {
|
|
16621
|
+
const { adapter, ref } = opts;
|
|
16622
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16623
|
+
let transcriptBytes;
|
|
16624
|
+
try {
|
|
16625
|
+
transcriptBytes = await readFile14(ref.path);
|
|
16626
|
+
} catch (error) {
|
|
16627
|
+
if (isMissingPathError(error)) {
|
|
16628
|
+
throw new Error(
|
|
16629
|
+
`session transcript not found at ${ref.path} \u2014 a bundle cannot be assembled without its transcript`
|
|
16630
|
+
);
|
|
16631
|
+
}
|
|
16632
|
+
throw error;
|
|
16633
|
+
}
|
|
16634
|
+
const transcriptSha256 = sha256Hex(transcriptBytes);
|
|
16635
|
+
const spans = await parseSession(adapter, ref, { signal: opts.signal });
|
|
16636
|
+
const outDir = resolve8(opts.outDir);
|
|
16637
|
+
await mkdir5(outDir, { recursive: true });
|
|
16638
|
+
if ((await readdir19(outDir)).length > 0) {
|
|
16639
|
+
throw new Error(
|
|
16640
|
+
`bundle output directory ${outDir} is not empty \u2014 pass a new directory so one bundle holds exactly one session`
|
|
16641
|
+
);
|
|
16642
|
+
}
|
|
16643
|
+
const absent = [];
|
|
16644
|
+
const slices = [];
|
|
16645
|
+
await mkdir5(join32(outDir, "session"), { recursive: true });
|
|
16646
|
+
await writeFile9(join32(outDir, "session", "transcript.jsonl"), transcriptBytes);
|
|
16647
|
+
const subagentsSource = join32(ref.path.replace(/\.jsonl$/, ""), "subagents");
|
|
16648
|
+
if ((await statOrNull(subagentsSource))?.isDirectory()) {
|
|
16649
|
+
await cp(subagentsSource, join32(outDir, "session", "subagents"), { recursive: true });
|
|
16650
|
+
} else {
|
|
16651
|
+
absent.push({ path: bundlePath("session", "subagents"), reason: `no subagents directory at ${subagentsSource}` });
|
|
16652
|
+
}
|
|
16653
|
+
await mkdir5(join32(outDir, "derived"), { recursive: true });
|
|
16654
|
+
const otlpPath = join32(outDir, "derived", "trace.otlp.jsonl");
|
|
16655
|
+
const investigation = await runTraceInvestigation({
|
|
16656
|
+
spans,
|
|
16657
|
+
harness: ref.harness,
|
|
16658
|
+
sources: [sessionReportSource(ref, spans)],
|
|
16659
|
+
cwds: ref.cwd ? [ref.cwd] : [],
|
|
16660
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16661
|
+
otlpOutPath: otlpPath,
|
|
16662
|
+
generatedAt,
|
|
16663
|
+
signal: opts.signal,
|
|
16664
|
+
log: opts.log
|
|
16665
|
+
});
|
|
16666
|
+
await writeFile9(join32(outDir, "derived", "report.md"), investigation.report, "utf8");
|
|
16667
|
+
const evidence3 = await buildPolicyEvidenceRecord(ref, spans, {
|
|
16668
|
+
generatedAt,
|
|
16669
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16670
|
+
// Bundle-relative pointer: the bundle must stay internally valid when moved.
|
|
16671
|
+
otlpPath: bundlePath("derived", "trace.otlp.jsonl"),
|
|
16672
|
+
sourceSha256: transcriptSha256
|
|
16673
|
+
});
|
|
16674
|
+
await writeFile9(join32(outDir, "derived", "evidence.jsonl"), serializePolicyEvidence([evidence3]), "utf8");
|
|
16675
|
+
const index = await buildSessionIndexFromRows([{ ref, spans }], {
|
|
16676
|
+
generatedAt,
|
|
16677
|
+
minLoopOccurrences: opts.minLoopOccurrences,
|
|
16678
|
+
selection: { command: "bundle", harness: ref.harness, session: ref.sessionId }
|
|
16679
|
+
});
|
|
16680
|
+
await writeFile9(join32(outDir, "derived", "session-index.json"), serializeSessionIndex(index), "utf8");
|
|
16681
|
+
const { firstSpanAt, lastSpanAt } = evidence3.metrics;
|
|
16682
|
+
const windowStartMs = firstSpanAt !== null ? Date.parse(firstSpanAt) - SESSION_WINDOW_PAD_MS : null;
|
|
16683
|
+
const windowEndMs = lastSpanAt !== null ? Date.parse(lastSpanAt) + SESSION_WINDOW_PAD_MS : null;
|
|
16684
|
+
const contextRoot = await findContextRoot(ref.cwd);
|
|
16685
|
+
if (!contextRoot) {
|
|
16686
|
+
absent.push({ path: "ledger", reason: `no context root found from session cwd ${ref.cwd ?? "(unknown)"}` });
|
|
16687
|
+
} else {
|
|
16688
|
+
const evolveDir = join32(contextRoot, ".evolve");
|
|
16689
|
+
if (!(await statOrNull(evolveDir))?.isDirectory()) {
|
|
16690
|
+
absent.push({ path: "ledger", reason: `no .evolve ledger at ${evolveDir}` });
|
|
16691
|
+
} else {
|
|
16692
|
+
const ledgerDir = join32(outDir, "ledger");
|
|
16693
|
+
await mkdir5(ledgerDir, { recursive: true });
|
|
16694
|
+
const ctx = { evolveDir, ledgerDir, windowStartMs, windowEndMs, slices, absent };
|
|
16695
|
+
await sliceLedgerJsonl(ctx, "experiments.jsonl");
|
|
16696
|
+
await sliceLedgerJsonl(ctx, "skill-runs.jsonl");
|
|
16697
|
+
await copyLedgerWhole(ctx, "current.json");
|
|
16698
|
+
await copyLedgerWhole(ctx, "scorecard.json");
|
|
16699
|
+
await copyLedgerWhole(ctx, "progress.md");
|
|
16700
|
+
await copyLatestHandoff(ctx);
|
|
16701
|
+
await copyWindowReflections(ctx);
|
|
16702
|
+
}
|
|
16703
|
+
}
|
|
16704
|
+
const gitLogPath = bundlePath("repo", "git-log.txt");
|
|
16705
|
+
if (!ref.cwd) {
|
|
16706
|
+
absent.push({ path: gitLogPath, reason: "session has no recorded cwd to read git history from" });
|
|
16707
|
+
} else if (windowStartMs === null || windowEndMs === null) {
|
|
16708
|
+
absent.push({ path: gitLogPath, reason: "session window unavailable (spans carry no timestamps)" });
|
|
16709
|
+
} else {
|
|
16710
|
+
const log = await runGitLog(ref.cwd, new Date(windowStartMs).toISOString(), new Date(windowEndMs).toISOString());
|
|
16711
|
+
if (log.succeeded) {
|
|
16712
|
+
await mkdir5(join32(outDir, "repo"), { recursive: true });
|
|
16713
|
+
await writeFile9(join32(outDir, "repo", "git-log.txt"), log.stdout ? `${log.stdout}
|
|
16714
|
+
` : "", "utf8");
|
|
16715
|
+
} else {
|
|
16716
|
+
absent.push({ path: gitLogPath, reason: `git log failed in ${ref.cwd}: ${log.error}` });
|
|
16717
|
+
}
|
|
16718
|
+
}
|
|
16719
|
+
const files = await hashSessionBundleFiles(outDir);
|
|
16720
|
+
const manifest = {
|
|
16721
|
+
schemaVersion: 2,
|
|
16722
|
+
kind: "traces.session_bundle",
|
|
16723
|
+
view: "full",
|
|
16724
|
+
createdAt: generatedAt,
|
|
16725
|
+
provenance: {
|
|
16726
|
+
sessionId: ref.sessionId,
|
|
16727
|
+
harness: ref.harness,
|
|
16728
|
+
cwd: ref.cwd,
|
|
16729
|
+
transcriptPath: resolve8(ref.path),
|
|
16730
|
+
transcriptSha256,
|
|
16731
|
+
contextRoot,
|
|
16732
|
+
tracesVersion: tracesPackageVersion(),
|
|
16733
|
+
sessionWindow: { firstSpanAt, lastSpanAt, padMs: SESSION_WINDOW_PAD_MS }
|
|
16734
|
+
},
|
|
16735
|
+
files,
|
|
16736
|
+
absent,
|
|
16737
|
+
// The full view is the whole record: it drops nothing, and says so rather
|
|
16738
|
+
// than leaving a reader to infer completeness from a missing field.
|
|
16739
|
+
excluded: [],
|
|
16740
|
+
ledgerSlices: slices,
|
|
16741
|
+
knownLimits: KNOWN_LIMITS
|
|
16742
|
+
};
|
|
16743
|
+
const manifestPath = await writeSessionBundleManifest(outDir, manifest);
|
|
16744
|
+
return { directory: outDir, manifestPath, manifest };
|
|
16745
|
+
}
|
|
16746
|
+
async function hashSessionBundleFiles(root) {
|
|
16747
|
+
const files = [];
|
|
16748
|
+
for (const path of await listSessionBundleFiles(root)) {
|
|
16749
|
+
const bytes = await readFile14(join32(root, path));
|
|
16750
|
+
files.push({ path, bytes: bytes.length, sha256: sha256Hex(bytes) });
|
|
16751
|
+
}
|
|
16752
|
+
return files;
|
|
16753
|
+
}
|
|
16754
|
+
async function writeSessionBundleManifest(outDir, manifest) {
|
|
16755
|
+
const manifestPath = join32(outDir, "manifest.json");
|
|
16756
|
+
await writeFile9(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
16757
|
+
`, "utf8");
|
|
16758
|
+
return manifestPath;
|
|
16759
|
+
}
|
|
16760
|
+
async function readSessionBundleManifest(bundleDir) {
|
|
16761
|
+
const path = join32(resolve8(bundleDir), "manifest.json");
|
|
16762
|
+
let text2;
|
|
16763
|
+
try {
|
|
16764
|
+
text2 = await readFile14(path, "utf8");
|
|
16765
|
+
} catch (error) {
|
|
16766
|
+
if (isMissingPathError(error)) {
|
|
16767
|
+
throw new Error(`no manifest.json at ${path} \u2014 ${bundleDir} is not a traces session bundle`);
|
|
16768
|
+
}
|
|
16769
|
+
throw error;
|
|
16770
|
+
}
|
|
16771
|
+
let parsed;
|
|
16772
|
+
try {
|
|
16773
|
+
parsed = JSON.parse(text2);
|
|
16774
|
+
} catch (error) {
|
|
16775
|
+
throw new Error(`${path} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
16776
|
+
}
|
|
16777
|
+
const manifest = parsed;
|
|
16778
|
+
if (manifest?.kind !== "traces.session_bundle") {
|
|
16779
|
+
throw new Error(`${path} has kind ${JSON.stringify(manifest?.kind)}, expected "traces.session_bundle"`);
|
|
16780
|
+
}
|
|
16781
|
+
if (manifest.schemaVersion !== 2) {
|
|
16782
|
+
throw new Error(
|
|
16783
|
+
`${path} is bundle schemaVersion ${JSON.stringify(manifest.schemaVersion)}; this traces reads 2. Re-run \`traces bundle\` to assemble it with the view-aware manifest.`
|
|
16784
|
+
);
|
|
16785
|
+
}
|
|
16786
|
+
if (manifest.view !== "full" && manifest.view !== "evidence-only") {
|
|
16787
|
+
throw new Error(`${path} has view ${JSON.stringify(manifest.view)}, expected "full" or "evidence-only"`);
|
|
16788
|
+
}
|
|
16789
|
+
return manifest;
|
|
16790
|
+
}
|
|
16791
|
+
var cachedVersion;
|
|
16792
|
+
function tracesPackageVersion() {
|
|
16793
|
+
if (cachedVersion) return cachedVersion;
|
|
16794
|
+
const pkg = JSON.parse(
|
|
16795
|
+
readFileSync5(new URL("../package.json", import.meta.url), "utf8")
|
|
16796
|
+
);
|
|
16797
|
+
if (typeof pkg.version !== "string" || !pkg.version) throw new Error("package.json is missing version");
|
|
16798
|
+
cachedVersion = pkg.version;
|
|
16799
|
+
return cachedVersion;
|
|
16800
|
+
}
|
|
16801
|
+
|
|
16802
|
+
// src/bundle-view.ts
|
|
16803
|
+
import { createHash as createHash15 } from "crypto";
|
|
16804
|
+
import { mkdir as mkdir6, readdir as readdir20, readFile as readFile15, writeFile as writeFile10 } from "fs/promises";
|
|
16805
|
+
import { dirname as dirname9, join as join33, resolve as resolve9 } from "path";
|
|
16806
|
+
var EVIDENCE_ONLY_ALLOW_LIST = [
|
|
16807
|
+
{
|
|
16808
|
+
path: "derived/session-index.json",
|
|
16809
|
+
owned: true,
|
|
16810
|
+
carries: "per-session counts, tool names, models, and nearby-context file metadata"
|
|
16811
|
+
},
|
|
16812
|
+
{
|
|
16813
|
+
path: "derived/evidence.jsonl",
|
|
16814
|
+
owned: true,
|
|
16815
|
+
carries: "execution statistics, token and tool totals, loop and error counts"
|
|
16816
|
+
},
|
|
16817
|
+
{
|
|
16818
|
+
path: "ledger/current.json",
|
|
16819
|
+
owned: false,
|
|
16820
|
+
carries: "the repo ledger current-state record"
|
|
16821
|
+
},
|
|
16822
|
+
{
|
|
16823
|
+
path: "ledger/scorecard.json",
|
|
16824
|
+
owned: false,
|
|
16825
|
+
carries: "the repo ledger scorecard rows"
|
|
16826
|
+
},
|
|
16827
|
+
{
|
|
16828
|
+
path: "ledger/skill-runs.jsonl",
|
|
16829
|
+
owned: false,
|
|
16830
|
+
carries: "skill-run rows: skill name, timestamp, and outcome fields"
|
|
16831
|
+
}
|
|
16832
|
+
];
|
|
16833
|
+
var EXCLUSION_RULES = [
|
|
16834
|
+
{
|
|
16835
|
+
matches: (path) => path === "session/transcript.jsonl" || path.startsWith("session/"),
|
|
16836
|
+
rule: "session-source",
|
|
16837
|
+
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"
|
|
16838
|
+
},
|
|
16839
|
+
{
|
|
16840
|
+
matches: (path) => path === "derived/report.md",
|
|
16841
|
+
rule: "session-text-derived",
|
|
16842
|
+
reason: "the deterministic report quotes the session verbatim: the first prompt line and evidence excerpts per finding"
|
|
16843
|
+
},
|
|
16844
|
+
{
|
|
16845
|
+
matches: (path) => path === "derived/trace.otlp.jsonl",
|
|
16846
|
+
rule: "session-text-derived",
|
|
16847
|
+
reason: "spans carry input.value, output.value and content: the full prompt and response text of every turn"
|
|
16848
|
+
},
|
|
16849
|
+
{
|
|
16850
|
+
matches: (path) => path === "ledger/progress.md",
|
|
16851
|
+
rule: "authored-prose",
|
|
16852
|
+
reason: "progress notes are written at session close and restate what the session concluded"
|
|
16853
|
+
},
|
|
16854
|
+
{
|
|
16855
|
+
matches: (path) => /^ledger\/handoff-.*\.md$/.test(path),
|
|
16856
|
+
rule: "authored-prose",
|
|
16857
|
+
reason: "a handoff is the session conclusion in prose"
|
|
16858
|
+
},
|
|
16859
|
+
{
|
|
16860
|
+
matches: (path) => path.startsWith("ledger/reflections/"),
|
|
16861
|
+
rule: "authored-prose",
|
|
16862
|
+
reason: "a reflection is the session conclusion in prose"
|
|
16863
|
+
},
|
|
16864
|
+
{
|
|
16865
|
+
matches: (path) => path === "ledger/experiments.jsonl",
|
|
16866
|
+
rule: "authored-prose",
|
|
16867
|
+
reason: "experiment rows carry free-text verdict and note fields that restate what the session concluded"
|
|
16868
|
+
},
|
|
16869
|
+
{
|
|
16870
|
+
matches: (path) => path === "repo/git-log.txt",
|
|
16871
|
+
rule: "authored-prose",
|
|
16872
|
+
reason: "commit subjects are prose the session authored"
|
|
16873
|
+
},
|
|
16874
|
+
{
|
|
16875
|
+
matches: () => true,
|
|
16876
|
+
rule: "not-allow-listed",
|
|
16877
|
+
reason: "the evidence-only view carries an explicit allow-list of counted artifacts, and this path is not on it"
|
|
16878
|
+
}
|
|
16879
|
+
];
|
|
16880
|
+
var SIGNATURE_WORDS = 8;
|
|
16881
|
+
var MIN_WORD_TOKENS = 7;
|
|
16882
|
+
function scrubIdentifiers(line) {
|
|
16883
|
+
return line.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, " ").replace(/\/?(?:[\w.@+~-]+\/)+[\w.@+~-]*/g, " ").replace(/\b[0-9a-f-]{8,}\b/gi, " ");
|
|
16884
|
+
}
|
|
16885
|
+
var PACKAGE_CONSTANT_PROSE = [POLICY_EVIDENCE_NOTE];
|
|
16886
|
+
function stripPackageConstants(text2) {
|
|
16887
|
+
let out = text2;
|
|
16888
|
+
for (const constant of PACKAGE_CONSTANT_PROSE) out = out.split(constant).join(" ");
|
|
16889
|
+
return out;
|
|
16890
|
+
}
|
|
16891
|
+
function collectJsonStrings(value, into) {
|
|
16892
|
+
if (typeof value === "string") into.push(value);
|
|
16893
|
+
else if (Array.isArray(value)) for (const item of value) collectJsonStrings(item, into);
|
|
16894
|
+
else if (value !== null && typeof value === "object") {
|
|
16895
|
+
for (const item of Object.values(value)) collectJsonStrings(item, into);
|
|
16896
|
+
}
|
|
16897
|
+
}
|
|
16898
|
+
function proseFragments(line) {
|
|
16899
|
+
const trimmed = line.trim();
|
|
16900
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
16901
|
+
try {
|
|
16902
|
+
const parsed = JSON.parse(trimmed);
|
|
16903
|
+
const strings = [];
|
|
16904
|
+
collectJsonStrings(parsed, strings);
|
|
16905
|
+
return strings;
|
|
16906
|
+
} catch {
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
return [line];
|
|
16910
|
+
}
|
|
16911
|
+
function sessionTextSignatures(text2) {
|
|
16912
|
+
const out = /* @__PURE__ */ new Set();
|
|
16913
|
+
for (const line of stripPackageConstants(text2).split("\n")) {
|
|
16914
|
+
for (const fragment of proseFragments(line)) {
|
|
16915
|
+
const words = scrubIdentifiers(fragment).toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(" ").filter(Boolean);
|
|
16916
|
+
for (let i = 0; i + SIGNATURE_WORDS <= words.length; i++) {
|
|
16917
|
+
const window = words.slice(i, i + SIGNATURE_WORDS);
|
|
16918
|
+
if (window.filter((word) => /^[a-z]+$/.test(word)).length < MIN_WORD_TOKENS) continue;
|
|
16919
|
+
out.add(window.join(" "));
|
|
16920
|
+
}
|
|
16921
|
+
}
|
|
16922
|
+
}
|
|
16923
|
+
return out;
|
|
16924
|
+
}
|
|
16925
|
+
async function findSessionTextLeaks(opts) {
|
|
16926
|
+
const index = /* @__PURE__ */ new Map();
|
|
16927
|
+
const carried = [];
|
|
16928
|
+
for (const path of opts.carriedPaths) {
|
|
16929
|
+
const signatures = sessionTextSignatures(await readFile15(join33(opts.viewDir, path), "utf8"));
|
|
16930
|
+
carried.push({ path, signatures: signatures.size });
|
|
16931
|
+
for (const signature of signatures) if (!index.has(signature)) index.set(signature, path);
|
|
16932
|
+
}
|
|
16933
|
+
const hits = /* @__PURE__ */ new Map();
|
|
16934
|
+
const comparedSources = [];
|
|
16935
|
+
let sourceSignatures = 0;
|
|
16936
|
+
for (const sourcePath of opts.excludedPaths) {
|
|
16937
|
+
const signatures = sessionTextSignatures(await readFile15(join33(opts.sourceDir, sourcePath), "utf8"));
|
|
16938
|
+
comparedSources.push(sourcePath);
|
|
16939
|
+
sourceSignatures += signatures.size;
|
|
16940
|
+
for (const signature of signatures) {
|
|
16941
|
+
const viewPath = index.get(signature);
|
|
16942
|
+
if (viewPath === void 0) continue;
|
|
16943
|
+
const hit = hits.get(viewPath) ?? { matched: /* @__PURE__ */ new Set(), sources: /* @__PURE__ */ new Set(), example: signature };
|
|
16944
|
+
hit.matched.add(signature);
|
|
16945
|
+
hit.sources.add(sourcePath);
|
|
16946
|
+
hits.set(viewPath, hit);
|
|
16947
|
+
}
|
|
16948
|
+
}
|
|
16949
|
+
const leaks = [...hits].map(([viewPath, hit]) => ({
|
|
16950
|
+
viewPath,
|
|
16951
|
+
sources: [...hit.sources].sort(),
|
|
16952
|
+
signatures: hit.matched.size,
|
|
16953
|
+
example: hit.example
|
|
16954
|
+
}));
|
|
16955
|
+
return { leaks, carried, comparedSources, sourceSignatures };
|
|
16956
|
+
}
|
|
16957
|
+
function classifyEvidenceOnlyPath(path) {
|
|
16958
|
+
const allowed = EVIDENCE_ONLY_ALLOW_LIST.find((entry) => entry.path === path);
|
|
16959
|
+
if (allowed) return { carried: true, owned: allowed.owned, carries: allowed.carries };
|
|
16960
|
+
const rule = EXCLUSION_RULES.find((entry) => entry.matches(path));
|
|
16961
|
+
return { carried: false, rule: rule.rule, reason: rule.reason };
|
|
16962
|
+
}
|
|
16963
|
+
var EVIDENCE_ONLY_LIMITS = [
|
|
16964
|
+
"the exclusion is verbatim-tight, not paraphrase-tight: a conclusion restated in different words inside a carried artifact is not detectable by content signatures",
|
|
16965
|
+
"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",
|
|
16966
|
+
"the view carries counts, names, identifiers, timestamps and hashes; a question that needs the words of a turn cannot be answered from it"
|
|
16967
|
+
];
|
|
16968
|
+
async function projectSessionBundle(opts) {
|
|
16969
|
+
if (opts.view !== "evidence-only") {
|
|
16970
|
+
throw new Error(
|
|
16971
|
+
`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".`
|
|
16972
|
+
);
|
|
16973
|
+
}
|
|
16974
|
+
const generatedAt = opts.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
16975
|
+
const sourceDir = resolve9(opts.bundleDir);
|
|
16976
|
+
const source = await readSessionBundleManifest(sourceDir);
|
|
16977
|
+
if (source.view !== "full") {
|
|
16978
|
+
throw new Error(
|
|
16979
|
+
`${sourceDir} is already the ${source.view} view \u2014 project from the full bundle, not from a view of it`
|
|
16980
|
+
);
|
|
16981
|
+
}
|
|
16982
|
+
const outDir = resolve9(opts.outDir);
|
|
16983
|
+
await mkdir6(outDir, { recursive: true });
|
|
16984
|
+
if ((await readdir20(outDir)).length > 0) {
|
|
16985
|
+
throw new Error(
|
|
16986
|
+
`view output directory ${outDir} is not empty \u2014 pass a new directory so one directory holds exactly one view`
|
|
16987
|
+
);
|
|
16988
|
+
}
|
|
16989
|
+
const sourceHashes = new Map(source.files.map((file) => [file.path, file]));
|
|
16990
|
+
const carriedPaths = [];
|
|
16991
|
+
const carries = /* @__PURE__ */ new Map();
|
|
16992
|
+
const excluded = [];
|
|
16993
|
+
const candidates = [];
|
|
16994
|
+
for (const path of await listSessionBundleFiles(sourceDir)) {
|
|
16995
|
+
if (path === "manifest.json") continue;
|
|
16996
|
+
const bytes = await readFile15(join33(sourceDir, path));
|
|
16997
|
+
const sha2562 = createHash15("sha256").update(bytes).digest("hex");
|
|
16998
|
+
const recorded = sourceHashes.get(path);
|
|
16999
|
+
if (!recorded) {
|
|
17000
|
+
throw new Error(
|
|
17001
|
+
`${join33(sourceDir, path)} is not listed in the bundle manifest \u2014 the bundle changed after assembly and cannot be projected; re-run \`traces bundle\``
|
|
17002
|
+
);
|
|
17003
|
+
}
|
|
17004
|
+
if (recorded.sha256 !== sha2562) {
|
|
17005
|
+
throw new Error(
|
|
17006
|
+
`${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\``
|
|
17007
|
+
);
|
|
17008
|
+
}
|
|
17009
|
+
const verdict = classifyEvidenceOnlyPath(path);
|
|
17010
|
+
if (!verdict.carried) {
|
|
17011
|
+
excluded.push({ path, bytes: bytes.length, sha256: sha2562, rule: verdict.rule, reason: verdict.reason });
|
|
17012
|
+
continue;
|
|
17013
|
+
}
|
|
17014
|
+
candidates.push({ path, bytes, sha256: sha2562, carries: verdict.carries, owned: verdict.owned });
|
|
17015
|
+
}
|
|
17016
|
+
const report = await findSessionTextLeaks({
|
|
17017
|
+
viewDir: sourceDir,
|
|
17018
|
+
sourceDir,
|
|
17019
|
+
carriedPaths: candidates.map((candidate) => candidate.path),
|
|
17020
|
+
excludedPaths: excluded.map((entry) => entry.path)
|
|
17021
|
+
});
|
|
17022
|
+
const leakByPath = new Map(report.leaks.map((leak) => [leak.viewPath, leak]));
|
|
17023
|
+
const ownedLeak = report.leaks.find((leak) => candidates.some((c) => c.path === leak.viewPath && c.owned));
|
|
17024
|
+
if (ownedLeak) {
|
|
17025
|
+
throw new Error(
|
|
17026
|
+
`${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.`
|
|
17027
|
+
);
|
|
17028
|
+
}
|
|
17029
|
+
for (const candidate of candidates) {
|
|
17030
|
+
const leak = leakByPath.get(candidate.path);
|
|
17031
|
+
if (leak) {
|
|
17032
|
+
excluded.push({
|
|
17033
|
+
path: candidate.path,
|
|
17034
|
+
bytes: candidate.bytes.length,
|
|
17035
|
+
sha256: candidate.sha256,
|
|
17036
|
+
rule: "content-signature",
|
|
17037
|
+
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`,
|
|
17038
|
+
contentMatch: { signatures: leak.signatures, sources: leak.sources }
|
|
17039
|
+
});
|
|
17040
|
+
continue;
|
|
17041
|
+
}
|
|
17042
|
+
const target = join33(outDir, candidate.path);
|
|
17043
|
+
await mkdir6(dirname9(target), { recursive: true });
|
|
17044
|
+
await writeFile10(target, candidate.bytes);
|
|
17045
|
+
carriedPaths.push(candidate.path);
|
|
17046
|
+
carries.set(candidate.path, candidate.carries);
|
|
17047
|
+
}
|
|
17048
|
+
const leakCheck = {
|
|
17049
|
+
signatureWords: SIGNATURE_WORDS,
|
|
17050
|
+
comparedSources: report.comparedSources,
|
|
17051
|
+
sourceSignatures: report.sourceSignatures,
|
|
17052
|
+
carried: report.carried.filter((entry) => carries.has(entry.path)).map((entry) => ({
|
|
17053
|
+
path: entry.path,
|
|
17054
|
+
carries: carries.get(entry.path),
|
|
17055
|
+
signatures: entry.signatures
|
|
17056
|
+
})),
|
|
17057
|
+
matches: 0
|
|
17058
|
+
};
|
|
17059
|
+
const sourceManifestBytes = await readFile15(join33(sourceDir, "manifest.json"));
|
|
17060
|
+
const manifest = {
|
|
17061
|
+
schemaVersion: 2,
|
|
17062
|
+
kind: "traces.session_bundle",
|
|
17063
|
+
view: "evidence-only",
|
|
17064
|
+
createdAt: generatedAt,
|
|
17065
|
+
provenance: source.provenance,
|
|
17066
|
+
files: await hashSessionBundleFiles(outDir),
|
|
17067
|
+
absent: source.absent,
|
|
17068
|
+
excluded,
|
|
17069
|
+
ledgerSlices: source.ledgerSlices.filter((slice) => carriedPaths.includes(slice.path)),
|
|
17070
|
+
knownLimits: [...source.knownLimits, ...EVIDENCE_ONLY_LIMITS],
|
|
17071
|
+
projection: {
|
|
17072
|
+
sourceDirectory: sourceDir,
|
|
17073
|
+
sourceManifestSha256: createHash15("sha256").update(sourceManifestBytes).digest("hex"),
|
|
17074
|
+
leakCheck
|
|
17075
|
+
}
|
|
17076
|
+
};
|
|
17077
|
+
const manifestPath = await writeSessionBundleManifest(outDir, manifest);
|
|
17078
|
+
return { directory: outDir, manifestPath, manifest, leakCheck };
|
|
17079
|
+
}
|
|
17080
|
+
|
|
15872
17081
|
// src/upload-state.ts
|
|
15873
|
-
import { mkdir as
|
|
17082
|
+
import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile11 } from "fs/promises";
|
|
15874
17083
|
import { homedir as homedir12 } from "os";
|
|
15875
|
-
import { dirname as
|
|
17084
|
+
import { dirname as dirname10, join as join34 } from "path";
|
|
15876
17085
|
import { contentHash } from "@tangle-network/agent-eval";
|
|
15877
17086
|
function stateFile() {
|
|
15878
|
-
const base = process.env.XDG_STATE_HOME ??
|
|
15879
|
-
return
|
|
17087
|
+
const base = process.env.XDG_STATE_HOME ?? join34(homedir12(), ".local", "state");
|
|
17088
|
+
return join34(base, "tangle-traces", "uploaded.json");
|
|
15880
17089
|
}
|
|
15881
17090
|
function uploadKey(harness, sessionId) {
|
|
15882
17091
|
return `${harness}:${sessionId}`;
|
|
@@ -15889,15 +17098,15 @@ function outboundHash(events, identity) {
|
|
|
15889
17098
|
}
|
|
15890
17099
|
async function loadState() {
|
|
15891
17100
|
try {
|
|
15892
|
-
return JSON.parse(await
|
|
17101
|
+
return JSON.parse(await readFile16(stateFile(), "utf8"));
|
|
15893
17102
|
} catch {
|
|
15894
17103
|
return {};
|
|
15895
17104
|
}
|
|
15896
17105
|
}
|
|
15897
17106
|
async function saveState(state) {
|
|
15898
17107
|
const f = stateFile();
|
|
15899
|
-
await
|
|
15900
|
-
await
|
|
17108
|
+
await mkdir7(dirname10(f), { recursive: true });
|
|
17109
|
+
await writeFile11(f, `${JSON.stringify(state, null, 2)}
|
|
15901
17110
|
`, "utf8");
|
|
15902
17111
|
}
|
|
15903
17112
|
function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
@@ -15905,10 +17114,10 @@ function alreadyUploaded(state, harness, sessionId, hash2) {
|
|
|
15905
17114
|
}
|
|
15906
17115
|
|
|
15907
17116
|
// src/upload.ts
|
|
15908
|
-
import { readFile as
|
|
17117
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
15909
17118
|
import { createRequire as createRequire2 } from "module";
|
|
15910
17119
|
import { hostname as hostname2 } from "os";
|
|
15911
|
-
import { basename as basename15, join as
|
|
17120
|
+
import { basename as basename15, join as join35 } from "path";
|
|
15912
17121
|
import { hostedClientFromEnv } from "@tangle-network/agent-eval/hosted";
|
|
15913
17122
|
import { REDACTION_VERSION } from "@tangle-network/agent-eval/traces";
|
|
15914
17123
|
var require2 = createRequire2(import.meta.url);
|
|
@@ -15932,7 +17141,7 @@ async function planUpload(opts = {}) {
|
|
|
15932
17141
|
async function gitBranch(cwd) {
|
|
15933
17142
|
if (!cwd) return void 0;
|
|
15934
17143
|
try {
|
|
15935
|
-
const head = await
|
|
17144
|
+
const head = await readFile17(join35(cwd, ".git", "HEAD"), "utf8");
|
|
15936
17145
|
const m = head.match(/^ref:\s*refs\/heads\/(.+)$/m);
|
|
15937
17146
|
return m?.[1]?.trim();
|
|
15938
17147
|
} catch {
|
|
@@ -15941,16 +17150,16 @@ async function gitBranch(cwd) {
|
|
|
15941
17150
|
}
|
|
15942
17151
|
async function sessionMeta(item) {
|
|
15943
17152
|
const meta = {
|
|
15944
|
-
[
|
|
15945
|
-
[
|
|
15946
|
-
[
|
|
15947
|
-
[
|
|
15948
|
-
[
|
|
15949
|
-
[
|
|
15950
|
-
};
|
|
15951
|
-
if (item.ref.cwd) meta[
|
|
17153
|
+
[ATTR.HARNESS]: item.ref.harness,
|
|
17154
|
+
[ATTR.SESSION_FILE]: basename15(item.ref.path),
|
|
17155
|
+
[ATTR.HOST]: hostname2(),
|
|
17156
|
+
[ATTR.UPLOADER]: `tangle-traces@${TRACES_VERSION}`,
|
|
17157
|
+
[ATTR.REDACTION_VERSION]: REDACTION_VERSION,
|
|
17158
|
+
[ATTR.REDACTION_COUNT]: item.redaction.redactionCount
|
|
17159
|
+
};
|
|
17160
|
+
if (item.ref.cwd) meta[ATTR.CWD] = item.ref.cwd;
|
|
15952
17161
|
const branch = await gitBranch(item.ref.cwd);
|
|
15953
|
-
if (branch) meta[
|
|
17162
|
+
if (branch) meta[ATTR.GIT_BRANCH] = branch;
|
|
15954
17163
|
return meta;
|
|
15955
17164
|
}
|
|
15956
17165
|
var msToNano = (epochMs) => (BigInt(epochMs) * 1000000n).toString();
|
|
@@ -15961,8 +17170,8 @@ function toTraceSpanEvents(spans, rootMeta) {
|
|
|
15961
17170
|
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attributes2[k] = v;
|
|
15962
17171
|
else if (v != null) attributes2[k] = JSON.stringify(v);
|
|
15963
17172
|
}
|
|
15964
|
-
attributes2[
|
|
15965
|
-
attributes2[
|
|
17173
|
+
attributes2[ATTR.SESSION_ID] = s.trace_id;
|
|
17174
|
+
attributes2[ATTR.INGEST_SOURCE] = INGEST_SOURCE_CLI;
|
|
15966
17175
|
if (s.parent_span_id === null) Object.assign(attributes2, rootMeta);
|
|
15967
17176
|
const startMs = parseIsoToEpochMs(s.start_time);
|
|
15968
17177
|
const endMs = Math.max(startMs, parseIsoToEpochMs(s.end_time));
|
|
@@ -16021,7 +17230,7 @@ async function prepareOutbound(plan, opts, uploadedAt) {
|
|
|
16021
17230
|
const key = uploadKey(item.ref.harness, item.ref.sessionId);
|
|
16022
17231
|
const previous = plan.state[key];
|
|
16023
17232
|
const meta = await sessionMeta(item);
|
|
16024
|
-
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [
|
|
17233
|
+
const eventsAt = (timestamp) => toTraceSpanEvents(item.spans, { ...meta, [ATTR.UPLOADED_AT]: timestamp });
|
|
16025
17234
|
let eventTimestamp2 = previous?.uploadedAt ?? uploadedAt;
|
|
16026
17235
|
let events = eventsAt(eventTimestamp2);
|
|
16027
17236
|
let hash2 = outboundHash(events, identity);
|
|
@@ -16099,6 +17308,15 @@ async function executeUpload(plan, opts = {}) {
|
|
|
16099
17308
|
|
|
16100
17309
|
export {
|
|
16101
17310
|
appendAll,
|
|
17311
|
+
ATTR,
|
|
17312
|
+
INGEST_SOURCE_CLI,
|
|
17313
|
+
environmentAttributes,
|
|
17314
|
+
stampEnvironmentAttrs,
|
|
17315
|
+
environmentFromSpanAttributes,
|
|
17316
|
+
DEFAULT_HARNESS,
|
|
17317
|
+
sessionIdFromAttributes,
|
|
17318
|
+
indexSessionIdsByTrace,
|
|
17319
|
+
stampSessionIdentity,
|
|
16102
17320
|
parseIsoToEpochMs,
|
|
16103
17321
|
parseSince,
|
|
16104
17322
|
resolveOtlpInputFiles,
|
|
@@ -16115,12 +17333,6 @@ export {
|
|
|
16115
17333
|
capText,
|
|
16116
17334
|
ACTOR_ATTR,
|
|
16117
17335
|
userPromptSpan,
|
|
16118
|
-
ATTR2 as ATTR,
|
|
16119
|
-
INGEST_SOURCE_CLI,
|
|
16120
|
-
DEFAULT_HARNESS,
|
|
16121
|
-
sessionIdFromAttributes,
|
|
16122
|
-
indexSessionIdsByTrace,
|
|
16123
|
-
stampSessionIdentity,
|
|
16124
17336
|
isChatTrajectoryMessage,
|
|
16125
17337
|
isChatTrajectory,
|
|
16126
17338
|
chatTrajectoryToSpans,
|
|
@@ -16173,6 +17385,7 @@ export {
|
|
|
16173
17385
|
scanSessions,
|
|
16174
17386
|
SessionWorkflowError,
|
|
16175
17387
|
describeSessionRelationship,
|
|
17388
|
+
POLICY_EVIDENCE_NOTE,
|
|
16176
17389
|
buildPolicyEvidenceRecord,
|
|
16177
17390
|
collectPolicyEvidence,
|
|
16178
17391
|
serializePolicyEvidence,
|
|
@@ -16233,6 +17446,8 @@ export {
|
|
|
16233
17446
|
commandRedactor,
|
|
16234
17447
|
writeHodoscopeInput,
|
|
16235
17448
|
hodoscopeAnalyzer,
|
|
17449
|
+
httpJsonTransport,
|
|
17450
|
+
primeAnalyzer,
|
|
16236
17451
|
CODING_REDACTION_RULES,
|
|
16237
17452
|
TRACES_REDACTION_RULES,
|
|
16238
17453
|
redactSpans,
|
|
@@ -16263,7 +17478,7 @@ export {
|
|
|
16263
17478
|
analyzeSteeringChain,
|
|
16264
17479
|
REACTIONS,
|
|
16265
17480
|
analyzeReactions,
|
|
16266
|
-
|
|
17481
|
+
sessionReportSource,
|
|
16267
17482
|
buildTraceFindingPacket,
|
|
16268
17483
|
loadTracesConfig,
|
|
16269
17484
|
mergeTracesConfig,
|
|
@@ -16320,10 +17535,20 @@ export {
|
|
|
16320
17535
|
collectSessionWorkflow,
|
|
16321
17536
|
collectSessionSelection,
|
|
16322
17537
|
buildSessionIndex,
|
|
17538
|
+
findContextRoot,
|
|
16323
17539
|
buildSessionIndexFromRows,
|
|
16324
17540
|
collectSessionIndex,
|
|
16325
17541
|
serializeSessionIndex,
|
|
16326
17542
|
writeSessionIndexFile,
|
|
17543
|
+
listSessionBundleFiles,
|
|
17544
|
+
assembleSessionBundle,
|
|
17545
|
+
hashSessionBundleFiles,
|
|
17546
|
+
writeSessionBundleManifest,
|
|
17547
|
+
readSessionBundleManifest,
|
|
17548
|
+
sessionTextSignatures,
|
|
17549
|
+
findSessionTextLeaks,
|
|
17550
|
+
classifyEvidenceOnlyPath,
|
|
17551
|
+
projectSessionBundle,
|
|
16327
17552
|
uploadKey,
|
|
16328
17553
|
sessionHash,
|
|
16329
17554
|
outboundHash,
|