@agent-finops/core 0.9.8 → 0.9.10

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.
@@ -1351,6 +1351,8 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
1351
1351
  const home = homedir();
1352
1352
  const since = options.sinceIso ? Date.parse(options.sinceIso) : undefined;
1353
1353
  const sinceMs = typeof since === "number" && Number.isFinite(since) ? since : undefined;
1354
+ const until = options.workspaceDailyFacts && options.untilIso ? Date.parse(options.untilIso) : undefined;
1355
+ const untilMs = typeof until === "number" && Number.isFinite(until) ? until : undefined;
1354
1356
  const scanned = await Promise.all(registry.map(async (runtime) => {
1355
1357
  const { descriptor } = runtime;
1356
1358
  const diagnostics = [];
@@ -1422,7 +1424,7 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
1422
1424
  const diagnosticsBefore = diagnostics.length;
1423
1425
  const unreadableBefore = scan.unreadableFiles;
1424
1426
  const filesParsedBefore = scan.filesParsed;
1425
- const context = { filePath: file, sinceMs, scan, diagnostics };
1427
+ const context = { filePath: file, sinceMs, untilMs, scan, diagnostics };
1426
1428
  const parsedCalls = options.workspaceDailyFacts && descriptor.id === "codex"
1427
1429
  ? await readCodexDailyFinancialFile(context)
1428
1430
  : options.workspaceDailyFacts && descriptor.id === "claude-code"
@@ -1453,8 +1455,12 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
1453
1455
  }));
1454
1456
  // Sources scan concurrently, but flatten in registry order to preserve the
1455
1457
  // long-standing Claude-then-Codex output and diagnostic contract.
1458
+ const inWorkspaceWindow = (call) => {
1459
+ const timestamp = Date.parse(call.timestamp);
1460
+ return (sinceMs === undefined || timestamp >= sinceMs) && (untilMs === undefined || timestamp < untilMs);
1461
+ };
1456
1462
  const calls = scanned.flatMap((entry) => entry.calls);
1457
- const normalizedCalls = dedupeCumulativeSessionCalls(calls, (agent) => {
1463
+ const normalizedCalls = dedupeCumulativeSessionCalls(options.workspaceDailyFacts ? calls.filter(inWorkspaceWindow) : calls, (agent) => {
1458
1464
  const source = scanned.find((entry) => entry.scan.agent === agent);
1459
1465
  if (source) {
1460
1466
  recordParseDiagnostic(agent, source.scan, source.diagnostics, {
@@ -1463,9 +1469,7 @@ export async function loadLocalAgentFinancialUsageWithFormats(registry, options
1463
1469
  });
1464
1470
  }
1465
1471
  });
1466
- const filtered = typeof sinceMs === "number"
1467
- ? normalizedCalls.filter((call) => Date.parse(call.timestamp) >= sinceMs)
1468
- : normalizedCalls;
1472
+ const filtered = normalizedCalls.filter(inWorkspaceWindow);
1469
1473
  return {
1470
1474
  records: aggregateCallsForFormats(filtered, registry.map((entry) => entry.descriptor)),
1471
1475
  calls: filtered,
@@ -1610,7 +1614,7 @@ function codexHeaderAttribution(header) {
1610
1614
  }
1611
1615
  /** @internal Runtime hook owned by the Claude Code registry entry. */
1612
1616
  export async function readClaudeCodeFinancialFileForRegistry(context, workspaceDailyFacts = false) {
1613
- const { filePath, sinceMs, scan, diagnostics } = context;
1617
+ const { filePath, sinceMs, untilMs, scan, diagnostics } = context;
1614
1618
  if (!await shouldStreamFile(filePath, sinceMs, "claude-code", scan, diagnostics)) {
1615
1619
  return [];
1616
1620
  }
@@ -1620,11 +1624,21 @@ export async function readClaudeCodeFinancialFileForRegistry(context, workspaceD
1620
1624
  let streamed;
1621
1625
  try {
1622
1626
  streamed = await streamJsonlRecords(filePath, (entry) => {
1627
+ if (workspaceDailyFacts && entry.type === "assistant" && isRecord(entry.message)
1628
+ && isRecord(entry.message.usage) && stringOf(entry.message.model) !== "<synthetic>"
1629
+ && !toIso(stringOf(entry.timestamp))) {
1630
+ fileDiagnostics.push({ code: "unsupported_token_shape", count: 1, workspaceReason: "missing_identity" });
1631
+ return;
1632
+ }
1633
+ const timestamp = toIso(stringOf(entry.timestamp));
1634
+ if (workspaceDailyFacts && timestamp && untilMs !== undefined && Date.parse(timestamp) >= untilMs)
1635
+ return;
1636
+ const entryDiagnostics = [];
1623
1637
  const call = parseClaudeFinancialEntry(entry, filePath,
1624
- // Window-blind on purpose: cached values must contain the complete
1625
- // file so a narrow-window run can never truncate a wider one. The
1626
- // loader's final timestamp filter performs all narrowing.
1627
- undefined, seen, (diagnostic) => fileDiagnostics.push(diagnostic));
1638
+ // Workspace mode disables the cumulative cache, so dated historical
1639
+ // events outside its window must not contribute diagnostics. Ordinary
1640
+ // financial cache parses remain window-blind.
1641
+ workspaceDailyFacts ? sinceMs : undefined, seen, (diagnostic) => entryDiagnostics.push(diagnostic));
1628
1642
  if (call) {
1629
1643
  if (workspaceDailyFacts) {
1630
1644
  const nativeAgent = stringOf(entry.agentId);
@@ -1635,7 +1649,12 @@ export async function readClaudeCodeFinancialFileForRegistry(context, workspaceD
1635
1649
  }
1636
1650
  calls.push(call);
1637
1651
  }
1638
- });
1652
+ for (const diagnostic of entryDiagnostics) {
1653
+ fileDiagnostics.push(workspaceDailyFacts && call?.sessionId && timestamp
1654
+ && call.usageSupport === "unsupported_token_shape" && diagnostic.code === "unsupported_token_shape"
1655
+ ? { ...diagnostic, workspaceFactCoverage: "unknown_tokens" } : diagnostic);
1656
+ }
1657
+ }, workspaceDailyFacts);
1639
1658
  }
1640
1659
  catch (error) {
1641
1660
  recordUnreadableFile("claude-code", scan, diagnostics, error);
@@ -1697,13 +1716,42 @@ export async function readCodexFinancialFileForRegistry(context) {
1697
1716
  });
1698
1717
  return call ? [call] : [];
1699
1718
  }
1719
+ /** Privacy-safe explanation only. This never changes whether a counter is supported. */
1720
+ function workspaceCounterFailureReason(current, baseline) {
1721
+ const input = tokenComponentOf(current.input_tokens), output = tokenComponentOf(current.output_tokens);
1722
+ const cached = optionalTokenComponent(current, "cached_input_tokens");
1723
+ const total = optionalTokenComponent(current, "total_tokens");
1724
+ if (input === undefined || output === undefined || cached.present && cached.value === undefined
1725
+ || total.present && total.value === undefined)
1726
+ return "missing_or_invalid_components";
1727
+ if ((cached.value ?? 0) > input)
1728
+ return "cache_exceeds_input";
1729
+ if (total.value !== undefined && total.value < input + output)
1730
+ return "total_below_components";
1731
+ if ((total.value ?? 0) > 0 && input === 0 && output === 0)
1732
+ return "positive_total_without_components";
1733
+ if (baseline) {
1734
+ if (!parseCodexCumulativeUsage(baseline).supported)
1735
+ return "invalid_baseline";
1736
+ const priorInput = tokenComponentOf(baseline.input_tokens);
1737
+ const priorOutput = tokenComponentOf(baseline.output_tokens);
1738
+ const priorCached = optionalTokenComponent(baseline, "cached_input_tokens").value ?? 0;
1739
+ const priorTotal = optionalTokenComponent(baseline, "total_tokens").value;
1740
+ if (input < priorInput || output < priorOutput || (cached.value ?? 0) < priorCached
1741
+ || input - (cached.value ?? 0) < priorInput - priorCached
1742
+ || total.value !== undefined && priorTotal !== undefined && total.value < priorTotal)
1743
+ return "counter_decreased";
1744
+ }
1745
+ return "unsupported_components";
1746
+ }
1700
1747
  /** Workspace-only event deltas. The existing snapshot reader remains cumulative. */
1701
1748
  async function readCodexDailyFinancialFile(context) {
1702
- const { filePath, sinceMs, scan, diagnostics } = context;
1749
+ const { filePath, sinceMs, untilMs, scan, diagnostics } = context;
1703
1750
  if (!await shouldStreamFile(filePath, sinceMs, "codex", scan, diagnostics))
1704
1751
  return [];
1705
1752
  const state = createCodexFinancialStreamState(), calls = [];
1706
- const report = () => recordParseDiagnostic("codex", scan, diagnostics, { code: "unsupported_token_shape", count: 1 });
1753
+ const report = (workspaceReason, markerBacked = false) => recordParseDiagnostic("codex", scan, diagnostics, { code: "unsupported_token_shape", count: 1, workspaceReason,
1754
+ ...(markerBacked ? { workspaceFactCoverage: "unknown_tokens" } : {}) });
1707
1755
  let priorEventAt;
1708
1756
  try {
1709
1757
  const streamed = await streamJsonlRecords(filePath, entry => {
@@ -1713,18 +1761,21 @@ async function readCodexDailyFinancialFile(context) {
1713
1761
  if (entry.type !== "event_msg" || payload?.type !== "token_count"
1714
1762
  || state.hasInheritedHistory && !state.rootTaskStarted)
1715
1763
  return;
1764
+ const timestamp = toIso(stringOf(entry.timestamp));
1765
+ const inWindow = !timestamp || (sinceMs === undefined || Date.parse(timestamp) >= sinceMs)
1766
+ && (untilMs === undefined || Date.parse(timestamp) < untilMs);
1716
1767
  const info = isRecord(payload.info) ? payload.info : undefined;
1717
1768
  const total = info && isRecord(info.total_token_usage) ? info.total_token_usage : undefined;
1718
1769
  if (!total) {
1719
1770
  // A usage-bearing event without its cumulative endpoint cannot be
1720
1771
  // placed safely: a later counter may span this event's UTC day.
1721
- if (info && isRecord(info.last_token_usage))
1722
- report();
1772
+ if (inWindow && info && isRecord(info.last_token_usage))
1773
+ report("missing_endpoint");
1723
1774
  return;
1724
1775
  }
1725
- const timestamp = toIso(stringOf(entry.timestamp));
1726
1776
  if (!timestamp || !state.sessionId) {
1727
- report();
1777
+ if (inWindow)
1778
+ report("missing_identity");
1728
1779
  return;
1729
1780
  }
1730
1781
  const parsed = parseCodexCumulativeUsage(total, previousTotal);
@@ -1740,8 +1791,6 @@ async function readCodexDailyFinancialFile(context) {
1740
1791
  if (supported && previousTotal && parsed.usage.inputTokens === 0 && parsed.usage.outputTokens === 0
1741
1792
  && (parsed.usage.cacheReadTokens ?? 0) === 0 && (parsed.reportedTotalTokens ?? 0) === 0)
1742
1793
  return;
1743
- if (!supported)
1744
- report();
1745
1794
  const workingDirectory = absoluteWorkingDirectory(state.rootCwd);
1746
1795
  // The cumulative endpoint fingerprint is stable across copies of a
1747
1796
  // rollout. Different deltas for that same endpoint become a conflict in
@@ -1756,7 +1805,10 @@ async function readCodexDailyFinancialFile(context) {
1756
1805
  usage: parsed.usage,
1757
1806
  ...(supported && parsed.tokenComponentEvidence ? { tokenComponentEvidence: parsed.tokenComponentEvidence } : {}),
1758
1807
  ...(parsed.reportedTotalTokens !== undefined ? { reportedTotalTokens: parsed.reportedTotalTokens } : {}) });
1759
- });
1808
+ if (inWindow && !supported)
1809
+ report(!chronological ? "nonchronological" : !firstDayKnown ? "first_day_unknown"
1810
+ : !cachedShapeStable ? "cache_shape" : workspaceCounterFailureReason(total, previousTotal), chronological);
1811
+ }, true);
1760
1812
  if (streamed.hadContent)
1761
1813
  scan.filesParsed++;
1762
1814
  if (streamed.malformedLines)
@@ -2840,14 +2892,93 @@ function financialSourceScan(descriptor) {
2840
2892
  scan.jsonlValidationCoverage = "complete";
2841
2893
  return scan;
2842
2894
  }
2843
- async function streamJsonlRecords(file, onRecord) {
2895
+ /** Re-encode literal control characters inside JSON strings without changing their value.
2896
+ * Ordinary readers stay strict; this compatibility path is reserved for Workspace facts. */
2897
+ function parseWorkspaceJsonRecord(text) {
2898
+ try {
2899
+ return { value: JSON.parse(text) };
2900
+ }
2901
+ catch { /* Try a lossless string encoding repair. */ }
2902
+ let quoted = false, escaped = false, normalized = "";
2903
+ for (const character of text) {
2904
+ if (quoted && character.charCodeAt(0) < 32) {
2905
+ // An existing escape before a raw control character has no unambiguous
2906
+ // JSON interpretation. Do not guess its intended value.
2907
+ if (escaped)
2908
+ return undefined;
2909
+ normalized += `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
2910
+ continue;
2911
+ }
2912
+ normalized += character;
2913
+ if (escaped) {
2914
+ escaped = false;
2915
+ continue;
2916
+ }
2917
+ if (quoted && character === "\\") {
2918
+ escaped = true;
2919
+ continue;
2920
+ }
2921
+ if (character === '"')
2922
+ quoted = !quoted;
2923
+ }
2924
+ try {
2925
+ return { value: JSON.parse(normalized) };
2926
+ }
2927
+ catch {
2928
+ return undefined;
2929
+ }
2930
+ }
2931
+ async function streamJsonlRecords(file, onRecord, workspaceCompatibility = false) {
2844
2932
  const input = createReadStream(file, { encoding: "utf8" });
2845
2933
  const lines = createInterface({ input, crlfDelay: Infinity });
2846
2934
  let hadContent = false;
2847
2935
  let malformedLines = 0;
2936
+ let pending = [], pendingBytes = 0;
2937
+ const accept = (value) => { if (isRecord(value))
2938
+ onRecord(value); };
2848
2939
  try {
2849
2940
  for await (const line of lines) {
2850
2941
  hadContent = true;
2942
+ if (workspaceCompatibility) {
2943
+ if (!pending.length && !line.trim())
2944
+ continue;
2945
+ if (pending.length) {
2946
+ pendingBytes += Buffer.byteLength(line, "utf8") + 1;
2947
+ if (pending.length < 256 && pendingBytes <= 32 * 1024 * 1024) {
2948
+ const combined = parseWorkspaceJsonRecord([...pending, line].join("\n"));
2949
+ if (combined) {
2950
+ accept(combined.value);
2951
+ pending = [];
2952
+ pendingBytes = 0;
2953
+ continue;
2954
+ }
2955
+ }
2956
+ // A complete following record is a boundary. Never let a damaged
2957
+ // preceding record swallow otherwise readable financial evidence.
2958
+ const standalone = parseWorkspaceJsonRecord(line);
2959
+ if (standalone) {
2960
+ malformedLines += pending.length;
2961
+ pending = [];
2962
+ pendingBytes = 0;
2963
+ accept(standalone.value);
2964
+ continue;
2965
+ }
2966
+ if (pending.length >= 256 || pendingBytes > 32 * 1024 * 1024) {
2967
+ malformedLines += pending.length;
2968
+ pending = [];
2969
+ pendingBytes = 0;
2970
+ }
2971
+ }
2972
+ const parsed = parseWorkspaceJsonRecord(line);
2973
+ if (parsed) {
2974
+ accept(parsed.value);
2975
+ continue;
2976
+ }
2977
+ pending.push(line);
2978
+ if (pending.length === 1)
2979
+ pendingBytes = Buffer.byteLength(line, "utf8");
2980
+ continue;
2981
+ }
2851
2982
  if (!line.trim())
2852
2983
  continue;
2853
2984
  let entry;
@@ -2858,12 +2989,9 @@ async function streamJsonlRecords(file, onRecord) {
2858
2989
  malformedLines += 1;
2859
2990
  continue;
2860
2991
  }
2861
- // Parsed event type is checked before any deeper traversal. This keeps
2862
- // qualitative payloads out of the fast path while still validating
2863
- // every JSONL line and reporting malformed coverage honestly.
2864
- if (isRecord(entry))
2865
- onRecord(entry);
2992
+ accept(entry);
2866
2993
  }
2994
+ malformedLines += pending.length;
2867
2995
  }
2868
2996
  finally {
2869
2997
  lines.close();
@@ -3639,6 +3767,8 @@ function recordParseDiagnostic(agent, scan, diagnostics, diagnostic) {
3639
3767
  agent,
3640
3768
  code: diagnostic.code,
3641
3769
  severity: "warning",
3770
+ ...(diagnostic.workspaceReason ? { workspaceReason: diagnostic.workspaceReason } : {}),
3771
+ ...(diagnostic.workspaceFactCoverage ? { workspaceFactCoverage: diagnostic.workspaceFactCoverage } : {}),
3642
3772
  message: `${diagnostic.count} ${agentLabel(agent)} token snapshot(s) lacked the complete, internally consistent fields required for safe normalization and pricing.`,
3643
3773
  count: diagnostic.count
3644
3774
  });
@@ -150,9 +150,9 @@ declare const documentSchema: z.ZodObject<{
150
150
  tool: z.ZodOptional<z.ZodNumber>;
151
151
  total: z.ZodOptional<z.ZodNumber>;
152
152
  cacheAccounting: z.ZodEnum<{
153
- unknown: "unknown";
154
153
  included: "included";
155
154
  none: "none";
155
+ unknown: "unknown";
156
156
  }>;
157
157
  }, z.core.$strict>>;
158
158
  usage: z.ZodObject<{
@@ -189,8 +189,8 @@ declare const documentSchema: z.ZodObject<{
189
189
  activity: z.ZodOptional<z.ZodObject<{
190
190
  summary: z.ZodString;
191
191
  kind: z.ZodEnum<{
192
- project: "project";
193
192
  agent: "agent";
193
+ project: "project";
194
194
  file: "file";
195
195
  task: "task";
196
196
  automation: "automation";
@@ -369,9 +369,9 @@ declare const documentSchema: z.ZodObject<{
369
369
  tool: z.ZodOptional<z.ZodNumber>;
370
370
  total: z.ZodOptional<z.ZodNumber>;
371
371
  cacheAccounting: z.ZodEnum<{
372
- unknown: "unknown";
373
372
  included: "included";
374
373
  none: "none";
374
+ unknown: "unknown";
375
375
  }>;
376
376
  }, z.core.$strict>>;
377
377
  usage: z.ZodObject<{
@@ -408,8 +408,8 @@ declare const documentSchema: z.ZodObject<{
408
408
  activity: z.ZodOptional<z.ZodObject<{
409
409
  summary: z.ZodString;
410
410
  kind: z.ZodEnum<{
411
- project: "project";
412
411
  agent: "agent";
412
+ project: "project";
413
413
  file: "file";
414
414
  task: "task";
415
415
  automation: "automation";
@@ -121,9 +121,9 @@ declare const valueSchema: z.ZodObject<{
121
121
  tool: z.ZodOptional<z.ZodNumber>;
122
122
  total: z.ZodOptional<z.ZodNumber>;
123
123
  cacheAccounting: z.ZodEnum<{
124
- unknown: "unknown";
125
124
  included: "included";
126
125
  none: "none";
126
+ unknown: "unknown";
127
127
  }>;
128
128
  }, z.core.$strict>>;
129
129
  usage: z.ZodObject<{
@@ -160,8 +160,8 @@ declare const valueSchema: z.ZodObject<{
160
160
  activity: z.ZodOptional<z.ZodObject<{
161
161
  summary: z.ZodString;
162
162
  kind: z.ZodEnum<{
163
- project: "project";
164
163
  agent: "agent";
164
+ project: "project";
165
165
  file: "file";
166
166
  task: "task";
167
167
  automation: "automation";
@@ -347,9 +347,9 @@ export declare const qualitativeEntryValueSchema: z.ZodObject<{
347
347
  tool: z.ZodOptional<z.ZodNumber>;
348
348
  total: z.ZodOptional<z.ZodNumber>;
349
349
  cacheAccounting: z.ZodEnum<{
350
- unknown: "unknown";
351
350
  included: "included";
352
351
  none: "none";
352
+ unknown: "unknown";
353
353
  }>;
354
354
  }, z.core.$strict>>;
355
355
  usage: z.ZodObject<{
@@ -386,8 +386,8 @@ export declare const qualitativeEntryValueSchema: z.ZodObject<{
386
386
  activity: z.ZodOptional<z.ZodObject<{
387
387
  summary: z.ZodString;
388
388
  kind: z.ZodEnum<{
389
- project: "project";
390
389
  agent: "agent";
390
+ project: "project";
391
391
  file: "file";
392
392
  task: "task";
393
393
  automation: "automation";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-finops/core",
3
- "version": "0.9.8",
3
+ "version": "0.9.10",
4
4
  "funding": "https://asktilden.com",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",