@agent-finops/core 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/actionPlanner.d.ts +140 -0
- package/dist/actionPlanner.js +938 -0
- package/dist/actionVerification.d.ts +1240 -0
- package/dist/actionVerification.js +1028 -0
- package/dist/activitySnapshot.d.ts +142 -50
- package/dist/activitySnapshot.js +145 -6
- package/dist/activitySnapshotCache.d.ts +8 -1
- package/dist/activitySnapshotCache.js +103 -7
- package/dist/agentEconomicsReceipt.d.ts +74 -74
- package/dist/glance.d.ts +27 -1
- package/dist/glance.js +151 -12
- package/dist/index.d.ts +11 -2
- package/dist/index.js +10 -1
- package/dist/localAgentFormats/gemini.js +2 -2
- package/dist/localAgentFormats/registry.js +6 -2
- package/dist/localAgentFormats/runtimeRegistry.js +5 -2
- package/dist/localAgentFormats/types.d.ts +2 -1
- package/dist/localAgentLogs.d.ts +362 -3
- package/dist/localAgentLogs.js +1964 -165
- package/dist/modelPricing.d.ts +1 -1
- package/dist/modelPricing.js +1 -1
- package/dist/projectEconomics.d.ts +617 -0
- package/dist/projectEconomics.js +620 -0
- package/dist/projectEconomicsBuilder.d.ts +89 -0
- package/dist/projectEconomicsBuilder.js +473 -0
- package/dist/projectIndexStore.d.ts +545 -0
- package/dist/projectIndexStore.js +606 -0
- package/dist/providerConnectors.d.ts +59 -1
- package/dist/providerConnectors.js +175 -11
- package/dist/qualitativeIndexCache.d.ts +494 -0
- package/dist/qualitativeIndexCache.js +930 -0
- package/dist/resultCard.d.ts +350 -0
- package/dist/resultCard.js +604 -0
- package/dist/runtimeCommands.d.ts +21 -0
- package/dist/runtimeCommands.js +27 -0
- package/dist/scanGuard.d.ts +3 -1
- package/dist/scanGuard.js +164 -4
- package/dist/schema.d.ts +31 -31
- package/dist/sessionVitals.d.ts +145 -0
- package/dist/sessionVitals.js +521 -0
- package/dist/toolInvocations.d.ts +40 -1
- package/dist/toolInvocations.js +101 -20
- package/package.json +1 -1
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { dedupeCumulativeSessionCalls, sanitizeLocalActivityText } from "./localAgentLogs.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build one deterministic V0 row per Claude Code or Codex session.
|
|
5
|
+
*
|
|
6
|
+
* Token evidence fails closed at the session boundary: one unsupported,
|
|
7
|
+
* invalid, or mixed-scope call prevents a partial sum from looking complete.
|
|
8
|
+
*/
|
|
9
|
+
export function extractSessionVitalsV0(calls) {
|
|
10
|
+
const excludedCalls = {
|
|
11
|
+
unsupportedAgent: 0,
|
|
12
|
+
missingSessionIdentity: 0,
|
|
13
|
+
invalidTimestamp: 0
|
|
14
|
+
};
|
|
15
|
+
const eligible = [];
|
|
16
|
+
for (const call of calls) {
|
|
17
|
+
if (call.agent !== "claude-code" && call.agent !== "codex") {
|
|
18
|
+
excludedCalls.unsupportedAgent += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!call.sessionId) {
|
|
22
|
+
excludedCalls.missingSessionIdentity += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!validObservedTimestamp(call.timestamp)) {
|
|
26
|
+
excludedCalls.invalidTimestamp += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
eligible.push(call);
|
|
30
|
+
}
|
|
31
|
+
const deduplicated = dedupeCumulativeSessionCalls(eligible);
|
|
32
|
+
// A subagent transcript that shares its parent's sessionId is still its own
|
|
33
|
+
// run: merging it into the parent would emit a session shape no real task
|
|
34
|
+
// has (mixed models, mixed subagent flags, conflicting completion markers)
|
|
35
|
+
// and would permanently block cohort comparability for that agent. Calls
|
|
36
|
+
// without a subagent identity keep their existing grouping unchanged.
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
for (const call of deduplicated) {
|
|
39
|
+
const key = `${call.agent}\u0000${call.sessionId}\u0000${call.subagentId ?? ""}`;
|
|
40
|
+
groups.set(key, [...(groups.get(key) ?? []), call]);
|
|
41
|
+
}
|
|
42
|
+
const subagentCompletionLookup = subagentCompletionsByIdentity(eligible);
|
|
43
|
+
const sessions = [...groups.values()]
|
|
44
|
+
.map((group) => buildSessionVital(group, subagentCompletionLookup))
|
|
45
|
+
.sort((left, right) => left.observedFrom.localeCompare(right.observedFrom) ||
|
|
46
|
+
left.agent.localeCompare(right.agent) ||
|
|
47
|
+
left.sessionRef.localeCompare(right.sessionRef));
|
|
48
|
+
const sessionsWithObservedTokens = sessions.filter((session) => session.tokenEvidence.status === "observed").length;
|
|
49
|
+
return {
|
|
50
|
+
schemaVersion: 0,
|
|
51
|
+
sessions,
|
|
52
|
+
coverage: {
|
|
53
|
+
inputCalls: calls.length,
|
|
54
|
+
deduplicatedCalls: deduplicated.length,
|
|
55
|
+
eligibleCalls: eligible.length,
|
|
56
|
+
emittedSessions: sessions.length,
|
|
57
|
+
sessionsWithObservedTokens,
|
|
58
|
+
sessionsWithMissingTokens: sessions.length - sessionsWithObservedTokens,
|
|
59
|
+
excludedCalls
|
|
60
|
+
},
|
|
61
|
+
privacy: {
|
|
62
|
+
rawSessionIds: false,
|
|
63
|
+
promptOrResponseText: false,
|
|
64
|
+
absolutePaths: false,
|
|
65
|
+
uploaded: false
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function buildSessionVital(calls, subagentCompletionLookup) {
|
|
70
|
+
const ordered = calls.slice().sort((left, right) => Date.parse(left.timestamp) - Date.parse(right.timestamp));
|
|
71
|
+
const first = ordered[0];
|
|
72
|
+
const last = ordered[ordered.length - 1];
|
|
73
|
+
const explicitStarts = ordered
|
|
74
|
+
.map((call) => normalizedTimestamp(call.startedAt))
|
|
75
|
+
.filter((value) => Boolean(value));
|
|
76
|
+
const observedFrom = [...explicitStarts, normalizedTimestamp(first.timestamp)]
|
|
77
|
+
.sort((left, right) => Date.parse(left) - Date.parse(right))[0];
|
|
78
|
+
const observedTo = normalizedTimestamp(last.timestamp);
|
|
79
|
+
const durationMs = Date.parse(observedTo) - Date.parse(observedFrom);
|
|
80
|
+
const project = oneSafeProject(ordered);
|
|
81
|
+
const projectRef = oneProjectRef(ordered);
|
|
82
|
+
const models = [...new Set(ordered.map((call) => safeMetadata(call.model, 96)).filter(Boolean))]
|
|
83
|
+
.sort();
|
|
84
|
+
const sourceVersions = safeSourceVersions(ordered);
|
|
85
|
+
const latestTurn = latestTurnEvidence(ordered);
|
|
86
|
+
const activity = sessionActivity(ordered);
|
|
87
|
+
const rateLimits = latestRateLimits(ordered);
|
|
88
|
+
const subagentId = oneSubagentId(ordered);
|
|
89
|
+
const completion = sessionCompletion(ordered, observedTo, subagentId === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: subagentCompletionLookup.get(subagentCompletionKey(first.agent, first.sessionId, subagentId)));
|
|
92
|
+
return {
|
|
93
|
+
sessionRef: pseudonymousSessionRef(first.agent, first.sessionId, subagentId),
|
|
94
|
+
...(subagentId === undefined
|
|
95
|
+
? {}
|
|
96
|
+
: { parentSessionRef: pseudonymousSessionRef(first.agent, first.sessionId) }),
|
|
97
|
+
agent: first.agent,
|
|
98
|
+
sessionType: sessionType(ordered, subagentId !== undefined),
|
|
99
|
+
...(project ? { project } : {}),
|
|
100
|
+
...(projectRef ? { projectRef } : {}),
|
|
101
|
+
models,
|
|
102
|
+
sourceVersions,
|
|
103
|
+
observedFrom,
|
|
104
|
+
observedTo,
|
|
105
|
+
...(durationMs > 0 ? { observedDurationMs: durationMs } : {}),
|
|
106
|
+
completion,
|
|
107
|
+
tokenEvidence: tokenEvidence(ordered),
|
|
108
|
+
...(latestTurn ? { latestTurn } : {}),
|
|
109
|
+
...(activity ? { activity } : {}),
|
|
110
|
+
...(rateLimits ? { rateLimits } : {}),
|
|
111
|
+
provenance: {
|
|
112
|
+
source: "parsed_local_agent_calls",
|
|
113
|
+
confidence: "observed",
|
|
114
|
+
uploaded: false
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function tokenEvidence(calls) {
|
|
119
|
+
if (calls.some((call) => call.usageSupport === "unsupported_token_shape")) {
|
|
120
|
+
return { status: "missing", reason: "unsupported_token_shape" };
|
|
121
|
+
}
|
|
122
|
+
const scopes = new Set(calls.map((call) => call.usageScope));
|
|
123
|
+
if (scopes.size !== 1 || scopes.has(undefined)) {
|
|
124
|
+
return { status: "missing", reason: "mixed_usage_scope" };
|
|
125
|
+
}
|
|
126
|
+
if (calls.some((call) => !validUsage(call))) {
|
|
127
|
+
return { status: "missing", reason: "invalid_token_evidence" };
|
|
128
|
+
}
|
|
129
|
+
const basis = calls[0].usageScope === "session_cumulative"
|
|
130
|
+
? "session_cumulative"
|
|
131
|
+
: "turn_sum";
|
|
132
|
+
const inputTokens = sum(calls, (call) => call.usage.inputTokens);
|
|
133
|
+
const outputTokens = sum(calls, (call) => call.usage.outputTokens);
|
|
134
|
+
const cacheReadTokens = completeSum(calls, (call) => call.usage.cacheReadTokens);
|
|
135
|
+
const cacheWrite5mTokens = completeSum(calls, (call) => call.usage.cacheWrite5mTokens);
|
|
136
|
+
const cacheWrite1hTokens = completeSum(calls, (call) => call.usage.cacheWrite1hTokens);
|
|
137
|
+
const thoughtTokens = completeSum(calls, (call) => call.usage.thoughtTokens);
|
|
138
|
+
const toolTokens = completeSum(calls, (call) => call.usage.toolTokens);
|
|
139
|
+
const reportedTotalTokens = completeSum(calls, (call) => call.reportedTotalTokens);
|
|
140
|
+
const componentTotalTokens = inputTokens + outputTokens +
|
|
141
|
+
(cacheReadTokens ?? 0) +
|
|
142
|
+
(cacheWrite5mTokens ?? 0) +
|
|
143
|
+
(cacheWrite1hTokens ?? 0) +
|
|
144
|
+
(thoughtTokens ?? 0) +
|
|
145
|
+
(toolTokens ?? 0);
|
|
146
|
+
if (![inputTokens, outputTokens, cacheReadTokens, cacheWrite5mTokens,
|
|
147
|
+
cacheWrite1hTokens, thoughtTokens, toolTokens, reportedTotalTokens,
|
|
148
|
+
componentTotalTokens].every(validOptionalCount)) {
|
|
149
|
+
return { status: "missing", reason: "invalid_token_evidence" };
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
status: "observed",
|
|
153
|
+
basis,
|
|
154
|
+
inputTokens,
|
|
155
|
+
outputTokens,
|
|
156
|
+
...(cacheReadTokens !== undefined ? { cacheReadTokens } : {}),
|
|
157
|
+
...(cacheWrite5mTokens !== undefined ? { cacheWrite5mTokens } : {}),
|
|
158
|
+
...(cacheWrite1hTokens !== undefined ? { cacheWrite1hTokens } : {}),
|
|
159
|
+
...(thoughtTokens !== undefined ? { thoughtTokens } : {}),
|
|
160
|
+
...(toolTokens !== undefined ? { toolTokens } : {}),
|
|
161
|
+
componentTotalTokens,
|
|
162
|
+
...(reportedTotalTokens !== undefined ? { reportedTotalTokens } : {}),
|
|
163
|
+
componentEvidence: mergedTokenComponentEvidence(calls, reportedTotalTokens)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function mergedTokenComponentEvidence(calls, reportedTotalTokens) {
|
|
167
|
+
const evidence = calls.map((call) => call.tokenComponentEvidence ?? inferredTokenComponentEvidence(call));
|
|
168
|
+
const all = (pick, value) => evidence.every((item) => pick(item) === value);
|
|
169
|
+
return {
|
|
170
|
+
inputTokens: "observed",
|
|
171
|
+
outputTokens: "observed",
|
|
172
|
+
cacheReadTokens: all((item) => item.cacheReadTokens, "observed")
|
|
173
|
+
? "observed"
|
|
174
|
+
: "not_separately_reported",
|
|
175
|
+
cacheWriteTokens: all((item) => item.cacheWriteTokens, "observed")
|
|
176
|
+
? "observed"
|
|
177
|
+
: all((item) => item.cacheWriteTokens, "not_separately_reported")
|
|
178
|
+
? "not_separately_reported"
|
|
179
|
+
: "partial",
|
|
180
|
+
thoughtTokens: all((item) => item.thoughtTokens, "observed")
|
|
181
|
+
? "observed"
|
|
182
|
+
: "not_separately_reported",
|
|
183
|
+
toolTokens: all((item) => item.toolTokens, "observed")
|
|
184
|
+
? "observed"
|
|
185
|
+
: "not_separately_reported",
|
|
186
|
+
componentTotalTokens: all((item) => item.calculatedTotalTokens, "calculated_complete") ? "calculated_complete" : "calculated_partial",
|
|
187
|
+
reportedTotalTokens: reportedTotalTokens === undefined
|
|
188
|
+
? "not_reported"
|
|
189
|
+
: "provider_reported"
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** Conservative fallback for additive in-memory callers predating parser evidence. */
|
|
193
|
+
function inferredTokenComponentEvidence(call) {
|
|
194
|
+
const hasCacheRead = call.usage.cacheReadTokens !== undefined;
|
|
195
|
+
const hasWrite5m = call.usage.cacheWrite5mTokens !== undefined;
|
|
196
|
+
const hasWrite1h = call.usage.cacheWrite1hTokens !== undefined;
|
|
197
|
+
const complete = call.agent === "codex" ||
|
|
198
|
+
(hasCacheRead && hasWrite5m && hasWrite1h);
|
|
199
|
+
return {
|
|
200
|
+
inputTokens: "observed",
|
|
201
|
+
outputTokens: "observed",
|
|
202
|
+
cacheReadTokens: hasCacheRead ? "observed" : "not_separately_reported",
|
|
203
|
+
cacheWriteTokens: hasWrite5m && hasWrite1h
|
|
204
|
+
? "observed"
|
|
205
|
+
: hasWrite5m || hasWrite1h
|
|
206
|
+
? "partial"
|
|
207
|
+
: "not_separately_reported",
|
|
208
|
+
thoughtTokens: call.usage.thoughtTokens === undefined
|
|
209
|
+
? "not_separately_reported"
|
|
210
|
+
: "observed",
|
|
211
|
+
toolTokens: call.usage.toolTokens === undefined
|
|
212
|
+
? "not_separately_reported"
|
|
213
|
+
: "observed",
|
|
214
|
+
calculatedTotalTokens: complete
|
|
215
|
+
? "calculated_complete"
|
|
216
|
+
: "calculated_partial",
|
|
217
|
+
reportedTotalTokens: call.reportedTotalTokens === undefined
|
|
218
|
+
? "not_reported"
|
|
219
|
+
: "provider_reported"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function latestTurnEvidence(calls) {
|
|
223
|
+
const call = calls
|
|
224
|
+
.filter((candidate) => candidate.latestTurnUsage && candidate.usageSupport !== "unsupported_token_shape")
|
|
225
|
+
.sort((left, right) => Date.parse(right.timestamp) - Date.parse(left.timestamp))[0];
|
|
226
|
+
const usage = call?.latestTurnUsage;
|
|
227
|
+
if (!usage || !validTurnUsage(usage))
|
|
228
|
+
return undefined;
|
|
229
|
+
return {
|
|
230
|
+
inputTokens: usage.inputTokens,
|
|
231
|
+
outputTokens: usage.outputTokens,
|
|
232
|
+
...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
233
|
+
...(usage.cacheWrite5mTokens !== undefined ? { cacheWrite5mTokens: usage.cacheWrite5mTokens } : {}),
|
|
234
|
+
...(usage.cacheWrite1hTokens !== undefined ? { cacheWrite1hTokens: usage.cacheWrite1hTokens } : {}),
|
|
235
|
+
...(usage.thoughtTokens !== undefined ? { thoughtTokens: usage.thoughtTokens } : {}),
|
|
236
|
+
...(usage.toolTokens !== undefined ? { toolTokens: usage.toolTokens } : {}),
|
|
237
|
+
contextTokens: usage.contextTokens,
|
|
238
|
+
totalTokens: usage.totalTokens,
|
|
239
|
+
source: usage.source
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function sessionActivity(calls) {
|
|
243
|
+
const activity = calls.map((call) => call.activity).filter((value) => value !== undefined);
|
|
244
|
+
if (activity.length === 0)
|
|
245
|
+
return undefined;
|
|
246
|
+
if (activity.some((item) => !validActivityCount(item.promptCount) ||
|
|
247
|
+
!validActivityCount(item.toolCallCount))) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const workTypes = [...new Set(activity.map((item) => `${item.kind}\u0000${item.action}`))];
|
|
251
|
+
if (workTypes.length !== 1)
|
|
252
|
+
return undefined;
|
|
253
|
+
const [kind, action] = workTypes[0].split("\u0000");
|
|
254
|
+
return {
|
|
255
|
+
kind,
|
|
256
|
+
action,
|
|
257
|
+
// Parsers can attach the same cumulative activity snapshot to several turns;
|
|
258
|
+
// max preserves that evidence without multiplying it by the turn count.
|
|
259
|
+
promptCount: Math.max(...activity.map((item) => item.promptCount)),
|
|
260
|
+
toolCallCount: Math.max(...activity.map((item) => item.toolCallCount))
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function sessionType(calls, hasSubagentIdentity) {
|
|
264
|
+
const flags = [...new Set(calls.flatMap((call) => call.activity ? [call.activity.isSubagent] : []))];
|
|
265
|
+
if (hasSubagentIdentity) {
|
|
266
|
+
// A per-transcript subagent identity is itself subagent evidence (the
|
|
267
|
+
// host only writes agentId onto sidechain lines). An explicit
|
|
268
|
+
// contradicting parent flag still fails closed to unknown.
|
|
269
|
+
return flags.every((flag) => flag === true) ? "subagent" : "unknown";
|
|
270
|
+
}
|
|
271
|
+
if (flags.length !== 1)
|
|
272
|
+
return "unknown";
|
|
273
|
+
return flags[0] ? "subagent" : "parent";
|
|
274
|
+
}
|
|
275
|
+
/** The group key already splits by subagentId; mixed groups fail to none. */
|
|
276
|
+
function oneSubagentId(calls) {
|
|
277
|
+
const values = [...new Set(calls.map((call) => call.subagentId))];
|
|
278
|
+
return values.length === 1 ? values[0] : undefined;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Latest host-recorded completion per subagent run, joined across transcript
|
|
282
|
+
* files. Claude Code writes a subagent's completion (Task tool result) into
|
|
283
|
+
* the owning transcript, never into the subagent's own file.
|
|
284
|
+
*/
|
|
285
|
+
function subagentCompletionsByIdentity(calls) {
|
|
286
|
+
const latest = new Map();
|
|
287
|
+
for (const call of calls) {
|
|
288
|
+
for (const record of call.subagentCompletions ?? []) {
|
|
289
|
+
const observedAt = normalizedTimestamp(record.observedAt);
|
|
290
|
+
if (!record.subagentId || !observedAt)
|
|
291
|
+
continue;
|
|
292
|
+
const key = subagentCompletionKey(call.agent, call.sessionId, record.subagentId);
|
|
293
|
+
const prior = latest.get(key);
|
|
294
|
+
if (!prior || Date.parse(observedAt) > Date.parse(prior)) {
|
|
295
|
+
latest.set(key, observedAt);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return latest;
|
|
300
|
+
}
|
|
301
|
+
function subagentCompletionKey(agent, sessionId, subagentId) {
|
|
302
|
+
return `${agent}\u0000${sessionId}\u0000${subagentId}`;
|
|
303
|
+
}
|
|
304
|
+
function safeSourceVersions(calls) {
|
|
305
|
+
const observed = calls.map((call) => call.sourceVersion?.trim());
|
|
306
|
+
// Partial host-version evidence is still missing at the session boundary;
|
|
307
|
+
// it cannot support an exact-version cohort claim.
|
|
308
|
+
if (observed.some((value) => !value))
|
|
309
|
+
return [];
|
|
310
|
+
const safe = observed.map(safeSourceVersion);
|
|
311
|
+
if (safe.some((value) => value === undefined))
|
|
312
|
+
return [];
|
|
313
|
+
return [...new Set(safe)].sort();
|
|
314
|
+
}
|
|
315
|
+
function sessionCompletion(calls, observedTo, crossFileObservedAt) {
|
|
316
|
+
const expectedEvidence = calls[0].agent === "claude-code"
|
|
317
|
+
? "claude_turn_duration"
|
|
318
|
+
: "codex_task_complete";
|
|
319
|
+
const completions = calls.map((call) => call.completion);
|
|
320
|
+
if (completions.every((completion) => completion === undefined)) {
|
|
321
|
+
// Subagent transcripts carry no in-file completion marker; the owning
|
|
322
|
+
// transcript's Task tool result is the host's explicit completion
|
|
323
|
+
// evidence for that run. A record older than the run's last observed
|
|
324
|
+
// activity contradicts itself and fails closed.
|
|
325
|
+
if (crossFileObservedAt !== undefined) {
|
|
326
|
+
const normalized = normalizedTimestamp(crossFileObservedAt);
|
|
327
|
+
if (normalized && Date.parse(normalized) >= Date.parse(observedTo)) {
|
|
328
|
+
return {
|
|
329
|
+
status: "completed",
|
|
330
|
+
evidence: "claude_task_result",
|
|
331
|
+
observedAt: normalized
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
status: "missing",
|
|
336
|
+
evidence: "missing",
|
|
337
|
+
reason: "inconsistent_completion_evidence"
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
status: "missing",
|
|
342
|
+
evidence: "missing",
|
|
343
|
+
reason: "completion_marker_not_observed"
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (completions.some((completion) => !completion || completion.status !== "completed" ||
|
|
347
|
+
completion.evidence !== expectedEvidence ||
|
|
348
|
+
normalizedTimestamp(completion.observedAt) === undefined ||
|
|
349
|
+
Date.parse(completion.observedAt) < Date.parse(observedTo))) {
|
|
350
|
+
return {
|
|
351
|
+
status: "missing",
|
|
352
|
+
evidence: "missing",
|
|
353
|
+
reason: "inconsistent_completion_evidence"
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const normalized = completions.map((completion) => normalizedTimestamp(completion.observedAt));
|
|
357
|
+
if (new Set(normalized).size !== 1) {
|
|
358
|
+
return {
|
|
359
|
+
status: "missing",
|
|
360
|
+
evidence: "missing",
|
|
361
|
+
reason: "inconsistent_completion_evidence"
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
status: "completed",
|
|
366
|
+
evidence: expectedEvidence,
|
|
367
|
+
observedAt: normalized[0]
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function safeSourceVersion(value) {
|
|
371
|
+
const sanitized = safeMetadata(value, 96);
|
|
372
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(sanitized)
|
|
373
|
+
? sanitized
|
|
374
|
+
: undefined;
|
|
375
|
+
}
|
|
376
|
+
function latestRateLimits(calls) {
|
|
377
|
+
const candidates = calls
|
|
378
|
+
.map((call) => call.rateLimits)
|
|
379
|
+
.filter((value) => value && normalizedTimestamp(value.observedAt))
|
|
380
|
+
.sort((left, right) => Date.parse(right.observedAt) - Date.parse(left.observedAt));
|
|
381
|
+
const latest = candidates[0];
|
|
382
|
+
if (!latest)
|
|
383
|
+
return undefined;
|
|
384
|
+
const windows = latest.windows.flatMap((window) => {
|
|
385
|
+
const name = safeMetadata(window.name, 80);
|
|
386
|
+
const resetsAt = normalizedTimestamp(window.resetsAt);
|
|
387
|
+
if (!name || !resetsAt ||
|
|
388
|
+
!Number.isFinite(window.usedPercent) || window.usedPercent < 0 || window.usedPercent > 100 ||
|
|
389
|
+
!Number.isSafeInteger(window.windowMinutes) || window.windowMinutes <= 0) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
return [{
|
|
393
|
+
kind: window.kind,
|
|
394
|
+
name,
|
|
395
|
+
usedPercent: window.usedPercent,
|
|
396
|
+
windowMinutes: window.windowMinutes,
|
|
397
|
+
resetsAt
|
|
398
|
+
}];
|
|
399
|
+
});
|
|
400
|
+
if (windows.length === 0)
|
|
401
|
+
return undefined;
|
|
402
|
+
const planType = safeMetadata(latest.planType ?? "", 80);
|
|
403
|
+
return {
|
|
404
|
+
observedAt: normalizedTimestamp(latest.observedAt),
|
|
405
|
+
...(planType ? { planType } : {}),
|
|
406
|
+
windows
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function oneSafeProject(calls) {
|
|
410
|
+
const observed = calls
|
|
411
|
+
.map((call) => call.project?.trim())
|
|
412
|
+
.filter((value) => Boolean(value));
|
|
413
|
+
// Do not let one safe-looking label override conflicting path-shaped or
|
|
414
|
+
// placeholder metadata elsewhere in the same session.
|
|
415
|
+
if (observed.some((value) => safeProject(value) === undefined))
|
|
416
|
+
return undefined;
|
|
417
|
+
const projects = [...new Set(observed.map((value) => safeProject(value)))];
|
|
418
|
+
return projects.length === 1 ? projects[0] : undefined;
|
|
419
|
+
}
|
|
420
|
+
function oneProjectRef(calls) {
|
|
421
|
+
const observed = calls.map((call) => {
|
|
422
|
+
const supplied = call.workingDirectoryRef?.trim();
|
|
423
|
+
const directory = call.workingDirectory?.trim();
|
|
424
|
+
const derived = directory && directory.length <= 4_096 && !hasControl(directory)
|
|
425
|
+
? projectRefForWorkingDirectory(directory)
|
|
426
|
+
: undefined;
|
|
427
|
+
if (supplied && (!/^avref_[a-f0-9]{64}$/.test(supplied) || derived && supplied !== derived)) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
return supplied || derived;
|
|
431
|
+
});
|
|
432
|
+
if (observed.some((value) => !value)) {
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
const references = [...new Set(observed)];
|
|
436
|
+
return references.length === 1 ? references[0] : undefined;
|
|
437
|
+
}
|
|
438
|
+
function projectRefForWorkingDirectory(directory) {
|
|
439
|
+
return `avref_${createHash("sha256")
|
|
440
|
+
.update("project-working-directory")
|
|
441
|
+
.update("\u0000")
|
|
442
|
+
.update(directory)
|
|
443
|
+
.digest("hex")}`;
|
|
444
|
+
}
|
|
445
|
+
function hasControl(value) {
|
|
446
|
+
return /[\u0000-\u001f\u007f]/.test(value);
|
|
447
|
+
}
|
|
448
|
+
function safeProject(value) {
|
|
449
|
+
if (!value || value === "(home)" || /[\\/]/.test(value))
|
|
450
|
+
return undefined;
|
|
451
|
+
return safeMetadata(value, 120) || undefined;
|
|
452
|
+
}
|
|
453
|
+
function safeMetadata(value, maxLength) {
|
|
454
|
+
return sanitizeLocalActivityText(value)
|
|
455
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
456
|
+
.replace(/\s+/g, " ")
|
|
457
|
+
.trim()
|
|
458
|
+
.slice(0, maxLength);
|
|
459
|
+
}
|
|
460
|
+
function pseudonymousSessionRef(agent, sessionId, subagentId) {
|
|
461
|
+
const hash = createHash("sha256").update(agent).update("\u0000").update(sessionId);
|
|
462
|
+
if (subagentId !== undefined) {
|
|
463
|
+
// Domain-separated so a subagent ref can never collide with a plain
|
|
464
|
+
// session ref, while parent refs stay byte-identical to their pre-split
|
|
465
|
+
// values (existing action-verification records keep matching).
|
|
466
|
+
hash.update("\u0000subagent\u0000").update(subagentId);
|
|
467
|
+
}
|
|
468
|
+
return `avref_${hash.digest("hex")}`;
|
|
469
|
+
}
|
|
470
|
+
function validUsage(call) {
|
|
471
|
+
return [
|
|
472
|
+
call.usage.inputTokens,
|
|
473
|
+
call.usage.outputTokens,
|
|
474
|
+
call.usage.cacheReadTokens,
|
|
475
|
+
call.usage.cacheWrite5mTokens,
|
|
476
|
+
call.usage.cacheWrite1hTokens,
|
|
477
|
+
call.usage.thoughtTokens,
|
|
478
|
+
call.usage.toolTokens,
|
|
479
|
+
call.reportedTotalTokens
|
|
480
|
+
].every(validOptionalCount);
|
|
481
|
+
}
|
|
482
|
+
function validTurnUsage(usage) {
|
|
483
|
+
return [
|
|
484
|
+
usage.inputTokens,
|
|
485
|
+
usage.outputTokens,
|
|
486
|
+
usage.cacheReadTokens,
|
|
487
|
+
usage.cacheWrite5mTokens,
|
|
488
|
+
usage.cacheWrite1hTokens,
|
|
489
|
+
usage.thoughtTokens,
|
|
490
|
+
usage.toolTokens,
|
|
491
|
+
usage.contextTokens,
|
|
492
|
+
usage.totalTokens
|
|
493
|
+
].every(validOptionalCount) && usage.totalTokens >= usage.contextTokens;
|
|
494
|
+
}
|
|
495
|
+
function validOptionalCount(value) {
|
|
496
|
+
return value === undefined || Number.isSafeInteger(value) && value >= 0;
|
|
497
|
+
}
|
|
498
|
+
function validActivityCount(value) {
|
|
499
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
500
|
+
}
|
|
501
|
+
function sum(calls, select) {
|
|
502
|
+
return calls.reduce((total, call) => total + select(call), 0);
|
|
503
|
+
}
|
|
504
|
+
function completeSum(calls, select) {
|
|
505
|
+
const values = calls.map(select);
|
|
506
|
+
return values.every((value) => value !== undefined)
|
|
507
|
+
? values.reduce((total, value) => total + value, 0)
|
|
508
|
+
: undefined;
|
|
509
|
+
}
|
|
510
|
+
function normalizedTimestamp(value) {
|
|
511
|
+
if (!value)
|
|
512
|
+
return undefined;
|
|
513
|
+
const milliseconds = Date.parse(value);
|
|
514
|
+
return Number.isFinite(milliseconds) && milliseconds > 0
|
|
515
|
+
? new Date(milliseconds).toISOString()
|
|
516
|
+
: undefined;
|
|
517
|
+
}
|
|
518
|
+
function validObservedTimestamp(value) {
|
|
519
|
+
return normalizedTimestamp(value) !== undefined;
|
|
520
|
+
}
|
|
521
|
+
//# sourceMappingURL=sessionVitals.js.map
|
|
@@ -100,6 +100,11 @@ export type ParsedInvocationFile = {
|
|
|
100
100
|
assistantTurns: number;
|
|
101
101
|
contextSignal: SessionContextSignal;
|
|
102
102
|
};
|
|
103
|
+
/** Private-index proof used to narrow one aggregated Codex window exactly. */
|
|
104
|
+
export type ParsedInvocationWindowProof = {
|
|
105
|
+
earliestCountedAt?: string;
|
|
106
|
+
allCountedEventsTimestamped: boolean;
|
|
107
|
+
};
|
|
103
108
|
export type ToolInvocationOptions = {
|
|
104
109
|
/** default: join(homedir(), ".claude", "projects") */
|
|
105
110
|
claudeProjectsDir?: string;
|
|
@@ -118,13 +123,47 @@ export type ToolInvocationOptions = {
|
|
|
118
123
|
export declare function parseClaudeCodeInvocations(content: string, sinceMs?: number): ParsedInvocationFile;
|
|
119
124
|
/** Parse ONE Codex rollout's tool/skill/subagent invocations. */
|
|
120
125
|
export declare function parseCodexInvocations(content: string, sinceMs?: number): ParsedInvocationFile;
|
|
126
|
+
/**
|
|
127
|
+
* Serializable snapshot of one Codex invocation collector, used by the
|
|
128
|
+
* checkpointed streaming path to carry aggregation across bounded runs. It
|
|
129
|
+
* contains only what the finished summary itself persists — tool/skill/
|
|
130
|
+
* command names, file basenames, opaque session ids, counters and window
|
|
131
|
+
* proof timestamps — never raw transcript text or absolute paths.
|
|
132
|
+
*/
|
|
133
|
+
export type CodexInvocationCollectorSnapshot = {
|
|
134
|
+
counts: Array<[string, number]>;
|
|
135
|
+
mcpTools: string[];
|
|
136
|
+
skills: string[];
|
|
137
|
+
subagents: string[];
|
|
138
|
+
commands: string[];
|
|
139
|
+
fileReads: Array<[string, number]>;
|
|
140
|
+
assistantTurns: number;
|
|
141
|
+
sessionId?: string;
|
|
142
|
+
rootSessionMetaSeen: boolean;
|
|
143
|
+
rootStartedAtMs?: number;
|
|
144
|
+
rootTaskStarted: boolean;
|
|
145
|
+
lastActivityAt?: string;
|
|
146
|
+
compactionEvents: number;
|
|
147
|
+
isSubagent: boolean;
|
|
148
|
+
parentSessionId?: string;
|
|
149
|
+
nestedSessions: Array<[string, NestedSessionMetadata]>;
|
|
150
|
+
earliestCountedMs?: number;
|
|
151
|
+
allCountedEventsTimestamped: boolean;
|
|
152
|
+
};
|
|
153
|
+
/** Fail-closed structural check for a restored collector snapshot. */
|
|
154
|
+
export declare function isCodexInvocationCollectorSnapshot(value: unknown): value is CodexInvocationCollectorSnapshot;
|
|
121
155
|
/**
|
|
122
156
|
* Stateful Codex invocation parser used to share localAgentLogs' JSONL pass.
|
|
123
157
|
* One collector is created per rollout file and discarded after `finish()`.
|
|
158
|
+
* A checkpointed stream restores a prior run's snapshot; the window (sinceMs)
|
|
159
|
+
* must be the one the snapshot was created under — the caller pins it in the
|
|
160
|
+
* checkpoint envelope.
|
|
124
161
|
*/
|
|
125
|
-
export declare function createCodexInvocationCollector(sinceMs?: number): {
|
|
162
|
+
export declare function createCodexInvocationCollector(sinceMs?: number, restored?: CodexInvocationCollectorSnapshot): {
|
|
126
163
|
consume: (entry: Record<string, unknown>) => void;
|
|
127
164
|
finish: () => ParsedInvocationFile;
|
|
165
|
+
windowProof: () => ParsedInvocationWindowProof;
|
|
166
|
+
snapshot: () => CodexInvocationCollectorSnapshot;
|
|
128
167
|
};
|
|
129
168
|
/** Scan this machine's Claude Code + Codex transcripts and aggregate invocations. */
|
|
130
169
|
export declare function loadToolInvocations(options?: ToolInvocationOptions): Promise<InvocationSummary>;
|