@tangle-network/traces 0.23.1 → 1.0.1
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 +61 -1078
- package/dist/analyst-model-call-JVUOYZ5V.js +20 -0
- package/dist/ask-2KF4KSPB.js +39 -0
- package/dist/check-YF6ZHPGG.js +322 -0
- package/dist/chunk-2XXABIRL.js +25 -0
- package/dist/chunk-4VKPN432.js +138 -0
- package/dist/chunk-6GOG5KIK.js +53 -0
- package/dist/chunk-7USMM4MA.js +5695 -0
- package/dist/chunk-CG5QHQO4.js +492 -0
- package/dist/chunk-CQ6UYZG6.js +1217 -0
- package/dist/chunk-DU534HAX.js +263 -0
- package/dist/chunk-FC4V25CG.js +503 -0
- package/dist/chunk-GLUDGUGJ.js +232 -0
- package/dist/chunk-GNRXKQEQ.js +120 -0
- package/dist/chunk-K2KH2HHD.js +228 -0
- package/dist/chunk-M3W6TZHQ.js +381 -0
- package/dist/chunk-O52XK7OB.js +19 -0
- package/dist/chunk-PYH3PL3N.js +62 -0
- package/dist/chunk-QLVP37YB.js +782 -0
- package/dist/chunk-QUHUMZ4W.js +759 -0
- package/dist/chunk-REO2JQJY.js +535 -0
- package/dist/chunk-RZL5GAWH.js +60 -0
- package/dist/chunk-TOCNGML5.js +273 -0
- package/dist/chunk-UQSAGXPH.js +208 -0
- package/dist/chunk-VFOLC4OX.js +407 -0
- package/dist/chunk-WF7PTDCZ.js +3921 -0
- package/dist/chunk-YAX72TST.js +259 -0
- package/dist/cli.js +2086 -2050
- package/dist/diff-J4RAIOXO.js +16 -0
- package/dist/improvement-LCCVGGUS.js +25 -0
- package/dist/index.d.ts +1235 -4357
- package/dist/index.js +53 -1204
- package/dist/live-XXL7VZWB.js +14 -0
- package/dist/mcp-EYIDKBEK.js +85 -0
- package/dist/report-E3G2T5TJ.js +338 -0
- package/dist/run-watch-RAOOAXOF.js +649 -0
- package/dist/supervisor-report-RMPLLBGZ.js +29 -0
- package/dist/supervisor-run-context-WO6VOTGV.js +30 -0
- package/dist/upload-3R7GRSKS.js +20 -0
- package/dist/verify-safe-C5ZXZOZ2.js +8 -0
- package/docs/analyze.md +76 -0
- package/docs/ask.md +88 -0
- package/docs/bundle.md +40 -0
- package/docs/check.md +77 -0
- package/docs/convert.md +44 -0
- package/docs/develop.md +32 -0
- package/docs/diff.md +38 -0
- package/docs/facts.md +114 -0
- package/docs/mcp.md +36 -0
- package/docs/migrate-1.0.md +33 -0
- package/docs/parsers.md +66 -0
- package/docs/release-1.0.0.md +147 -0
- package/docs/release-1.0.1.md +4 -0
- package/docs/sdk.md +77 -0
- package/docs/select.md +70 -0
- package/docs/skills.md +46 -0
- package/docs/upload.md +47 -0
- package/docs/watch-jsonl.v1.example.jsonl +5 -0
- package/docs/watch.md +66 -0
- package/examples/README.md +9 -0
- package/examples/custom-analyst.ts +65 -74
- package/install.sh +22 -2
- package/package.json +10 -9
- package/skills/inspect-agent-traces/SKILL.md +47 -110
- package/dist/chunk-7DVZCCZI.js +0 -20983
- package/docs/replay-verify.md +0 -123
- package/docs/stream-findings.svg +0 -39
- package/docs/trace-analysts.md +0 -643
- package/examples/custom-backend.ts +0 -21
- package/examples/external-engines.ts +0 -62
- package/examples/improvement-config.mjs +0 -63
- package/examples/observe-and-alert.ts +0 -19
- package/examples/register-harness.ts +0 -26
- package/skills/build-trace-analyst/SKILL.md +0 -76
- package/skills/build-trace-analyst/agents/openai.yaml +0 -4
|
@@ -0,0 +1,3921 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INTERRUPT_TEXT,
|
|
3
|
+
PIECE_START,
|
|
4
|
+
authorOfTypedTurn,
|
|
5
|
+
isFleetTurn,
|
|
6
|
+
isHarnessCommand,
|
|
7
|
+
pieceBoundary,
|
|
8
|
+
readSessionSpans,
|
|
9
|
+
readsStoreRoots
|
|
10
|
+
} from "./chunk-7USMM4MA.js";
|
|
11
|
+
import {
|
|
12
|
+
ACTOR_ATTR,
|
|
13
|
+
ATTR,
|
|
14
|
+
INHERITED_SOURCE_ATTR,
|
|
15
|
+
INHERITED_SPAN_ATTR,
|
|
16
|
+
SESSION_CWD_ATTR,
|
|
17
|
+
SESSION_TOTAL_CACHED_INPUT_ATTR,
|
|
18
|
+
SESSION_TOTAL_CACHE_WRITE_ATTR,
|
|
19
|
+
SESSION_TOTAL_INPUT_ATTR,
|
|
20
|
+
SESSION_TOTAL_OUTPUT_ATTR,
|
|
21
|
+
SESSION_TOTAL_REASONING_ATTR,
|
|
22
|
+
SUBAGENT_SPAWN_ATTR,
|
|
23
|
+
SUBAGENT_SPAWN_TASK_ATTR,
|
|
24
|
+
SUBAGENT_TASK_ATTR,
|
|
25
|
+
capText,
|
|
26
|
+
indexSessionIdsByTrace,
|
|
27
|
+
isSubagentSpan,
|
|
28
|
+
isSynthesizedSpan,
|
|
29
|
+
sessionIdFromAttributes,
|
|
30
|
+
stampSessionIdentity
|
|
31
|
+
} from "./chunk-UQSAGXPH.js";
|
|
32
|
+
import {
|
|
33
|
+
validateOtlpSpans
|
|
34
|
+
} from "./chunk-CQ6UYZG6.js";
|
|
35
|
+
import {
|
|
36
|
+
canonicalJson,
|
|
37
|
+
isInnerToolCall
|
|
38
|
+
} from "./chunk-DU534HAX.js";
|
|
39
|
+
|
|
40
|
+
// src/session-relationship.ts
|
|
41
|
+
var SessionWorkflowError = class extends Error {
|
|
42
|
+
code;
|
|
43
|
+
constructor(code, message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "SessionWorkflowError";
|
|
46
|
+
this.code = code;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
function stringAttribute(span, key) {
|
|
50
|
+
const value = span?.attributes[key];
|
|
51
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
52
|
+
}
|
|
53
|
+
function numberAttribute(span, key) {
|
|
54
|
+
const value = span?.attributes[key];
|
|
55
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
56
|
+
}
|
|
57
|
+
function parseSessionIds(value, key) {
|
|
58
|
+
if (value === void 0) return [];
|
|
59
|
+
let parsed = value;
|
|
60
|
+
if (typeof value === "string") {
|
|
61
|
+
try {
|
|
62
|
+
parsed = JSON.parse(value);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new SessionWorkflowError(
|
|
65
|
+
"SESSION_WORKFLOW_INVALID_RELATION",
|
|
66
|
+
`${key} must be a JSON array of non-empty session IDs`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string" || item.length === 0)) {
|
|
71
|
+
throw new SessionWorkflowError(
|
|
72
|
+
"SESSION_WORKFLOW_INVALID_RELATION",
|
|
73
|
+
`${key} must be a JSON array of non-empty session IDs`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
function sessionRoot(ref, spans) {
|
|
79
|
+
const roots = spans.filter((span) => span.parent_span_id === null);
|
|
80
|
+
return roots.find((span) => sessionIdFromAttributes(span.attributes) === ref.sessionId) ?? roots.find((span) => span.trace_id === ref.sessionId) ?? roots[0] ?? spans[0];
|
|
81
|
+
}
|
|
82
|
+
function describeSessionRelationship(ref, spans) {
|
|
83
|
+
const root = sessionRoot(ref, spans);
|
|
84
|
+
const sessionId = sessionIdFromAttributes(root?.attributes ?? {}) ?? root?.trace_id ?? ref.sessionId;
|
|
85
|
+
const parentSessionId = stringAttribute(root, "traces.parent_session_id");
|
|
86
|
+
const childSessionIds = /* @__PURE__ */ new Set();
|
|
87
|
+
const spawnedChildSessionIds = /* @__PURE__ */ new Set();
|
|
88
|
+
const resumedChildSessionIds = /* @__PURE__ */ new Set();
|
|
89
|
+
const unjoinedSpawnPaths = /* @__PURE__ */ new Set();
|
|
90
|
+
for (const span of spans) {
|
|
91
|
+
const direct = parseSessionIds(span.attributes["traces.child_session_ids"], "traces.child_session_ids");
|
|
92
|
+
for (const id of direct) childSessionIds.add(id);
|
|
93
|
+
const operation = span.attributes["traces.codex.agent_operation"];
|
|
94
|
+
const agentSessionIds = parseSessionIds(
|
|
95
|
+
span.attributes["traces.codex.agent_session_ids"],
|
|
96
|
+
"traces.codex.agent_session_ids"
|
|
97
|
+
);
|
|
98
|
+
const operationName = String(operation);
|
|
99
|
+
if (["spawn_agent", "send_input", "send_message", "followup_task"].includes(operationName)) {
|
|
100
|
+
for (const id of agentSessionIds) childSessionIds.add(id);
|
|
101
|
+
}
|
|
102
|
+
if (operationName === "spawn_agent") {
|
|
103
|
+
for (const id of [...direct, ...agentSessionIds]) spawnedChildSessionIds.add(id);
|
|
104
|
+
}
|
|
105
|
+
if (["send_input", "send_message", "followup_task"].includes(operationName)) {
|
|
106
|
+
for (const id of [...direct, ...agentSessionIds]) resumedChildSessionIds.add(id);
|
|
107
|
+
}
|
|
108
|
+
const lifecycleChild = stringAttribute(span, "traces.codex.subagent_thread_id");
|
|
109
|
+
if (lifecycleChild) childSessionIds.add(lifecycleChild);
|
|
110
|
+
const spawnPath = stringAttribute(span, "traces.codex.spawn_agent_path");
|
|
111
|
+
if (operationName === "spawn_agent" && spawnPath && agentSessionIds.length === 0 && direct.length === 0) {
|
|
112
|
+
unjoinedSpawnPaths.add(spawnPath);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const ids of [childSessionIds, spawnedChildSessionIds, resumedChildSessionIds]) {
|
|
116
|
+
ids.delete(sessionId);
|
|
117
|
+
if (parentSessionId) ids.delete(parentSessionId);
|
|
118
|
+
}
|
|
119
|
+
const role = stringAttribute(root, "traces.session.role");
|
|
120
|
+
const depth = numberAttribute(root, "traces.codex.agent_depth");
|
|
121
|
+
const agentNickname = stringAttribute(root, "traces.codex.agent_nickname");
|
|
122
|
+
const agentRole = stringAttribute(root, "traces.codex.agent_role");
|
|
123
|
+
const agentPath = stringAttribute(root, "traces.codex.agent_path");
|
|
124
|
+
const rawTaskScope = stringAttribute(root, "traces.codex.task_scope");
|
|
125
|
+
const taskScope = rawTaskScope === "all" || rawTaskScope === "latest" || rawTaskScope === "turn" || rawTaskScope === "fork-current" ? rawTaskScope : void 0;
|
|
126
|
+
const turnId = stringAttribute(root, "traces.codex.turn_id");
|
|
127
|
+
return {
|
|
128
|
+
sessionId,
|
|
129
|
+
role: role === "operator" || role === "child" ? role : "unknown",
|
|
130
|
+
...parentSessionId ? { parentSessionId } : {},
|
|
131
|
+
childSessionIds: [...childSessionIds].sort(),
|
|
132
|
+
spawnedChildSessionIds: [...spawnedChildSessionIds].sort(),
|
|
133
|
+
resumedChildSessionIds: [...resumedChildSessionIds].filter((sessionId2) => !spawnedChildSessionIds.has(sessionId2)).sort(),
|
|
134
|
+
unjoinedSpawnPaths: [...unjoinedSpawnPaths].sort(),
|
|
135
|
+
...depth !== void 0 ? { depth } : {},
|
|
136
|
+
...agentNickname ? { agentNickname } : {},
|
|
137
|
+
...agentRole ? { agentRole } : {},
|
|
138
|
+
...agentPath ? { agentPath } : {},
|
|
139
|
+
...taskScope ? { taskScope } : {},
|
|
140
|
+
...turnId ? { turnId } : {},
|
|
141
|
+
...taskScope && taskScope !== "all" && root ? { taskStartedAt: root.start_time } : {}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/shell-commands.ts
|
|
146
|
+
var SHELLS = /* @__PURE__ */ new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "busybox"]);
|
|
147
|
+
var COMMAND_PREFIXES = /* @__PURE__ */ new Set([
|
|
148
|
+
"env",
|
|
149
|
+
"command",
|
|
150
|
+
"builtin",
|
|
151
|
+
"exec",
|
|
152
|
+
"nohup",
|
|
153
|
+
"time",
|
|
154
|
+
"sudo",
|
|
155
|
+
"doas",
|
|
156
|
+
"nice",
|
|
157
|
+
"stdbuf",
|
|
158
|
+
"setsid",
|
|
159
|
+
"then",
|
|
160
|
+
"do",
|
|
161
|
+
"else",
|
|
162
|
+
"elif",
|
|
163
|
+
"if",
|
|
164
|
+
"while",
|
|
165
|
+
"until",
|
|
166
|
+
"!",
|
|
167
|
+
"{",
|
|
168
|
+
"("
|
|
169
|
+
]);
|
|
170
|
+
var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
171
|
+
var MAX_SHELL_DEPTH = 4;
|
|
172
|
+
var MAX_WORDS = 2e4;
|
|
173
|
+
function basename(word) {
|
|
174
|
+
const cut = word.lastIndexOf("/");
|
|
175
|
+
return cut === -1 ? word : word.slice(cut + 1);
|
|
176
|
+
}
|
|
177
|
+
var ShellScanner = class {
|
|
178
|
+
text;
|
|
179
|
+
length;
|
|
180
|
+
depth;
|
|
181
|
+
commands = [];
|
|
182
|
+
word = [];
|
|
183
|
+
wordStarted = false;
|
|
184
|
+
current = [];
|
|
185
|
+
words = 0;
|
|
186
|
+
heredocs = [];
|
|
187
|
+
constructor(text, depth) {
|
|
188
|
+
this.text = text;
|
|
189
|
+
this.length = text.length;
|
|
190
|
+
this.depth = depth;
|
|
191
|
+
}
|
|
192
|
+
run() {
|
|
193
|
+
let index = 0;
|
|
194
|
+
while (index < this.length && this.words < MAX_WORDS) {
|
|
195
|
+
index = this.step(index);
|
|
196
|
+
}
|
|
197
|
+
this.endCommand();
|
|
198
|
+
return this.commands;
|
|
199
|
+
}
|
|
200
|
+
step(index) {
|
|
201
|
+
const text = this.text;
|
|
202
|
+
const char = text[index];
|
|
203
|
+
if (char === "\\") {
|
|
204
|
+
if (index + 1 >= this.length) return index + 1;
|
|
205
|
+
if (text[index + 1] !== "\n") this.push(text[index + 1]);
|
|
206
|
+
return index + 2;
|
|
207
|
+
}
|
|
208
|
+
if (char === "'") return this.readSingleQuote(index);
|
|
209
|
+
if (char === '"') return this.readDoubleQuote(index);
|
|
210
|
+
if (char === "`") return this.readBackTick(index);
|
|
211
|
+
if (char === "$" && text[index + 1] === "(" && text[index + 2] === "(") {
|
|
212
|
+
const close = text.indexOf("))", index + 3);
|
|
213
|
+
const end = close === -1 ? this.length : close + 2;
|
|
214
|
+
this.pushText(text.slice(index, end));
|
|
215
|
+
return end;
|
|
216
|
+
}
|
|
217
|
+
if (char === "$" && text[index + 1] === "(") return this.readSubstitution(index + 2);
|
|
218
|
+
if (char === "$" && text[index + 1] === "{") {
|
|
219
|
+
const end = this.findBalanced(index + 2, "{", "}") + 1;
|
|
220
|
+
this.pushText(text.slice(index, end));
|
|
221
|
+
return end;
|
|
222
|
+
}
|
|
223
|
+
if (char === "#" && !this.wordStarted) {
|
|
224
|
+
const newline = text.indexOf("\n", index);
|
|
225
|
+
return newline === -1 ? this.length : newline;
|
|
226
|
+
}
|
|
227
|
+
if (char === "<" && text[index + 1] === "<" && text[index + 2] !== "<") return this.readHeredocHeader(index);
|
|
228
|
+
if (char === ">" || char === "<") return this.readRedirect(index);
|
|
229
|
+
if (char === "\n") {
|
|
230
|
+
this.endCommand();
|
|
231
|
+
return this.heredocs.length > 0 ? this.skipHeredocBodies(index + 1) : index + 1;
|
|
232
|
+
}
|
|
233
|
+
if (char === ";" || char === "&" || char === "|" || char === "(" || char === ")") {
|
|
234
|
+
this.endCommand();
|
|
235
|
+
return index + 1;
|
|
236
|
+
}
|
|
237
|
+
if (char === " " || char === " " || char === "\r") {
|
|
238
|
+
this.endWord();
|
|
239
|
+
return index + 1;
|
|
240
|
+
}
|
|
241
|
+
if (char >= "0" && char <= "9" && !this.wordStarted && (text[index + 1] === ">" || text[index + 1] === "<")) {
|
|
242
|
+
return this.readRedirect(index + 1);
|
|
243
|
+
}
|
|
244
|
+
this.push(char);
|
|
245
|
+
return index + 1;
|
|
246
|
+
}
|
|
247
|
+
push(char) {
|
|
248
|
+
this.word.push(char);
|
|
249
|
+
this.wordStarted = true;
|
|
250
|
+
}
|
|
251
|
+
pushText(text) {
|
|
252
|
+
if (text.length > 0) this.word.push(text);
|
|
253
|
+
this.wordStarted = true;
|
|
254
|
+
}
|
|
255
|
+
endWord() {
|
|
256
|
+
if (!this.wordStarted) return;
|
|
257
|
+
this.current.push(this.word.join(""));
|
|
258
|
+
this.words += 1;
|
|
259
|
+
this.word = [];
|
|
260
|
+
this.wordStarted = false;
|
|
261
|
+
}
|
|
262
|
+
endCommand() {
|
|
263
|
+
this.endWord();
|
|
264
|
+
if (this.current.length > 0) this.commands.push(this.normalize(this.current));
|
|
265
|
+
this.current = [];
|
|
266
|
+
}
|
|
267
|
+
/** Drop the leading assignments and prefix words so `w[0]` is the command name. */
|
|
268
|
+
normalize(words) {
|
|
269
|
+
let start = 0;
|
|
270
|
+
while (start < words.length) {
|
|
271
|
+
const word = words[start];
|
|
272
|
+
if (ASSIGNMENT.test(word) || COMMAND_PREFIXES.has(basename(word))) start += 1;
|
|
273
|
+
else break;
|
|
274
|
+
}
|
|
275
|
+
return start === 0 || start >= words.length ? words : words.slice(start);
|
|
276
|
+
}
|
|
277
|
+
readSingleQuote(index) {
|
|
278
|
+
const end = this.text.indexOf("'", index + 1);
|
|
279
|
+
const stop = end === -1 ? this.length : end;
|
|
280
|
+
this.pushText(this.text.slice(index + 1, stop));
|
|
281
|
+
this.wordStarted = true;
|
|
282
|
+
return stop + 1;
|
|
283
|
+
}
|
|
284
|
+
readDoubleQuote(index) {
|
|
285
|
+
let cursor = index + 1;
|
|
286
|
+
this.wordStarted = true;
|
|
287
|
+
while (cursor < this.length) {
|
|
288
|
+
const char = this.text[cursor];
|
|
289
|
+
if (char === '"') return cursor + 1;
|
|
290
|
+
if (char === "\\") {
|
|
291
|
+
if (cursor + 1 < this.length && this.text[cursor + 1] !== "\n") this.push(this.text[cursor + 1]);
|
|
292
|
+
cursor += 2;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (char === "`") {
|
|
296
|
+
cursor = this.readBackTick(cursor);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (char === "$" && this.text[cursor + 1] === "(") {
|
|
300
|
+
cursor = this.readSubstitution(cursor + 2);
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
this.push(char);
|
|
304
|
+
cursor += 1;
|
|
305
|
+
}
|
|
306
|
+
return this.length;
|
|
307
|
+
}
|
|
308
|
+
/** Scan the commands inside `$( … )` and continue after the closing paren. */
|
|
309
|
+
readSubstitution(index) {
|
|
310
|
+
const end = this.findBalanced(index, "(", ")");
|
|
311
|
+
this.descend(this.text.slice(index, end));
|
|
312
|
+
this.wordStarted = true;
|
|
313
|
+
return end + 1;
|
|
314
|
+
}
|
|
315
|
+
readBackTick(index) {
|
|
316
|
+
const end = this.text.indexOf("`", index + 1);
|
|
317
|
+
const stop = end === -1 ? this.length : end;
|
|
318
|
+
this.descend(this.text.slice(index + 1, stop));
|
|
319
|
+
this.wordStarted = true;
|
|
320
|
+
return stop + 1;
|
|
321
|
+
}
|
|
322
|
+
descend(script) {
|
|
323
|
+
if (this.depth >= MAX_SHELL_DEPTH || script.length === 0) return;
|
|
324
|
+
for (const command of shellCommands(script, this.depth + 1)) this.commands.push(command);
|
|
325
|
+
}
|
|
326
|
+
findBalanced(index, open2, close) {
|
|
327
|
+
let depth = 1;
|
|
328
|
+
let cursor = index;
|
|
329
|
+
while (cursor < this.length) {
|
|
330
|
+
const char = this.text[cursor];
|
|
331
|
+
if (char === "\\") cursor += 2;
|
|
332
|
+
else if (char === open2) {
|
|
333
|
+
depth += 1;
|
|
334
|
+
cursor += 1;
|
|
335
|
+
} else if (char === close) {
|
|
336
|
+
depth -= 1;
|
|
337
|
+
if (depth === 0) return cursor;
|
|
338
|
+
cursor += 1;
|
|
339
|
+
} else cursor += 1;
|
|
340
|
+
}
|
|
341
|
+
return this.length;
|
|
342
|
+
}
|
|
343
|
+
/** `<<EOF`, `<<-'EOF'`: record the delimiter; the body is skipped at the newline. */
|
|
344
|
+
readHeredocHeader(index) {
|
|
345
|
+
this.endWord();
|
|
346
|
+
let cursor = index + 2;
|
|
347
|
+
let stripTabs = false;
|
|
348
|
+
if (this.text[cursor] === "-") {
|
|
349
|
+
stripTabs = true;
|
|
350
|
+
cursor += 1;
|
|
351
|
+
}
|
|
352
|
+
while (cursor < this.length && (this.text[cursor] === " " || this.text[cursor] === " ")) cursor += 1;
|
|
353
|
+
const delimiter = [];
|
|
354
|
+
while (cursor < this.length) {
|
|
355
|
+
const char = this.text[cursor];
|
|
356
|
+
if (char === "'" || char === '"') {
|
|
357
|
+
const end = this.text.indexOf(char, cursor + 1);
|
|
358
|
+
const stop = end === -1 ? this.length : end;
|
|
359
|
+
delimiter.push(this.text.slice(cursor + 1, stop));
|
|
360
|
+
cursor = stop + 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (char === "\\" && cursor + 1 < this.length) {
|
|
364
|
+
delimiter.push(this.text[cursor + 1]);
|
|
365
|
+
cursor += 2;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (" \n;&|<>()".includes(char)) break;
|
|
369
|
+
delimiter.push(char);
|
|
370
|
+
cursor += 1;
|
|
371
|
+
}
|
|
372
|
+
if (delimiter.length > 0) this.heredocs.push({ delimiter: delimiter.join(""), stripTabs });
|
|
373
|
+
return cursor;
|
|
374
|
+
}
|
|
375
|
+
skipHeredocBodies(index) {
|
|
376
|
+
let cursor = index;
|
|
377
|
+
for (const { delimiter, stripTabs } of this.heredocs) {
|
|
378
|
+
while (cursor < this.length) {
|
|
379
|
+
const newline = this.text.indexOf("\n", cursor);
|
|
380
|
+
const rawLine = newline === -1 ? this.text.slice(cursor) : this.text.slice(cursor, newline);
|
|
381
|
+
const line = (stripTabs ? rawLine.replace(/^\t+/, "") : rawLine).replace(/\r$/, "");
|
|
382
|
+
cursor = newline === -1 ? this.length : newline + 1;
|
|
383
|
+
if (line === delimiter) break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
this.heredocs = [];
|
|
387
|
+
return cursor;
|
|
388
|
+
}
|
|
389
|
+
/** Consume a redirection operator and its target so the target is not read as an argument. */
|
|
390
|
+
readRedirect(index) {
|
|
391
|
+
this.endWord();
|
|
392
|
+
let cursor = index;
|
|
393
|
+
while (cursor < this.length && "<>&".includes(this.text[cursor])) cursor += 1;
|
|
394
|
+
while (cursor < this.length && (this.text[cursor] === " " || this.text[cursor] === " ")) cursor += 1;
|
|
395
|
+
while (cursor < this.length && !" \n;&|()<>".includes(this.text[cursor])) {
|
|
396
|
+
const char = this.text[cursor];
|
|
397
|
+
if (char === "'" || char === '"') {
|
|
398
|
+
const close = this.text.indexOf(char, cursor + 1);
|
|
399
|
+
cursor = (close === -1 ? this.length : close) + 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
cursor += char === "\\" ? 2 : 1;
|
|
403
|
+
}
|
|
404
|
+
return cursor;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function shellCommands(script, depth = 0) {
|
|
408
|
+
if (depth > MAX_SHELL_DEPTH || script.length === 0) return [];
|
|
409
|
+
const commands = new ShellScanner(script, depth).run();
|
|
410
|
+
const out = [];
|
|
411
|
+
for (const words of commands) {
|
|
412
|
+
out.push(words);
|
|
413
|
+
const name = basename(words[0] ?? "");
|
|
414
|
+
if (!SHELLS.has(name) || depth >= MAX_SHELL_DEPTH) continue;
|
|
415
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
416
|
+
const word = words[index];
|
|
417
|
+
if (!word.startsWith("-") || word.startsWith("--") || !word.slice(1).includes("c")) continue;
|
|
418
|
+
const nested = words[index + 1];
|
|
419
|
+
if (nested !== void 0) out.push(...shellCommands(nested, depth + 1));
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return out;
|
|
424
|
+
}
|
|
425
|
+
var CODE_MODE_COMMAND = /\bexec_command\(\s*\{\s*(?:["']?cmd["']?\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`|[A-Za-z_$][\w$]*)|(cmd)\s*[,}])/gu;
|
|
426
|
+
var FOR_OF = /\bfor\s*\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\s+(\[[^\]]*\]|[A-Za-z_$][\w$]*)\s*\)\s*\{/gu;
|
|
427
|
+
var ARRAY_ELEMENT = /\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\$]|\\.|\$(?!\{))*`|-?\d+(?:\.\d+)?)\s*(?:,|$)/uy;
|
|
428
|
+
var MAX_LOOP_COMMANDS = 64;
|
|
429
|
+
var STRING_ESCAPES = { n: "\n", t: " ", r: "\r", b: "\b", f: "\f", v: "\v", 0: "\0", "\n": "" };
|
|
430
|
+
function stringLiteralText(literal) {
|
|
431
|
+
return literal.slice(1, -1).replace(/\\(u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[\s\S])/gu, (_, escape) => escape.length > 1 ? String.fromCodePoint(Number.parseInt(escape.replace(/[ux{}]/gu, ""), 16)) : STRING_ESCAPES[escape] ?? escape);
|
|
432
|
+
}
|
|
433
|
+
function literalArray(source) {
|
|
434
|
+
const inner = source.slice(1, -1);
|
|
435
|
+
const values = [];
|
|
436
|
+
ARRAY_ELEMENT.lastIndex = 0;
|
|
437
|
+
while (inner.slice(ARRAY_ELEMENT.lastIndex).trim() !== "") {
|
|
438
|
+
const match = ARRAY_ELEMENT.exec(inner);
|
|
439
|
+
if (!match) return void 0;
|
|
440
|
+
values.push(/^-?\d/u.test(match[1]) ? match[1] : stringLiteralText(match[1]));
|
|
441
|
+
}
|
|
442
|
+
return values;
|
|
443
|
+
}
|
|
444
|
+
function quoteEnd(text, start) {
|
|
445
|
+
for (let index = start + 1; index < text.length; index += 1) {
|
|
446
|
+
if (text[index] === "\\") index += 1;
|
|
447
|
+
else if (text[index] === text[start]) return index;
|
|
448
|
+
}
|
|
449
|
+
return -1;
|
|
450
|
+
}
|
|
451
|
+
function blockEnd(text, open2) {
|
|
452
|
+
let depth = 0;
|
|
453
|
+
for (let index = open2; index < text.length; index += 1) {
|
|
454
|
+
const char = text[index];
|
|
455
|
+
if (char === '"' || char === "'" || char === "`") index = quoteEnd(text, index);
|
|
456
|
+
else if (char === "/" && text[index + 1] === "/") index = text.indexOf("\n", index);
|
|
457
|
+
else if (char === "/" && text[index + 1] === "*") index = text.includes("*/", index + 2) ? text.indexOf("*/", index + 2) + 1 : -1;
|
|
458
|
+
else if (char === "{") depth += 1;
|
|
459
|
+
else if (char === "}" && --depth === 0) return index;
|
|
460
|
+
if (index < 0) return -1;
|
|
461
|
+
}
|
|
462
|
+
return -1;
|
|
463
|
+
}
|
|
464
|
+
function forOfLoops(script) {
|
|
465
|
+
const loops = [];
|
|
466
|
+
for (const match of script.matchAll(FOR_OF)) {
|
|
467
|
+
const open2 = match.index + match[0].length - 1;
|
|
468
|
+
const end = blockEnd(script, open2);
|
|
469
|
+
if (end === -1) continue;
|
|
470
|
+
let source = match[2];
|
|
471
|
+
if (!source.startsWith("[")) {
|
|
472
|
+
const name = source.replace(/\$/gu, "\\$");
|
|
473
|
+
source = new RegExp(`\\b(?:const|let|var)\\s+${name}\\s*=\\s*(\\[[^\\]]*\\])`, "u").exec(script)?.[1] ?? "";
|
|
474
|
+
}
|
|
475
|
+
loops.push({ variable: match[1], values: source.startsWith("[") ? literalArray(source) : void 0, start: match.index, end });
|
|
476
|
+
}
|
|
477
|
+
return loops;
|
|
478
|
+
}
|
|
479
|
+
function codeModeCommands(script) {
|
|
480
|
+
const loops = forOfLoops(script);
|
|
481
|
+
const commands = [];
|
|
482
|
+
for (const match of script.matchAll(CODE_MODE_COMMAND)) {
|
|
483
|
+
const written = match[1] ?? match[2];
|
|
484
|
+
const literal = /^["'`]/u.test(written);
|
|
485
|
+
const around = loops.filter((loop) => loop.start < match.index && match.index < loop.end);
|
|
486
|
+
let bindings = [{}];
|
|
487
|
+
for (const loop of around) {
|
|
488
|
+
if (loop.values === void 0 || bindings.length * loop.values.length > MAX_LOOP_COMMANDS) {
|
|
489
|
+
bindings = void 0;
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
bindings = bindings.flatMap((bound) => loop.values.map((value) => ({ ...bound, [loop.variable]: value })));
|
|
493
|
+
}
|
|
494
|
+
if (bindings === void 0) {
|
|
495
|
+
if (literal) commands.push(stringLiteralText(written));
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
for (const bound of bindings) {
|
|
499
|
+
if (!literal) {
|
|
500
|
+
if (bound[written] !== void 0) commands.push(bound[written]);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const text = stringLiteralText(written);
|
|
504
|
+
commands.push(written.startsWith("`") ? text.replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/gu, (part, name) => bound[name] ?? part) : text);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return commands.filter((command) => command.length > 0);
|
|
508
|
+
}
|
|
509
|
+
function isCodeModeScript(input) {
|
|
510
|
+
const trimmed = input.trimStart();
|
|
511
|
+
return !trimmed.startsWith("{") && !trimmed.startsWith("*** Begin Patch") && trimmed.includes("exec_command(");
|
|
512
|
+
}
|
|
513
|
+
function commandTextFromInput(input) {
|
|
514
|
+
const trimmed = input.trim();
|
|
515
|
+
if (isCodeModeScript(trimmed)) {
|
|
516
|
+
const commands = codeModeCommands(trimmed);
|
|
517
|
+
return commands.length > 0 ? commands.join("\n") : void 0;
|
|
518
|
+
}
|
|
519
|
+
if (!trimmed.startsWith("{")) return void 0;
|
|
520
|
+
let parsed;
|
|
521
|
+
try {
|
|
522
|
+
parsed = JSON.parse(trimmed);
|
|
523
|
+
} catch {
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
527
|
+
const record = parsed;
|
|
528
|
+
const value = record.command ?? record.cmd;
|
|
529
|
+
if (typeof value === "string") return value.length > 0 ? value : void 0;
|
|
530
|
+
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
531
|
+
const words = value.filter((entry) => typeof entry === "string");
|
|
532
|
+
if (words.length !== value.length) return void 0;
|
|
533
|
+
const head = basename(words[0] ?? "");
|
|
534
|
+
if (SHELLS.has(head)) {
|
|
535
|
+
const flag = words.findIndex((word, position) => position > 0 && /^-[a-z]*c$/.test(word));
|
|
536
|
+
if (flag !== -1 && words[flag + 1] !== void 0) return words[flag + 1];
|
|
537
|
+
}
|
|
538
|
+
return words.join(" ");
|
|
539
|
+
}
|
|
540
|
+
function isShellWrapper(words) {
|
|
541
|
+
if (!SHELLS.has(basename(words[0] ?? ""))) return false;
|
|
542
|
+
for (let index = 1; index < words.length; index += 1) {
|
|
543
|
+
const word = words[index];
|
|
544
|
+
if (!word.startsWith("-") || word.startsWith("--") || !word.slice(1).includes("c")) continue;
|
|
545
|
+
return words[index + 1] !== void 0;
|
|
546
|
+
}
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
function commandLines(script) {
|
|
550
|
+
return simpleCommands(script).map((words) => words.join(" "));
|
|
551
|
+
}
|
|
552
|
+
function simpleCommands(script) {
|
|
553
|
+
return shellCommands(script).filter((words) => words.length > 0 && !isShellWrapper(words));
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/turn-index.ts
|
|
557
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
558
|
+
import { appendFile, link, mkdir as mkdir2, open, readFile as readFile2, rename as rename2, stat as stat2, unlink, utimes, writeFile as writeFile2 } from "fs/promises";
|
|
559
|
+
import { homedir as homedir2 } from "os";
|
|
560
|
+
import { join as join3 } from "path";
|
|
561
|
+
import { setTimeout as sleep } from "timers/promises";
|
|
562
|
+
|
|
563
|
+
// src/session-facts.ts
|
|
564
|
+
import { createHash } from "crypto";
|
|
565
|
+
|
|
566
|
+
// src/session-facts-withheld.ts
|
|
567
|
+
function withholdContent(facts) {
|
|
568
|
+
const withheld = () => ({
|
|
569
|
+
value: null,
|
|
570
|
+
spanIds: [],
|
|
571
|
+
unavailable: "Content was not requested."
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
...facts,
|
|
575
|
+
cwd: withheld(),
|
|
576
|
+
repo: withheld(),
|
|
577
|
+
humanTurns: withheld(),
|
|
578
|
+
finalMessages: withheld(),
|
|
579
|
+
changedFiles: withheld(),
|
|
580
|
+
commands: withheld(),
|
|
581
|
+
pullRequests: withheld(),
|
|
582
|
+
subagents: {
|
|
583
|
+
...facts.subagents,
|
|
584
|
+
value: facts.subagents.value?.map((entry) => ({
|
|
585
|
+
...entry,
|
|
586
|
+
taskName: null,
|
|
587
|
+
taskNameUnavailable: "Assignment text was not requested.",
|
|
588
|
+
agentType: null
|
|
589
|
+
})) ?? null
|
|
590
|
+
},
|
|
591
|
+
firstFailure: facts.firstFailure.status === "found" ? { ...facts.firstFailure, message: null, classification: facts.firstFailure.classification ? { ...facts.firstFailure.classification, reason: "Content was not requested." } : null } : facts.firstFailure,
|
|
592
|
+
measurements: {
|
|
593
|
+
...facts.measurements,
|
|
594
|
+
waste: {
|
|
595
|
+
...facts.measurements.waste,
|
|
596
|
+
failingStreaks: {
|
|
597
|
+
...facts.measurements.waste.failingStreaks,
|
|
598
|
+
value: facts.measurements.waste.failingStreaks.value?.map((streak) => ({ ...streak, command: null })) ?? null
|
|
599
|
+
}
|
|
600
|
+
},
|
|
601
|
+
limitStop: facts.measurements.limitStop ? { ...facts.measurements.limitStop, text: null } : null
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/session-measurements.ts
|
|
607
|
+
import { basename as basename2 } from "path";
|
|
608
|
+
|
|
609
|
+
// src/execution.ts
|
|
610
|
+
import {
|
|
611
|
+
summarizeExecution
|
|
612
|
+
} from "@tangle-network/agent-eval/contract";
|
|
613
|
+
import { otlpRowsToRunRecords } from "@tangle-network/agent-eval/traces";
|
|
614
|
+
import {
|
|
615
|
+
firstNumberAttr,
|
|
616
|
+
LLM_CACHE_WRITE_TOKEN_ATTR_KEYS,
|
|
617
|
+
LLM_CACHED_TOKEN_ATTR_KEYS,
|
|
618
|
+
LLM_INPUT_TOKEN_ATTR_KEYS,
|
|
619
|
+
LLM_MODEL_ATTR_KEYS,
|
|
620
|
+
LLM_OUTPUT_TOKEN_ATTR_KEYS,
|
|
621
|
+
LLM_REASONING_TOKEN_ATTR_KEYS,
|
|
622
|
+
OPENINFERENCE_SPAN_KIND
|
|
623
|
+
} from "@tangle-network/agent-eval/trace-attributes";
|
|
624
|
+
var COST_LABEL = "list price (Router rates)";
|
|
625
|
+
var routerPricer;
|
|
626
|
+
function routerPrices() {
|
|
627
|
+
routerPricer ??= import("@tangle-network/agent-runtime").then((runtime) => runtime.priceTokens);
|
|
628
|
+
return routerPricer;
|
|
629
|
+
}
|
|
630
|
+
function count(value) {
|
|
631
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
632
|
+
}
|
|
633
|
+
function modelOf(span) {
|
|
634
|
+
for (const key of LLM_MODEL_ATTR_KEYS) {
|
|
635
|
+
const value = span.attributes[key];
|
|
636
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
var UNRECORDED_MODEL = "(model not recorded)";
|
|
641
|
+
function modelCallContextTokens(span) {
|
|
642
|
+
return callUsage(span)?.input ?? null;
|
|
643
|
+
}
|
|
644
|
+
function callUsage(span) {
|
|
645
|
+
if (span.attributes[OPENINFERENCE_SPAN_KIND] !== "LLM") return null;
|
|
646
|
+
const fresh = count(firstNumberAttr(span.attributes, LLM_INPUT_TOKEN_ATTR_KEYS));
|
|
647
|
+
const output = count(firstNumberAttr(span.attributes, LLM_OUTPUT_TOKEN_ATTR_KEYS));
|
|
648
|
+
const cacheRead = count(firstNumberAttr(span.attributes, LLM_CACHED_TOKEN_ATTR_KEYS)) ?? 0;
|
|
649
|
+
const cacheWrite = count(firstNumberAttr(span.attributes, LLM_CACHE_WRITE_TOKEN_ATTR_KEYS)) ?? 0;
|
|
650
|
+
if ((fresh ?? 0) + (output ?? 0) + cacheRead + cacheWrite === 0) return null;
|
|
651
|
+
const model = modelOf(span);
|
|
652
|
+
return {
|
|
653
|
+
model: model ?? UNRECORDED_MODEL,
|
|
654
|
+
modelRecorded: model !== null,
|
|
655
|
+
input: (fresh ?? 0) + cacheRead + cacheWrite,
|
|
656
|
+
output: output ?? 0,
|
|
657
|
+
cacheRead,
|
|
658
|
+
cacheWrite,
|
|
659
|
+
reasoning: count(firstNumberAttr(span.attributes, LLM_REASONING_TOKEN_ATTR_KEYS)),
|
|
660
|
+
spanId: span.span_id
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function byModel(calls) {
|
|
664
|
+
const models = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const call of calls) {
|
|
666
|
+
const entry = models.get(call.model) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
667
|
+
entry.input += call.input;
|
|
668
|
+
entry.output += call.output;
|
|
669
|
+
entry.cacheRead += call.cacheRead;
|
|
670
|
+
entry.cacheWrite += call.cacheWrite;
|
|
671
|
+
models.set(call.model, entry);
|
|
672
|
+
}
|
|
673
|
+
return [...models].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([model, entry]) => ({ model, ...entry }));
|
|
674
|
+
}
|
|
675
|
+
function sum(values) {
|
|
676
|
+
const present = values.filter((value) => value !== null);
|
|
677
|
+
return present.length === 0 ? null : present.reduce((total, value) => total + value, 0);
|
|
678
|
+
}
|
|
679
|
+
function sessionTokens(own) {
|
|
680
|
+
const calls = own.map(callUsage).filter((call) => call !== null);
|
|
681
|
+
const root = own.find((span) => span.parent_span_id === null && count(span.attributes[SESSION_TOTAL_INPUT_ATTR]) !== null && count(span.attributes[SESSION_TOTAL_OUTPUT_ATTR]) !== null);
|
|
682
|
+
if (root) {
|
|
683
|
+
const input = count(root.attributes[SESSION_TOTAL_INPUT_ATTR]);
|
|
684
|
+
const output = count(root.attributes[SESSION_TOTAL_OUTPUT_ATTR]);
|
|
685
|
+
const cacheRead = count(root.attributes[SESSION_TOTAL_CACHED_INPUT_ATTR]) ?? 0;
|
|
686
|
+
const cacheWrite = count(root.attributes[SESSION_TOTAL_CACHE_WRITE_ATTR]) ?? 0;
|
|
687
|
+
const perCall = byModel(calls);
|
|
688
|
+
const recorded = [...new Set(calls.filter((call) => call.modelRecorded).map((call) => call.model))];
|
|
689
|
+
const callInput = calls.reduce((total, call) => total + call.input, 0);
|
|
690
|
+
const callOutput = calls.reduce((total, call) => total + call.output, 0);
|
|
691
|
+
const models = recorded.length === 1 ? [{ model: recorded[0], input, output, cacheRead, cacheWrite }] : recorded.length > 1 && callInput === input && callOutput === output ? perCall : [];
|
|
692
|
+
return {
|
|
693
|
+
input,
|
|
694
|
+
output,
|
|
695
|
+
cacheRead,
|
|
696
|
+
cacheWrite,
|
|
697
|
+
reasoning: count(root.attributes[SESSION_TOTAL_REASONING_ATTR]),
|
|
698
|
+
modelCalls: calls.length,
|
|
699
|
+
byModel: models,
|
|
700
|
+
spanIds: [root.span_id],
|
|
701
|
+
unavailable: null
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
if (calls.length === 0) {
|
|
705
|
+
return {
|
|
706
|
+
input: null,
|
|
707
|
+
output: null,
|
|
708
|
+
cacheRead: null,
|
|
709
|
+
cacheWrite: null,
|
|
710
|
+
reasoning: null,
|
|
711
|
+
modelCalls: 0,
|
|
712
|
+
byModel: [],
|
|
713
|
+
spanIds: [],
|
|
714
|
+
unavailable: "no model call of this session's own records reports token usage, and no root span carries the harness's cumulative counter"
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
input: calls.reduce((total, call) => total + call.input, 0),
|
|
719
|
+
output: calls.reduce((total, call) => total + call.output, 0),
|
|
720
|
+
cacheRead: calls.reduce((total, call) => total + call.cacheRead, 0),
|
|
721
|
+
cacheWrite: calls.reduce((total, call) => total + call.cacheWrite, 0),
|
|
722
|
+
reasoning: sum(calls.map((call) => call.reasoning)),
|
|
723
|
+
modelCalls: calls.length,
|
|
724
|
+
byModel: byModel(calls),
|
|
725
|
+
spanIds: calls.map((call) => call.spanId),
|
|
726
|
+
unavailable: null
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function needsPrice(tokens) {
|
|
730
|
+
return tokens.input !== null && tokens.input + (tokens.output ?? 0) > 0 && tokens.byModel.length > 0;
|
|
731
|
+
}
|
|
732
|
+
function roundUsd(usd) {
|
|
733
|
+
return Math.round(usd * 1e6) / 1e6;
|
|
734
|
+
}
|
|
735
|
+
function sessionCost(tokens, price) {
|
|
736
|
+
const unknown = (reason) => ({ usd: null, label: COST_LABEL, unavailable: reason });
|
|
737
|
+
if (tokens.input === null) return unknown(`no token counts: ${tokens.unavailable}`);
|
|
738
|
+
if (tokens.input + (tokens.output ?? 0) === 0) return { usd: 0, label: COST_LABEL, unavailable: null };
|
|
739
|
+
if (tokens.byModel.length === 0) {
|
|
740
|
+
return unknown("no model call of the session records its model, or several models share a cumulative counter the calls do not add up to");
|
|
741
|
+
}
|
|
742
|
+
if (price === void 0) return unknown("not priced yet: `await priceReport(report)` loads the price table and fills it");
|
|
743
|
+
const unpriced = tokens.byModel.filter((entry) => entry.model === UNRECORDED_MODEL || price({ model: entry.model, inputTokens: entry.input, outputTokens: entry.output, cacheReadTokens: entry.cacheRead, cacheWriteTokens: entry.cacheWrite }) === void 0);
|
|
744
|
+
if (unpriced.length > 0) {
|
|
745
|
+
return unknown(`the Router lists no price for ${unpriced.map((entry) => entry.model).join(", ")}`);
|
|
746
|
+
}
|
|
747
|
+
const usd = tokens.byModel.reduce((total, entry) => total + price({
|
|
748
|
+
model: entry.model,
|
|
749
|
+
inputTokens: entry.input,
|
|
750
|
+
outputTokens: entry.output,
|
|
751
|
+
cacheReadTokens: entry.cacheRead,
|
|
752
|
+
cacheWriteTokens: entry.cacheWrite
|
|
753
|
+
}), 0);
|
|
754
|
+
return { usd: roundUsd(usd), label: COST_LABEL, unavailable: null };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// src/skill-usage.ts
|
|
758
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from "fs/promises";
|
|
759
|
+
import { homedir } from "os";
|
|
760
|
+
import { dirname, join } from "path";
|
|
761
|
+
import { STORES } from "@tangle-network/harness-sessions/catalog";
|
|
762
|
+
var NEWLINE = 10;
|
|
763
|
+
var SLASH_LOOKAHEAD_LINES = 8;
|
|
764
|
+
var MAX_PENDING = 16;
|
|
765
|
+
var READ_CONCURRENCY = 8;
|
|
766
|
+
var CLAUDE_NEEDLES = [
|
|
767
|
+
'"commandName":"',
|
|
768
|
+
"Launching skill: ",
|
|
769
|
+
" (forked execution",
|
|
770
|
+
"<command-name>",
|
|
771
|
+
"Base directory for this skill: "
|
|
772
|
+
].map((n) => Buffer.from(n));
|
|
773
|
+
var SKILL_RESULT_TEXT = /^(?:Launching skill: (\S+)|Skill "([^"]+)" (?:launched|completed) \(forked execution)/;
|
|
774
|
+
var CODEX_NEEDLE = Buffer.from("SKILL.md");
|
|
775
|
+
var KIMI_NEEDLES = ['"type": "ToolCall"', '"type":"ToolCall"', '"type": "ToolCallPart"', '"type":"ToolCallPart"', '"type": "ToolResult"', '"type":"ToolResult"'].map((n) => Buffer.from(n));
|
|
776
|
+
var BASE_DIRECTORY = "Base directory for this skill: ";
|
|
777
|
+
var SLASH_COMMAND = /<command-name>\/?([^<\s]+)<\/command-name>/;
|
|
778
|
+
var SKILL_READ_VERB = /(?:^|[\s;&|(`'"])(?:cat|sed|head|tail|less|more|awk|nl|bat)\s/;
|
|
779
|
+
var SKILL_DOCUMENT = /(?:^|[\s'"=:(`])((?:[^\s'"`;|&()]*\/)?([A-Za-z0-9][A-Za-z0-9._-]*))\/SKILL\.md\b/g;
|
|
780
|
+
var SESSION_UUID = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/;
|
|
781
|
+
function skillLaunchedBy(resultText) {
|
|
782
|
+
const match = SKILL_RESULT_TEXT.exec(resultText);
|
|
783
|
+
return match ? match[1] ?? match[2] : null;
|
|
784
|
+
}
|
|
785
|
+
function slashCommandOf(text) {
|
|
786
|
+
return SLASH_COMMAND.exec(text)?.[1] ?? null;
|
|
787
|
+
}
|
|
788
|
+
function skillDocumentReads(text, output, options = {}) {
|
|
789
|
+
if (!text.includes("SKILL.md")) return [];
|
|
790
|
+
if (options.path ? !text.endsWith("/SKILL.md") : !SKILL_READ_VERB.test(text)) return [];
|
|
791
|
+
const reads = /* @__PURE__ */ new Map();
|
|
792
|
+
for (const match of text.matchAll(SKILL_DOCUMENT)) {
|
|
793
|
+
const directory = match[1];
|
|
794
|
+
if (!output.includes(`${directory}/SKILL.md: No such file`) && !reads.has(directory)) reads.set(directory, match[2]);
|
|
795
|
+
if (options.path) break;
|
|
796
|
+
}
|
|
797
|
+
return [...reads].map(([directory, skill]) => ({ skill, directory }));
|
|
798
|
+
}
|
|
799
|
+
function rootOfDirectory(directory) {
|
|
800
|
+
const slash = directory.lastIndexOf("/");
|
|
801
|
+
return slash > 0 ? directory.slice(0, slash) : null;
|
|
802
|
+
}
|
|
803
|
+
var TYPED_SLASH = /^\/([A-Za-z0-9][\w:.-]*)(?:\s|$)/u;
|
|
804
|
+
function skillUseOfSpans(spans, harness, sweepThreshold = DEFAULT_SWEEP_THRESHOLD) {
|
|
805
|
+
const counts = {};
|
|
806
|
+
const add = (skill, kind) => {
|
|
807
|
+
const row = counts[skill] ?? (counts[skill] = { model: 0, slash: 0, read: 0 });
|
|
808
|
+
row[kind] += 1;
|
|
809
|
+
};
|
|
810
|
+
let slash = null;
|
|
811
|
+
for (const span of spans) {
|
|
812
|
+
const content = typeof span.attributes.content === "string" ? span.attributes.content.trim() : "";
|
|
813
|
+
if (slash && (slash.seen += 1) > SLASH_LOOKAHEAD_LINES) slash = null;
|
|
814
|
+
if (span.name === "user.prompt") {
|
|
815
|
+
const actor = span.attributes[ACTOR_ATTR];
|
|
816
|
+
const named = slashCommandOf(content) ?? (actor === "human" ? TYPED_SLASH.exec(content)?.[1] ?? null : null);
|
|
817
|
+
if (named) {
|
|
818
|
+
slash = { skill: skillName(named), seen: 0 };
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
if (slash && content.length > 0 && actor !== "human" && !content.startsWith("<local-command-") && !content.startsWith("<command-")) {
|
|
822
|
+
add(slash.skill, "slash");
|
|
823
|
+
slash = null;
|
|
824
|
+
}
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const kind = span.attributes["openinference.span.kind"];
|
|
828
|
+
if (kind === "LLM" || kind === "TOOL" || span.name === "message.assistant") slash = null;
|
|
829
|
+
if (kind !== "TOOL" || span.status.code === "UNSET") continue;
|
|
830
|
+
const tool = typeof span.attributes["tool.name"] === "string" ? span.attributes["tool.name"] : span.name.replace(/^tool\./u, "");
|
|
831
|
+
const input = typeof span.attributes["input.value"] === "string" ? span.attributes["input.value"] : "";
|
|
832
|
+
const output = typeof span.attributes["output.value"] === "string" ? span.attributes["output.value"] : "";
|
|
833
|
+
if (tool === "Skill") {
|
|
834
|
+
if (span.status.code !== "OK") continue;
|
|
835
|
+
const named = skillLaunchedBy(output) ?? skillArgument(input);
|
|
836
|
+
if (named) add(skillName(named), "model");
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (harness === "claude-code") continue;
|
|
840
|
+
const command = commandTextFromInput(input);
|
|
841
|
+
const path = tool === "ReadFile" && span.status.code === "OK" ? pathArgument(input) : null;
|
|
842
|
+
const reads = command !== void 0 ? skillDocumentReads(command, output) : path ? skillDocumentReads(path, output, { path: true }) : [];
|
|
843
|
+
if (reads.length >= sweepThreshold) continue;
|
|
844
|
+
for (const read of reads) add(read.skill, "read");
|
|
845
|
+
}
|
|
846
|
+
return counts;
|
|
847
|
+
}
|
|
848
|
+
function jsonObject(text) {
|
|
849
|
+
try {
|
|
850
|
+
const value = JSON.parse(text);
|
|
851
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
852
|
+
} catch {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function skillArgument(input) {
|
|
857
|
+
const args = jsonObject(input);
|
|
858
|
+
const value = args?.skill ?? args?.command;
|
|
859
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
860
|
+
}
|
|
861
|
+
function pathArgument(input) {
|
|
862
|
+
const value = jsonObject(input)?.path;
|
|
863
|
+
return typeof value === "string" ? value : null;
|
|
864
|
+
}
|
|
865
|
+
function defaultSkillUsageSources() {
|
|
866
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
867
|
+
return {
|
|
868
|
+
claudeProjects: join(homedir(), STORES["claude-code.projects-jsonl"].root),
|
|
869
|
+
codexSessions: [join(codexHome, ...STORES["codex.rollout-jsonl"].root.split("/").slice(1)), join(codexHome, "archived_sessions")],
|
|
870
|
+
kimiSessions: join(homedir(), STORES["kimi.session-dir"].root)
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function defaultSkillUsageStorePath() {
|
|
874
|
+
const state = process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
875
|
+
return join(state, "traces", "skill-usage.json");
|
|
876
|
+
}
|
|
877
|
+
function eventKey(e) {
|
|
878
|
+
return `${e.harness}\0${e.id}`;
|
|
879
|
+
}
|
|
880
|
+
var SkillUsageStore = class _SkillUsageStore {
|
|
881
|
+
constructor(path) {
|
|
882
|
+
this.path = path;
|
|
883
|
+
}
|
|
884
|
+
path;
|
|
885
|
+
events = /* @__PURE__ */ new Map();
|
|
886
|
+
files = {};
|
|
887
|
+
static async load(path) {
|
|
888
|
+
const store = new _SkillUsageStore(path);
|
|
889
|
+
let raw;
|
|
890
|
+
try {
|
|
891
|
+
raw = await readFile(path, "utf8");
|
|
892
|
+
} catch (error) {
|
|
893
|
+
if (error.code === "ENOENT") return store;
|
|
894
|
+
throw error;
|
|
895
|
+
}
|
|
896
|
+
const parsed = JSON.parse(raw);
|
|
897
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.events) || typeof parsed.files !== "object") {
|
|
898
|
+
throw new Error(`skill usage store ${path} has an unknown format; move it aside to rebuild from transcripts`);
|
|
899
|
+
}
|
|
900
|
+
store.files = parsed.files ?? {};
|
|
901
|
+
for (const [harness, kind, skill, root, ts, session, id] of parsed.events) {
|
|
902
|
+
store.add({ harness, kind, skill, root, ts, session, id });
|
|
903
|
+
}
|
|
904
|
+
return store;
|
|
905
|
+
}
|
|
906
|
+
get size() {
|
|
907
|
+
return this.events.size;
|
|
908
|
+
}
|
|
909
|
+
list() {
|
|
910
|
+
return [...this.events.values()];
|
|
911
|
+
}
|
|
912
|
+
add(event) {
|
|
913
|
+
const key = eventKey(event);
|
|
914
|
+
if (this.events.has(key)) return false;
|
|
915
|
+
this.events.set(key, event);
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
setRoot(harness, id, root) {
|
|
919
|
+
const event = this.events.get(eventKey({ harness, id }));
|
|
920
|
+
if (event && event.root === null) event.root = root;
|
|
921
|
+
}
|
|
922
|
+
async save() {
|
|
923
|
+
const body = {
|
|
924
|
+
version: 1,
|
|
925
|
+
files: this.files,
|
|
926
|
+
events: this.list().map((e) => [e.harness, e.kind, e.skill, e.root, e.ts, e.session, e.id])
|
|
927
|
+
};
|
|
928
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
929
|
+
const temporary = `${this.path}.${process.pid}.tmp`;
|
|
930
|
+
await writeFile(temporary, JSON.stringify(body));
|
|
931
|
+
await rename(temporary, this.path);
|
|
932
|
+
}
|
|
933
|
+
/** Read bytes appended since the previous refresh. Cost is proportional to new bytes. */
|
|
934
|
+
async refresh(sources = defaultSkillUsageSources()) {
|
|
935
|
+
const claude = await this.refreshHarness("claude-code", await jsonlFiles([sources.claudeProjects]));
|
|
936
|
+
const codex = await this.refreshHarness("codex", await jsonlFiles(sources.codexSessions));
|
|
937
|
+
const kimi = await this.refreshHarness("kimi-code", await jsonlFiles(sources.kimiSessions ? [sources.kimiSessions] : [], "wire.jsonl"));
|
|
938
|
+
return [claude, codex, kimi];
|
|
939
|
+
}
|
|
940
|
+
async refreshHarness(harness, paths) {
|
|
941
|
+
const summary = { harness, files: paths.length, filesRead: 0, bytesRead: 0, eventsAdded: 0 };
|
|
942
|
+
let next = 0;
|
|
943
|
+
const worker = async () => {
|
|
944
|
+
while (next < paths.length) {
|
|
945
|
+
const path = paths[next++];
|
|
946
|
+
const read = await this.refreshFile(harness, path);
|
|
947
|
+
if (read === null) continue;
|
|
948
|
+
summary.filesRead += 1;
|
|
949
|
+
summary.bytesRead += read.bytes;
|
|
950
|
+
summary.eventsAdded += read.events;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
await Promise.all(Array.from({ length: READ_CONCURRENCY }, worker));
|
|
954
|
+
return summary;
|
|
955
|
+
}
|
|
956
|
+
async refreshFile(harness, path) {
|
|
957
|
+
let info;
|
|
958
|
+
try {
|
|
959
|
+
info = await stat(path);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error.code === "ENOENT") return null;
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
const previous = this.files[path];
|
|
965
|
+
const from = resumeOffset(previous, info);
|
|
966
|
+
if (from === null) return null;
|
|
967
|
+
const cursor = previous && from === previous.offset ? { ...previous } : { ino: info.ino, size: 0, offset: 0 };
|
|
968
|
+
const session = sessionFromPath(path);
|
|
969
|
+
let events = 0;
|
|
970
|
+
const { bytes: bytes2, end } = await readCompleteLines(path, from, info.size, (complete) => {
|
|
971
|
+
events += harness === "claude-code" ? this.scanClaude(complete, cursor, session) : harness === "codex" ? this.scanCodex(complete, session) : this.scanKimi(complete, cursor, kimiSessionFromPath(path));
|
|
972
|
+
});
|
|
973
|
+
cursor.offset = end;
|
|
974
|
+
cursor.ino = info.ino;
|
|
975
|
+
cursor.size = info.size;
|
|
976
|
+
if (cursor.pending?.length === 0) delete cursor.pending;
|
|
977
|
+
this.files[path] = cursor;
|
|
978
|
+
return { bytes: bytes2, events };
|
|
979
|
+
}
|
|
980
|
+
scanClaude(buffer, cursor, fileSession) {
|
|
981
|
+
let added = 0;
|
|
982
|
+
const pending = cursor.pending ?? [];
|
|
983
|
+
cursor.pending = [];
|
|
984
|
+
const lines = new LineIndex(buffer);
|
|
985
|
+
for (const command of pending) {
|
|
986
|
+
const outcome = this.resolveSlash(buffer, lines, 0, command);
|
|
987
|
+
if (outcome === "confirmed") added += 1;
|
|
988
|
+
else if (outcome === "pending") cursor.pending.push(command);
|
|
989
|
+
}
|
|
990
|
+
for (const start of hitLineStarts(buffer, CLAUDE_NEEDLES)) {
|
|
991
|
+
const end = lines.endOf(start);
|
|
992
|
+
const record = parseLine(buffer, start, end);
|
|
993
|
+
if (!record || record.type !== "user") continue;
|
|
994
|
+
const session = typeof record.sessionId === "string" ? record.sessionId : fileSession;
|
|
995
|
+
const ts = typeof record.timestamp === "string" ? record.timestamp : null;
|
|
996
|
+
const loaded = loadedSkill(record);
|
|
997
|
+
if (loaded) {
|
|
998
|
+
if (this.add({ harness: "claude-code", kind: "model", skill: skillName(loaded.skill), root: null, ts, session, id: loaded.id })) added += 1;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const text = messageText(record);
|
|
1002
|
+
if (record.isMeta === true && text.startsWith(BASE_DIRECTORY)) {
|
|
1003
|
+
if (typeof record.sourceToolUseID === "string") {
|
|
1004
|
+
this.setRoot("claude-code", record.sourceToolUseID, rootOf(text));
|
|
1005
|
+
}
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
const content = record.message?.content;
|
|
1009
|
+
if (typeof content !== "string" || !content.startsWith("<command-")) continue;
|
|
1010
|
+
const name = slashCommandOf(content);
|
|
1011
|
+
if (!name || typeof record.uuid !== "string") continue;
|
|
1012
|
+
const command = { uuid: record.uuid, skill: skillName(name), ts, session, seen: 0 };
|
|
1013
|
+
const outcome = this.resolveSlash(buffer, lines, end + 1, command);
|
|
1014
|
+
if (outcome === "confirmed") added += 1;
|
|
1015
|
+
else if (outcome === "pending" && cursor.pending.length < MAX_PENDING) cursor.pending.push(command);
|
|
1016
|
+
}
|
|
1017
|
+
return added;
|
|
1018
|
+
}
|
|
1019
|
+
/** Confirm a slash command by its expansion: the first later record whose parent it is. */
|
|
1020
|
+
resolveSlash(buffer, lines, from, command) {
|
|
1021
|
+
const at = buffer.indexOf(`"parentUuid":"${command.uuid}"`, from);
|
|
1022
|
+
let start = from;
|
|
1023
|
+
while (command.seen < SLASH_LOOKAHEAD_LINES) {
|
|
1024
|
+
if (start >= buffer.length) return "pending";
|
|
1025
|
+
const end = lines.endOf(start);
|
|
1026
|
+
command.seen += 1;
|
|
1027
|
+
if (at !== -1 && at >= start && at < end) {
|
|
1028
|
+
const record = parseLine(buffer, start, end);
|
|
1029
|
+
const text = record ? messageText(record) : "";
|
|
1030
|
+
if (!record || record.type !== "user" || record.isMeta !== true || text.length === 0) return "rejected";
|
|
1031
|
+
const added = this.add({
|
|
1032
|
+
harness: "claude-code",
|
|
1033
|
+
kind: "slash",
|
|
1034
|
+
skill: command.skill,
|
|
1035
|
+
root: text.startsWith(BASE_DIRECTORY) ? rootOf(text) : null,
|
|
1036
|
+
ts: command.ts,
|
|
1037
|
+
session: command.session,
|
|
1038
|
+
id: command.uuid
|
|
1039
|
+
});
|
|
1040
|
+
return added ? "confirmed" : "rejected";
|
|
1041
|
+
}
|
|
1042
|
+
start = end + 1;
|
|
1043
|
+
}
|
|
1044
|
+
return "rejected";
|
|
1045
|
+
}
|
|
1046
|
+
scanCodex(buffer, fileSession) {
|
|
1047
|
+
let added = 0;
|
|
1048
|
+
const lines = new LineIndex(buffer);
|
|
1049
|
+
for (const start of hitLineStarts(buffer, [CODEX_NEEDLE])) {
|
|
1050
|
+
const record = parseLine(buffer, start, lines.endOf(start));
|
|
1051
|
+
const payload = record?.payload;
|
|
1052
|
+
const item = payload?.item;
|
|
1053
|
+
if (payload?.type !== "item_completed" || item?.type !== "CommandExecution" || typeof item.id !== "string") continue;
|
|
1054
|
+
const command = Array.isArray(item.command) ? item.command.join(" ") : String(item.command ?? "");
|
|
1055
|
+
const output = typeof item.aggregated_output === "string" ? item.aggregated_output : String(item.stdout ?? "");
|
|
1056
|
+
const ts = typeof record?.timestamp === "string" ? record.timestamp : null;
|
|
1057
|
+
for (const { skill, directory } of skillDocumentReads(command, output)) {
|
|
1058
|
+
const event = {
|
|
1059
|
+
harness: "codex",
|
|
1060
|
+
kind: "read",
|
|
1061
|
+
skill,
|
|
1062
|
+
root: rootOfDirectory(directory),
|
|
1063
|
+
ts,
|
|
1064
|
+
session: fileSession,
|
|
1065
|
+
id: `${item.id}#${directory}`
|
|
1066
|
+
};
|
|
1067
|
+
if (this.add(event)) added += 1;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
return added;
|
|
1071
|
+
}
|
|
1072
|
+
scanKimi(buffer, cursor, session) {
|
|
1073
|
+
let added = 0;
|
|
1074
|
+
const calls = cursor.kimiCalls ?? {};
|
|
1075
|
+
const lines = new LineIndex(buffer);
|
|
1076
|
+
for (const start of hitLineStarts(buffer, KIMI_NEEDLES)) {
|
|
1077
|
+
const record = parseLine(buffer, start, lines.endOf(start));
|
|
1078
|
+
const message = record?.message;
|
|
1079
|
+
const payload = message?.payload;
|
|
1080
|
+
if (!payload) continue;
|
|
1081
|
+
if (message?.type === "ToolCall") {
|
|
1082
|
+
const fn = payload.function;
|
|
1083
|
+
const id = payload.id;
|
|
1084
|
+
cursor.kimiLastCall = typeof id === "string" ? id : void 0;
|
|
1085
|
+
if (typeof id === "string" && (fn?.name === "ReadFile" || fn?.name === "Shell")) {
|
|
1086
|
+
calls[id] = { name: fn.name, args: typeof fn.arguments === "string" ? fn.arguments : "", ts: kimiTimestamp(record?.timestamp) };
|
|
1087
|
+
}
|
|
1088
|
+
} else if (message?.type === "ToolCallPart") {
|
|
1089
|
+
const call = cursor.kimiLastCall ? calls[cursor.kimiLastCall] : void 0;
|
|
1090
|
+
if (call && typeof payload.arguments_part === "string") call.args += payload.arguments_part;
|
|
1091
|
+
} else if (message?.type === "ToolResult" && typeof payload.tool_call_id === "string") {
|
|
1092
|
+
const id = payload.tool_call_id;
|
|
1093
|
+
const call = calls[id];
|
|
1094
|
+
delete calls[id];
|
|
1095
|
+
if (!call) continue;
|
|
1096
|
+
const result = payload.return_value;
|
|
1097
|
+
if (result?.is_error !== false) continue;
|
|
1098
|
+
let args;
|
|
1099
|
+
try {
|
|
1100
|
+
args = JSON.parse(call.args);
|
|
1101
|
+
} catch {
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const output = typeof result.output === "string" ? result.output : "";
|
|
1105
|
+
const reads = call.name === "ReadFile" && typeof args.path === "string" ? skillDocumentReads(args.path, output, { path: true }) : call.name === "Shell" && typeof args.command === "string" ? skillDocumentReads(args.command, output) : [];
|
|
1106
|
+
for (const { skill, directory } of reads) {
|
|
1107
|
+
if (this.add({
|
|
1108
|
+
harness: "kimi-code",
|
|
1109
|
+
kind: "read",
|
|
1110
|
+
skill,
|
|
1111
|
+
root: rootOfDirectory(directory),
|
|
1112
|
+
ts: call.ts,
|
|
1113
|
+
session,
|
|
1114
|
+
id: `${session}:${id}#${directory}`
|
|
1115
|
+
})) added += 1;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
cursor.kimiCalls = calls;
|
|
1120
|
+
return added;
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
var LineIndex = class {
|
|
1124
|
+
constructor(buffer) {
|
|
1125
|
+
this.buffer = buffer;
|
|
1126
|
+
}
|
|
1127
|
+
buffer;
|
|
1128
|
+
endOf(start) {
|
|
1129
|
+
const end = this.buffer.indexOf(NEWLINE, start);
|
|
1130
|
+
return end === -1 ? this.buffer.length : end;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
function hitLineStarts(buffer, needles) {
|
|
1134
|
+
const starts = /* @__PURE__ */ new Set();
|
|
1135
|
+
for (const needle of needles) {
|
|
1136
|
+
let at = buffer.indexOf(needle);
|
|
1137
|
+
while (at !== -1) {
|
|
1138
|
+
const start = buffer.lastIndexOf(NEWLINE, at) + 1;
|
|
1139
|
+
starts.add(start);
|
|
1140
|
+
const end = buffer.indexOf(NEWLINE, at);
|
|
1141
|
+
if (end === -1) break;
|
|
1142
|
+
at = buffer.indexOf(needle, end + 1);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return [...starts].sort((a, b) => a - b);
|
|
1146
|
+
}
|
|
1147
|
+
function parseLine(buffer, start, end) {
|
|
1148
|
+
try {
|
|
1149
|
+
const value = JSON.parse(buffer.toString("utf8", start, end));
|
|
1150
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1151
|
+
} catch {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function messageText(record) {
|
|
1156
|
+
const content = record.message?.content;
|
|
1157
|
+
if (typeof content === "string") return content;
|
|
1158
|
+
if (!Array.isArray(content)) return "";
|
|
1159
|
+
const first = content[0];
|
|
1160
|
+
return first?.type === "text" && typeof first.text === "string" ? first.text : "";
|
|
1161
|
+
}
|
|
1162
|
+
function loadedSkill(record) {
|
|
1163
|
+
const content = record.message?.content;
|
|
1164
|
+
if (!Array.isArray(content)) return null;
|
|
1165
|
+
const result = record.toolUseResult;
|
|
1166
|
+
for (const part of content) {
|
|
1167
|
+
if (part?.type !== "tool_result" || typeof part.tool_use_id !== "string" || part.is_error === true) continue;
|
|
1168
|
+
if (result?.success === true && typeof result.commandName === "string") return { skill: result.commandName, id: part.tool_use_id };
|
|
1169
|
+
const text = typeof part.content === "string" ? part.content : Array.isArray(part.content) ? String(part.content[0]?.text ?? "") : "";
|
|
1170
|
+
const skill = skillLaunchedBy(text);
|
|
1171
|
+
if (skill) return { skill, id: part.tool_use_id };
|
|
1172
|
+
}
|
|
1173
|
+
return null;
|
|
1174
|
+
}
|
|
1175
|
+
function skillName(value) {
|
|
1176
|
+
return value.replace(/^\//, "");
|
|
1177
|
+
}
|
|
1178
|
+
function rootOf(baseDirectoryText) {
|
|
1179
|
+
const directory = baseDirectoryText.slice(BASE_DIRECTORY.length).split("\n", 1)[0].trim();
|
|
1180
|
+
return dirname(directory);
|
|
1181
|
+
}
|
|
1182
|
+
function sessionFromPath(path) {
|
|
1183
|
+
return SESSION_UUID.exec(path)?.[1] ?? path;
|
|
1184
|
+
}
|
|
1185
|
+
function kimiSessionFromPath(path) {
|
|
1186
|
+
const match = /\/([0-9a-f-]{36})(?:\/subagents\/([^/]+))?\/wire\.jsonl$/.exec(path);
|
|
1187
|
+
return match ? `${match[1]}${match[2] ? `:${match[2]}` : ""}` : path;
|
|
1188
|
+
}
|
|
1189
|
+
function kimiTimestamp(value) {
|
|
1190
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
1191
|
+
const date = new Date(value * 1e3);
|
|
1192
|
+
return Number.isNaN(date.getTime()) ? null : date.toISOString();
|
|
1193
|
+
}
|
|
1194
|
+
async function jsonlFiles(roots, name) {
|
|
1195
|
+
const files = [];
|
|
1196
|
+
for (const root of roots) {
|
|
1197
|
+
let entries;
|
|
1198
|
+
try {
|
|
1199
|
+
entries = await readdir(root, { recursive: true, withFileTypes: true });
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
if (error.code === "ENOENT") continue;
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
for (const entry of entries) {
|
|
1205
|
+
if (entry.isFile() && (name ? entry.name === name : entry.name.endsWith(".jsonl"))) files.push(join(entry.parentPath, entry.name));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return files.sort();
|
|
1209
|
+
}
|
|
1210
|
+
function defaultSkillCatalogRoots() {
|
|
1211
|
+
const codexHome = process.env.CODEX_HOME ?? join(homedir(), ".codex");
|
|
1212
|
+
return {
|
|
1213
|
+
claudeHome: join(homedir(), ".claude"),
|
|
1214
|
+
codexSkillDirs: [join(codexHome, "skills"), join(codexHome, "skills", ".system"), join(homedir(), ".agents", "skills")]
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
async function skillFolders(directory, prefix = "") {
|
|
1218
|
+
let entries;
|
|
1219
|
+
try {
|
|
1220
|
+
entries = await readdir(directory);
|
|
1221
|
+
} catch {
|
|
1222
|
+
return [];
|
|
1223
|
+
}
|
|
1224
|
+
const found = [];
|
|
1225
|
+
for (const entry of entries.sort()) {
|
|
1226
|
+
if (entry.startsWith(".")) continue;
|
|
1227
|
+
const path = join(directory, entry);
|
|
1228
|
+
try {
|
|
1229
|
+
await stat(join(path, "SKILL.md"));
|
|
1230
|
+
found.push({ name: `${prefix}${entry}`, path });
|
|
1231
|
+
} catch {
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
return found;
|
|
1235
|
+
}
|
|
1236
|
+
async function readJsonObject(path) {
|
|
1237
|
+
try {
|
|
1238
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
1239
|
+
return value && typeof value === "object" ? value : null;
|
|
1240
|
+
} catch {
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
async function readInstalledSkills(roots = defaultSkillCatalogRoots()) {
|
|
1245
|
+
const installed = [];
|
|
1246
|
+
for (const skill of await skillFolders(join(roots.claudeHome, "skills"))) {
|
|
1247
|
+
installed.push({ ...skill, harness: "claude-code" });
|
|
1248
|
+
}
|
|
1249
|
+
const settings = await readJsonObject(join(roots.claudeHome, "settings.json"));
|
|
1250
|
+
const enabled = settings?.enabledPlugins ?? {};
|
|
1251
|
+
const plugins = await readJsonObject(join(roots.claudeHome, "plugins", "installed_plugins.json"));
|
|
1252
|
+
const pluginEntries = plugins?.plugins ?? {};
|
|
1253
|
+
for (const [key, value] of Object.entries(pluginEntries)) {
|
|
1254
|
+
if (enabled[key] !== true) continue;
|
|
1255
|
+
const plugin = key.split("@", 1)[0];
|
|
1256
|
+
for (const install of Array.isArray(value) ? value : [value]) {
|
|
1257
|
+
if (typeof install?.installPath !== "string") continue;
|
|
1258
|
+
for (const skill of await skillFolders(join(install.installPath, "skills"), `${plugin}:`)) {
|
|
1259
|
+
installed.push({ ...skill, harness: "claude-code" });
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
for (const directory of roots.codexSkillDirs) {
|
|
1264
|
+
for (const skill of await skillFolders(directory)) installed.push({ ...skill, harness: "codex" });
|
|
1265
|
+
}
|
|
1266
|
+
return installed;
|
|
1267
|
+
}
|
|
1268
|
+
var DEFAULT_SWEEP_THRESHOLD = 3;
|
|
1269
|
+
function commandOf(event) {
|
|
1270
|
+
const hash = event.id.indexOf("#");
|
|
1271
|
+
return hash === -1 ? event.id : event.id.slice(0, hash);
|
|
1272
|
+
}
|
|
1273
|
+
function buildSkillUsageReport(events, installed, storePath, options = {}) {
|
|
1274
|
+
const sweepThreshold = options.sweepThreshold ?? DEFAULT_SWEEP_THRESHOLD;
|
|
1275
|
+
const readsByCommand = /* @__PURE__ */ new Map();
|
|
1276
|
+
for (const event of events) {
|
|
1277
|
+
if (event.kind === "read") readsByCommand.set(commandOf(event), (readsByCommand.get(commandOf(event)) ?? 0) + 1);
|
|
1278
|
+
}
|
|
1279
|
+
const sweeps = new Set([...readsByCommand].filter(([, reads]) => reads >= sweepThreshold).map(([command]) => command));
|
|
1280
|
+
const since = options.since;
|
|
1281
|
+
const sinceIso = since === void 0 ? null : new Date(since).toISOString();
|
|
1282
|
+
const inWindow = (event) => sinceIso === null || event.ts !== null && event.ts >= sinceIso;
|
|
1283
|
+
const rows = /* @__PURE__ */ new Map();
|
|
1284
|
+
let sweepReadsExcluded = 0;
|
|
1285
|
+
for (const event of events) {
|
|
1286
|
+
if (!inWindow(event)) continue;
|
|
1287
|
+
if (event.kind === "read" && sweeps.has(commandOf(event))) {
|
|
1288
|
+
sweepReadsExcluded += 1;
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
const row = rows.get(event.skill) ?? {
|
|
1292
|
+
skill: event.skill,
|
|
1293
|
+
model: 0,
|
|
1294
|
+
slash: 0,
|
|
1295
|
+
read: 0,
|
|
1296
|
+
sessions: 0,
|
|
1297
|
+
firstUsed: null,
|
|
1298
|
+
lastUsed: null,
|
|
1299
|
+
installedIn: [],
|
|
1300
|
+
sessionSet: /* @__PURE__ */ new Set()
|
|
1301
|
+
};
|
|
1302
|
+
row[event.kind] += 1;
|
|
1303
|
+
row.sessionSet.add(`${event.harness}\0${event.session}`);
|
|
1304
|
+
if (event.ts !== null) {
|
|
1305
|
+
if (row.firstUsed === null || event.ts < row.firstUsed) row.firstUsed = event.ts;
|
|
1306
|
+
if (row.lastUsed === null || event.ts > row.lastUsed) row.lastUsed = event.ts;
|
|
1307
|
+
}
|
|
1308
|
+
rows.set(event.skill, row);
|
|
1309
|
+
}
|
|
1310
|
+
const installedBySkill = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const skill of installed) {
|
|
1312
|
+
const harnesses = installedBySkill.get(skill.name) ?? /* @__PURE__ */ new Set();
|
|
1313
|
+
harnesses.add(skill.harness);
|
|
1314
|
+
installedBySkill.set(skill.name, harnesses);
|
|
1315
|
+
}
|
|
1316
|
+
const result = [];
|
|
1317
|
+
for (const { sessionSet, ...row } of rows.values()) {
|
|
1318
|
+
result.push({ ...row, sessions: sessionSet.size, installedIn: [...installedBySkill.get(row.skill) ?? []].sort() });
|
|
1319
|
+
}
|
|
1320
|
+
result.sort((a, b) => b.sessions - a.sessions || b.model + b.slash + b.read - (a.model + a.slash + a.read) || a.skill.localeCompare(b.skill));
|
|
1321
|
+
const unused = [...installedBySkill].filter(([skill]) => !rows.has(skill)).map(([skill, harnesses]) => ({ skill, installedIn: [...harnesses].sort() })).sort((a, b) => a.skill.localeCompare(b.skill));
|
|
1322
|
+
let firstEvent = null;
|
|
1323
|
+
let lastEvent = null;
|
|
1324
|
+
for (const event of events) {
|
|
1325
|
+
if (event.ts === null) continue;
|
|
1326
|
+
if (firstEvent === null || event.ts < firstEvent) firstEvent = event.ts;
|
|
1327
|
+
if (lastEvent === null || event.ts > lastEvent) lastEvent = event.ts;
|
|
1328
|
+
}
|
|
1329
|
+
return {
|
|
1330
|
+
since: sinceIso,
|
|
1331
|
+
storePath,
|
|
1332
|
+
refresh: options.refresh ?? null,
|
|
1333
|
+
storedEvents: events.length,
|
|
1334
|
+
firstEvent,
|
|
1335
|
+
lastEvent,
|
|
1336
|
+
rows: result,
|
|
1337
|
+
unused,
|
|
1338
|
+
sweepThreshold,
|
|
1339
|
+
sweepCommands: sweeps.size,
|
|
1340
|
+
sweepReadsExcluded,
|
|
1341
|
+
notIndexed: ["opencode", "pi", "gemini", "factory", "other harnesses"]
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function day(ts) {
|
|
1345
|
+
return ts === null ? "-" : ts.slice(0, 10);
|
|
1346
|
+
}
|
|
1347
|
+
function renderSkillUsage(report) {
|
|
1348
|
+
const window = report.since === null ? "in every stored event" : `since ${report.since.slice(0, 10)}`;
|
|
1349
|
+
const lines = [
|
|
1350
|
+
`${report.rows.length} skill${report.rows.length === 1 ? "" : "s"} used ${window} \xB7 ${report.unused.length} installed with no use (traces skills --unused) \xB7 no model call`
|
|
1351
|
+
];
|
|
1352
|
+
if (report.rows.length === 0) return lines.join("\n");
|
|
1353
|
+
const width = Math.max(5, ...report.rows.map((r) => r.skill.length));
|
|
1354
|
+
lines.push(`${"skill".padEnd(width)} model slash read sessions last installed`);
|
|
1355
|
+
for (const row of report.rows) {
|
|
1356
|
+
lines.push(
|
|
1357
|
+
`${row.skill.padEnd(width)} ${String(row.model).padStart(5)} ${String(row.slash).padStart(5)} ${String(row.read).padStart(5)} ${String(row.sessions).padStart(8)} ${day(row.lastUsed)} ${row.installedIn.join(",") || "-"}`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
return lines.join("\n");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/session-measurements.ts
|
|
1364
|
+
var STALL_MINUTES = 10;
|
|
1365
|
+
var STREAK_RUNS = 3;
|
|
1366
|
+
var COMMAND_TEXT_CAP = 300;
|
|
1367
|
+
var CONTEXT_BAND_TOKENS = 1e5;
|
|
1368
|
+
var CONTEXT_BANDS = 10;
|
|
1369
|
+
var LATENCY_MAX_MS = 6e5;
|
|
1370
|
+
function contextBandLabel(index) {
|
|
1371
|
+
return index >= CONTEXT_BANDS - 1 ? `${CONTEXT_BANDS - 1}00k+` : `${index * 100 === 0 ? 0 : `${index}00`}-${index + 1}00k`;
|
|
1372
|
+
}
|
|
1373
|
+
function rankPercentile(sorted, q) {
|
|
1374
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * q))];
|
|
1375
|
+
}
|
|
1376
|
+
function operatingSamplesOf(spans, tools) {
|
|
1377
|
+
const calls = spans.filter((span) => !isInherited(span)).map((span) => ({ span, context: modelCallContextTokens(span) })).filter((call) => call.context !== null).map((call) => ({ ...call, start: Date.parse(call.span.start_time), end: Date.parse(call.span.end_time) })).sort((a, b) => a.start - b.start);
|
|
1378
|
+
const results = tools.filter((span) => !isInherited(span) && span.status.code !== "UNSET").map((span) => Date.parse(span.end_time)).filter((at) => Number.isFinite(at)).sort((a, b) => a - b);
|
|
1379
|
+
const latencyMs = Array.from({ length: CONTEXT_BANDS }, () => []);
|
|
1380
|
+
let next = 0;
|
|
1381
|
+
let previousEnd = Number.NEGATIVE_INFINITY;
|
|
1382
|
+
for (const call of calls) {
|
|
1383
|
+
if (!Number.isFinite(call.start)) continue;
|
|
1384
|
+
let result;
|
|
1385
|
+
while (next < results.length && results[next] <= call.start) {
|
|
1386
|
+
if (results[next] > previousEnd) result = results[next];
|
|
1387
|
+
next++;
|
|
1388
|
+
}
|
|
1389
|
+
const elapsed = result === void 0 ? NaN : call.start - result;
|
|
1390
|
+
if (elapsed > 0 && elapsed < LATENCY_MAX_MS) latencyMs[Math.min(CONTEXT_BANDS - 1, Math.floor(call.context / CONTEXT_BAND_TOKENS))].push(elapsed);
|
|
1391
|
+
previousEnd = Number.isFinite(call.end) ? Math.max(call.end, call.start) : call.start;
|
|
1392
|
+
}
|
|
1393
|
+
return { contextTokens: calls.map((call) => call.context), latencyMs };
|
|
1394
|
+
}
|
|
1395
|
+
function modelCallMeasurement(samples) {
|
|
1396
|
+
const contexts = [...samples.contextTokens].sort((a, b) => a - b);
|
|
1397
|
+
return {
|
|
1398
|
+
calls: contexts.length,
|
|
1399
|
+
contextTokens: contexts.length === 0 ? { mean: null } : {
|
|
1400
|
+
mean: Math.round(contexts.reduce((sum2, value) => sum2 + value, 0) / contexts.length),
|
|
1401
|
+
// One call is a value, not a distribution.
|
|
1402
|
+
...contexts.length >= 2 ? { p50: rankPercentile(contexts, 0.5) } : {}
|
|
1403
|
+
},
|
|
1404
|
+
latencyByContext: samples.latencyMs.flatMap((values, index) => {
|
|
1405
|
+
if (values.length < 2) return [];
|
|
1406
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
1407
|
+
return [{ band: contextBandLabel(index), calls: sorted.length, p50Ms: rankPercentile(sorted, 0.5), p90Ms: rankPercentile(sorted, 0.9) }];
|
|
1408
|
+
})
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
var HOOK_ERROR = /hook error: (?:\[([^\]]+)\]|([A-Za-z0-9_.-]+)(?=:| \())?/u;
|
|
1412
|
+
function refusingGuard(span) {
|
|
1413
|
+
if (span.status.code !== "ERROR") return void 0;
|
|
1414
|
+
const match = HOOK_ERROR.exec(span.status.message ?? "");
|
|
1415
|
+
if (!match) return void 0;
|
|
1416
|
+
return match[1] ? basename2(match[1]) : match[2] ?? "(unnamed)";
|
|
1417
|
+
}
|
|
1418
|
+
function guardRefusalsOf(tools) {
|
|
1419
|
+
const byGuard = {};
|
|
1420
|
+
const spanIds = [];
|
|
1421
|
+
for (const span of tools) {
|
|
1422
|
+
const guard = refusingGuard(span);
|
|
1423
|
+
if (guard === void 0) continue;
|
|
1424
|
+
byGuard[guard] = (byGuard[guard] ?? 0) + 1;
|
|
1425
|
+
spanIds.push(span.span_id);
|
|
1426
|
+
}
|
|
1427
|
+
return { refusals: spanIds.length, byGuard, spanIds };
|
|
1428
|
+
}
|
|
1429
|
+
var LIMIT_TEXT = /^(?:you['’]ve hit your ([\w-]+) limit|claude ai (usage) limit reached|([\w-]+) limit reached\b|you['’]ve reached your ([\w-]+) limit)/iu;
|
|
1430
|
+
var LIMIT_NOTICE_MAX_CHARS = 300;
|
|
1431
|
+
var REPLY_SPANS = /* @__PURE__ */ new Set(["message.assistant", "llm.turn"]);
|
|
1432
|
+
function limitStopOf(spans) {
|
|
1433
|
+
const root = spans.find((span) => span.parent_span_id === null && span.status.code === "ERROR");
|
|
1434
|
+
const notice = root?.status.message?.trim() ?? "";
|
|
1435
|
+
const rootMatch = notice.length <= LIMIT_NOTICE_MAX_CHARS ? LIMIT_TEXT.exec(notice) : null;
|
|
1436
|
+
if (root && rootMatch) {
|
|
1437
|
+
return { kind: (rootMatch[1] ?? rootMatch[2] ?? rootMatch[3] ?? rootMatch[4] ?? "usage").toLowerCase(), text: notice, at: root.end_time, spanId: root.span_id };
|
|
1438
|
+
}
|
|
1439
|
+
let last;
|
|
1440
|
+
for (const span of spans) {
|
|
1441
|
+
if (isInherited(span) || !REPLY_SPANS.has(span.name) || typeof span.attributes.content !== "string") continue;
|
|
1442
|
+
if (!last || Date.parse(span.end_time) >= Date.parse(last.end_time)) last = span;
|
|
1443
|
+
}
|
|
1444
|
+
const text = typeof last?.attributes.content === "string" ? last.attributes.content.trim() : "";
|
|
1445
|
+
const match = last && text.length <= LIMIT_NOTICE_MAX_CHARS ? LIMIT_TEXT.exec(text) : null;
|
|
1446
|
+
if (!last || !match) return null;
|
|
1447
|
+
return { kind: (match[1] ?? match[2] ?? match[3] ?? match[4] ?? "usage").toLowerCase(), text, at: last.end_time, spanId: last.span_id };
|
|
1448
|
+
}
|
|
1449
|
+
var number = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
1450
|
+
function measure(spans, read, reason) {
|
|
1451
|
+
let total = 0;
|
|
1452
|
+
let measured = 0;
|
|
1453
|
+
for (const span of spans) {
|
|
1454
|
+
const value = read(span);
|
|
1455
|
+
if (value !== null) {
|
|
1456
|
+
total += value;
|
|
1457
|
+
measured += 1;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
total: measured > 0 || spans.length === 0 ? total : null,
|
|
1462
|
+
measured,
|
|
1463
|
+
eligible: spans.length,
|
|
1464
|
+
unavailable: measured === spans.length ? null : reason
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1467
|
+
function bytes(span, side) {
|
|
1468
|
+
const recorded = number(span.attributes[`traces.${side}.bytes`]);
|
|
1469
|
+
if (recorded !== null) return recorded;
|
|
1470
|
+
const text = span.attributes[`${side}.value`];
|
|
1471
|
+
if (typeof text !== "string" || span.attributes[`traces.${side}.truncated`] === true) return null;
|
|
1472
|
+
return Buffer.byteLength(text);
|
|
1473
|
+
}
|
|
1474
|
+
function toolMeasurements(tools) {
|
|
1475
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1476
|
+
for (const tool of tools) {
|
|
1477
|
+
const name = typeof tool.attributes["tool.name"] === "string" ? tool.attributes["tool.name"] : tool.name;
|
|
1478
|
+
const group = groups.get(name);
|
|
1479
|
+
if (group) group.push(tool);
|
|
1480
|
+
else groups.set(name, [tool]);
|
|
1481
|
+
}
|
|
1482
|
+
return [...groups].sort(([a], [b]) => a.localeCompare(b)).map(([name, spans]) => ({
|
|
1483
|
+
name,
|
|
1484
|
+
calls: spans.length,
|
|
1485
|
+
outcomes: {
|
|
1486
|
+
ok: spans.filter((span) => span.status.code === "OK").length,
|
|
1487
|
+
error: spans.filter((span) => span.status.code === "ERROR").length,
|
|
1488
|
+
unset: spans.filter((span) => span.status.code === "UNSET").length
|
|
1489
|
+
},
|
|
1490
|
+
inputBytes: measure(spans, (span) => bytes(span, "input"), "Input byte count missing or truncated without a size receipt."),
|
|
1491
|
+
outputBytes: measure(spans, (span) => bytes(span, "output"), "Output byte count missing or truncated without a size receipt."),
|
|
1492
|
+
callToResultMs: measure(spans, (span) => {
|
|
1493
|
+
if (span.status.code === "UNSET") return null;
|
|
1494
|
+
const elapsed = Date.parse(span.end_time) - Date.parse(span.start_time);
|
|
1495
|
+
return Number.isFinite(elapsed) && elapsed > 0 ? elapsed : null;
|
|
1496
|
+
}, "A completed span with distinct recorded endpoints is required; equal endpoints may be an adapter default."),
|
|
1497
|
+
spanIds: spans.map((span) => span.span_id)
|
|
1498
|
+
}));
|
|
1499
|
+
}
|
|
1500
|
+
function compactions(spans) {
|
|
1501
|
+
const events = spans.filter((span) => span.name === "session.compacted" && span.attributes[INHERITED_SOURCE_ATTR] !== "pre-task-prefix").sort((a, b) => Date.parse(a.start_time) - Date.parse(b.start_time));
|
|
1502
|
+
if (events.length === 0) return {
|
|
1503
|
+
value: null,
|
|
1504
|
+
spanIds: [],
|
|
1505
|
+
unavailable: "No compaction boundary was captured. This does not establish that no compaction occurred."
|
|
1506
|
+
};
|
|
1507
|
+
return {
|
|
1508
|
+
value: events.map((span, i) => {
|
|
1509
|
+
const previous = i > 0 ? events[i - 1] : void 0;
|
|
1510
|
+
const interval = previous ? Date.parse(span.start_time) - Date.parse(previous.start_time) : NaN;
|
|
1511
|
+
return {
|
|
1512
|
+
spanId: span.span_id,
|
|
1513
|
+
at: span.start_time,
|
|
1514
|
+
sincePreviousMs: Number.isFinite(interval) && interval >= 0 ? interval : null,
|
|
1515
|
+
latencyMs: number(span.attributes["traces.compaction.latency_ms"]),
|
|
1516
|
+
tokensBefore: number(span.attributes["traces.compaction.tokens_before"]),
|
|
1517
|
+
tokensAfter: number(span.attributes["traces.compaction.tokens_after"])
|
|
1518
|
+
};
|
|
1519
|
+
}),
|
|
1520
|
+
spanIds: events.map((span) => span.span_id),
|
|
1521
|
+
unavailable: null,
|
|
1522
|
+
partial: "Observed boundaries only. Latency and context sizes are null unless the source explicitly records them."
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
function contextFootprints(spans) {
|
|
1526
|
+
const out = [];
|
|
1527
|
+
for (const span of spans) {
|
|
1528
|
+
const fields = ["gen_ai.tool.definitions", "gen_ai.system_instructions"];
|
|
1529
|
+
if (span.name === "message.system" || span.name === "message.developer" || span.name === "user.prompt" && span.attributes["tangle.actor"] === "injected") fields.push("content");
|
|
1530
|
+
for (const field of fields) {
|
|
1531
|
+
const value = span.attributes[field];
|
|
1532
|
+
if (value === void 0 || value === null) continue;
|
|
1533
|
+
const text = typeof value === "string" ? value : canonicalJson(value);
|
|
1534
|
+
let entries = Array.isArray(value) ? value.length : null;
|
|
1535
|
+
if (typeof value === "string") {
|
|
1536
|
+
try {
|
|
1537
|
+
const parsed = JSON.parse(value);
|
|
1538
|
+
if (Array.isArray(parsed)) entries = parsed.length;
|
|
1539
|
+
} catch {
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
out.push({ spanId: span.span_id, field, bytes: Buffer.byteLength(text), lines: text.split("\n").length, entries, scope: "captured-attribute" });
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return out;
|
|
1546
|
+
}
|
|
1547
|
+
var MINUTE_MS = 6e4;
|
|
1548
|
+
var STALL_MS = STALL_MINUTES * MINUTE_MS;
|
|
1549
|
+
var STALL_LIST_CAP = 200;
|
|
1550
|
+
var minutes = (ms) => Math.round(ms / 6e3) / 10;
|
|
1551
|
+
function timeOf(value) {
|
|
1552
|
+
const ms = Date.parse(value);
|
|
1553
|
+
return Number.isFinite(ms) && ms > 0 ? ms : null;
|
|
1554
|
+
}
|
|
1555
|
+
var isInherited = (span) => span.attributes[INHERITED_SPAN_ATTR] === true;
|
|
1556
|
+
var promptText = (span) => span.name === "user.prompt" && typeof span.attributes.content === "string" ? span.attributes.content.trim() : "";
|
|
1557
|
+
function endsWait(span) {
|
|
1558
|
+
if (span.name !== "user.prompt") return false;
|
|
1559
|
+
const actor = span.attributes[ACTOR_ATTR];
|
|
1560
|
+
const content = promptText(span);
|
|
1561
|
+
return actor === "human" || actor === "agent" || INTERRUPT_TEXT.test(content) || isFleetTurn(content);
|
|
1562
|
+
}
|
|
1563
|
+
var BACKGROUND_RESULT = /^<task-notification>/u;
|
|
1564
|
+
var toolNameOf = (span) => typeof span.attributes["tool.name"] === "string" ? span.attributes["tool.name"] : span.name.replace(/^tool\./u, "");
|
|
1565
|
+
function isRollback(line) {
|
|
1566
|
+
const words = line.split(" ");
|
|
1567
|
+
if (basename2(words[0] ?? "") !== "git") return false;
|
|
1568
|
+
let at = 1;
|
|
1569
|
+
while (at < words.length && words[at].startsWith("-")) at += words[at] === "-C" || words[at] === "-c" ? 2 : 1;
|
|
1570
|
+
return words[at] === "revert" || words[at] === "reset" && words.slice(at + 1).includes("--hard");
|
|
1571
|
+
}
|
|
1572
|
+
function timeline(records) {
|
|
1573
|
+
const points = [];
|
|
1574
|
+
for (const span of records) {
|
|
1575
|
+
const start = timeOf(span.start_time);
|
|
1576
|
+
const end = timeOf(span.end_time);
|
|
1577
|
+
if (start !== null) points.push({ at: start, span, end: false });
|
|
1578
|
+
if (end !== null && (start === null || end > start)) points.push({ at: end, span, end: true });
|
|
1579
|
+
}
|
|
1580
|
+
return points.sort((left, right) => left.at - right.at);
|
|
1581
|
+
}
|
|
1582
|
+
function unionMs(intervals) {
|
|
1583
|
+
let total = 0;
|
|
1584
|
+
let reach = Number.NEGATIVE_INFINITY;
|
|
1585
|
+
for (const [start, end] of [...intervals].sort((a, b) => a[0] - b[0])) {
|
|
1586
|
+
if (end <= reach) continue;
|
|
1587
|
+
total += end - Math.max(start, reach);
|
|
1588
|
+
reach = end;
|
|
1589
|
+
}
|
|
1590
|
+
return total;
|
|
1591
|
+
}
|
|
1592
|
+
function measureTime(spans, records, tools) {
|
|
1593
|
+
const points = timeline(records.filter((span) => !isInherited(span)));
|
|
1594
|
+
const stalls = [];
|
|
1595
|
+
const stallIntervals = [];
|
|
1596
|
+
let activeMs = 0;
|
|
1597
|
+
for (let i = 1; i < points.length; i++) {
|
|
1598
|
+
const before = points[i - 1].at;
|
|
1599
|
+
const gap = points[i].at - before;
|
|
1600
|
+
activeMs += Math.min(gap, STALL_MS);
|
|
1601
|
+
if (gap < STALL_MS) continue;
|
|
1602
|
+
const ending = [];
|
|
1603
|
+
for (let j = i; j < points.length && points[j].at === points[i].at; j++) ending.push(points[j]);
|
|
1604
|
+
if (ending.some((point) => !point.end && endsWait(point.span))) continue;
|
|
1605
|
+
const result = ending.find((point) => point.end && point.span.attributes["openinference.span.kind"] === "TOOL" || !point.end && BACKGROUND_RESULT.test(promptText(point.span)));
|
|
1606
|
+
stalls.push({
|
|
1607
|
+
at: new Date(before).toISOString(),
|
|
1608
|
+
minutes: minutes(gap),
|
|
1609
|
+
kind: result ? "waiting on a command" : "idle",
|
|
1610
|
+
spanId: (result ?? ending[0]).span.span_id
|
|
1611
|
+
});
|
|
1612
|
+
stallIntervals.push([before, points[i].at]);
|
|
1613
|
+
}
|
|
1614
|
+
const noTime = "no record of this session's own carries a readable time";
|
|
1615
|
+
const stallFact = points.length === 0 ? { value: null, spanIds: [], unavailable: noTime } : {
|
|
1616
|
+
value: stalls.slice(0, STALL_LIST_CAP),
|
|
1617
|
+
spanIds: stalls.map((stall) => stall.spanId),
|
|
1618
|
+
unavailable: null,
|
|
1619
|
+
...stalls.length > STALL_LIST_CAP ? { partial: `${stalls.length - STALL_LIST_CAP} of ${stalls.length} stalls omitted; the spanIds are complete` } : {}
|
|
1620
|
+
};
|
|
1621
|
+
const own = tools.filter((span) => !isInherited(span));
|
|
1622
|
+
const runs = [];
|
|
1623
|
+
for (const span of own) {
|
|
1624
|
+
const input = span.attributes["input.value"];
|
|
1625
|
+
const script = typeof input === "string" ? commandTextFromInput(input) : void 0;
|
|
1626
|
+
if (script !== void 0) runs.push({ span, script, command: script.replace(/\s+/gu, " ").trim(), statusKnown: !isCodeModeScript(input) });
|
|
1627
|
+
}
|
|
1628
|
+
const statusUnknown = runs.filter((run) => !run.statusKnown).length;
|
|
1629
|
+
const unread = own.length > 0 && !own.some((span) => typeof span.attributes["input.value"] === "string") ? "no tool call recorded its arguments, so which commands ran cannot be read" : null;
|
|
1630
|
+
const byCommand = /* @__PURE__ */ new Map();
|
|
1631
|
+
for (const run of runs) {
|
|
1632
|
+
const group = byCommand.get(run.command);
|
|
1633
|
+
if (group) group.push(run);
|
|
1634
|
+
else byCommand.set(run.command, [run]);
|
|
1635
|
+
}
|
|
1636
|
+
const repeated = [...byCommand.values()].filter((group) => group.length > 1);
|
|
1637
|
+
const rollbacks = runs.flatMap((run) => commandLines(run.script).filter(isRollback).map(() => run.span.span_id));
|
|
1638
|
+
const streaks = [];
|
|
1639
|
+
const streakIntervals = [];
|
|
1640
|
+
const open2 = /* @__PURE__ */ new Map();
|
|
1641
|
+
const close = (command) => {
|
|
1642
|
+
const failed = open2.get(command);
|
|
1643
|
+
open2.delete(command);
|
|
1644
|
+
if (!failed || failed.length < STREAK_RUNS) return;
|
|
1645
|
+
const start = Math.min(...failed.map((run) => timeOf(run.span.start_time) ?? Number.POSITIVE_INFINITY));
|
|
1646
|
+
const end = Math.max(...failed.map((run) => timeOf(run.span.end_time) ?? timeOf(run.span.start_time) ?? Number.NEGATIVE_INFINITY));
|
|
1647
|
+
const known = Number.isFinite(start) && Number.isFinite(end) && end >= start;
|
|
1648
|
+
streaks.push({
|
|
1649
|
+
command: failed[0].command.length > COMMAND_TEXT_CAP ? `${failed[0].command.slice(0, COMMAND_TEXT_CAP - 1)}\u2026` : failed[0].command,
|
|
1650
|
+
tool: toolNameOf(failed[0].span),
|
|
1651
|
+
runs: failed.length,
|
|
1652
|
+
minutes: known ? minutes(end - start) : 0,
|
|
1653
|
+
at: failed[0].span.start_time,
|
|
1654
|
+
spanIds: failed.map((run) => run.span.span_id)
|
|
1655
|
+
});
|
|
1656
|
+
if (known) streakIntervals.push([start, end]);
|
|
1657
|
+
};
|
|
1658
|
+
const runOf = new Map(runs.map((run) => [run.span, run]));
|
|
1659
|
+
for (const span of spans) {
|
|
1660
|
+
if (endsWait(span)) {
|
|
1661
|
+
for (const command of [...open2.keys()]) close(command);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const run = runOf.get(span);
|
|
1665
|
+
if (!run || !run.statusKnown) continue;
|
|
1666
|
+
if (span.status.code === "OK") close(run.command);
|
|
1667
|
+
else if (span.status.code === "ERROR") {
|
|
1668
|
+
const failed = open2.get(run.command);
|
|
1669
|
+
if (failed) failed.push(run);
|
|
1670
|
+
else open2.set(run.command, [run]);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
for (const command of [...open2.keys()]) close(command);
|
|
1674
|
+
streaks.sort((left, right) => (timeOf(left.at) ?? 0) - (timeOf(right.at) ?? 0));
|
|
1675
|
+
const commandFact = (value, spanIds) => unread === null ? { value, spanIds, unavailable: null } : { value: null, spanIds: [], unavailable: unread };
|
|
1676
|
+
const unavailable = [unread, points.length === 0 ? noTime : null].filter((reason) => reason !== null).join("; ");
|
|
1677
|
+
return {
|
|
1678
|
+
waste: {
|
|
1679
|
+
minutes: unavailable === "" ? minutes(unionMs([...streakIntervals, ...stallIntervals])) : null,
|
|
1680
|
+
unavailable: unavailable === "" ? null : unavailable,
|
|
1681
|
+
failingStreaks: unread === null && statusUnknown > 0 ? {
|
|
1682
|
+
...commandFact(streaks, streaks.flatMap((streak) => streak.spanIds)),
|
|
1683
|
+
partial: `${statusUnknown} of ${runs.length} command runs were inside Codex code-mode scripts, which record the script's status, not the command's exit code, so no streak among them can be read`
|
|
1684
|
+
} : commandFact(streaks, streaks.flatMap((streak) => streak.spanIds)),
|
|
1685
|
+
stalls: stallFact,
|
|
1686
|
+
rollbacks: commandFact(rollbacks.length, rollbacks),
|
|
1687
|
+
repeatedCommands: commandFact(
|
|
1688
|
+
{ commands: repeated.length, extraRuns: repeated.reduce((sum2, group) => sum2 + group.length - 1, 0) },
|
|
1689
|
+
repeated.flatMap((group) => group.slice(1).map((run) => run.span.span_id))
|
|
1690
|
+
)
|
|
1691
|
+
},
|
|
1692
|
+
activeMinutes: points.length === 0 ? { value: null, spanIds: [], unavailable: noTime } : { value: minutes(activeMs), spanIds: [], unavailable: null }
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
function computeSessionMeasurements(input) {
|
|
1696
|
+
const { spans, records, tools, sessionId, harness } = input;
|
|
1697
|
+
return {
|
|
1698
|
+
skills: skillUseOfSpans(spans.filter((span) => !isInherited(span)), harness),
|
|
1699
|
+
...measureTime(spans, records, tools),
|
|
1700
|
+
relationship: describeSessionRelationship({
|
|
1701
|
+
sessionId: sessionId ?? spans[0]?.trace_id ?? "",
|
|
1702
|
+
harness: harness ?? "",
|
|
1703
|
+
path: "",
|
|
1704
|
+
cwd: null,
|
|
1705
|
+
mtimeMs: 0
|
|
1706
|
+
}, spans),
|
|
1707
|
+
tools: toolMeasurements(tools),
|
|
1708
|
+
compactions: compactions(spans),
|
|
1709
|
+
contextFootprints: contextFootprints(spans),
|
|
1710
|
+
modelCalls: modelCallMeasurement(input.operating ?? operatingSamplesOf(spans, tools)),
|
|
1711
|
+
guardRefusals: guardRefusalsOf(tools),
|
|
1712
|
+
limitStop: limitStopOf(spans)
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/session-facts.ts
|
|
1717
|
+
import { OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND3, TOOL_NAME } from "@tangle-network/agent-eval/trace-attributes";
|
|
1718
|
+
import { ingestSpans, rankFirstFailure } from "@tangle-network/agent-eval/diagnosis";
|
|
1719
|
+
|
|
1720
|
+
// src/session-turns.ts
|
|
1721
|
+
import { OPENINFERENCE_SPAN_KIND as OPENINFERENCE_SPAN_KIND2 } from "@tangle-network/agent-eval/trace-attributes";
|
|
1722
|
+
function stringAttr(span, key) {
|
|
1723
|
+
const value = span.attributes[key];
|
|
1724
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1725
|
+
}
|
|
1726
|
+
var FACT_TEXT_CAP = 2e3;
|
|
1727
|
+
var ANSWERED_TEXT_CAP = 1500;
|
|
1728
|
+
var TURN_EXCLUSION_REASONS = {
|
|
1729
|
+
injected: "harness-injected content rather than typed text: an instruction file, an environment-context block, a system reminder, a task or subagent notification, an interrupt or turn-aborted marker, or a skill or slash-command expansion",
|
|
1730
|
+
agent: "a parent agent sent this prompt to this session; no person typed it",
|
|
1731
|
+
"subagent-spawn": "the brief that spawned this run, written by the agent that spawned it",
|
|
1732
|
+
"tool-result": "a tool result the harness surfaced as a user turn",
|
|
1733
|
+
fleet: "another agent or session sent this turn in: a fleet message, a session-to-session message, an operator handoff, or a lane brief (a first turn over 1,500 characters); no person typed it here",
|
|
1734
|
+
relay: "a person pasted another agent's output: a long, markdown-heavy turn after the session's first",
|
|
1735
|
+
unclassified: "the span carries no actor, so whether a person typed it cannot be read from the spans"
|
|
1736
|
+
};
|
|
1737
|
+
var INHERITED_TURN_REASON = "context this session carries but did not receive: history copied from the parent when the session forked, or the turns a compaction retained. The person typed it into the parent thread, not into this session";
|
|
1738
|
+
var DUPLICATE_TURN_REASON = "a second record of the turn before it: the same text at the same instant, with no model call, tool call or assistant message between them";
|
|
1739
|
+
var SUBAGENT_TURN_REASON = "a turn inside a subagent this session spawned, not a turn of this session: the person addressed the session, and the brief and notifications the subagent received are the parent agent talking to it";
|
|
1740
|
+
var HARNESS_COMMAND_REASON = "a command the person gave the harness (/model, /clear, \u2026), not a message to the agent";
|
|
1741
|
+
var CLAIM_WORD = /\b(?:done|fixed|complete|completed|finished|implemented|merged|shipped|landed|resolved|verified|all green|tests pass(?:ed)?|ready|works now)\b/i;
|
|
1742
|
+
var CLAIM_DENIED = /\b(?:not|no|never|yet|still|cannot|can['’]?t|won['’]?t|isn['’]?t|aren['’]?t|wasn['’]?t|didn['’]?t|doesn['’]?t|hasn['’]?t|haven['’]?t|fail(?:s|ed|ing)?|broken|blocked)\b/i;
|
|
1743
|
+
function openingSentence(text) {
|
|
1744
|
+
const plain = text.trimStart().replace(/[*_`#>]/g, "").replace(/^\s*(?:[-•→]|\d+[.)])\s+/u, "");
|
|
1745
|
+
const end = /[.!?:;](?=\s|$)|\n/.exec(plain);
|
|
1746
|
+
return (end ? plain.slice(0, end.index + end[0].length) : plain).slice(0, 200).trim();
|
|
1747
|
+
}
|
|
1748
|
+
function isCompletionClaim(text) {
|
|
1749
|
+
const opening = openingSentence(text);
|
|
1750
|
+
return CLAIM_WORD.test(opening) && !CLAIM_DENIED.test(opening) && !opening.endsWith("?");
|
|
1751
|
+
}
|
|
1752
|
+
var REJECTED_TOOL_TEXT = /The user doesn't want to proceed with this tool use|tool use was rejected|rejected by (?:the )?user|user (?:rejected|declined|denied) (?:this|the)/iu;
|
|
1753
|
+
function isAgentActivity(span) {
|
|
1754
|
+
if (span.name === "user.prompt") return false;
|
|
1755
|
+
const kind = span.attributes[OPENINFERENCE_SPAN_KIND2];
|
|
1756
|
+
return kind === "TOOL" || kind === "LLM" || span.name === "message.assistant" || span.name.startsWith("message.agent.");
|
|
1757
|
+
}
|
|
1758
|
+
function rawContent(span) {
|
|
1759
|
+
return typeof span.attributes.content === "string" ? span.attributes.content : "";
|
|
1760
|
+
}
|
|
1761
|
+
function turnText(span) {
|
|
1762
|
+
return capText(rawContent(span), FACT_TEXT_CAP);
|
|
1763
|
+
}
|
|
1764
|
+
function normalizedTurnText(span) {
|
|
1765
|
+
return turnText(span).replace(/\s+/g, " ").trim();
|
|
1766
|
+
}
|
|
1767
|
+
var CAPTURE_CUT_MARKER = /… \[\+(\d+) chars\]$/;
|
|
1768
|
+
function clipAnswered(raw) {
|
|
1769
|
+
const text = raw.trim();
|
|
1770
|
+
const marker = CAPTURE_CUT_MARKER.exec(text);
|
|
1771
|
+
const body = marker ? text.slice(0, marker.index) : text;
|
|
1772
|
+
if (body.length <= ANSWERED_TEXT_CAP) return text;
|
|
1773
|
+
const lastKept = body.charCodeAt(ANSWERED_TEXT_CAP - 1);
|
|
1774
|
+
const keep = lastKept >= 55296 && lastKept <= 56319 ? ANSWERED_TEXT_CAP - 1 : ANSWERED_TEXT_CAP;
|
|
1775
|
+
const hidden = body.length - keep + (marker ? Number(marker[1]) : 0);
|
|
1776
|
+
return `${body.slice(0, keep)}\u2026 [+${hidden} chars]`;
|
|
1777
|
+
}
|
|
1778
|
+
function isOwnAgentMessage(span) {
|
|
1779
|
+
return span.name === "message.assistant" && span.attributes[INHERITED_SPAN_ATTR] !== true && rawContent(span).trim().length > 0;
|
|
1780
|
+
}
|
|
1781
|
+
function isRejectedTool(span) {
|
|
1782
|
+
if (span.attributes[OPENINFERENCE_SPAN_KIND2] !== "TOOL" || span.status.code !== "ERROR" || span.attributes[INHERITED_SPAN_ATTR] === true) return false;
|
|
1783
|
+
const output = span.attributes["output.value"];
|
|
1784
|
+
return REJECTED_TOOL_TEXT.test(`${span.status.message ?? ""}
|
|
1785
|
+
${typeof output === "string" ? output.slice(0, 2e3) : ""}`);
|
|
1786
|
+
}
|
|
1787
|
+
var TURN_FOLD_START = { typed: false, kept: null, activity: false };
|
|
1788
|
+
function typedTurnsOf(spans, start = TURN_FOLD_START) {
|
|
1789
|
+
const excluded = /* @__PURE__ */ new Map();
|
|
1790
|
+
const drop = (reason, author, span) => {
|
|
1791
|
+
const key = `${reason}\0${author}`;
|
|
1792
|
+
excluded.set(key, [...excluded.get(key) ?? [], { spanId: span.span_id, at: span.start_time }]);
|
|
1793
|
+
};
|
|
1794
|
+
const turns = [];
|
|
1795
|
+
let kept = start.kept;
|
|
1796
|
+
let activity = start.activity;
|
|
1797
|
+
let lastAgentMessage = null;
|
|
1798
|
+
let lastTurn = null;
|
|
1799
|
+
let awaitingReply = [];
|
|
1800
|
+
let pending = /* @__PURE__ */ new Set();
|
|
1801
|
+
let agentWroteSinceHuman = false;
|
|
1802
|
+
for (const span of spans) {
|
|
1803
|
+
if (isSubagentSpan(span.attributes)) {
|
|
1804
|
+
if (span.name === "user.prompt") drop(SUBAGENT_TURN_REASON, stringAttr(span, ACTOR_ATTR) ?? "unclassified", span);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (span.name !== "user.prompt") {
|
|
1808
|
+
if (isOwnAgentMessage(span)) {
|
|
1809
|
+
lastAgentMessage = span;
|
|
1810
|
+
agentWroteSinceHuman = true;
|
|
1811
|
+
for (const turn2 of awaitingReply) {
|
|
1812
|
+
turn2.reply = clipAnswered(rawContent(span));
|
|
1813
|
+
turn2.replySpanId = span.span_id;
|
|
1814
|
+
}
|
|
1815
|
+
awaitingReply = [];
|
|
1816
|
+
}
|
|
1817
|
+
if (isRejectedTool(span)) pending.add("rejected-tool");
|
|
1818
|
+
if (isAgentActivity(span)) activity = true;
|
|
1819
|
+
continue;
|
|
1820
|
+
}
|
|
1821
|
+
const actor = stringAttr(span, ACTOR_ATTR) ?? "unclassified";
|
|
1822
|
+
if (span.attributes[INHERITED_SPAN_ATTR] === true) {
|
|
1823
|
+
const source = stringAttr(span, INHERITED_SOURCE_ATTR);
|
|
1824
|
+
drop(source === null ? INHERITED_TURN_REASON : `${INHERITED_TURN_REASON} (${source})`, actor, span);
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
const text = rawContent(span);
|
|
1828
|
+
if (INTERRUPT_TEXT.test(text.trim())) {
|
|
1829
|
+
pending.add("interrupt");
|
|
1830
|
+
if (lastTurn) lastTurn.interrupted = true;
|
|
1831
|
+
}
|
|
1832
|
+
if (actor !== "human") {
|
|
1833
|
+
drop(TURN_EXCLUSION_REASONS[actor] ?? `the adapter classified this turn as "${actor}", not as a person typing`, actor, span);
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
const author = authorOfTypedTurn(text, { firstTurn: !start.typed && turns.length === 0 });
|
|
1837
|
+
if (author === "injected") {
|
|
1838
|
+
drop(TURN_EXCLUSION_REASONS.injected, "injected", span);
|
|
1839
|
+
continue;
|
|
1840
|
+
}
|
|
1841
|
+
if (author === "human" && isHarnessCommand(text)) {
|
|
1842
|
+
drop(HARNESS_COMMAND_REASON, "human", span);
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
const normalized = normalizedTurnText(span);
|
|
1846
|
+
const duplicate = author === "human" && kept !== null && kept.at === span.start_time && kept.text === normalized && normalized.length > 0 && !activity;
|
|
1847
|
+
if (duplicate) {
|
|
1848
|
+
drop(DUPLICATE_TURN_REASON, "human", span);
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const answered = lastAgentMessage ? rawContent(lastAgentMessage) : null;
|
|
1852
|
+
if (agentWroteSinceHuman && answered !== null && isCompletionClaim(answered)) pending.add("after-claim");
|
|
1853
|
+
const turn = {
|
|
1854
|
+
actor: "human",
|
|
1855
|
+
author,
|
|
1856
|
+
at: span.start_time,
|
|
1857
|
+
text: turnText(span),
|
|
1858
|
+
spanId: span.span_id,
|
|
1859
|
+
answered: answered === null ? null : clipAnswered(answered),
|
|
1860
|
+
answeredSpanId: lastAgentMessage?.span_id ?? null,
|
|
1861
|
+
reply: null,
|
|
1862
|
+
replySpanId: null,
|
|
1863
|
+
interrupted: false,
|
|
1864
|
+
// A fleet message is another agent talking: a signal is a person's.
|
|
1865
|
+
signals: author === "fleet" ? [] : SIGNAL_ORDER.filter((signal) => pending.has(signal))
|
|
1866
|
+
};
|
|
1867
|
+
turns.push(turn);
|
|
1868
|
+
lastTurn = turn;
|
|
1869
|
+
awaitingReply.push(turn);
|
|
1870
|
+
if (author === "human") {
|
|
1871
|
+
kept = { at: span.start_time, text: normalized };
|
|
1872
|
+
activity = false;
|
|
1873
|
+
pending = /* @__PURE__ */ new Set();
|
|
1874
|
+
agentWroteSinceHuman = false;
|
|
1875
|
+
} else {
|
|
1876
|
+
drop(TURN_EXCLUSION_REASONS[author], author, span);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return { turns, excluded, state: { typed: start.typed || turns.length > 0, kept, activity } };
|
|
1880
|
+
}
|
|
1881
|
+
var SIGNAL_ORDER = ["interrupt", "rejected-tool", "after-claim"];
|
|
1882
|
+
|
|
1883
|
+
// src/pull-request-facts.ts
|
|
1884
|
+
var GH_NAMES = /* @__PURE__ */ new Set(["gh", "gh-drew"]);
|
|
1885
|
+
var CREATE_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1886
|
+
"-H",
|
|
1887
|
+
"--head",
|
|
1888
|
+
"-B",
|
|
1889
|
+
"--base",
|
|
1890
|
+
"-R",
|
|
1891
|
+
"--repo",
|
|
1892
|
+
"-t",
|
|
1893
|
+
"--title",
|
|
1894
|
+
"-b",
|
|
1895
|
+
"--body",
|
|
1896
|
+
"-F",
|
|
1897
|
+
"--body-file",
|
|
1898
|
+
"-a",
|
|
1899
|
+
"--assignee",
|
|
1900
|
+
"-l",
|
|
1901
|
+
"--label",
|
|
1902
|
+
"-m",
|
|
1903
|
+
"--milestone",
|
|
1904
|
+
"-p",
|
|
1905
|
+
"--project",
|
|
1906
|
+
"-r",
|
|
1907
|
+
"--reviewer",
|
|
1908
|
+
"-T",
|
|
1909
|
+
"--template"
|
|
1910
|
+
]);
|
|
1911
|
+
var MERGE_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
1912
|
+
"-t",
|
|
1913
|
+
"--subject",
|
|
1914
|
+
"-b",
|
|
1915
|
+
"--body",
|
|
1916
|
+
"-F",
|
|
1917
|
+
"--body-file",
|
|
1918
|
+
"--match-head-commit",
|
|
1919
|
+
"-R",
|
|
1920
|
+
"--repo",
|
|
1921
|
+
"-A",
|
|
1922
|
+
"--author-email"
|
|
1923
|
+
]);
|
|
1924
|
+
var ANSI = /\u001B\[[0-9;?]*[A-Za-z]|\u001B\][^\u0007]*\u0007/g;
|
|
1925
|
+
var PR_URL_LINE = /^https?:\/\/[^/\s]+\/[^/\s]+\/[^/\s]+\/pull\/(\d+)\/?$/;
|
|
1926
|
+
var PR_NUMBER = /https?:\/\/[^/\s"']+\/[^/\s"']+\/[^/\s"']+\/pull\/(\d+)|(?:^|\s)#(\d+)\b/g;
|
|
1927
|
+
var MERGED_NUMBER = /(?:(?:Squashed and merged|Rebased and merged|Merged) pull request|Pull request)\s+(?:[\w.\-]+\/[\w.\-]+)?#(\d+)/;
|
|
1928
|
+
var CREATE_REACHED_GH = /createPullRequest|pull request create failed|must first push the current branch|a pull request for branch|already exists|Creating pull request for|No commits between|Head ref must be a branch|GraphQL:|HTTP 4\d\d/i;
|
|
1929
|
+
var MERGE_REACHED_GH = /mergePullRequest|pull request|Merged|not mergeable|GraphQL:|failed to run git|base branch policy|approving review is required|HTTP 4\d\d/i;
|
|
1930
|
+
var PULL_REQUEST_URL = /https?:\/\/[^/\s"']+\/[^/\s"']+\/[^/\s"']+\/pull\/\d+/;
|
|
1931
|
+
var PUSH_TRACKING = /branch '([^']+)' set up to track/;
|
|
1932
|
+
var PUSH_NEW_BRANCH = /\[new branch\]\s+\S+\s+->\s+(\S+)/;
|
|
1933
|
+
var SCRIPT_RESULT_START = '{"chunk_id"';
|
|
1934
|
+
function jsonObjectEnd(text, start) {
|
|
1935
|
+
let depth = 0;
|
|
1936
|
+
let inString = false;
|
|
1937
|
+
for (let index = start; index < text.length; index += 1) {
|
|
1938
|
+
const char = text[index];
|
|
1939
|
+
if (inString) {
|
|
1940
|
+
if (char === "\\") index += 1;
|
|
1941
|
+
else if (char === '"') inString = false;
|
|
1942
|
+
} else if (char === '"') inString = true;
|
|
1943
|
+
else if (char === "{") depth += 1;
|
|
1944
|
+
else if (char === "}" && --depth === 0) return index + 1;
|
|
1945
|
+
}
|
|
1946
|
+
return -1;
|
|
1947
|
+
}
|
|
1948
|
+
function scriptResults(output) {
|
|
1949
|
+
const results = [];
|
|
1950
|
+
for (let from = output.indexOf(SCRIPT_RESULT_START); from !== -1; from = output.indexOf(SCRIPT_RESULT_START, from + 1)) {
|
|
1951
|
+
const end = jsonObjectEnd(output, from);
|
|
1952
|
+
if (end === -1) break;
|
|
1953
|
+
let value;
|
|
1954
|
+
try {
|
|
1955
|
+
value = JSON.parse(output.slice(from, end));
|
|
1956
|
+
} catch {
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const session = value.session_id;
|
|
1960
|
+
results.push({
|
|
1961
|
+
sessionId: typeof session === "number" || typeof session === "string" ? String(session) : null,
|
|
1962
|
+
exitCode: typeof value.exit_code === "number" ? value.exit_code : null,
|
|
1963
|
+
output: typeof value.output === "string" ? value.output : ""
|
|
1964
|
+
});
|
|
1965
|
+
from = end - 1;
|
|
1966
|
+
}
|
|
1967
|
+
return results;
|
|
1968
|
+
}
|
|
1969
|
+
function stringAttribute2(span, key) {
|
|
1970
|
+
const value = span.attributes[key];
|
|
1971
|
+
return typeof value === "string" ? value : null;
|
|
1972
|
+
}
|
|
1973
|
+
function basename3(word) {
|
|
1974
|
+
const cut = word.lastIndexOf("/");
|
|
1975
|
+
return cut === -1 ? word : word.slice(cut + 1);
|
|
1976
|
+
}
|
|
1977
|
+
function commandRecords(spans) {
|
|
1978
|
+
const scripted = /* @__PURE__ */ new Map();
|
|
1979
|
+
const codeMode = /* @__PURE__ */ new Set();
|
|
1980
|
+
for (const span of spans) {
|
|
1981
|
+
const input = stringAttribute2(span, "input.value");
|
|
1982
|
+
if (input === null) continue;
|
|
1983
|
+
const script = commandTextFromInput(input);
|
|
1984
|
+
if (script === void 0) continue;
|
|
1985
|
+
scripted.set(span, script);
|
|
1986
|
+
if (isCodeModeScript(input)) codeMode.add(span);
|
|
1987
|
+
}
|
|
1988
|
+
const innerParents = /* @__PURE__ */ new Set();
|
|
1989
|
+
const innerScripts = /* @__PURE__ */ new Set();
|
|
1990
|
+
for (const [span, script] of scripted) {
|
|
1991
|
+
if (!isInnerToolCall(span.attributes)) continue;
|
|
1992
|
+
if (span.parent_span_id !== null) innerParents.add(span.parent_span_id);
|
|
1993
|
+
innerScripts.add(script);
|
|
1994
|
+
}
|
|
1995
|
+
const records = [];
|
|
1996
|
+
for (const [span, script] of scripted) {
|
|
1997
|
+
const outer = !isInnerToolCall(span.attributes);
|
|
1998
|
+
if (outer && (innerParents.has(span.span_id) || innerScripts.has(script))) continue;
|
|
1999
|
+
const exit = span.attributes["process.exit_code"];
|
|
2000
|
+
const exitCode = typeof exit === "number" ? exit : null;
|
|
2001
|
+
const output = stringAttribute2(span, "output.value") ?? "";
|
|
2002
|
+
const results = codeMode.has(span) ? scriptResults(output) : [];
|
|
2003
|
+
const header = RUNNING_SESSION.exec(output)?.[1];
|
|
2004
|
+
records.push({
|
|
2005
|
+
spanId: span.span_id,
|
|
2006
|
+
at: span.start_time,
|
|
2007
|
+
script,
|
|
2008
|
+
output,
|
|
2009
|
+
text: [output, ...results.map((result) => result.output)].join("\n"),
|
|
2010
|
+
running: [
|
|
2011
|
+
...header === void 0 ? [] : [header],
|
|
2012
|
+
...results.filter((result) => result.sessionId !== null && result.exitCode === null).map((result) => result.sessionId)
|
|
2013
|
+
],
|
|
2014
|
+
failed: codeMode.has(span) || (exitCode === null ? span.status.code === "ERROR" : exitCode !== 0),
|
|
2015
|
+
inputTruncated: span.attributes["traces.input.truncated"] === true,
|
|
2016
|
+
outputTruncated: span.attributes["traces.output.truncated"] === true
|
|
2017
|
+
});
|
|
2018
|
+
}
|
|
2019
|
+
return records;
|
|
2020
|
+
}
|
|
2021
|
+
function parseFlags(args, valueFlags) {
|
|
2022
|
+
const flags = /* @__PURE__ */ new Map();
|
|
2023
|
+
const positional = [];
|
|
2024
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2025
|
+
const arg = args[index];
|
|
2026
|
+
if (arg === "--") {
|
|
2027
|
+
positional.push(...args.slice(index + 1));
|
|
2028
|
+
break;
|
|
2029
|
+
}
|
|
2030
|
+
if (arg.startsWith("--") && arg.includes("=")) {
|
|
2031
|
+
const cut = arg.indexOf("=");
|
|
2032
|
+
flags.set(arg.slice(0, cut), arg.slice(cut + 1));
|
|
2033
|
+
} else if (valueFlags.has(arg)) {
|
|
2034
|
+
flags.set(arg, args[index + 1] ?? true);
|
|
2035
|
+
index += 1;
|
|
2036
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
2037
|
+
flags.set(arg, true);
|
|
2038
|
+
} else {
|
|
2039
|
+
positional.push(arg);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return { flags, positional };
|
|
2043
|
+
}
|
|
2044
|
+
function flagValue(flags, ...names) {
|
|
2045
|
+
for (const name of names) {
|
|
2046
|
+
const value = flags.get(name);
|
|
2047
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
2048
|
+
}
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
function printedPullRequestNumbers(output) {
|
|
2052
|
+
const numbers = [];
|
|
2053
|
+
for (const raw of output.split("\n")) {
|
|
2054
|
+
const line = raw.replace(ANSI, "").split("\r").pop().trim();
|
|
2055
|
+
const match = PR_URL_LINE.exec(line);
|
|
2056
|
+
if (match) numbers.push(match[1]);
|
|
2057
|
+
}
|
|
2058
|
+
return numbers;
|
|
2059
|
+
}
|
|
2060
|
+
function numbersInLine(line) {
|
|
2061
|
+
const found = [];
|
|
2062
|
+
for (const match of line.matchAll(PR_NUMBER)) found.push(match[1] ?? match[2]);
|
|
2063
|
+
return found;
|
|
2064
|
+
}
|
|
2065
|
+
function inferHeadBranch(commands, upTo, output) {
|
|
2066
|
+
for (let index = upTo - 1; index >= 0; index -= 1) {
|
|
2067
|
+
const words = commands[index];
|
|
2068
|
+
if (basename3(words[0] ?? "") !== "git" || words.length < 2) continue;
|
|
2069
|
+
const sub = words[1];
|
|
2070
|
+
const rest = words.slice(2);
|
|
2071
|
+
if (sub === "push") {
|
|
2072
|
+
const positional = rest.filter((word) => !word.startsWith("-"));
|
|
2073
|
+
const ref = positional[positional.length - 1];
|
|
2074
|
+
if (positional.length >= 2 && ref !== void 0 && ref !== "HEAD") {
|
|
2075
|
+
const branch = (ref.includes(":") ? ref.slice(ref.indexOf(":") + 1) : ref).replace(/^\+/, "");
|
|
2076
|
+
if (branch.length > 0) return branch;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (sub === "checkout" || sub === "switch") {
|
|
2080
|
+
for (let position = 0; position < rest.length - 1; position += 1) {
|
|
2081
|
+
if (["-b", "-B", "-c", "-C", "--create", "--force-create"].includes(rest[position])) return rest[position + 1];
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
if (sub === "worktree" && rest[0] === "add") {
|
|
2085
|
+
for (let position = 1; position < rest.length - 1; position += 1) {
|
|
2086
|
+
if (rest[position] === "-b" || rest[position] === "-B") return rest[position + 1];
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
return PUSH_TRACKING.exec(output)?.[1] ?? PUSH_NEW_BRANCH.exec(output)?.[1] ?? null;
|
|
2091
|
+
}
|
|
2092
|
+
function reachedGh(record, action, sawNumber) {
|
|
2093
|
+
if (!record.failed) return true;
|
|
2094
|
+
if (sawNumber || PULL_REQUEST_URL.test(record.text)) return true;
|
|
2095
|
+
return action === "create" ? CREATE_REACHED_GH.test(record.text) : MERGE_REACHED_GH.test(record.text);
|
|
2096
|
+
}
|
|
2097
|
+
function createEvent(record, recordIndex, words, commands, commandIndex, printed) {
|
|
2098
|
+
const { flags } = parseFlags(words.slice(3), CREATE_VALUE_FLAGS);
|
|
2099
|
+
if (flags.has("--help") || flags.has("-h")) return null;
|
|
2100
|
+
if (!reachedGh(record, "create", printed !== null)) return null;
|
|
2101
|
+
const printedNumber = printed?.number ?? null;
|
|
2102
|
+
const headFlag = flagValue(flags, "--head", "-H");
|
|
2103
|
+
const headBranch = headFlag ?? inferHeadBranch(commands, commandIndex, record.text);
|
|
2104
|
+
const evidence = printed?.pollSpanId ? `the pull-request URL a later poll of this backgrounded command printed, in span ${printed.pollSpanId}` : printedNumber !== null ? `the pull-request URL this command printed, in span ${record.spanId}` : headFlag !== null ? "the --head branch given to the command" : headBranch !== null ? "the branch this same command pushed or created before calling gh" : "none: the command named no head branch and printed no pull-request URL";
|
|
2105
|
+
return {
|
|
2106
|
+
action: "create",
|
|
2107
|
+
number: printedNumber,
|
|
2108
|
+
headBranch,
|
|
2109
|
+
command: words.join(" "),
|
|
2110
|
+
evidence,
|
|
2111
|
+
spanIds: printed?.pollSpanId ? [record.spanId, printed.pollSpanId] : [record.spanId],
|
|
2112
|
+
at: record.at,
|
|
2113
|
+
failed: record.failed,
|
|
2114
|
+
recordIndex,
|
|
2115
|
+
outputTruncated: record.outputTruncated
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function mergeEvent(record, recordIndex, words) {
|
|
2119
|
+
const { flags, positional } = parseFlags(words.slice(3), MERGE_VALUE_FLAGS);
|
|
2120
|
+
if (flags.has("--help") || flags.has("-h")) return null;
|
|
2121
|
+
const target = positional[0] ?? null;
|
|
2122
|
+
const numberInCommand = target === null ? null : /^#?(\d+)$/.exec(target)?.[1] ?? numbersInLine(target)[0] ?? null;
|
|
2123
|
+
const numberInOutput = numberInCommand === null ? MERGED_NUMBER.exec(record.text)?.[1] ?? null : null;
|
|
2124
|
+
const number2 = numberInCommand ?? numberInOutput;
|
|
2125
|
+
if (!reachedGh(record, "merge", number2 !== null)) return null;
|
|
2126
|
+
const evidence = numberInCommand !== null ? "the pull request the command names" : numberInOutput !== null ? `the merge line this command printed, in span ${record.spanId}` : target !== null ? "the branch the command names; no output that joins to it stated a number" : "none: the command named neither a pull request nor a branch";
|
|
2127
|
+
return {
|
|
2128
|
+
action: "merge",
|
|
2129
|
+
number: number2,
|
|
2130
|
+
headBranch: number2 === null ? target : null,
|
|
2131
|
+
command: words.join(" "),
|
|
2132
|
+
evidence,
|
|
2133
|
+
spanIds: [record.spanId],
|
|
2134
|
+
at: record.at,
|
|
2135
|
+
failed: record.failed,
|
|
2136
|
+
recordIndex,
|
|
2137
|
+
outputTruncated: record.outputTruncated
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
function eventsFromRecord(record, recordIndex, polled) {
|
|
2141
|
+
if (!/\bpr\b/.test(record.script)) return [];
|
|
2142
|
+
const commands = shellCommands(record.script);
|
|
2143
|
+
const printed = [...printedPullRequestNumbers(record.text).map((number2) => ({ number: number2, pollSpanId: null })), ...polled];
|
|
2144
|
+
const events = [];
|
|
2145
|
+
let createIndex = 0;
|
|
2146
|
+
for (let index = 0; index < commands.length; index += 1) {
|
|
2147
|
+
const words = commands[index];
|
|
2148
|
+
if (words.length < 3 || !GH_NAMES.has(basename3(words[0])) || words[1] !== "pr") continue;
|
|
2149
|
+
const action = words[2];
|
|
2150
|
+
if (action === "create") {
|
|
2151
|
+
const printedNumber = printed[createIndex] ?? null;
|
|
2152
|
+
createIndex += 1;
|
|
2153
|
+
const event2 = createEvent(record, recordIndex, words, commands, index, printedNumber);
|
|
2154
|
+
if (event2) events.push(event2);
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
if (action !== "merge") continue;
|
|
2158
|
+
const event = mergeEvent(record, recordIndex, words);
|
|
2159
|
+
if (event) events.push(event);
|
|
2160
|
+
}
|
|
2161
|
+
return events;
|
|
2162
|
+
}
|
|
2163
|
+
function numberFromLaterOutput(records, after, headBranch) {
|
|
2164
|
+
for (let index = after + 1; index < records.length; index += 1) {
|
|
2165
|
+
const record = records[index];
|
|
2166
|
+
const found = /* @__PURE__ */ new Set();
|
|
2167
|
+
for (const raw of `${record.script}
|
|
2168
|
+
${record.text}`.split("\n")) {
|
|
2169
|
+
const line = raw.replace(ANSI, "");
|
|
2170
|
+
if (!line.includes(headBranch)) continue;
|
|
2171
|
+
for (const number2 of numbersInLine(line)) found.add(number2);
|
|
2172
|
+
}
|
|
2173
|
+
if (found.size === 1) return { number: [...found][0], spanId: record.spanId };
|
|
2174
|
+
}
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
function identify(event) {
|
|
2178
|
+
const identifier = event.number ?? event.headBranch;
|
|
2179
|
+
return {
|
|
2180
|
+
identifier,
|
|
2181
|
+
number: event.number,
|
|
2182
|
+
headBranch: event.headBranch,
|
|
2183
|
+
command: event.command,
|
|
2184
|
+
evidence: event.evidence,
|
|
2185
|
+
spanIds: [...new Set(event.spanIds)],
|
|
2186
|
+
at: event.at,
|
|
2187
|
+
unavailable: identifier === null ? "the command named no pull-request number and no head branch, and no output that joins to it stated one" : null
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function distinct(events) {
|
|
2191
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2192
|
+
for (const event of events) {
|
|
2193
|
+
const key = event.number ?? event.headBranch ?? `command:${event.command}`;
|
|
2194
|
+
groups.set(key, [...groups.get(key) ?? [], event]);
|
|
2195
|
+
}
|
|
2196
|
+
return [...groups.values()].map((group) => {
|
|
2197
|
+
const shown = group.find((event) => !event.failed) ?? group[0];
|
|
2198
|
+
return { ...identify(shown), spanIds: [...new Set(group.flatMap((event) => event.spanIds))] };
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
var RUNNING_SESSION = /Process running with session ID (\d+)/;
|
|
2202
|
+
function pollsSession(input, session) {
|
|
2203
|
+
if (!input.includes(session)) return false;
|
|
2204
|
+
if (isCodeModeScript(input) || input.includes("write_stdin(")) {
|
|
2205
|
+
return new RegExp(`write_stdin\\(\\s*\\{[^}]*\\bsession_id["']?\\s*:\\s*${session}\\b`, "u").test(input);
|
|
2206
|
+
}
|
|
2207
|
+
try {
|
|
2208
|
+
return String(JSON.parse(input).session_id) === session;
|
|
2209
|
+
} catch {
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
function polledNumbers(spans, record) {
|
|
2214
|
+
const found = [];
|
|
2215
|
+
for (const session of record.running) {
|
|
2216
|
+
for (const span of spans) {
|
|
2217
|
+
if (span.start_time < record.at || span.span_id === record.spanId) continue;
|
|
2218
|
+
const input = stringAttribute2(span, "input.value");
|
|
2219
|
+
if (input === null || !pollsSession(input, session)) continue;
|
|
2220
|
+
const output = stringAttribute2(span, "output.value") ?? "";
|
|
2221
|
+
const text = [output, ...scriptResults(output).map((result) => result.output)].join("\n");
|
|
2222
|
+
const numbers = new Set(printedPullRequestNumbers(text));
|
|
2223
|
+
if (numbers.size !== 1) continue;
|
|
2224
|
+
found.push({ number: [...numbers][0], pollSpanId: span.span_id });
|
|
2225
|
+
break;
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
return found;
|
|
2229
|
+
}
|
|
2230
|
+
function readPullRequests(spans) {
|
|
2231
|
+
const records = commandRecords(spans);
|
|
2232
|
+
if (records.length === 0) {
|
|
2233
|
+
return {
|
|
2234
|
+
facts: null,
|
|
2235
|
+
spanIds: [],
|
|
2236
|
+
commandSpans: 0,
|
|
2237
|
+
partial: null,
|
|
2238
|
+
unavailable: "no span in this trace carries an executed command (an input.value with a command or cmd field), so whether the agent created or merged a pull request cannot be read from these spans"
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
const events = [];
|
|
2242
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
2243
|
+
const record = records[index];
|
|
2244
|
+
const creates = /\bpr\b/.test(record.script) && record.running.length > 0;
|
|
2245
|
+
events.push(...eventsFromRecord(record, index, creates ? polledNumbers(spans, record) : []));
|
|
2246
|
+
}
|
|
2247
|
+
const numberByBranch = /* @__PURE__ */ new Map();
|
|
2248
|
+
for (const event of events) {
|
|
2249
|
+
if (event.action !== "create" || event.number === null || event.headBranch === null) continue;
|
|
2250
|
+
if (!numberByBranch.has(event.headBranch)) numberByBranch.set(event.headBranch, event.number);
|
|
2251
|
+
}
|
|
2252
|
+
let unjoinedTruncated = 0;
|
|
2253
|
+
for (const event of events) {
|
|
2254
|
+
if (event.number !== null || event.headBranch === null) continue;
|
|
2255
|
+
const sibling = numberByBranch.get(event.headBranch);
|
|
2256
|
+
if (sibling !== void 0) {
|
|
2257
|
+
event.number = sibling;
|
|
2258
|
+
event.evidence = `${event.evidence}, resolved to the number another create for this branch printed`;
|
|
2259
|
+
continue;
|
|
2260
|
+
}
|
|
2261
|
+
const later2 = numberFromLaterOutput(records, event.recordIndex, event.headBranch);
|
|
2262
|
+
if (later2 !== null) {
|
|
2263
|
+
event.number = later2.number;
|
|
2264
|
+
event.spanIds.push(later2.spanId);
|
|
2265
|
+
event.evidence = `${event.evidence}, resolved to the number a later output states for this branch, in span ${later2.spanId}`;
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
if (event.outputTruncated) unjoinedTruncated += 1;
|
|
2269
|
+
}
|
|
2270
|
+
const created = distinct(events.filter((event) => event.action === "create"));
|
|
2271
|
+
const merged = distinct(events.filter((event) => event.action === "merge"));
|
|
2272
|
+
const truncatedCommands = records.filter((record) => record.inputTruncated && /\bpr\b/.test(record.script)).length;
|
|
2273
|
+
const notes = [];
|
|
2274
|
+
if (truncatedCommands > 0) {
|
|
2275
|
+
notes.push(
|
|
2276
|
+
`${truncatedCommands} command span(s) mentioning a pull request had truncated input; a gh command past the cut is not in this list`
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
if (unjoinedTruncated > 0) {
|
|
2280
|
+
notes.push(
|
|
2281
|
+
`${unjoinedTruncated} entr(y|ies) kept a branch identity while the output that could have named its number was truncated`
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
return {
|
|
2285
|
+
facts: { created, merged },
|
|
2286
|
+
spanIds: [...new Set([...created, ...merged].flatMap((fact) => fact.spanIds))],
|
|
2287
|
+
commandSpans: records.length,
|
|
2288
|
+
unavailable: null,
|
|
2289
|
+
partial: notes.length > 0 ? notes.join("; ") : null
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// src/session-identity.ts
|
|
2294
|
+
import { readFileSync } from "fs";
|
|
2295
|
+
import { join as join2 } from "path";
|
|
2296
|
+
import { ATTR as ATTR2 } from "@tangle-network/agent-trace-contract";
|
|
2297
|
+
var AGENT_IDENTITY_KEYS = [
|
|
2298
|
+
["profileDigest", ATTR2.profileDigest],
|
|
2299
|
+
["profileName", ATTR2.profileName],
|
|
2300
|
+
["model", ATTR2.agentModel],
|
|
2301
|
+
["harness", ATTR2.harness],
|
|
2302
|
+
["skillsDigest", ATTR2.skillsDigest],
|
|
2303
|
+
["directiveVariant", ATTR2.directiveVariant]
|
|
2304
|
+
];
|
|
2305
|
+
function identityFieldsOf(attributes) {
|
|
2306
|
+
const fields = {};
|
|
2307
|
+
for (const [field, key] of AGENT_IDENTITY_KEYS) {
|
|
2308
|
+
const value = attributes[key];
|
|
2309
|
+
if (typeof value === "string" && value.length > 0) fields[field] = value;
|
|
2310
|
+
}
|
|
2311
|
+
return fields;
|
|
2312
|
+
}
|
|
2313
|
+
function mergeAgentIdentity(sources) {
|
|
2314
|
+
const merged = {};
|
|
2315
|
+
let source = null;
|
|
2316
|
+
for (const entry of sources) {
|
|
2317
|
+
for (const [field] of AGENT_IDENTITY_KEYS) {
|
|
2318
|
+
const value = entry.fields[field];
|
|
2319
|
+
if (value === void 0 || merged[field] !== void 0) continue;
|
|
2320
|
+
merged[field] = value;
|
|
2321
|
+
source ??= entry.source;
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return source === null ? null : { ...merged, source };
|
|
2325
|
+
}
|
|
2326
|
+
var PLAIN_ID = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/u;
|
|
2327
|
+
var RECORD_DIR = join2(".local", "state", "session-identity", "claude-code");
|
|
2328
|
+
var DIRECTIVES_LOG = join2(".claude", "directives-log.jsonl");
|
|
2329
|
+
function readJson(path) {
|
|
2330
|
+
try {
|
|
2331
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
2332
|
+
} catch {
|
|
2333
|
+
return void 0;
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
function sessionIdentityLookup(home) {
|
|
2337
|
+
let variants;
|
|
2338
|
+
const variantOf = (sessionId) => {
|
|
2339
|
+
if (variants === void 0) {
|
|
2340
|
+
variants = /* @__PURE__ */ new Map();
|
|
2341
|
+
let text = "";
|
|
2342
|
+
try {
|
|
2343
|
+
text = readFileSync(join2(home, DIRECTIVES_LOG), "utf8");
|
|
2344
|
+
} catch {
|
|
2345
|
+
}
|
|
2346
|
+
for (const line of text.split("\n")) {
|
|
2347
|
+
if (!line.includes('"variant"')) continue;
|
|
2348
|
+
try {
|
|
2349
|
+
const row = JSON.parse(line);
|
|
2350
|
+
if (typeof row.session === "string" && typeof row.variant === "string" && row.variant.length > 0 && !variants.has(row.session)) {
|
|
2351
|
+
variants.set(row.session, row.variant);
|
|
2352
|
+
}
|
|
2353
|
+
} catch {
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
return variants.get(sessionId);
|
|
2358
|
+
};
|
|
2359
|
+
return ({ sessionId, harness }) => {
|
|
2360
|
+
if (harness !== "claude-code") {
|
|
2361
|
+
return { sources: [], reason: `no hook records who acted in ${harness ?? "this harness's"} sessions, so only spans could say` };
|
|
2362
|
+
}
|
|
2363
|
+
const recordPath = join2(home, RECORD_DIR, `${sessionId}.json`);
|
|
2364
|
+
const sources = [];
|
|
2365
|
+
if (PLAIN_ID.test(sessionId)) {
|
|
2366
|
+
const record = readJson(recordPath);
|
|
2367
|
+
const attributes = record !== null && typeof record === "object" ? record.attributes : void 0;
|
|
2368
|
+
if (attributes !== null && typeof attributes === "object") {
|
|
2369
|
+
const fields = identityFieldsOf(attributes);
|
|
2370
|
+
if (Object.keys(fields).length > 0) sources.push({ source: "session-identity", fields });
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const variant = variantOf(sessionId);
|
|
2374
|
+
if (variant !== void 0) sources.push({ source: "directives-log", fields: { directiveVariant: variant } });
|
|
2375
|
+
return {
|
|
2376
|
+
sources,
|
|
2377
|
+
reason: `no session identity record at ${recordPath} and no entry in ${join2(home, DIRECTIVES_LOG)}: the session predates the SessionStart hook that writes the record, or the hook is not installed`
|
|
2378
|
+
};
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// src/session-facts.ts
|
|
2383
|
+
var SESSION_FACTS_MEASUREMENT_VERSION = "8";
|
|
2384
|
+
var SPAN_SYNTHESIZED_ATTR = "traces.codex.span_synthesized";
|
|
2385
|
+
var CORRUPTION_COUNT_ATTR = "traces.session.corruption_count";
|
|
2386
|
+
var CORRUPTION_RECEIPT_VERSION_ATTR = "traces.session.corruption.receipt_version";
|
|
2387
|
+
var FACT_LIST_CAP = 200;
|
|
2388
|
+
var CHANGED_FILE_LIST_CAP = 5e3;
|
|
2389
|
+
function attr(span, key) {
|
|
2390
|
+
return span.attributes[key];
|
|
2391
|
+
}
|
|
2392
|
+
function stringAttr2(span, key) {
|
|
2393
|
+
const value = span.attributes[key];
|
|
2394
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2395
|
+
}
|
|
2396
|
+
function orderOf(span) {
|
|
2397
|
+
const step = span.attributes.step;
|
|
2398
|
+
const start = Date.parse(span.start_time);
|
|
2399
|
+
return [typeof step === "number" ? step : Number.MAX_SAFE_INTEGER, Number.isFinite(start) ? start : 0];
|
|
2400
|
+
}
|
|
2401
|
+
function byTraceOrder(left, right) {
|
|
2402
|
+
const [leftStep, leftStart] = orderOf(left);
|
|
2403
|
+
const [rightStep, rightStart] = orderOf(right);
|
|
2404
|
+
if (leftStart !== rightStart) return leftStart - rightStart;
|
|
2405
|
+
return leftStep - rightStep;
|
|
2406
|
+
}
|
|
2407
|
+
function isSynthesizedSpan2(span) {
|
|
2408
|
+
if (isSynthesizedSpan(span.attributes)) return true;
|
|
2409
|
+
if (attr(span, SPAN_SYNTHESIZED_ATTR) === true) return true;
|
|
2410
|
+
return stringAttr2(span, "traces.codex.subagent_thread_id") !== null && stringAttr2(span, "traces.codex.subagent_path") !== null;
|
|
2411
|
+
}
|
|
2412
|
+
function isToolSpan(span) {
|
|
2413
|
+
return attr(span, OPENINFERENCE_SPAN_KIND3) === "TOOL";
|
|
2414
|
+
}
|
|
2415
|
+
function toolName(span) {
|
|
2416
|
+
return stringAttr2(span, TOOL_NAME) ?? span.name.replace(/^tool\./, "");
|
|
2417
|
+
}
|
|
2418
|
+
function inputValue(span) {
|
|
2419
|
+
const value = span.attributes["input.value"];
|
|
2420
|
+
return typeof value === "string" ? value : null;
|
|
2421
|
+
}
|
|
2422
|
+
function inputTruncated(span) {
|
|
2423
|
+
return span.attributes["traces.input.truncated"] === true;
|
|
2424
|
+
}
|
|
2425
|
+
var PATCH_HEADER = /\*\*\* (Add|Update|Delete|Move to) File: ([^\n"\\]+)/g;
|
|
2426
|
+
var PATCH_OPERATION = {
|
|
2427
|
+
Add: "add",
|
|
2428
|
+
Update: "update",
|
|
2429
|
+
Delete: "delete",
|
|
2430
|
+
"Move to": "move"
|
|
2431
|
+
};
|
|
2432
|
+
var FILE_CHANGE_SPAN = "file.change";
|
|
2433
|
+
var FILE_CHANGE_KIND_ATTR = "traces.codex.file_change_kind";
|
|
2434
|
+
var UNRESOLVED_PATH = /(?:^|\/)\$/;
|
|
2435
|
+
var FILE_PATH_TOOLS = /* @__PURE__ */ new Set(["Edit", "MultiEdit", "Write", "NotebookEdit", "edit", "write"]);
|
|
2436
|
+
var FILE_PATH_KEY = /"(?:file_path|filePath|path|notebook_path)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
2437
|
+
var SUBAGENT_TYPE_KEY = /"subagent_type"\s*:\s*"((?:[^"\\]|\\.)*)"/u;
|
|
2438
|
+
function decodeJsonString(raw) {
|
|
2439
|
+
try {
|
|
2440
|
+
const value = JSON.parse(`"${raw}"`);
|
|
2441
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
2442
|
+
} catch {
|
|
2443
|
+
return null;
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function recordChangedPath(into, path, operation, spanId) {
|
|
2447
|
+
const entry = into.get(path) ?? { operations: [], spanIds: [] };
|
|
2448
|
+
if (!entry.operations.includes(operation)) entry.operations.push(operation);
|
|
2449
|
+
if (!entry.spanIds.includes(spanId)) entry.spanIds.push(spanId);
|
|
2450
|
+
into.set(path, entry);
|
|
2451
|
+
}
|
|
2452
|
+
function harnessChangedPaths(span) {
|
|
2453
|
+
const input = inputValue(span);
|
|
2454
|
+
if (input === null) return [];
|
|
2455
|
+
let parsed;
|
|
2456
|
+
try {
|
|
2457
|
+
parsed = JSON.parse(input);
|
|
2458
|
+
} catch {
|
|
2459
|
+
return [];
|
|
2460
|
+
}
|
|
2461
|
+
if (parsed === null || typeof parsed !== "object") return [];
|
|
2462
|
+
const record = parsed;
|
|
2463
|
+
const path = typeof record.path === "string" ? record.path.trim() : "";
|
|
2464
|
+
if (path.length === 0) return [];
|
|
2465
|
+
const kind = stringAttr2(span, FILE_CHANGE_KIND_ATTR) ?? (typeof record.kind === "string" ? record.kind : "");
|
|
2466
|
+
const movePath = typeof record.move_path === "string" ? record.move_path.trim() : "";
|
|
2467
|
+
return [
|
|
2468
|
+
{ path, operation: kind.length > 0 ? kind : "update" },
|
|
2469
|
+
...movePath.length > 0 ? [{ path: movePath, operation: "move" }] : []
|
|
2470
|
+
];
|
|
2471
|
+
}
|
|
2472
|
+
function recordRecoveredPath(into, resolved, path, operation, spanId) {
|
|
2473
|
+
if (UNRESOLVED_PATH.test(path)) return false;
|
|
2474
|
+
const known = resolved.filter((candidate) => candidate === path || candidate.endsWith(`/${path}`));
|
|
2475
|
+
if (known.length > 1) return true;
|
|
2476
|
+
recordChangedPath(into, known[0] ?? path, operation, spanId);
|
|
2477
|
+
return true;
|
|
2478
|
+
}
|
|
2479
|
+
function changedFilesOf(toolSpans, spans) {
|
|
2480
|
+
const paths = /* @__PURE__ */ new Map();
|
|
2481
|
+
for (const span of spans) {
|
|
2482
|
+
if (span.name !== FILE_CHANGE_SPAN || span.status.code === "ERROR") continue;
|
|
2483
|
+
for (const change of harnessChangedPaths(span)) {
|
|
2484
|
+
recordChangedPath(paths, change.path, change.operation, span.span_id);
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
const resolved = [...paths.keys()];
|
|
2488
|
+
let truncatedInputs = 0;
|
|
2489
|
+
let unresolvedPaths = 0;
|
|
2490
|
+
for (const span of toolSpans) {
|
|
2491
|
+
const input = inputValue(span);
|
|
2492
|
+
if (input === null) continue;
|
|
2493
|
+
let matched = false;
|
|
2494
|
+
for (const match of input.matchAll(PATCH_HEADER)) {
|
|
2495
|
+
const operation = PATCH_OPERATION[match[1]];
|
|
2496
|
+
const path = match[2]?.trim();
|
|
2497
|
+
if (!operation || !path) continue;
|
|
2498
|
+
matched = true;
|
|
2499
|
+
if (!recordRecoveredPath(paths, resolved, path, operation, span.span_id)) unresolvedPaths += 1;
|
|
2500
|
+
}
|
|
2501
|
+
if (!matched && FILE_PATH_TOOLS.has(toolName(span))) {
|
|
2502
|
+
for (const match of input.matchAll(FILE_PATH_KEY)) {
|
|
2503
|
+
const path = decodeJsonString(match[1] ?? "");
|
|
2504
|
+
if (!path) continue;
|
|
2505
|
+
matched = true;
|
|
2506
|
+
if (!recordRecoveredPath(paths, resolved, path, "update", span.span_id)) unresolvedPaths += 1;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (matched && inputTruncated(span)) truncatedInputs += 1;
|
|
2510
|
+
}
|
|
2511
|
+
const files = [...paths].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([path, entry]) => ({ path, operations: entry.operations, spanIds: entry.spanIds }));
|
|
2512
|
+
return { files, truncatedInputs, unresolvedPaths };
|
|
2513
|
+
}
|
|
2514
|
+
function subagentsOf(ownSpans, spans) {
|
|
2515
|
+
const lifecycleByPath = /* @__PURE__ */ new Map();
|
|
2516
|
+
const lifecycleByParent = /* @__PURE__ */ new Map();
|
|
2517
|
+
for (const span of spans) {
|
|
2518
|
+
if (!isSynthesizedSpan2(span)) continue;
|
|
2519
|
+
const path = stringAttr2(span, "traces.codex.subagent_path");
|
|
2520
|
+
if (path !== null) {
|
|
2521
|
+
lifecycleByPath.set(path, [...lifecycleByPath.get(path) ?? [], span.span_id]);
|
|
2522
|
+
}
|
|
2523
|
+
const parent = span.parent_span_id;
|
|
2524
|
+
if (parent !== null && span.attributes[SUBAGENT_TASK_ATTR] !== void 0) {
|
|
2525
|
+
lifecycleByParent.set(parent, [...lifecycleByParent.get(parent) ?? [], span.span_id]);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
const spawns = [];
|
|
2529
|
+
for (const span of ownSpans) {
|
|
2530
|
+
const isSpawn = attr(span, "traces.codex.agent_operation") === "spawn_agent" || stringAttr2(span, "traces.codex.spawn_agent_path") !== null || attr(span, SUBAGENT_SPAWN_ATTR) === true;
|
|
2531
|
+
if (!isSpawn || isSynthesizedSpan2(span)) continue;
|
|
2532
|
+
const taskName = stringAttr2(span, "traces.codex.spawn_agent_path") ?? stringAttr2(span, SUBAGENT_SPAWN_TASK_ATTR);
|
|
2533
|
+
const agentType = SUBAGENT_TYPE_KEY.exec(inputValue(span) ?? "")?.[1];
|
|
2534
|
+
spawns.push({
|
|
2535
|
+
taskName,
|
|
2536
|
+
taskNameUnavailable: taskName === null ? "the spawn span carries no task name; the harness recorded none for this call" : null,
|
|
2537
|
+
agentType: agentType === void 0 ? null : decodeJsonString(agentType),
|
|
2538
|
+
startedAt: span.start_time,
|
|
2539
|
+
status: span.status.code,
|
|
2540
|
+
spanIds: [
|
|
2541
|
+
span.span_id,
|
|
2542
|
+
...taskName ? lifecycleByPath.get(taskName) ?? [] : [],
|
|
2543
|
+
...lifecycleByParent.get(span.span_id) ?? []
|
|
2544
|
+
]
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
return spawns;
|
|
2548
|
+
}
|
|
2549
|
+
function toolCounts(spans) {
|
|
2550
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2551
|
+
for (const span of spans) {
|
|
2552
|
+
const row = rows.get(toolName(span)) ?? { calls: 0, failed: 0 };
|
|
2553
|
+
row.calls += 1;
|
|
2554
|
+
if (span.status.code === "ERROR") row.failed += 1;
|
|
2555
|
+
rows.set(toolName(span), row);
|
|
2556
|
+
}
|
|
2557
|
+
return [...rows].map(([name, row]) => ({ name, ...row })).sort((left, right) => right.calls - left.calls || left.name.localeCompare(right.name));
|
|
2558
|
+
}
|
|
2559
|
+
function finalMessagesOf(spans) {
|
|
2560
|
+
const last = /* @__PURE__ */ new Map();
|
|
2561
|
+
for (const span of spans) {
|
|
2562
|
+
let task;
|
|
2563
|
+
if (span.name === "message.assistant") {
|
|
2564
|
+
task = isSubagentSpan(span.attributes) ? stringAttr2(span, SUBAGENT_TASK_ATTR) ?? "unattributed subagent" : null;
|
|
2565
|
+
} else if (span.name.startsWith("message.agent.")) {
|
|
2566
|
+
task = stringAttr2(span, "traces.codex.agent_message_author") ?? "unattributed subagent";
|
|
2567
|
+
}
|
|
2568
|
+
if (task === void 0) continue;
|
|
2569
|
+
const previous = last.get(task);
|
|
2570
|
+
if (!previous || byTraceOrder(previous, span) <= 0) last.set(task, span);
|
|
2571
|
+
}
|
|
2572
|
+
return [...last].sort(([left], [right]) => left === null ? -1 : right === null ? 1 : left.localeCompare(right)).map(([task, span]) => ({
|
|
2573
|
+
task,
|
|
2574
|
+
at: span.end_time,
|
|
2575
|
+
text: capText(typeof span.attributes.content === "string" ? span.attributes.content : "", FACT_TEXT_CAP),
|
|
2576
|
+
spanId: span.span_id
|
|
2577
|
+
}));
|
|
2578
|
+
}
|
|
2579
|
+
var EXIT_CODE = /^(?:Process exited with code|Exit code:?|Command failed with exit code)[ \t]+(-?\d+)\.?[ \t]*$/imu;
|
|
2580
|
+
function printedExitCode(span) {
|
|
2581
|
+
const output = span.attributes["output.value"];
|
|
2582
|
+
if (typeof output !== "string") return null;
|
|
2583
|
+
const end = output.indexOf("\nOutput:\n");
|
|
2584
|
+
const code = EXIT_CODE.exec(end >= 0 ? output.slice(0, end) : output.slice(0, 400))?.[1];
|
|
2585
|
+
return code === void 0 ? null : Number(code);
|
|
2586
|
+
}
|
|
2587
|
+
var SHELL_STATE_BUILTINS = /* @__PURE__ */ new Set(["cd", "pushd", "popd", "export", "set", "unset", "source", ".", ":", "true", "false", "exit", "shift", "local", "alias"]);
|
|
2588
|
+
var DATA_FIRST_PROGRAMS = /* @__PURE__ */ new Set([
|
|
2589
|
+
"awk",
|
|
2590
|
+
"basename",
|
|
2591
|
+
"cat",
|
|
2592
|
+
"chmod",
|
|
2593
|
+
"column",
|
|
2594
|
+
"cp",
|
|
2595
|
+
"curl",
|
|
2596
|
+
"cut",
|
|
2597
|
+
"date",
|
|
2598
|
+
"diff",
|
|
2599
|
+
"dirname",
|
|
2600
|
+
"du",
|
|
2601
|
+
"echo",
|
|
2602
|
+
"egrep",
|
|
2603
|
+
"fgrep",
|
|
2604
|
+
"file",
|
|
2605
|
+
"find",
|
|
2606
|
+
"grep",
|
|
2607
|
+
"head",
|
|
2608
|
+
"jq",
|
|
2609
|
+
"kill",
|
|
2610
|
+
"less",
|
|
2611
|
+
"ln",
|
|
2612
|
+
"ls",
|
|
2613
|
+
"mkdir",
|
|
2614
|
+
"mv",
|
|
2615
|
+
"nl",
|
|
2616
|
+
"open",
|
|
2617
|
+
"printf",
|
|
2618
|
+
"readlink",
|
|
2619
|
+
"realpath",
|
|
2620
|
+
"rg",
|
|
2621
|
+
"rm",
|
|
2622
|
+
"sed",
|
|
2623
|
+
"sleep",
|
|
2624
|
+
"sort",
|
|
2625
|
+
"stat",
|
|
2626
|
+
"tail",
|
|
2627
|
+
"tee",
|
|
2628
|
+
"test",
|
|
2629
|
+
"[",
|
|
2630
|
+
"touch",
|
|
2631
|
+
"tr",
|
|
2632
|
+
"uniq",
|
|
2633
|
+
"wc",
|
|
2634
|
+
"wget",
|
|
2635
|
+
"which",
|
|
2636
|
+
"xargs"
|
|
2637
|
+
]);
|
|
2638
|
+
var PLAIN_WORD = /^[A-Za-z][\w:-]{0,39}$/u;
|
|
2639
|
+
function commandName(words) {
|
|
2640
|
+
const program = words[0].slice(words[0].lastIndexOf("/") + 1);
|
|
2641
|
+
const next = words[1];
|
|
2642
|
+
return next !== void 0 && !DATA_FIRST_PROGRAMS.has(program) && PLAIN_WORD.test(next) ? `${program} ${next}` : program;
|
|
2643
|
+
}
|
|
2644
|
+
function callCommands(span) {
|
|
2645
|
+
const input = inputValue(span);
|
|
2646
|
+
const script = input === null ? void 0 : commandTextFromInput(input);
|
|
2647
|
+
if (script === void 0) return void 0;
|
|
2648
|
+
const inScript = isCodeModeScript(input);
|
|
2649
|
+
const commands = simpleCommands(script);
|
|
2650
|
+
const programs = commands.flatMap((words, index) => SHELL_STATE_BUILTINS.has(words[0]) ? [] : [index]);
|
|
2651
|
+
const own = !inScript && programs.length === 1 && span.status.code !== "UNSET" ? programs[0] : -1;
|
|
2652
|
+
return { commands, inScript, failed: commands.map((_, index) => index === own ? span.status.code === "ERROR" : null) };
|
|
2653
|
+
}
|
|
2654
|
+
function commandsOf(tools) {
|
|
2655
|
+
const own = tools.filter((span) => span.attributes[INHERITED_SPAN_ATTR] !== true);
|
|
2656
|
+
if (own.length > 0 && !own.some((span) => inputValue(span) !== null)) {
|
|
2657
|
+
return { value: null, spanIds: [], unavailable: "no tool call recorded its arguments, so which commands ran cannot be read" };
|
|
2658
|
+
}
|
|
2659
|
+
const rows = /* @__PURE__ */ new Map();
|
|
2660
|
+
const spanIds = [];
|
|
2661
|
+
for (const span of own) {
|
|
2662
|
+
const call = callCommands(span);
|
|
2663
|
+
if (call === void 0) continue;
|
|
2664
|
+
let ran = false;
|
|
2665
|
+
call.commands.forEach((words, index) => {
|
|
2666
|
+
if (SHELL_STATE_BUILTINS.has(words[0])) return;
|
|
2667
|
+
ran = true;
|
|
2668
|
+
const name = commandName(words);
|
|
2669
|
+
const row = rows.get(name) ?? { runs: 0, failed: 0, inScripts: 0, nonZeroExits: [], lastSpanId: span.span_id };
|
|
2670
|
+
row.runs += 1;
|
|
2671
|
+
row.lastSpanId = span.span_id;
|
|
2672
|
+
if (call.failed[index] === true) {
|
|
2673
|
+
row.failed += 1;
|
|
2674
|
+
row.nonZeroExits.push({ code: printedExitCode(span), inScript: call.inScript, at: span.start_time, spanId: span.span_id });
|
|
2675
|
+
}
|
|
2676
|
+
if (call.inScript) row.inScripts += 1;
|
|
2677
|
+
rows.set(name, row);
|
|
2678
|
+
});
|
|
2679
|
+
if (ran) spanIds.push(span.span_id);
|
|
2680
|
+
}
|
|
2681
|
+
const counts = [...rows].map(([command, row]) => ({ command, ...row })).sort((left, right) => right.runs - left.runs || left.command.localeCompare(right.command));
|
|
2682
|
+
return listFact(counts, spanIds);
|
|
2683
|
+
}
|
|
2684
|
+
function capList(items, cap = FACT_LIST_CAP) {
|
|
2685
|
+
if (items.length <= cap) return { kept: items };
|
|
2686
|
+
return {
|
|
2687
|
+
kept: items.slice(0, cap),
|
|
2688
|
+
partial: `${items.length - cap} of ${items.length} entries omitted; the count above the list is complete`
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
function listFact(items, spanIds, cap = FACT_LIST_CAP) {
|
|
2692
|
+
const { kept, partial } = capList(items, cap);
|
|
2693
|
+
return { value: kept, spanIds: [...new Set(spanIds)], unavailable: null, ...partial ? { partial } : {} };
|
|
2694
|
+
}
|
|
2695
|
+
function changedFilesFact(files, truncatedInputs, unresolvedPaths) {
|
|
2696
|
+
const fact = listFact(files, files.flatMap((entry) => entry.spanIds), CHANGED_FILE_LIST_CAP);
|
|
2697
|
+
const gaps = [
|
|
2698
|
+
...fact.partial ? [fact.partial] : [],
|
|
2699
|
+
...truncatedInputs > 0 ? [
|
|
2700
|
+
`${truncatedInputs} contributing tool span(s) had truncated input; paths named after the cut are not in this list`
|
|
2701
|
+
] : [],
|
|
2702
|
+
...unresolvedPaths > 0 ? [
|
|
2703
|
+
`${unresolvedPaths} recovered path(s) still held an unexpanded variable and were dropped; each names a file only if the harness also recorded that change`
|
|
2704
|
+
] : []
|
|
2705
|
+
];
|
|
2706
|
+
return { ...fact, ...gaps.length > 0 ? { partial: gaps.join("; ") } : {} };
|
|
2707
|
+
}
|
|
2708
|
+
function excludedTurnFacts(excluded, keep) {
|
|
2709
|
+
return [...excluded].map(([key, entries]) => {
|
|
2710
|
+
const [reason, author] = key.split("\0");
|
|
2711
|
+
return { reason, author, spanIds: entries.filter((entry) => keep(entry.at)).map((entry) => entry.spanId) };
|
|
2712
|
+
}).filter((entry) => entry.spanIds.length > 0).sort((left, right) => left.reason.localeCompare(right.reason) || left.author.localeCompare(right.author)).map(({ reason, author, spanIds }) => ({ reason, author, turns: spanIds.length, spanIds }));
|
|
2713
|
+
}
|
|
2714
|
+
function windowBounds(window) {
|
|
2715
|
+
if (!window || window.since === void 0 && window.until === void 0) return null;
|
|
2716
|
+
const bound = (value, name, open2) => {
|
|
2717
|
+
if (value === void 0) return open2;
|
|
2718
|
+
const ms = Date.parse(value);
|
|
2719
|
+
if (!Number.isFinite(ms)) throw new RangeError(`window ${name} is not a readable time: ${value}`);
|
|
2720
|
+
return ms;
|
|
2721
|
+
};
|
|
2722
|
+
const sinceMs = bound(window.since, "since", Number.NEGATIVE_INFINITY);
|
|
2723
|
+
const untilMs = bound(window.until, "until", Number.POSITIVE_INFINITY);
|
|
2724
|
+
if (!(sinceMs < untilMs)) throw new RangeError(`window since (${window.since}) must be before until (${window.until})`);
|
|
2725
|
+
return {
|
|
2726
|
+
since: window.since === void 0 ? null : new Date(sinceMs).toISOString(),
|
|
2727
|
+
until: window.until === void 0 ? null : new Date(untilMs).toISOString(),
|
|
2728
|
+
sinceMs,
|
|
2729
|
+
untilMs
|
|
2730
|
+
};
|
|
2731
|
+
}
|
|
2732
|
+
function insideWindow(bounds, time) {
|
|
2733
|
+
if (bounds === null) return true;
|
|
2734
|
+
const ms = Date.parse(time);
|
|
2735
|
+
return Number.isFinite(ms) && ms >= bounds.sinceMs && ms < bounds.untilMs;
|
|
2736
|
+
}
|
|
2737
|
+
function isRecordSpan(span) {
|
|
2738
|
+
return span.parent_span_id !== null && span.attributes[CORRUPTION_RECEIPT_VERSION_ATTR] === void 0;
|
|
2739
|
+
}
|
|
2740
|
+
function computeSessionFacts(spans, options = {}) {
|
|
2741
|
+
const bounds = windowBounds(options.window);
|
|
2742
|
+
return groupByTrace(spans).sort((left, right) => compareIds(left.traceId, right.traceId)).map((group) => readSession(group.traceId, group.sessionId, group.spans, options, bounds).facts);
|
|
2743
|
+
}
|
|
2744
|
+
function compareIds(left, right) {
|
|
2745
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
2746
|
+
}
|
|
2747
|
+
function groupByTrace(spans) {
|
|
2748
|
+
const { sessionByTrace } = indexSessionIdsByTrace(spans);
|
|
2749
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
2750
|
+
for (const span of spans) {
|
|
2751
|
+
const group = byTrace.get(span.trace_id);
|
|
2752
|
+
if (group) group.push(span);
|
|
2753
|
+
else byTrace.set(span.trace_id, [span]);
|
|
2754
|
+
}
|
|
2755
|
+
return [...byTrace].map(([traceId, traceSpans]) => ({
|
|
2756
|
+
traceId,
|
|
2757
|
+
sessionId: sessionByTrace.get(traceId) ?? null,
|
|
2758
|
+
spans: traceSpans
|
|
2759
|
+
}));
|
|
2760
|
+
}
|
|
2761
|
+
function readSession(traceId, sessionId, unordered, options, bounds) {
|
|
2762
|
+
const spans = [...unordered].sort(byTraceOrder);
|
|
2763
|
+
const harness = spans.map((span) => stringAttr2(span, "service.name")).find((name) => name !== null) ?? null;
|
|
2764
|
+
const ownSpans = spans.filter((span) => !isSubagentSpan(span.attributes));
|
|
2765
|
+
const toolSpans = spans.filter(isToolSpan);
|
|
2766
|
+
const synthesized = toolSpans.filter(isSynthesizedSpan2);
|
|
2767
|
+
const invoked = ownSpans.filter((span) => isToolSpan(span) && !isSynthesizedSpan2(span));
|
|
2768
|
+
const subagentTools = toolSpans.filter(
|
|
2769
|
+
(span) => isSubagentSpan(span.attributes) && !isSynthesizedSpan2(span)
|
|
2770
|
+
);
|
|
2771
|
+
const byName = {};
|
|
2772
|
+
for (const span of invoked) byName[toolName(span)] = (byName[toolName(span)] ?? 0) + 1;
|
|
2773
|
+
const subagents = subagentsOf(ownSpans, spans);
|
|
2774
|
+
const pullRequests = readPullRequests(ownSpans);
|
|
2775
|
+
const inWindow = (at) => insideWindow(bounds, at);
|
|
2776
|
+
const promptSpans = spans.filter((span) => span.name === "user.prompt" && inWindow(span.start_time));
|
|
2777
|
+
const turnFacts = typedTurnsOf(spans);
|
|
2778
|
+
const typedTurns = turnFacts.turns.filter((turn) => inWindow(turn.at));
|
|
2779
|
+
const humanTurns = typedTurns.filter((turn) => turn.author === "human");
|
|
2780
|
+
const excludedTurns = excludedTurnFacts(turnFacts.excluded, inWindow);
|
|
2781
|
+
const actorCounts = /* @__PURE__ */ new Map();
|
|
2782
|
+
for (const span of promptSpans) {
|
|
2783
|
+
const actor = stringAttr2(span, ACTOR_ATTR) ?? "unclassified";
|
|
2784
|
+
actorCounts.set(actor, [...actorCounts.get(actor) ?? [], span.span_id]);
|
|
2785
|
+
}
|
|
2786
|
+
const finalMessages = finalMessagesOf(spans);
|
|
2787
|
+
const { files, truncatedInputs, unresolvedPaths } = changedFilesOf(invoked, ownSpans);
|
|
2788
|
+
const starts = ownSpans.filter((span) => Number.isFinite(Date.parse(span.start_time)));
|
|
2789
|
+
const ends = ownSpans.filter((span) => Number.isFinite(Date.parse(span.end_time)));
|
|
2790
|
+
const firstSpan = starts.reduce(
|
|
2791
|
+
(best, span) => best === null || Date.parse(span.start_time) < Date.parse(best.start_time) ? span : best,
|
|
2792
|
+
null
|
|
2793
|
+
);
|
|
2794
|
+
const lastSpan = ends.reduce(
|
|
2795
|
+
(best, span) => best === null || Date.parse(span.end_time) > Date.parse(best.end_time) ? span : best,
|
|
2796
|
+
null
|
|
2797
|
+
);
|
|
2798
|
+
const tokens = sessionTokens(ownSpans);
|
|
2799
|
+
const operating = operatingSamplesOf(ownSpans, invoked);
|
|
2800
|
+
const receipts = spans.filter((span) => span.attributes[CORRUPTION_RECEIPT_VERSION_ATTR] !== void 0);
|
|
2801
|
+
const recordSpans = spans.filter(isRecordSpan).length;
|
|
2802
|
+
const corruptionSpan = spans.find((span) => typeof span.attributes[CORRUPTION_COUNT_ATTR] === "number");
|
|
2803
|
+
const cwdSpan = spans.find((span) => stringAttr2(span, SESSION_CWD_ATTR) !== null);
|
|
2804
|
+
const repoSpan = spans.find((span) => stringAttr2(span, ATTR.GIT_REPOSITORY) !== null);
|
|
2805
|
+
const firstFailure = ownSpans.length === 0 ? { status: "unknown", traceId, unsetCount: 0, reason: "no span of this session's own records" } : rankFirstFailure(ingestSpans(ownSpans, { contentIncluded: true }).spans);
|
|
2806
|
+
const failedSpan = firstFailure.status === "found" ? ownSpans.find((span) => span.span_id === firstFailure.spanId) : void 0;
|
|
2807
|
+
const agent = agentFact(spans, sessionId, harness, options.agentIdentity);
|
|
2808
|
+
const facts = {
|
|
2809
|
+
schemaVersion: 1,
|
|
2810
|
+
kind: "traces.session_facts",
|
|
2811
|
+
traceId,
|
|
2812
|
+
sessionId,
|
|
2813
|
+
harness,
|
|
2814
|
+
parentSessionId: spans.filter((span) => span.parent_span_id === null).map((span) => stringAttr2(span, "traces.parent_session_id")).find((id) => id !== null) ?? null,
|
|
2815
|
+
cwd: cwdSpan ? { value: stringAttr2(cwdSpan, SESSION_CWD_ATTR), spanIds: [cwdSpan.span_id], unavailable: null } : {
|
|
2816
|
+
value: null,
|
|
2817
|
+
spanIds: [],
|
|
2818
|
+
unavailable: `no span carries ${SESSION_CWD_ATTR}: the session source records no working directory, or the adapter that read it does not read one`
|
|
2819
|
+
},
|
|
2820
|
+
spanCount: spans.length,
|
|
2821
|
+
recordSpans,
|
|
2822
|
+
unreadRecords: corruptionSpan ? {
|
|
2823
|
+
value: corruptionSpan.attributes[CORRUPTION_COUNT_ATTR],
|
|
2824
|
+
spanIds: [corruptionSpan.span_id, ...receipts.map((span) => span.span_id)],
|
|
2825
|
+
unavailable: null
|
|
2826
|
+
} : {
|
|
2827
|
+
value: null,
|
|
2828
|
+
spanIds: [],
|
|
2829
|
+
unavailable: `no span carries ${CORRUPTION_COUNT_ATTR}; this trace was not read through the session integrity path, so whether any record was skipped is unknown`
|
|
2830
|
+
},
|
|
2831
|
+
repo: repoSpan ? {
|
|
2832
|
+
value: {
|
|
2833
|
+
repository: stringAttr2(repoSpan, ATTR.GIT_REPOSITORY),
|
|
2834
|
+
branch: stringAttr2(repoSpan, ATTR.GIT_BRANCH_NAME),
|
|
2835
|
+
commit: stringAttr2(repoSpan, ATTR.GIT_COMMIT)
|
|
2836
|
+
},
|
|
2837
|
+
spanIds: [repoSpan.span_id],
|
|
2838
|
+
unavailable: null
|
|
2839
|
+
} : {
|
|
2840
|
+
value: null,
|
|
2841
|
+
spanIds: [],
|
|
2842
|
+
unavailable: `no span carries ${ATTR.GIT_REPOSITORY}: the recorded cwd is not a checkout with a remote, or no longer exists`
|
|
2843
|
+
},
|
|
2844
|
+
measurements: computeSessionMeasurements({ spans: ownSpans, records: ownSpans.filter(isRecordSpan), tools: invoked, sessionId, harness, operating }),
|
|
2845
|
+
toolCalls: {
|
|
2846
|
+
value: invoked.length,
|
|
2847
|
+
spanIds: invoked.map((span) => span.span_id),
|
|
2848
|
+
unavailable: null
|
|
2849
|
+
},
|
|
2850
|
+
synthesizedToolSpans: {
|
|
2851
|
+
value: synthesized.length,
|
|
2852
|
+
spanIds: synthesized.map((span) => span.span_id),
|
|
2853
|
+
unavailable: null
|
|
2854
|
+
},
|
|
2855
|
+
subagentTools: {
|
|
2856
|
+
value: toolCounts(subagentTools),
|
|
2857
|
+
spanIds: subagentTools.map((span) => span.span_id),
|
|
2858
|
+
unavailable: null
|
|
2859
|
+
},
|
|
2860
|
+
toolCallsByName: {
|
|
2861
|
+
value: byName,
|
|
2862
|
+
spanIds: invoked.map((span) => span.span_id),
|
|
2863
|
+
unavailable: null
|
|
2864
|
+
},
|
|
2865
|
+
subagents: listFact(subagents, subagents.flatMap((entry) => entry.spanIds)),
|
|
2866
|
+
pullRequests: pullRequests.facts ? {
|
|
2867
|
+
value: pullRequests.facts,
|
|
2868
|
+
spanIds: pullRequests.spanIds,
|
|
2869
|
+
unavailable: null,
|
|
2870
|
+
...pullRequests.partial ? { partial: pullRequests.partial } : {}
|
|
2871
|
+
} : { value: null, spanIds: [], unavailable: pullRequests.unavailable ?? "unread" },
|
|
2872
|
+
humanTurns: listFact(humanTurns, humanTurns.map((turn) => turn.spanId)),
|
|
2873
|
+
excludedTurns: listFact(excludedTurns, excludedTurns.flatMap((entry) => entry.spanIds)),
|
|
2874
|
+
turnsByActor: listFact(
|
|
2875
|
+
[...actorCounts].sort(([left], [right]) => left.localeCompare(right)).map(([actor, spanIds]) => ({ actor, turns: spanIds.length, spanIds })),
|
|
2876
|
+
promptSpans.map((span) => span.span_id)
|
|
2877
|
+
),
|
|
2878
|
+
finalMessages: listFact(finalMessages, finalMessages.map((entry) => entry.spanId)),
|
|
2879
|
+
changedFiles: changedFilesFact(files, truncatedInputs, unresolvedPaths),
|
|
2880
|
+
commands: commandsOf(invoked),
|
|
2881
|
+
firstRecordAt: firstSpan ? { value: firstSpan.start_time, spanIds: [firstSpan.span_id], unavailable: null } : { value: null, spanIds: [], unavailable: "no span of this session's own records carries a parseable start time" },
|
|
2882
|
+
lastRecordAt: lastSpan ? { value: lastSpan.end_time, spanIds: [lastSpan.span_id], unavailable: null } : { value: null, spanIds: [], unavailable: "no span of this session's own records carries a parseable end time" },
|
|
2883
|
+
// rankFirstFailure throws on an empty list (distinct from "every span is
|
|
2884
|
+
// UNSET", which it reports as status 'unknown'); a trace whose spans are
|
|
2885
|
+
// all traces.span.subagent — every record folded into subagents' own
|
|
2886
|
+
// reports — legitimately has no records of its own to rank.
|
|
2887
|
+
firstFailure: failedSpan ? { ...firstFailure, at: failedSpan.end_time } : firstFailure,
|
|
2888
|
+
tokens,
|
|
2889
|
+
// Priced by `priceReport`, which loads the price table only when a session needs it.
|
|
2890
|
+
cost: sessionCost(tokens, void 0),
|
|
2891
|
+
tokenTotal: tokens.input === null ? { value: null, spanIds: [], unavailable: tokens.unavailable } : { value: tokens.input + (tokens.output ?? 0), spanIds: tokens.spanIds, unavailable: null },
|
|
2892
|
+
agent
|
|
2893
|
+
};
|
|
2894
|
+
if (options.includeContent !== false) return { facts, spans, turns: typedTurns, operating };
|
|
2895
|
+
return { facts: withholdContent(facts), spans, turns: [], operating };
|
|
2896
|
+
}
|
|
2897
|
+
function agentFact(spans, sessionId, harness, lookup) {
|
|
2898
|
+
const root = spans.find((span) => span.parent_span_id === null);
|
|
2899
|
+
const fromSpans = root ? identityFieldsOf(root.attributes) : {};
|
|
2900
|
+
const outside = sessionId !== null && lookup ? lookup({ sessionId, harness }) : void 0;
|
|
2901
|
+
const value = mergeAgentIdentity([{ source: "spans", fields: fromSpans }, ...outside?.sources ?? []]);
|
|
2902
|
+
if (value !== null) {
|
|
2903
|
+
return { value, spanIds: root && Object.keys(fromSpans).length > 0 ? [root.span_id] : [], unavailable: null };
|
|
2904
|
+
}
|
|
2905
|
+
return {
|
|
2906
|
+
value: null,
|
|
2907
|
+
spanIds: [],
|
|
2908
|
+
unavailable: `no root span carries ${AGENT_IDENTITY_KEYS.map(([, key]) => key).join(", ")}; ` + (outside?.reason ?? "no source outside the spans was read")
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
async function priceReport(report) {
|
|
2912
|
+
if (!report.sessions.some((session) => needsPrice(session.tokens))) return report;
|
|
2913
|
+
const price = await routerPrices();
|
|
2914
|
+
return { ...report, sessions: report.sessions.map((session) => ({ ...session, cost: sessionCost(session.tokens, price) })) };
|
|
2915
|
+
}
|
|
2916
|
+
function buildSessionFactsReport(spans, options = {}) {
|
|
2917
|
+
const builder = createSessionFactsReportBuilder(options);
|
|
2918
|
+
builder.add(spans);
|
|
2919
|
+
return builder.build();
|
|
2920
|
+
}
|
|
2921
|
+
var WINDOW_SESSION_RULE = "a session is selected when at least one of its records starts or ends inside [since, until)";
|
|
2922
|
+
var WINDOW_TURN_RULE = "humanTurns, excludedTurns and turnsByActor keep only user.prompt turns that started inside [since, until); every other fact, and each turn's answered text, covers the whole session";
|
|
2923
|
+
var SWEEP_SESSION_RULE = "the sessions the selection named or found; one that could not be read is named here and skipped";
|
|
2924
|
+
var SWEEP_TURN_RULE = "every turn of each selected session";
|
|
2925
|
+
function createSessionFactsReportBuilder(options = {}) {
|
|
2926
|
+
const bounds = windowBounds(options.window);
|
|
2927
|
+
const sessions = [];
|
|
2928
|
+
const digests = [];
|
|
2929
|
+
const outsideWindow = [];
|
|
2930
|
+
const unreadSessions = [];
|
|
2931
|
+
let spanCount = 0;
|
|
2932
|
+
let evidenceThrough = null;
|
|
2933
|
+
const select = (group) => {
|
|
2934
|
+
const digest = createHash("sha256");
|
|
2935
|
+
for (const span of [...group.spans].sort((a, b) => a.span_id.localeCompare(b.span_id))) {
|
|
2936
|
+
digest.update(canonicalJson(span)).update("\n");
|
|
2937
|
+
for (const time of [span.start_time, span.end_time]) {
|
|
2938
|
+
if (Number.isFinite(Date.parse(time)) && (evidenceThrough === null || Date.parse(time) > Date.parse(evidenceThrough))) evidenceThrough = time;
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
digests.push({ traceId: group.traceId, digest: digest.digest("hex") });
|
|
2942
|
+
spanCount += group.spans.length;
|
|
2943
|
+
const reading = readSession(group.traceId, group.sessionId, group.spans, options, bounds);
|
|
2944
|
+
sessions.push(reading.facts);
|
|
2945
|
+
options.onSession?.(reading);
|
|
2946
|
+
};
|
|
2947
|
+
return {
|
|
2948
|
+
add(spans) {
|
|
2949
|
+
for (const group of groupByTrace(spans)) {
|
|
2950
|
+
if (bounds === null && options.nameUnread !== true) {
|
|
2951
|
+
select(group);
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
const records = group.spans.filter(isRecordSpan);
|
|
2955
|
+
if (records.length === 0) {
|
|
2956
|
+
const unread = group.spans.find((span) => typeof span.attributes[CORRUPTION_COUNT_ATTR] === "number");
|
|
2957
|
+
unreadSessions.push({
|
|
2958
|
+
sessionId: group.sessionId,
|
|
2959
|
+
traceId: group.traceId,
|
|
2960
|
+
source: null,
|
|
2961
|
+
reason: `no record could be read (${unread ? `${unread.attributes[CORRUPTION_COUNT_ATTR]} unread record(s)` : "no record spans"})` + (bounds === null ? "" : ", so whether the session holds a record inside the window is unknown")
|
|
2962
|
+
});
|
|
2963
|
+
continue;
|
|
2964
|
+
}
|
|
2965
|
+
if (bounds === null || records.some((span) => insideWindow(bounds, span.start_time) || insideWindow(bounds, span.end_time))) select(group);
|
|
2966
|
+
else outsideWindow.push(group.sessionId ?? group.traceId);
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
addUnread(session) {
|
|
2970
|
+
unreadSessions.push(session);
|
|
2971
|
+
},
|
|
2972
|
+
build() {
|
|
2973
|
+
const digest = createHash("sha256");
|
|
2974
|
+
for (const entry of [...digests].sort((a, b) => a.traceId.localeCompare(b.traceId) || a.digest.localeCompare(b.digest))) {
|
|
2975
|
+
digest.update(`${entry.traceId}
|
|
2976
|
+
${entry.digest}
|
|
2977
|
+
`);
|
|
2978
|
+
}
|
|
2979
|
+
return {
|
|
2980
|
+
schemaVersion: 1,
|
|
2981
|
+
kind: "traces.session_facts_report",
|
|
2982
|
+
measurementVersion: SESSION_FACTS_MEASUREMENT_VERSION,
|
|
2983
|
+
sourceDigest: "sha256:" + digest.digest("hex"),
|
|
2984
|
+
evidenceThrough,
|
|
2985
|
+
contentIncluded: options.includeContent !== false,
|
|
2986
|
+
generatedAt: options.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2987
|
+
harness: options.harness ?? null,
|
|
2988
|
+
spanCount,
|
|
2989
|
+
selection: bounds === null && unreadSessions.length === 0 ? null : {
|
|
2990
|
+
since: bounds?.since ?? null,
|
|
2991
|
+
until: bounds?.until ?? null,
|
|
2992
|
+
sessionRule: bounds === null ? SWEEP_SESSION_RULE : WINDOW_SESSION_RULE,
|
|
2993
|
+
turnRule: bounds === null ? SWEEP_TURN_RULE : WINDOW_TURN_RULE,
|
|
2994
|
+
outsideWindow: [...outsideWindow].sort(compareIds),
|
|
2995
|
+
unreadSessions: [...unreadSessions]
|
|
2996
|
+
},
|
|
2997
|
+
// In the order they were selected: a named session first, each spawned
|
|
2998
|
+
// child after it in the order it started.
|
|
2999
|
+
sessions: [...sessions]
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
var SHEET_TEXT_CAP = 100;
|
|
3005
|
+
function sheetText(text, cap = SHEET_TEXT_CAP) {
|
|
3006
|
+
const line = text.trim().split("\n", 1)[0].replace(/\s+/gu, " ");
|
|
3007
|
+
return line.length > cap || text.trim().includes("\n") ? `${line.slice(0, cap - 1)}\u2026` : line;
|
|
3008
|
+
}
|
|
3009
|
+
function utcMinute(iso) {
|
|
3010
|
+
const ms = Date.parse(iso);
|
|
3011
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString().slice(5, 16).replace("T", " ") : iso;
|
|
3012
|
+
}
|
|
3013
|
+
function minutesText(minutes2) {
|
|
3014
|
+
if (minutes2 > 0 && minutes2 < 1) return `${Math.round(minutes2 * 60)} s`;
|
|
3015
|
+
return minutes2 < 60 ? `${Math.round(minutes2)} min` : `${(minutes2 / 60).toFixed(1)} h`;
|
|
3016
|
+
}
|
|
3017
|
+
function plural(count2, noun) {
|
|
3018
|
+
return `${count2} ${noun}${count2 === 1 ? "" : "s"}`;
|
|
3019
|
+
}
|
|
3020
|
+
function topCounts(counts, keep = 3, note = () => "") {
|
|
3021
|
+
const rows = Object.entries(counts).sort(([leftName, left], [rightName, right]) => right - left || leftName.localeCompare(rightName));
|
|
3022
|
+
return rows.slice(0, keep).map(([name, count2]) => `${name} ${count2}${note(name)}`).join(", ") + (rows.length > keep ? ", \u2026" : "");
|
|
3023
|
+
}
|
|
3024
|
+
function failedNote(failed, inScripts) {
|
|
3025
|
+
const parts = [...failed > 0 ? [`${failed} failed`] : [], ...inScripts > 0 ? [`${inScripts} in scripts`] : []];
|
|
3026
|
+
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
3027
|
+
}
|
|
3028
|
+
var SHEET_COMMANDS = 5;
|
|
3029
|
+
function commandLine(facts) {
|
|
3030
|
+
const commands = facts.commands.value;
|
|
3031
|
+
if (commands === null) return `commands unknown: ${facts.commands.unavailable}`;
|
|
3032
|
+
if (commands.length === 0) return null;
|
|
3033
|
+
const runs = commands.reduce((sum2, row) => sum2 + row.runs, 0);
|
|
3034
|
+
const shown = commands.slice(0, SHEET_COMMANDS).map((row) => `${row.command} ${row.runs}${failedNote(row.failed, row.inScripts)}`);
|
|
3035
|
+
return `commands ${runs}: ${shown.join(", ")}${commands.length > SHEET_COMMANDS ? ", \u2026" : ""}`;
|
|
3036
|
+
}
|
|
3037
|
+
function renderSessionFacts(report) {
|
|
3038
|
+
const lines = [];
|
|
3039
|
+
const selection = report.selection;
|
|
3040
|
+
if (selection !== null || report.sessions.length !== 1) {
|
|
3041
|
+
const window = selection && (selection.since !== null || selection.until !== null) ? ` \xB7 window ${selection.since ?? "open"} \u2192 ${selection.until ?? "open"}, ${selection.outsideWindow.length} read with no record inside it` : "";
|
|
3042
|
+
const unread = selection?.unreadSessions ?? [];
|
|
3043
|
+
lines.push(`${plural(report.sessions.length, "session")}${window}${unread.length > 0 ? ` \xB7 ${unread.length} unreadable` : ""} \xB7 $0, no model call`);
|
|
3044
|
+
for (const entry of unread.slice(0, 3)) {
|
|
3045
|
+
lines.push(`unreadable ${entry.sessionId ?? entry.traceId ?? entry.source ?? "unknown"}: ${sheetText(entry.reason)}`);
|
|
3046
|
+
}
|
|
3047
|
+
if (unread.length > 3) lines.push(`\u2026 ${unread.length - 3} more unreadable: --format json | jq .selection.unreadSessions`);
|
|
3048
|
+
}
|
|
3049
|
+
for (const facts of report.sessions) {
|
|
3050
|
+
if (lines.length > 0) lines.push("");
|
|
3051
|
+
lines.push(...sessionSheet(facts));
|
|
3052
|
+
}
|
|
3053
|
+
return `${lines.join("\n")}
|
|
3054
|
+
`;
|
|
3055
|
+
}
|
|
3056
|
+
function sessionSheet(facts) {
|
|
3057
|
+
const lines = [sheetHeader(facts)];
|
|
3058
|
+
if (facts.agent.value) lines.push(`who ${renderAgent(facts.agent.value)}`);
|
|
3059
|
+
lines.push(turnLine(facts));
|
|
3060
|
+
const subagents = facts.subagents.value ?? [];
|
|
3061
|
+
const names = subagents.flatMap((entry) => entry.taskName === null ? [] : [
|
|
3062
|
+
`${sheetText(entry.taskName, 40)}${entry.agentType ? ` [${sheetText(entry.agentType, 40)}]` : ""}`
|
|
3063
|
+
]);
|
|
3064
|
+
const failedCalls = new Map(facts.measurements.tools.map((row) => [row.name, row.outcomes.error]));
|
|
3065
|
+
lines.push(
|
|
3066
|
+
`tools ${facts.toolCalls.value}${facts.toolCalls.value ? ` (${topCounts(facts.toolCallsByName.value ?? {}, 3, (name) => failedNote(failedCalls.get(name) ?? 0, 0))})` : ""} \xB7 subagents ${subagents.length}${names.length > 0 ? ` (${names.slice(0, 3).join(", ")}${names.length > 3 ? ", \u2026" : ""})` : ""}`
|
|
3067
|
+
);
|
|
3068
|
+
const subagentTools = facts.subagentTools.value ?? [];
|
|
3069
|
+
if (subagentTools.length > 0) {
|
|
3070
|
+
const calls = Object.fromEntries(subagentTools.map((row) => [row.name, row.calls]));
|
|
3071
|
+
const failed = new Map(subagentTools.map((row) => [row.name, row.failed]));
|
|
3072
|
+
lines.push(`subagent tools ${facts.subagentTools.spanIds.length} (${topCounts(calls, 3, (name) => failedNote(failed.get(name) ?? 0, 0))}), not in the tools count`);
|
|
3073
|
+
}
|
|
3074
|
+
const commands = commandLine(facts);
|
|
3075
|
+
if (commands !== null) lines.push(commands);
|
|
3076
|
+
const skills = Object.fromEntries(Object.entries(facts.measurements.skills).map(([name, use]) => [name, use.model + use.slash + use.read]));
|
|
3077
|
+
if (Object.keys(skills).length > 0) lines.push(`skills ${topCounts(skills, 5)}`);
|
|
3078
|
+
lines.push(`tokens ${renderTokens(facts.tokens, facts.cost)}`);
|
|
3079
|
+
lines.push(wasteLine(facts));
|
|
3080
|
+
lines.push(pullRequestLine(facts));
|
|
3081
|
+
lines.push(`first failure ${renderFirstFailure(facts.firstFailure)}`);
|
|
3082
|
+
const final = facts.finalMessages.value?.find((message) => message.task === null);
|
|
3083
|
+
if (final && final.text.trim().length > 0) lines.push(`final "${sheetText(final.text)}"`);
|
|
3084
|
+
if ((facts.unreadRecords.value ?? 0) > 0) {
|
|
3085
|
+
lines.push(`unread records ${facts.unreadRecords.value}: the facts above are computed from the rest`);
|
|
3086
|
+
}
|
|
3087
|
+
return lines;
|
|
3088
|
+
}
|
|
3089
|
+
function sheetHeader(facts) {
|
|
3090
|
+
const parts = [`${(facts.sessionId ?? facts.traceId).slice(0, 8)} ${facts.harness ?? "unknown harness"}`];
|
|
3091
|
+
const repo = facts.repo.value;
|
|
3092
|
+
const place = repo ? `${repo.repository.split("/").at(-1)}${repo.branch ? ` (${repo.branch})` : ""}` : facts.cwd.value?.split("/").filter(Boolean).at(-1);
|
|
3093
|
+
if (place) parts.push(place);
|
|
3094
|
+
const first = facts.firstRecordAt.value;
|
|
3095
|
+
const last = facts.lastRecordAt.value;
|
|
3096
|
+
if (first !== null && last !== null) {
|
|
3097
|
+
const [from, to] = [utcMinute(first), utcMinute(last)];
|
|
3098
|
+
parts.push(`${from}\u2192${from.slice(0, 5) === to.slice(0, 5) ? to.slice(6) : to} UTC`);
|
|
3099
|
+
}
|
|
3100
|
+
const active = facts.measurements.activeMinutes.value;
|
|
3101
|
+
if (active !== null) parts.push(`active ${minutesText(active)}`);
|
|
3102
|
+
if (facts.parentSessionId) parts.push(`child of ${facts.parentSessionId.slice(0, 8)}`);
|
|
3103
|
+
return parts.join(" \xB7 ");
|
|
3104
|
+
}
|
|
3105
|
+
function turnLine(facts) {
|
|
3106
|
+
const turns = facts.humanTurns.value;
|
|
3107
|
+
if (turns === null) return `human turns unknown: ${facts.humanTurns.unavailable}`;
|
|
3108
|
+
const excluded = {};
|
|
3109
|
+
for (const entry of facts.excludedTurns.value ?? []) excluded[entry.author] = (excluded[entry.author] ?? 0) + entry.turns;
|
|
3110
|
+
const signalled = (signal) => turns.filter((turn) => turn.signals.includes(signal)).length;
|
|
3111
|
+
return [
|
|
3112
|
+
`human turns ${turns.length}${Object.keys(excluded).length > 0 ? ` (excluded: ${topCounts(excluded, 6)})` : ""}`,
|
|
3113
|
+
`interrupts ${turns.filter((turn) => turn.interrupted).length}`,
|
|
3114
|
+
...signalled("rejected-tool") > 0 ? [`after a rejected tool ${signalled("rejected-tool")}`] : [],
|
|
3115
|
+
...signalled("after-claim") > 0 ? [`after a completion claim ${signalled("after-claim")}`] : []
|
|
3116
|
+
].join(" \xB7 ");
|
|
3117
|
+
}
|
|
3118
|
+
function wasteLine(facts) {
|
|
3119
|
+
const waste = facts.measurements.waste;
|
|
3120
|
+
if (waste.minutes === null) return `waste unknown: ${waste.unavailable}`;
|
|
3121
|
+
const parts = [];
|
|
3122
|
+
const streaks = waste.failingStreaks.value ?? [];
|
|
3123
|
+
const worst = [...streaks].sort((left, right) => right.minutes - left.minutes || right.runs - left.runs)[0];
|
|
3124
|
+
if (worst) {
|
|
3125
|
+
const command = worst.command === null ? `a ${worst.tool} command` : `\`${sheetText(worst.command, 60)}\``;
|
|
3126
|
+
parts.push(`${command} failed ${worst.runs}\xD7 in a row (${minutesText(worst.minutes)})${streaks.length > 1 ? ` + ${plural(streaks.length - 1, "more streak")}` : ""}`);
|
|
3127
|
+
}
|
|
3128
|
+
const stalls = waste.stalls.value ?? [];
|
|
3129
|
+
if (stalls.length > 0) {
|
|
3130
|
+
parts.push(`${plural(stalls.length, "stall")} \u2265 ${STALL_MINUTES} min (${minutesText(stalls.reduce((sum2, stall) => sum2 + stall.minutes, 0))})`);
|
|
3131
|
+
}
|
|
3132
|
+
if ((waste.rollbacks.value ?? 0) > 0) parts.push(`reverts ${waste.rollbacks.value}`);
|
|
3133
|
+
return `waste ${minutesText(waste.minutes)}${parts.length > 0 ? `: ${parts.join(" \xB7 ")}` : ""}`;
|
|
3134
|
+
}
|
|
3135
|
+
function pullRequestLine(facts) {
|
|
3136
|
+
const files = facts.changedFiles.value === null ? "" : ` \xB7 changed ${plural(facts.changedFiles.value.length, "file")}`;
|
|
3137
|
+
const prs = facts.pullRequests.value;
|
|
3138
|
+
if (prs === null) return `PRs unknown: ${facts.pullRequests.unavailable}${files}`;
|
|
3139
|
+
const named = (entries) => {
|
|
3140
|
+
const ids = entries.flatMap((entry) => entry.identifier === null ? [] : [/^\d+$/u.test(entry.identifier) ? `#${entry.identifier}` : entry.identifier]);
|
|
3141
|
+
const unnumbered = entries.length - ids.length;
|
|
3142
|
+
if (entries.length === 0) return "none";
|
|
3143
|
+
return [...ids, ...unnumbered > 0 ? [`${ids.length > 0 ? "+ " : ""}${unnumbered} unnumbered`] : []].join(" ");
|
|
3144
|
+
};
|
|
3145
|
+
return `PRs opened ${named(prs.created)} \xB7 merged ${named(prs.merged)}${files}`;
|
|
3146
|
+
}
|
|
3147
|
+
var MATCH_TEXT_CAP = 300;
|
|
3148
|
+
function matchExcerpt(text, pattern) {
|
|
3149
|
+
const flat = text.replace(/\s+/gu, " ").trim();
|
|
3150
|
+
if (flat.length <= MATCH_TEXT_CAP) return flat;
|
|
3151
|
+
const at = pattern.exec(flat)?.index ?? 0;
|
|
3152
|
+
let start = Math.max(0, Math.min(at - 100, flat.length - MATCH_TEXT_CAP));
|
|
3153
|
+
let end = start + MATCH_TEXT_CAP;
|
|
3154
|
+
if (start > 0) start += 1;
|
|
3155
|
+
if (end < flat.length) end -= 1;
|
|
3156
|
+
return `${start > 0 ? "\u2026" : ""}${flat.slice(start, end)}${end < flat.length ? "\u2026" : ""}`;
|
|
3157
|
+
}
|
|
3158
|
+
function sessionRows(spans, turns) {
|
|
3159
|
+
const rows = [];
|
|
3160
|
+
const byId = new Map(spans.map((span) => [span.span_id, span]));
|
|
3161
|
+
for (const turn of turns) {
|
|
3162
|
+
const span = byId.get(turn.spanId);
|
|
3163
|
+
if (turn.author === "human" && span) rows.push({ kind: "human", at: span.start_time, spanId: span.span_id, text: rawContent(span) });
|
|
3164
|
+
}
|
|
3165
|
+
for (const span of spans) {
|
|
3166
|
+
if (isSubagentSpan(span.attributes)) continue;
|
|
3167
|
+
if (isOwnAgentMessage(span)) {
|
|
3168
|
+
rows.push({ kind: "agent", at: span.start_time, spanId: span.span_id, text: rawContent(span) });
|
|
3169
|
+
continue;
|
|
3170
|
+
}
|
|
3171
|
+
if (!isToolSpan(span) || isSynthesizedSpan2(span)) continue;
|
|
3172
|
+
const call = callCommands(span);
|
|
3173
|
+
if (call === void 0) continue;
|
|
3174
|
+
call.commands.forEach((words, index) => {
|
|
3175
|
+
rows.push({ kind: "command", at: span.start_time, spanId: span.span_id, text: words.join(" "), failed: call.failed[index] ?? null });
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
return rows;
|
|
3179
|
+
}
|
|
3180
|
+
function matchRows(sessionId, rows, pattern, window) {
|
|
3181
|
+
const bounds = windowBounds(window);
|
|
3182
|
+
const matches = [];
|
|
3183
|
+
for (const row of rows) {
|
|
3184
|
+
if (!insideWindow(bounds, row.at)) continue;
|
|
3185
|
+
pattern.lastIndex = 0;
|
|
3186
|
+
if (!pattern.test(row.text)) continue;
|
|
3187
|
+
pattern.lastIndex = 0;
|
|
3188
|
+
matches.push({
|
|
3189
|
+
sessionId,
|
|
3190
|
+
kind: row.kind,
|
|
3191
|
+
at: row.at,
|
|
3192
|
+
spanId: row.spanId,
|
|
3193
|
+
text: matchExcerpt(row.text, pattern),
|
|
3194
|
+
...row.kind === "command" ? { failed: row.failed ?? null } : {}
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
return matches.sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
|
|
3198
|
+
}
|
|
3199
|
+
function sessionMatches(reading, pattern, window) {
|
|
3200
|
+
return matchRows(reading.facts.sessionId, sessionRows(reading.spans, reading.turns), pattern, window);
|
|
3201
|
+
}
|
|
3202
|
+
function readSessionIndex(spans, options = {}) {
|
|
3203
|
+
const groups = groupByTrace(spans);
|
|
3204
|
+
const group = groups.find((entry) => entry.sessionId === options.sessionId) ?? groups[0];
|
|
3205
|
+
const ordered = [...group?.spans ?? []].sort(byTraceOrder);
|
|
3206
|
+
const own = ordered.filter((span) => !isSubagentSpan(span.attributes));
|
|
3207
|
+
const typed = typedTurnsOf(ordered, options.state);
|
|
3208
|
+
const human = typed.turns.filter((turn) => turn.author === "human");
|
|
3209
|
+
let firstAt = null;
|
|
3210
|
+
let lastAt = null;
|
|
3211
|
+
for (const span of own) {
|
|
3212
|
+
if (Number.isFinite(Date.parse(span.start_time)) && (firstAt === null || Date.parse(span.start_time) < Date.parse(firstAt))) firstAt = span.start_time;
|
|
3213
|
+
if (Number.isFinite(Date.parse(span.end_time)) && (lastAt === null || Date.parse(span.end_time) > Date.parse(lastAt))) lastAt = span.end_time;
|
|
3214
|
+
}
|
|
3215
|
+
return {
|
|
3216
|
+
sessionId: group?.sessionId ?? null,
|
|
3217
|
+
parentSessionId: ordered.filter((span) => span.parent_span_id === null).map((span) => stringAttr2(span, "traces.parent_session_id")).find((id) => id !== null) ?? null,
|
|
3218
|
+
cwd: ordered.map((span) => stringAttr2(span, SESSION_CWD_ATTR)).find((cwd) => cwd !== null) ?? null,
|
|
3219
|
+
firstAt,
|
|
3220
|
+
lastAt,
|
|
3221
|
+
records: ordered.filter(isRecordSpan).length,
|
|
3222
|
+
humanTurns: human.map((turn) => turn.spanId),
|
|
3223
|
+
firstPrompt: human[0]?.text ?? null,
|
|
3224
|
+
rows: sessionRows(ordered, typed.turns),
|
|
3225
|
+
state: typed.state
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
function renderTokens(tokens, cost) {
|
|
3229
|
+
if (tokens.input === null) return `unknown: ${tokens.unavailable}`;
|
|
3230
|
+
const parts = [`in ${tokenCount(tokens.input)}`, `out ${tokenCount(tokens.output ?? 0)}`];
|
|
3231
|
+
if ((tokens.cacheRead ?? 0) > 0) parts.push(`cache read ${tokenCount(tokens.cacheRead)}`);
|
|
3232
|
+
if ((tokens.cacheWrite ?? 0) > 0) parts.push(`cache write ${tokenCount(tokens.cacheWrite)}`);
|
|
3233
|
+
parts.push(cost.usd === null ? `cost unknown (${cost.unavailable})` : `$${cost.usd.toFixed(cost.usd < 1 ? 4 : 2)} ${cost.label}`);
|
|
3234
|
+
return parts.join(" \xB7 ");
|
|
3235
|
+
}
|
|
3236
|
+
function tokenCount(value) {
|
|
3237
|
+
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
3238
|
+
if (value >= 1e3) return `${Math.round(value / 1e3)}k`;
|
|
3239
|
+
return String(value);
|
|
3240
|
+
}
|
|
3241
|
+
function renderAgent(agent) {
|
|
3242
|
+
const digest = agent.profileDigest?.replace(/^(sha256:)?([0-9a-f]{4})[0-9a-f]+([0-9a-f]{4})$/u, (_, prefix, head, tail) => `${prefix ?? ""}${head}\u2026${tail}`);
|
|
3243
|
+
const profile = [agent.profileName, digest].filter(Boolean).join(" ");
|
|
3244
|
+
return `${[profile, agent.model, agent.directiveVariant].filter(Boolean).join(" \xB7 ")} (from ${agent.source})`;
|
|
3245
|
+
}
|
|
3246
|
+
function renderFirstFailure(failure) {
|
|
3247
|
+
if (failure.status === "unknown") return `unknown: ${sheetText(failure.reason)}`;
|
|
3248
|
+
if (failure.status === "none") return `none: ${sheetText(failure.reason)}`;
|
|
3249
|
+
if (failure.status === "ambiguous") return `ambiguous ${failure.stage}: ${sheetText(failure.reason)} (spans ${failure.candidates.join(", ")})`;
|
|
3250
|
+
const when = failure.at ? `${utcMinute(failure.at).slice(6)} ` : "";
|
|
3251
|
+
const message = failure.message ? `: ${sheetText(failure.message, 80)}` : "";
|
|
3252
|
+
return `${when}${failure.name}${message} (blame ${failure.blame}, span ${failure.spanId})`;
|
|
3253
|
+
}
|
|
3254
|
+
var PREPARED_CONTEXT_BYTE_CEILING = 3e4;
|
|
3255
|
+
function renderSessionFactsContext(report, options = {}) {
|
|
3256
|
+
const ceiling = options.byteCeiling ?? PREPARED_CONTEXT_BYTE_CEILING;
|
|
3257
|
+
const header = "SESSION FACTS (deterministic, computed from the spans, no model call). Each fact names the span ids it came from: cite those spans, never this sheet. A null value carries the reason the spans cannot support it \u2014 do not replace it with a guess.";
|
|
3258
|
+
let sessions = report.sessions.map(compactFacts);
|
|
3259
|
+
const encode = (dropped2) => `${header}
|
|
3260
|
+
${JSON.stringify({ sessions, ...dropped2.length ? { omitted_fields: dropped2 } : {} })}`;
|
|
3261
|
+
const dropped = [];
|
|
3262
|
+
const sheds = [
|
|
3263
|
+
["human_turn_answered", (facts) => {
|
|
3264
|
+
for (const turn of facts.human_turns) delete turn.answered;
|
|
3265
|
+
}],
|
|
3266
|
+
["human_turn_text", (facts) => {
|
|
3267
|
+
for (const turn of facts.human_turns) delete turn.text;
|
|
3268
|
+
}],
|
|
3269
|
+
["final_message_text", (facts) => {
|
|
3270
|
+
for (const message of facts.final_messages) delete message.text;
|
|
3271
|
+
}],
|
|
3272
|
+
["pull_request_commands", (facts) => {
|
|
3273
|
+
for (const entry of [...facts.pull_requests.created ?? [], ...facts.pull_requests.merged ?? []]) {
|
|
3274
|
+
delete entry.command;
|
|
3275
|
+
delete entry.span_ids;
|
|
3276
|
+
}
|
|
3277
|
+
}],
|
|
3278
|
+
["tool_call_span_ids", (facts) => {
|
|
3279
|
+
facts.tool_calls.span_ids = [];
|
|
3280
|
+
}],
|
|
3281
|
+
["measurements", (facts) => {
|
|
3282
|
+
delete facts.measurements;
|
|
3283
|
+
}],
|
|
3284
|
+
["tool_calls_by_name", (facts) => {
|
|
3285
|
+
delete facts.tool_calls_by_name;
|
|
3286
|
+
}],
|
|
3287
|
+
["excluded_turn_span_ids", (facts) => {
|
|
3288
|
+
facts.excluded_turns = facts.excluded_turns?.map((entry) => {
|
|
3289
|
+
const { span_ids: _dropped, ...rest } = entry;
|
|
3290
|
+
return rest;
|
|
3291
|
+
});
|
|
3292
|
+
}],
|
|
3293
|
+
["changed_files", (facts) => {
|
|
3294
|
+
facts.changed_files = { count: facts.changed_files.count };
|
|
3295
|
+
}],
|
|
3296
|
+
["turns_by_actor", (facts) => {
|
|
3297
|
+
delete facts.turns_by_actor;
|
|
3298
|
+
}],
|
|
3299
|
+
["pull_request_evidence", (facts) => {
|
|
3300
|
+
for (const entry of [...facts.pull_requests.created ?? [], ...facts.pull_requests.merged ?? []]) {
|
|
3301
|
+
delete entry.evidence;
|
|
3302
|
+
}
|
|
3303
|
+
}],
|
|
3304
|
+
["excluded_turns", (facts) => {
|
|
3305
|
+
delete facts.excluded_turns;
|
|
3306
|
+
}],
|
|
3307
|
+
["subagents", (facts) => {
|
|
3308
|
+
facts.subagents = { count: facts.subagents.count };
|
|
3309
|
+
}],
|
|
3310
|
+
["final_messages", (facts) => {
|
|
3311
|
+
facts.final_messages = [];
|
|
3312
|
+
}],
|
|
3313
|
+
["human_turns", (facts) => {
|
|
3314
|
+
facts.human_turns = [];
|
|
3315
|
+
}]
|
|
3316
|
+
];
|
|
3317
|
+
for (const [name, shed] of sheds) {
|
|
3318
|
+
if (Buffer.byteLength(encode(dropped)) <= ceiling) break;
|
|
3319
|
+
for (const facts of sessions) shed(facts);
|
|
3320
|
+
dropped.push(name);
|
|
3321
|
+
}
|
|
3322
|
+
let text = encode(dropped);
|
|
3323
|
+
while (Buffer.byteLength(text) > ceiling && sessions.length > 1) {
|
|
3324
|
+
sessions = sessions.slice(0, Math.max(1, Math.floor(sessions.length / 2)));
|
|
3325
|
+
text = encode([...dropped, `sessions_beyond_${sessions.length}`]);
|
|
3326
|
+
}
|
|
3327
|
+
if (Buffer.byteLength(text) <= ceiling) return text;
|
|
3328
|
+
const refusal = `${header}
|
|
3329
|
+
${JSON.stringify({ sessions: [], omitted_fields: ["all: the byte ceiling is below one session record"] })}`;
|
|
3330
|
+
return Buffer.byteLength(refusal) <= ceiling ? refusal : "";
|
|
3331
|
+
}
|
|
3332
|
+
function factJson(fact) {
|
|
3333
|
+
return fact.value === null ? { value: null, unavailable: fact.unavailable } : { value: fact.value, span_ids: fact.spanIds };
|
|
3334
|
+
}
|
|
3335
|
+
function compactPullRequests(fact) {
|
|
3336
|
+
if (fact.value === null) return { value: null, unavailable: fact.unavailable ?? "unread" };
|
|
3337
|
+
const entry = (pr) => ({
|
|
3338
|
+
id: pr.identifier,
|
|
3339
|
+
number: pr.number,
|
|
3340
|
+
head_branch: pr.headBranch,
|
|
3341
|
+
evidence: pr.evidence,
|
|
3342
|
+
command: pr.command,
|
|
3343
|
+
span_ids: pr.spanIds,
|
|
3344
|
+
...pr.unavailable ? { unavailable: pr.unavailable } : {}
|
|
3345
|
+
});
|
|
3346
|
+
return {
|
|
3347
|
+
created: fact.value.created.map(entry),
|
|
3348
|
+
merged: fact.value.merged.map(entry),
|
|
3349
|
+
...fact.partial ? { partial: fact.partial } : {}
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
function compactFacts(facts) {
|
|
3353
|
+
return {
|
|
3354
|
+
session_id: facts.sessionId,
|
|
3355
|
+
trace_id: facts.traceId,
|
|
3356
|
+
cwd: factJson(facts.cwd),
|
|
3357
|
+
agent: factJson(facts.agent),
|
|
3358
|
+
spans: facts.spanCount,
|
|
3359
|
+
tool_calls: {
|
|
3360
|
+
count: facts.toolCalls.value,
|
|
3361
|
+
synthesized_excluded: facts.synthesizedToolSpans.value,
|
|
3362
|
+
subagent_excluded: facts.subagentTools.spanIds.length,
|
|
3363
|
+
span_ids: facts.toolCalls.spanIds
|
|
3364
|
+
},
|
|
3365
|
+
tool_calls_by_name: facts.toolCallsByName.value,
|
|
3366
|
+
subagents: {
|
|
3367
|
+
count: facts.subagents.value?.length ?? 0,
|
|
3368
|
+
entries: facts.subagents.value?.map((entry) => ({
|
|
3369
|
+
task_name: entry.taskName,
|
|
3370
|
+
...entry.taskNameUnavailable ? { task_name_unavailable: entry.taskNameUnavailable } : {},
|
|
3371
|
+
...entry.agentType ? { agent_type: entry.agentType } : {},
|
|
3372
|
+
started_at: entry.startedAt,
|
|
3373
|
+
status: entry.status,
|
|
3374
|
+
span_ids: entry.spanIds
|
|
3375
|
+
}))
|
|
3376
|
+
},
|
|
3377
|
+
pull_requests: compactPullRequests(facts.pullRequests),
|
|
3378
|
+
human_turns: (facts.humanTurns.value ?? []).map((turn) => ({
|
|
3379
|
+
at: turn.at,
|
|
3380
|
+
span_id: turn.spanId,
|
|
3381
|
+
text: turn.text,
|
|
3382
|
+
answered: turn.answered
|
|
3383
|
+
})),
|
|
3384
|
+
excluded_turns: facts.excludedTurns.value?.map((entry) => ({
|
|
3385
|
+
reason: entry.reason,
|
|
3386
|
+
turns: entry.turns,
|
|
3387
|
+
span_ids: entry.spanIds
|
|
3388
|
+
})),
|
|
3389
|
+
turns_by_actor: facts.turnsByActor.value?.map((entry) => ({
|
|
3390
|
+
actor: entry.actor,
|
|
3391
|
+
turns: entry.turns,
|
|
3392
|
+
span_ids: entry.spanIds
|
|
3393
|
+
})),
|
|
3394
|
+
final_messages: (facts.finalMessages.value ?? []).map((message) => ({
|
|
3395
|
+
task: message.task,
|
|
3396
|
+
at: message.at,
|
|
3397
|
+
span_id: message.spanId,
|
|
3398
|
+
text: message.text
|
|
3399
|
+
})),
|
|
3400
|
+
changed_files: {
|
|
3401
|
+
count: facts.changedFiles.value?.length ?? 0,
|
|
3402
|
+
paths: facts.changedFiles.value?.map((file) => ({
|
|
3403
|
+
path: file.path,
|
|
3404
|
+
operations: file.operations,
|
|
3405
|
+
span_ids: file.spanIds
|
|
3406
|
+
})),
|
|
3407
|
+
...facts.changedFiles.partial ? { partial: facts.changedFiles.partial } : {}
|
|
3408
|
+
},
|
|
3409
|
+
first_record_at: factJson(facts.firstRecordAt),
|
|
3410
|
+
last_record_at: factJson(facts.lastRecordAt),
|
|
3411
|
+
token_total: factJson(facts.tokenTotal),
|
|
3412
|
+
first_failure: compactFirstFailure(facts.firstFailure),
|
|
3413
|
+
measurements: facts.measurements
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
function compactFirstFailure(failure) {
|
|
3417
|
+
if (failure.status === "unknown") {
|
|
3418
|
+
return { status: "unknown", reason: failure.reason, unset_count: failure.unsetCount };
|
|
3419
|
+
}
|
|
3420
|
+
if (failure.status === "none") return { status: "none", reason: failure.reason };
|
|
3421
|
+
if (failure.status === "ambiguous") {
|
|
3422
|
+
return { status: "ambiguous", stage: failure.stage, reason: failure.reason, span_ids: failure.candidates };
|
|
3423
|
+
}
|
|
3424
|
+
return {
|
|
3425
|
+
status: "found",
|
|
3426
|
+
stage: failure.stage,
|
|
3427
|
+
span_id: failure.spanId,
|
|
3428
|
+
name: failure.name,
|
|
3429
|
+
kind: failure.kind,
|
|
3430
|
+
blame: failure.blame,
|
|
3431
|
+
message: failure.message
|
|
3432
|
+
};
|
|
3433
|
+
}
|
|
3434
|
+
function readSessionTurnGroups(spans, window) {
|
|
3435
|
+
const bounds = windowBounds(window);
|
|
3436
|
+
return groupByTrace(spans).flatMap((group) => {
|
|
3437
|
+
const records = group.spans.filter(isRecordSpan);
|
|
3438
|
+
if (records.length > 0 && bounds !== null && !records.some((span) => insideWindow(bounds, span.start_time) || insideWindow(bounds, span.end_time))) return [];
|
|
3439
|
+
const ordered = [...group.spans].sort(byTraceOrder);
|
|
3440
|
+
return [{
|
|
3441
|
+
sessionId: group.sessionId,
|
|
3442
|
+
records: records.length,
|
|
3443
|
+
harness: ordered.map((span) => stringAttr2(span, "service.name")).find((value) => value !== null) ?? null,
|
|
3444
|
+
cwd: ordered.map((span) => stringAttr2(span, SESSION_CWD_ATTR)).find((value) => value !== null) ?? null,
|
|
3445
|
+
turns: typedTurnsOf(ordered).turns.filter((turn) => insideWindow(bounds, turn.at))
|
|
3446
|
+
}];
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
// src/turn-index.ts
|
|
3451
|
+
function resumeOffset(previous, info) {
|
|
3452
|
+
if (!previous) return 0;
|
|
3453
|
+
if (previous.ino === info.ino && previous.size === info.size) return null;
|
|
3454
|
+
return previous.ino === info.ino && info.size >= previous.size && previous.offset <= info.size ? previous.offset : 0;
|
|
3455
|
+
}
|
|
3456
|
+
var CHUNK_BYTES = 8 * 1024 * 1024;
|
|
3457
|
+
var NEWLINE2 = 10;
|
|
3458
|
+
async function readCompleteLines(path, from, to, consume) {
|
|
3459
|
+
const handle = await open(path, "r");
|
|
3460
|
+
let bytes2 = 0;
|
|
3461
|
+
let end = from;
|
|
3462
|
+
let carry = Buffer.alloc(0);
|
|
3463
|
+
try {
|
|
3464
|
+
const chunk = Buffer.allocUnsafe(Math.min(CHUNK_BYTES, Math.max(0, to - from)));
|
|
3465
|
+
let position = from;
|
|
3466
|
+
while (position < to) {
|
|
3467
|
+
const { bytesRead } = await handle.read(chunk, 0, Math.min(chunk.length, to - position), position);
|
|
3468
|
+
if (bytesRead === 0) break;
|
|
3469
|
+
position += bytesRead;
|
|
3470
|
+
bytes2 += bytesRead;
|
|
3471
|
+
const buffer = carry.length > 0 ? Buffer.concat([carry, chunk.subarray(0, bytesRead)]) : chunk.subarray(0, bytesRead);
|
|
3472
|
+
const complete = buffer.lastIndexOf(NEWLINE2) + 1;
|
|
3473
|
+
if (complete > 0) {
|
|
3474
|
+
await consume(Buffer.from(buffer.subarray(0, complete)));
|
|
3475
|
+
end += complete;
|
|
3476
|
+
}
|
|
3477
|
+
carry = Buffer.from(buffer.subarray(complete));
|
|
3478
|
+
}
|
|
3479
|
+
} finally {
|
|
3480
|
+
await handle.close();
|
|
3481
|
+
}
|
|
3482
|
+
return { bytes: bytes2, end, rest: carry };
|
|
3483
|
+
}
|
|
3484
|
+
var INDEX_VERSION = 4;
|
|
3485
|
+
var EMPTY_SESSION = {
|
|
3486
|
+
sessionId: null,
|
|
3487
|
+
parentSessionId: null,
|
|
3488
|
+
cwd: null,
|
|
3489
|
+
firstAt: null,
|
|
3490
|
+
lastAt: null,
|
|
3491
|
+
records: 0,
|
|
3492
|
+
humanTurns: [],
|
|
3493
|
+
firstPrompt: null
|
|
3494
|
+
};
|
|
3495
|
+
var PIECE_WORKERS = 4;
|
|
3496
|
+
var LOCK_HEARTBEAT_MS = 2e4;
|
|
3497
|
+
var LOCK_STALE_MS = 12e4;
|
|
3498
|
+
function defaultTurnIndexDir() {
|
|
3499
|
+
const state = process.env.XDG_STATE_HOME ?? join3(homedir2(), ".local", "state");
|
|
3500
|
+
return join3(state, "traces", "turn-index");
|
|
3501
|
+
}
|
|
3502
|
+
function later(left, right) {
|
|
3503
|
+
if (left === null) return right;
|
|
3504
|
+
if (right === null) return left;
|
|
3505
|
+
return Date.parse(right) > Date.parse(left) ? right : left;
|
|
3506
|
+
}
|
|
3507
|
+
function earlier(left, right) {
|
|
3508
|
+
if (left === null) return right;
|
|
3509
|
+
if (right === null) return left;
|
|
3510
|
+
return Date.parse(right) < Date.parse(left) ? right : left;
|
|
3511
|
+
}
|
|
3512
|
+
var FIRST_PROMPT_CHARS = 200;
|
|
3513
|
+
function extend(session, read) {
|
|
3514
|
+
const prompt = session.firstPrompt ?? read.firstPrompt;
|
|
3515
|
+
return {
|
|
3516
|
+
sessionId: session.sessionId ?? read.sessionId,
|
|
3517
|
+
parentSessionId: session.parentSessionId ?? read.parentSessionId,
|
|
3518
|
+
cwd: session.cwd ?? read.cwd,
|
|
3519
|
+
firstAt: earlier(session.firstAt, read.firstAt),
|
|
3520
|
+
lastAt: later(session.lastAt, read.lastAt),
|
|
3521
|
+
records: session.records + read.records,
|
|
3522
|
+
humanTurns: [.../* @__PURE__ */ new Set([...session.humanTurns, ...read.humanTurns])],
|
|
3523
|
+
firstPrompt: prompt === null ? null : prompt.replace(/\s+/gu, " ").trim().slice(0, FIRST_PROMPT_CHARS)
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
function storedRows(rows) {
|
|
3527
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3528
|
+
return rows.map((row) => {
|
|
3529
|
+
const key = `${row.kind}\0${row.spanId}`;
|
|
3530
|
+
const n = seen.get(key) ?? 0;
|
|
3531
|
+
seen.set(key, n + 1);
|
|
3532
|
+
return row.kind === "command" ? [row.kind, row.at, row.spanId, n, row.text, row.failed ?? null] : [row.kind, row.at, row.spanId, n, row.text];
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
function serialize(rows) {
|
|
3536
|
+
return rows.map((row) => `${JSON.stringify(row)}
|
|
3537
|
+
`).join("");
|
|
3538
|
+
}
|
|
3539
|
+
function parseLines(buffer) {
|
|
3540
|
+
const records = [];
|
|
3541
|
+
const bytes2 = [];
|
|
3542
|
+
let start = 0;
|
|
3543
|
+
while (start < buffer.length) {
|
|
3544
|
+
let end = buffer.indexOf(NEWLINE2, start);
|
|
3545
|
+
if (end === -1) end = buffer.length;
|
|
3546
|
+
records.push(parseRecord(buffer.toString("utf8", start, end)));
|
|
3547
|
+
bytes2.push(end + 1 - start);
|
|
3548
|
+
start = end + 1;
|
|
3549
|
+
}
|
|
3550
|
+
return { records, bytes: bytes2 };
|
|
3551
|
+
}
|
|
3552
|
+
function parseRecord(line) {
|
|
3553
|
+
const text = line.replace(/\r$/u, "");
|
|
3554
|
+
if (text.trim().length === 0) return null;
|
|
3555
|
+
try {
|
|
3556
|
+
const value = JSON.parse(text);
|
|
3557
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
3558
|
+
} catch {
|
|
3559
|
+
return null;
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
var MARK_BYTES = 4096;
|
|
3563
|
+
async function markOf(path, offset) {
|
|
3564
|
+
if (offset === 0) return "";
|
|
3565
|
+
const handle = await open(path, "r");
|
|
3566
|
+
try {
|
|
3567
|
+
const hash = createHash2("sha256");
|
|
3568
|
+
for (const start of [0, Math.max(0, offset - MARK_BYTES)]) {
|
|
3569
|
+
const buffer = Buffer.alloc(Math.min(MARK_BYTES, offset - start));
|
|
3570
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
|
|
3571
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
3572
|
+
}
|
|
3573
|
+
return hash.digest("hex").slice(0, 32);
|
|
3574
|
+
} finally {
|
|
3575
|
+
await handle.close();
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
function errorText(error) {
|
|
3579
|
+
return error instanceof Error ? error.message : String(error);
|
|
3580
|
+
}
|
|
3581
|
+
var TurnIndex = class _TurnIndex {
|
|
3582
|
+
constructor(dir) {
|
|
3583
|
+
this.dir = dir;
|
|
3584
|
+
}
|
|
3585
|
+
dir;
|
|
3586
|
+
files = {};
|
|
3587
|
+
/** Whole reads, one after another. */
|
|
3588
|
+
queue = Promise.resolve();
|
|
3589
|
+
changed = false;
|
|
3590
|
+
/** What this process writes into the lock, so it removes only its own. */
|
|
3591
|
+
token = `${process.pid} ${randomUUID()}`;
|
|
3592
|
+
heartbeat;
|
|
3593
|
+
/** Open the index at `dir`, waiting while another traces process is refreshing it. */
|
|
3594
|
+
static async open(dir = defaultTurnIndexDir()) {
|
|
3595
|
+
const index = new _TurnIndex(dir);
|
|
3596
|
+
await mkdir2(join3(dir, "rows"), { recursive: true });
|
|
3597
|
+
await index.lock();
|
|
3598
|
+
try {
|
|
3599
|
+
const parsed = JSON.parse(await readFile2(join3(dir, "index.json"), "utf8"));
|
|
3600
|
+
if (parsed.version === INDEX_VERSION && typeof parsed.files === "object" && parsed.files !== null) index.files = parsed.files;
|
|
3601
|
+
} catch {
|
|
3602
|
+
}
|
|
3603
|
+
return index;
|
|
3604
|
+
}
|
|
3605
|
+
/** Open the index, run `use` on it, and close it whatever `use` does. */
|
|
3606
|
+
static async with(use, dir) {
|
|
3607
|
+
const index = await _TurnIndex.open(dir);
|
|
3608
|
+
try {
|
|
3609
|
+
return await use(index);
|
|
3610
|
+
} finally {
|
|
3611
|
+
await index.close();
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
get lockPath() {
|
|
3615
|
+
return join3(this.dir, "lock");
|
|
3616
|
+
}
|
|
3617
|
+
async lock() {
|
|
3618
|
+
for (; ; ) {
|
|
3619
|
+
try {
|
|
3620
|
+
const handle = await open(this.lockPath, "wx");
|
|
3621
|
+
await handle.writeFile(this.token);
|
|
3622
|
+
await handle.close();
|
|
3623
|
+
this.heartbeat = setInterval(() => {
|
|
3624
|
+
const now = /* @__PURE__ */ new Date();
|
|
3625
|
+
utimes(this.lockPath, now, now).catch(() => {
|
|
3626
|
+
});
|
|
3627
|
+
}, LOCK_HEARTBEAT_MS);
|
|
3628
|
+
this.heartbeat.unref();
|
|
3629
|
+
return;
|
|
3630
|
+
} catch (error) {
|
|
3631
|
+
if (error.code !== "EEXIST") throw error;
|
|
3632
|
+
}
|
|
3633
|
+
const stale = await this.staleLock();
|
|
3634
|
+
if (stale === null) await sleep(100);
|
|
3635
|
+
else await this.removeStale(stale);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
/**
|
|
3639
|
+
* The inode of the lock when the process that wrote it left it behind: its
|
|
3640
|
+
* pid is gone, or it went untouched for {@link LOCK_STALE_MS}. Null while a
|
|
3641
|
+
* live process holds it, or once it is gone.
|
|
3642
|
+
*/
|
|
3643
|
+
async staleLock() {
|
|
3644
|
+
let handle;
|
|
3645
|
+
try {
|
|
3646
|
+
handle = await open(this.lockPath, "r");
|
|
3647
|
+
} catch {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
try {
|
|
3651
|
+
const [info, text] = await Promise.all([handle.stat(), handle.readFile("utf8")]);
|
|
3652
|
+
if (Date.now() - info.mtimeMs > LOCK_STALE_MS) return info.ino;
|
|
3653
|
+
const pid = Number(text.split(" ")[0]);
|
|
3654
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
3655
|
+
try {
|
|
3656
|
+
process.kill(pid, 0);
|
|
3657
|
+
return null;
|
|
3658
|
+
} catch (error) {
|
|
3659
|
+
return error.code === "ESRCH" ? info.ino : null;
|
|
3660
|
+
}
|
|
3661
|
+
} finally {
|
|
3662
|
+
await handle.close();
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Remove the stale lock `ino`, and only that one: two processes can find
|
|
3667
|
+
* the same lock stale, and the second must not remove the lock the first
|
|
3668
|
+
* then took. The lock is moved aside first; when what moved is another
|
|
3669
|
+
* process's new lock, it goes back.
|
|
3670
|
+
*/
|
|
3671
|
+
async removeStale(ino) {
|
|
3672
|
+
const aside = `${this.lockPath}.${this.token.replace(" ", ".")}`;
|
|
3673
|
+
try {
|
|
3674
|
+
await rename2(this.lockPath, aside);
|
|
3675
|
+
} catch {
|
|
3676
|
+
return;
|
|
3677
|
+
}
|
|
3678
|
+
if ((await stat2(aside)).ino !== ino) await link(aside, this.lockPath).catch(() => {
|
|
3679
|
+
});
|
|
3680
|
+
await unlink(aside);
|
|
3681
|
+
}
|
|
3682
|
+
/** Save what was read and let the next process in. */
|
|
3683
|
+
async close() {
|
|
3684
|
+
clearInterval(this.heartbeat);
|
|
3685
|
+
try {
|
|
3686
|
+
if (!this.changed) return;
|
|
3687
|
+
const temporary = join3(this.dir, `index.json.${process.pid}.tmp`);
|
|
3688
|
+
await writeFile2(temporary, JSON.stringify({ version: INDEX_VERSION, files: this.files }));
|
|
3689
|
+
await rename2(temporary, join3(this.dir, "index.json"));
|
|
3690
|
+
} finally {
|
|
3691
|
+
const held = await readFile2(this.lockPath, "utf8").catch(() => "");
|
|
3692
|
+
if (held === this.token) await unlink(this.lockPath).catch(() => {
|
|
3693
|
+
});
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
rowsPath(key) {
|
|
3697
|
+
return join3(this.dir, "rows", `${createHash2("sha256").update(key).digest("hex").slice(0, 24)}.jsonl`);
|
|
3698
|
+
}
|
|
3699
|
+
static keyOf(adapter, ref) {
|
|
3700
|
+
return `${adapter.harness}\0${ref.path}\0${ref.sessionId}`;
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* Bring each session up to date: read what it appended since the last
|
|
3704
|
+
* refresh, or all of it the first time. A session that cannot be read is
|
|
3705
|
+
* recorded with its reason, never dropped.
|
|
3706
|
+
*/
|
|
3707
|
+
async refresh(sessions) {
|
|
3708
|
+
const stats = { path: this.dir, sessions: sessions.length, sessionsRead: 0, bytesRead: 0, wholeReads: 0, rowsAdded: 0 };
|
|
3709
|
+
let next = 0;
|
|
3710
|
+
const worker = async () => {
|
|
3711
|
+
while (next < sessions.length) {
|
|
3712
|
+
const { adapter, ref } = sessions[next++];
|
|
3713
|
+
const read = await this.refreshOne(adapter, ref);
|
|
3714
|
+
if (read === null) continue;
|
|
3715
|
+
this.changed = true;
|
|
3716
|
+
stats.sessionsRead += 1;
|
|
3717
|
+
stats.bytesRead += read.bytes;
|
|
3718
|
+
stats.rowsAdded += read.rows;
|
|
3719
|
+
if (read.whole) stats.wholeReads += 1;
|
|
3720
|
+
}
|
|
3721
|
+
};
|
|
3722
|
+
await Promise.all(Array.from({ length: PIECE_WORKERS }, worker));
|
|
3723
|
+
return stats;
|
|
3724
|
+
}
|
|
3725
|
+
async refreshOne(adapter, ref) {
|
|
3726
|
+
const key = _TurnIndex.keyOf(adapter, ref);
|
|
3727
|
+
let info;
|
|
3728
|
+
try {
|
|
3729
|
+
info = await stat2(ref.path);
|
|
3730
|
+
} catch (error) {
|
|
3731
|
+
const entry = this.blank({ ino: 0, size: 0, mtimeMs: 0 }, "whole");
|
|
3732
|
+
entry.unread = errorText(error);
|
|
3733
|
+
this.files[key] = entry;
|
|
3734
|
+
return { bytes: 0, rows: 0, whole: false };
|
|
3735
|
+
}
|
|
3736
|
+
const previous = this.files[key];
|
|
3737
|
+
const pieces = readsStoreRoots(adapter) && adapter.readsPieces(ref) && previous?.mode !== "whole";
|
|
3738
|
+
if (pieces) {
|
|
3739
|
+
try {
|
|
3740
|
+
return await this.readPieces(key, adapter, ref, info, previous);
|
|
3741
|
+
} catch {
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
const version = ref.path.endsWith(".db") ? `m${ref.mtimeMs}` : `${info.ino}:${info.size}:${info.mtimeMs}`;
|
|
3745
|
+
if (previous?.mode === "whole" && previous.version === version) return null;
|
|
3746
|
+
return this.readWhole(key, adapter, ref, info, version);
|
|
3747
|
+
}
|
|
3748
|
+
blank(info, mode) {
|
|
3749
|
+
return {
|
|
3750
|
+
mode,
|
|
3751
|
+
ino: info.ino,
|
|
3752
|
+
size: info.size,
|
|
3753
|
+
mtimeMs: info.mtimeMs,
|
|
3754
|
+
version: "",
|
|
3755
|
+
offset: 0,
|
|
3756
|
+
mark: "",
|
|
3757
|
+
carry: null,
|
|
3758
|
+
turn: TURN_FOLD_START,
|
|
3759
|
+
session: EMPTY_SESSION,
|
|
3760
|
+
tail: null,
|
|
3761
|
+
unread: null
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
/** Read a session whole, as `facts <ref>` reads it, replacing every row it had. */
|
|
3765
|
+
async readWhole(key, adapter, ref, info, version) {
|
|
3766
|
+
const entry = this.blank(info, "whole");
|
|
3767
|
+
entry.version = version;
|
|
3768
|
+
const read = this.queue.then(async () => {
|
|
3769
|
+
let rows = [];
|
|
3770
|
+
try {
|
|
3771
|
+
const read2 = readSessionIndex(await readSessionSpans(adapter, ref), { sessionId: ref.sessionId });
|
|
3772
|
+
entry.session = extend(EMPTY_SESSION, read2);
|
|
3773
|
+
entry.turn = read2.state;
|
|
3774
|
+
rows = storedRows(read2.rows);
|
|
3775
|
+
if (read2.records === 0) entry.unread = "no record could be read";
|
|
3776
|
+
} catch (error) {
|
|
3777
|
+
entry.unread = errorText(error);
|
|
3778
|
+
}
|
|
3779
|
+
await writeFile2(this.rowsPath(key), serialize(rows));
|
|
3780
|
+
this.files[key] = entry;
|
|
3781
|
+
return rows.length;
|
|
3782
|
+
});
|
|
3783
|
+
this.queue = read.then(() => {
|
|
3784
|
+
}, () => {
|
|
3785
|
+
});
|
|
3786
|
+
return { bytes: 0, rows: await read, whole: true };
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Read what a Claude Code or Codex file appended since `previous`: commit
|
|
3790
|
+
* the rows of every piece up to the last response, and keep that response's
|
|
3791
|
+
* rows as the uncommitted tail, read again next time.
|
|
3792
|
+
*/
|
|
3793
|
+
async readPieces(key, adapter, ref, info, previous) {
|
|
3794
|
+
const old = previous?.mode === "pieces" ? previous : void 0;
|
|
3795
|
+
let from = resumeOffset(old, info);
|
|
3796
|
+
if (old && (from === null ? old.mtimeMs !== info.mtimeMs : from > 0) && old.mark !== await markOf(ref.path, old.offset)) from = 0;
|
|
3797
|
+
if (from === null) {
|
|
3798
|
+
if (old && old.mtimeMs !== info.mtimeMs) {
|
|
3799
|
+
this.files[key] = { ...old, mtimeMs: info.mtimeMs };
|
|
3800
|
+
this.changed = true;
|
|
3801
|
+
}
|
|
3802
|
+
return null;
|
|
3803
|
+
}
|
|
3804
|
+
const entry = from === 0 || !old ? this.blank(info, "pieces") : { ...old, tail: null };
|
|
3805
|
+
let carry = entry.carry ?? PIECE_START;
|
|
3806
|
+
let pending = { records: [], bytes: [] };
|
|
3807
|
+
const committed = [];
|
|
3808
|
+
const piece = async (records) => {
|
|
3809
|
+
const read2 = await adapter.readPiece(ref, records, carry);
|
|
3810
|
+
carry = read2.carry;
|
|
3811
|
+
const spans = validateOtlpSpans(read2.spans, `${adapter.harness} piece of ${ref.path}`);
|
|
3812
|
+
if (spans.length > 0) stampSessionIdentity(spans, ref.sessionId);
|
|
3813
|
+
return readSessionIndex(spans, { state: entry.turn });
|
|
3814
|
+
};
|
|
3815
|
+
const read = await readCompleteLines(ref.path, from, info.size, async (buffer) => {
|
|
3816
|
+
const lines = parseLines(buffer);
|
|
3817
|
+
const records = [...pending.records, ...lines.records];
|
|
3818
|
+
const bytes2 = [...pending.bytes, ...lines.bytes];
|
|
3819
|
+
if (carry.lines === 0 && !adapter.readsPieces(ref, records)) throw new Error("read whole");
|
|
3820
|
+
const cut = pieceBoundary(adapter.nativeHarness, records, carry);
|
|
3821
|
+
if (cut > 0) {
|
|
3822
|
+
const result = await piece(records.slice(0, cut));
|
|
3823
|
+
entry.session = extend(entry.session, result);
|
|
3824
|
+
entry.turn = result.state;
|
|
3825
|
+
committed.push(...storedRows(result.rows));
|
|
3826
|
+
entry.offset += bytes2.slice(0, cut).reduce((sum2, value) => sum2 + value, 0);
|
|
3827
|
+
entry.carry = carry;
|
|
3828
|
+
}
|
|
3829
|
+
pending = { records: records.slice(cut), bytes: bytes2.slice(cut) };
|
|
3830
|
+
});
|
|
3831
|
+
const last = read.rest.length > 0 ? parseRecord(read.rest.toString("utf8")) : null;
|
|
3832
|
+
const tailRecords = last ? [...pending.records, last] : pending.records;
|
|
3833
|
+
if (entry.offset === 0 && tailRecords.length > 0 && !adapter.readsPieces(ref, tailRecords)) throw new Error("read whole");
|
|
3834
|
+
if (tailRecords.length > 0) {
|
|
3835
|
+
const saved = carry;
|
|
3836
|
+
const result = await piece(tailRecords);
|
|
3837
|
+
carry = saved;
|
|
3838
|
+
entry.tail = { session: extend(EMPTY_SESSION, result), rows: storedRows(result.rows) };
|
|
3839
|
+
}
|
|
3840
|
+
entry.ino = info.ino;
|
|
3841
|
+
entry.size = info.size;
|
|
3842
|
+
entry.mtimeMs = info.mtimeMs;
|
|
3843
|
+
entry.mark = await markOf(ref.path, entry.offset);
|
|
3844
|
+
if (from === 0) await writeFile2(this.rowsPath(key), serialize(committed));
|
|
3845
|
+
else if (committed.length > 0) await appendFile(this.rowsPath(key), serialize(committed));
|
|
3846
|
+
this.files[key] = entry;
|
|
3847
|
+
return { bytes: read.bytes, rows: committed.length + (entry.tail?.rows.length ?? 0), whole: false };
|
|
3848
|
+
}
|
|
3849
|
+
/** The session as the index holds it: its committed rows' facts and its tail's, or why it could not be read. */
|
|
3850
|
+
session(adapter, ref) {
|
|
3851
|
+
const entry = this.files[_TurnIndex.keyOf(adapter, ref)];
|
|
3852
|
+
if (!entry) return null;
|
|
3853
|
+
const session = entry.tail ? extend(entry.session, entry.tail.session) : entry.session;
|
|
3854
|
+
return { session, unread: entry.unread ?? (session.records === 0 ? "no record could be read" : null) };
|
|
3855
|
+
}
|
|
3856
|
+
/**
|
|
3857
|
+
* A session's rows in the order a whole read lists them: its human turns,
|
|
3858
|
+
* then its agent messages and commands, each in trace order. A row read
|
|
3859
|
+
* twice (a harness that writes a record again) is listed once.
|
|
3860
|
+
*/
|
|
3861
|
+
async rows(adapter, ref) {
|
|
3862
|
+
const key = _TurnIndex.keyOf(adapter, ref);
|
|
3863
|
+
const entry = this.files[key];
|
|
3864
|
+
if (!entry) return [];
|
|
3865
|
+
let text = "";
|
|
3866
|
+
try {
|
|
3867
|
+
text = await readFile2(this.rowsPath(key), "utf8");
|
|
3868
|
+
} catch (error) {
|
|
3869
|
+
if (error.code !== "ENOENT") throw error;
|
|
3870
|
+
}
|
|
3871
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3872
|
+
const human = [];
|
|
3873
|
+
const other = [];
|
|
3874
|
+
const take = ([kind, at, spanId, n, body, failed]) => {
|
|
3875
|
+
const key2 = `${kind}\0${spanId}\0${n}`;
|
|
3876
|
+
if (seen.has(key2)) return;
|
|
3877
|
+
seen.add(key2);
|
|
3878
|
+
(kind === "human" ? human : other).push({ kind, at, spanId, text: body, ...kind === "command" ? { failed: failed ?? null } : {} });
|
|
3879
|
+
};
|
|
3880
|
+
let start = 0;
|
|
3881
|
+
while (start < text.length) {
|
|
3882
|
+
let end = text.indexOf("\n", start);
|
|
3883
|
+
if (end === -1) end = text.length;
|
|
3884
|
+
if (end > start) take(JSON.parse(text.slice(start, end)));
|
|
3885
|
+
start = end + 1;
|
|
3886
|
+
}
|
|
3887
|
+
for (const row of entry.tail?.rows ?? []) take(row);
|
|
3888
|
+
return [...human, ...other];
|
|
3889
|
+
}
|
|
3890
|
+
};
|
|
3891
|
+
|
|
3892
|
+
export {
|
|
3893
|
+
SessionWorkflowError,
|
|
3894
|
+
describeSessionRelationship,
|
|
3895
|
+
commandTextFromInput,
|
|
3896
|
+
commandLines,
|
|
3897
|
+
TurnIndex,
|
|
3898
|
+
defaultSkillUsageStorePath,
|
|
3899
|
+
SkillUsageStore,
|
|
3900
|
+
readInstalledSkills,
|
|
3901
|
+
buildSkillUsageReport,
|
|
3902
|
+
renderSkillUsage,
|
|
3903
|
+
CONTEXT_BANDS,
|
|
3904
|
+
modelCallMeasurement,
|
|
3905
|
+
sessionIdentityLookup,
|
|
3906
|
+
isSynthesizedSpan2 as isSynthesizedSpan,
|
|
3907
|
+
computeSessionFacts,
|
|
3908
|
+
priceReport,
|
|
3909
|
+
buildSessionFactsReport,
|
|
3910
|
+
createSessionFactsReportBuilder,
|
|
3911
|
+
sheetText,
|
|
3912
|
+
utcMinute,
|
|
3913
|
+
minutesText,
|
|
3914
|
+
topCounts,
|
|
3915
|
+
renderSessionFacts,
|
|
3916
|
+
matchRows,
|
|
3917
|
+
sessionMatches,
|
|
3918
|
+
renderFirstFailure,
|
|
3919
|
+
renderSessionFactsContext,
|
|
3920
|
+
readSessionTurnGroups
|
|
3921
|
+
};
|