@hasna/economy 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +12 -9
  3. package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
  4. package/dist/chunks/billing-bkpxy4kv.js +13 -0
  5. package/dist/chunks/config-j9mv3ehk.js +15 -0
  6. package/dist/chunks/index-2hwzmfgv.js +3407 -0
  7. package/dist/chunks/index-49f12css.js +124 -0
  8. package/dist/chunks/index-d480p2wh.js +1322 -0
  9. package/dist/chunks/index-ez9thsr7.js +3440 -0
  10. package/dist/chunks/index-hesxq90g.js +62 -0
  11. package/dist/chunks/index-k540hfww.js +258 -0
  12. package/dist/chunks/index-p35cyhc4.js +4 -0
  13. package/dist/chunks/index-r1jv3kpy.js +451 -0
  14. package/dist/chunks/index-w3bvv0ns.js +26 -0
  15. package/dist/chunks/index-xsah93s2.js +2085 -0
  16. package/dist/chunks/index-zpvea08j.js +3372 -0
  17. package/dist/chunks/menubar-6xtkt17h.js +296 -0
  18. package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
  19. package/dist/chunks/pricing-f80p81gr.js +29 -0
  20. package/dist/chunks/pricing-vf2pycmh.js +30 -0
  21. package/dist/chunks/pricing-y21mcjsa.js +30 -0
  22. package/dist/chunks/serve-2knem98x.js +4655 -0
  23. package/dist/chunks/sqlite-store-501f32ts.js +258 -0
  24. package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
  25. package/dist/chunks/sqlite-store-st99garx.js +258 -0
  26. package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
  27. package/dist/chunks/tui-khmnkat2.js +22 -0
  28. package/dist/chunks/watch-4y064yys.js +196 -0
  29. package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
  30. package/dist/cli/commands/brief.d.ts.map +1 -1
  31. package/dist/cli/commands/extras.d.ts.map +1 -1
  32. package/dist/cli/commands/journal.d.ts +3 -0
  33. package/dist/cli/commands/journal.d.ts.map +1 -0
  34. package/dist/cli/commands/menubar.d.ts +97 -4
  35. package/dist/cli/commands/menubar.d.ts.map +1 -1
  36. package/dist/cli/commands/tui.d.ts +1 -1
  37. package/dist/cli/commands/tui.d.ts.map +1 -1
  38. package/dist/cli/commands/watch.d.ts.map +1 -1
  39. package/dist/cli/index.js +1358 -10114
  40. package/dist/db/cloud.d.ts +7 -9
  41. package/dist/db/cloud.d.ts.map +1 -1
  42. package/dist/db/database.d.ts +14 -12
  43. package/dist/db/database.d.ts.map +1 -1
  44. package/dist/db/ingest-concurrency-worker.d.ts +2 -0
  45. package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
  46. package/dist/db/ingest-errors.d.ts +9 -0
  47. package/dist/db/ingest-errors.d.ts.map +1 -0
  48. package/dist/db/ingest-schema.d.ts +3 -0
  49. package/dist/db/ingest-schema.d.ts.map +1 -0
  50. package/dist/db/ingest-specs.d.ts +8 -0
  51. package/dist/db/ingest-specs.d.ts.map +1 -0
  52. package/dist/db/ingest-test-contract.d.ts +3 -0
  53. package/dist/db/ingest-test-contract.d.ts.map +1 -0
  54. package/dist/db/ingest-validation.d.ts +12 -0
  55. package/dist/db/ingest-validation.d.ts.map +1 -0
  56. package/dist/db/ingest.d.ts +27 -0
  57. package/dist/db/ingest.d.ts.map +1 -0
  58. package/dist/db/legacy-schema.d.ts +7 -0
  59. package/dist/db/legacy-schema.d.ts.map +1 -0
  60. package/dist/db/observation-contract.d.ts +3 -0
  61. package/dist/db/observation-contract.d.ts.map +1 -0
  62. package/dist/db/observation-provenance.d.ts +23 -0
  63. package/dist/db/observation-provenance.d.ts.map +1 -0
  64. package/dist/db/observation-schema.d.ts +24 -0
  65. package/dist/db/observation-schema.d.ts.map +1 -0
  66. package/dist/db/pg-connection.d.ts +10 -0
  67. package/dist/db/pg-connection.d.ts.map +1 -0
  68. package/dist/db/pg-legacy-schema.d.ts +12 -0
  69. package/dist/db/pg-legacy-schema.d.ts.map +1 -0
  70. package/dist/db/pg-migrate.d.ts +4 -9
  71. package/dist/db/pg-migrate.d.ts.map +1 -1
  72. package/dist/db/pg-migrations.d.ts.map +1 -1
  73. package/dist/db/pg-protocol.d.ts +28 -0
  74. package/dist/db/pg-protocol.d.ts.map +1 -0
  75. package/dist/db/sqlite-store.d.ts +4 -0
  76. package/dist/db/sqlite-store.d.ts.map +1 -0
  77. package/dist/db/sync-pg.d.ts +17 -21
  78. package/dist/db/sync-pg.d.ts.map +1 -1
  79. package/dist/db/third-party-sqlite.d.ts +10 -0
  80. package/dist/db/third-party-sqlite.d.ts.map +1 -0
  81. package/dist/index.d.ts +3 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +1171 -2465
  84. package/dist/ingest/claude-quota.d.ts.map +1 -1
  85. package/dist/ingest/claude.d.ts.map +1 -1
  86. package/dist/ingest/codex-quota.d.ts.map +1 -1
  87. package/dist/ingest/codex.d.ts +25 -0
  88. package/dist/ingest/codex.d.ts.map +1 -1
  89. package/dist/ingest/collection-outcome.d.ts +17 -0
  90. package/dist/ingest/collection-outcome.d.ts.map +1 -0
  91. package/dist/ingest/cursor.d.ts.map +1 -1
  92. package/dist/ingest/gemini.d.ts.map +1 -1
  93. package/dist/ingest/hermes.d.ts +1 -1
  94. package/dist/ingest/hermes.d.ts.map +1 -1
  95. package/dist/ingest/loops.d.ts.map +1 -1
  96. package/dist/ingest/opencode.d.ts +1 -1
  97. package/dist/ingest/opencode.d.ts.map +1 -1
  98. package/dist/ingest/pi.d.ts +1 -1
  99. package/dist/ingest/pi.d.ts.map +1 -1
  100. package/dist/ingest/quota-observation.d.ts +16 -0
  101. package/dist/ingest/quota-observation.d.ts.map +1 -0
  102. package/dist/ingest/source-time.d.ts +4 -0
  103. package/dist/ingest/source-time.d.ts.map +1 -0
  104. package/dist/lib/accounts-store.d.ts +39 -13
  105. package/dist/lib/accounts-store.d.ts.map +1 -1
  106. package/dist/lib/analytics.d.ts.map +1 -1
  107. package/dist/lib/api-display-url.d.ts +1 -1
  108. package/dist/lib/autosync-gate.d.ts +12 -5
  109. package/dist/lib/autosync-gate.d.ts.map +1 -1
  110. package/dist/lib/billing-diff.d.ts +18 -40
  111. package/dist/lib/billing-diff.d.ts.map +1 -1
  112. package/dist/lib/brief.d.ts +2 -0
  113. package/dist/lib/brief.d.ts.map +1 -1
  114. package/dist/lib/budget-decimal.d.ts +14 -0
  115. package/dist/lib/budget-decimal.d.ts.map +1 -0
  116. package/dist/lib/cloud-ingest.d.ts +15 -0
  117. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  118. package/dist/lib/cloud-storage.d.ts.map +1 -1
  119. package/dist/lib/ingest-batches.d.ts +21 -0
  120. package/dist/lib/ingest-batches.d.ts.map +1 -0
  121. package/dist/lib/ingest-journal-client.d.ts +37 -0
  122. package/dist/lib/ingest-journal-client.d.ts.map +1 -0
  123. package/dist/lib/ingest-journal-lock.d.ts +5 -0
  124. package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
  125. package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
  126. package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
  127. package/dist/lib/ingest-journal-operation.d.ts +55 -0
  128. package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
  129. package/dist/lib/ingest-journal.d.ts +73 -0
  130. package/dist/lib/ingest-journal.d.ts.map +1 -0
  131. package/dist/lib/money-display.d.ts +13 -0
  132. package/dist/lib/money-display.d.ts.map +1 -0
  133. package/dist/lib/periods.d.ts +12 -0
  134. package/dist/lib/periods.d.ts.map +1 -1
  135. package/dist/lib/pricing.d.ts +14 -0
  136. package/dist/lib/pricing.d.ts.map +1 -1
  137. package/dist/lib/savings.d.ts +64 -7
  138. package/dist/lib/savings.d.ts.map +1 -1
  139. package/dist/lib/store/index.d.ts +25 -21
  140. package/dist/lib/store/index.d.ts.map +1 -1
  141. package/dist/lib/summary-contract.d.ts +32 -0
  142. package/dist/lib/summary-contract.d.ts.map +1 -0
  143. package/dist/lib/summary-test-contract.d.ts +3 -0
  144. package/dist/lib/summary-test-contract.d.ts.map +1 -0
  145. package/dist/lib/sync-all.d.ts +4 -0
  146. package/dist/lib/sync-all.d.ts.map +1 -1
  147. package/dist/lib/sync-maintenance.d.ts.map +1 -1
  148. package/dist/lib/sync-report.d.ts +4 -0
  149. package/dist/lib/sync-report.d.ts.map +1 -0
  150. package/dist/lib/test-keychain-fixture.d.ts +9 -0
  151. package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
  152. package/dist/mcp/agent-registry-store.d.ts +30 -0
  153. package/dist/mcp/agent-registry-store.d.ts.map +1 -0
  154. package/dist/mcp/agent-registry.d.ts +9 -16
  155. package/dist/mcp/agent-registry.d.ts.map +1 -1
  156. package/dist/mcp/index.js +2291 -3458
  157. package/dist/mcp/ingest-journal-tool.d.ts +2 -0
  158. package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
  159. package/dist/mcp/server.d.ts.map +1 -1
  160. package/dist/openapi.d.ts.map +1 -1
  161. package/dist/otel/index.js +1960 -289
  162. package/dist/server/human-auth.d.ts +20 -0
  163. package/dist/server/human-auth.d.ts.map +1 -0
  164. package/dist/server/index.js +10639 -6720
  165. package/dist/server/legacy-inventory.d.ts +52 -0
  166. package/dist/server/legacy-inventory.d.ts.map +1 -0
  167. package/dist/server/legacy-operations.d.ts +64 -0
  168. package/dist/server/legacy-operations.d.ts.map +1 -0
  169. package/dist/server/legacy-plan.d.ts +55 -0
  170. package/dist/server/legacy-plan.d.ts.map +1 -0
  171. package/dist/server/legacy-preservation.d.ts +58 -0
  172. package/dist/server/legacy-preservation.d.ts.map +1 -0
  173. package/dist/server/pg-sync-worker.js +195 -21
  174. package/dist/server/serve.d.ts +20 -16
  175. package/dist/server/serve.d.ts.map +1 -1
  176. package/dist/server/service-admission.d.ts +10 -0
  177. package/dist/server/service-admission.d.ts.map +1 -0
  178. package/dist/server/service-dispatcher.d.ts +45 -0
  179. package/dist/server/service-dispatcher.d.ts.map +1 -0
  180. package/dist/server/service-operations.d.ts +5 -0
  181. package/dist/server/service-operations.d.ts.map +1 -0
  182. package/dist/server/service-protocol.d.ts +50 -0
  183. package/dist/server/service-protocol.d.ts.map +1 -0
  184. package/dist/server/service-worker.d.ts +2 -0
  185. package/dist/server/service-worker.d.ts.map +1 -0
  186. package/dist/server/service-worker.js +8038 -0
  187. package/dist/types/index.d.ts +66 -0
  188. package/dist/types/index.d.ts.map +1 -1
  189. package/docs/README.md +1 -1
  190. package/docs/cli.md +1 -1
  191. package/docs/configuration.md +5 -5
  192. package/docs/cost-reporting.md +49 -0
  193. package/docs/historical-data-reconciliation.md +203 -0
  194. package/docs/ingest-journal.md +108 -0
  195. package/docs/ingestion.md +60 -4
  196. package/docs/mcp.md +2 -2
  197. package/docs/native-budget-save.md +84 -0
  198. package/docs/native-human-auth.md +102 -0
  199. package/docs/postgresql-legacy-schema-repair.md +30 -0
  200. package/docs/rest-api.md +2 -2
  201. package/package.json +25 -13
  202. package/dist/lib/test-hermetic-accounts.d.ts +0 -7
  203. package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
  204. package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
  205. package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
@@ -15,6 +15,7 @@ var __export = (target, all) => {
15
15
  });
16
16
  };
17
17
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
18
+ var __require = import.meta.require;
18
19
 
19
20
  // src/db/sqlite-adapter.ts
20
21
  import { Database } from "bun:sqlite";
@@ -72,6 +73,851 @@ class SqliteAdapter {
72
73
  }
73
74
  var init_sqlite_adapter = () => {};
74
75
 
76
+ // src/db/observation-schema.ts
77
+ var OBSERVATION_COLUMNS, SCAN_RECEIPTS_SQL = `CREATE TABLE IF NOT EXISTS scan_receipts (
78
+ id TEXT PRIMARY KEY, machine_id TEXT NOT NULL, source TEXT NOT NULL,
79
+ attempted_at TEXT NOT NULL, completed_at TEXT NOT NULL, status TEXT NOT NULL,
80
+ latest_event_at TEXT, parser_version TEXT NOT NULL, error_code TEXT,
81
+ updated_at TEXT NOT NULL
82
+ );
83
+ CREATE INDEX IF NOT EXISTS idx_scan_receipts_machine_source
84
+ ON scan_receipts(machine_id, source, completed_at);`, OBSERVATION_PG_MIGRATIONS;
85
+ var init_observation_schema = __esm(() => {
86
+ OBSERVATION_COLUMNS = {
87
+ usage_snapshots: {
88
+ provider: "TEXT NOT NULL DEFAULT ''",
89
+ account_key: "TEXT NOT NULL DEFAULT ''",
90
+ observed_at: "TEXT",
91
+ source_timestamp: "TEXT",
92
+ window_start: "TEXT",
93
+ window_end: "TEXT",
94
+ reset_at: "TEXT",
95
+ window_seconds: "INTEGER",
96
+ value_kind: "TEXT NOT NULL DEFAULT 'unknown'"
97
+ },
98
+ subscriptions: {
99
+ account_key: "TEXT NOT NULL DEFAULT ''",
100
+ observed_station: "TEXT",
101
+ observed_at: "TEXT",
102
+ fee_basis: "TEXT NOT NULL DEFAULT 'unknown'",
103
+ included_basis: "TEXT NOT NULL DEFAULT 'unknown'"
104
+ }
105
+ };
106
+ OBSERVATION_PG_MIGRATIONS = [
107
+ ...Object.entries(OBSERVATION_COLUMNS).flatMap(([table, columns]) => Object.entries(columns).map(([column, type]) => `ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column} ${type}`)),
108
+ SCAN_RECEIPTS_SQL
109
+ ];
110
+ });
111
+
112
+ // src/db/ingest-errors.ts
113
+ var IngestError;
114
+ var init_ingest_errors = __esm(() => {
115
+ IngestError = class IngestError extends Error {
116
+ status;
117
+ code;
118
+ field;
119
+ expected;
120
+ constructor(status, code, field, expected) {
121
+ super(`${field}: ${expected}`);
122
+ this.status = status;
123
+ this.code = code;
124
+ this.field = field;
125
+ this.expected = expected;
126
+ this.name = "IngestError";
127
+ }
128
+ };
129
+ });
130
+
131
+ // src/db/observation-provenance.ts
132
+ import { createHash } from "crypto";
133
+ function observationId(input) {
134
+ return digest([input.provider, input.account_key, input.machine_id, input.metric, new Date(input.observed_at).toISOString()]);
135
+ }
136
+ function subscriptionId(input) {
137
+ return digest([input.provider, input.account_key]);
138
+ }
139
+ function scanReceiptId(input) {
140
+ return digest([input.machine_id, input.source, new Date(input.attempted_at).toISOString()]);
141
+ }
142
+ function required(row, keys, path) {
143
+ for (const key of keys)
144
+ if (typeof row[key] !== "string" || !row[key].trim() || row[key].length > 256) {
145
+ invalid(`${path}.${key}`, "expected nonempty text of at most 256 characters");
146
+ }
147
+ }
148
+ function validateObservationProvenance(table, row, path) {
149
+ if (table === "usage_snapshots") {
150
+ if (!["gauge", "cumulative", "delta", "unknown"].includes(String(row["value_kind"] === undefined ? "unknown" : row["value_kind"])))
151
+ invalid(`${path}.value_kind`, "expected gauge, cumulative, delta or unknown");
152
+ const newProvenance = ["provider", "account_key", "observed_at", "source_timestamp", "window_start", "window_end", "reset_at", "window_seconds"].some((k) => row[k] != null && row[k] !== "") || row["value_kind"] != null && row["value_kind"] !== "unknown";
153
+ if (newProvenance) {
154
+ required(row, ["provider", "account_key", "machine_id", "observed_at", "metric"], path);
155
+ if (row["id"] !== observationId(row))
156
+ invalid(`${path}.id`, "expected SHA-256 identity of provider, account_key, machine_id, metric and observed_at");
157
+ if (row["date"] !== String(row["observed_at"]).slice(0, 10))
158
+ invalid(`${path}.date`, "expected the UTC observation date");
159
+ if (row["window_seconds"] != null && (!Number.isSafeInteger(row["window_seconds"]) || Number(row["window_seconds"]) <= 0))
160
+ invalid(`${path}.window_seconds`, "expected positive integer seconds or null");
161
+ if (row["window_start"] && row["window_end"] && Date.parse(String(row["window_start"])) >= Date.parse(String(row["window_end"])))
162
+ invalid(`${path}.window_end`, "expected a time after window_start");
163
+ }
164
+ }
165
+ if (table === "subscriptions") {
166
+ for (const key of ["fee_basis", "included_basis"])
167
+ if (!["unknown", "observed", "configured"].includes(String(row[key] === undefined ? "unknown" : row[key])))
168
+ invalid(`${path}.${key}`, "expected unknown, observed or configured");
169
+ const identified = row["account_key"] != null && row["account_key"] !== "" || row["observed_at"] != null;
170
+ if (identified || ["fee_basis", "included_basis"].some((k) => row[k] != null && row[k] !== "unknown")) {
171
+ required(row, ["provider", "account_key", "observed_at"], path);
172
+ if (row["id"] !== subscriptionId(row))
173
+ invalid(`${path}.id`, "expected SHA-256 identity of provider and account_key");
174
+ if (row["updated_at"] !== row["observed_at"])
175
+ invalid(`${path}.updated_at`, "expected the observation timestamp");
176
+ }
177
+ }
178
+ if (table === "scan_receipts") {
179
+ required(row, ["machine_id", "source", "attempted_at", "completed_at", "parser_version"], path);
180
+ if (!["success", "unchanged", "missing", "unsupported", "failed"].includes(String(row["status"])))
181
+ invalid(`${path}.status`, "expected success, unchanged, missing, unsupported or failed");
182
+ if (row["id"] !== scanReceiptId(row))
183
+ invalid(`${path}.id`, "expected SHA-256 identity of machine_id, source and attempted_at");
184
+ if (Date.parse(String(row["completed_at"])) < Date.parse(String(row["attempted_at"])))
185
+ invalid(`${path}.completed_at`, "expected a time at or after attempted_at");
186
+ if (row["updated_at"] !== row["completed_at"])
187
+ invalid(`${path}.updated_at`, "expected completed_at");
188
+ if (row["error_code"] != null && (typeof row["error_code"] !== "string" || !SCAN_ERROR_CODES.includes(row["error_code"])))
189
+ invalid(`${path}.error_code`, "expected a supported authored scan error code or null");
190
+ if (["success", "unchanged"].includes(String(row["status"])) && row["error_code"] != null)
191
+ invalid(`${path}.error_code`, "expected null for a successful scan");
192
+ }
193
+ }
194
+ var SCAN_ERROR_CODES, digest = (parts) => createHash("sha256").update(JSON.stringify(parts)).digest("hex"), invalid = (path, expected) => {
195
+ throw new IngestError(400, "INVALID_INGEST", path, expected);
196
+ };
197
+ var init_observation_provenance = __esm(() => {
198
+ init_ingest_errors();
199
+ SCAN_ERROR_CODES = [
200
+ "SOURCE_MISSING",
201
+ "SOURCE_UNREADABLE",
202
+ "SOURCE_INVALID",
203
+ "LEGACY_AGGREGATE_REQUIRES_MIGRATION",
204
+ "USAGE_EVENTS_UNAVAILABLE",
205
+ "USAGE_EVENTS_INCOMPLETE",
206
+ "PROVIDER_CREDENTIAL_UNAVAILABLE",
207
+ "PROVIDER_CREDENTIAL_UNREADABLE",
208
+ "PROVIDER_AUTH_REFUSED",
209
+ "PROVIDER_REFUSED",
210
+ "PROVIDER_UNAVAILABLE",
211
+ "ACCOUNT_IDENTITY_UNAVAILABLE",
212
+ "SUBSCRIPTION_QUOTA_NOT_APPLICABLE",
213
+ "TEAM_USAGE_ATTRIBUTION_UNAVAILABLE",
214
+ "CROSS_APP_LOCAL_READ_REFUSED",
215
+ "COLLECTION_FAILED"
216
+ ];
217
+ });
218
+
219
+ // src/db/ingest-specs.ts
220
+ function ingestSpecs() {
221
+ return [
222
+ {
223
+ table: "requests",
224
+ pk: ["id"],
225
+ columns: [
226
+ "id",
227
+ "agent",
228
+ "session_id",
229
+ "model",
230
+ "input_tokens",
231
+ "output_tokens",
232
+ "cache_read_tokens",
233
+ "cache_create_tokens",
234
+ "cache_create_5m_tokens",
235
+ "cache_create_1h_tokens",
236
+ "cost_usd",
237
+ "cost_basis",
238
+ "duration_ms",
239
+ "timestamp",
240
+ "source_request_id",
241
+ "machine_id",
242
+ "cost_center_id",
243
+ "attribution_tag",
244
+ "account_key",
245
+ "account_tool",
246
+ "account_name",
247
+ "account_email",
248
+ "account_source",
249
+ "updated_at",
250
+ "synced_at"
251
+ ],
252
+ map: (r, now) => [
253
+ strOr(r["id"]),
254
+ strOr(r["agent"]),
255
+ strOr(r["session_id"]),
256
+ strOr(r["model"]),
257
+ numOr(r["input_tokens"]),
258
+ numOr(r["output_tokens"]),
259
+ numOr(r["cache_read_tokens"]),
260
+ numOr(r["cache_create_tokens"]),
261
+ numOr(r["cache_create_5m_tokens"] ?? r["cache_create_tokens"]),
262
+ numOr(r["cache_create_1h_tokens"]),
263
+ numOr(r["cost_usd"]),
264
+ strOr(r["cost_basis"], "estimated"),
265
+ numOr(r["duration_ms"]),
266
+ strOr(r["timestamp"]),
267
+ r["source_request_id"] == null ? null : strOr(r["source_request_id"]),
268
+ strOr(r["machine_id"]),
269
+ r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
270
+ strOr(r["attribution_tag"]),
271
+ strOr(r["account_key"]),
272
+ strOr(r["account_tool"]),
273
+ strOr(r["account_name"]),
274
+ strOr(r["account_email"]),
275
+ strOr(r["account_source"]),
276
+ strOr(r["updated_at"], now),
277
+ strOr(r["synced_at"])
278
+ ]
279
+ },
280
+ {
281
+ table: "sessions",
282
+ pk: ["id"],
283
+ columns: [
284
+ "id",
285
+ "agent",
286
+ "project_path",
287
+ "project_name",
288
+ "started_at",
289
+ "ended_at",
290
+ "total_cost_usd",
291
+ "total_tokens",
292
+ "request_count",
293
+ "machine_id",
294
+ "cost_center_id",
295
+ "attribution_tag",
296
+ "account_key",
297
+ "account_tool",
298
+ "account_name",
299
+ "account_email",
300
+ "account_source",
301
+ "updated_at",
302
+ "synced_at"
303
+ ],
304
+ map: (r, now) => [
305
+ strOr(r["id"]),
306
+ strOr(r["agent"]),
307
+ strOr(r["project_path"]),
308
+ strOr(r["project_name"]),
309
+ strOr(r["started_at"]),
310
+ r["ended_at"] == null ? null : strOr(r["ended_at"]),
311
+ numOr(r["total_cost_usd"]),
312
+ numOr(r["total_tokens"]),
313
+ numOr(r["request_count"]),
314
+ strOr(r["machine_id"]),
315
+ r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
316
+ strOr(r["attribution_tag"]),
317
+ strOr(r["account_key"]),
318
+ strOr(r["account_tool"]),
319
+ strOr(r["account_name"]),
320
+ strOr(r["account_email"]),
321
+ strOr(r["account_source"]),
322
+ strOr(r["updated_at"], now),
323
+ strOr(r["synced_at"])
324
+ ]
325
+ },
326
+ {
327
+ table: "projects",
328
+ pk: ["id"],
329
+ columns: ["id", "path", "name", "description", "tags", "created_at"],
330
+ map: (r) => [
331
+ strOr(r["id"]),
332
+ strOr(r["path"]),
333
+ strOr(r["name"]),
334
+ r["description"] == null ? null : strOr(r["description"]),
335
+ typeof r["tags"] === "string" ? r["tags"] : JSON.stringify(r["tags"] ?? []),
336
+ strOr(r["created_at"])
337
+ ]
338
+ },
339
+ {
340
+ table: "budgets",
341
+ pk: ["id"],
342
+ columns: ["id", "project_path", "agent", "period", "limit_usd", "alert_at_percent", "cost_center_id", "created_at", "updated_at", "metric"],
343
+ map: (r) => [
344
+ strOr(r["id"]),
345
+ r["project_path"] == null ? null : strOr(r["project_path"]),
346
+ r["agent"] == null ? null : strOr(r["agent"]),
347
+ strOr(r["period"]),
348
+ numOr(r["limit_usd"]),
349
+ numOr(r["alert_at_percent"], 80),
350
+ r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
351
+ strOr(r["created_at"]),
352
+ strOr(r["updated_at"]),
353
+ strOr(r["metric"], "legacy_request_cost_usd")
354
+ ]
355
+ },
356
+ {
357
+ table: "goals",
358
+ pk: ["id"],
359
+ columns: ["id", "period", "project_path", "agent", "limit_usd", "created_at", "updated_at"],
360
+ map: (r) => [
361
+ strOr(r["id"]),
362
+ strOr(r["period"]),
363
+ r["project_path"] == null ? null : strOr(r["project_path"]),
364
+ r["agent"] == null ? null : strOr(r["agent"]),
365
+ numOr(r["limit_usd"]),
366
+ strOr(r["created_at"]),
367
+ strOr(r["updated_at"])
368
+ ]
369
+ },
370
+ {
371
+ table: "billing_daily",
372
+ pk: ["date", "provider", "description"],
373
+ columns: ["date", "provider", "description", "cost_usd", "updated_at"],
374
+ map: (r) => [strOr(r["date"]), strOr(r["provider"]), strOr(r["description"]), numOr(r["cost_usd"]), strOr(r["updated_at"])]
375
+ },
376
+ {
377
+ table: "model_pricing",
378
+ pk: ["model"],
379
+ columns: [
380
+ "model",
381
+ "input_per_1m",
382
+ "output_per_1m",
383
+ "cache_read_per_1m",
384
+ "cache_write_per_1m",
385
+ "cache_write_1h_per_1m",
386
+ "cache_storage_per_1m_hour",
387
+ "updated_at"
388
+ ],
389
+ map: (r) => [
390
+ strOr(r["model"]),
391
+ numOr(r["input_per_1m"]),
392
+ numOr(r["output_per_1m"]),
393
+ numOr(r["cache_read_per_1m"]),
394
+ numOr(r["cache_write_per_1m"]),
395
+ numOr(r["cache_write_1h_per_1m"]),
396
+ numOr(r["cache_storage_per_1m_hour"]),
397
+ strOr(r["updated_at"])
398
+ ]
399
+ },
400
+ {
401
+ table: "subscriptions",
402
+ pk: ["id"],
403
+ columns: [
404
+ "id",
405
+ "agent",
406
+ "provider",
407
+ "plan",
408
+ "monthly_fee_usd",
409
+ "included_usage_usd",
410
+ "billing_cycle_start",
411
+ "reset_policy",
412
+ "active",
413
+ "created_at",
414
+ "updated_at",
415
+ "account_key",
416
+ "observed_station",
417
+ "observed_at",
418
+ "fee_basis",
419
+ "included_basis"
420
+ ],
421
+ map: (r) => [
422
+ strOr(r["id"]),
423
+ r["agent"] == null ? null : strOr(r["agent"]),
424
+ strOr(r["provider"]),
425
+ strOr(r["plan"]),
426
+ numOr(r["monthly_fee_usd"]),
427
+ numOr(r["included_usage_usd"]),
428
+ r["billing_cycle_start"] == null ? null : strOr(r["billing_cycle_start"]),
429
+ strOr(r["reset_policy"], "unknown"),
430
+ numOr(r["active"], 1),
431
+ strOr(r["created_at"]),
432
+ strOr(r["updated_at"]),
433
+ strOr(r["account_key"]),
434
+ r["observed_station"] ?? null,
435
+ r["observed_at"] ?? null,
436
+ strOr(r["fee_basis"], "unknown"),
437
+ strOr(r["included_basis"], "unknown")
438
+ ]
439
+ },
440
+ {
441
+ table: "usage_snapshots",
442
+ pk: ["id"],
443
+ columns: [
444
+ "id",
445
+ "agent",
446
+ "date",
447
+ "metric",
448
+ "value",
449
+ "unit",
450
+ "machine_id",
451
+ "updated_at",
452
+ "provider",
453
+ "account_key",
454
+ "observed_at",
455
+ "source_timestamp",
456
+ "window_start",
457
+ "window_end",
458
+ "reset_at",
459
+ "window_seconds",
460
+ "value_kind"
461
+ ],
462
+ map: (r, now) => [
463
+ strOr(r["id"], `${strOr(r["agent"])}-${strOr(r["date"])}-${strOr(r["metric"])}-${strOr(r["machine_id"])}`),
464
+ strOr(r["agent"]),
465
+ strOr(r["date"]),
466
+ strOr(r["metric"]),
467
+ numOr(r["value"]),
468
+ strOr(r["unit"]),
469
+ strOr(r["machine_id"]),
470
+ strOr(r["updated_at"], now),
471
+ strOr(r["provider"]),
472
+ strOr(r["account_key"]),
473
+ r["observed_at"] ?? null,
474
+ r["source_timestamp"] ?? null,
475
+ r["window_start"] ?? null,
476
+ r["window_end"] ?? null,
477
+ r["reset_at"] ?? null,
478
+ r["window_seconds"] ?? null,
479
+ strOr(r["value_kind"], "unknown")
480
+ ]
481
+ },
482
+ {
483
+ table: "scan_receipts",
484
+ pk: ["id"],
485
+ columns: ["id", "machine_id", "source", "attempted_at", "completed_at", "status", "latest_event_at", "parser_version", "error_code", "updated_at"],
486
+ map: (r) => [strOr(r["id"]), strOr(r["machine_id"]), strOr(r["source"]), strOr(r["attempted_at"]), strOr(r["completed_at"]), strOr(r["status"]), r["latest_event_at"] ?? null, strOr(r["parser_version"]), r["error_code"] ?? null, strOr(r["updated_at"], strOr(r["completed_at"]))]
487
+ }
488
+ ];
489
+ }
490
+ var numOr = (v, d = 0) => {
491
+ if (v == null)
492
+ return d;
493
+ const num = Number(v);
494
+ return Number.isFinite(num) ? num : d;
495
+ }, strOr = (v, d = "") => v == null ? d : String(v);
496
+
497
+ // src/lib/agents.ts
498
+ function isAgent(value) {
499
+ return AGENTS.includes(value);
500
+ }
501
+ var AGENTS;
502
+ var init_agents = __esm(() => {
503
+ AGENTS = [
504
+ "claude",
505
+ "takumi",
506
+ "codex",
507
+ "gemini",
508
+ "opencode",
509
+ "cursor",
510
+ "pi",
511
+ "hermes"
512
+ ];
513
+ });
514
+
515
+ // src/db/ingest-validation.ts
516
+ import { createHash as createHash2 } from "crypto";
517
+ function stableJson(value) {
518
+ if (Array.isArray(value))
519
+ return `[${value.map(stableJson).join(",")}]`;
520
+ if (value !== null && typeof value === "object") {
521
+ const record = value;
522
+ return `{${Object.keys(record).sort().map((k) => `${JSON.stringify(k)}:${stableJson(record[k])}`).join(",")}}`;
523
+ }
524
+ return JSON.stringify(value);
525
+ }
526
+ function canonicalIngestBodyHash(body) {
527
+ return createHash2("sha256").update(stableJson(body)).digest("hex");
528
+ }
529
+ function invalid2(field, expected) {
530
+ throw new IngestError(400, "INVALID_INGEST", field, expected);
531
+ }
532
+ function plain(value) {
533
+ return value !== null && typeof value === "object" && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
534
+ }
535
+ function date(value) {
536
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value;
537
+ }
538
+ function instant(value) {
539
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value) && date(value.slice(0, 10)) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 19) === value.slice(0, 19);
540
+ }
541
+ function validateIngest(body) {
542
+ if (!plain(body))
543
+ invalid2("body", "expected an object of supported table arrays");
544
+ const specs = ingestSpecs();
545
+ const allowed = new Set(["schema_version", "idempotency_key", "subscription_mode", "budget_mode", ...specs.map((s) => s.table)]);
546
+ for (const key of Object.keys(body))
547
+ if (!allowed.has(key))
548
+ invalid2("body", "unknown field; expected a supported table, schema_version, subscription_mode, budget_mode or idempotency_key");
549
+ if (body["subscription_mode"] !== undefined && (typeof body["subscription_mode"] !== "string" || !["quota_observation", "configuration"].includes(body["subscription_mode"]) || !Array.isArray(body["subscriptions"]))) {
550
+ invalid2("subscription_mode", "expected quota_observation or configuration with a subscriptions array");
551
+ }
552
+ if (body["schema_version"] !== undefined && body["schema_version"] !== 1)
553
+ invalid2("schema_version", "expected 1");
554
+ if (body["idempotency_key"] !== undefined && (typeof body["idempotency_key"] !== "string" || !/^[A-Za-z0-9._:-]{1,200}$/.test(body["idempotency_key"])))
555
+ invalid2("idempotency_key", "expected 1\u2013200 letters, digits, dot, underscore, colon or hyphen");
556
+ const createBudget = body["budget_mode"] !== undefined;
557
+ if (createBudget) {
558
+ if (body["budget_mode"] !== "create")
559
+ invalid2("budget_mode", "expected create");
560
+ if (body["schema_version"] !== 1)
561
+ invalid2("schema_version", "budget creation requires 1");
562
+ if (typeof body["idempotency_key"] !== "string")
563
+ invalid2("idempotency_key", "budget creation requires a stable caller-owned key");
564
+ if (Object.keys(body).some((k) => !["schema_version", "idempotency_key", "budget_mode", "budgets"].includes(k)))
565
+ invalid2("body", "budget creation accepts only schema_version, idempotency_key, budget_mode and budgets");
566
+ if (!Array.isArray(body["budgets"]) || body["budgets"].length !== 1)
567
+ invalid2("budgets", "budget creation requires exactly one rule");
568
+ }
569
+ let count = 0;
570
+ let bytes = 0;
571
+ const groups = [];
572
+ for (const spec of specs) {
573
+ const input = body[spec.table];
574
+ if (input === undefined)
575
+ continue;
576
+ if (!Array.isArray(input))
577
+ invalid2(spec.table, "expected an array");
578
+ count += input.length;
579
+ if (count > MAX_INGEST_ROWS)
580
+ throw new IngestError(413, "INGEST_TOO_LARGE", "body", `maximum ${MAX_INGEST_ROWS} rows; split into bounded commands`);
581
+ const rows = [];
582
+ for (let i = 0;i < input.length; i++) {
583
+ const raw = input[i];
584
+ const path = `${spec.table}[${i}]`;
585
+ if (!plain(raw))
586
+ invalid2(path, "expected a row object");
587
+ const row = {};
588
+ for (const key of Object.keys(raw)) {
589
+ if (!spec.columns.includes(key))
590
+ invalid2(/^[a-z_]{1,64}$/.test(key) ? `${path}.${key}` : path, "unknown column for this table");
591
+ const value = raw[key];
592
+ if (["observed_at", "source_timestamp", "window_start", "window_end", "reset_at", "attempted_at", "completed_at", "latest_event_at"].includes(key) && value === "")
593
+ invalid2(`${path}.${key}`, "expected an ISO UTC timestamp or null");
594
+ if (numeric.has(key) && !(key === "window_seconds" && value === null)) {
595
+ if (typeof value !== "number" || !Number.isFinite(value) || integers.has(key) && !Number.isSafeInteger(value) || value < 0 && !(spec.table === "billing_daily" && key === "cost_usd"))
596
+ invalid2(`${path}.${key}`, "expected a finite numeric value in the supported range");
597
+ if (key === "active" && value !== 0 && value !== 1)
598
+ invalid2(`${path}.active`, "expected 0 or 1");
599
+ } else if (key === "tags" && Array.isArray(value)) {
600
+ if (value.length > 100 || value.some((t) => typeof t !== "string" || t.length > 256))
601
+ invalid2(`${path}.tags`, "expected at most 100 string tags of at most 256 characters");
602
+ } else if (value !== null && (typeof value !== "string" || value.length > 16384 || value.includes("\x00"))) {
603
+ invalid2(`${path}.${key}`, "expected text of at most 16384 characters without NUL, or null");
604
+ }
605
+ if (typeof value === "string" && (key === "date" || key === "billing_cycle_start") && value !== "" && !date(value))
606
+ invalid2(`${path}.${key}`, "expected a valid YYYY-MM-DD date");
607
+ if (typeof value === "string" && (key.endsWith("_at") || ["timestamp", "source_timestamp", "window_start", "window_end"].includes(key)) && value !== "") {
608
+ if (!instant(value))
609
+ invalid2(`${path}.${key}`, "expected an ISO UTC timestamp ending in Z");
610
+ row[key] = new Date(value).toISOString();
611
+ } else
612
+ row[key] = value;
613
+ }
614
+ for (const key of required2[spec.table] ?? spec.pk) {
615
+ if (typeof row[key] !== "string" || row[key] === "")
616
+ invalid2(`${path}.${key}`, "expected nonempty text");
617
+ }
618
+ for (const key of spec.pk)
619
+ if (typeof (row[key] ?? "") !== "string" || String(row[key] ?? "").length > 1024)
620
+ invalid2(`${path}.${key}`, "expected a primary key of at most 1024 characters");
621
+ if (spec.table === "budgets" && !["daily", "weekly", "monthly"].includes(String(row["period"])))
622
+ invalid2(`${path}.period`, "expected daily, weekly or monthly");
623
+ if (spec.table === "budgets") {
624
+ if (row["metric"] !== undefined && !["legacy_request_cost_usd", "metered_api_usd"].includes(String(row["metric"])))
625
+ invalid2(`${path}.metric`, "expected legacy_request_cost_usd or metered_api_usd");
626
+ if (createBudget) {
627
+ if (String(row["id"]).trim() === "")
628
+ invalid2(`${path}.id`, "expected a nonempty caller-owned rule identity");
629
+ if (row["metric"] !== "metered_api_usd")
630
+ invalid2(`${path}.metric`, "budget creation requires metered_api_usd");
631
+ if (typeof row["limit_usd"] !== "number" || row["limit_usd"] <= 0)
632
+ invalid2(`${path}.limit_usd`, "expected a positive finite USD amount");
633
+ if (!Number.isInteger(row["alert_at_percent"]) || Number(row["alert_at_percent"]) < 1 || Number(row["alert_at_percent"]) > 100)
634
+ invalid2(`${path}.alert_at_percent`, "expected an integer from 1 to 100");
635
+ for (const field of ["created_at", "updated_at"])
636
+ if (typeof row[field] !== "string" || !instant(row[field]))
637
+ invalid2(`${path}.${field}`, "expected an explicit ISO UTC timestamp ending in Z; preserve it on retry");
638
+ for (const field of ["project_path", "cost_center_id"])
639
+ if (row[field] !== undefined && row[field] !== null && (typeof row[field] !== "string" || row[field].trim() === ""))
640
+ invalid2(`${path}.${field}`, "expected nonempty text or null for all scopes");
641
+ if (row["agent"] !== undefined && row["agent"] !== null && (typeof row["agent"] !== "string" || !isAgent(row["agent"])))
642
+ invalid2(`${path}.agent`, "expected a supported Economy agent or null for all agents");
643
+ }
644
+ }
645
+ if (spec.table === "goals" && !["day", "week", "month", "year"].includes(String(row["period"])))
646
+ invalid2(`${path}.period`, "expected day, week, month or year");
647
+ if (spec.table === "scan_receipts" && row["updated_at"] === undefined)
648
+ row["updated_at"] = row["completed_at"];
649
+ validateObservationProvenance(spec.table, row, path);
650
+ bytes += Buffer.byteLength(stableJson(row));
651
+ if (bytes > MAX_INGEST_BYTES)
652
+ throw new IngestError(413, "INGEST_TOO_LARGE", "body", `maximum ${MAX_INGEST_BYTES} JSON bytes; split into bounded commands`);
653
+ rows.push(row);
654
+ }
655
+ groups.push({ spec, rows });
656
+ }
657
+ if (Buffer.byteLength(stableJson(body)) > MAX_INGEST_BYTES)
658
+ throw new IngestError(413, "INGEST_TOO_LARGE", "body", `maximum ${MAX_INGEST_BYTES} JSON bytes; split into bounded commands`);
659
+ return groups;
660
+ }
661
+ var MAX_INGEST_ROWS = 1e4, MAX_INGEST_BYTES, integers, numeric, required2;
662
+ var init_ingest_validation = __esm(() => {
663
+ init_observation_provenance();
664
+ init_ingest_errors();
665
+ init_agents();
666
+ MAX_INGEST_BYTES = 4 * 1024 * 1024;
667
+ integers = new Set([
668
+ "input_tokens",
669
+ "output_tokens",
670
+ "cache_read_tokens",
671
+ "cache_create_tokens",
672
+ "cache_create_5m_tokens",
673
+ "cache_create_1h_tokens",
674
+ "duration_ms",
675
+ "total_tokens",
676
+ "request_count",
677
+ "active",
678
+ "window_seconds"
679
+ ]);
680
+ numeric = new Set([
681
+ ...integers,
682
+ "cost_usd",
683
+ "total_cost_usd",
684
+ "limit_usd",
685
+ "alert_at_percent",
686
+ "input_per_1m",
687
+ "output_per_1m",
688
+ "cache_read_per_1m",
689
+ "cache_write_per_1m",
690
+ "cache_write_1h_per_1m",
691
+ "cache_storage_per_1m_hour",
692
+ "monthly_fee_usd",
693
+ "included_usage_usd",
694
+ "value"
695
+ ]);
696
+ required2 = {
697
+ requests: ["id", "agent", "session_id", "model", "timestamp"],
698
+ sessions: ["id", "agent", "started_at"],
699
+ projects: ["id", "path", "name", "created_at"],
700
+ budgets: ["id", "period"],
701
+ goals: ["id", "period"],
702
+ billing_daily: ["date", "provider"],
703
+ model_pricing: ["model"],
704
+ subscriptions: ["id", "provider", "plan"],
705
+ usage_snapshots: ["id", "agent", "date", "metric", "unit"],
706
+ scan_receipts: ["id", "machine_id", "source", "attempted_at", "completed_at", "status", "parser_version"]
707
+ };
708
+ });
709
+
710
+ // src/db/ingest-schema.ts
711
+ var INGEST_SCHEMA_SQL = `
712
+ CREATE TABLE IF NOT EXISTS ingest_session_locks (
713
+ session_id TEXT PRIMARY KEY
714
+ );
715
+ CREATE TABLE IF NOT EXISTS ingest_batches (
716
+ operation_key TEXT PRIMARY KEY,
717
+ actor_id TEXT NOT NULL,
718
+ payload_hash TEXT NOT NULL,
719
+ response_json TEXT NOT NULL,
720
+ created_at TEXT NOT NULL
721
+ );
722
+ CREATE TABLE IF NOT EXISTS ingest_preimages (
723
+ id TEXT PRIMARY KEY,
724
+ operation_key TEXT NOT NULL REFERENCES ingest_batches(operation_key),
725
+ table_name TEXT NOT NULL,
726
+ entity_key TEXT NOT NULL,
727
+ record_json TEXT NOT NULL,
728
+ record_sha256 TEXT NOT NULL,
729
+ recorded_at TEXT NOT NULL,
730
+ UNIQUE (operation_key, table_name, entity_key)
731
+ );
732
+ CREATE INDEX IF NOT EXISTS idx_ingest_preimages_entity
733
+ ON ingest_preimages(table_name, entity_key);
734
+ `;
735
+
736
+ // src/db/legacy-schema.ts
737
+ var LEGACY_LEDGER_SQL = `
738
+ CREATE TABLE IF NOT EXISTS legacy_reconciliations (
739
+ operation_key TEXT PRIMARY KEY NOT NULL,
740
+ payload_sha256 TEXT NOT NULL CHECK(length(payload_sha256)=64),
741
+ plan_sha256 TEXT NOT NULL CHECK(length(plan_sha256)=64),
742
+ capsule_sha256 TEXT NOT NULL CHECK(length(capsule_sha256)=64),
743
+ capsule_json TEXT NOT NULL,
744
+ actor_ref TEXT NOT NULL,
745
+ authority_ref TEXT NOT NULL,
746
+ created_at TEXT NOT NULL,
747
+ response_json TEXT NOT NULL
748
+ );
749
+ CREATE TABLE IF NOT EXISTS legacy_reconciliation_targets (
750
+ operation_key TEXT NOT NULL REFERENCES legacy_reconciliations(operation_key),
751
+ request_id TEXT NOT NULL,
752
+ session_id TEXT NOT NULL,
753
+ disposition TEXT NOT NULL CHECK(disposition IN ('quarantine_aggregate','mark_billing_unknown')),
754
+ original_sha256 TEXT NOT NULL CHECK(length(original_sha256)=64),
755
+ PRIMARY KEY(operation_key,request_id)
756
+ );
757
+ CREATE INDEX IF NOT EXISTS idx_legacy_targets_request ON legacy_reconciliation_targets(request_id);
758
+ CREATE INDEX IF NOT EXISTS idx_legacy_targets_session ON legacy_reconciliation_targets(session_id);
759
+ CREATE TABLE IF NOT EXISTS legacy_rollbacks (
760
+ operation_key TEXT PRIMARY KEY NOT NULL,
761
+ apply_key TEXT NOT NULL UNIQUE REFERENCES legacy_reconciliations(operation_key),
762
+ payload_sha256 TEXT NOT NULL CHECK(length(payload_sha256)=64),
763
+ actor_ref TEXT NOT NULL,
764
+ authority_ref TEXT NOT NULL,
765
+ created_at TEXT NOT NULL,
766
+ response_json TEXT NOT NULL
767
+ );
768
+ `, requestColumns, sessionColumns, q = (s) => '"' + s + '"', requestProjection, totals, sessionProjection, LEGACY_VIEWS_SQL, immutableTables, collision = (table) => table === "legacy_reconciliation_targets" ? "operation_key=NEW.operation_key AND request_id=NEW.request_id" : table === "legacy_rollbacks" ? "operation_key=NEW.operation_key OR apply_key=NEW.apply_key" : "operation_key=NEW.operation_key", LEGACY_SQLITE_SQL, pgViews, LEGACY_PG_SQL;
769
+ var init_legacy_schema = __esm(() => {
770
+ requestColumns = [
771
+ "id",
772
+ "agent",
773
+ "session_id",
774
+ "model",
775
+ "input_tokens",
776
+ "output_tokens",
777
+ "cache_read_tokens",
778
+ "cache_create_tokens",
779
+ "cache_create_5m_tokens",
780
+ "cache_create_1h_tokens",
781
+ "cost_usd",
782
+ "cost_basis",
783
+ "duration_ms",
784
+ "timestamp",
785
+ "source_request_id",
786
+ "machine_id",
787
+ "cost_center_id",
788
+ "attribution_tag",
789
+ "account_key",
790
+ "account_tool",
791
+ "account_name",
792
+ "account_email",
793
+ "account_source",
794
+ "updated_at",
795
+ "synced_at"
796
+ ];
797
+ sessionColumns = [
798
+ "id",
799
+ "agent",
800
+ "project_path",
801
+ "project_name",
802
+ "started_at",
803
+ "ended_at",
804
+ "total_cost_usd",
805
+ "total_tokens",
806
+ "request_count",
807
+ "machine_id",
808
+ "cost_center_id",
809
+ "attribution_tag",
810
+ "account_key",
811
+ "account_tool",
812
+ "account_name",
813
+ "account_email",
814
+ "account_source",
815
+ "updated_at",
816
+ "synced_at"
817
+ ];
818
+ requestProjection = requestColumns.map((column) => column === "cost_basis" ? "CASE WHEN EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.request_id=r.id AND a.disposition='mark_billing_unknown') THEN 'unknown' ELSE r.cost_basis END AS cost_basis" : "r." + q(column)).join(`,
819
+ `);
820
+ totals = {
821
+ total_cost_usd: "COALESCE(SUM(cost_usd),0)",
822
+ total_tokens: "COALESCE(SUM(input_tokens+output_tokens+cache_read_tokens+cache_create_tokens),0)",
823
+ request_count: "COUNT(*)"
824
+ };
825
+ sessionProjection = sessionColumns.map((column) => totals[column] ? `CASE WHEN EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.session_id=s.id)
826
+ THEN (SELECT ${totals[column]} FROM economy_effective_requests r WHERE r.session_id=s.id)
827
+ ELSE s.${q(column)} END AS ${q(column)}` : "s." + q(column)).join(`,
828
+ `);
829
+ LEGACY_VIEWS_SQL = `
830
+ CREATE VIEW IF NOT EXISTS economy_legacy_active AS
831
+ SELECT t.* FROM legacy_reconciliation_targets t
832
+ WHERE NOT EXISTS (SELECT 1 FROM legacy_rollbacks b WHERE b.apply_key=t.operation_key);
833
+ CREATE VIEW IF NOT EXISTS economy_effective_requests AS
834
+ SELECT ${requestProjection} FROM requests r
835
+ WHERE NOT EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.request_id=r.id AND a.disposition='quarantine_aggregate');
836
+ CREATE VIEW IF NOT EXISTS economy_effective_sessions AS
837
+ SELECT ${sessionProjection} FROM sessions s
838
+ WHERE NOT EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.session_id=s.id)
839
+ OR EXISTS (SELECT 1 FROM economy_effective_requests r WHERE r.session_id=s.id);
840
+ `;
841
+ immutableTables = ["legacy_reconciliations", "legacy_reconciliation_targets", "legacy_rollbacks"];
842
+ LEGACY_SQLITE_SQL = LEGACY_LEDGER_SQL + LEGACY_VIEWS_SQL + immutableTables.map((table) => `
843
+ CREATE TRIGGER IF NOT EXISTS ${table}_no_update BEFORE UPDATE ON ${table}
844
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation history is immutable'); END;
845
+ CREATE TRIGGER IF NOT EXISTS ${table}_no_delete BEFORE DELETE ON ${table}
846
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation history is immutable'); END;
847
+ CREATE TRIGGER IF NOT EXISTS ${table}_no_replace BEFORE INSERT ON ${table}
848
+ WHEN EXISTS(SELECT 1 FROM ${table} WHERE ${collision(table)})
849
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation history is immutable'); END;
850
+ `).join("") + `
851
+ CREATE TRIGGER IF NOT EXISTS legacy_target_no_overlap BEFORE INSERT ON legacy_reconciliation_targets
852
+ WHEN EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.session_id=NEW.session_id AND a.operation_key<>NEW.operation_key)
853
+ OR EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.request_id=NEW.request_id)
854
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation is already active for this session'); END;
855
+ CREATE TRIGGER IF NOT EXISTS legacy_request_no_insert BEFORE INSERT ON requests
856
+ WHEN EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.request_id=NEW.id)
857
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation protects this request'); END;
858
+ CREATE TRIGGER IF NOT EXISTS legacy_request_no_update BEFORE UPDATE ON requests
859
+ WHEN EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.request_id=NEW.id OR a.request_id=OLD.id)
860
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation protects this request'); END;
861
+ CREATE TRIGGER IF NOT EXISTS legacy_request_no_delete BEFORE DELETE ON requests
862
+ WHEN EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.request_id=OLD.id)
863
+ BEGIN SELECT RAISE(ABORT,'legacy reconciliation protects this request'); END;
864
+ `;
865
+ pgViews = LEGACY_VIEWS_SQL.replace(/CREATE VIEW IF NOT EXISTS (\w+) AS/g, "CREATE VIEW $1 WITH (security_invoker=true) AS");
866
+ LEGACY_PG_SQL = LEGACY_LEDGER_SQL + pgViews + `
867
+ CREATE FUNCTION economy_legacy_history_immutable() RETURNS trigger LANGUAGE plpgsql AS $body$
868
+ BEGIN
869
+ RAISE EXCEPTION 'legacy reconciliation history is immutable' USING ERRCODE='23000';
870
+ END
871
+ $body$;
872
+ ` + immutableTables.map((table) => `
873
+ CREATE TRIGGER ${table}_immutable BEFORE UPDATE OR DELETE OR TRUNCATE ON ${table}
874
+ FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_history_immutable();
875
+ `).join("") + `
876
+ CREATE FUNCTION economy_legacy_request_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
877
+ BEGIN
878
+ IF TG_OP='TRUNCATE' THEN
879
+ IF EXISTS(SELECT 1 FROM economy_legacy_active) THEN
880
+ RAISE EXCEPTION 'legacy reconciliation protects requests' USING ERRCODE='23000';
881
+ END IF;
882
+ RETURN NULL;
883
+ END IF;
884
+ IF TG_OP IN ('INSERT','UPDATE') THEN
885
+ IF EXISTS(SELECT 1 FROM economy_legacy_active WHERE request_id=NEW.id) THEN
886
+ RAISE EXCEPTION 'legacy reconciliation protects this request' USING ERRCODE='23000';
887
+ END IF;
888
+ END IF;
889
+ IF TG_OP IN ('UPDATE','DELETE') THEN
890
+ IF EXISTS(SELECT 1 FROM economy_legacy_active WHERE request_id=OLD.id) THEN
891
+ RAISE EXCEPTION 'legacy reconciliation protects this request' USING ERRCODE='23000';
892
+ END IF;
893
+ END IF;
894
+ IF TG_OP='DELETE' THEN RETURN OLD; END IF;
895
+ RETURN NEW;
896
+ END
897
+ $body$;
898
+ CREATE TRIGGER legacy_request_guard BEFORE INSERT OR UPDATE OR DELETE ON requests
899
+ FOR EACH ROW EXECUTE FUNCTION economy_legacy_request_guard();
900
+ CREATE TRIGGER legacy_request_no_truncate BEFORE TRUNCATE ON requests
901
+ FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_request_guard();
902
+ CREATE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
903
+ BEGIN
904
+ IF EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.session_id=NEW.session_id AND a.operation_key<>NEW.operation_key)
905
+ OR EXISTS(SELECT 1 FROM economy_legacy_active a WHERE a.request_id=NEW.request_id) THEN
906
+ RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
907
+ END IF;
908
+ RETURN NEW;
909
+ END
910
+ $body$;
911
+ CREATE TRIGGER legacy_target_no_overlap BEFORE INSERT ON legacy_reconciliation_targets
912
+ FOR EACH ROW EXECUTE FUNCTION economy_legacy_target_guard();
913
+ `;
914
+ });
915
+
916
+ // src/db/ingest.ts
917
+ var init_ingest = __esm(() => {
918
+ init_ingest_errors();
919
+ init_ingest_validation();
920
+ });
75
921
  // src/lib/pricing.ts
76
922
  var exports_pricing = {};
77
923
  __export(exports_pricing, {
@@ -83,6 +929,9 @@ __export(exports_pricing, {
83
929
  ensurePricingSeeded: () => ensurePricingSeeded,
84
930
  computeCostFromDb: () => computeCostFromDb,
85
931
  computeCost: () => computeCost,
932
+ assessCostFromRows: () => assessCostFromRows,
933
+ assessCostFromDb: () => assessCostFromDb,
934
+ UnknownModelPricingError: () => UnknownModelPricingError,
86
935
  DEFAULT_PRICING: () => DEFAULT_PRICING
87
936
  });
88
937
  function normalizeModelName(raw) {
@@ -259,16 +1108,18 @@ function isFreeModel(model) {
259
1108
  return model.trim().toLowerCase().endsWith(":free");
260
1109
  }
261
1110
  function computeCost(model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
262
- const pricing = getPricing(model);
263
- if (!pricing)
264
- return 0;
265
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
1111
+ return requirePricedCost(assessResolvedCost(model, null, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours));
266
1112
  }
267
1113
  function computeCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
268
- const pricing = getPricingFromDb(db, model) ?? getPricing(model);
269
- if (!pricing)
270
- return 0;
271
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
1114
+ return requirePricedCost(assessCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours));
1115
+ }
1116
+ function requirePricedCost(assessment) {
1117
+ if (assessment.usd === null)
1118
+ throw new UnknownModelPricingError;
1119
+ return assessment.usd;
1120
+ }
1121
+ function assessCostFromDb(db, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
1122
+ return assessResolvedCost(model, getPricingFromDb(db, model), inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
272
1123
  }
273
1124
  function getPricingFromRows(rows, model) {
274
1125
  if (isFreeModel(model))
@@ -284,10 +1135,32 @@ function getPricingFromRows(rows, model) {
284
1135
  return modelPricingFromDbRow(match);
285
1136
  }
286
1137
  function estimateCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
287
- const pricing = getPricingFromRows(rows, model) ?? getPricing(model);
1138
+ return requirePricedCost(assessCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours));
1139
+ }
1140
+ function assessCostFromRows(rows, model, inputTokens, outputTokens, cacheReadTokens = 0, cacheWriteTokens = 0, cacheWrite1hTokens = 0, cacheStorageTokenHours = 0) {
1141
+ return assessResolvedCost(model, getPricingFromRows(rows, model), inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
1142
+ }
1143
+ function assessResolvedCost(model, configured, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
1144
+ const counts = { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens };
1145
+ for (const [field, value] of Object.entries(counts)) {
1146
+ if (!Number.isSafeInteger(value) || value < 0)
1147
+ throw new RangeError(`${field} must be a nonnegative safe integer`);
1148
+ }
1149
+ if (!Number.isFinite(cacheStorageTokenHours) || cacheStorageTokenHours < 0) {
1150
+ throw new RangeError("cacheStorageTokenHours must be finite and nonnegative");
1151
+ }
1152
+ if (isFreeModel(model))
1153
+ return { usd: 0, pricing: "explicit_free" };
1154
+ const pricing = configured ?? getPricing(model);
288
1155
  if (!pricing)
289
- return 0;
290
- return computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
1156
+ return { usd: null, pricing: "missing" };
1157
+ if (Object.values(pricing).some((value) => value !== undefined && (!Number.isFinite(value) || value < 0))) {
1158
+ throw new RangeError("Model pricing rates must be finite and nonnegative");
1159
+ }
1160
+ const usd = computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
1161
+ if (!Number.isFinite(usd))
1162
+ throw new RangeError("Cost estimate exceeds the supported numeric range");
1163
+ return { usd, pricing: configured ? "configured" : "catalog" };
291
1164
  }
292
1165
  function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
293
1166
  if (isFreeModel(model))
@@ -308,7 +1181,7 @@ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cache
308
1181
  }
309
1182
  return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
310
1183
  }
311
- var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
1184
+ var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS, UnknownModelPricingError;
312
1185
  var init_pricing = __esm(() => {
313
1186
  init_database();
314
1187
  DEFAULT_PRICING = {
@@ -587,6 +1460,17 @@ var init_pricing = __esm(() => {
587
1460
  cacheReadPer1M: 0
588
1461
  }
589
1462
  };
1463
+ UnknownModelPricingError = class UnknownModelPricingError extends Error {
1464
+ code = "MODEL_PRICING_UNAVAILABLE";
1465
+ constructor() {
1466
+ super("MODEL_PRICING_UNAVAILABLE: Pricing unavailable for model; configure its rates before estimating cost");
1467
+ this.name = "UnknownModelPricingError";
1468
+ }
1469
+ };
1470
+ });
1471
+ // src/lib/summary-contract.ts
1472
+ var init_summary_contract = __esm(() => {
1473
+ init_pricing();
590
1474
  });
591
1475
 
592
1476
  // src/db/database.ts
@@ -648,6 +1532,9 @@ function pathsResolverResolve(kind, options) {
648
1532
  function dataDir(options) {
649
1533
  return pathsResolverResolve("data", options);
650
1534
  }
1535
+ function cacheDir(options) {
1536
+ return pathsResolverResolve("cache", options);
1537
+ }
651
1538
  function normalizeMachineId(value) {
652
1539
  const id = value?.trim().toLowerCase().split(".")[0];
653
1540
  return id && id.length > 0 ? id : null;
@@ -728,59 +1615,6 @@ function getDbPath() {
728
1615
  return process.env["ECONOMY_DB"];
729
1616
  return join(getDataDir(), "economy.db");
730
1617
  }
731
- function isSqliteBusyError(error) {
732
- const candidate = error;
733
- const code = typeof candidate.code === "string" ? candidate.code : "";
734
- const message = typeof candidate.message === "string" ? candidate.message : String(error);
735
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
736
- }
737
- function retryDelayMs(attempt) {
738
- return Math.min(1000, 50 * 2 ** attempt);
739
- }
740
- function withSqliteBusyRetry(operation, context) {
741
- const maxAttempts = 8;
742
- let lastError;
743
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
744
- try {
745
- return operation();
746
- } catch (error) {
747
- if (!isSqliteBusyError(error))
748
- throw error;
749
- lastError = error;
750
- if (attempt === maxAttempts - 1)
751
- break;
752
- Bun.sleepSync(retryDelayMs(attempt));
753
- }
754
- }
755
- throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
756
- }
757
- function openDatabase(dbPath, skipSeed = false) {
758
- const path = dbPath ?? getDbPath();
759
- if (path !== ":memory:") {
760
- const dir = path.substring(0, path.lastIndexOf("/"));
761
- if (dir && !existsSync(dir))
762
- mkdirSync(dir, { recursive: true });
763
- }
764
- const db = withSqliteBusyRetry(() => {
765
- const opened = new SqliteAdapter(path);
766
- try {
767
- opened.exec("PRAGMA busy_timeout = 10000");
768
- opened.exec("PRAGMA journal_mode = WAL");
769
- opened.exec("PRAGMA foreign_keys = ON");
770
- initSchema(opened);
771
- return opened;
772
- } catch (error) {
773
- try {
774
- opened.close();
775
- } catch {}
776
- throw error;
777
- }
778
- }, `opening ${path}`);
779
- if (!skipSeed) {
780
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
781
- }
782
- return db;
783
- }
784
1618
  function quoteSqlIdent(identifier) {
785
1619
  return `"${identifier.replace(/"/g, '""')}"`;
786
1620
  }
@@ -802,6 +1636,7 @@ function addColumnIfMissing(db, table, column, definition) {
802
1636
  }
803
1637
  }
804
1638
  function initSchema(db) {
1639
+ db.exec(INGEST_SCHEMA_SQL);
805
1640
  db.exec(`
806
1641
  CREATE TABLE IF NOT EXISTS cost_centers (
807
1642
  id TEXT PRIMARY KEY,
@@ -993,6 +1828,7 @@ function initSchema(db) {
993
1828
  addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
994
1829
  addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
995
1830
  addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
1831
+ addColumnIfMissing(db, "budgets", "metric", `TEXT NOT NULL DEFAULT 'legacy_request_cost_usd' CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`);
996
1832
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
997
1833
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
998
1834
  }
@@ -1026,6 +1862,12 @@ function initSchema(db) {
1026
1862
  CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
1027
1863
  CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
1028
1864
  `);
1865
+ for (const [table, columns] of Object.entries(OBSERVATION_COLUMNS)) {
1866
+ for (const [column, type] of Object.entries(columns))
1867
+ addColumnIfMissing(db, table, column, type);
1868
+ }
1869
+ db.exec(SCAN_RECEIPTS_SQL);
1870
+ db.exec(LEGACY_SQLITE_SQL);
1029
1871
  }
1030
1872
  function upsertCostCenter(db, center) {
1031
1873
  const updatedAt = center.updated_at ?? center.created_at;
@@ -1036,7 +1878,7 @@ function upsertCostCenter(db, center) {
1036
1878
  `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
1037
1879
  }
1038
1880
  function upsertRequest(db, req) {
1039
- const now = req.updated_at ?? new Date().toISOString();
1881
+ const now = req.updated_at ?? req.timestamp;
1040
1882
  db.prepare(`
1041
1883
  INSERT OR REPLACE INTO requests
1042
1884
  (id, agent, session_id, model, input_tokens, output_tokens,
@@ -1048,7 +1890,7 @@ function upsertRequest(db, req) {
1048
1890
  `).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
1049
1891
  }
1050
1892
  function upsertSession(db, session) {
1051
- const now = session.updated_at ?? new Date().toISOString();
1893
+ const now = session.updated_at ?? session.ended_at ?? session.started_at;
1052
1894
  db.prepare(`
1053
1895
  INSERT OR REPLACE INTO sessions
1054
1896
  (id, agent, project_path, project_name, started_at, ended_at,
@@ -1060,30 +1902,30 @@ function upsertSession(db, session) {
1060
1902
  function rollupSession(db, sessionId) {
1061
1903
  db.prepare(`
1062
1904
  UPDATE sessions SET
1063
- total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
1064
- total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
1065
- request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
1066
- ended_at = (SELECT MAX(timestamp) FROM requests WHERE session_id = ?),
1905
+ total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM economy_effective_requests WHERE session_id = ?),
1906
+ total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM economy_effective_requests WHERE session_id = ?),
1907
+ request_count = (SELECT COUNT(*) FROM economy_effective_requests WHERE session_id = ?),
1908
+ ended_at = (SELECT MAX(timestamp) FROM economy_effective_requests WHERE session_id = ?),
1067
1909
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
1068
- THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
1910
+ THEN (SELECT MIN(timestamp) FROM economy_effective_requests WHERE session_id = ?)
1069
1911
  ELSE started_at END,
1070
1912
  account_key = CASE WHEN account_key = '' OR account_key IS NULL
1071
- THEN COALESCE((SELECT account_key FROM requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
1913
+ THEN COALESCE((SELECT account_key FROM economy_effective_requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
1072
1914
  ELSE account_key END,
1073
1915
  account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
1074
- THEN COALESCE((SELECT account_tool FROM requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
1916
+ THEN COALESCE((SELECT account_tool FROM economy_effective_requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
1075
1917
  ELSE account_tool END,
1076
1918
  account_name = CASE WHEN account_name = '' OR account_name IS NULL
1077
- THEN COALESCE((SELECT account_name FROM requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
1919
+ THEN COALESCE((SELECT account_name FROM economy_effective_requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
1078
1920
  ELSE account_name END,
1079
1921
  account_email = CASE WHEN account_email = '' OR account_email IS NULL
1080
- THEN COALESCE((SELECT account_email FROM requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
1922
+ THEN COALESCE((SELECT account_email FROM economy_effective_requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
1081
1923
  ELSE account_email END,
1082
1924
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
1083
- THEN COALESCE((SELECT account_source FROM requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
1925
+ THEN COALESCE((SELECT account_source FROM economy_effective_requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
1084
1926
  ELSE account_source END,
1085
1927
  cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1086
- THEN COALESCE((SELECT cost_center_id FROM requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
1928
+ THEN COALESCE((SELECT cost_center_id FROM economy_effective_requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
1087
1929
  ELSE cost_center_id END
1088
1930
  WHERE id = ?
1089
1931
  `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
@@ -1121,7 +1963,13 @@ function seedModelPricing(db, defaults) {
1121
1963
  }
1122
1964
  var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
1123
1965
  var init_database = __esm(() => {
1124
- init_sqlite_adapter();
1966
+ init_observation_schema();
1967
+ init_observation_provenance();
1968
+ init_ingest_validation();
1969
+ init_legacy_schema();
1970
+ init_ingest();
1971
+ init_ingest_errors();
1972
+ init_summary_contract();
1125
1973
  PATHS_RESOLVER_KIND_ENV = {
1126
1974
  config: "HASNA_CONFIG_HOME",
1127
1975
  data: "HASNA_DATA_HOME",
@@ -1131,28 +1979,73 @@ var init_database = __esm(() => {
1131
1979
  PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1132
1980
  });
1133
1981
 
1982
+ // src/db/sqlite-store.ts
1983
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
1984
+ function isSqliteBusyError(error) {
1985
+ const candidate = error;
1986
+ const code = typeof candidate.code === "string" ? candidate.code : "";
1987
+ const message = typeof candidate.message === "string" ? candidate.message : String(error);
1988
+ return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
1989
+ }
1990
+ function retryDelayMs(attempt) {
1991
+ return Math.min(1000, 50 * 2 ** attempt);
1992
+ }
1993
+ function withSqliteBusyRetry(operation, context) {
1994
+ const maxAttempts = 8;
1995
+ let lastError;
1996
+ for (let attempt = 0;attempt < maxAttempts; attempt++) {
1997
+ try {
1998
+ return operation();
1999
+ } catch (error) {
2000
+ if (!isSqliteBusyError(error))
2001
+ throw error;
2002
+ lastError = error;
2003
+ if (attempt === maxAttempts - 1)
2004
+ break;
2005
+ Bun.sleepSync(retryDelayMs(attempt));
2006
+ }
2007
+ }
2008
+ throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
2009
+ }
2010
+ function openDatabase(dbPath, skipSeed = false) {
2011
+ const path = dbPath ?? getDbPath();
2012
+ if (path !== ":memory:") {
2013
+ const dir = path.substring(0, path.lastIndexOf("/"));
2014
+ if (dir && !existsSync2(dir))
2015
+ mkdirSync2(dir, { recursive: true });
2016
+ }
2017
+ const db = withSqliteBusyRetry(() => {
2018
+ const opened = new SqliteAdapter(path);
2019
+ try {
2020
+ opened.exec("PRAGMA busy_timeout = 10000");
2021
+ opened.exec("PRAGMA journal_mode = WAL");
2022
+ opened.exec("PRAGMA foreign_keys = ON");
2023
+ initSchema(opened);
2024
+ return opened;
2025
+ } catch (error) {
2026
+ try {
2027
+ opened.close();
2028
+ } catch {}
2029
+ throw error;
2030
+ }
2031
+ }, `opening ${path}`);
2032
+ if (!skipSeed) {
2033
+ Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
2034
+ }
2035
+ return db;
2036
+ }
2037
+ var init_sqlite_store = __esm(() => {
2038
+ init_sqlite_adapter();
2039
+ init_database();
2040
+ init_database();
2041
+ });
2042
+
1134
2043
  // src/otel/index.ts
1135
- init_database();
2044
+ init_sqlite_store();
1136
2045
 
1137
2046
  // src/ingest/otel.ts
1138
2047
  init_database();
1139
-
1140
- // src/lib/agents.ts
1141
- var AGENTS = [
1142
- "claude",
1143
- "takumi",
1144
- "codex",
1145
- "gemini",
1146
- "opencode",
1147
- "cursor",
1148
- "pi",
1149
- "hermes"
1150
- ];
1151
- function isAgent(value) {
1152
- return AGENTS.includes(value);
1153
- }
1154
-
1155
- // src/ingest/otel.ts
2048
+ init_agents();
1156
2049
  function attrValue(attr) {
1157
2050
  if (!attr?.value)
1158
2051
  return;
@@ -1478,6 +2371,10 @@ async function ingestOtelRows(db, rows) {
1478
2371
  init_database();
1479
2372
  init_pricing();
1480
2373
 
2374
+ // src/ingest/collection-outcome.ts
2375
+ import { AsyncLocalStorage } from "async_hooks";
2376
+ var context = new AsyncLocalStorage;
2377
+
1481
2378
  // src/ingest/claude.ts
1482
2379
  init_database();
1483
2380
  init_pricing();
@@ -1489,172 +2386,6 @@ import { homedir as homedir2 } from "os";
1489
2386
  import { join as join2 } from "path";
1490
2387
  import { z } from "zod";
1491
2388
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
1492
- var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
1493
- var profileSchema = z.object({
1494
- name: z.string(),
1495
- tool: z.string(),
1496
- email: z.string().optional(),
1497
- displayName: z.string().optional(),
1498
- dir: z.string(),
1499
- description: z.string().optional(),
1500
- createdAt: z.string(),
1501
- lastUsedAt: z.string().optional()
1502
- });
1503
- var toolDefSchema = z.object({
1504
- id: z.string(),
1505
- label: z.string(),
1506
- envVar: z.string(),
1507
- defaultDir: z.string(),
1508
- bin: z.string()
1509
- });
1510
- var storeSchema = z.object({
1511
- version: z.literal(1),
1512
- current: z.record(z.string(), z.string()).default({}),
1513
- applied: z.record(z.string(), z.string()).default({}),
1514
- toolLocks: z.record(z.string(), z.string()).default({}),
1515
- profiles: z.array(profileSchema).default([]),
1516
- tools: z.array(toolDefSchema).default([])
1517
- });
1518
- var BUILTIN_TOOLS = [
1519
- { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
1520
- { id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
1521
- { id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
1522
- { id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
1523
- { id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
1524
- { id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
1525
- { id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
1526
- { id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
1527
- { id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
1528
- { id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
1529
- { id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
1530
- { id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
1531
- ];
1532
-
1533
- // src/ingest/claude.ts
1534
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
1535
- var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
1536
-
1537
- // src/ingest/codex.ts
1538
- import { homedir as homedir4 } from "os";
1539
- import { join as join4, basename as basename2 } from "path";
1540
- init_database();
1541
- init_pricing();
1542
- var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
1543
- var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
1544
- var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
1545
- var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
1546
-
1547
- // src/ingest/gemini.ts
1548
- init_database();
1549
- init_pricing();
1550
- import { homedir as homedir5 } from "os";
1551
- import { join as join5, basename as basename3 } from "path";
1552
- var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
1553
- var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
1554
-
1555
- // src/ingest/opencode.ts
1556
- init_database();
1557
- init_pricing();
1558
- import { homedir as homedir6 } from "os";
1559
- import { join as join6 } from "path";
1560
- var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
1561
-
1562
- // src/ingest/cursor.ts
1563
- init_database();
1564
-
1565
- // src/ingest/pi.ts
1566
- init_database();
1567
- import { homedir as homedir7 } from "os";
1568
- import { join as join7 } from "path";
1569
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
1570
-
1571
- // src/ingest/hermes.ts
1572
- init_database();
1573
- import { homedir as homedir8 } from "os";
1574
- import { join as join8 } from "path";
1575
- var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
1576
-
1577
- // src/ingest/loops.ts
1578
- init_database();
1579
- init_pricing();
1580
-
1581
- // src/ingest/claude-quota.ts
1582
- init_database();
1583
-
1584
- // src/lib/paths.ts
1585
- import { homedir as homedir9 } from "os";
1586
- import { join as join9 } from "path";
1587
- function getHomeDir2() {
1588
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
1589
- }
1590
- function agentPaths() {
1591
- const home = getHomeDir2();
1592
- return {
1593
- claudeProjects: join9(home, ".claude", "projects"),
1594
- claudeCredentials: join9(home, ".claude", ".credentials.json"),
1595
- takumiProjects: join9(home, ".takumi", "projects"),
1596
- codexDir: join9(home, ".codex"),
1597
- codexDb: join9(home, ".codex", "state_5.sqlite"),
1598
- codexAuth: join9(home, ".codex", "auth.json"),
1599
- codexConfig: join9(home, ".codex", "config.toml"),
1600
- geminiTmp: join9(home, ".gemini", "tmp"),
1601
- geminiHistory: join9(home, ".gemini", "history"),
1602
- opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
1603
- piSessions: join9(home, ".pi", "agent", "sessions"),
1604
- hermesDir: join9(home, ".hermes"),
1605
- hermesDb: join9(home, ".hermes", "state.db")
1606
- };
1607
- }
1608
-
1609
- // src/ingest/claude-quota.ts
1610
- var CREDENTIALS_PATH = agentPaths().claudeCredentials;
1611
-
1612
- // src/ingest/codex-quota.ts
1613
- init_database();
1614
- var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
1615
-
1616
- // src/lib/sync-all.ts
1617
- init_database();
1618
-
1619
- // src/ingest/billing.ts
1620
- init_database();
1621
-
1622
- // src/lib/sync-maintenance.ts
1623
- init_database();
1624
-
1625
- // src/lib/cloud-ingest.ts
1626
- var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
1627
- function exportIngestRows(db) {
1628
- const body = {};
1629
- for (const table of CLOUD_INGEST_TABLES) {
1630
- const rows = db.prepare(`SELECT * FROM ${table}`).all();
1631
- if (rows.length > 0)
1632
- body[table] = rows;
1633
- }
1634
- return body;
1635
- }
1636
- function unwrapEnvelope(raw) {
1637
- if (raw && typeof raw === "object" && "data" in raw) {
1638
- const data = raw.data;
1639
- return data && typeof data === "object" ? data : {};
1640
- }
1641
- return raw ?? {};
1642
- }
1643
- async function postIngest(cloud, body) {
1644
- const raw = await cloud.client.transport.post("/ingest", body);
1645
- const payload = unwrapEnvelope(raw);
1646
- return {
1647
- ingested: payload["ingested"] ?? {},
1648
- total: Number(payload["total"] ?? 0)
1649
- };
1650
- }
1651
- async function pushIngestRows(cloud, scratch) {
1652
- const body = exportIngestRows(scratch);
1653
- if (Object.keys(body).length === 0)
1654
- return { posted: false, ingested: {}, total: 0 };
1655
- const response = await postIngest(cloud, body);
1656
- return { posted: true, ingested: response.ingested, total: response.total };
1657
- }
1658
2389
 
1659
2390
  // src/lib/cloud-storage.ts
1660
2391
  import {
@@ -1670,18 +2401,6 @@ import {
1670
2401
  } from "@hasna/contracts/client";
1671
2402
  import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
1672
2403
  var ECONOMY_APP = "economy";
1673
- var RETIRED_STORAGE_MODE_KEYS = [
1674
- "HASNA_ECONOMY_STORAGE_MODE",
1675
- "HASNA_ECONOMY_MODE",
1676
- "ECONOMY_STORAGE_MODE",
1677
- "ECONOMY_MODE"
1678
- ];
1679
- function assertNoRetiredStorageMode(env) {
1680
- const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
1681
- if (!legacyKey)
1682
- return;
1683
- throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
1684
- }
1685
2404
  var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
1686
2405
  function envFlagSet(env, key) {
1687
2406
  const raw = env[key];
@@ -1756,7 +2475,7 @@ function resolveEconomyStorageClient(name, env, overrides = {}) {
1756
2475
  };
1757
2476
  }
1758
2477
  function localEconomyNotice() {
1759
- return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
2478
+ return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
1760
2479
  }
1761
2480
  function credentialRequiredError(env, cause) {
1762
2481
  const keys = clientTransportEnvKeys(ECONOMY_APP);
@@ -1768,7 +2487,6 @@ function credentialRequiredError(env, cause) {
1768
2487
  var cache;
1769
2488
  var localNoticePrinted = false;
1770
2489
  function resolveEconomyCloudStorage(env = process.env, overrides) {
1771
- assertNoRetiredStorageMode(env);
1772
2490
  const clientEnv = env;
1773
2491
  if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
1774
2492
  return { active: false, client: null };
@@ -1796,19 +2514,961 @@ function economyCloudStorage(env = process.env) {
1796
2514
  }
1797
2515
  return cache;
1798
2516
  }
2517
+
2518
+ // src/lib/accounts-store.ts
2519
+ var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
2520
+ var profileSchema = z.object({
2521
+ name: z.string(),
2522
+ tool: z.string(),
2523
+ email: z.string().optional(),
2524
+ displayName: z.string().optional(),
2525
+ dir: z.string(),
2526
+ description: z.string().optional(),
2527
+ createdAt: z.string(),
2528
+ lastUsedAt: z.string().optional()
2529
+ });
2530
+ var toolDefSchema = z.object({
2531
+ id: z.string(),
2532
+ label: z.string(),
2533
+ envVar: z.string(),
2534
+ defaultDir: z.string(),
2535
+ bin: z.string()
2536
+ });
2537
+ var storeSchema = z.object({
2538
+ version: z.literal(1),
2539
+ current: z.record(z.string(), z.string()).default({}),
2540
+ applied: z.record(z.string(), z.string()).default({}),
2541
+ toolLocks: z.record(z.string(), z.string()).default({}),
2542
+ profiles: z.array(profileSchema).default([]),
2543
+ tools: z.array(toolDefSchema).default([])
2544
+ });
2545
+ var BUILTIN_TOOLS = [
2546
+ { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
2547
+ { id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
2548
+ { id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
2549
+ { id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
2550
+ { id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
2551
+ { id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
2552
+ { id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
2553
+ { id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
2554
+ { id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
2555
+ { id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
2556
+ { id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
2557
+ { id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
2558
+ ];
2559
+
2560
+ // src/ingest/claude.ts
2561
+ var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
2562
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
2563
+
2564
+ // src/ingest/codex.ts
2565
+ init_database();
2566
+ init_pricing();
2567
+ import { homedir as homedir4 } from "os";
2568
+ import { join as join4, basename as basename2 } from "path";
2569
+ var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
2570
+ var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
2571
+ var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
2572
+ var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
2573
+
2574
+ // src/ingest/gemini.ts
2575
+ init_database();
2576
+ init_pricing();
2577
+ import { homedir as homedir5 } from "os";
2578
+ import { join as join5, basename as basename3 } from "path";
2579
+ var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
2580
+ var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
2581
+
2582
+ // src/ingest/opencode.ts
2583
+ init_database();
2584
+ init_pricing();
2585
+ import { homedir as homedir6 } from "os";
2586
+ import { join as join6 } from "path";
2587
+ var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
2588
+
2589
+ // src/ingest/quota-observation.ts
2590
+ init_database();
2591
+ init_observation_provenance();
2592
+
2593
+ // src/ingest/pi.ts
2594
+ init_database();
2595
+ import { homedir as homedir7 } from "os";
2596
+ import { join as join7 } from "path";
2597
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
2598
+
2599
+ // src/ingest/hermes.ts
2600
+ init_database();
2601
+ import { homedir as homedir8 } from "os";
2602
+ import { join as join8 } from "path";
2603
+ var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
2604
+
2605
+ // src/ingest/loops.ts
2606
+ init_database();
2607
+ init_pricing();
2608
+
2609
+ // src/ingest/codex-quota.ts
2610
+ var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
2611
+
2612
+ // src/lib/sync-all.ts
2613
+ init_database();
2614
+ init_observation_provenance();
2615
+
2616
+ // src/ingest/billing.ts
2617
+ init_database();
2618
+
2619
+ // src/lib/sync-maintenance.ts
2620
+ init_database();
2621
+
2622
+ // src/lib/ingest-batches.ts
2623
+ init_ingest_validation();
2624
+ import { createHash as createHash3 } from "crypto";
2625
+ function* ingestBatches(body) {
2626
+ let rows = {};
2627
+ let count = 0;
2628
+ let bytes = 0;
2629
+ const finish = () => {
2630
+ const digest2 = createHash3("sha256").update(stableJson(rows)).digest("hex");
2631
+ const batch = { ...rows, schema_version: 1, idempotency_key: `scan-${digest2}` };
2632
+ rows = {};
2633
+ count = 0;
2634
+ bytes = 0;
2635
+ if (Buffer.byteLength(JSON.stringify(batch)) > MAX_INGEST_BYTES)
2636
+ throw new Error("Ingest batch exceeds the server byte limit");
2637
+ return batch;
2638
+ };
2639
+ const rowLimit = Math.min(MAX_INGEST_ROWS, 500);
2640
+ const byteLimit = MAX_INGEST_BYTES - 4096;
2641
+ for (const table of Object.keys(body).sort()) {
2642
+ const sorted = body[table].map((row) => ({ row, canonical: stableJson(row) })).sort((a, b) => a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0);
2643
+ for (const { row, canonical } of sorted) {
2644
+ const size = Buffer.byteLength(canonical) + 1;
2645
+ if (size > byteLimit)
2646
+ throw new Error("One ingestion row exceeds the server byte limit");
2647
+ if (count > 0 && (count >= rowLimit || bytes + size > byteLimit))
2648
+ yield finish();
2649
+ (rows[table] ??= []).push(row);
2650
+ count++;
2651
+ bytes += size;
2652
+ }
2653
+ }
2654
+ if (count > 0)
2655
+ yield finish();
2656
+ }
2657
+
2658
+ class IngestBatchFailure extends Error {
2659
+ pending_key;
2660
+ completed;
2661
+ pending_command;
2662
+ constructor(pending_key, completed, pending_command) {
2663
+ super("Ingest batch did not return a verified receipt; retain the exact staged command and reconcile before retrying");
2664
+ this.pending_key = pending_key;
2665
+ this.completed = completed;
2666
+ this.pending_command = pending_command;
2667
+ this.name = "IngestBatchFailure";
2668
+ }
2669
+ }
2670
+
2671
+ // src/lib/ingest-journal.ts
2672
+ init_ingest_validation();
2673
+ init_database();
2674
+ import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
2675
+ import { createHash as createHash4 } from "crypto";
2676
+ import { join as join9, resolve as resolve2 } from "path";
2677
+ var MAX_COMMANDS = 1000;
2678
+ var MAX_BYTES = 64 * 1024 * 1024;
2679
+ var RECEIPT_RESERVE = 4096;
2680
+ var MAINTENANCE_RESERVE = 16384;
2681
+ var sha = (s) => createHash4("sha256").update(s).digest("hex");
2682
+
2683
+ class PendingIngestError extends Error {
2684
+ pending_keys;
2685
+ code = "INGEST_RECONCILIATION_REQUIRED";
2686
+ constructor(pending_keys) {
2687
+ super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
2688
+ this.pending_keys = pending_keys;
2689
+ this.name = "PendingIngestError";
2690
+ }
2691
+ }
2692
+ function definitiveIngestRefusal(status, body) {
2693
+ const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
2694
+ if (!Object.hasOwn(reasons, status))
2695
+ return null;
2696
+ const legacy = body?.expected === "legacy_aggregate_requires_migration";
2697
+ return {
2698
+ status,
2699
+ code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
2700
+ reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
2701
+ };
2702
+ }
2703
+ function getIngestJournalPath() {
2704
+ return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join9(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
2705
+ }
2706
+ function targetId(cloud) {
2707
+ const url = new URL(cloud.client.transport.baseUrl);
2708
+ if (url.username || url.password || url.search || url.hash)
2709
+ throw new Error("Ingest authority must not contain URL credentials or capabilities");
2710
+ return sha(url.toString().replace(/\/+$/, ""));
2711
+ }
2712
+ function secureDirectory(path) {
2713
+ mkdirSync3(path, { recursive: true, mode: 448 });
2714
+ const stat = lstatSync(path);
2715
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
2716
+ throw new Error("Ingest journal must be a private owner directory");
2717
+ }
2718
+ function readJson(path, max) {
2719
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
2720
+ try {
2721
+ const st = fstatSync(fd);
2722
+ if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
2723
+ throw new Error("Unsafe ingest journal record");
2724
+ return JSON.parse(readFileSync(fd, "utf8"));
2725
+ } finally {
2726
+ closeSync(fd);
2727
+ }
2728
+ }
2729
+ function createExact(path, value) {
2730
+ const bytes = JSON.stringify(value);
2731
+ let fd;
2732
+ try {
2733
+ fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
2734
+ } catch (error) {
2735
+ if (error.code === "EEXIST")
2736
+ return false;
2737
+ throw error;
2738
+ }
2739
+ try {
2740
+ writeFileSync(fd, bytes);
2741
+ fsyncSync(fd);
2742
+ } finally {
2743
+ closeSync(fd);
2744
+ }
2745
+ if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
2746
+ throw new Error("Ingest journal readback failed");
2747
+ const directory = openSync(resolve2(path, ".."), constants.O_RDONLY);
2748
+ try {
2749
+ fsyncSync(directory);
2750
+ } finally {
2751
+ closeSync(directory);
2752
+ }
2753
+ return true;
2754
+ }
2755
+ function unwrap(raw) {
2756
+ return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
2757
+ }
2758
+ function verifyIngestReceipt(raw, command) {
2759
+ const receipt = unwrap(raw);
2760
+ if (!receipt || typeof receipt !== "object" || !receipt.ingested || typeof receipt.ingested !== "object" || Array.isArray(receipt.ingested) || !Number.isSafeInteger(receipt.total) || receipt.total < 0 || typeof receipt.operation_key !== "string" || !/^[a-f0-9]{64}$/.test(receipt.operation_key) || typeof receipt.replayed !== "boolean")
2761
+ throw new Error("Invalid ingest receipt");
2762
+ let sum = 0;
2763
+ for (const [table, count] of Object.entries(receipt.ingested)) {
2764
+ if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
2765
+ throw new Error("Invalid ingest receipt");
2766
+ sum += count;
2767
+ }
2768
+ if (sum !== receipt.total)
2769
+ throw new Error("Invalid ingest receipt");
2770
+ const projected = {
2771
+ ingested: Object.fromEntries(Object.entries(receipt.ingested)),
2772
+ total: receipt.total,
2773
+ operation_key: receipt.operation_key,
2774
+ replayed: receipt.replayed,
2775
+ stale: receipt.stale ?? 0,
2776
+ unchanged: receipt.unchanged ?? 0
2777
+ };
2778
+ const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
2779
+ if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
2780
+ throw new Error("Invalid ingest receipt");
2781
+ return projected;
2782
+ }
2783
+ function sameReceipt(left, right) {
2784
+ const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
2785
+ return stableJson(meaning(left)) === stableJson(meaning(right));
2786
+ }
2787
+ async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
2788
+ const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
2789
+ const raw = unwrap(await cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs }));
2790
+ if (!raw || raw.body_hash !== bodyHash)
2791
+ throw new Error("Invalid reconciliation response");
2792
+ if (raw.status === "absent")
2793
+ return null;
2794
+ if (raw.status !== "committed")
2795
+ throw new Error("Invalid reconciliation response");
2796
+ return raw.receipt;
2797
+ }
2798
+
2799
+ class IngestJournal {
2800
+ directory;
2801
+ target;
2802
+ inventoryBytes = 0;
2803
+ freshlyStaged = new Set;
2804
+ constructor(cloud, path = getIngestJournalPath()) {
2805
+ this.target = targetId(cloud);
2806
+ secureDirectory(path);
2807
+ this.directory = join9(realpathSync(path), this.target);
2808
+ secureDirectory(this.directory);
2809
+ }
2810
+ names() {
2811
+ const names = [];
2812
+ const directory = opendirSync(this.directory);
2813
+ try {
2814
+ for (let entry;entry = directory.readSync(); ) {
2815
+ if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
2816
+ continue;
2817
+ names.push(entry.name);
2818
+ if (names.length > MAX_COMMANDS * 2)
2819
+ throw new Error("Ingest journal capacity reached; preserve and reconcile records");
2820
+ }
2821
+ } finally {
2822
+ directory.closeSync();
2823
+ }
2824
+ this.inventoryBytes = 0;
2825
+ const set = new Set(names);
2826
+ for (const name of names) {
2827
+ if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
2828
+ throw new Error("Unexpected ingest journal entry");
2829
+ if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
2830
+ throw new Error("Orphaned ingest outcome requires maintenance recovery");
2831
+ if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
2832
+ throw new Error("Conflicting ingest receipt and refusal");
2833
+ const stat = lstatSync(join9(this.directory, name));
2834
+ if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
2835
+ throw new Error("Unsafe ingest journal record");
2836
+ if (stat.size > MAX_INGEST_BYTES + 4096)
2837
+ throw new Error("Oversized ingest journal record");
2838
+ this.inventoryBytes += stat.size;
2839
+ if (name.endsWith(".refusal.json"))
2840
+ this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
2841
+ }
2842
+ return names;
2843
+ }
2844
+ file(key, kind) {
2845
+ return join9(this.directory, `${sha(key)}.${kind}.json`);
2846
+ }
2847
+ intent(name) {
2848
+ const record = readJson(join9(this.directory, name), MAX_INGEST_BYTES + 4096);
2849
+ if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
2850
+ throw new Error("Invalid ingest journal intent");
2851
+ validateIngest(record.command);
2852
+ return record;
2853
+ }
2854
+ status() {
2855
+ const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
2856
+ const set = new Set(names);
2857
+ const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
2858
+ const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
2859
+ const bytes = this.inventoryBytes;
2860
+ return { target: this.target, commands: intents.length, completed, rejected, pending: intents.length - completed - rejected, bytes, reserved_bytes: bytes + (intents.length - completed - rejected) * RECEIPT_RESERVE + MAINTENANCE_RESERVE };
2861
+ }
2862
+ canStage(command, status = this.status()) {
2863
+ const name = `${sha(command.idempotency_key)}.intent.json`;
2864
+ try {
2865
+ if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
2866
+ throw new Error("Ingest command key is already bound to different content");
2867
+ return true;
2868
+ } catch (error) {
2869
+ if (error.code !== "ENOENT")
2870
+ throw error;
2871
+ }
2872
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
2873
+ return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
2874
+ }
2875
+ receipt(command) {
2876
+ if (this.refusal(command))
2877
+ throw new Error("Conflicting ingest receipt and refusal");
2878
+ const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
2879
+ if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
2880
+ throw new Error("Invalid receipt journal binding");
2881
+ return verifyIngestReceipt(record.receipt, command);
2882
+ }
2883
+ stage(command) {
2884
+ validateIngest(command);
2885
+ const path = this.file(command.idempotency_key, "intent");
2886
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
2887
+ if (!this.canStage(command))
2888
+ throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
2889
+ if (createExact(path, record)) {
2890
+ this.freshlyStaged.add(command.idempotency_key);
2891
+ return true;
2892
+ }
2893
+ this.freshlyStaged.delete(command.idempotency_key);
2894
+ const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
2895
+ if (old.body_hash !== record.body_hash)
2896
+ throw new Error("Ingest command key is already bound to different content");
2897
+ return false;
2898
+ }
2899
+ accept(command, raw) {
2900
+ if (this.refusal(command))
2901
+ throw new Error("A refused command cannot accept a receipt");
2902
+ this.freshlyStaged.delete(command.idempotency_key);
2903
+ const receipt = verifyIngestReceipt(raw, command);
2904
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
2905
+ if (intent.body_hash !== canonicalIngestBodyHash(command))
2906
+ throw new Error("Ingest receipt content conflicts with exact intent");
2907
+ const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
2908
+ const path = this.file(command.idempotency_key, "receipt");
2909
+ if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
2910
+ throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
2911
+ if (!createExact(path, record)) {
2912
+ const old = readJson(path, MAX_INGEST_BYTES);
2913
+ if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
2914
+ throw new Error("Ingest receipt journal conflict");
2915
+ }
2916
+ return receipt;
2917
+ }
2918
+ recordRefusal(command, refusal) {
2919
+ if (!this.freshlyStaged.delete(command.idempotency_key))
2920
+ throw new Error("Refusal requires this invocation's fresh command");
2921
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
2922
+ if (intent.body_hash !== canonicalIngestBodyHash(command))
2923
+ throw new Error("Refusal conflicts with exact intent");
2924
+ const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
2925
+ if (!expected || stableJson(expected) !== stableJson(refusal))
2926
+ throw new Error("Invalid definitive refusal");
2927
+ const names = this.names();
2928
+ if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
2929
+ throw new Error("A completed command cannot be refused");
2930
+ const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
2931
+ if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
2932
+ throw new Error("Refusal capacity exhausted; exact intent retained");
2933
+ if (!createExact(this.file(command.idempotency_key, "refusal"), record))
2934
+ throw new Error("Existing refusal requires inspection");
2935
+ this.refusal(command);
2936
+ }
2937
+ refusal(command) {
2938
+ let record;
2939
+ try {
2940
+ record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
2941
+ } catch (error) {
2942
+ if (error.code === "ENOENT")
2943
+ return null;
2944
+ throw error;
2945
+ }
2946
+ const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
2947
+ const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
2948
+ if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
2949
+ throw new Error("Invalid refusal journal binding");
2950
+ try {
2951
+ lstatSync(this.file(command.idempotency_key, "receipt"));
2952
+ throw new Error("Conflicting ingest receipt and refusal");
2953
+ } catch (error) {
2954
+ if (error.code !== "ENOENT")
2955
+ throw error;
2956
+ }
2957
+ return expected;
2958
+ }
2959
+ async reconcileCommand(cloud, command, timeoutMs = 1e4) {
2960
+ const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
2961
+ return receipt ? this.accept(command, receipt) : null;
2962
+ }
2963
+ assertCloud(cloud) {
2964
+ if (targetId(cloud) !== this.target)
2965
+ throw new Error("Ingest journal authority changed");
2966
+ }
2967
+ async lookupReceipt(cloud, command, timeoutMs = 1e4) {
2968
+ this.assertCloud(cloud);
2969
+ const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
2970
+ return receipt === null ? null : verifyIngestReceipt(receipt, command);
2971
+ }
2972
+ async reconcilePending(cloud) {
2973
+ const names = this.names(), unresolved = [];
2974
+ const deadline = performance.now() + 30000;
2975
+ for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
2976
+ const intent = this.intent(name);
2977
+ if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
2978
+ this.refusal(intent.command);
2979
+ continue;
2980
+ }
2981
+ if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
2982
+ const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
2983
+ if (record.target !== this.target || record.body_hash !== intent.body_hash)
2984
+ throw new Error("Invalid receipt journal binding");
2985
+ verifyIngestReceipt(record.receipt, intent.command);
2986
+ continue;
2987
+ }
2988
+ if (performance.now() >= deadline) {
2989
+ unresolved.push(intent.command.idempotency_key);
2990
+ continue;
2991
+ }
2992
+ try {
2993
+ if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
2994
+ unresolved.push(intent.command.idempotency_key);
2995
+ } catch {
2996
+ unresolved.push(intent.command.idempotency_key);
2997
+ }
2998
+ }
2999
+ if (unresolved.length)
3000
+ throw new PendingIngestError(unresolved);
3001
+ }
3002
+ }
3003
+
3004
+ // src/lib/ingest-journal-maintenance.ts
3005
+ import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
3006
+ import { createHash as createHash5 } from "crypto";
3007
+ import { join as join10 } from "path";
3008
+ var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
3009
+ var digest2 = /^[a-f0-9]{64}$/;
3010
+ var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
3011
+
3012
+ class JournalMaintenanceError extends Error {
3013
+ reason;
3014
+ target;
3015
+ code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
3016
+ constructor(reason, target) {
3017
+ super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
3018
+ this.reason = reason;
3019
+ this.target = target;
3020
+ this.name = "JournalMaintenanceError";
3021
+ }
3022
+ }
3023
+ function exists(path) {
3024
+ try {
3025
+ lstatSync2(path);
3026
+ return true;
3027
+ } catch (error) {
3028
+ if (error.code === "ENOENT")
3029
+ return false;
3030
+ throw error;
3031
+ }
3032
+ }
3033
+ function snapshot(path) {
3034
+ const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
3035
+ try {
3036
+ const stat = fstatSync2(fd);
3037
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
3038
+ throw new Error("Unsafe maintenance source");
3039
+ const bytes = readFileSync2(fd), after = fstatSync2(fd);
3040
+ if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
3041
+ throw new Error("Maintenance source changed");
3042
+ return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
3043
+ } finally {
3044
+ closeSync2(fd);
3045
+ }
3046
+ }
3047
+ function sameSource(path, expected) {
3048
+ const { capture_sha256: _capture, ...original } = expected;
3049
+ if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
3050
+ throw new Error("Maintenance source changed; preserve for inspection");
3051
+ }
3052
+ function validAnchor(value, target) {
3053
+ const a = value;
3054
+ if (a?.version !== 1 || a.target !== target || !/^[A-Za-z0-9._:-]{1,200}$/.test(a.key) || !digest2.test(a.body_hash) || !digest2.test(a.receipt?.operation_key))
3055
+ throw new Error("Invalid journal authority anchor");
3056
+ return a;
3057
+ }
3058
+ async function confirm(cloud, anchor, deadline) {
3059
+ if (Date.now() >= deadline)
3060
+ throw new Error("Maintenance deadline reached");
3061
+ const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
3062
+ if (current === null || !sameReceipt(current, anchor.receipt))
3063
+ throw new Error("Exact Economy receipt is unavailable or changed");
3064
+ }
3065
+ async function verifyJournalAuthority(journal, cloud) {
3066
+ journal.assertCloud(cloud);
3067
+ const path = join10(journal.directory, ".authority.json");
3068
+ if (!exists(path)) {
3069
+ const names = journal.names();
3070
+ const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
3071
+ if (!name)
3072
+ return;
3073
+ const intent = journal.intent(name), receipt = journal.receipt(intent.command);
3074
+ createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
3075
+ }
3076
+ await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
3077
+ }
3078
+ function recoveryFactory(cloud, env = process.env) {
3079
+ return async (operationRoot, checkpoint, deadline) => {
3080
+ if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
3081
+ throw new Error("recovery_not_configured");
3082
+ const economy = new URL(cloud.client.transport.baseUrl);
3083
+ const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
3084
+ if (!fleet && !env.HASNA_TRASH_API_URL)
3085
+ throw new Error("self_host_trash_authority_required");
3086
+ const sdk = await import("@hasna/trash/sdk");
3087
+ if (sdk.VERSION !== "0.2.3")
3088
+ throw new Error("trash_version_mismatch");
3089
+ const boundedFetch = (input, init) => {
3090
+ const remaining = deadline - Date.now();
3091
+ if (remaining <= 0)
3092
+ throw new Error("maintenance_deadline");
3093
+ const signal = AbortSignal.timeout(remaining);
3094
+ return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
3095
+ };
3096
+ return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
3097
+ };
3098
+ }
3099
+
3100
+ class IngestJournalMaintenance {
3101
+ journal;
3102
+ cloud;
3103
+ factory;
3104
+ control;
3105
+ operationRoot;
3106
+ constructor(journal, cloud, factory = recoveryFactory(cloud)) {
3107
+ this.journal = journal;
3108
+ this.cloud = cloud;
3109
+ this.factory = factory;
3110
+ journal.assertCloud(cloud);
3111
+ this.control = join10(journal.directory, ".maintenance");
3112
+ this.operationRoot = join10(journal.directory, ".recovery");
3113
+ }
3114
+ manifest() {
3115
+ secureDirectory(this.control);
3116
+ const names = readdirSync2(this.control);
3117
+ if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
3118
+ throw new Error("Unexpected maintenance entry");
3119
+ let size = 0;
3120
+ for (const name of names)
3121
+ size += lstatSync2(join10(this.control, name)).size;
3122
+ if (size > MAINTENANCE_RESERVE)
3123
+ throw new Error("Maintenance metadata exceeds reserve");
3124
+ const m = validAnchor(readJson(join10(this.control, "manifest.json"), 8192), this.journal.target);
3125
+ for (const part of [m.intent, m.receipt_file])
3126
+ if (!part || !digest2.test(part.sha256) || !digest2.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
3127
+ throw new Error("Invalid maintenance snapshot");
3128
+ return m;
3129
+ }
3130
+ pending(backend) {
3131
+ const pending = backend.pending(4);
3132
+ if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
3133
+ throw new Error("Unexpected Trash recovery state");
3134
+ return pending;
3135
+ }
3136
+ async backend(deadline) {
3137
+ const backend = await this.factory(this.operationRoot, async (point, operation) => {
3138
+ if (operation.path === this.control)
3139
+ return;
3140
+ const m = this.manifest();
3141
+ const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
3142
+ if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
3143
+ throw new Error("Unexpected Trash operation");
3144
+ const expected = kind === "receipt" ? m.receipt_file : m.intent;
3145
+ if (point === "afterSnapshot") {
3146
+ if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
3147
+ throw new Error("Trash snapshot mismatch");
3148
+ sameSource(operation.path, expected);
3149
+ const path = join10(this.control, `${kind}.operation.json`);
3150
+ createExact(path, { id: operation.id });
3151
+ if (readJson(path, 1024).id !== operation.id)
3152
+ throw new Error("Conflicting Trash operation");
3153
+ }
3154
+ if (point === "beforeStage") {
3155
+ await confirm(this.cloud, m, deadline);
3156
+ sameSource(operation.path, expected);
3157
+ }
3158
+ }, deadline);
3159
+ const status = await backend.doctor();
3160
+ if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
3161
+ throw new Error("Trash station authority unavailable");
3162
+ return backend;
3163
+ }
3164
+ async entry(backend, id, path, expected) {
3165
+ const e = await backend.api.get(id);
3166
+ if (e.id !== id || e.originalPath !== path || e.kind !== "file" || e.sha256 !== expected.capture_sha256 || e.sizeBytes !== expected.sizeBytes || e.mode !== expected.mode || e.state !== "trashed")
3167
+ throw new Error("Trash preservation readback refused");
3168
+ if (exists(path))
3169
+ throw new Error("Retired source reappeared; preserve for inspection");
3170
+ }
3171
+ async pairFile(backend, m, kind, deadline) {
3172
+ await confirm(this.cloud, m, deadline);
3173
+ const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
3174
+ const saved = join10(this.control, `${kind}.operation.json`);
3175
+ let id;
3176
+ if (exists(saved))
3177
+ id = readJson(saved, 1024).id;
3178
+ const pending = this.pending(backend);
3179
+ const matches = pending.filter((p) => p.path === path);
3180
+ if (matches.length > 1 || id && !uuid.test(id))
3181
+ throw new Error("Conflicting Trash operation");
3182
+ if (matches[0]) {
3183
+ if (id && id !== matches[0].id)
3184
+ throw new Error("Conflicting Trash operation");
3185
+ id = matches[0].id;
3186
+ const operation = backend.journal.read(id);
3187
+ if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
3188
+ throw new Error("Invalid Trash operation binding");
3189
+ if (exists(path))
3190
+ sameSource(path, expected);
3191
+ await backend.recover(id, { takeAbandonedLock: true });
3192
+ } else if (!id) {
3193
+ sameSource(path, expected);
3194
+ const status = await backend.doctor();
3195
+ const e = await backend.put(path, { retentionDays: status.retentionDays });
3196
+ id = e.id;
3197
+ if (!exists(saved) || readJson(saved, 1024).id !== id)
3198
+ throw new Error("Trash did not persist its capture binding");
3199
+ }
3200
+ await this.entry(backend, id, path, expected);
3201
+ }
3202
+ async recover() {
3203
+ if (!exists(this.control) && !exists(this.operationRoot))
3204
+ return;
3205
+ if (exists(this.operationRoot))
3206
+ secureDirectory(this.operationRoot);
3207
+ if (!exists(this.control) && readdirSync2(this.operationRoot).every((n) => n === ".locks"))
3208
+ return;
3209
+ const deadline = Date.now() + 30000;
3210
+ try {
3211
+ await this.finish(await this.backend(deadline), deadline);
3212
+ } catch (error) {
3213
+ if (error instanceof JournalMaintenanceError)
3214
+ throw error;
3215
+ throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
3216
+ }
3217
+ }
3218
+ async finish(backend, deadline) {
3219
+ const pending = this.pending(backend);
3220
+ const control = pending.filter((p) => p.path === this.control);
3221
+ if (control.length) {
3222
+ if (control.length !== 1 || pending.length !== 1)
3223
+ throw new Error("Conflicting maintenance cleanup");
3224
+ const op = backend.journal.read(control[0].id);
3225
+ if (op.kind !== "capture" || op.path !== this.control)
3226
+ throw new Error("Invalid maintenance cleanup");
3227
+ await backend.recover(op.id, { takeAbandonedLock: true });
3228
+ if (exists(this.control) || this.pending(backend).length)
3229
+ throw new Error("Maintenance cleanup unresolved");
3230
+ return;
3231
+ }
3232
+ if (!exists(this.control)) {
3233
+ if (pending.length)
3234
+ throw new Error("Orphaned maintenance operation");
3235
+ return;
3236
+ }
3237
+ if (readdirSync2(this.control).length === 0) {
3238
+ if (pending.length)
3239
+ throw new Error("Orphaned maintenance operation");
3240
+ const status2 = await backend.doctor();
3241
+ await backend.put(this.control, { retentionDays: status2.retentionDays });
3242
+ return;
3243
+ }
3244
+ const m = this.manifest();
3245
+ if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
3246
+ throw new Error("Unexpected recovery path");
3247
+ await this.pairFile(backend, m, "receipt", deadline);
3248
+ await this.pairFile(backend, m, "intent", deadline);
3249
+ if (this.pending(backend).length)
3250
+ throw new Error("Unfinished pair capture");
3251
+ await confirm(this.cloud, m, deadline);
3252
+ const status = await backend.doctor();
3253
+ await backend.put(this.control, { retentionDays: status.retentionDays });
3254
+ if (exists(this.control) || this.pending(backend).length)
3255
+ throw new Error("Maintenance cleanup unresolved");
3256
+ }
3257
+ async maintain(limit = 32) {
3258
+ if (!Number.isInteger(limit) || limit < 1 || limit > 32)
3259
+ throw new Error("Journal maintenance limit must be 1\u201332");
3260
+ await this.recover();
3261
+ await verifyJournalAuthority(this.journal, this.cloud);
3262
+ const deadline = Date.now() + 30000;
3263
+ let retired = 0, bytes = 0;
3264
+ let backend;
3265
+ try {
3266
+ const names = this.journal.names();
3267
+ for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
3268
+ if (retired >= limit || Date.now() >= deadline || retired > 0 && bytes >= 1048576)
3269
+ break;
3270
+ if (!names.includes(name.replace(".intent.", ".receipt.")))
3271
+ continue;
3272
+ const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
3273
+ backend ??= await this.backend(deadline);
3274
+ const m = {
3275
+ version: 1,
3276
+ target: this.journal.target,
3277
+ key: intent.command.idempotency_key,
3278
+ body_hash: intent.body_hash,
3279
+ receipt,
3280
+ intent: snapshot(join10(this.journal.directory, name)),
3281
+ receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
3282
+ };
3283
+ await confirm(this.cloud, m, deadline);
3284
+ m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
3285
+ m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
3286
+ secureDirectory(this.control);
3287
+ if (!createExact(join10(this.control, "manifest.json"), m))
3288
+ throw new Error("Existing maintenance transaction");
3289
+ await this.finish(backend, deadline);
3290
+ retired++;
3291
+ bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
3292
+ }
3293
+ return { retired, remaining: this.journal.status().completed };
3294
+ } catch (error) {
3295
+ if (error instanceof JournalMaintenanceError)
3296
+ throw error;
3297
+ const reason = error instanceof Error && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch"].includes(error.message) ? error.message : backend ? "recovery_unresolved" : "recovery_authority_unavailable";
3298
+ throw new JournalMaintenanceError(reason, this.journal.target);
3299
+ }
3300
+ }
3301
+ async admit(command) {
3302
+ const status = this.journal.status();
3303
+ const fits = this.journal.canStage(command, status);
3304
+ if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
3305
+ return;
3306
+ try {
3307
+ await this.maintain();
3308
+ } catch (error) {
3309
+ if (exists(this.control) || !this.journal.canStage(command))
3310
+ throw error;
3311
+ }
3312
+ if (!this.journal.canStage(command))
3313
+ throw new JournalMaintenanceError("capacity", this.journal.target);
3314
+ }
3315
+ }
3316
+
3317
+ // src/lib/ingest-journal-lock.ts
3318
+ import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
3319
+ import { join as join11 } from "path";
3320
+ async function withIngestJournalLock(directory, action) {
3321
+ if (process.platform !== "linux" && process.platform !== "darwin")
3322
+ throw new Error("Ingest journal locking requires Linux or macOS");
3323
+ const { dlopen, FFIType } = await import("bun:ffi");
3324
+ const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
3325
+ flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
3326
+ });
3327
+ const path = join11(directory, ".lease");
3328
+ let fd;
3329
+ try {
3330
+ fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
3331
+ const stat = fstatSync3(fd), current = lstatSync3(path);
3332
+ if (!stat.isFile() || stat.size !== 0 || stat.nlink !== 1 || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
3333
+ throw new Error("Unsafe ingest journal lease");
3334
+ if (library.symbols.flock(fd, 6) !== 0) {
3335
+ const error = new Error("Ingest journal is busy; retry after the current operation finishes");
3336
+ Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
3337
+ throw error;
3338
+ }
3339
+ return await action();
3340
+ } finally {
3341
+ if (fd !== undefined)
3342
+ closeSync3(fd);
3343
+ library.close();
3344
+ }
3345
+ }
3346
+
3347
+ // src/lib/ingest-journal-operation.ts
3348
+ import { z as z2 } from "zod";
3349
+ var ingestJournalOperationSchema = z2.discriminatedUnion("action", [
3350
+ z2.object({ action: z2.literal("status") }).strict(),
3351
+ z2.object({ action: z2.literal("reconcile") }).strict(),
3352
+ z2.object({ action: z2.literal("maintain"), confirm: z2.literal(true), limit: z2.number().int().min(1).max(32).default(32) }).strict()
3353
+ ]);
3354
+
3355
+ // src/lib/cloud-ingest.ts
3356
+ import { HasnaHttpError } from "@hasna/contracts/client";
3357
+ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
3358
+ function exportIngestRows(db) {
3359
+ const body = {};
3360
+ for (const table of CLOUD_INGEST_TABLES) {
3361
+ const rows = db.prepare(`SELECT * FROM ${table}`).all();
3362
+ if (rows.length > 0)
3363
+ body[table] = rows;
3364
+ }
3365
+ return body;
3366
+ }
3367
+
3368
+ class IngestRefused extends Error {
3369
+ status;
3370
+ code;
3371
+ reason;
3372
+ constructor(status, code, reason = code) {
3373
+ super(`Ingestion refused: ${reason}; exact command intent retained`);
3374
+ this.status = status;
3375
+ this.code = code;
3376
+ this.reason = reason;
3377
+ this.name = "IngestRefused";
3378
+ }
3379
+ }
3380
+ async function postIngest(cloud, body) {
3381
+ const journal = new IngestJournal(cloud);
3382
+ return withIngestJournalLock(journal.directory, async () => {
3383
+ const maintenance = new IngestJournalMaintenance(journal, cloud);
3384
+ await maintenance.recover();
3385
+ await journal.reconcilePending(cloud);
3386
+ await verifyJournalAuthority(journal, cloud);
3387
+ const ingested = {};
3388
+ const completed = [];
3389
+ let total = 0;
3390
+ for (const batch of ingestBatches(body)) {
3391
+ try {
3392
+ const refused = journal.refusal(batch);
3393
+ if (refused)
3394
+ throw new IngestRefused(refused.status, refused.code, refused.reason);
3395
+ await maintenance.admit(batch);
3396
+ const known = await journal.lookupReceipt(cloud, batch);
3397
+ const fresh = journal.stage(batch);
3398
+ const sendFresh = async () => {
3399
+ let response;
3400
+ try {
3401
+ response = await cloud.client.transport.post("/ingest", batch, { retry: false });
3402
+ } catch (error) {
3403
+ const refusal = error instanceof HasnaHttpError ? definitiveIngestRefusal(error.status, error.body) : null;
3404
+ if (refusal) {
3405
+ journal.recordRefusal(batch, refusal);
3406
+ throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
3407
+ }
3408
+ throw error;
3409
+ }
3410
+ return journal.accept(batch, response);
3411
+ };
3412
+ const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
3413
+ await verifyJournalAuthority(journal, cloud);
3414
+ if (!payload)
3415
+ throw new PendingIngestError([batch.idempotency_key]);
3416
+ verifyIngestReceipt(payload, batch);
3417
+ const counts = payload["ingested"];
3418
+ if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
3419
+ throw new Error("Invalid ingest receipt");
3420
+ let sum = 0;
3421
+ for (const [table, count] of Object.entries(counts)) {
3422
+ if (!CLOUD_INGEST_TABLES.includes(table) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)
3423
+ throw new Error("Invalid ingest receipt");
3424
+ sum += count;
3425
+ ingested[table] = (ingested[table] ?? 0) + count;
3426
+ }
3427
+ if (sum !== payload["total"])
3428
+ throw new Error("Invalid ingest receipt");
3429
+ total += sum;
3430
+ const operation = payload["operation_key"];
3431
+ if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
3432
+ throw new Error("Invalid ingest receipt");
3433
+ completed.push({
3434
+ idempotency_key: batch.idempotency_key,
3435
+ ...typeof operation === "string" ? { operation_key: operation } : {},
3436
+ replayed: payload["replayed"] === true
3437
+ });
3438
+ } catch (error) {
3439
+ if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
3440
+ throw error;
3441
+ if (error instanceof HasnaHttpError && error.status >= 400 && error.status < 500 && error.status !== 408) {
3442
+ const body2 = error.body;
3443
+ throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
3444
+ }
3445
+ throw new IngestBatchFailure(batch.idempotency_key, completed, batch);
3446
+ }
3447
+ }
3448
+ return { ingested, total, receipts: completed };
3449
+ });
3450
+ }
3451
+ async function pushIngestRows(cloud, scratch) {
3452
+ const body = exportIngestRows(scratch);
3453
+ if (Object.keys(body).length === 0)
3454
+ return { posted: false, ingested: {}, total: 0, receipts: [] };
3455
+ const response = await postIngest(cloud, body);
3456
+ return { posted: true, ...response };
3457
+ }
1799
3458
  // package.json
1800
3459
  var package_default = {
1801
3460
  name: "@hasna/economy",
1802
- version: "0.4.0",
3461
+ version: "0.5.1",
1803
3462
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
1804
3463
  repository: {
1805
3464
  type: "git",
1806
- url: "git+https://github.com/hasna/economy.git"
3465
+ url: "https://github.com/hasna/apps.git",
3466
+ directory: "apps/economy"
1807
3467
  },
1808
3468
  bugs: {
1809
- url: "https://github.com/hasna/economy/issues"
3469
+ url: "https://github.com/hasna/apps/issues"
1810
3470
  },
1811
- homepage: "https://github.com/hasna/economy#readme",
3471
+ homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
1812
3472
  type: "module",
1813
3473
  main: "dist/index.js",
1814
3474
  types: "dist/index.d.ts",
@@ -1837,14 +3497,15 @@ var package_default = {
1837
3497
  "CHANGELOG.md",
1838
3498
  "SECURITY.md",
1839
3499
  "CONTRIBUTING.md",
1840
- "CODE_OF_CONDUCT.md"
3500
+ "CODE_OF_CONDUCT.md",
3501
+ "!dist/lib/test-hermetic-*"
1841
3502
  ],
1842
3503
  scripts: {
1843
- build: "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
1844
- "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
1845
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
1846
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
1847
- "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
3504
+ build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
3505
+ "build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
3506
+ "build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
3507
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
3508
+ "build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
1848
3509
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
1849
3510
  prepack: "bun run build && bun run artifact-scan",
1850
3511
  typecheck: "tsc --noEmit",
@@ -1881,7 +3542,8 @@ var package_default = {
1881
3542
  chalk: "^5.4.1",
1882
3543
  commander: "^13.1.0",
1883
3544
  pg: "8.13.1",
1884
- zod: "^3.24.2"
3545
+ zod: "^3.24.2",
3546
+ "@hasna/trash": "0.2.3"
1885
3547
  },
1886
3548
  devDependencies: {
1887
3549
  "@types/bun": "1.3.14",
@@ -1891,7 +3553,16 @@ var package_default = {
1891
3553
  },
1892
3554
  trustedDependencies: [
1893
3555
  "@hasna/projects"
1894
- ]
3556
+ ],
3557
+ overrides: {
3558
+ "fast-uri": "3.1.8",
3559
+ hono: "4.13.7",
3560
+ "ip-address": "10.7.1",
3561
+ nanoid: "5.1.16",
3562
+ "path-to-regexp": "8.4.0",
3563
+ undici: "6.28.1",
3564
+ "express-rate-limit": "8.7.0"
3565
+ }
1895
3566
  };
1896
3567
 
1897
3568
  // src/lib/package-metadata.ts