@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.
Files changed (44) hide show
  1. package/README.md +5 -3
  2. package/dist/actionPlanner.d.ts +140 -0
  3. package/dist/actionPlanner.js +938 -0
  4. package/dist/actionVerification.d.ts +1240 -0
  5. package/dist/actionVerification.js +1028 -0
  6. package/dist/activitySnapshot.d.ts +142 -50
  7. package/dist/activitySnapshot.js +145 -6
  8. package/dist/activitySnapshotCache.d.ts +8 -1
  9. package/dist/activitySnapshotCache.js +103 -7
  10. package/dist/agentEconomicsReceipt.d.ts +74 -74
  11. package/dist/glance.d.ts +27 -1
  12. package/dist/glance.js +151 -12
  13. package/dist/index.d.ts +11 -2
  14. package/dist/index.js +10 -1
  15. package/dist/localAgentFormats/gemini.js +2 -2
  16. package/dist/localAgentFormats/registry.js +6 -2
  17. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  18. package/dist/localAgentFormats/types.d.ts +2 -1
  19. package/dist/localAgentLogs.d.ts +362 -3
  20. package/dist/localAgentLogs.js +1964 -165
  21. package/dist/modelPricing.d.ts +1 -1
  22. package/dist/modelPricing.js +1 -1
  23. package/dist/projectEconomics.d.ts +617 -0
  24. package/dist/projectEconomics.js +620 -0
  25. package/dist/projectEconomicsBuilder.d.ts +89 -0
  26. package/dist/projectEconomicsBuilder.js +473 -0
  27. package/dist/projectIndexStore.d.ts +545 -0
  28. package/dist/projectIndexStore.js +606 -0
  29. package/dist/providerConnectors.d.ts +59 -1
  30. package/dist/providerConnectors.js +175 -11
  31. package/dist/qualitativeIndexCache.d.ts +494 -0
  32. package/dist/qualitativeIndexCache.js +930 -0
  33. package/dist/resultCard.d.ts +350 -0
  34. package/dist/resultCard.js +604 -0
  35. package/dist/runtimeCommands.d.ts +21 -0
  36. package/dist/runtimeCommands.js +27 -0
  37. package/dist/scanGuard.d.ts +3 -1
  38. package/dist/scanGuard.js +164 -4
  39. package/dist/schema.d.ts +31 -31
  40. package/dist/sessionVitals.d.ts +145 -0
  41. package/dist/sessionVitals.js +521 -0
  42. package/dist/toolInvocations.d.ts +40 -1
  43. package/dist/toolInvocations.js +101 -20
  44. 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>;