@agent-finops/core 0.8.1 → 0.9.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.
Files changed (51) 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/agentDraftToken.d.ts +80 -0
  11. package/dist/agentDraftToken.js +188 -0
  12. package/dist/agentEconomicsReceipt.d.ts +74 -74
  13. package/dist/agentLoopContract.d.ts +27 -0
  14. package/dist/agentLoopContract.js +36 -0
  15. package/dist/glance.d.ts +27 -1
  16. package/dist/glance.js +151 -12
  17. package/dist/guidedAnswer.d.ts +51 -0
  18. package/dist/guidedAnswer.js +352 -0
  19. package/dist/index.d.ts +14 -2
  20. package/dist/index.js +13 -1
  21. package/dist/localAgentFormats/gemini.js +2 -2
  22. package/dist/localAgentFormats/registry.js +6 -2
  23. package/dist/localAgentFormats/runtimeRegistry.js +5 -2
  24. package/dist/localAgentFormats/types.d.ts +2 -1
  25. package/dist/localAgentLogs.d.ts +362 -3
  26. package/dist/localAgentLogs.js +1964 -165
  27. package/dist/modelPricing.d.ts +1 -1
  28. package/dist/modelPricing.js +1 -1
  29. package/dist/projectEconomics.d.ts +617 -0
  30. package/dist/projectEconomics.js +620 -0
  31. package/dist/projectEconomicsBuilder.d.ts +89 -0
  32. package/dist/projectEconomicsBuilder.js +473 -0
  33. package/dist/projectIndexStore.d.ts +545 -0
  34. package/dist/projectIndexStore.js +606 -0
  35. package/dist/providerConnectors.d.ts +161 -1
  36. package/dist/providerConnectors.js +406 -11
  37. package/dist/qualitativeIndexCache.d.ts +494 -0
  38. package/dist/qualitativeIndexCache.js +930 -0
  39. package/dist/resultCard.d.ts +350 -0
  40. package/dist/resultCard.js +604 -0
  41. package/dist/runtimeCommands.d.ts +36 -0
  42. package/dist/runtimeCommands.js +50 -0
  43. package/dist/scanGuard.d.ts +3 -1
  44. package/dist/scanGuard.js +164 -4
  45. package/dist/schema.d.ts +33 -31
  46. package/dist/schema.js +9 -1
  47. package/dist/sessionVitals.d.ts +145 -0
  48. package/dist/sessionVitals.js +521 -0
  49. package/dist/toolInvocations.d.ts +40 -1
  50. package/dist/toolInvocations.js +101 -20
  51. package/package.json +1 -1
@@ -0,0 +1,145 @@
1
+ import { type LocalAgentActivity, type LocalAgentCall, type LocalAgentRateLimitWindow, type LocalAgentTurnUsage } from "./localAgentLogs.js";
2
+ /**
3
+ * Additive, privacy-reduced session evidence for future before/after tests.
4
+ *
5
+ * V0 deliberately accepts already-parsed LocalAgentCall values rather than
6
+ * reading transcripts itself. It never carries prompts, responses, absolute
7
+ * paths, filenames, or raw provider session identifiers.
8
+ */
9
+ export type SessionVitalsAgentV0 = "claude-code" | "codex";
10
+ export type SessionVitalsTokenEvidenceV0 = {
11
+ status: "observed";
12
+ basis: "turn_sum" | "session_cumulative";
13
+ inputTokens: number;
14
+ outputTokens: number;
15
+ cacheReadTokens?: number;
16
+ cacheWrite5mTokens?: number;
17
+ cacheWrite1hTokens?: number;
18
+ thoughtTokens?: number;
19
+ toolTokens?: number;
20
+ componentTotalTokens: number;
21
+ reportedTotalTokens?: number;
22
+ componentEvidence: {
23
+ inputTokens: "observed";
24
+ outputTokens: "observed";
25
+ cacheReadTokens: "observed" | "not_separately_reported";
26
+ cacheWriteTokens: "observed" | "partial" | "not_separately_reported";
27
+ thoughtTokens: "observed" | "not_separately_reported";
28
+ toolTokens: "observed" | "not_separately_reported";
29
+ componentTotalTokens: "calculated_complete" | "calculated_partial";
30
+ reportedTotalTokens: "provider_reported" | "not_reported";
31
+ };
32
+ } | {
33
+ status: "missing";
34
+ reason: "unsupported_token_shape" | "mixed_usage_scope" | "invalid_token_evidence";
35
+ };
36
+ export type SessionVitalsLatestTurnV0 = {
37
+ inputTokens: number;
38
+ outputTokens: number;
39
+ cacheReadTokens?: number;
40
+ cacheWrite5mTokens?: number;
41
+ cacheWrite1hTokens?: number;
42
+ thoughtTokens?: number;
43
+ toolTokens?: number;
44
+ contextTokens: number;
45
+ totalTokens: number;
46
+ source: LocalAgentTurnUsage["source"];
47
+ };
48
+ export type SessionVitalsRateLimitWindowV0 = {
49
+ kind: LocalAgentRateLimitWindow["kind"];
50
+ name: string;
51
+ usedPercent: number;
52
+ windowMinutes: number;
53
+ resetsAt: string;
54
+ };
55
+ export type SessionVitalsCompletionV0 = {
56
+ status: "completed";
57
+ /**
58
+ * A completed session snapshot, not permanent transcript closure.
59
+ * `claude_task_result` is the host-recorded Task tool result
60
+ * (`status: "completed"`) written into the owning transcript when a
61
+ * subagent run finishes — the only explicit completion marker Claude
62
+ * Code produces for subagent transcript files.
63
+ */
64
+ evidence: "claude_turn_duration" | "codex_task_complete" | "claude_task_result";
65
+ observedAt: string;
66
+ } | {
67
+ status: "missing";
68
+ evidence: "missing";
69
+ reason: "completion_marker_not_observed" | "inconsistent_completion_evidence";
70
+ };
71
+ export type SessionVitalV0 = {
72
+ /** Stable AV-compatible pseudonym. The raw transcript session id is never returned. */
73
+ sessionRef: string;
74
+ /**
75
+ * Pseudonym of the owning parent session, present only on rows split out of
76
+ * a shared-sessionId subagent transcript. Matches the parent row's
77
+ * `sessionRef`; raw identifiers are never returned.
78
+ */
79
+ parentSessionRef?: string;
80
+ agent: SessionVitalsAgentV0;
81
+ /** Unknown remains ineligible for automatic before/after cohort matching. */
82
+ sessionType: "parent" | "subagent" | "unknown";
83
+ /** Existing privacy-reduced project label; paths and suspicious values are omitted. */
84
+ project?: string;
85
+ /** Stable opaque identity derived from one consistent native working directory. */
86
+ projectRef?: string;
87
+ models: string[];
88
+ /** Empty means the parser did not report a safe version; never inferred. */
89
+ sourceVersions: string[];
90
+ observedFrom: string;
91
+ observedTo: string;
92
+ /** Omitted unless a distinct, valid start and end are present. */
93
+ observedDurationMs?: number;
94
+ /** Explicit host completion boundary for this snapshot; inactivity is never treated as completion. */
95
+ completion: SessionVitalsCompletionV0;
96
+ tokenEvidence: SessionVitalsTokenEvidenceV0;
97
+ latestTurn?: SessionVitalsLatestTurnV0;
98
+ activity?: {
99
+ kind: LocalAgentActivity["kind"];
100
+ action: LocalAgentActivity["action"];
101
+ promptCount: number;
102
+ toolCallCount: number;
103
+ };
104
+ rateLimits?: {
105
+ observedAt: string;
106
+ planType?: string;
107
+ windows: SessionVitalsRateLimitWindowV0[];
108
+ };
109
+ provenance: {
110
+ source: "parsed_local_agent_calls";
111
+ confidence: "observed";
112
+ uploaded: false;
113
+ };
114
+ };
115
+ export type SessionVitalsV0 = {
116
+ schemaVersion: 0;
117
+ sessions: SessionVitalV0[];
118
+ coverage: {
119
+ inputCalls: number;
120
+ deduplicatedCalls: number;
121
+ eligibleCalls: number;
122
+ emittedSessions: number;
123
+ sessionsWithObservedTokens: number;
124
+ sessionsWithMissingTokens: number;
125
+ excludedCalls: {
126
+ unsupportedAgent: number;
127
+ missingSessionIdentity: number;
128
+ invalidTimestamp: number;
129
+ };
130
+ };
131
+ privacy: {
132
+ rawSessionIds: false;
133
+ promptOrResponseText: false;
134
+ absolutePaths: false;
135
+ uploaded: false;
136
+ };
137
+ };
138
+ /**
139
+ * Build one deterministic V0 row per Claude Code or Codex session.
140
+ *
141
+ * Token evidence fails closed at the session boundary: one unsupported,
142
+ * invalid, or mixed-scope call prevents a partial sum from looking complete.
143
+ */
144
+ export declare function extractSessionVitalsV0(calls: readonly LocalAgentCall[]): SessionVitalsV0;
145
+ //# sourceMappingURL=sessionVitals.d.ts.map
@@ -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