farming-code 2.2.14 → 2.2.17

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 (136) hide show
  1. package/README.md +3 -3
  2. package/README.zh_cn.md +3 -3
  3. package/THIRD_PARTY_NOTICES.md +17 -1
  4. package/backend/acp/packaged-codex-acp.js +13 -0
  5. package/backend/acp-runtime.js +413 -24
  6. package/backend/agent-manager.js +572 -557
  7. package/backend/agent-provider-session.js +15 -1
  8. package/backend/agent-runtime-binding.js +9 -55
  9. package/backend/chat-runtime.js +28 -0
  10. package/backend/codex-rollout-follower.js +83 -0
  11. package/backend/codex-session-archive.js +12 -1
  12. package/backend/codex-session-history.js +94 -0
  13. package/backend/codex-transcript.js +2 -1
  14. package/backend/config-manager.js +25 -17
  15. package/backend/control-api.js +3 -1
  16. package/backend/farming-app-cli.js +21 -0
  17. package/backend/farming-session-store.js +2 -1
  18. package/backend/native-pty-host-client.js +59 -2
  19. package/backend/npm-update-helper.js +65 -7
  20. package/backend/provider-adapters.js +123 -38
  21. package/backend/provider-session-service.js +138 -34
  22. package/backend/server.js +94 -105
  23. package/backend/storage-layout.js +5 -16
  24. package/backend/update-service.js +94 -5
  25. package/backend/usage-history-client.js +305 -0
  26. package/backend/usage-history-scanner.generated.js +2250 -0
  27. package/backend/usage-history-scanner.ts +2718 -0
  28. package/backend/usage-history-smoke.js +64 -0
  29. package/backend/usage-history-worker.js +34 -0
  30. package/backend/usage-monitor.js +188 -588
  31. package/backend/vendor/usage-parsers/CC_STATISTICS_LICENSE +21 -0
  32. package/backend/vendor/usage-parsers/CODEXBAR_LICENSE +21 -0
  33. package/backend/vendor/usage-parsers/VENDOR.json +45 -0
  34. package/dist/acp/LICENSE.codex-acp +190 -0
  35. package/dist/acp/codex-acp-1.1.4.js +30718 -0
  36. package/dist/assets/App-6yWdwzj8.js +210 -0
  37. package/dist/assets/{FileEditorMarkdownPreview-UlVd0O4D.js → FileEditorMarkdownPreview-R0jwwLME.js} +4 -4
  38. package/dist/assets/FileEditorPane-DNy3LAqS.js +2 -0
  39. package/dist/assets/{IconGlyphs-AU22gPDY.js → IconGlyphs-BsUfqpKN.js} +1 -1
  40. package/dist/assets/ProjectFilesSection-BhDEUVHA.js +12 -0
  41. package/dist/assets/{ReviewPage-CeDKDyZh.js → ReviewPage-ChtByBe3.js} +1 -1
  42. package/dist/assets/{abnfDiagram-VRR7QNED-BgawzDQT.js → abnfDiagram-VRR7QNED-nnglrXFi.js} +1 -1
  43. package/dist/assets/{architectureDiagram-ZJ3FMSHR-DUShwPg0.js → architectureDiagram-ZJ3FMSHR-BEXMaFfY.js} +1 -1
  44. package/dist/assets/{blockDiagram-677ZJIJ3-CeGZviXR.js → blockDiagram-677ZJIJ3-1mCezSBk.js} +1 -1
  45. package/dist/assets/{c4Diagram-LMCZKHZV-CLhYht6S.js → c4Diagram-LMCZKHZV-DhTiI6Uv.js} +1 -1
  46. package/dist/assets/channel-BVWNatUB.js +1 -0
  47. package/dist/assets/{chunk-32BRIVSS-LFYpT-Cu.js → chunk-32BRIVSS-CCdsKd73.js} +1 -1
  48. package/dist/assets/{chunk-52WLFC77-BH6jDw9J.js → chunk-52WLFC77-Gncjk4LF.js} +1 -1
  49. package/dist/assets/{chunk-C7G6YPKG-B_mIUlb7.js → chunk-C7G6YPKG-CiDVUP82.js} +1 -1
  50. package/dist/assets/{chunk-EX3LRPZG-C63hVuAJ.js → chunk-EX3LRPZG-z6gXP2_N.js} +1 -1
  51. package/dist/assets/{chunk-FWX5IMBZ-CboX-WF_.js → chunk-FWX5IMBZ-DMZBccQa.js} +2 -2
  52. package/dist/assets/{chunk-HOUHSVGY-BOgSd6Un.js → chunk-HOUHSVGY-D8tIeoPk.js} +1 -1
  53. package/dist/assets/{chunk-ICXQ74PX-C0K-pgZt.js → chunk-ICXQ74PX-b6oQ2Q17.js} +1 -1
  54. package/dist/assets/{chunk-MOJQB5TN-CVMfwN_7.js → chunk-MOJQB5TN-DAVH_p2a.js} +1 -1
  55. package/dist/assets/{chunk-OGEWGWER-DLT3pxyw.js → chunk-OGEWGWER-DxoiMCI-.js} +1 -1
  56. package/dist/assets/{chunk-PUDLZKDR-Cd079yIQ.js → chunk-PUDLZKDR-NsOWh7HM.js} +1 -1
  57. package/dist/assets/{chunk-Q4XR5HBZ-D0jHa6SO.js → chunk-Q4XR5HBZ-Di4H7L21.js} +1 -1
  58. package/dist/assets/{chunk-V7JOEXUC-CVDtR75H.js → chunk-V7JOEXUC-DttpZg9Y.js} +1 -1
  59. package/dist/assets/{chunk-VAUOI2AC-B-W2_9Ot.js → chunk-VAUOI2AC-UHNr_yhX.js} +1 -1
  60. package/dist/assets/{chunk-VR4S4FIN-FD3kWh32.js → chunk-VR4S4FIN-D-Fmtd4Z.js} +1 -1
  61. package/dist/assets/{chunk-WYO6CB5R-C2bGaS5r.js → chunk-WYO6CB5R-CUxMtfK2.js} +4 -4
  62. package/dist/assets/{chunk-ZGVPDNZ5-ymgts0zo.js → chunk-ZGVPDNZ5-CoV2QEl0.js} +1 -1
  63. package/dist/assets/classDiagram-OUVF2IWQ-Db0ib5iE.js +1 -0
  64. package/dist/assets/classDiagram-v2-EOCWNBFH-Bkf69XF9.js +1 -0
  65. package/dist/assets/code-dark-5esj0bRu.css +1 -0
  66. package/dist/assets/{cynefinDiagram-TSTJHNR4-CwpobXIl.js → cynefinDiagram-TSTJHNR4-0xPI92Lg.js} +1 -1
  67. package/dist/assets/{dagre-VKFMJZFB-Dkhlfjna.js → dagre-VKFMJZFB-DfuMG4gU.js} +1 -1
  68. package/dist/assets/{diagram-FQU43EPY-hA8dicp6.js → diagram-FQU43EPY-CT00bLZ6.js} +1 -1
  69. package/dist/assets/{diagram-G47NLZAW-BTY6JTzO.js → diagram-G47NLZAW-Xbh7zXUx.js} +1 -1
  70. package/dist/assets/{diagram-NH7WQ7WH-xkaHHX9M.js → diagram-NH7WQ7WH-SRDVPfn7.js} +1 -1
  71. package/dist/assets/{diagram-OA4YK3LP-gk_fg9UU.js → diagram-OA4YK3LP-CQRli4Np.js} +1 -1
  72. package/dist/assets/{diagram-WEI45ONY-BvC8xd6T.js → diagram-WEI45ONY-DMf17WQi.js} +1 -1
  73. package/dist/assets/{ebnfDiagram-CCIWWBDH-CR7RaNat.js → ebnfDiagram-CCIWWBDH-DhpFkgJ5.js} +1 -1
  74. package/dist/assets/{erDiagram-Q63AITRT-_M-JFPbj.js → erDiagram-Q63AITRT-1IwxWmJo.js} +1 -1
  75. package/dist/assets/file-icons-DpbrD5ZP.js +1 -0
  76. package/dist/assets/flowDiagram-23GEKE2U-DYFo2xmB.js +1 -0
  77. package/dist/assets/{ganttDiagram-NO4QXBWP-Bl8P1yf_.js → ganttDiagram-NO4QXBWP-BNCdfyr1.js} +1 -1
  78. package/dist/assets/{gitGraphDiagram-IHSO6WYX-DoHa5ysz.js → gitGraphDiagram-IHSO6WYX-pbyWZuiX.js} +1 -1
  79. package/dist/assets/{index-BA-iH2Kg.js → index-C8ZFKye1.js} +2 -2
  80. package/dist/assets/{infoDiagram-FWYZ7A6U-DSFcSuUs.js → infoDiagram-FWYZ7A6U-BcTOgDbe.js} +1 -1
  81. package/dist/assets/{ishikawaDiagram-FXEZZL3T-DdkqullI.js → ishikawaDiagram-FXEZZL3T-DCdvGKrn.js} +1 -1
  82. package/dist/assets/{journeyDiagram-5HDEW3XC-B4hRnQJr.js → journeyDiagram-5HDEW3XC-BZ-1q_IT.js} +1 -1
  83. package/dist/assets/{kanban-definition-HUTT4EX6-tm2FZB2E.js → kanban-definition-HUTT4EX6-BIgejCjj.js} +1 -1
  84. package/dist/assets/{line-CmsjK4kn.js → line-CfptFMmX.js} +1 -1
  85. package/dist/assets/main-DSO3i_Gs.css +1 -0
  86. package/dist/assets/{mermaid.core-C7UT_EEL.js → mermaid.core-gSpX30pV.js} +3 -3
  87. package/dist/assets/{mindmap-definition-LN4V7U3C-DplfjGms.js → mindmap-definition-LN4V7U3C-BKBaI4LW.js} +1 -1
  88. package/dist/assets/{pegDiagram-2B236MQR-lu3SdkQs.js → pegDiagram-2B236MQR-sBvDY3VX.js} +1 -1
  89. package/dist/assets/{pieDiagram-ENE6RG2P-3fHbmDf9.js → pieDiagram-ENE6RG2P-DWlWcu08.js} +1 -1
  90. package/dist/assets/{quadrantDiagram-ABIIQ3AL-a5qoWz-X.js → quadrantDiagram-ABIIQ3AL-DZutUd3J.js} +1 -1
  91. package/dist/assets/{railroadDiagram-RFXS5EU6-D2TOuXW9.js → railroadDiagram-RFXS5EU6-DQb3lXGM.js} +1 -1
  92. package/dist/assets/{requirementDiagram-TGXJPOKE-LQBWpUvd.js → requirementDiagram-TGXJPOKE-DqFGXu2e.js} +1 -1
  93. package/dist/assets/{sankeyDiagram-HTMAVEWB-B1ohYoyK.js → sankeyDiagram-HTMAVEWB-d7ECahnO.js} +1 -1
  94. package/dist/assets/{sequenceDiagram-DBY2YBRQ-C3vM-hMq.js → sequenceDiagram-DBY2YBRQ-DdsbKqct.js} +1 -1
  95. package/dist/assets/{stateDiagram-2N3HPSRC-BvFhOryI.js → stateDiagram-2N3HPSRC-P6iSrgwR.js} +1 -1
  96. package/dist/assets/stateDiagram-v2-6OUMAXLB-DIYSf_qh.js +1 -0
  97. package/dist/assets/{swimlanes-5IMT3BWC-DKF_b-2s.js → swimlanes-5IMT3BWC-BGnZJx1M.js} +1 -1
  98. package/dist/assets/swimlanesDiagram-G3AALYLV-BIkrlKrZ.js +8 -0
  99. package/dist/assets/{timeline-definition-FHXFAJF6-vAs11ux_.js → timeline-definition-FHXFAJF6-rA-r3aUR.js} +1 -1
  100. package/dist/assets/{vennDiagram-L72KCM5P-BybTX_9B.js → vennDiagram-L72KCM5P-DiVKUjA9.js} +1 -1
  101. package/dist/assets/{wardleyDiagram-EHGQE667-CicUmMBR.js → wardleyDiagram-EHGQE667-Cfxh7UOn.js} +1 -1
  102. package/dist/assets/workspace-editor-model-D0Im34S5.js +1 -0
  103. package/dist/assets/{workspace-editor-monaco-BICJESG0.js → workspace-editor-monaco-BIpqcWf1.js} +1 -1
  104. package/dist/assets/workspace-editor-monaco-DLlws5Ig.js +1 -0
  105. package/dist/assets/{workspace-view-state-DvYG_9PH.js → workspace-view-state-BpPINu84.js} +1 -1
  106. package/dist/assets/{xychartDiagram-FW5EYKEG-BhWDyCX-.js → xychartDiagram-FW5EYKEG--hmryQeL.js} +1 -1
  107. package/dist/crt-mermaid-renderer.js +313 -313
  108. package/dist/index.html +1 -1
  109. package/frontend/session-bridge.js +24 -2
  110. package/frontend/skins/crt/app.js +203 -85
  111. package/frontend/skins/crt/index.html +2 -11
  112. package/frontend/skins/crt/styles/effects.css +2 -34
  113. package/package.json +30 -12
  114. package/shared/backend-connection-status.d.ts +25 -0
  115. package/shared/backend-connection-status.js +40 -0
  116. package/shared/browser-protocol.js +3 -3
  117. package/backend/app-server-api.js +0 -917
  118. package/backend/codex-app-server-home.js +0 -39
  119. package/backend/codex-app-server-runtime.js +0 -901
  120. package/dist/assets/App-Bumi772v.js +0 -206
  121. package/dist/assets/FileEditorPane-Dxx4rKOg.js +0 -2
  122. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +0 -12
  123. package/dist/assets/channel-Uw90AO0b.js +0 -1
  124. package/dist/assets/classDiagram-OUVF2IWQ-DKW_dtku.js +0 -1
  125. package/dist/assets/classDiagram-v2-EOCWNBFH-C8Fi7-yv.js +0 -1
  126. package/dist/assets/code-dark-DPiuyeSp.css +0 -1
  127. package/dist/assets/code-mobile-3C2Rr-CN.css +0 -1
  128. package/dist/assets/file-icons-Bw2qd5iT.js +0 -1
  129. package/dist/assets/flowDiagram-23GEKE2U-DAiS5Y7G.js +0 -1
  130. package/dist/assets/main-Bqtb5kuz.css +0 -1
  131. package/dist/assets/stateDiagram-v2-6OUMAXLB-WMrz4PNo.js +0 -1
  132. package/dist/assets/swimlanesDiagram-G3AALYLV-BGswC9oC.js +0 -8
  133. package/dist/assets/workspace-editor-model-BQol4qbA.js +0 -1
  134. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +0 -1
  135. package/scripts/sync-ghostty-vendor.js +0 -66
  136. /package/dist/assets/{core-D0LFJkDt.js → core-B9MwRjDE.js} +0 -0
@@ -0,0 +1,2718 @@
1
+ /**
2
+ * Persistent local token-history scanner for Farming.
3
+ *
4
+ * Codex cumulative-counter accounting is a TypeScript adaptation of CodexBar
5
+ * v0.45.2 (commit 91560ca98e776b96fdf910d4a0423c2f0c07a3b9), specifically
6
+ * CostUsageScanner.swift and CodexSubagentRolloutShape.swift. CodexBar is MIT
7
+ * licensed, Copyright (c) 2026 Peter Steinberger. See THIRD_PARTY_NOTICES.md.
8
+ *
9
+ * Claude message parsing follows the usage/message-id normalization semantics
10
+ * of cc-statistics 1.1.0 (commit c98be0af52bbc7f09a1f277747744ace48d9e014),
11
+ * also MIT licensed. Farming owns the filesystem checkpoints, SQLite schema,
12
+ * scan budgeting, and browser-facing result shape.
13
+ *
14
+ * This module intentionally uses only Node built-ins. It runs in a Worker so
15
+ * synchronous SQLite and filesystem operations cannot block Farming's server.
16
+ */
17
+
18
+ import fs from 'node:fs';
19
+ import path from 'node:path';
20
+ import crypto from 'node:crypto';
21
+ import { DatabaseSync } from 'node:sqlite';
22
+
23
+ const SCHEMA_VERSION = 11;
24
+ const SOURCE_VERSION = 'farming-usage-ts-1-codexbar-v0.45.2';
25
+ const PREFIX_BYTES = 64 * 1024;
26
+ const READ_CHUNK_BYTES = 1024 * 1024;
27
+ const MAX_LINE_BYTES = 32 * 1024 * 1024;
28
+ const TRUNCATED_EDGE_BYTES = 512 * 1024;
29
+ const DEFAULT_RETENTION_DAYS = 52 * 7;
30
+ const DEFAULT_RECENT_RAW_MS = 24 * 60 * 60 * 1000;
31
+ const DEFAULT_SCAN_BUDGET_MS = 5_000;
32
+ const SEEN_TOTALS_LIMIT = 64;
33
+ const DIRECTORY_AUDIT_BATCH = 256;
34
+ const DIRECTORY_ENTRY_BATCH = 256;
35
+ const SOURCE_AUDIT_BATCH = 128;
36
+
37
+ type Provider = 'codex' | 'claude';
38
+ type Totals = {
39
+ input: number;
40
+ cached: number;
41
+ output: number;
42
+ cacheWrite: number;
43
+ };
44
+ type UsageEvent = {
45
+ timestamp: number;
46
+ sessionId: string;
47
+ totalTokens: number;
48
+ inputTokens: number;
49
+ outputTokens: number;
50
+ cacheReadTokens: number;
51
+ cacheWriteTokens: number;
52
+ unattributedTokens: number;
53
+ };
54
+ type CodexState = {
55
+ sessionId: string;
56
+ leafSessionId: string | null;
57
+ parentSessionId: string | null;
58
+ forkTimestamp: number | null;
59
+ projectPath: string;
60
+ lastAssistantTimestamp: number | null;
61
+ copiedPrefix: boolean;
62
+ isSubagent: boolean;
63
+ ownBoundarySeen: boolean;
64
+ pendingOwnedBoundary: boolean;
65
+ inheritedRaw: Totals | null;
66
+ counted: Totals | null;
67
+ rawBaseline: Totals | null;
68
+ watermark: Totals | null;
69
+ seenRawTotals: Totals[];
70
+ divergent: boolean;
71
+ interleaved: boolean;
72
+ skippedUnresolvedForkTotal: boolean;
73
+ quotas: Record<string, { timestamp: number | null; rateLimits: unknown }>;
74
+ };
75
+ type ClaudeState = {
76
+ sessionId: string;
77
+ projectPath: string;
78
+ };
79
+ type ParserState = CodexState | ClaudeState;
80
+ type SourceCandidate = {
81
+ provider: Provider;
82
+ filePath: string;
83
+ stat: fs.Stats;
84
+ sessionId: string;
85
+ parentSessionId: string | null;
86
+ forkTimestamp: number | null;
87
+ copiedPrefix: boolean;
88
+ parentBaseline: Totals | null;
89
+ };
90
+ type Metrics = Record<string, number | boolean | Record<Provider, number>>;
91
+ type CollectRequest = {
92
+ cacheFile: string;
93
+ legacyCacheFile?: string;
94
+ nowMs?: number;
95
+ retentionDays?: number;
96
+ recentRawMs?: number;
97
+ scanBudgetMs?: number;
98
+ roots?: Partial<Record<Provider, string[]>>;
99
+ };
100
+
101
+ const ZERO: Totals = { input: 0, cached: 0, output: 0, cacheWrite: 0 };
102
+ const TOKEN_FIELDS = ['input', 'cached', 'output', 'cacheWrite'] as const;
103
+
104
+ const SCHEMA_SQL = `
105
+ CREATE TABLE IF NOT EXISTS metadata (
106
+ key TEXT PRIMARY KEY,
107
+ value TEXT NOT NULL
108
+ ) WITHOUT ROWID;
109
+ CREATE TABLE IF NOT EXISTS source_files (
110
+ path TEXT PRIMARY KEY,
111
+ provider TEXT NOT NULL,
112
+ directory_path TEXT NOT NULL,
113
+ directory_generation INTEGER NOT NULL,
114
+ size INTEGER NOT NULL,
115
+ mtime_ms REAL NOT NULL,
116
+ committed_offset INTEGER NOT NULL,
117
+ file_dev INTEGER NOT NULL,
118
+ file_ino INTEGER NOT NULL,
119
+ prefix_sha256 TEXT NOT NULL,
120
+ suffix_sha256 TEXT NOT NULL,
121
+ parser_state TEXT NOT NULL,
122
+ session_id TEXT NOT NULL,
123
+ scan_complete INTEGER NOT NULL,
124
+ last_checked_ms INTEGER NOT NULL
125
+ ) WITHOUT ROWID;
126
+ CREATE INDEX IF NOT EXISTS source_files_provider_session
127
+ ON source_files(provider, session_id);
128
+ CREATE INDEX IF NOT EXISTS source_files_directory
129
+ ON source_files(directory_path);
130
+ CREATE INDEX IF NOT EXISTS source_files_audit
131
+ ON source_files(last_checked_ms, mtime_ms);
132
+ CREATE INDEX IF NOT EXISTS source_files_recent
133
+ ON source_files(mtime_ms DESC);
134
+ CREATE INDEX IF NOT EXISTS source_files_directory_generation
135
+ ON source_files(directory_path, directory_generation);
136
+ CREATE TABLE IF NOT EXISTS discovery_directories (
137
+ path TEXT PRIMARY KEY,
138
+ provider TEXT NOT NULL,
139
+ root_path TEXT NOT NULL,
140
+ parent_path TEXT NOT NULL,
141
+ is_root INTEGER NOT NULL,
142
+ present INTEGER NOT NULL,
143
+ pending INTEGER NOT NULL,
144
+ scan_generation INTEGER NOT NULL,
145
+ parent_generation INTEGER NOT NULL,
146
+ mtime_ms REAL NOT NULL,
147
+ file_dev INTEGER NOT NULL,
148
+ file_ino INTEGER NOT NULL,
149
+ last_checked_ms INTEGER NOT NULL
150
+ ) WITHOUT ROWID;
151
+ CREATE INDEX IF NOT EXISTS discovery_directories_pending
152
+ ON discovery_directories(present, pending, is_root DESC, last_checked_ms);
153
+ CREATE INDEX IF NOT EXISTS discovery_directories_audit
154
+ ON discovery_directories(present, is_root DESC, last_checked_ms);
155
+ CREATE INDEX IF NOT EXISTS discovery_directories_parent_generation
156
+ ON discovery_directories(parent_path, present, parent_generation);
157
+ CREATE TABLE IF NOT EXISTS discovery_source_queue (
158
+ path TEXT PRIMARY KEY,
159
+ provider TEXT NOT NULL,
160
+ action TEXT NOT NULL,
161
+ priority REAL NOT NULL,
162
+ queued_at_ms INTEGER NOT NULL
163
+ ) WITHOUT ROWID;
164
+ CREATE INDEX IF NOT EXISTS discovery_source_queue_priority
165
+ ON discovery_source_queue(action, priority, queued_at_ms);
166
+ CREATE TABLE IF NOT EXISTS codex_quota_candidates (
167
+ source_path TEXT NOT NULL,
168
+ limit_id TEXT NOT NULL,
169
+ timestamp_ms INTEGER,
170
+ rate_limits_json TEXT NOT NULL,
171
+ PRIMARY KEY(source_path, limit_id)
172
+ ) WITHOUT ROWID;
173
+ CREATE INDEX IF NOT EXISTS codex_quota_candidates_latest
174
+ ON codex_quota_candidates(timestamp_ms DESC);
175
+ CREATE TABLE IF NOT EXISTS cache_stats (
176
+ singleton INTEGER PRIMARY KEY CHECK(singleton = 1),
177
+ codex_files INTEGER NOT NULL,
178
+ claude_files INTEGER NOT NULL,
179
+ codex_incomplete INTEGER NOT NULL,
180
+ claude_incomplete INTEGER NOT NULL,
181
+ committed_bytes INTEGER NOT NULL,
182
+ hourly_rows INTEGER NOT NULL,
183
+ recent_rows INTEGER NOT NULL,
184
+ dedupe_rows INTEGER NOT NULL,
185
+ pending_directories INTEGER NOT NULL,
186
+ queued_sources INTEGER NOT NULL
187
+ );
188
+ INSERT OR IGNORE INTO cache_stats VALUES(1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
189
+ CREATE TABLE IF NOT EXISTS usage_hourly (
190
+ source_path TEXT NOT NULL,
191
+ provider TEXT NOT NULL,
192
+ session_id TEXT NOT NULL,
193
+ bucket_start_ms INTEGER NOT NULL,
194
+ input_tokens INTEGER NOT NULL,
195
+ output_tokens INTEGER NOT NULL,
196
+ cache_read_tokens INTEGER NOT NULL,
197
+ cache_write_tokens INTEGER NOT NULL,
198
+ event_count INTEGER NOT NULL,
199
+ PRIMARY KEY(source_path, provider, session_id, bucket_start_ms),
200
+ FOREIGN KEY(source_path) REFERENCES source_files(path) ON DELETE CASCADE
201
+ ) WITHOUT ROWID;
202
+ CREATE INDEX IF NOT EXISTS usage_hourly_provider_time
203
+ ON usage_hourly(provider, bucket_start_ms);
204
+ CREATE TABLE IF NOT EXISTS usage_provider_hourly (
205
+ provider TEXT NOT NULL,
206
+ bucket_start_ms INTEGER NOT NULL,
207
+ input_tokens INTEGER NOT NULL,
208
+ output_tokens INTEGER NOT NULL,
209
+ cache_read_tokens INTEGER NOT NULL,
210
+ cache_write_tokens INTEGER NOT NULL,
211
+ event_count INTEGER NOT NULL,
212
+ PRIMARY KEY(provider, bucket_start_ms)
213
+ ) WITHOUT ROWID;
214
+ CREATE TABLE IF NOT EXISTS recent_events (
215
+ source_path TEXT NOT NULL,
216
+ event_key TEXT NOT NULL,
217
+ provider TEXT NOT NULL,
218
+ session_id TEXT NOT NULL,
219
+ timestamp_ms INTEGER NOT NULL,
220
+ input_tokens INTEGER NOT NULL,
221
+ output_tokens INTEGER NOT NULL,
222
+ cache_read_tokens INTEGER NOT NULL,
223
+ cache_write_tokens INTEGER NOT NULL,
224
+ PRIMARY KEY(source_path, event_key),
225
+ FOREIGN KEY(source_path) REFERENCES source_files(path) ON DELETE CASCADE
226
+ ) WITHOUT ROWID;
227
+ CREATE INDEX IF NOT EXISTS recent_events_provider_time
228
+ ON recent_events(provider, timestamp_ms);
229
+ CREATE TABLE IF NOT EXISTS claude_messages (
230
+ owner_session_id TEXT NOT NULL,
231
+ message_id TEXT NOT NULL,
232
+ source_path TEXT NOT NULL,
233
+ timestamp_ms INTEGER NOT NULL,
234
+ input_tokens INTEGER NOT NULL,
235
+ output_tokens INTEGER NOT NULL,
236
+ cache_read_tokens INTEGER NOT NULL,
237
+ cache_write_tokens INTEGER NOT NULL,
238
+ PRIMARY KEY(owner_session_id, message_id)
239
+ ) WITHOUT ROWID;
240
+ CREATE INDEX IF NOT EXISTS claude_messages_time
241
+ ON claude_messages(timestamp_ms);
242
+ CREATE TRIGGER IF NOT EXISTS source_files_stats_insert
243
+ AFTER INSERT ON source_files BEGIN
244
+ UPDATE cache_stats SET
245
+ codex_files = codex_files + (NEW.provider = 'codex'),
246
+ claude_files = claude_files + (NEW.provider = 'claude'),
247
+ codex_incomplete = codex_incomplete + (NEW.provider = 'codex' AND NEW.scan_complete = 0),
248
+ claude_incomplete = claude_incomplete + (NEW.provider = 'claude' AND NEW.scan_complete = 0),
249
+ committed_bytes = committed_bytes + NEW.committed_offset
250
+ WHERE singleton = 1;
251
+ END;
252
+ CREATE TRIGGER IF NOT EXISTS source_files_stats_delete
253
+ AFTER DELETE ON source_files BEGIN
254
+ UPDATE cache_stats SET
255
+ codex_files = codex_files - (OLD.provider = 'codex'),
256
+ claude_files = claude_files - (OLD.provider = 'claude'),
257
+ codex_incomplete = codex_incomplete - (OLD.provider = 'codex' AND OLD.scan_complete = 0),
258
+ claude_incomplete = claude_incomplete - (OLD.provider = 'claude' AND OLD.scan_complete = 0),
259
+ committed_bytes = committed_bytes - OLD.committed_offset
260
+ WHERE singleton = 1;
261
+ END;
262
+ CREATE TRIGGER IF NOT EXISTS source_files_stats_update
263
+ AFTER UPDATE OF provider, scan_complete, committed_offset ON source_files BEGIN
264
+ UPDATE cache_stats SET
265
+ codex_files = codex_files - (OLD.provider = 'codex') + (NEW.provider = 'codex'),
266
+ claude_files = claude_files - (OLD.provider = 'claude') + (NEW.provider = 'claude'),
267
+ codex_incomplete = codex_incomplete
268
+ - (OLD.provider = 'codex' AND OLD.scan_complete = 0)
269
+ + (NEW.provider = 'codex' AND NEW.scan_complete = 0),
270
+ claude_incomplete = claude_incomplete
271
+ - (OLD.provider = 'claude' AND OLD.scan_complete = 0)
272
+ + (NEW.provider = 'claude' AND NEW.scan_complete = 0),
273
+ committed_bytes = committed_bytes - OLD.committed_offset + NEW.committed_offset
274
+ WHERE singleton = 1;
275
+ END;
276
+ CREATE TRIGGER IF NOT EXISTS usage_hourly_stats_insert
277
+ AFTER INSERT ON usage_hourly BEGIN
278
+ UPDATE cache_stats SET hourly_rows = hourly_rows + 1 WHERE singleton = 1;
279
+ END;
280
+ CREATE TRIGGER IF NOT EXISTS usage_hourly_stats_delete
281
+ AFTER DELETE ON usage_hourly BEGIN
282
+ UPDATE cache_stats SET hourly_rows = hourly_rows - 1 WHERE singleton = 1;
283
+ END;
284
+ CREATE TRIGGER IF NOT EXISTS recent_events_stats_insert
285
+ AFTER INSERT ON recent_events BEGIN
286
+ UPDATE cache_stats SET recent_rows = recent_rows + 1 WHERE singleton = 1;
287
+ END;
288
+ CREATE TRIGGER IF NOT EXISTS recent_events_stats_delete
289
+ AFTER DELETE ON recent_events BEGIN
290
+ UPDATE cache_stats SET recent_rows = recent_rows - 1 WHERE singleton = 1;
291
+ END;
292
+ CREATE TRIGGER IF NOT EXISTS claude_messages_stats_insert
293
+ AFTER INSERT ON claude_messages BEGIN
294
+ UPDATE cache_stats SET dedupe_rows = dedupe_rows + 1 WHERE singleton = 1;
295
+ END;
296
+ CREATE TRIGGER IF NOT EXISTS claude_messages_stats_delete
297
+ AFTER DELETE ON claude_messages BEGIN
298
+ UPDATE cache_stats SET dedupe_rows = dedupe_rows - 1 WHERE singleton = 1;
299
+ END;
300
+ CREATE TRIGGER IF NOT EXISTS discovery_directories_stats_insert
301
+ AFTER INSERT ON discovery_directories WHEN NEW.present = 1 AND NEW.pending = 1 BEGIN
302
+ UPDATE cache_stats SET pending_directories = pending_directories + 1 WHERE singleton = 1;
303
+ END;
304
+ CREATE TRIGGER IF NOT EXISTS discovery_directories_stats_delete
305
+ AFTER DELETE ON discovery_directories WHEN OLD.present = 1 AND OLD.pending = 1 BEGIN
306
+ UPDATE cache_stats SET pending_directories = pending_directories - 1 WHERE singleton = 1;
307
+ END;
308
+ CREATE TRIGGER IF NOT EXISTS discovery_directories_stats_update
309
+ AFTER UPDATE OF present, pending ON discovery_directories
310
+ WHEN (OLD.present = 1 AND OLD.pending = 1) != (NEW.present = 1 AND NEW.pending = 1)
311
+ BEGIN
312
+ UPDATE cache_stats SET pending_directories = pending_directories
313
+ - (OLD.present = 1 AND OLD.pending = 1)
314
+ + (NEW.present = 1 AND NEW.pending = 1)
315
+ WHERE singleton = 1;
316
+ END;
317
+ CREATE TRIGGER IF NOT EXISTS discovery_source_queue_stats_insert
318
+ AFTER INSERT ON discovery_source_queue BEGIN
319
+ UPDATE cache_stats SET queued_sources = queued_sources + 1 WHERE singleton = 1;
320
+ END;
321
+ CREATE TRIGGER IF NOT EXISTS discovery_source_queue_stats_delete
322
+ AFTER DELETE ON discovery_source_queue BEGIN
323
+ UPDATE cache_stats SET queued_sources = queued_sources - 1 WHERE singleton = 1;
324
+ END;
325
+ `;
326
+
327
+ function integer(value: unknown): number {
328
+ const parsed = Number(value);
329
+ return Number.isFinite(parsed) ? Math.max(0, Math.trunc(parsed)) : 0;
330
+ }
331
+
332
+ function timestampMs(value: unknown): number | null {
333
+ if (typeof value === 'number') {
334
+ if (!Number.isFinite(value) || value <= 0) return null;
335
+ return Math.trunc(value < 10_000_000_000 ? value * 1000 : value);
336
+ }
337
+ if (typeof value !== 'string' || !value) return null;
338
+ if (/^\d+$/.test(value)) return timestampMs(Number(value));
339
+ const parsed = Date.parse(value);
340
+ return Number.isFinite(parsed) ? parsed : null;
341
+ }
342
+
343
+ function cloneTotals(value: Totals | null | undefined): Totals | null {
344
+ return value ? { ...value } : null;
345
+ }
346
+
347
+ function addTotals(left: Totals | null, right: Totals): Totals {
348
+ const base = left || ZERO;
349
+ return {
350
+ input: base.input + right.input,
351
+ cached: base.cached + right.cached,
352
+ output: base.output + right.output,
353
+ cacheWrite: base.cacheWrite + right.cacheWrite,
354
+ };
355
+ }
356
+
357
+ function minTotals(left: Totals, right: Totals): Totals {
358
+ return {
359
+ input: Math.min(left.input, right.input),
360
+ cached: Math.min(left.cached, right.cached),
361
+ output: Math.min(left.output, right.output),
362
+ cacheWrite: Math.min(left.cacheWrite, right.cacheWrite),
363
+ };
364
+ }
365
+
366
+ function maxTotals(left: Totals | null, right: Totals): Totals {
367
+ if (!left) return { ...right };
368
+ return {
369
+ input: Math.max(left.input, right.input),
370
+ cached: Math.max(left.cached, right.cached),
371
+ output: Math.max(left.output, right.output),
372
+ cacheWrite: Math.max(left.cacheWrite, right.cacheWrite),
373
+ };
374
+ }
375
+
376
+ function subtractTotals(current: Totals, baseline: Totals | null): Totals {
377
+ const base = baseline || ZERO;
378
+ return {
379
+ input: Math.max(0, current.input - base.input),
380
+ cached: Math.max(0, current.cached - base.cached),
381
+ output: Math.max(0, current.output - base.output),
382
+ cacheWrite: Math.max(0, current.cacheWrite - base.cacheWrite),
383
+ };
384
+ }
385
+
386
+ function totalsEqual(left: Totals | null, right: Totals | null): boolean {
387
+ if (!left || !right) return left === right;
388
+ return TOKEN_FIELDS.every(field => left[field] === right[field]);
389
+ }
390
+
391
+ function totalsAtLeast(left: Totals, right: Totals): boolean {
392
+ return TOKEN_FIELDS.every(field => left[field] >= right[field]);
393
+ }
394
+
395
+ function totalsAtMost(left: Totals, right: Totals): boolean {
396
+ return TOKEN_FIELDS.every(field => left[field] <= right[field]);
397
+ }
398
+
399
+ function hasUsage(value: Totals | null): value is Totals {
400
+ return Boolean(value && TOKEN_FIELDS.some(field => value[field] > 0));
401
+ }
402
+
403
+ function localHourMs(value: number): number {
404
+ const date = new Date(value);
405
+ date.setMinutes(0, 0, 0);
406
+ return date.getTime();
407
+ }
408
+
409
+ function removeDatabaseFiles(filePath: string): void {
410
+ for (const suffix of ['', '-wal', '-shm']) {
411
+ try {
412
+ fs.rmSync(`${filePath}${suffix}`, { force: true });
413
+ } catch (error) {
414
+ if (suffix === '') throw error;
415
+ }
416
+ }
417
+ }
418
+
419
+ function databaseCompatible(filePath: string): boolean {
420
+ if (!fs.existsSync(filePath)) return true;
421
+ try {
422
+ const database = new DatabaseSync(filePath, { readOnly: true });
423
+ try {
424
+ const tables = database.prepare(
425
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'",
426
+ ).all() as Array<{ name: string }>;
427
+ if (tables.length === 0) return true;
428
+ const schema = database.prepare(
429
+ "SELECT value FROM metadata WHERE key = 'schema_version'",
430
+ ).get() as { value?: string } | undefined;
431
+ const source = database.prepare(
432
+ "SELECT value FROM metadata WHERE key = 'source_version'",
433
+ ).get() as { value?: string } | undefined;
434
+ const names = new Set(tables.map(row => row.name));
435
+ const triggers = new Set((database.prepare(
436
+ "SELECT name FROM sqlite_master WHERE type = 'trigger'",
437
+ ).all() as Array<{ name: string }>).map(row => row.name));
438
+ const stats = database.prepare(
439
+ 'SELECT singleton FROM cache_stats WHERE singleton = 1',
440
+ ).get() as { singleton?: number } | undefined;
441
+ return schema?.value === String(SCHEMA_VERSION)
442
+ && source?.value === SOURCE_VERSION
443
+ && [
444
+ 'source_files',
445
+ 'usage_hourly',
446
+ 'usage_provider_hourly',
447
+ 'recent_events',
448
+ 'claude_messages',
449
+ 'discovery_directories',
450
+ 'discovery_source_queue',
451
+ 'codex_quota_candidates',
452
+ 'cache_stats',
453
+ ]
454
+ .every(name => names.has(name))
455
+ && [
456
+ 'source_files_stats_insert',
457
+ 'source_files_stats_delete',
458
+ 'source_files_stats_update',
459
+ 'usage_hourly_stats_insert',
460
+ 'usage_hourly_stats_delete',
461
+ 'recent_events_stats_insert',
462
+ 'recent_events_stats_delete',
463
+ 'claude_messages_stats_insert',
464
+ 'claude_messages_stats_delete',
465
+ 'discovery_directories_stats_insert',
466
+ 'discovery_directories_stats_delete',
467
+ 'discovery_directories_stats_update',
468
+ 'discovery_source_queue_stats_insert',
469
+ 'discovery_source_queue_stats_delete',
470
+ ].every(name => triggers.has(name))
471
+ && stats?.singleton === 1;
472
+ } finally {
473
+ database.close();
474
+ }
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+
480
+ function openDatabase(filePath: string): { database: DatabaseSync; rebuilt: boolean } {
481
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
482
+ const compatible = databaseCompatible(filePath);
483
+ if (!compatible) {
484
+ closeCacheDirectoryCursors(filePath);
485
+ removeDatabaseFiles(filePath);
486
+ }
487
+ const database = new DatabaseSync(filePath);
488
+ database.exec('PRAGMA journal_mode=WAL');
489
+ database.exec('PRAGMA synchronous=NORMAL');
490
+ database.exec('PRAGMA foreign_keys=ON');
491
+ database.exec('PRAGMA busy_timeout=30000');
492
+ database.exec(SCHEMA_SQL);
493
+ database.prepare(
494
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES('schema_version', ?)",
495
+ ).run(String(SCHEMA_VERSION));
496
+ database.prepare(
497
+ "INSERT OR REPLACE INTO metadata(key, value) VALUES('source_version', ?)",
498
+ ).run(SOURCE_VERSION);
499
+ try {
500
+ fs.chmodSync(filePath, 0o600);
501
+ } catch {
502
+ // Best effort on filesystems without POSIX modes.
503
+ }
504
+ return { database, rebuilt: !compatible };
505
+ }
506
+
507
+ function prefixHash(filePath: string, size?: number): string {
508
+ const descriptor = fs.openSync(filePath, 'r');
509
+ try {
510
+ const lengthLimit = Math.min(PREFIX_BYTES, size ?? PREFIX_BYTES);
511
+ const buffer = Buffer.allocUnsafe(lengthLimit);
512
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
513
+ return crypto.createHash('sha256').update(buffer.subarray(0, length)).digest('hex');
514
+ } finally {
515
+ fs.closeSync(descriptor);
516
+ }
517
+ }
518
+
519
+ function suffixHash(filePath: string, size?: number): string {
520
+ const descriptor = fs.openSync(filePath, 'r');
521
+ try {
522
+ const fileSize = size ?? fs.fstatSync(descriptor).size;
523
+ const length = Math.min(fileSize, PREFIX_BYTES);
524
+ const buffer = Buffer.allocUnsafe(length);
525
+ const read = fs.readSync(
526
+ descriptor,
527
+ buffer,
528
+ 0,
529
+ length,
530
+ Math.max(0, fileSize - length),
531
+ );
532
+ return crypto.createHash('sha256').update(buffer.subarray(0, read)).digest('hex');
533
+ } finally {
534
+ fs.closeSync(descriptor);
535
+ }
536
+ }
537
+
538
+ function claudeOwnerSession(filePath: string): string {
539
+ const parent = path.dirname(filePath);
540
+ if (path.basename(parent) === 'subagents') {
541
+ return path.basename(path.dirname(parent));
542
+ }
543
+ return path.basename(filePath, '.jsonl');
544
+ }
545
+
546
+ function defaultState(provider: Provider, filePath: string): ParserState {
547
+ if (provider === 'claude') {
548
+ return {
549
+ sessionId: claudeOwnerSession(filePath),
550
+ projectPath: '',
551
+ };
552
+ }
553
+ return {
554
+ sessionId: path.basename(filePath, '.jsonl'),
555
+ leafSessionId: null,
556
+ parentSessionId: null,
557
+ forkTimestamp: null,
558
+ projectPath: '',
559
+ lastAssistantTimestamp: null,
560
+ copiedPrefix: false,
561
+ isSubagent: false,
562
+ ownBoundarySeen: false,
563
+ pendingOwnedBoundary: false,
564
+ inheritedRaw: null,
565
+ counted: null,
566
+ rawBaseline: null,
567
+ watermark: null,
568
+ seenRawTotals: [],
569
+ divergent: false,
570
+ interleaved: false,
571
+ skippedUnresolvedForkTotal: false,
572
+ quotas: {},
573
+ };
574
+ }
575
+
576
+ function normalizedCodexTotals(value: unknown): Totals | null {
577
+ if (!value || typeof value !== 'object') return null;
578
+ const usage = value as Record<string, unknown>;
579
+ const cached = integer(usage.cached_input_tokens ?? usage.cache_read_input_tokens);
580
+ const rawInput = integer(usage.input_tokens);
581
+ const result = {
582
+ // Codex input_tokens includes cached_input_tokens. Farming displays the
583
+ // mutually-exclusive breakdown while retaining CodexBar's cumulative math.
584
+ input: Math.max(0, rawInput - cached),
585
+ cached,
586
+ output: integer(usage.output_tokens),
587
+ cacheWrite: integer(usage.cache_creation_input_tokens),
588
+ };
589
+ return hasUsage(result) ? result : null;
590
+ }
591
+
592
+ function claudeTotals(value: unknown): Totals | null {
593
+ if (!value || typeof value !== 'object') return null;
594
+ const usage = value as Record<string, unknown>;
595
+ const result = {
596
+ input: integer(usage.input_tokens),
597
+ cached: integer(usage.cache_read_input_tokens),
598
+ output: integer(usage.output_tokens),
599
+ cacheWrite: integer(usage.cache_creation_input_tokens),
600
+ };
601
+ return hasUsage(result) ? result : null;
602
+ }
603
+
604
+ function parseLargeJsonEdge(first: Buffer, tail: Buffer): Record<string, unknown> | null {
605
+ const text = `${first.toString('utf8')}\n${tail.toString('utf8')}`;
606
+ const timestamp = /"timestamp"\s*:\s*"([^"]+)"/.exec(text)?.[1];
607
+ const topType = /"type"\s*:\s*"(assistant|event_msg|session_meta|response_item|turn_context)"/
608
+ .exec(text)?.[1];
609
+ if (!topType) return null;
610
+ if (topType === 'assistant') {
611
+ const sessionId = /"sessionId"\s*:\s*"([^"]+)"/.exec(text)?.[1];
612
+ const messageId = /"id"\s*:\s*"([^"]+)"/.exec(text)?.[1];
613
+ // Claude serializes the canonical message.usage after message.content.
614
+ // Nested tool input may itself contain a usage object, so the last retained
615
+ // occurrence is the only safe edge candidate for an oversized row.
616
+ const usageStart = text.lastIndexOf('"usage"');
617
+ const usageText = usageStart >= 0 ? text.slice(usageStart, usageStart + 2048) : '';
618
+ return {
619
+ type: 'assistant',
620
+ timestamp,
621
+ sessionId,
622
+ message: {
623
+ id: messageId,
624
+ usage: {
625
+ input_tokens: integer(/"input_tokens"\s*:\s*(\d+)/.exec(usageText)?.[1]),
626
+ output_tokens: integer(/"output_tokens"\s*:\s*(\d+)/.exec(usageText)?.[1]),
627
+ cache_read_input_tokens: integer(
628
+ /"cache_read_input_tokens"\s*:\s*(\d+)/.exec(usageText)?.[1],
629
+ ),
630
+ cache_creation_input_tokens: integer(
631
+ /"cache_creation_input_tokens"\s*:\s*(\d+)/.exec(usageText)?.[1],
632
+ ),
633
+ },
634
+ },
635
+ };
636
+ }
637
+ if (
638
+ topType !== 'event_msg'
639
+ || !/"payload"\s*:\s*\{\s*"type"\s*:\s*"token_count"/.test(text)
640
+ ) {
641
+ return null;
642
+ }
643
+ const totalStart = text.indexOf('"total_token_usage"');
644
+ const lastStart = text.indexOf('"last_token_usage"');
645
+ const totalsText = totalStart >= 0 ? text.slice(totalStart, totalStart + 1024) : '';
646
+ const lastText = lastStart >= 0 ? text.slice(lastStart, lastStart + 1024) : '';
647
+ const fields = (source: string) => ({
648
+ input_tokens: integer(/"input_tokens"\s*:\s*(\d+)/.exec(source)?.[1]),
649
+ cached_input_tokens: integer(/"cached_input_tokens"\s*:\s*(\d+)/.exec(source)?.[1]),
650
+ output_tokens: integer(/"output_tokens"\s*:\s*(\d+)/.exec(source)?.[1]),
651
+ });
652
+ return {
653
+ type: 'event_msg',
654
+ timestamp,
655
+ payload: {
656
+ type: 'token_count',
657
+ info: {
658
+ total_token_usage: fields(totalsText),
659
+ last_token_usage: fields(lastText),
660
+ },
661
+ },
662
+ };
663
+ }
664
+
665
+ function interesting(provider: Provider, buffer: Buffer): boolean {
666
+ if (provider === 'claude') return buffer.includes(Buffer.from('"assistant"'));
667
+ return [
668
+ '"session_meta"',
669
+ '"token_count"',
670
+ '"agent_message"',
671
+ '"response_item"',
672
+ '"turn_context"',
673
+ '"task_started"',
674
+ '"inter_agent_communication_metadata"',
675
+ ].some(marker => buffer.includes(Buffer.from(marker)));
676
+ }
677
+
678
+ function parseRecord(
679
+ provider: Provider,
680
+ fullLine: Buffer | null,
681
+ first: Buffer,
682
+ tail: Buffer,
683
+ ): Record<string, unknown> | null {
684
+ const searchable = fullLine || Buffer.concat([first, tail]);
685
+ if (!interesting(provider, searchable)) return null;
686
+ if (fullLine) {
687
+ try {
688
+ const parsed = JSON.parse(fullLine.toString('utf8'));
689
+ return parsed && typeof parsed === 'object' ? parsed : null;
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+ return parseLargeJsonEdge(first, tail);
695
+ }
696
+
697
+ function divergentDelta(
698
+ rawBaseline: Totals | null,
699
+ countedBaseline: Totals | null,
700
+ current: Totals,
701
+ ): Totals {
702
+ const raw = rawBaseline || ZERO;
703
+ const counted = countedBaseline || ZERO;
704
+ const component = (rawValue: number, countedValue: number, currentValue: number) => (
705
+ currentValue >= rawValue
706
+ ? Math.max(0, currentValue - rawValue)
707
+ : Math.max(0, currentValue - countedValue)
708
+ );
709
+ return {
710
+ input: component(raw.input, counted.input, current.input),
711
+ cached: component(raw.cached, counted.cached, current.cached),
712
+ output: component(raw.output, counted.output, current.output),
713
+ cacheWrite: component(raw.cacheWrite, counted.cacheWrite, current.cacheWrite),
714
+ };
715
+ }
716
+
717
+ function containedDelta(
718
+ watermark: Totals | null,
719
+ counted: Totals | null,
720
+ current: Totals,
721
+ ): Totals {
722
+ const water = watermark || ZERO;
723
+ const base = counted || ZERO;
724
+ const component = (waterValue: number, countedValue: number, currentValue: number) => (
725
+ currentValue >= waterValue
726
+ ? Math.max(0, currentValue - Math.max(waterValue, countedValue))
727
+ : Math.max(0, currentValue - countedValue)
728
+ );
729
+ return {
730
+ input: component(water.input, base.input, current.input),
731
+ cached: component(water.cached, base.cached, current.cached),
732
+ output: component(water.output, base.output, current.output),
733
+ cacheWrite: component(water.cacheWrite, base.cacheWrite, current.cacheWrite),
734
+ };
735
+ }
736
+
737
+ /**
738
+ * CodexBar's cumulative-counter reducer. The monotonic watermark is the
739
+ * load-bearing protection against Ultra-mode high/low lineage flips; the
740
+ * bounded exact-value set is only an optimization for repeated telemetry.
741
+ */
742
+ function applyCodexTotals(
743
+ state: CodexState,
744
+ last: Totals | null,
745
+ total: Totals | null,
746
+ ): Totals | null {
747
+ if (total && state.seenRawTotals.some(value => totalsEqual(value, total))) {
748
+ return null;
749
+ }
750
+ if (total && state.watermark && TOKEN_FIELDS.some(
751
+ field => total[field] < state.watermark![field],
752
+ )) {
753
+ state.interleaved = true;
754
+ }
755
+ const watermarkBaseline = state.watermark || state.rawBaseline;
756
+ let delta: Totals | null = null;
757
+ if (last) {
758
+ delta = last;
759
+ if (total) {
760
+ if (state.interleaved) {
761
+ delta = minTotals(last, containedDelta(watermarkBaseline, state.counted, total));
762
+ } else {
763
+ const totalDelta = subtractTotals(total, watermarkBaseline);
764
+ if (
765
+ !state.divergent
766
+ && totalsAtLeast(total, watermarkBaseline || ZERO)
767
+ && totalsAtMost(totalDelta, last)
768
+ ) {
769
+ delta = totalDelta;
770
+ }
771
+ }
772
+ state.counted = addTotals(state.counted, delta);
773
+ state.rawBaseline = { ...total };
774
+ if (!totalsEqual(state.rawBaseline, state.counted)) state.divergent = true;
775
+ } else {
776
+ state.counted = addTotals(state.counted, delta);
777
+ state.rawBaseline = cloneTotals(state.counted);
778
+ state.watermark = maxTotals(state.watermark, state.counted!);
779
+ }
780
+ } else if (total) {
781
+ delta = state.interleaved
782
+ ? containedDelta(watermarkBaseline, state.counted, total)
783
+ : state.divergent
784
+ ? divergentDelta(watermarkBaseline, state.counted, total)
785
+ : subtractTotals(total, watermarkBaseline);
786
+ state.counted = addTotals(state.counted, delta);
787
+ state.rawBaseline = { ...total };
788
+ if (!totalsEqual(state.rawBaseline, state.counted)) state.divergent = true;
789
+ }
790
+
791
+ if (total) {
792
+ state.watermark = maxTotals(state.watermark, total);
793
+ if (!state.seenRawTotals.some(value => totalsEqual(value, total))) {
794
+ state.seenRawTotals.push({ ...total });
795
+ state.seenRawTotals = state.seenRawTotals.slice(-SEEN_TOTALS_LIMIT);
796
+ }
797
+ }
798
+ return hasUsage(delta) ? delta : null;
799
+ }
800
+
801
+ function parseCodexMeta(record: Record<string, unknown>): {
802
+ id: string | null;
803
+ parentId: string | null;
804
+ timestamp: number | null;
805
+ cwd: string;
806
+ isSubagent: boolean;
807
+ } | null {
808
+ if (record.type !== 'session_meta') return null;
809
+ const payload = record.payload && typeof record.payload === 'object'
810
+ ? record.payload as Record<string, unknown>
811
+ : {};
812
+ const source = payload.source && typeof payload.source === 'object'
813
+ ? payload.source as Record<string, unknown>
814
+ : {};
815
+ const subagent = source.subagent && typeof source.subagent === 'object'
816
+ ? source.subagent as Record<string, unknown>
817
+ : {};
818
+ const spawn = subagent.thread_spawn && typeof subagent.thread_spawn === 'object'
819
+ ? subagent.thread_spawn as Record<string, unknown>
820
+ : {};
821
+ const id = typeof payload.id === 'string' && payload.id.trim() ? payload.id.trim() : null;
822
+ const explicitParent = typeof payload.forked_from_id === 'string'
823
+ ? payload.forked_from_id.trim()
824
+ : '';
825
+ const spawnedParent = typeof spawn.parent_thread_id === 'string'
826
+ ? spawn.parent_thread_id.trim()
827
+ : '';
828
+ return {
829
+ id,
830
+ parentId: explicitParent || spawnedParent || null,
831
+ timestamp: timestampMs(payload.timestamp) || timestampMs(record.timestamp),
832
+ cwd: typeof payload.cwd === 'string' ? payload.cwd : '',
833
+ isSubagent: Boolean(source.subagent),
834
+ };
835
+ }
836
+
837
+ function updateCodexMetadata(state: CodexState, record: Record<string, unknown>): void {
838
+ const metadata = parseCodexMeta(record);
839
+ if (!metadata) return;
840
+ if (!state.leafSessionId) {
841
+ state.leafSessionId = metadata.id;
842
+ if (metadata.id) state.sessionId = metadata.id;
843
+ state.parentSessionId = metadata.parentId;
844
+ state.forkTimestamp = metadata.timestamp;
845
+ state.isSubagent = metadata.isSubagent;
846
+ if (metadata.cwd) state.projectPath = metadata.cwd;
847
+ return;
848
+ }
849
+ if (metadata.id && metadata.id !== state.leafSessionId) {
850
+ state.copiedPrefix = true;
851
+ state.ownBoundarySeen = false;
852
+ state.pendingOwnedBoundary = false;
853
+ if (!state.parentSessionId) state.parentSessionId = metadata.id;
854
+ return;
855
+ }
856
+ if (!state.parentSessionId && metadata.parentId) {
857
+ state.parentSessionId = metadata.parentId;
858
+ state.forkTimestamp = metadata.timestamp || state.forkTimestamp;
859
+ }
860
+ if (!state.projectPath && metadata.cwd) state.projectPath = metadata.cwd;
861
+ }
862
+
863
+ function codexRecord(
864
+ record: Record<string, unknown>,
865
+ state: CodexState,
866
+ ): { usage: Totals | null; timestamp: number | null; quota: unknown } {
867
+ updateCodexMetadata(state, record);
868
+ const recordTimestamp = timestampMs(record.timestamp);
869
+ const payload = record.payload && typeof record.payload === 'object'
870
+ ? record.payload as Record<string, unknown>
871
+ : {};
872
+ if (
873
+ record.type === 'event_msg'
874
+ && payload.type === 'agent_message'
875
+ && typeof payload.message === 'string'
876
+ ) {
877
+ state.pendingOwnedBoundary = false;
878
+ state.lastAssistantTimestamp = recordTimestamp;
879
+ return { usage: null, timestamp: recordTimestamp, quota: null };
880
+ }
881
+ if (record.type === 'response_item') {
882
+ state.pendingOwnedBoundary = false;
883
+ const visibleAssistant = payload.type === 'function_call'
884
+ || payload.type === 'web_search_call'
885
+ || (payload.type === 'message' && payload.role === 'assistant');
886
+ if (visibleAssistant) state.lastAssistantTimestamp = recordTimestamp;
887
+ return { usage: null, timestamp: recordTimestamp, quota: null };
888
+ }
889
+ if (record.type === 'turn_context') {
890
+ if (state.copiedPrefix && state.isSubagent) state.pendingOwnedBoundary = true;
891
+ return { usage: null, timestamp: recordTimestamp, quota: null };
892
+ }
893
+ if (payload.type === 'inter_agent_communication_metadata') {
894
+ if (state.copiedPrefix && state.isSubagent && state.pendingOwnedBoundary) {
895
+ state.ownBoundarySeen = true;
896
+ }
897
+ state.pendingOwnedBoundary = false;
898
+ return { usage: null, timestamp: recordTimestamp, quota: null };
899
+ }
900
+ if (payload.type === 'task_started') {
901
+ if (state.copiedPrefix && !state.isSubagent) state.ownBoundarySeen = true;
902
+ return { usage: null, timestamp: recordTimestamp, quota: null };
903
+ }
904
+ if (record.type !== 'event_msg' || payload.type !== 'token_count') {
905
+ state.pendingOwnedBoundary = false;
906
+ return { usage: null, timestamp: recordTimestamp, quota: null };
907
+ }
908
+ state.pendingOwnedBoundary = false;
909
+
910
+ const info = payload.info && typeof payload.info === 'object'
911
+ ? payload.info as Record<string, unknown>
912
+ : {};
913
+ const total = normalizedCodexTotals(info.total_token_usage);
914
+ const last = normalizedCodexTotals(info.last_token_usage);
915
+ const quota = payload.rate_limits && typeof payload.rate_limits === 'object'
916
+ ? payload.rate_limits
917
+ : null;
918
+
919
+ if (state.copiedPrefix && !state.inheritedRaw) {
920
+ if (!state.ownBoundarySeen) {
921
+ if (total) state.inheritedRaw = maxTotals(state.inheritedRaw, total);
922
+ return { usage: null, timestamp: recordTimestamp, quota };
923
+ }
924
+ }
925
+
926
+ let adjustedTotal = total;
927
+ if (state.parentSessionId) {
928
+ if (state.inheritedRaw && total) {
929
+ adjustedTotal = subtractTotals(total, state.inheritedRaw);
930
+ } else if (!state.copiedPrefix && !state.skippedUnresolvedForkTotal) {
931
+ state.skippedUnresolvedForkTotal = true;
932
+ if (last) {
933
+ if (total) state.inheritedRaw = subtractTotals(total, last);
934
+ adjustedTotal = { ...last };
935
+ } else {
936
+ state.inheritedRaw = cloneTotals(total);
937
+ return { usage: null, timestamp: recordTimestamp, quota };
938
+ }
939
+ } else if (!state.copiedPrefix && !state.inheritedRaw) {
940
+ if (!last) return { usage: null, timestamp: recordTimestamp, quota };
941
+ if (total) state.inheritedRaw = subtractTotals(total, last);
942
+ adjustedTotal = { ...last };
943
+ }
944
+ }
945
+ const usage = applyCodexTotals(state, last, adjustedTotal);
946
+ return {
947
+ usage,
948
+ timestamp: state.lastAssistantTimestamp || recordTimestamp,
949
+ quota,
950
+ };
951
+ }
952
+
953
+ function claudeRecord(
954
+ record: Record<string, unknown>,
955
+ state: ClaudeState,
956
+ ): { usage: Totals | null; timestamp: number | null; messageId: string | null } {
957
+ if (record.type !== 'assistant') {
958
+ return { usage: null, timestamp: null, messageId: null };
959
+ }
960
+ const message = record.message && typeof record.message === 'object'
961
+ ? record.message as Record<string, unknown>
962
+ : {};
963
+ const usage = claudeTotals(message.usage);
964
+ if (typeof record.cwd === 'string' && record.cwd) state.projectPath = record.cwd;
965
+ if (typeof record.sessionId === 'string' && record.sessionId && !state.sessionId) {
966
+ state.sessionId = record.sessionId;
967
+ }
968
+ return {
969
+ usage,
970
+ timestamp: timestampMs(record.timestamp),
971
+ messageId: typeof message.id === 'string' && message.id ? message.id : null,
972
+ };
973
+ }
974
+
975
+ function hourlyAdd(
976
+ database: DatabaseSync,
977
+ sourcePath: string,
978
+ provider: Provider,
979
+ sessionId: string,
980
+ timestamp: number,
981
+ usage: Totals,
982
+ eventCount = 1,
983
+ ): void {
984
+ database.prepare(`
985
+ INSERT INTO usage_hourly(
986
+ source_path, provider, session_id, bucket_start_ms,
987
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, event_count
988
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
989
+ ON CONFLICT(source_path, provider, session_id, bucket_start_ms) DO UPDATE SET
990
+ input_tokens = input_tokens + excluded.input_tokens,
991
+ output_tokens = output_tokens + excluded.output_tokens,
992
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
993
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
994
+ event_count = event_count + excluded.event_count
995
+ `).run(
996
+ sourcePath,
997
+ provider,
998
+ sessionId,
999
+ localHourMs(timestamp),
1000
+ usage.input,
1001
+ usage.output,
1002
+ usage.cached,
1003
+ usage.cacheWrite,
1004
+ eventCount,
1005
+ );
1006
+ database.prepare(`
1007
+ INSERT INTO usage_provider_hourly(
1008
+ provider, bucket_start_ms, input_tokens, output_tokens,
1009
+ cache_read_tokens, cache_write_tokens, event_count
1010
+ ) VALUES(?, ?, ?, ?, ?, ?, ?)
1011
+ ON CONFLICT(provider, bucket_start_ms) DO UPDATE SET
1012
+ input_tokens = input_tokens + excluded.input_tokens,
1013
+ output_tokens = output_tokens + excluded.output_tokens,
1014
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
1015
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
1016
+ event_count = event_count + excluded.event_count
1017
+ `).run(
1018
+ provider,
1019
+ localHourMs(timestamp),
1020
+ usage.input,
1021
+ usage.output,
1022
+ usage.cached,
1023
+ usage.cacheWrite,
1024
+ eventCount,
1025
+ );
1026
+ if (eventCount < 0) {
1027
+ database.prepare(`
1028
+ DELETE FROM usage_provider_hourly
1029
+ WHERE provider = ? AND bucket_start_ms = ? AND event_count <= 0
1030
+ `).run(provider, localHourMs(timestamp));
1031
+ }
1032
+ }
1033
+
1034
+ function hourlyDelta(
1035
+ database: DatabaseSync,
1036
+ sourcePath: string,
1037
+ provider: Provider,
1038
+ sessionId: string,
1039
+ timestamp: number,
1040
+ usage: Totals,
1041
+ direction: 1 | -1,
1042
+ ): void {
1043
+ const scaled = {
1044
+ input: usage.input * direction,
1045
+ cached: usage.cached * direction,
1046
+ output: usage.output * direction,
1047
+ cacheWrite: usage.cacheWrite * direction,
1048
+ };
1049
+ hourlyAdd(database, sourcePath, provider, sessionId, timestamp, scaled, direction);
1050
+ if (direction < 0) {
1051
+ database.prepare(`
1052
+ DELETE FROM usage_hourly
1053
+ WHERE source_path = ? AND provider = ? AND session_id = ?
1054
+ AND bucket_start_ms = ? AND event_count <= 0
1055
+ `).run(sourcePath, provider, sessionId, localHourMs(timestamp));
1056
+ }
1057
+ }
1058
+
1059
+ function recentUpsert(
1060
+ database: DatabaseSync,
1061
+ sourcePath: string,
1062
+ eventKey: string,
1063
+ provider: Provider,
1064
+ sessionId: string,
1065
+ timestamp: number,
1066
+ usage: Totals,
1067
+ ): void {
1068
+ database.prepare(`
1069
+ INSERT INTO recent_events(
1070
+ source_path, event_key, provider, session_id, timestamp_ms,
1071
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
1072
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)
1073
+ ON CONFLICT(source_path, event_key) DO UPDATE SET
1074
+ provider = excluded.provider,
1075
+ session_id = excluded.session_id,
1076
+ timestamp_ms = excluded.timestamp_ms,
1077
+ input_tokens = excluded.input_tokens,
1078
+ output_tokens = excluded.output_tokens,
1079
+ cache_read_tokens = excluded.cache_read_tokens,
1080
+ cache_write_tokens = excluded.cache_write_tokens
1081
+ `).run(
1082
+ sourcePath,
1083
+ eventKey,
1084
+ provider,
1085
+ sessionId,
1086
+ timestamp,
1087
+ usage.input,
1088
+ usage.output,
1089
+ usage.cached,
1090
+ usage.cacheWrite,
1091
+ );
1092
+ }
1093
+
1094
+ function claudeMessageUpsert(
1095
+ database: DatabaseSync,
1096
+ sourcePath: string,
1097
+ ownerSessionId: string,
1098
+ messageId: string,
1099
+ timestamp: number,
1100
+ usage: Totals,
1101
+ recentBoundary: number,
1102
+ ): void {
1103
+ const existing = database.prepare(`
1104
+ SELECT source_path, timestamp_ms, input_tokens, output_tokens,
1105
+ cache_read_tokens, cache_write_tokens
1106
+ FROM claude_messages
1107
+ WHERE owner_session_id = ? AND message_id = ?
1108
+ `).get(ownerSessionId, messageId) as Record<string, unknown> | undefined;
1109
+ if (
1110
+ existing
1111
+ && (
1112
+ Number(existing.output_tokens) > usage.output
1113
+ || (
1114
+ Number(existing.output_tokens) === usage.output
1115
+ && String(existing.source_path) <= sourcePath
1116
+ )
1117
+ )
1118
+ ) {
1119
+ return;
1120
+ }
1121
+ if (existing) {
1122
+ const oldUsage = {
1123
+ input: Number(existing.input_tokens),
1124
+ output: Number(existing.output_tokens),
1125
+ cached: Number(existing.cache_read_tokens),
1126
+ cacheWrite: Number(existing.cache_write_tokens),
1127
+ };
1128
+ hourlyDelta(
1129
+ database,
1130
+ String(existing.source_path),
1131
+ 'claude',
1132
+ ownerSessionId,
1133
+ Number(existing.timestamp_ms),
1134
+ oldUsage,
1135
+ -1,
1136
+ );
1137
+ database.prepare(
1138
+ 'DELETE FROM recent_events WHERE source_path = ? AND event_key = ?',
1139
+ ).run(String(existing.source_path), `message:${messageId}`);
1140
+ }
1141
+ database.prepare(`
1142
+ INSERT INTO claude_messages(
1143
+ owner_session_id, message_id, source_path, timestamp_ms,
1144
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
1145
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?)
1146
+ ON CONFLICT(owner_session_id, message_id) DO UPDATE SET
1147
+ source_path = excluded.source_path,
1148
+ timestamp_ms = excluded.timestamp_ms,
1149
+ input_tokens = excluded.input_tokens,
1150
+ output_tokens = excluded.output_tokens,
1151
+ cache_read_tokens = excluded.cache_read_tokens,
1152
+ cache_write_tokens = excluded.cache_write_tokens
1153
+ WHERE excluded.output_tokens > claude_messages.output_tokens
1154
+ OR (
1155
+ excluded.output_tokens = claude_messages.output_tokens
1156
+ AND excluded.source_path < claude_messages.source_path
1157
+ )
1158
+ `).run(
1159
+ ownerSessionId,
1160
+ messageId,
1161
+ sourcePath,
1162
+ timestamp,
1163
+ usage.input,
1164
+ usage.output,
1165
+ usage.cached,
1166
+ usage.cacheWrite,
1167
+ );
1168
+ hourlyDelta(
1169
+ database,
1170
+ sourcePath,
1171
+ 'claude',
1172
+ ownerSessionId,
1173
+ timestamp,
1174
+ usage,
1175
+ 1,
1176
+ );
1177
+ if (timestamp >= recentBoundary) {
1178
+ recentUpsert(
1179
+ database,
1180
+ sourcePath,
1181
+ `message:${messageId}`,
1182
+ 'claude',
1183
+ ownerSessionId,
1184
+ timestamp,
1185
+ usage,
1186
+ );
1187
+ }
1188
+ }
1189
+
1190
+ function sourceRow(database: DatabaseSync, filePath: string): Record<string, unknown> | undefined {
1191
+ return database.prepare('SELECT * FROM source_files WHERE path = ?').get(filePath) as
1192
+ Record<string, unknown> | undefined;
1193
+ }
1194
+
1195
+ function directoryGeneration(database: DatabaseSync, directoryPath: string): number {
1196
+ const row = database.prepare(
1197
+ 'SELECT scan_generation FROM discovery_directories WHERE path = ?',
1198
+ ).get(directoryPath) as { scan_generation?: number } | undefined;
1199
+ return Number(row?.scan_generation || 0);
1200
+ }
1201
+
1202
+ function insertSource(
1203
+ database: DatabaseSync,
1204
+ candidate: SourceCandidate,
1205
+ state: ParserState,
1206
+ hash: string,
1207
+ ): void {
1208
+ database.prepare(`
1209
+ INSERT INTO source_files(
1210
+ path, provider, directory_path, directory_generation,
1211
+ size, mtime_ms, committed_offset, file_dev, file_ino,
1212
+ prefix_sha256, suffix_sha256, parser_state, session_id, scan_complete,
1213
+ last_checked_ms
1214
+ ) VALUES(?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, 0, ?)
1215
+ `).run(
1216
+ candidate.filePath,
1217
+ candidate.provider,
1218
+ path.dirname(candidate.filePath),
1219
+ directoryGeneration(database, path.dirname(candidate.filePath)),
1220
+ candidate.stat.size,
1221
+ candidate.stat.mtimeMs,
1222
+ Number(candidate.stat.dev),
1223
+ Number(candidate.stat.ino),
1224
+ hash,
1225
+ suffixHash(candidate.filePath, candidate.stat.size),
1226
+ JSON.stringify(state),
1227
+ state.sessionId,
1228
+ Date.now(),
1229
+ );
1230
+ }
1231
+
1232
+ function deleteSource(database: DatabaseSync, filePath: string): void {
1233
+ database.prepare(`
1234
+ INSERT INTO usage_provider_hourly(
1235
+ provider, bucket_start_ms, input_tokens, output_tokens,
1236
+ cache_read_tokens, cache_write_tokens, event_count
1237
+ )
1238
+ SELECT provider, bucket_start_ms, -input_tokens, -output_tokens,
1239
+ -cache_read_tokens, -cache_write_tokens, -event_count
1240
+ FROM usage_hourly
1241
+ WHERE source_path = ?
1242
+ ON CONFLICT(provider, bucket_start_ms) DO UPDATE SET
1243
+ input_tokens = input_tokens + excluded.input_tokens,
1244
+ output_tokens = output_tokens + excluded.output_tokens,
1245
+ cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
1246
+ cache_write_tokens = cache_write_tokens + excluded.cache_write_tokens,
1247
+ event_count = event_count + excluded.event_count
1248
+ `).run(filePath);
1249
+ database.prepare(
1250
+ 'DELETE FROM usage_provider_hourly WHERE event_count <= 0',
1251
+ ).run();
1252
+ database.prepare('DELETE FROM claude_messages WHERE source_path = ?').run(filePath);
1253
+ database.prepare('DELETE FROM codex_quota_candidates WHERE source_path = ?').run(filePath);
1254
+ database.prepare('DELETE FROM source_files WHERE path = ?').run(filePath);
1255
+ }
1256
+
1257
+ function processRecord(
1258
+ database: DatabaseSync,
1259
+ candidate: SourceCandidate,
1260
+ state: ParserState,
1261
+ record: Record<string, unknown>,
1262
+ lineEnd: number,
1263
+ recentBoundary: number,
1264
+ metrics: Metrics,
1265
+ ): void {
1266
+ if (candidate.provider === 'codex') {
1267
+ const codexState = state as CodexState;
1268
+ const result = codexRecord(record, codexState);
1269
+ if (result.quota && typeof result.quota === 'object') {
1270
+ const quota = result.quota as Record<string, unknown>;
1271
+ const key = String(quota.limit_id || quota.limitId || '');
1272
+ const previous = codexState.quotas[key];
1273
+ const observedAt = timestampMs(record.timestamp);
1274
+ if (!previous || observedAt === null || previous.timestamp === null || observedAt >= previous.timestamp) {
1275
+ codexState.quotas[key] = { timestamp: observedAt, rateLimits: result.quota };
1276
+ database.prepare(`
1277
+ INSERT INTO codex_quota_candidates(
1278
+ source_path, limit_id, timestamp_ms, rate_limits_json
1279
+ ) VALUES(?, ?, ?, ?)
1280
+ ON CONFLICT(source_path, limit_id) DO UPDATE SET
1281
+ timestamp_ms = excluded.timestamp_ms,
1282
+ rate_limits_json = excluded.rate_limits_json
1283
+ WHERE excluded.timestamp_ms IS NULL
1284
+ OR codex_quota_candidates.timestamp_ms IS NULL
1285
+ OR excluded.timestamp_ms >= codex_quota_candidates.timestamp_ms
1286
+ `).run(
1287
+ candidate.filePath,
1288
+ key,
1289
+ observedAt,
1290
+ JSON.stringify(result.quota),
1291
+ );
1292
+ }
1293
+ }
1294
+ if (!result.usage || !result.timestamp) return;
1295
+ hourlyAdd(
1296
+ database,
1297
+ candidate.filePath,
1298
+ 'codex',
1299
+ codexState.sessionId,
1300
+ result.timestamp,
1301
+ result.usage,
1302
+ );
1303
+ if (result.timestamp >= recentBoundary) {
1304
+ recentUpsert(
1305
+ database,
1306
+ candidate.filePath,
1307
+ `offset:${lineEnd}`,
1308
+ 'codex',
1309
+ codexState.sessionId,
1310
+ result.timestamp,
1311
+ result.usage,
1312
+ );
1313
+ }
1314
+ metrics.parsed_events = Number(metrics.parsed_events) + 1;
1315
+ return;
1316
+ }
1317
+
1318
+ const claudeState = state as ClaudeState;
1319
+ const result = claudeRecord(record, claudeState);
1320
+ if (!result.usage || !result.timestamp) return;
1321
+ if (result.messageId) {
1322
+ claudeMessageUpsert(
1323
+ database,
1324
+ candidate.filePath,
1325
+ claudeState.sessionId,
1326
+ result.messageId,
1327
+ result.timestamp,
1328
+ result.usage,
1329
+ recentBoundary,
1330
+ );
1331
+ } else {
1332
+ hourlyAdd(
1333
+ database,
1334
+ candidate.filePath,
1335
+ 'claude',
1336
+ claudeState.sessionId,
1337
+ result.timestamp,
1338
+ result.usage,
1339
+ );
1340
+ if (result.timestamp >= recentBoundary) {
1341
+ recentUpsert(
1342
+ database,
1343
+ candidate.filePath,
1344
+ `offset:${lineEnd}`,
1345
+ 'claude',
1346
+ claudeState.sessionId,
1347
+ result.timestamp,
1348
+ result.usage,
1349
+ );
1350
+ }
1351
+ }
1352
+ metrics.parsed_events = Number(metrics.parsed_events) + 1;
1353
+ }
1354
+
1355
+ function isCompleteJsonValue(
1356
+ filePath: string,
1357
+ startOffset: number,
1358
+ endOffset: number,
1359
+ ): boolean {
1360
+ const descriptor = fs.openSync(filePath, 'r');
1361
+ let physicalOffset = startOffset;
1362
+ let started = false;
1363
+ let finished = false;
1364
+ let inString = false;
1365
+ let escaped = false;
1366
+ let invalid = false;
1367
+ const stack: number[] = [];
1368
+ try {
1369
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
1370
+ while (physicalOffset < endOffset && !invalid) {
1371
+ const length = fs.readSync(
1372
+ descriptor,
1373
+ buffer,
1374
+ 0,
1375
+ Math.min(buffer.length, endOffset - physicalOffset),
1376
+ physicalOffset,
1377
+ );
1378
+ if (length === 0) break;
1379
+ physicalOffset += length;
1380
+ for (const byte of buffer.subarray(0, length)) {
1381
+ if (!started) {
1382
+ if (byte === 0x20 || byte === 0x09 || byte === 0x0d || byte === 0x0a) continue;
1383
+ if (byte !== 0x7b && byte !== 0x5b) {
1384
+ invalid = true;
1385
+ break;
1386
+ }
1387
+ started = true;
1388
+ stack.push(byte);
1389
+ continue;
1390
+ }
1391
+ if (finished) {
1392
+ if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0d && byte !== 0x0a) {
1393
+ invalid = true;
1394
+ break;
1395
+ }
1396
+ continue;
1397
+ }
1398
+ if (inString) {
1399
+ if (escaped) {
1400
+ escaped = false;
1401
+ } else if (byte === 0x5c) {
1402
+ escaped = true;
1403
+ } else if (byte === 0x22) {
1404
+ inString = false;
1405
+ }
1406
+ continue;
1407
+ }
1408
+ if (byte === 0x22) {
1409
+ inString = true;
1410
+ } else if (byte === 0x7b || byte === 0x5b) {
1411
+ stack.push(byte);
1412
+ } else if (byte === 0x7d || byte === 0x5d) {
1413
+ const expected = byte === 0x7d ? 0x7b : 0x5b;
1414
+ if (stack.pop() !== expected) {
1415
+ invalid = true;
1416
+ break;
1417
+ }
1418
+ if (stack.length === 0) finished = true;
1419
+ }
1420
+ }
1421
+ }
1422
+ return physicalOffset >= endOffset
1423
+ && started
1424
+ && finished
1425
+ && !inString
1426
+ && !escaped
1427
+ && !invalid
1428
+ && stack.length === 0;
1429
+ } finally {
1430
+ fs.closeSync(descriptor);
1431
+ }
1432
+ }
1433
+
1434
+ function scanFile(
1435
+ database: DatabaseSync,
1436
+ candidate: SourceCandidate,
1437
+ recentBoundary: number,
1438
+ deadline: number,
1439
+ metrics: Metrics,
1440
+ ): boolean {
1441
+ let row = sourceRow(database, candidate.filePath);
1442
+ if (
1443
+ row
1444
+ && row.provider === candidate.provider
1445
+ && Number(row.size) === candidate.stat.size
1446
+ && Number(row.mtime_ms) === candidate.stat.mtimeMs
1447
+ && Number(row.scan_complete) === 1
1448
+ ) {
1449
+ metrics.reused_files = Number(metrics.reused_files) + 1;
1450
+ return true;
1451
+ }
1452
+
1453
+ let rebuild = !row || row.provider !== candidate.provider;
1454
+ if (row && !rebuild) {
1455
+ const sameIdentity = Number(row.file_dev) === Number(candidate.stat.dev)
1456
+ && Number(row.file_ino) === Number(candidate.stat.ino);
1457
+ if (candidate.stat.size < Number(row.committed_offset) || !sameIdentity) {
1458
+ rebuild = true;
1459
+ } else if (candidate.stat.mtimeMs !== Number(row.mtime_ms)) {
1460
+ if (
1461
+ prefixHash(candidate.filePath, Number(row.size)) !== row.prefix_sha256
1462
+ || suffixHash(candidate.filePath, Number(row.size)) !== row.suffix_sha256
1463
+ ) {
1464
+ rebuild = true;
1465
+ } else if (
1466
+ candidate.stat.size === Number(row.size)
1467
+ && Number(row.scan_complete) === 1
1468
+ ) {
1469
+ database.prepare(
1470
+ 'UPDATE source_files SET mtime_ms = ?, file_dev = ?, file_ino = ?, '
1471
+ + 'last_checked_ms = ? WHERE path = ?',
1472
+ ).run(
1473
+ candidate.stat.mtimeMs,
1474
+ Number(candidate.stat.dev),
1475
+ Number(candidate.stat.ino),
1476
+ Date.now(),
1477
+ candidate.filePath,
1478
+ );
1479
+ metrics.reused_files = Number(metrics.reused_files) + 1;
1480
+ return true;
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ if (rebuild) {
1486
+ if (row) {
1487
+ deleteSource(database, candidate.filePath);
1488
+ metrics[`${candidate.provider}_cache_rebuilt`] = true;
1489
+ }
1490
+ const state = defaultState(candidate.provider, candidate.filePath);
1491
+ if (candidate.provider === 'codex') {
1492
+ const codexState = state as CodexState;
1493
+ codexState.sessionId = candidate.sessionId;
1494
+ codexState.parentSessionId = candidate.parentSessionId;
1495
+ codexState.forkTimestamp = candidate.forkTimestamp;
1496
+ codexState.copiedPrefix = candidate.copiedPrefix;
1497
+ codexState.inheritedRaw = cloneTotals(candidate.parentBaseline);
1498
+ }
1499
+ insertSource(database, candidate, state, prefixHash(candidate.filePath));
1500
+ row = sourceRow(database, candidate.filePath);
1501
+ metrics.rebuilt_files = Number(metrics.rebuilt_files) + 1;
1502
+ } else {
1503
+ metrics.appended_files = Number(metrics.appended_files) + 1;
1504
+ }
1505
+ if (!row) throw new Error(`Unable to create usage checkpoint for ${candidate.filePath}`);
1506
+
1507
+ const state = JSON.parse(String(row.parser_state)) as ParserState;
1508
+ let committedOffset = Number(row.committed_offset);
1509
+ let physicalOffset = committedOffset;
1510
+ let fullChunks: Buffer[] = [];
1511
+ let lineBytes = 0;
1512
+ let first = Buffer.alloc(0);
1513
+ let tail = Buffer.alloc(0);
1514
+ let truncated = false;
1515
+ let stoppedForBudget = false;
1516
+
1517
+ const resetLine = () => {
1518
+ fullChunks = [];
1519
+ lineBytes = 0;
1520
+ first = Buffer.alloc(0);
1521
+ tail = Buffer.alloc(0);
1522
+ truncated = false;
1523
+ };
1524
+ const appendPart = (part: Buffer) => {
1525
+ lineBytes += part.length;
1526
+ if (!truncated && lineBytes <= MAX_LINE_BYTES) {
1527
+ fullChunks.push(part);
1528
+ return;
1529
+ }
1530
+ if (!truncated) {
1531
+ const combined = Buffer.concat(fullChunks);
1532
+ first = combined.subarray(0, TRUNCATED_EDGE_BYTES);
1533
+ tail = combined.subarray(Math.max(0, combined.length - TRUNCATED_EDGE_BYTES));
1534
+ fullChunks = [];
1535
+ truncated = true;
1536
+ }
1537
+ if (first.length < TRUNCATED_EDGE_BYTES) {
1538
+ const needed = TRUNCATED_EDGE_BYTES - first.length;
1539
+ first = Buffer.concat([first, part.subarray(0, needed)]);
1540
+ }
1541
+ tail = Buffer.concat([tail, part]);
1542
+ if (tail.length > TRUNCATED_EDGE_BYTES) {
1543
+ tail = tail.subarray(tail.length - TRUNCATED_EDGE_BYTES);
1544
+ }
1545
+ };
1546
+ const finishLine = (lineEnd: number, requireCompleteJson = false): boolean => {
1547
+ const fullLine = truncated ? null : Buffer.concat(fullChunks);
1548
+ if (requireCompleteJson) {
1549
+ if (fullLine) {
1550
+ try {
1551
+ JSON.parse(fullLine.toString('utf8'));
1552
+ } catch {
1553
+ return false;
1554
+ }
1555
+ } else if (!isCompleteJsonValue(candidate.filePath, committedOffset, lineEnd)) {
1556
+ return false;
1557
+ }
1558
+ }
1559
+ const record = parseRecord(candidate.provider, fullLine, first, tail);
1560
+ if (record) {
1561
+ processRecord(
1562
+ database,
1563
+ candidate,
1564
+ state,
1565
+ record,
1566
+ lineEnd,
1567
+ recentBoundary,
1568
+ metrics,
1569
+ );
1570
+ }
1571
+ committedOffset = lineEnd;
1572
+ resetLine();
1573
+ return true;
1574
+ };
1575
+
1576
+ const descriptor = fs.openSync(candidate.filePath, 'r');
1577
+ try {
1578
+ const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
1579
+ while (true) {
1580
+ const length = fs.readSync(
1581
+ descriptor,
1582
+ chunk,
1583
+ 0,
1584
+ chunk.length,
1585
+ physicalOffset,
1586
+ );
1587
+ if (length === 0) break;
1588
+ metrics.bytes_read = Number(metrics.bytes_read) + length;
1589
+ const block = chunk.subarray(0, length);
1590
+ let cursor = 0;
1591
+ while (cursor < block.length) {
1592
+ const newline = block.indexOf(0x0a, cursor);
1593
+ const end = newline < 0 ? block.length : newline + 1;
1594
+ appendPart(Buffer.from(block.subarray(cursor, end)));
1595
+ physicalOffset += end - cursor;
1596
+ cursor = end;
1597
+ if (newline >= 0) finishLine(physicalOffset);
1598
+ }
1599
+ if (Date.now() >= deadline && lineBytes === 0 && physicalOffset < candidate.stat.size) {
1600
+ stoppedForBudget = true;
1601
+ break;
1602
+ }
1603
+ }
1604
+ } finally {
1605
+ fs.closeSync(descriptor);
1606
+ }
1607
+
1608
+ const finalStat = fs.statSync(candidate.filePath);
1609
+ if (!stoppedForBudget && physicalOffset >= finalStat.size && lineBytes > 0) {
1610
+ finishLine(physicalOffset, true);
1611
+ }
1612
+ const scanComplete = !stoppedForBudget
1613
+ && physicalOffset >= finalStat.size
1614
+ && committedOffset >= finalStat.size;
1615
+ database.prepare(`
1616
+ UPDATE source_files SET
1617
+ size = ?, mtime_ms = ?, committed_offset = ?, file_dev = ?, file_ino = ?,
1618
+ prefix_sha256 = ?, suffix_sha256 = ?, parser_state = ?, session_id = ?,
1619
+ scan_complete = ?, last_checked_ms = ?
1620
+ WHERE path = ?
1621
+ `).run(
1622
+ finalStat.size,
1623
+ finalStat.mtimeMs,
1624
+ committedOffset,
1625
+ Number(finalStat.dev),
1626
+ Number(finalStat.ino),
1627
+ prefixHash(candidate.filePath, finalStat.size),
1628
+ suffixHash(candidate.filePath, finalStat.size),
1629
+ JSON.stringify(state),
1630
+ state.sessionId,
1631
+ scanComplete ? 1 : 0,
1632
+ Date.now(),
1633
+ candidate.filePath,
1634
+ );
1635
+ metrics[scanComplete ? 'scanned_files' : 'partial_files'] =
1636
+ Number(metrics[scanComplete ? 'scanned_files' : 'partial_files']) + 1;
1637
+ return scanComplete;
1638
+ }
1639
+
1640
+ function probeCodex(filePath: string): {
1641
+ sessionId: string;
1642
+ parentSessionId: string | null;
1643
+ forkTimestamp: number | null;
1644
+ copiedPrefix: boolean;
1645
+ } {
1646
+ const fallback = path.basename(filePath, '.jsonl');
1647
+ let descriptor: number | null = null;
1648
+ try {
1649
+ descriptor = fs.openSync(filePath, 'r');
1650
+ // Codex writes the authoritative session_meta first. A small bounded probe
1651
+ // avoids rereading megabytes from every uncached rollout during discovery.
1652
+ const buffer = Buffer.allocUnsafe(PREFIX_BYTES);
1653
+ const length = fs.readSync(descriptor, buffer, 0, buffer.length, 0);
1654
+ const lines = buffer.subarray(0, length).toString('utf8').split('\n');
1655
+ let leaf: ReturnType<typeof parseCodexMeta> = null;
1656
+ let copiedPrefix = false;
1657
+ for (const line of lines) {
1658
+ if (!line.includes('session_meta')) continue;
1659
+ try {
1660
+ const parsed = JSON.parse(line);
1661
+ const metadata = parseCodexMeta(parsed);
1662
+ if (!metadata) continue;
1663
+ if (!leaf) {
1664
+ leaf = metadata;
1665
+ } else if (metadata.id && metadata.id !== leaf.id) {
1666
+ copiedPrefix = true;
1667
+ }
1668
+ } catch {
1669
+ // A partial final probe line is expected.
1670
+ }
1671
+ }
1672
+ return {
1673
+ sessionId: leaf?.id || fallback,
1674
+ parentSessionId: leaf?.parentId || null,
1675
+ forkTimestamp: leaf?.timestamp || null,
1676
+ copiedPrefix,
1677
+ };
1678
+ } catch {
1679
+ return {
1680
+ sessionId: fallback,
1681
+ parentSessionId: null,
1682
+ forkTimestamp: null,
1683
+ copiedPrefix: false,
1684
+ };
1685
+ } finally {
1686
+ if (descriptor !== null) fs.closeSync(descriptor);
1687
+ }
1688
+ }
1689
+
1690
+ function codexRawTotalsAt(
1691
+ filePath: string,
1692
+ cutoff: number,
1693
+ metrics: Metrics,
1694
+ ): Totals | null {
1695
+ const descriptor = fs.openSync(filePath, 'r');
1696
+ let physicalOffset = 0;
1697
+ let chunks: Buffer[] = [];
1698
+ let lineBytes = 0;
1699
+ let latest: Totals | null = null;
1700
+ let accumulated: Totals | null = null;
1701
+ let done = false;
1702
+ const consume = () => {
1703
+ if (lineBytes === 0 || lineBytes > 512 * 1024) {
1704
+ chunks = [];
1705
+ lineBytes = 0;
1706
+ return;
1707
+ }
1708
+ try {
1709
+ const record = JSON.parse(Buffer.concat(chunks).toString('utf8')) as
1710
+ Record<string, unknown>;
1711
+ const observedAt = timestampMs(record.timestamp);
1712
+ if (observedAt && observedAt > cutoff) {
1713
+ done = true;
1714
+ return;
1715
+ }
1716
+ const payload = record.payload && typeof record.payload === 'object'
1717
+ ? record.payload as Record<string, unknown>
1718
+ : {};
1719
+ if (record.type !== 'event_msg' || payload.type !== 'token_count') return;
1720
+ const info = payload.info && typeof payload.info === 'object'
1721
+ ? payload.info as Record<string, unknown>
1722
+ : {};
1723
+ const total = normalizedCodexTotals(info.total_token_usage);
1724
+ const last = normalizedCodexTotals(info.last_token_usage);
1725
+ if (total) {
1726
+ latest = total;
1727
+ accumulated = total;
1728
+ } else if (last) {
1729
+ accumulated = addTotals(accumulated, last);
1730
+ latest = accumulated;
1731
+ }
1732
+ } catch {
1733
+ // Only complete, canonical token rows contribute to a parent snapshot.
1734
+ } finally {
1735
+ chunks = [];
1736
+ lineBytes = 0;
1737
+ }
1738
+ };
1739
+ try {
1740
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
1741
+ while (!done) {
1742
+ const length = fs.readSync(
1743
+ descriptor,
1744
+ buffer,
1745
+ 0,
1746
+ buffer.length,
1747
+ physicalOffset,
1748
+ );
1749
+ if (length === 0) break;
1750
+ metrics.bytes_read = Number(metrics.bytes_read) + length;
1751
+ const block = buffer.subarray(0, length);
1752
+ let cursor = 0;
1753
+ while (cursor < block.length && !done) {
1754
+ const newline = block.indexOf(0x0a, cursor);
1755
+ const end = newline < 0 ? block.length : newline + 1;
1756
+ const part = Buffer.from(block.subarray(cursor, end));
1757
+ lineBytes += part.length;
1758
+ if (lineBytes <= 512 * 1024) chunks.push(part);
1759
+ physicalOffset += part.length;
1760
+ cursor = end;
1761
+ if (newline >= 0) consume();
1762
+ }
1763
+ }
1764
+ if (!done && lineBytes > 0) consume();
1765
+ return cloneTotals(latest);
1766
+ } finally {
1767
+ fs.closeSync(descriptor);
1768
+ }
1769
+ }
1770
+
1771
+ function rowEvent(row: Record<string, unknown>): UsageEvent {
1772
+ const input = Number(row.input_tokens);
1773
+ const output = Number(row.output_tokens);
1774
+ const cached = Number(row.cache_read_tokens);
1775
+ const cacheWrite = Number(row.cache_write_tokens);
1776
+ return {
1777
+ timestamp: Number(row.timestamp_ms),
1778
+ sessionId: String(row.session_id),
1779
+ totalTokens: input + output + cached + cacheWrite,
1780
+ inputTokens: input,
1781
+ outputTokens: output,
1782
+ cacheReadTokens: cached,
1783
+ cacheWriteTokens: cacheWrite,
1784
+ unattributedTokens: 0,
1785
+ };
1786
+ }
1787
+
1788
+ function providerEvents(
1789
+ database: DatabaseSync,
1790
+ provider: Provider,
1791
+ retentionCutoff: number,
1792
+ recentBoundary: number,
1793
+ ): UsageEvent[] {
1794
+ const rows = database.prepare(`
1795
+ SELECT provider || '-aggregate' AS session_id, bucket_start_ms AS timestamp_ms,
1796
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
1797
+ FROM usage_provider_hourly
1798
+ WHERE provider = ? AND bucket_start_ms >= ? AND bucket_start_ms < ?
1799
+ UNION ALL
1800
+ SELECT session_id, timestamp_ms,
1801
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens
1802
+ FROM recent_events
1803
+ WHERE provider = ? AND timestamp_ms >= ?
1804
+ ORDER BY timestamp_ms, session_id
1805
+ `).all(
1806
+ provider,
1807
+ localHourMs(retentionCutoff),
1808
+ recentBoundary,
1809
+ provider,
1810
+ recentBoundary,
1811
+ ) as Array<Record<string, unknown>>;
1812
+ return rows.map(rowEvent);
1813
+ }
1814
+
1815
+ function latestQuotas(database: DatabaseSync): unknown[] {
1816
+ const rows = database.prepare(`
1817
+ SELECT timestamp_ms, rate_limits_json
1818
+ FROM codex_quota_candidates
1819
+ ORDER BY timestamp_ms DESC
1820
+ LIMIT 32
1821
+ `).all() as Array<{ timestamp_ms: number | null; rate_limits_json: string }>;
1822
+ const candidates: Array<{ timestamp: number | null; rateLimits: unknown }> = [];
1823
+ for (const row of rows) {
1824
+ try {
1825
+ candidates.push({
1826
+ timestamp: row.timestamp_ms === null ? null : Number(row.timestamp_ms),
1827
+ rateLimits: JSON.parse(row.rate_limits_json),
1828
+ });
1829
+ } catch {
1830
+ // A malformed disposable cache row is ignored until its source is rebuilt.
1831
+ }
1832
+ }
1833
+ return candidates;
1834
+ }
1835
+
1836
+ function scalarCount(database: DatabaseSync, sql: string): number {
1837
+ const row = database.prepare(sql).get() as { count: number };
1838
+ return Number(row.count);
1839
+ }
1840
+
1841
+ function metadataValue(database: DatabaseSync, key: string): string {
1842
+ const row = database.prepare('SELECT value FROM metadata WHERE key = ?').get(key) as
1843
+ { value?: string } | undefined;
1844
+ return String(row?.value || '');
1845
+ }
1846
+
1847
+ function setMetadataValue(database: DatabaseSync, key: string, value: string): void {
1848
+ database.prepare(
1849
+ 'INSERT OR REPLACE INTO metadata(key, value) VALUES(?, ?)',
1850
+ ).run(key, value);
1851
+ }
1852
+
1853
+ function configuredRoots(roots: Partial<Record<Provider, string[]>>): Array<{
1854
+ provider: Provider;
1855
+ rootPath: string;
1856
+ }> {
1857
+ const configured: Array<{ provider: Provider; rootPath: string }> = [];
1858
+ for (const provider of ['codex', 'claude'] as const) {
1859
+ for (const root of Array.from(new Set(roots[provider] || [])).sort()) {
1860
+ configured.push({ provider, rootPath: path.resolve(root) });
1861
+ }
1862
+ }
1863
+ return configured;
1864
+ }
1865
+
1866
+ function resetDiscovery(database: DatabaseSync, cacheFile: string): void {
1867
+ closeCacheDirectoryCursors(cacheFile);
1868
+ database.exec(`
1869
+ DELETE FROM discovery_source_queue;
1870
+ DELETE FROM discovery_directories;
1871
+ DELETE FROM codex_quota_candidates;
1872
+ DELETE FROM claude_messages;
1873
+ DELETE FROM usage_provider_hourly;
1874
+ DELETE FROM source_files;
1875
+ `);
1876
+ }
1877
+
1878
+ function ensureDiscoveryRoots(
1879
+ database: DatabaseSync,
1880
+ roots: Partial<Record<Provider, string[]>>,
1881
+ nowMs: number,
1882
+ cacheFile: string,
1883
+ ): void {
1884
+ const configured = configuredRoots(roots);
1885
+ const signature = JSON.stringify(configured);
1886
+ if (metadataValue(database, 'roots_signature') !== signature) {
1887
+ resetDiscovery(database, cacheFile);
1888
+ setMetadataValue(database, 'roots_signature', signature);
1889
+ setMetadataValue(database, 'discovery_ready', '0');
1890
+ }
1891
+ const configuredPaths = new Set(configured.map(entry => entry.rootPath));
1892
+ database.prepare(
1893
+ 'DELETE FROM discovery_directories WHERE is_root = 1 AND path NOT IN ('
1894
+ + (configured.length ? configured.map(() => '?').join(',') : "''")
1895
+ + ')',
1896
+ ).run(...configured.map(entry => entry.rootPath));
1897
+ for (const { provider, rootPath } of configured) {
1898
+ database.prepare(`
1899
+ INSERT INTO discovery_directories(
1900
+ path, provider, root_path, parent_path, is_root, present, pending,
1901
+ scan_generation, parent_generation, mtime_ms, file_dev, file_ino,
1902
+ last_checked_ms
1903
+ ) VALUES(?, ?, ?, '', 1, 1, 1, 0, 0, -1, -1, -1, 0)
1904
+ ON CONFLICT(path) DO UPDATE SET
1905
+ provider = excluded.provider,
1906
+ root_path = excluded.root_path,
1907
+ is_root = 1,
1908
+ present = 1,
1909
+ last_checked_ms = MIN(discovery_directories.last_checked_ms, ?)
1910
+ `).run(rootPath, provider, rootPath, nowMs);
1911
+ }
1912
+ if (configuredPaths.size === 0) setMetadataValue(database, 'discovery_ready', '1');
1913
+ }
1914
+
1915
+ function queueSource(
1916
+ database: DatabaseSync,
1917
+ provider: Provider,
1918
+ filePath: string,
1919
+ action: 'upsert' | 'remove',
1920
+ priority: number,
1921
+ nowMs: number,
1922
+ ): void {
1923
+ database.prepare(`
1924
+ INSERT INTO discovery_source_queue(path, provider, action, priority, queued_at_ms)
1925
+ VALUES(?, ?, ?, ?, ?)
1926
+ ON CONFLICT(path) DO UPDATE SET
1927
+ provider = excluded.provider,
1928
+ action = excluded.action,
1929
+ priority = excluded.priority,
1930
+ queued_at_ms = excluded.queued_at_ms
1931
+ `).run(filePath, provider, action, priority, nowMs);
1932
+ }
1933
+
1934
+ type DirectoryCursor = {
1935
+ directory: fs.Dir;
1936
+ startStat: fs.Stats;
1937
+ generation: number;
1938
+ };
1939
+ const directoryCursors = new Map<string, DirectoryCursor>();
1940
+
1941
+ function directoryCursorKey(cacheFile: string, directoryPath: string): string {
1942
+ return `${path.resolve(cacheFile)}\0${directoryPath}`;
1943
+ }
1944
+
1945
+ function closeDirectoryCursor(cacheFile: string, directoryPath: string): void {
1946
+ const key = directoryCursorKey(cacheFile, directoryPath);
1947
+ const cursor = directoryCursors.get(key);
1948
+ if (!cursor) return;
1949
+ directoryCursors.delete(key);
1950
+ try {
1951
+ cursor.directory.closeSync();
1952
+ } catch {
1953
+ // The directory may already have disappeared.
1954
+ }
1955
+ }
1956
+
1957
+ function closeCacheDirectoryCursors(cacheFile: string): void {
1958
+ const prefix = `${path.resolve(cacheFile)}\0`;
1959
+ for (const [key, cursor] of directoryCursors) {
1960
+ if (!key.startsWith(prefix)) continue;
1961
+ directoryCursors.delete(key);
1962
+ try {
1963
+ cursor.directory.closeSync();
1964
+ } catch {
1965
+ // The directory may already have disappeared.
1966
+ }
1967
+ }
1968
+ }
1969
+
1970
+ function markDirectoryTreeMissing(
1971
+ database: DatabaseSync,
1972
+ directoryPath: string,
1973
+ nowMs: number,
1974
+ cacheFile: string,
1975
+ ): void {
1976
+ closeDirectoryCursor(cacheFile, directoryPath);
1977
+ const prefix = `${directoryPath}${path.sep}%`;
1978
+ database.prepare(
1979
+ 'UPDATE discovery_directories SET present = 0, pending = 0, last_checked_ms = ? '
1980
+ + 'WHERE path = ? OR path LIKE ?',
1981
+ ).run(nowMs, directoryPath, prefix);
1982
+ database.prepare(`
1983
+ INSERT INTO discovery_source_queue(path, provider, action, priority, queued_at_ms)
1984
+ SELECT path, provider, 'remove', -1, ?
1985
+ FROM source_files
1986
+ WHERE directory_path = ? OR directory_path LIKE ?
1987
+ ON CONFLICT(path) DO UPDATE SET
1988
+ action = CASE
1989
+ WHEN discovery_source_queue.action = 'upsert' THEN 'upsert'
1990
+ ELSE 'remove'
1991
+ END,
1992
+ queued_at_ms = MIN(discovery_source_queue.queued_at_ms, excluded.queued_at_ms)
1993
+ `).run(nowMs, directoryPath, prefix);
1994
+ }
1995
+
1996
+ function auditDirectories(
1997
+ database: DatabaseSync,
1998
+ nowMs: number,
1999
+ deadline: number,
2000
+ metrics: Metrics,
2001
+ cacheFile: string,
2002
+ ): void {
2003
+ const rows = database.prepare(`
2004
+ SELECT * FROM discovery_directories
2005
+ WHERE present = 1
2006
+ ORDER BY is_root DESC, last_checked_ms ASC
2007
+ LIMIT ?
2008
+ `).all(DIRECTORY_AUDIT_BATCH) as Array<Record<string, unknown>>;
2009
+ for (const row of rows) {
2010
+ if (Date.now() >= deadline) break;
2011
+ const directoryPath = String(row.path);
2012
+ try {
2013
+ const stat = fs.statSync(directoryPath);
2014
+ const changed = !stat.isDirectory()
2015
+ || Number(row.file_dev) !== Number(stat.dev)
2016
+ || Number(row.file_ino) !== Number(stat.ino)
2017
+ || Number(row.mtime_ms) !== stat.mtimeMs;
2018
+ database.prepare(`
2019
+ UPDATE discovery_directories SET
2020
+ present = ?, pending = CASE WHEN ? THEN 1 ELSE pending END,
2021
+ mtime_ms = ?, file_dev = ?, file_ino = ?, last_checked_ms = ?
2022
+ WHERE path = ?
2023
+ `).run(
2024
+ stat.isDirectory() ? 1 : 0,
2025
+ changed ? 1 : 0,
2026
+ stat.mtimeMs,
2027
+ Number(stat.dev),
2028
+ Number(stat.ino),
2029
+ nowMs,
2030
+ directoryPath,
2031
+ );
2032
+ if (!stat.isDirectory()) {
2033
+ markDirectoryTreeMissing(database, directoryPath, nowMs, cacheFile);
2034
+ }
2035
+ } catch {
2036
+ markDirectoryTreeMissing(database, directoryPath, nowMs, cacheFile);
2037
+ }
2038
+ metrics.audited_directories = Number(metrics.audited_directories) + 1;
2039
+ }
2040
+ }
2041
+
2042
+ function scanPendingDirectories(
2043
+ database: DatabaseSync,
2044
+ nowMs: number,
2045
+ deadline: number,
2046
+ metrics: Metrics,
2047
+ cacheFile: string,
2048
+ ): void {
2049
+ const remainingEntryBudget = DIRECTORY_ENTRY_BATCH
2050
+ - Number(metrics.enumerated_entries);
2051
+ if (remainingEntryBudget <= 0) return;
2052
+ const row = database.prepare(`
2053
+ SELECT * FROM discovery_directories
2054
+ WHERE present = 1 AND pending = 1
2055
+ ORDER BY is_root DESC, last_checked_ms ASC
2056
+ LIMIT 1
2057
+ `).get() as Record<string, unknown> | undefined;
2058
+ if (!row || Date.now() >= deadline) return;
2059
+ const directoryPath = String(row.path);
2060
+ const provider = String(row.provider) as Provider;
2061
+ const cursorKey = directoryCursorKey(cacheFile, directoryPath);
2062
+ let cursor = directoryCursors.get(cursorKey);
2063
+ try {
2064
+ if (!cursor) {
2065
+ const startStat = fs.statSync(directoryPath);
2066
+ const generation = Number(row.scan_generation) + 1;
2067
+ cursor = {
2068
+ directory: fs.opendirSync(directoryPath),
2069
+ startStat,
2070
+ generation,
2071
+ };
2072
+ directoryCursors.set(cursorKey, cursor);
2073
+ database.prepare(
2074
+ 'UPDATE discovery_directories '
2075
+ + 'SET scan_generation = ?, last_checked_ms = ? WHERE path = ?',
2076
+ ).run(generation, nowMs, directoryPath);
2077
+ }
2078
+ } catch {
2079
+ markDirectoryTreeMissing(database, directoryPath, nowMs, cacheFile);
2080
+ return;
2081
+ }
2082
+
2083
+ const entries: fs.Dirent[] = [];
2084
+ let complete = false;
2085
+ try {
2086
+ while (entries.length < remainingEntryBudget && Date.now() < deadline) {
2087
+ const entry = cursor.directory.readSync();
2088
+ if (!entry) {
2089
+ complete = true;
2090
+ break;
2091
+ }
2092
+ entries.push(entry);
2093
+ }
2094
+ } catch {
2095
+ closeDirectoryCursor(cacheFile, directoryPath);
2096
+ return;
2097
+ }
2098
+
2099
+ database.exec('BEGIN IMMEDIATE');
2100
+ try {
2101
+ for (const entry of entries) {
2102
+ const child = path.join(directoryPath, entry.name);
2103
+ if (entry.isDirectory()) {
2104
+ let childStat: fs.Stats;
2105
+ try {
2106
+ childStat = fs.statSync(child);
2107
+ } catch {
2108
+ continue;
2109
+ }
2110
+ database.prepare(`
2111
+ INSERT INTO discovery_directories(
2112
+ path, provider, root_path, parent_path, is_root, present, pending,
2113
+ scan_generation, parent_generation, mtime_ms, file_dev, file_ino,
2114
+ last_checked_ms
2115
+ ) VALUES(?, ?, ?, ?, 0, 1, 1, 0, ?, ?, ?, ?, 0)
2116
+ ON CONFLICT(path) DO UPDATE SET
2117
+ present = 1,
2118
+ parent_generation = excluded.parent_generation,
2119
+ pending = CASE
2120
+ WHEN discovery_directories.file_dev != excluded.file_dev
2121
+ OR discovery_directories.file_ino != excluded.file_ino
2122
+ OR discovery_directories.mtime_ms != excluded.mtime_ms
2123
+ THEN 1 ELSE discovery_directories.pending
2124
+ END,
2125
+ mtime_ms = excluded.mtime_ms,
2126
+ file_dev = excluded.file_dev,
2127
+ file_ino = excluded.file_ino
2128
+ `).run(
2129
+ child,
2130
+ provider,
2131
+ String(row.root_path),
2132
+ directoryPath,
2133
+ cursor.generation,
2134
+ childStat.mtimeMs,
2135
+ Number(childStat.dev),
2136
+ Number(childStat.ino),
2137
+ );
2138
+ } else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
2139
+ try {
2140
+ const stat = fs.statSync(child);
2141
+ const cached = sourceRow(database, child);
2142
+ if (cached) {
2143
+ database.prepare(
2144
+ 'UPDATE source_files SET directory_generation = ? WHERE path = ?',
2145
+ ).run(cursor.generation, child);
2146
+ }
2147
+ if (
2148
+ !cached
2149
+ || Number(cached.size) !== stat.size
2150
+ || Number(cached.mtime_ms) !== stat.mtimeMs
2151
+ || Number(cached.file_dev) !== Number(stat.dev)
2152
+ || Number(cached.file_ino) !== Number(stat.ino)
2153
+ || Number(cached.scan_complete) !== 1
2154
+ ) {
2155
+ queueSource(database, provider, child, 'upsert', stat.mtimeMs, nowMs);
2156
+ } else {
2157
+ database.prepare(
2158
+ 'UPDATE source_files SET last_checked_ms = ? WHERE path = ?',
2159
+ ).run(nowMs, child);
2160
+ metrics.reused_files = Number(metrics.reused_files) + 1;
2161
+ }
2162
+ } catch {
2163
+ // A later directory pass or source audit retries transient entries.
2164
+ }
2165
+ }
2166
+ }
2167
+
2168
+ if (complete) {
2169
+ const endStat = fs.statSync(directoryPath);
2170
+ const stable = Number(cursor.startStat.dev) === Number(endStat.dev)
2171
+ && Number(cursor.startStat.ino) === Number(endStat.ino)
2172
+ && cursor.startStat.mtimeMs === endStat.mtimeMs;
2173
+ if (stable) {
2174
+ const missingChildren = database.prepare(`
2175
+ SELECT path FROM discovery_directories
2176
+ WHERE parent_path = ? AND present = 1 AND parent_generation < ?
2177
+ `).all(directoryPath, cursor.generation) as Array<{ path: string }>;
2178
+ for (const child of missingChildren) {
2179
+ markDirectoryTreeMissing(database, child.path, nowMs, cacheFile);
2180
+ }
2181
+ database.prepare(`
2182
+ INSERT INTO discovery_source_queue(path, provider, action, priority, queued_at_ms)
2183
+ SELECT path, provider, 'remove', -1, ?
2184
+ FROM source_files
2185
+ WHERE directory_path = ? AND directory_generation < ?
2186
+ ON CONFLICT(path) DO UPDATE SET
2187
+ action = 'remove',
2188
+ priority = -1,
2189
+ queued_at_ms = excluded.queued_at_ms
2190
+ `).run(nowMs, directoryPath, cursor.generation);
2191
+ }
2192
+ database.prepare(`
2193
+ UPDATE discovery_directories SET
2194
+ present = 1, pending = ?, mtime_ms = ?, file_dev = ?, file_ino = ?,
2195
+ last_checked_ms = ?
2196
+ WHERE path = ?
2197
+ `).run(
2198
+ stable ? 0 : 1,
2199
+ endStat.mtimeMs,
2200
+ Number(endStat.dev),
2201
+ Number(endStat.ino),
2202
+ nowMs,
2203
+ directoryPath,
2204
+ );
2205
+ }
2206
+ database.exec('COMMIT');
2207
+ } catch (error) {
2208
+ database.exec('ROLLBACK');
2209
+ closeDirectoryCursor(cacheFile, directoryPath);
2210
+ if (
2211
+ error
2212
+ && typeof error === 'object'
2213
+ && 'code' in error
2214
+ && error.code === 'ENOENT'
2215
+ ) {
2216
+ markDirectoryTreeMissing(database, directoryPath, nowMs, cacheFile);
2217
+ return;
2218
+ }
2219
+ throw error;
2220
+ } finally {
2221
+ if (complete) closeDirectoryCursor(cacheFile, directoryPath);
2222
+ }
2223
+ metrics.enumerated_entries = Number(metrics.enumerated_entries) + entries.length;
2224
+ if (complete) {
2225
+ metrics.scanned_directories = Number(metrics.scanned_directories) + 1;
2226
+ if (
2227
+ Number(metrics.enumerated_entries) < DIRECTORY_ENTRY_BATCH
2228
+ && Date.now() < deadline
2229
+ ) {
2230
+ scanPendingDirectories(database, nowMs, deadline, metrics, cacheFile);
2231
+ }
2232
+ }
2233
+ }
2234
+
2235
+ function auditSources(
2236
+ database: DatabaseSync,
2237
+ nowMs: number,
2238
+ deadline: number,
2239
+ metrics: Metrics,
2240
+ ): void {
2241
+ const rows = database.prepare(`
2242
+ SELECT * FROM source_files
2243
+ ORDER BY last_checked_ms ASC
2244
+ LIMIT ?
2245
+ `).all(SOURCE_AUDIT_BATCH) as Array<Record<string, unknown>>;
2246
+ const recent = database.prepare(`
2247
+ SELECT * FROM source_files
2248
+ ORDER BY mtime_ms DESC
2249
+ LIMIT ?
2250
+ `).all(Math.min(32, SOURCE_AUDIT_BATCH)) as Array<Record<string, unknown>>;
2251
+ const byPath = new Map([...recent, ...rows].map(row => [String(row.path), row]));
2252
+ for (const row of byPath.values()) {
2253
+ if (Date.now() >= deadline) break;
2254
+ const filePath = String(row.path);
2255
+ try {
2256
+ const stat = fs.statSync(filePath);
2257
+ const changed = stat.size !== Number(row.size)
2258
+ || stat.mtimeMs !== Number(row.mtime_ms)
2259
+ || Number(stat.dev) !== Number(row.file_dev)
2260
+ || Number(stat.ino) !== Number(row.file_ino)
2261
+ || Number(row.scan_complete) !== 1;
2262
+ if (changed) {
2263
+ queueSource(
2264
+ database,
2265
+ String(row.provider) as Provider,
2266
+ filePath,
2267
+ 'upsert',
2268
+ stat.mtimeMs,
2269
+ nowMs,
2270
+ );
2271
+ } else {
2272
+ database.prepare(
2273
+ 'UPDATE source_files SET last_checked_ms = ? WHERE path = ?',
2274
+ ).run(nowMs, filePath);
2275
+ metrics.reused_files = Number(metrics.reused_files) + 1;
2276
+ }
2277
+ } catch {
2278
+ queueSource(
2279
+ database,
2280
+ String(row.provider) as Provider,
2281
+ filePath,
2282
+ 'remove',
2283
+ -1,
2284
+ nowMs,
2285
+ );
2286
+ }
2287
+ metrics.audited_files = Number(metrics.audited_files) + 1;
2288
+ }
2289
+ }
2290
+
2291
+ function candidateFromPath(
2292
+ database: DatabaseSync,
2293
+ provider: Provider,
2294
+ filePath: string,
2295
+ ): SourceCandidate {
2296
+ const stat = fs.statSync(filePath);
2297
+ const cached = sourceRow(database, filePath);
2298
+ if (provider === 'codex') {
2299
+ let probe: ReturnType<typeof probeCodex>;
2300
+ if (cached?.provider === 'codex') {
2301
+ try {
2302
+ const state = JSON.parse(String(cached.parser_state)) as CodexState;
2303
+ probe = {
2304
+ sessionId: state.sessionId,
2305
+ parentSessionId: state.parentSessionId,
2306
+ forkTimestamp: state.forkTimestamp,
2307
+ copiedPrefix: state.copiedPrefix,
2308
+ };
2309
+ } catch {
2310
+ probe = probeCodex(filePath);
2311
+ }
2312
+ } else {
2313
+ probe = probeCodex(filePath);
2314
+ }
2315
+ return { provider, filePath, stat, ...probe, parentBaseline: null };
2316
+ }
2317
+ return {
2318
+ provider,
2319
+ filePath,
2320
+ stat,
2321
+ sessionId: claudeOwnerSession(filePath),
2322
+ parentSessionId: null,
2323
+ forkTimestamp: null,
2324
+ copiedPrefix: false,
2325
+ parentBaseline: null,
2326
+ };
2327
+ }
2328
+
2329
+ function moveSource(
2330
+ database: DatabaseSync,
2331
+ row: Record<string, unknown>,
2332
+ candidate: SourceCandidate,
2333
+ ): void {
2334
+ const oldPath = String(row.path);
2335
+ database.prepare(`
2336
+ INSERT INTO source_files(
2337
+ path, provider, directory_path, directory_generation,
2338
+ size, mtime_ms, committed_offset, file_dev, file_ino,
2339
+ prefix_sha256, suffix_sha256, parser_state, session_id, scan_complete,
2340
+ last_checked_ms
2341
+ ) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2342
+ `).run(
2343
+ candidate.filePath,
2344
+ candidate.provider,
2345
+ path.dirname(candidate.filePath),
2346
+ directoryGeneration(database, path.dirname(candidate.filePath)),
2347
+ candidate.stat.size,
2348
+ candidate.stat.mtimeMs,
2349
+ Number(row.committed_offset),
2350
+ Number(candidate.stat.dev),
2351
+ Number(candidate.stat.ino),
2352
+ String(row.prefix_sha256),
2353
+ suffixHash(candidate.filePath, candidate.stat.size),
2354
+ String(row.parser_state),
2355
+ String(row.session_id),
2356
+ candidate.stat.size === Number(row.size) && Number(row.scan_complete) === 1 ? 1 : 0,
2357
+ Date.now(),
2358
+ );
2359
+ database.prepare(
2360
+ 'UPDATE usage_hourly SET source_path = ? WHERE source_path = ?',
2361
+ ).run(candidate.filePath, oldPath);
2362
+ database.prepare(
2363
+ 'UPDATE recent_events SET source_path = ? WHERE source_path = ?',
2364
+ ).run(candidate.filePath, oldPath);
2365
+ database.prepare(
2366
+ 'UPDATE claude_messages SET source_path = ? WHERE source_path = ?',
2367
+ ).run(candidate.filePath, oldPath);
2368
+ database.prepare(
2369
+ 'UPDATE codex_quota_candidates SET source_path = ? WHERE source_path = ?',
2370
+ ).run(candidate.filePath, oldPath);
2371
+ database.prepare('DELETE FROM source_files WHERE path = ?').run(oldPath);
2372
+ }
2373
+
2374
+ function prepareCodexCandidate(
2375
+ database: DatabaseSync,
2376
+ candidate: SourceCandidate,
2377
+ metrics: Metrics,
2378
+ ): boolean {
2379
+ if (sourceRow(database, candidate.filePath)) return true;
2380
+ const existing = database.prepare(
2381
+ "SELECT * FROM source_files WHERE provider = 'codex' AND session_id = ? AND path != ? "
2382
+ + 'ORDER BY mtime_ms DESC LIMIT 1',
2383
+ ).get(candidate.sessionId, candidate.filePath) as Record<string, unknown> | undefined;
2384
+ if (!existing) return true;
2385
+ const sameIdentity = Number(existing.file_dev) === Number(candidate.stat.dev)
2386
+ && Number(existing.file_ino) === Number(candidate.stat.ino);
2387
+ if (sameIdentity && !fs.existsSync(String(existing.path))) {
2388
+ moveSource(database, existing, candidate);
2389
+ metrics.moved_files = Number(metrics.moved_files) + 1;
2390
+ return true;
2391
+ }
2392
+ const existingArchived = String(existing.path).includes(`${path.sep}archived_sessions${path.sep}`);
2393
+ const candidateArchived = candidate.filePath.includes(`${path.sep}archived_sessions${path.sep}`);
2394
+ const candidateWins = (existingArchived && !candidateArchived)
2395
+ || (existingArchived === candidateArchived
2396
+ && candidate.stat.mtimeMs > Number(existing.mtime_ms));
2397
+ if (!candidateWins) return false;
2398
+ deleteSource(database, String(existing.path));
2399
+ return true;
2400
+ }
2401
+
2402
+ function resolveParentBaselineFromDatabase(
2403
+ database: DatabaseSync,
2404
+ candidate: SourceCandidate,
2405
+ cache: Map<string, Totals | null>,
2406
+ metrics: Metrics,
2407
+ ): Totals | null {
2408
+ if (!candidate.parentSessionId || !candidate.forkTimestamp) return null;
2409
+ const key = `${candidate.parentSessionId}:${candidate.forkTimestamp}`;
2410
+ if (cache.has(key)) return cloneTotals(cache.get(key));
2411
+ const row = database.prepare(
2412
+ "SELECT path FROM source_files WHERE provider = 'codex' AND session_id = ? LIMIT 1",
2413
+ ).get(candidate.parentSessionId) as { path?: string } | undefined;
2414
+ const baseline = row?.path && row.path !== candidate.filePath
2415
+ ? codexRawTotalsAt(row.path, candidate.forkTimestamp, metrics)
2416
+ : null;
2417
+ cache.set(key, cloneTotals(baseline));
2418
+ return baseline;
2419
+ }
2420
+
2421
+ function processSourceQueue(
2422
+ database: DatabaseSync,
2423
+ recentBoundary: number,
2424
+ deadline: number,
2425
+ metrics: Metrics,
2426
+ ): void {
2427
+ const parentCache = new Map<string, Totals | null>();
2428
+ while (Date.now() < deadline) {
2429
+ const task = (database.prepare(`
2430
+ SELECT * FROM discovery_source_queue
2431
+ WHERE action = 'upsert'
2432
+ ORDER BY priority ASC, queued_at_ms ASC
2433
+ LIMIT 1
2434
+ `).get() || database.prepare(`
2435
+ SELECT * FROM discovery_source_queue
2436
+ WHERE action = 'remove'
2437
+ ORDER BY priority ASC, queued_at_ms ASC
2438
+ LIMIT 1
2439
+ `).get()) as Record<string, unknown> | undefined;
2440
+ if (!task) return;
2441
+ const filePath = String(task.path);
2442
+ if (String(task.action) === 'remove') {
2443
+ database.exec('BEGIN IMMEDIATE');
2444
+ try {
2445
+ database.prepare(
2446
+ 'DELETE FROM discovery_source_queue WHERE path = ?',
2447
+ ).run(filePath);
2448
+ try {
2449
+ const stat = fs.statSync(filePath);
2450
+ queueSource(
2451
+ database,
2452
+ String(task.provider) as Provider,
2453
+ filePath,
2454
+ 'upsert',
2455
+ stat.mtimeMs,
2456
+ Date.now(),
2457
+ );
2458
+ } catch (error) {
2459
+ if (
2460
+ !error
2461
+ || typeof error !== 'object'
2462
+ || !('code' in error)
2463
+ || error.code !== 'ENOENT'
2464
+ ) {
2465
+ throw error;
2466
+ }
2467
+ deleteSource(database, filePath);
2468
+ metrics.removed_files = Number(metrics.removed_files) + 1;
2469
+ }
2470
+ database.exec('COMMIT');
2471
+ } catch (error) {
2472
+ database.exec('ROLLBACK');
2473
+ throw error;
2474
+ }
2475
+ continue;
2476
+ }
2477
+ try {
2478
+ const candidate = candidateFromPath(
2479
+ database,
2480
+ String(task.provider) as Provider,
2481
+ filePath,
2482
+ );
2483
+ database.exec('BEGIN IMMEDIATE');
2484
+ let complete = false;
2485
+ try {
2486
+ if (candidate.provider === 'codex') {
2487
+ if (!prepareCodexCandidate(database, candidate, metrics)) {
2488
+ database.prepare('DELETE FROM discovery_source_queue WHERE path = ?').run(filePath);
2489
+ database.exec('COMMIT');
2490
+ continue;
2491
+ }
2492
+ if (
2493
+ !sourceRow(database, filePath)
2494
+ && candidate.parentSessionId
2495
+ && !database.prepare(
2496
+ "SELECT 1 FROM source_files WHERE provider = 'codex' AND session_id = ? LIMIT 1",
2497
+ ).get(candidate.parentSessionId)
2498
+ ) {
2499
+ const otherUpsert = database.prepare(
2500
+ "SELECT 1 FROM discovery_source_queue "
2501
+ + "WHERE action = 'upsert' AND path != ? LIMIT 1",
2502
+ ).get(filePath);
2503
+ if (otherUpsert) {
2504
+ const row = database.prepare(
2505
+ "SELECT priority + 1 AS priority FROM discovery_source_queue "
2506
+ + "WHERE action = 'upsert' ORDER BY priority DESC LIMIT 1",
2507
+ ).get() as { priority: number };
2508
+ database.prepare(
2509
+ 'UPDATE discovery_source_queue SET priority = ?, queued_at_ms = ? WHERE path = ?',
2510
+ ).run(Number(row.priority), Date.now(), filePath);
2511
+ database.exec('COMMIT');
2512
+ continue;
2513
+ }
2514
+ }
2515
+ if (!sourceRow(database, filePath) && candidate.parentSessionId) {
2516
+ candidate.parentBaseline = resolveParentBaselineFromDatabase(
2517
+ database,
2518
+ candidate,
2519
+ parentCache,
2520
+ metrics,
2521
+ );
2522
+ }
2523
+ }
2524
+ complete = scanFile(database, candidate, recentBoundary, deadline, metrics);
2525
+ database.exec('COMMIT');
2526
+ } catch (error) {
2527
+ database.exec('ROLLBACK');
2528
+ throw error;
2529
+ }
2530
+ if (complete) {
2531
+ database.prepare('DELETE FROM discovery_source_queue WHERE path = ?').run(filePath);
2532
+ } else {
2533
+ return;
2534
+ }
2535
+ } catch (error) {
2536
+ if (
2537
+ error
2538
+ && typeof error === 'object'
2539
+ && 'code' in error
2540
+ && error.code === 'ENOENT'
2541
+ ) {
2542
+ database.exec('BEGIN IMMEDIATE');
2543
+ try {
2544
+ deleteSource(database, filePath);
2545
+ database.prepare(
2546
+ 'DELETE FROM discovery_source_queue WHERE path = ?',
2547
+ ).run(filePath);
2548
+ database.exec('COMMIT');
2549
+ } catch (cleanupError) {
2550
+ database.exec('ROLLBACK');
2551
+ throw cleanupError;
2552
+ }
2553
+ metrics.removed_files = Number(metrics.removed_files) + 1;
2554
+ continue;
2555
+ }
2556
+ metrics.errors = Number(metrics.errors) + 1;
2557
+ const errors = metrics.errors_by_provider as Record<Provider, number>;
2558
+ errors[String(task.provider) as Provider] += 1;
2559
+ return;
2560
+ }
2561
+ }
2562
+ }
2563
+
2564
+ export function collectUsage(request: CollectRequest): Record<string, unknown> {
2565
+ const cacheFile = path.resolve(request.cacheFile);
2566
+ const legacyCacheFile = request.legacyCacheFile
2567
+ ? path.resolve(request.legacyCacheFile)
2568
+ : path.join(path.dirname(cacheFile), 'cc-statistics-usage-v1.sqlite3');
2569
+ if (legacyCacheFile !== cacheFile && fs.existsSync(legacyCacheFile)) {
2570
+ removeDatabaseFiles(legacyCacheFile);
2571
+ }
2572
+ const nowMs = request.nowMs || Date.now();
2573
+ const retentionDays = Math.max(1, request.retentionDays || DEFAULT_RETENTION_DAYS);
2574
+ const recentRawMs = Math.max(60_000, request.recentRawMs || DEFAULT_RECENT_RAW_MS);
2575
+ const scanBudgetMs = Math.max(100, request.scanBudgetMs || DEFAULT_SCAN_BUDGET_MS);
2576
+ const { database, rebuilt } = openDatabase(cacheFile);
2577
+ const deadline = Date.now() + scanBudgetMs;
2578
+ const metrics: Metrics = {
2579
+ discovered_files: 0,
2580
+ scanned_directories: 0,
2581
+ audited_directories: 0,
2582
+ enumerated_entries: 0,
2583
+ audited_files: 0,
2584
+ scanned_files: 0,
2585
+ partial_files: 0,
2586
+ reused_files: 0,
2587
+ appended_files: 0,
2588
+ rebuilt_files: 0,
2589
+ parsed_events: 0,
2590
+ bytes_read: 0,
2591
+ pruned_events: 0,
2592
+ removed_files: 0,
2593
+ moved_files: 0,
2594
+ errors: 0,
2595
+ errors_by_provider: { codex: 0, claude: 0 },
2596
+ cache_rebuilt: rebuilt,
2597
+ codex_cache_rebuilt: false,
2598
+ claude_cache_rebuilt: false,
2599
+ };
2600
+ let completed = false;
2601
+
2602
+ try {
2603
+ const retentionCutoff = nowMs - retentionDays * 24 * 60 * 60 * 1000;
2604
+ const recentCutoff = Math.max(retentionCutoff, nowMs - recentRawMs);
2605
+ const recentBoundary = localHourMs(recentCutoff);
2606
+ database.exec('BEGIN IMMEDIATE');
2607
+ try {
2608
+ ensureDiscoveryRoots(database, request.roots || {}, nowMs, cacheFile);
2609
+ auditDirectories(database, nowMs, deadline, metrics, cacheFile);
2610
+ database.exec('COMMIT');
2611
+ } catch (error) {
2612
+ database.exec('ROLLBACK');
2613
+ throw error;
2614
+ }
2615
+ scanPendingDirectories(database, nowMs, deadline, metrics, cacheFile);
2616
+ if (Date.now() < deadline) auditSources(database, nowMs, deadline, metrics);
2617
+ if (Date.now() < deadline) {
2618
+ processSourceQueue(database, recentBoundary, deadline, metrics);
2619
+ }
2620
+
2621
+ database.exec('BEGIN IMMEDIATE');
2622
+ try {
2623
+ let pruned = 0;
2624
+ pruned += Number(database.prepare(
2625
+ 'DELETE FROM usage_hourly WHERE bucket_start_ms < ?',
2626
+ ).run(localHourMs(retentionCutoff)).changes);
2627
+ database.prepare(
2628
+ 'DELETE FROM usage_provider_hourly WHERE bucket_start_ms < ?',
2629
+ ).run(localHourMs(retentionCutoff));
2630
+ pruned += Number(database.prepare(
2631
+ 'DELETE FROM recent_events WHERE timestamp_ms < ?',
2632
+ ).run(recentBoundary).changes);
2633
+ pruned += Number(database.prepare(
2634
+ 'DELETE FROM claude_messages WHERE timestamp_ms < ?',
2635
+ ).run(retentionCutoff).changes);
2636
+ metrics.pruned_events = pruned;
2637
+ database.exec('COMMIT');
2638
+ } catch (error) {
2639
+ database.exec('ROLLBACK');
2640
+ throw error;
2641
+ }
2642
+
2643
+ const stats = database.prepare(
2644
+ 'SELECT * FROM cache_stats WHERE singleton = 1',
2645
+ ).get() as Record<string, number>;
2646
+ const pendingDirectories = Number(stats.pending_directories);
2647
+ const pendingFiles = Number(stats.queued_sources);
2648
+ const incompleteFiles = Number(stats.codex_incomplete)
2649
+ + Number(stats.claude_incomplete);
2650
+ const scanComplete = pendingDirectories === 0
2651
+ && pendingFiles === 0
2652
+ && incompleteFiles === 0
2653
+ && Number(metrics.errors) === 0;
2654
+ if (scanComplete) setMetadataValue(database, 'discovery_ready', '1');
2655
+ const discoveryReady = metadataValue(database, 'discovery_ready') === '1';
2656
+ const discovered = {
2657
+ codex: Number(stats.codex_files),
2658
+ claude: Number(stats.claude_files),
2659
+ };
2660
+ metrics.discovered_files = discovered.codex + discovered.claude;
2661
+ metrics.scan_complete = scanComplete;
2662
+ metrics.pending_files = pendingFiles;
2663
+ metrics.pending_directories = pendingDirectories;
2664
+ metrics.discovery_ready = discoveryReady;
2665
+ metrics.hourly_rows = Number(stats.hourly_rows);
2666
+ metrics.recent_rows = Number(stats.recent_rows);
2667
+ metrics.committed_bytes = Number(stats.committed_bytes);
2668
+ metrics.dedupe_rows = Number(stats.dedupe_rows);
2669
+ metrics.codex_fingerprint_rows = 0;
2670
+ metrics.codex_session_rows = discovered.codex;
2671
+ metrics.codex_session_cache_rows = 0;
2672
+ const providerComplete = {
2673
+ codex: discoveryReady && Number(stats.codex_incomplete) === 0,
2674
+ claude: discoveryReady && Number(stats.claude_incomplete) === 0,
2675
+ };
2676
+
2677
+ const providers: Record<Provider, Record<string, unknown>> = {
2678
+ codex: {},
2679
+ claude: {},
2680
+ };
2681
+ for (const provider of ['codex', 'claude'] as const) {
2682
+ const providerErrors = (metrics.errors_by_provider as Record<Provider, number>)[provider];
2683
+ const available = discovered[provider] > 0 && providerErrors === 0;
2684
+ providers[provider] = {
2685
+ source: provider === 'codex'
2686
+ ? 'Farming local history · CodexBar 0.45.2-derived'
2687
+ : 'Farming local history · cc-statistics 1.1.0-derived',
2688
+ events: !providerComplete[provider]
2689
+ ? []
2690
+ : providerEvents(database, provider, retentionCutoff, recentBoundary),
2691
+ quotaCandidates: provider === 'codex' ? latestQuotas(database) : [],
2692
+ fileCount: discovered[provider],
2693
+ available,
2694
+ partial: !providerComplete[provider],
2695
+ ...(!available
2696
+ ? {
2697
+ reason: providerErrors
2698
+ ? `${providerErrors} session file(s) could not be parsed.`
2699
+ : 'No local session files were found.',
2700
+ }
2701
+ : {}),
2702
+ };
2703
+ }
2704
+ const result = {
2705
+ schemaVersion: SCHEMA_VERSION,
2706
+ source: SOURCE_VERSION,
2707
+ sampledAt: nowMs,
2708
+ retentionDays,
2709
+ providers,
2710
+ cache: { ...metrics, path: cacheFile },
2711
+ };
2712
+ completed = true;
2713
+ return result;
2714
+ } finally {
2715
+ if (!completed) closeCacheDirectoryCursors(cacheFile);
2716
+ database.close();
2717
+ }
2718
+ }