@agent-finops/core 0.9.5 → 0.9.7

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.
@@ -1,4 +1,5 @@
1
1
  import { loadAgentInventory } from "./agentInventory.js";
2
+ import { safeUntrustedLabel, WITHHELD_ENTITY_LABEL, WITHHELD_FILE_LABEL } from "./untrustedLabel.js";
2
3
  import { findPricingRule } from "./modelPricing.js";
3
4
  import { loadToolInvocations } from "./toolInvocations.js";
4
5
  /**
@@ -97,15 +98,21 @@ export function computeDeadContext(items, invocations, config) {
97
98
  }
98
99
  dead.push({
99
100
  kind: item.kind,
100
- name: item.name,
101
+ // Skill, subagent, slash-command, hook and MCP SERVER names, read off
102
+ // disk. They are printed by name on the readout and in the artifact.
103
+ name: safeUntrustedLabel(item.name, WITHHELD_ENTITY_LABEL),
101
104
  scope: item.scope,
102
105
  activation: item.activation,
106
+ // The STRUCTURED siblings travel with the name to every surface the name
107
+ // does, including the Apply artifact. Neutralizing the name and leaving
108
+ // the path beside it raw is the same inversion Blocker A was.
109
+ // `host` is the InventoryHost enum, not free text — bounded by the type.
103
110
  host: item.host,
104
111
  invocationTracking: item.invocationTracking,
105
112
  alwaysLoadedTokens: item.alwaysLoadedTokens,
106
113
  weightConfidence: item.weightConfidence,
107
- path: item.path,
108
- ownerDirs: item.ownerDirs
114
+ path: item.path === undefined ? undefined : safeUntrustedLabel(item.path, WITHHELD_FILE_LABEL),
115
+ ownerDirs: item.ownerDirs?.map((dir) => safeUntrustedLabel(dir, WITHHELD_FILE_LABEL))
109
116
  });
110
117
  }
111
118
  dead.sort((a, b) => b.alwaysLoadedTokens - a.alwaysLoadedTokens);
package/dist/glance.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { dedupeCumulativeSessionCalls, sanitizeLocalActivityText } from "./localAgentLogs.js";
2
- import { canPriceTokenUsageAtScope, estimateTokenCostUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
2
+ import { canPriceTokenUsageAtScope, estimateTokenCostsUsd, PRICING_TABLE_AS_OF } from "./modelPricing.js";
3
3
  import { subscriptionPlans } from "./planMath.js";
4
4
  import { buildContextHealth } from "./contextHealth.js";
5
5
  import { localAgentFormatDescriptors, localAgentFormatSupports } from "./localAgentFormats/registry.js";
@@ -106,7 +106,7 @@ export function buildUsageGlance(calls, options = {}) {
106
106
  "Claude Code transcripts do not report plan headroom. Missing limits remain unavailable instead of being inferred.",
107
107
  "Cursor and GitHub Copilot require their provider connections because their local chat stores are not treated as authoritative billing transcripts.",
108
108
  ...(qualitativeComplete ? [] : [
109
- "Main focus, anomaly, and context-change handoff are unavailable because the bounded qualitative index is incomplete; no global driver was inferred from a selected subset."
109
+ "Main focus, anomaly, and context-change handoff are unavailable because some session transcripts have not been read yet; no global driver was inferred from a partly-read subset."
110
110
  ])
111
111
  ];
112
112
  return {
@@ -202,10 +202,10 @@ function buildCoverageLimitedPrimaryAction(input) {
202
202
  kind: "session_handoff",
203
203
  intent: "inspect_current_work",
204
204
  label: project ? `Refresh evidence · ${project}` : "Refresh evidence",
205
- detail: `Main focus unavailable · qualitative index ${status}`,
205
+ detail: `Main focus unavailable · session transcripts ${status === "partial" ? "only partly read" : "not available"}`,
206
206
  ...(project ? { project } : {}),
207
207
  agentPrompt: [
208
- "aibill's bounded qualitative evidence is incomplete.",
208
+ "Some coding-agent session transcripts have not been read yet.",
209
209
  "Do not infer a global main focus, waste cause, or context change from the selected subset.",
210
210
  `Run \`${aibillImproveCommandV0()}\` from the exact project root to refresh the private index, then review the new evidence before editing.`
211
211
  ].join("\n"),
@@ -793,9 +793,13 @@ function limitActionName(limit) {
793
793
  function callCost(call) {
794
794
  if (call.usageSupport === "unsupported_token_shape")
795
795
  return undefined;
796
- if (!canPriceTokenUsageAtScope(call.model, call.usage, call.usageScope === "turn" ? "request" : "aggregate"))
796
+ if (!canPriceTokenUsageAtScope(call.model, call.usage, call.usageScope === "turn" ? "request" : "aggregate", call.maxRequestPromptTokens))
797
797
  return undefined;
798
- return estimateTokenCostUsd(call.model, call.usage);
798
+ // Tier must come from the largest single request, exactly as the report's
799
+ // aggregation does — otherwise the statusline and the receipt disagree on
800
+ // the same session (a cache-heavy Codex session voided here while the
801
+ // receipt prices it, or priced here at 2x).
802
+ return estimateTokenCostsUsd(call.model, [call.usage], [call.maxRequestPromptTokens]);
799
803
  }
800
804
  function inputSideTokens(call) {
801
805
  return call.usage.inputTokens +
package/dist/insights.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { hasCallLevelProvenance, hasPricedEvidence, spendComparisonKey, spendInsightSchema } from "./schema.js";
2
+ import { safeUntrustedLabel, safeUntrustedLabels, WITHHELD_ENTITY_LABEL, WITHHELD_OPERATION_LABEL } from "./untrustedLabel.js";
2
3
  const confidenceRank = {
3
4
  verified: 0,
4
5
  estimated: 1,
@@ -32,7 +33,10 @@ function spikeInsights(records, summary) {
32
33
  const topProject = topBreakdown(currentRecords, (record) => record.projectId);
33
34
  const topModels = breakdown(currentRecords, (record) => record.model).slice(0, 2).map((entry) => entry.key);
34
35
  const deltaUsd = roundMoney(anomaly.currentAmountUsd - anomaly.previousAmountUsd);
35
- const likelyOwner = topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner";
36
+ // Ownership lead is an agent/project/client id off the records it lands
37
+ // mid-sentence in `summary`, so it is neutralized at the interpolation
38
+ // point like every other untrusted fragment.
39
+ const likelyOwner = safeEntity(topAgent?.key ?? topProject?.key ?? topClient?.key ?? "an unassigned owner");
36
40
  const cohortSuffix = stableSuffix(anomaly.comparisonKey ?? "legacy");
37
41
  const isProviderBilledCost = currentRecords.length > 0 && currentRecords.every((record) => record.usageGranularity === "billing_bucket" &&
38
42
  record.costConfidence === "verified");
@@ -50,14 +54,14 @@ function spikeInsights(records, summary) {
50
54
  { label: `Previous cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.previousAmountUsd) },
51
55
  { label: `Current cohort ${isProviderBilledCost ? "spend" : "value"}`, value: formatUsd(anomaly.currentAmountUsd) },
52
56
  { label: `${evidenceLabel} increase`, value: formatUsd(deltaUsd), detail: `${formatMultiplier(anomaly.multiplier)} day-over-day multiplier` },
53
- topAgent ? { label: "Ownership lead", value: topAgent.key, detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
54
- topClient ? { label: "Client concentration", value: topClient.key, detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
55
- topModels.length > 0 ? { label: "Dominant models", value: topModels.join(", ") } : undefined
57
+ topAgent ? { label: "Ownership lead", value: safeEntity(topAgent.key), detail: `${formatUsd(topAgent.amountUsd)} across ${topAgent.recordCount} cohort records` } : undefined,
58
+ topClient ? { label: "Client concentration", value: safeEntity(topClient.key), detail: `${formatUsd(topClient.amountUsd)} on spike day` } : undefined,
59
+ topModels.length > 0 ? { label: "Dominant models", value: safeUntrustedLabels(topModels).join(", ") } : undefined
56
60
  ]),
57
- affectedClients: keysFrom(currentRecords, (record) => record.clientId),
58
- affectedProjects: keysFrom(currentRecords, (record) => record.projectId),
59
- affectedAgents: keysFrom(currentRecords, (record) => record.agentId),
60
- affectedModels: keysFrom(currentRecords, (record) => record.model),
61
+ affectedClients: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.clientId)),
62
+ affectedProjects: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.projectId)),
63
+ affectedAgents: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.agentId)),
64
+ affectedModels: safeUntrustedLabels(keysFrom(currentRecords, (record) => record.model)),
61
65
  estimatedImpactUsd: deltaUsd,
62
66
  confidence: anomaly.confidence,
63
67
  recommendedAction: `Review and reconcile the provider-cohort records from ${anomaly.key}, confirm the accountable owner, and obtain run-level evidence before diagnosing behavior or changing a policy.`,
@@ -81,24 +85,27 @@ function agentCostDriverInsights(records, summary) {
81
85
  const topModel = topBreakdown(agentRecords, (record) => record.model);
82
86
  const hasRunLevelEvidence = agentRecords.length > 0 && agentRecords.every(hasCallLevelProvenance);
83
87
  return [{
84
- id: `agent-spend-concentration-${topAgent.key}`,
88
+ // The id is a STRUCTURED field beside the neutralized title, and it is
89
+ // rendered (`Canonical candidate ID: ...`). Slug the neutralized form, not
90
+ // the raw key.
91
+ id: `agent-spend-concentration-${slug(safeEntity(topAgent.key))}`,
85
92
  kind: "optimization_opportunity",
86
93
  severity: "medium",
87
- title: `${topAgent.key} spend concentration needs owner and budget review`,
88
- summary: `${topAgent.key} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
94
+ title: `${safeEntity(topAgent.key)} spend concentration needs owner and budget review`,
95
+ summary: `${safeEntity(topAgent.key)} is attached to ${formatPercent(share)} of tracked spend. Concentration alone does not prove abnormal behavior or an avoidable dollar amount${hasRunLevelEvidence ? "." : "; the evidence is aggregate rather than run-level."}`,
89
96
  evidence: compactEvidence([
90
97
  { label: "Attributed spend", value: formatUsd(topAgent.amountUsd), detail: `${topAgent.recordCount} ${hasRunLevelEvidence ? "call-level" : "aggregate"} record${topAgent.recordCount === 1 ? "" : "s"}` },
91
98
  { label: "Share of tracked spend", value: formatPercent(share) },
92
- topModel ? { label: "Dominant model or billing label", value: topModel.key, detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
93
- topOperation ? { label: "Operation label", value: topOperation.key, detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
99
+ topModel ? { label: "Dominant model or billing label", value: safeEntity(topModel.key), detail: `${formatUsd(topModel.amountUsd)} in this concentration` } : undefined,
100
+ topOperation ? { label: "Operation label", value: safeEntity(topOperation.key), detail: hasRunLevelEvidence ? "Call-level attribution" : "Not verified as one call or run" } : undefined
94
101
  ]),
95
- affectedClients: keysFrom(agentRecords, (record) => record.clientId),
96
- affectedProjects: keysFrom(agentRecords, (record) => record.projectId),
97
- affectedAgents: [topAgent.key],
98
- affectedModels: keysFrom(agentRecords, (record) => record.model),
102
+ affectedClients: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.clientId)),
103
+ affectedProjects: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.projectId)),
104
+ affectedAgents: [safeEntity(topAgent.key)],
105
+ affectedModels: safeUntrustedLabels(keysFrom(agentRecords, (record) => record.model)),
99
106
  estimatedImpactUsd: 0,
100
107
  confidence: topAgent.confidence,
101
- recommendedAction: `Confirm who owns ${topAgent.key}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
108
+ recommendedAction: `Confirm who owns ${safeEntity(topAgent.key)}, reconcile the spend to its approved budget, and collect behavioral evidence before setting a cap or savings target.`,
102
109
  verificationNeeded: "Confirm the budget owner and expected range; concentration alone is not behavioral evidence."
103
110
  }];
104
111
  }
@@ -113,7 +120,7 @@ function contextBloatInsights(records) {
113
120
  const scopedRecords = topOperation
114
121
  ? highInputRecords.filter((record) => record.operation === topOperation.key)
115
122
  : highInputRecords;
116
- const operationLabel = topOperation?.key ?? "large-context calls";
123
+ const operationLabel = safeUntrustedLabel(topOperation?.key ?? "large-context calls", WITHHELD_OPERATION_LABEL);
117
124
  const totalInputTokens = scopedRecords.reduce((total, record) => total + record.inputTokens, 0);
118
125
  const scopedSpend = roundMoney(sumRecords(scopedRecords));
119
126
  if (scopedSpend < 20) {
@@ -131,16 +138,27 @@ function contextBloatInsights(records) {
131
138
  { label: "Spend attached to large context", value: formatUsd(scopedSpend) },
132
139
  { label: "Dominant operation", value: operationLabel }
133
140
  ],
134
- affectedClients: keysFrom(scopedRecords, (record) => record.clientId),
135
- affectedProjects: keysFrom(scopedRecords, (record) => record.projectId),
136
- affectedAgents: keysFrom(scopedRecords, (record) => record.agentId),
137
- affectedModels: keysFrom(scopedRecords, (record) => record.model),
141
+ affectedClients: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.clientId)),
142
+ affectedProjects: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.projectId)),
143
+ affectedAgents: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.agentId)),
144
+ affectedModels: safeUntrustedLabels(keysFrom(scopedRecords, (record) => record.model)),
138
145
  estimatedImpactUsd: 0,
139
146
  confidence: combinedConfidence(scopedRecords.map((record) => record.costConfidence)),
140
147
  recommendedAction: `Inspect representative ${operationLabel} prompts locally and run a matched before/after with the same acceptance criteria before proposing one reversible context change.`,
141
148
  verificationNeeded: "Measure token and quality deltas on matched calls; no savings counterfactual is present yet."
142
149
  }];
143
150
  }
151
+ /**
152
+ * A breakdown key rendered for a HUMAN. The same slot holds a client, a
153
+ * project, an agent, a model or an operation depending on which grouping won,
154
+ * so it takes the dimension-neutral marker.
155
+ *
156
+ * Display only. The raw key is still what `records.filter(...)` matches on —
157
+ * rewriting a matching key would silently empty the cohort behind the finding.
158
+ */
159
+ function safeEntity(value) {
160
+ return safeUntrustedLabel(value, WITHHELD_ENTITY_LABEL);
161
+ }
144
162
  function topBreakdown(records, select) {
145
163
  return breakdown(records, select)[0];
146
164
  }
@@ -49,6 +49,16 @@ export type LocalAgentCall = {
49
49
  latestTurnUsage?: LocalAgentTurnUsage;
50
50
  /** Whether `usage` is one model turn or the session's cumulative financial total. */
51
51
  usageScope?: "turn" | "session_cumulative";
52
+ /**
53
+ * Largest single-request prompt (effective input, cache reads included)
54
+ * observed within a `session_cumulative` `usage`. Tiered per-request pricing
55
+ * is selected per request, never from a cumulative sum: a cache-heavy session
56
+ * routinely clears a per-request tier threshold in aggregate while no single
57
+ * request did. This evidence lets pricing keep such a total on the base tier
58
+ * (exact) instead of failing closed to "missing". Absent for turn-scoped
59
+ * calls, which are already a single request.
60
+ */
61
+ maxRequestPromptTokens?: number;
52
62
  /**
53
63
  * Whether the transcript exposed the input/output components required for
54
64
  * pricing. A total-only snapshot is still usage evidence, but pricing it as
@@ -374,7 +384,7 @@ export type LocalAgentStreamCheckpointAdapter = {
374
384
  writeStreamCheckpoint: (agent: LocalAgentFormatId, pathHash: string, checkpoint: Readonly<LocalAgentStreamCheckpointRecord>) => Promise<void>;
375
385
  deleteStreamCheckpoint: (agent: LocalAgentFormatId, pathHash: string) => Promise<void>;
376
386
  };
377
- export declare const localAgentFinancialParserVersion = 1;
387
+ export declare const localAgentFinancialParserVersion = 3;
378
388
  /**
379
389
  * Qualitative parser contract version. Bumped to 2 with the checkpointed
380
390
  * streaming path (A4b): entries and checkpoints written by the pre-streaming
@@ -385,9 +395,15 @@ export declare const localAgentFinancialParserVersion = 1;
385
395
  * cross-file completion evidence (`subagentCompletions`, from both Task tool
386
396
  * results and background task-notifications): entries persisted by the
387
397
  * collapsing parser must re-parse rather than silently keep merging subagent
388
- * runs into their parent session.
398
+ * runs into their parent session. Bumped to 5 when Codex reducer checkpoints
399
+ * gained `maxTurnPromptTokens` (largest single request): a checkpoint written
400
+ * by v4 lacks the running per-request maximum, so restoring it could under-read
401
+ * the tier evidence for a cumulative session that crossed the boundary — a
402
+ * version mismatch discards it and re-parses from scratch instead. Bumped to 6
403
+ * with the user-fork inherited-baseline fix: v5 entries and checkpoints treated
404
+ * a fork's replayed parent history as the child's own usage and activity.
389
405
  */
390
- export declare const localAgentQualitativeParserVersion = 4;
406
+ export declare const localAgentQualitativeParserVersion = 6;
391
407
  /**
392
408
  * Conservative launch defaults for action-capable qualitative evidence.
393
409
  * Callers must still inspect `qualitativeCoverage` before deriving a finding:
@@ -22,7 +22,15 @@ export function latestObservedWorkingDirectory(calls) {
22
22
  .sort((left, right) => right.timestamp.localeCompare(left.timestamp))[0]
23
23
  ?.workingDirectory;
24
24
  }
25
- export const localAgentFinancialParserVersion = 1;
25
+ // Bumped to 2 when session-cumulative Codex calls gained `maxRequestPromptTokens`
26
+ // (largest single request in the session). Financial caches written by v1 lack
27
+ // this per-request tier evidence, so a >272K-cumulative Codex session would
28
+ // stay voided to "missing" on reuse; a version mismatch re-parses instead.
29
+ // Bumped to 3 when user forks (`forked_from_id`) started resetting their
30
+ // inherited baseline: v2 entries priced a fork's replayed parent history as
31
+ // the child's own usage, so those cached amounts are overstated and must not
32
+ // be reused.
33
+ export const localAgentFinancialParserVersion = 3;
26
34
  /**
27
35
  * Qualitative parser contract version. Bumped to 2 with the checkpointed
28
36
  * streaming path (A4b): entries and checkpoints written by the pre-streaming
@@ -33,9 +41,15 @@ export const localAgentFinancialParserVersion = 1;
33
41
  * cross-file completion evidence (`subagentCompletions`, from both Task tool
34
42
  * results and background task-notifications): entries persisted by the
35
43
  * collapsing parser must re-parse rather than silently keep merging subagent
36
- * runs into their parent session.
44
+ * runs into their parent session. Bumped to 5 when Codex reducer checkpoints
45
+ * gained `maxTurnPromptTokens` (largest single request): a checkpoint written
46
+ * by v4 lacks the running per-request maximum, so restoring it could under-read
47
+ * the tier evidence for a cumulative session that crossed the boundary — a
48
+ * version mismatch discards it and re-parses from scratch instead. Bumped to 6
49
+ * with the user-fork inherited-baseline fix: v5 entries and checkpoints treated
50
+ * a fork's replayed parent history as the child's own usage and activity.
37
51
  */
38
- export const localAgentQualitativeParserVersion = 4;
52
+ export const localAgentQualitativeParserVersion = 6;
39
53
  /**
40
54
  * Conservative launch defaults for action-capable qualitative evidence.
41
55
  * Callers must still inspect `qualitativeCoverage` before deriving a finding:
@@ -384,6 +398,42 @@ function parseCodexTurnUsage(value) {
384
398
  }
385
399
  };
386
400
  }
401
+ /**
402
+ * The tier-relevant prompt size of one Codex `last_token_usage` turn: its full
403
+ * request input (Codex `input_tokens` already includes cached input), which is
404
+ * exactly `effectivePromptTokens` of the parsed turn usage. Returned only when
405
+ * the field parses to a non-negative number, so an unreadable turn never lowers
406
+ * a running maximum. Used to prove whether any single request in a cumulative
407
+ * session crossed a per-request tier threshold.
408
+ */
409
+ function codexTurnPromptTokens(turn) {
410
+ const input = tokenComponentOf(turn.input_tokens);
411
+ return input === undefined ? undefined : input;
412
+ }
413
+ /**
414
+ * Whether a Codex rollout replays another session's history before its own
415
+ * work, so the cumulative counter at its first real task is an inherited
416
+ * baseline rather than this session's usage.
417
+ *
418
+ * TWO kinds of rollout do this, and only the first was recognized before:
419
+ * - subagent rollouts (`thread_source: "subagent"`, or a `source.subagent`);
420
+ * - USER FORKS (`forked_from_id`), which carry `thread_source: "user"` and
421
+ * replay the parent transcript verbatim. Observed: a forked rollout whose
422
+ * first 9,051 of 9,442 token_count events are byte-identical copies of the
423
+ * parent's, all with earlier parent timestamps, carrying 1,204,265,192 of
424
+ * its 1,256,637,395 final cumulative input tokens (95.9%) — parent usage
425
+ * that was being billed again under the child, and again down each fork
426
+ * chain.
427
+ *
428
+ * This is deliberately NOT folded into `isSubagent`: that flag also drives
429
+ * activity attribution and checkpoint identity, where a user fork is a normal
430
+ * user session and must not be relabelled a subagent.
431
+ */
432
+ function codexHasInheritedHistory(payload) {
433
+ return stringOf(payload.thread_source) === "subagent" ||
434
+ isRecord(payload.source) && "subagent" in payload.source ||
435
+ stringOf(payload.forked_from_id) !== undefined;
436
+ }
387
437
  /** Parse one Claude Code transcript (JSONL). Exported for tests. */
388
438
  export function parseClaudeCodeTranscript(content, filePath = "", sinceMs, onDiagnostic) {
389
439
  const calls = [];
@@ -634,6 +684,7 @@ function createCodexRolloutParserState() {
634
684
  fileCounts: new Map(),
635
685
  toolCallCount: 0,
636
686
  isSubagent: false,
687
+ hasInheritedHistory: false,
637
688
  malformedLines: 0
638
689
  };
639
690
  }
@@ -666,6 +717,7 @@ function consumeCodexRolloutLine(state, line, onEntry) {
666
717
  state.rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
667
718
  state.isSubagent = stringOf(payload.thread_source) === "subagent" ||
668
719
  isRecord(payload.source) && "subagent" in payload.source;
720
+ state.hasInheritedHistory = codexHasInheritedHistory(payload);
669
721
  state.parentSessionId = stringOf(payload.parent_thread_id);
670
722
  }
671
723
  if (entry.type === "turn_context" && payload) {
@@ -677,7 +729,7 @@ function consumeCodexRolloutLine(state, line, onEntry) {
677
729
  state.rootCwd = stringOf(payload.cwd);
678
730
  }
679
731
  }
680
- if (state.isSubagent &&
732
+ if (state.hasInheritedHistory &&
681
733
  !state.rootTaskStarted &&
682
734
  payload?.type === "task_started" &&
683
735
  isRootSpecificTaskStart(payload.started_at, state.rootStartedAtMs)) {
@@ -688,6 +740,11 @@ function consumeCodexRolloutLine(state, line, onEntry) {
688
740
  // prompts/files cannot become the child's focus. Restored checkpoint
689
741
  // evidence predating the boundary is parent history too and resets with
690
742
  // it (the raw session cwd deliberately survives, exactly as rootCwd).
743
+ //
744
+ // Replayed task_started events keep the PARENT's original started_at, so
745
+ // `isRootSpecificTaskStart` rejects them and the first accepted task is
746
+ // this session's own — which is why the boundary lands exactly at the end
747
+ // of the replay.
691
748
  state.inheritedUsageBaseline = state.lastTotal;
692
749
  state.lastTotal = undefined;
693
750
  state.rootTaskStarted = true;
@@ -699,10 +756,11 @@ function consumeCodexRolloutLine(state, line, onEntry) {
699
756
  state.toolCallCount = 0;
700
757
  state.model = undefined;
701
758
  state.lastTurn = undefined;
759
+ state.maxTurnPromptTokens = undefined;
702
760
  state.lastRateLimits = undefined;
703
761
  state.lastActivityAt = toIso(stringOf(entry.timestamp)) ?? state.startedAt;
704
762
  }
705
- if (payload?.type === "task_started" && (!state.isSubagent || state.rootTaskStarted)) {
763
+ if (payload?.type === "task_started" && (!state.hasInheritedHistory || state.rootTaskStarted)) {
706
764
  state.pendingTaskTurnId = stringOf(payload.turn_id);
707
765
  state.completedTask = undefined;
708
766
  }
@@ -757,6 +815,10 @@ function consumeCodexRolloutLine(state, line, onEntry) {
757
815
  if (turn) {
758
816
  state.lastTurn = turn;
759
817
  state.lastActivityAt = eventTimestamp;
818
+ const turnPrompt = codexTurnPromptTokens(turn);
819
+ if (turnPrompt !== undefined) {
820
+ state.maxTurnPromptTokens = Math.max(state.maxTurnPromptTokens ?? 0, turnPrompt);
821
+ }
760
822
  }
761
823
  const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
762
824
  if (rateLimits) {
@@ -774,7 +836,7 @@ function finishCodexRolloutParse(state, onDiagnostic) {
774
836
  if (state.malformedLines > 0) {
775
837
  onDiagnostic?.({ code: "malformed_jsonl", count: state.malformedLines });
776
838
  }
777
- if (!state.lastTotal || state.isSubagent && !state.rootTaskStarted)
839
+ if (!state.lastTotal || state.hasInheritedHistory && !state.rootTaskStarted)
778
840
  return [];
779
841
  const parsedUsage = parseCodexCumulativeUsage(state.lastTotal, state.inheritedUsageBaseline);
780
842
  const parsedTurn = state.lastTurn
@@ -834,6 +896,9 @@ function finishCodexRolloutParse(state, onDiagnostic) {
834
896
  : {}),
835
897
  usageScope: "session_cumulative",
836
898
  usageSupport,
899
+ ...(usageSupport === "complete" && state.maxTurnPromptTokens !== undefined
900
+ ? { maxRequestPromptTokens: state.maxTurnPromptTokens }
901
+ : {}),
837
902
  ...(parsedUsage.reportedTotalTokens !== undefined
838
903
  ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
839
904
  : {}),
@@ -1999,6 +2064,7 @@ function serializeCodexReducerState(state) {
1999
2064
  ...(state.rootStartedAtMs !== undefined ? { rootStartedAtMs: state.rootStartedAtMs } : {}),
2000
2065
  rootTaskStarted: state.rootTaskStarted,
2001
2066
  isSubagent: state.isSubagent,
2067
+ hasInheritedHistory: state.hasInheritedHistory,
2002
2068
  ...(state.parentSessionId !== undefined ? { parentSessionId: state.parentSessionId } : {}),
2003
2069
  ...(state.pendingTaskTurnId !== undefined ? { pendingTaskTurnId: state.pendingTaskTurnId } : {}),
2004
2070
  ...(state.completedTask !== undefined ? { completedTask: state.completedTask } : {}),
@@ -2006,6 +2072,9 @@ function serializeCodexReducerState(state) {
2006
2072
  ...(state.lastActivityAt !== undefined ? { lastActivityAt: state.lastActivityAt } : {}),
2007
2073
  ...(state.lastTotal !== undefined ? { lastTotal: state.lastTotal } : {}),
2008
2074
  ...(state.lastTurn !== undefined ? { lastTurn: state.lastTurn } : {}),
2075
+ ...(state.maxTurnPromptTokens !== undefined
2076
+ ? { maxTurnPromptTokens: state.maxTurnPromptTokens }
2077
+ : {}),
2009
2078
  ...(state.inheritedUsageBaseline !== undefined
2010
2079
  ? { inheritedUsageBaseline: state.inheritedUsageBaseline }
2011
2080
  : {}),
@@ -2050,6 +2119,8 @@ function isPersistedCodexReducerState(value) {
2050
2119
  return typeof value.rootSessionMetaSeen === "boolean" &&
2051
2120
  typeof value.rootTaskStarted === "boolean" &&
2052
2121
  typeof value.isSubagent === "boolean" &&
2122
+ (value.hasInheritedHistory === undefined ||
2123
+ typeof value.hasInheritedHistory === "boolean") &&
2053
2124
  optionalString(value.model) &&
2054
2125
  optionalString(value.sessionId) &&
2055
2126
  optionalString(value.sourceVersion) &&
@@ -2062,6 +2133,7 @@ function isPersistedCodexReducerState(value) {
2062
2133
  optionalString(value.lastActivityAt) &&
2063
2134
  optionalRecord(value.lastTotal) &&
2064
2135
  optionalRecord(value.lastTurn) &&
2136
+ optionalFinite(value.maxTurnPromptTokens) &&
2065
2137
  optionalRecord(value.inheritedUsageBaseline) &&
2066
2138
  validRateLimits &&
2067
2139
  validRootCwd &&
@@ -2084,6 +2156,10 @@ function restoreCodexReducerState(persisted) {
2084
2156
  state.rootStartedAtMs = persisted.rootStartedAtMs;
2085
2157
  state.rootTaskStarted = persisted.rootTaskStarted;
2086
2158
  state.isSubagent = persisted.isSubagent;
2159
+ // Pre-fork-fix checkpoints carry no flag; fall back to the subagent bit so a
2160
+ // restored subagent keeps its boundary (a restored user fork re-parses, its
2161
+ // parser version having changed).
2162
+ state.hasInheritedHistory = persisted.hasInheritedHistory ?? persisted.isSubagent;
2087
2163
  state.parentSessionId = persisted.parentSessionId;
2088
2164
  state.pendingTaskTurnId = persisted.pendingTaskTurnId;
2089
2165
  state.completedTask = persisted.completedTask;
@@ -2091,6 +2167,7 @@ function restoreCodexReducerState(persisted) {
2091
2167
  state.lastActivityAt = persisted.lastActivityAt;
2092
2168
  state.lastTotal = persisted.lastTotal;
2093
2169
  state.lastTurn = persisted.lastTurn;
2170
+ state.maxTurnPromptTokens = persisted.maxTurnPromptTokens;
2094
2171
  state.inheritedUsageBaseline = persisted.inheritedUsageBaseline;
2095
2172
  state.lastRateLimits = persisted.lastRateLimits;
2096
2173
  state.restoredRootCwd = persisted.rootCwd;
@@ -2726,8 +2803,11 @@ async function readCodexFinancialFile(file) {
2726
2803
  const rootPayload = rootEntry?.type === "session_meta" && isRecord(rootEntry.payload)
2727
2804
  ? rootEntry.payload
2728
2805
  : undefined;
2729
- const isSubagent = Boolean(rootPayload) && (stringOf(rootPayload?.thread_source) === "subagent" ||
2730
- isRecord(rootPayload?.source) && "subagent" in rootPayload.source);
2806
+ // A rollout that replays another session's history must be read WHOLE:
2807
+ // its inherited-baseline boundary sits mid-file, and a proof-complete tail
2808
+ // would never reach it. User forks need this exactly as subagents do.
2809
+ const hasInheritedHistory = rootPayload !== undefined &&
2810
+ codexHasInheritedHistory(rootPayload);
2731
2811
  const entriesReverse = [];
2732
2812
  let malformedLines = 0;
2733
2813
  let prefilteredLines = 0;
@@ -2738,7 +2818,7 @@ async function readCodexFinancialFile(file) {
2738
2818
  let suffixPartsReverse = [];
2739
2819
  let stoppedEarly = false;
2740
2820
  let bytesSkipped = 0;
2741
- const proof = createCodexReverseProof(isSubagent, !rootPayload);
2821
+ const proof = createCodexReverseProof(hasInheritedHistory, !rootPayload);
2742
2822
  while (position > 0 && !stoppedEarly) {
2743
2823
  const chunkStart = Math.max(0, position - FINANCIAL_REVERSE_CHUNK_BYTES);
2744
2824
  const length = position - chunkStart;
@@ -2979,9 +3059,9 @@ function skipJsonWhitespace(input, start) {
2979
3059
  index += 1;
2980
3060
  return index;
2981
3061
  }
2982
- function createCodexReverseProof(isSubagent, forceFullScan) {
3062
+ function createCodexReverseProof(hasInheritedHistory, forceFullScan) {
2983
3063
  return {
2984
- isSubagent,
3064
+ hasInheritedHistory,
2985
3065
  forceFullScan,
2986
3066
  totalSeen: false,
2987
3067
  turnSeen: false,
@@ -2990,7 +3070,7 @@ function createCodexReverseProof(isSubagent, forceFullScan) {
2990
3070
  };
2991
3071
  }
2992
3072
  function observeCodexReverseProof(proof, entry) {
2993
- if (proof.forceFullScan || proof.isSubagent)
3073
+ if (proof.forceFullScan || proof.hasInheritedHistory)
2994
3074
  return false;
2995
3075
  const payload = isRecord(entry.payload) ? entry.payload : undefined;
2996
3076
  const info = payload?.type === "token_count" && isRecord(payload.info)
@@ -3007,7 +3087,7 @@ function observeCodexReverseProof(proof, entry) {
3007
3087
  if (entry.type === "turn_context" && stringOf(payload?.model)) {
3008
3088
  proof.modelSeen = true;
3009
3089
  }
3010
- return !proof.isSubagent &&
3090
+ return !proof.hasInheritedHistory &&
3011
3091
  proof.totalSeen &&
3012
3092
  proof.turnSeen &&
3013
3093
  proof.rateLimitsSeen &&
@@ -3112,7 +3192,8 @@ function createCodexFinancialStreamState() {
3112
3192
  return {
3113
3193
  rootSessionMetaSeen: false,
3114
3194
  rootTaskStarted: false,
3115
- isSubagent: false
3195
+ isSubagent: false,
3196
+ hasInheritedHistory: false
3116
3197
  };
3117
3198
  }
3118
3199
  function consumeCodexFinancialEntry(state, entry) {
@@ -3126,12 +3207,13 @@ function consumeCodexFinancialEntry(state, entry) {
3126
3207
  state.rootStartedAtMs = timestampMilliseconds(payload.timestamp ?? entry.timestamp);
3127
3208
  state.isSubagent = stringOf(payload.thread_source) === "subagent" ||
3128
3209
  isRecord(payload.source) && "subagent" in payload.source;
3210
+ state.hasInheritedHistory = codexHasInheritedHistory(payload);
3129
3211
  }
3130
3212
  if (entry.type === "turn_context" && payload) {
3131
3213
  state.model = stringOf(payload.model) ?? state.model;
3132
3214
  state.rootCwd ??= stringOf(payload.cwd);
3133
3215
  }
3134
- if (state.isSubagent &&
3216
+ if (state.hasInheritedHistory &&
3135
3217
  !state.rootTaskStarted &&
3136
3218
  payload?.type === "task_started" &&
3137
3219
  isRootSpecificTaskStart(payload.started_at, state.rootStartedAtMs)) {
@@ -3140,6 +3222,7 @@ function consumeCodexFinancialEntry(state, entry) {
3140
3222
  state.rootTaskStarted = true;
3141
3223
  state.model = undefined;
3142
3224
  state.lastTurn = undefined;
3225
+ state.maxTurnPromptTokens = undefined;
3143
3226
  state.lastRateLimits = undefined;
3144
3227
  state.lastActivityAt = toIso(stringOf(entry.timestamp)) ?? state.startedAt;
3145
3228
  }
@@ -3162,13 +3245,17 @@ function consumeCodexFinancialEntry(state, entry) {
3162
3245
  if (turn) {
3163
3246
  state.lastTurn = turn;
3164
3247
  state.lastActivityAt = eventTimestamp;
3248
+ const turnPrompt = codexTurnPromptTokens(turn);
3249
+ if (turnPrompt !== undefined) {
3250
+ state.maxTurnPromptTokens = Math.max(state.maxTurnPromptTokens ?? 0, turnPrompt);
3251
+ }
3165
3252
  }
3166
3253
  const rateLimits = parseCodexRateLimits(payload.rate_limits, eventTimestamp);
3167
3254
  if (rateLimits)
3168
3255
  state.lastRateLimits = rateLimits;
3169
3256
  }
3170
3257
  function finishCodexFinancialStream(state, onDiagnostic) {
3171
- if (!state.lastTotal || state.isSubagent && !state.rootTaskStarted)
3258
+ if (!state.lastTotal || state.hasInheritedHistory && !state.rootTaskStarted)
3172
3259
  return undefined;
3173
3260
  const parsedUsage = parseCodexCumulativeUsage(state.lastTotal, state.inheritedUsageBaseline);
3174
3261
  const parsedTurn = state.lastTurn
@@ -3196,6 +3283,9 @@ function finishCodexFinancialStream(state, onDiagnostic) {
3196
3283
  : {}),
3197
3284
  usageScope: "session_cumulative",
3198
3285
  usageSupport,
3286
+ ...(usageSupport === "complete" && state.maxTurnPromptTokens !== undefined
3287
+ ? { maxRequestPromptTokens: state.maxTurnPromptTokens }
3288
+ : {}),
3199
3289
  ...(parsedUsage.reportedTotalTokens !== undefined
3200
3290
  ? { reportedTotalTokens: parsedUsage.reportedTotalTokens }
3201
3291
  : {}),
@@ -3234,10 +3324,14 @@ export function aggregateCallsForFormats(calls, descriptors) {
3234
3324
  const usageSupported = groupCalls.every((call) => call.usageSupport !== "unsupported_token_shape");
3235
3325
  const sourceVersions = [...new Set(groupCalls.flatMap((call) => call.sourceVersion ? [call.sourceVersion] : []))].sort().slice(0, 8);
3236
3326
  const tieredPricingEvidenceSupported = !usesPromptTieredPricing(model) ||
3237
- groupCalls.every((call) => canPriceTokenUsageAtScope(model, call.usage, call.usageScope === "turn" ? "request" : "aggregate") && (agent !== "gemini-cli" || hasCompleteGeminiPromptEvidence(call)));
3327
+ groupCalls.every((call) => canPriceTokenUsageAtScope(model, call.usage, call.usageScope === "turn" ? "request" : "aggregate", call.maxRequestPromptTokens) && (agent !== "gemini-cli" || hasCompleteGeminiPromptEvidence(call)));
3238
3328
  const amountUsd = usageSupported && tieredPricingEvidenceSupported && format
3239
3329
  ? usesPromptTieredPricing(model)
3240
- ? estimateTokenCostsUsd(model, groupCalls.map((call) => call.usage))
3330
+ ? estimateTokenCostsUsd(model, groupCalls.map((call) => call.usage),
3331
+ // Fix each cumulative slice's tier from its largest single request
3332
+ // rather than its cache-inflated sum. Turn-scoped slices carry no
3333
+ // such evidence and keep selecting their tier from their own prompt.
3334
+ groupCalls.map((call) => call.maxRequestPromptTokens))
3241
3335
  : estimateTokenCostUsd(model, usage)
3242
3336
  : undefined;
3243
3337
  const priced = usageSupported && typeof amountUsd === "number";
@@ -50,8 +50,17 @@ export declare function estimateTokenCostUsd(model: string, usage: TokenUsage):
50
50
  * models whose entire request moves to a higher rate above a prompt-size
51
51
  * threshold; pricing a daily token sum would incorrectly treat many small
52
52
  * requests as one large request.
53
+ *
54
+ * `tierPromptTokens[i]`, when provided, fixes the tier of `usages[i]` from
55
+ * request-level evidence instead of the slice's own prompt total. A
56
+ * session-cumulative slice is a sum of many requests whose prompt total
57
+ * routinely clears a per-request threshold on cache reads alone, even though no
58
+ * single request did; supplying the largest single request's prompt keeps such
59
+ * a slice on the base tier (and pricing it there is exact, since the base rate
60
+ * distributes over the sum). Omitting the array preserves single-request
61
+ * behaviour: each slice's own prompt selects its tier.
53
62
  */
54
- export declare function estimateTokenCostsUsd(model: string, usages: readonly TokenUsage[]): number | undefined;
63
+ export declare function estimateTokenCostsUsd(model: string, usages: readonly TokenUsage[], tierPromptTokens?: readonly (number | undefined)[]): number | undefined;
55
64
  /** Whether this model's rate selection depends on each request's prompt size. */
56
65
  export declare function usesPromptTieredPricing(model: string): boolean;
57
66
  /** Prompt-size threshold for tiered request pricing, when one is published. */
@@ -60,9 +69,13 @@ export declare function promptTierThreshold(model: string): number | undefined;
60
69
  * Tiered prices are selected per request, never from a multi-request sum.
61
70
  * An aggregate is still unambiguous when its entire non-negative prompt-side
62
71
  * total is at or below the threshold; then no constituent request can have
63
- * crossed it. Larger aggregates fail closed until request-level evidence is
64
- * available.
72
+ * crossed it. It is also unambiguous when request-level evidence
73
+ * (`maxRequestPromptTokens`, the largest single request the aggregate contains)
74
+ * proves that no constituent request crossed the threshold: every request was
75
+ * base-tier, so the whole sum is base-tier and prices exactly at the base rate.
76
+ * Larger aggregates without such evidence fail closed to keep an unpriceable
77
+ * total honestly "missing" rather than guessing a tier.
65
78
  */
66
- export declare function canPriceTokenUsageAtScope(model: string, usage: TokenUsage, scope: "request" | "aggregate"): boolean;
79
+ export declare function canPriceTokenUsageAtScope(model: string, usage: TokenUsage, scope: "request" | "aggregate", maxRequestPromptTokens?: number): boolean;
67
80
  export {};
68
81
  //# sourceMappingURL=modelPricing.d.ts.map