@hasna/economy 0.3.28 → 0.5.0

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 (216) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +18 -22
  4. package/SECURITY.md +1 -1
  5. package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
  6. package/dist/chunks/billing-bkpxy4kv.js +13 -0
  7. package/dist/chunks/config-j9mv3ehk.js +15 -0
  8. package/dist/chunks/index-2hwzmfgv.js +3407 -0
  9. package/dist/chunks/index-axh32d30.js +1313 -0
  10. package/dist/chunks/index-emvs5ph2.js +2085 -0
  11. package/dist/chunks/index-ez9thsr7.js +3440 -0
  12. package/dist/chunks/index-hesxq90g.js +62 -0
  13. package/dist/chunks/index-k540hfww.js +258 -0
  14. package/dist/chunks/index-kgy9b6bf.js +451 -0
  15. package/dist/chunks/index-kn5572kd.js +124 -0
  16. package/dist/chunks/index-p35cyhc4.js +4 -0
  17. package/dist/chunks/index-w3bvv0ns.js +26 -0
  18. package/dist/chunks/index-zpvea08j.js +3372 -0
  19. package/dist/chunks/menubar-6xtkt17h.js +296 -0
  20. package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
  21. package/dist/chunks/pricing-f80p81gr.js +29 -0
  22. package/dist/chunks/pricing-vf2pycmh.js +30 -0
  23. package/dist/chunks/pricing-y21mcjsa.js +30 -0
  24. package/dist/chunks/serve-0k69e4h9.js +4655 -0
  25. package/dist/chunks/sqlite-store-501f32ts.js +258 -0
  26. package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
  27. package/dist/chunks/sqlite-store-st99garx.js +258 -0
  28. package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
  29. package/dist/chunks/tui-7f8f951w.js +22 -0
  30. package/dist/chunks/watch-mm1j39vy.js +196 -0
  31. package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
  32. package/dist/cli/commands/brief.d.ts.map +1 -1
  33. package/dist/cli/commands/extras.d.ts.map +1 -1
  34. package/dist/cli/commands/journal.d.ts +3 -0
  35. package/dist/cli/commands/journal.d.ts.map +1 -0
  36. package/dist/cli/commands/menubar.d.ts +97 -4
  37. package/dist/cli/commands/menubar.d.ts.map +1 -1
  38. package/dist/cli/commands/tui.d.ts +1 -1
  39. package/dist/cli/commands/tui.d.ts.map +1 -1
  40. package/dist/cli/commands/watch.d.ts.map +1 -1
  41. package/dist/cli/index.js +1524 -9998
  42. package/dist/db/cloud.d.ts +18 -12
  43. package/dist/db/cloud.d.ts.map +1 -1
  44. package/dist/db/database.d.ts +33 -12
  45. package/dist/db/database.d.ts.map +1 -1
  46. package/dist/db/ingest-concurrency-worker.d.ts +2 -0
  47. package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
  48. package/dist/db/ingest-errors.d.ts +9 -0
  49. package/dist/db/ingest-errors.d.ts.map +1 -0
  50. package/dist/db/ingest-schema.d.ts +3 -0
  51. package/dist/db/ingest-schema.d.ts.map +1 -0
  52. package/dist/db/ingest-specs.d.ts +8 -0
  53. package/dist/db/ingest-specs.d.ts.map +1 -0
  54. package/dist/db/ingest-test-contract.d.ts +3 -0
  55. package/dist/db/ingest-test-contract.d.ts.map +1 -0
  56. package/dist/db/ingest-validation.d.ts +12 -0
  57. package/dist/db/ingest-validation.d.ts.map +1 -0
  58. package/dist/db/ingest.d.ts +27 -0
  59. package/dist/db/ingest.d.ts.map +1 -0
  60. package/dist/db/legacy-schema.d.ts +7 -0
  61. package/dist/db/legacy-schema.d.ts.map +1 -0
  62. package/dist/db/observation-contract.d.ts +3 -0
  63. package/dist/db/observation-contract.d.ts.map +1 -0
  64. package/dist/db/observation-provenance.d.ts +23 -0
  65. package/dist/db/observation-provenance.d.ts.map +1 -0
  66. package/dist/db/observation-schema.d.ts +24 -0
  67. package/dist/db/observation-schema.d.ts.map +1 -0
  68. package/dist/db/pg-connection.d.ts +10 -0
  69. package/dist/db/pg-connection.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 +1281 -2405
  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 +9 -0
  96. package/dist/ingest/loops.d.ts.map +1 -1
  97. package/dist/ingest/opencode.d.ts +1 -1
  98. package/dist/ingest/opencode.d.ts.map +1 -1
  99. package/dist/ingest/pi.d.ts +1 -1
  100. package/dist/ingest/pi.d.ts.map +1 -1
  101. package/dist/ingest/quota-observation.d.ts +16 -0
  102. package/dist/ingest/quota-observation.d.ts.map +1 -0
  103. package/dist/ingest/source-time.d.ts +4 -0
  104. package/dist/ingest/source-time.d.ts.map +1 -0
  105. package/dist/lib/accounts-store.d.ts +109 -0
  106. package/dist/lib/accounts-store.d.ts.map +1 -0
  107. package/dist/lib/accounts.d.ts +2 -1
  108. package/dist/lib/accounts.d.ts.map +1 -1
  109. package/dist/lib/analytics.d.ts.map +1 -1
  110. package/dist/lib/api-display-url.d.ts +19 -0
  111. package/dist/lib/api-display-url.d.ts.map +1 -0
  112. package/dist/lib/autosync-gate.d.ts +22 -0
  113. package/dist/lib/autosync-gate.d.ts.map +1 -0
  114. package/dist/lib/billing-diff.d.ts +18 -40
  115. package/dist/lib/billing-diff.d.ts.map +1 -1
  116. package/dist/lib/brief.d.ts +2 -0
  117. package/dist/lib/brief.d.ts.map +1 -1
  118. package/dist/lib/budget-decimal.d.ts +14 -0
  119. package/dist/lib/budget-decimal.d.ts.map +1 -0
  120. package/dist/lib/cloud-ingest.d.ts +30 -1
  121. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  122. package/dist/lib/cloud-storage.d.ts +166 -10
  123. package/dist/lib/cloud-storage.d.ts.map +1 -1
  124. package/dist/lib/gatherer.d.ts.map +1 -1
  125. package/dist/lib/ingest-batches.d.ts +21 -0
  126. package/dist/lib/ingest-batches.d.ts.map +1 -0
  127. package/dist/lib/ingest-journal-client.d.ts +37 -0
  128. package/dist/lib/ingest-journal-client.d.ts.map +1 -0
  129. package/dist/lib/ingest-journal-lock.d.ts +5 -0
  130. package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
  131. package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
  132. package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
  133. package/dist/lib/ingest-journal-operation.d.ts +55 -0
  134. package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
  135. package/dist/lib/ingest-journal.d.ts +73 -0
  136. package/dist/lib/ingest-journal.d.ts.map +1 -0
  137. package/dist/lib/money-display.d.ts +13 -0
  138. package/dist/lib/money-display.d.ts.map +1 -0
  139. package/dist/lib/periods.d.ts +12 -0
  140. package/dist/lib/periods.d.ts.map +1 -1
  141. package/dist/lib/pricing.d.ts +14 -0
  142. package/dist/lib/pricing.d.ts.map +1 -1
  143. package/dist/lib/savings.d.ts +64 -7
  144. package/dist/lib/savings.d.ts.map +1 -1
  145. package/dist/lib/serve-auth.d.ts.map +1 -1
  146. package/dist/lib/store/index.d.ts +29 -22
  147. package/dist/lib/store/index.d.ts.map +1 -1
  148. package/dist/lib/summary-contract.d.ts +32 -0
  149. package/dist/lib/summary-contract.d.ts.map +1 -0
  150. package/dist/lib/summary-test-contract.d.ts +3 -0
  151. package/dist/lib/summary-test-contract.d.ts.map +1 -0
  152. package/dist/lib/sync-all.d.ts +4 -0
  153. package/dist/lib/sync-all.d.ts.map +1 -1
  154. package/dist/lib/sync-maintenance.d.ts.map +1 -1
  155. package/dist/lib/sync-report.d.ts +4 -0
  156. package/dist/lib/sync-report.d.ts.map +1 -0
  157. package/dist/lib/test-keychain-fixture.d.ts +9 -0
  158. package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
  159. package/dist/mcp/agent-registry-store.d.ts +30 -0
  160. package/dist/mcp/agent-registry-store.d.ts.map +1 -0
  161. package/dist/mcp/agent-registry.d.ts +78 -0
  162. package/dist/mcp/agent-registry.d.ts.map +1 -0
  163. package/dist/mcp/harness.d.ts +26 -0
  164. package/dist/mcp/harness.d.ts.map +1 -0
  165. package/dist/mcp/index.js +3219 -3357
  166. package/dist/mcp/ingest-journal-tool.d.ts +2 -0
  167. package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
  168. package/dist/mcp/server.d.ts.map +1 -1
  169. package/dist/openapi.d.ts.map +1 -1
  170. package/dist/otel/index.js +2197 -121
  171. package/dist/server/human-auth.d.ts +20 -0
  172. package/dist/server/human-auth.d.ts.map +1 -0
  173. package/dist/server/index.js +10477 -6378
  174. package/dist/server/legacy-inventory.d.ts +52 -0
  175. package/dist/server/legacy-inventory.d.ts.map +1 -0
  176. package/dist/server/legacy-operations.d.ts +64 -0
  177. package/dist/server/legacy-operations.d.ts.map +1 -0
  178. package/dist/server/legacy-plan.d.ts +55 -0
  179. package/dist/server/legacy-plan.d.ts.map +1 -0
  180. package/dist/server/legacy-preservation.d.ts +58 -0
  181. package/dist/server/legacy-preservation.d.ts.map +1 -0
  182. package/dist/server/pg-sync-worker.js +195 -21
  183. package/dist/server/serve.d.ts +20 -18
  184. package/dist/server/serve.d.ts.map +1 -1
  185. package/dist/server/service-admission.d.ts +10 -0
  186. package/dist/server/service-admission.d.ts.map +1 -0
  187. package/dist/server/service-dispatcher.d.ts +45 -0
  188. package/dist/server/service-dispatcher.d.ts.map +1 -0
  189. package/dist/server/service-operations.d.ts +5 -0
  190. package/dist/server/service-operations.d.ts.map +1 -0
  191. package/dist/server/service-protocol.d.ts +50 -0
  192. package/dist/server/service-protocol.d.ts.map +1 -0
  193. package/dist/server/service-worker.d.ts +2 -0
  194. package/dist/server/service-worker.d.ts.map +1 -0
  195. package/dist/server/service-worker.js +8029 -0
  196. package/dist/types/index.d.ts +66 -0
  197. package/dist/types/index.d.ts.map +1 -1
  198. package/docs/README.md +1 -1
  199. package/docs/cli.md +2 -3
  200. package/docs/configuration.md +25 -12
  201. package/docs/cost-reporting.md +49 -0
  202. package/docs/historical-data-reconciliation.md +203 -0
  203. package/docs/ingest-journal.md +108 -0
  204. package/docs/ingestion.md +61 -5
  205. package/docs/mcp.md +5 -3
  206. package/docs/native-budget-save.md +84 -0
  207. package/docs/native-human-auth.md +102 -0
  208. package/docs/otel.md +8 -2
  209. package/docs/rest-api.md +4 -4
  210. package/package.json +17 -20
  211. package/postinstall.js +52 -1
  212. package/dashboard/README.md +0 -26
  213. package/dist/cli/brains.d.ts +0 -3
  214. package/dist/cli/brains.d.ts.map +0 -1
  215. package/dist/lib/test-hermetic-accounts.d.ts +0 -6
  216. package/dist/lib/test-hermetic-accounts.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
@@ -595,7 +1479,62 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
595
1479
  import { hostname, platform } from "os";
596
1480
  import { homedir } from "os";
597
1481
  import { join, resolve } from "path";
598
- import { dataDir } from "@hasna/paths";
1482
+ import { homedir as pathsResolverHomedir } from "os";
1483
+ import { join as pathsResolverJoin } from "path";
1484
+ function pathsResolverAssertApp(app) {
1485
+ if (typeof app !== "string" || app.length === 0) {
1486
+ throw new TypeError("paths: app must be a non-empty string");
1487
+ }
1488
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
1489
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
1490
+ }
1491
+ }
1492
+ function pathsResolverAssertKind(kind) {
1493
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
1494
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
1495
+ }
1496
+ }
1497
+ function pathsResolverBaseDir(kind, options) {
1498
+ pathsResolverAssertKind(kind);
1499
+ const env = options.env ?? process.env;
1500
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
1501
+ if (typeof override === "string" && override.length > 0)
1502
+ return override;
1503
+ const home = options.home ?? pathsResolverHomedir();
1504
+ const platform2 = options.platform ?? process.platform;
1505
+ if (platform2 === "darwin") {
1506
+ switch (kind) {
1507
+ case "config":
1508
+ case "data":
1509
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
1510
+ case "cache":
1511
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
1512
+ case "state":
1513
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
1514
+ }
1515
+ }
1516
+ switch (kind) {
1517
+ case "config":
1518
+ return pathsResolverJoin(home, ".config", "hasna");
1519
+ case "data":
1520
+ return pathsResolverJoin(home, ".local", "share", "hasna");
1521
+ case "state":
1522
+ return pathsResolverJoin(home, ".local", "state", "hasna");
1523
+ case "cache":
1524
+ return pathsResolverJoin(home, ".cache", "hasna");
1525
+ }
1526
+ }
1527
+ function pathsResolverResolve(kind, options) {
1528
+ pathsResolverAssertApp(options.app);
1529
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
1530
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
1531
+ }
1532
+ function dataDir(options) {
1533
+ return pathsResolverResolve("data", options);
1534
+ }
1535
+ function cacheDir(options) {
1536
+ return pathsResolverResolve("cache", options);
1537
+ }
599
1538
  function normalizeMachineId(value) {
600
1539
  const id = value?.trim().toLowerCase().split(".")[0];
601
1540
  return id && id.length > 0 ? id : null;
@@ -618,7 +1557,7 @@ function macHostMachineId() {
618
1557
  return null;
619
1558
  }
620
1559
  function getMachineId() {
621
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
1560
+ const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
622
1561
  if (envMachine)
623
1562
  return envMachine;
624
1563
  const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
@@ -676,59 +1615,6 @@ function getDbPath() {
676
1615
  return process.env["ECONOMY_DB"];
677
1616
  return join(getDataDir(), "economy.db");
678
1617
  }
679
- function isSqliteBusyError(error) {
680
- const candidate = error;
681
- const code = typeof candidate.code === "string" ? candidate.code : "";
682
- const message = typeof candidate.message === "string" ? candidate.message : String(error);
683
- return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
684
- }
685
- function retryDelayMs(attempt) {
686
- return Math.min(1000, 50 * 2 ** attempt);
687
- }
688
- function withSqliteBusyRetry(operation, context) {
689
- const maxAttempts = 8;
690
- let lastError;
691
- for (let attempt = 0;attempt < maxAttempts; attempt++) {
692
- try {
693
- return operation();
694
- } catch (error) {
695
- if (!isSqliteBusyError(error))
696
- throw error;
697
- lastError = error;
698
- if (attempt === maxAttempts - 1)
699
- break;
700
- Bun.sleepSync(retryDelayMs(attempt));
701
- }
702
- }
703
- 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 });
704
- }
705
- function openDatabase(dbPath, skipSeed = false) {
706
- const path = dbPath ?? getDbPath();
707
- if (path !== ":memory:") {
708
- const dir = path.substring(0, path.lastIndexOf("/"));
709
- if (dir && !existsSync(dir))
710
- mkdirSync(dir, { recursive: true });
711
- }
712
- const db = withSqliteBusyRetry(() => {
713
- const opened = new SqliteAdapter(path);
714
- try {
715
- opened.exec("PRAGMA busy_timeout = 10000");
716
- opened.exec("PRAGMA journal_mode = WAL");
717
- opened.exec("PRAGMA foreign_keys = ON");
718
- initSchema(opened);
719
- return opened;
720
- } catch (error) {
721
- try {
722
- opened.close();
723
- } catch {}
724
- throw error;
725
- }
726
- }, `opening ${path}`);
727
- if (!skipSeed) {
728
- Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
729
- }
730
- return db;
731
- }
732
1618
  function quoteSqlIdent(identifier) {
733
1619
  return `"${identifier.replace(/"/g, '""')}"`;
734
1620
  }
@@ -750,6 +1636,7 @@ function addColumnIfMissing(db, table, column, definition) {
750
1636
  }
751
1637
  }
752
1638
  function initSchema(db) {
1639
+ db.exec(INGEST_SCHEMA_SQL);
753
1640
  db.exec(`
754
1641
  CREATE TABLE IF NOT EXISTS cost_centers (
755
1642
  id TEXT PRIMARY KEY,
@@ -941,6 +1828,7 @@ function initSchema(db) {
941
1828
  addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
942
1829
  addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
943
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'))`);
944
1832
  if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
945
1833
  db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
946
1834
  }
@@ -974,6 +1862,12 @@ function initSchema(db) {
974
1862
  CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
975
1863
  CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
976
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);
977
1871
  }
978
1872
  function upsertCostCenter(db, center) {
979
1873
  const updatedAt = center.updated_at ?? center.created_at;
@@ -984,7 +1878,7 @@ function upsertCostCenter(db, center) {
984
1878
  `).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
985
1879
  }
986
1880
  function upsertRequest(db, req) {
987
- const now = req.updated_at ?? new Date().toISOString();
1881
+ const now = req.updated_at ?? req.timestamp;
988
1882
  db.prepare(`
989
1883
  INSERT OR REPLACE INTO requests
990
1884
  (id, agent, session_id, model, input_tokens, output_tokens,
@@ -996,7 +1890,7 @@ function upsertRequest(db, req) {
996
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 ?? "");
997
1891
  }
998
1892
  function upsertSession(db, session) {
999
- const now = session.updated_at ?? new Date().toISOString();
1893
+ const now = session.updated_at ?? session.ended_at ?? session.started_at;
1000
1894
  db.prepare(`
1001
1895
  INSERT OR REPLACE INTO sessions
1002
1896
  (id, agent, project_path, project_name, started_at, ended_at,
@@ -1008,30 +1902,30 @@ function upsertSession(db, session) {
1008
1902
  function rollupSession(db, sessionId) {
1009
1903
  db.prepare(`
1010
1904
  UPDATE sessions SET
1011
- total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM requests WHERE session_id = ?),
1012
- total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM requests WHERE session_id = ?),
1013
- request_count = (SELECT COUNT(*) FROM requests WHERE session_id = ?),
1014
- 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 = ?),
1015
1909
  started_at = CASE WHEN started_at = '' OR started_at IS NULL
1016
- THEN (SELECT MIN(timestamp) FROM requests WHERE session_id = ?)
1910
+ THEN (SELECT MIN(timestamp) FROM economy_effective_requests WHERE session_id = ?)
1017
1911
  ELSE started_at END,
1018
1912
  account_key = CASE WHEN account_key = '' OR account_key IS NULL
1019
- 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), '')
1020
1914
  ELSE account_key END,
1021
1915
  account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
1022
- 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), '')
1023
1917
  ELSE account_tool END,
1024
1918
  account_name = CASE WHEN account_name = '' OR account_name IS NULL
1025
- 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), '')
1026
1920
  ELSE account_name END,
1027
1921
  account_email = CASE WHEN account_email = '' OR account_email IS NULL
1028
- 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), '')
1029
1923
  ELSE account_email END,
1030
1924
  account_source = CASE WHEN account_source = '' OR account_source IS NULL
1031
- 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), '')
1032
1926
  ELSE account_source END,
1033
1927
  cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
1034
- 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)
1035
1929
  ELSE cost_center_id END
1036
1930
  WHERE id = ?
1037
1931
  `).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
@@ -1067,32 +1961,91 @@ function seedModelPricing(db, defaults) {
1067
1961
  });
1068
1962
  }
1069
1963
  }
1964
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
1070
1965
  var init_database = __esm(() => {
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();
1973
+ PATHS_RESOLVER_KIND_ENV = {
1974
+ config: "HASNA_CONFIG_HOME",
1975
+ data: "HASNA_DATA_HOME",
1976
+ state: "HASNA_STATE_HOME",
1977
+ cache: "HASNA_CACHE_HOME"
1978
+ };
1979
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1980
+ });
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(() => {
1071
2038
  init_sqlite_adapter();
2039
+ init_database();
2040
+ init_database();
1072
2041
  });
1073
2042
 
1074
2043
  // src/otel/index.ts
1075
- init_database();
2044
+ init_sqlite_store();
1076
2045
 
1077
2046
  // src/ingest/otel.ts
1078
2047
  init_database();
1079
-
1080
- // src/lib/agents.ts
1081
- var AGENTS = [
1082
- "claude",
1083
- "takumi",
1084
- "codex",
1085
- "gemini",
1086
- "opencode",
1087
- "cursor",
1088
- "pi",
1089
- "hermes"
1090
- ];
1091
- function isAgent(value) {
1092
- return AGENTS.includes(value);
1093
- }
1094
-
1095
- // src/ingest/otel.ts
2048
+ init_agents();
1096
2049
  function attrValue(attr) {
1097
2050
  if (!attr?.value)
1098
2051
  return;
@@ -1413,19 +2366,1109 @@ async function ingestOtelRows(db, rows) {
1413
2366
  rollupSession(db, sessionId);
1414
2367
  return { requests, sessions: sessions.size };
1415
2368
  }
2369
+
2370
+ // src/lib/cloud-ingest.ts
2371
+ init_database();
2372
+ init_pricing();
2373
+
2374
+ // src/ingest/collection-outcome.ts
2375
+ import { AsyncLocalStorage } from "async_hooks";
2376
+ var context = new AsyncLocalStorage;
2377
+
2378
+ // src/ingest/claude.ts
2379
+ init_database();
2380
+ init_pricing();
2381
+ import { homedir as homedir3 } from "os";
2382
+ import { join as join3, basename } from "path";
2383
+
2384
+ // src/lib/accounts-store.ts
2385
+ import { homedir as homedir2 } from "os";
2386
+ import { join as join2 } from "path";
2387
+ import { z } from "zod";
2388
+ import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
2389
+
2390
+ // src/lib/cloud-storage.ts
2391
+ import {
2392
+ appConfigDiskValue,
2393
+ ClientTransportConfigurationError,
2394
+ clientTransportEnvKeys,
2395
+ createClientTransport,
2396
+ credentialDiskSources,
2397
+ credentialOverrideEnvKey,
2398
+ credentialPointerEnvKey,
2399
+ CREDENTIAL_PROFILE_ENV_KEY,
2400
+ keychainConfigValue
2401
+ } from "@hasna/contracts/client";
2402
+ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
2403
+ var ECONOMY_APP = "economy";
2404
+ var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
2405
+ function envFlagSet(env, key) {
2406
+ const raw = env[key];
2407
+ if (raw === undefined)
2408
+ return false;
2409
+ const value = raw.trim().toLowerCase();
2410
+ return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
2411
+ }
2412
+ function localStorageExplicitlyOptedIn(env) {
2413
+ return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
2414
+ }
2415
+ function economyAuthorityEnvKeys() {
2416
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2417
+ return [
2418
+ ...keys.apiUrlKeys,
2419
+ ...keys.apiKeyKeys,
2420
+ credentialOverrideEnvKey(ECONOMY_APP),
2421
+ credentialPointerEnvKey(ECONOMY_APP),
2422
+ CREDENTIAL_PROFILE_ENV_KEY
2423
+ ];
2424
+ }
2425
+ function hasEconomyEnvAuthorityIntent(env) {
2426
+ return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
2427
+ }
2428
+ function hostedAuthorityConfigured(env, options = {}) {
2429
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2430
+ if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
2431
+ return true;
2432
+ if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
2433
+ return true;
2434
+ return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
2435
+ }
2436
+ function economyResolverEnv(env) {
2437
+ const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
2438
+ if (blanks.length === 0)
2439
+ return env;
2440
+ const next = { ...env };
2441
+ for (const key of blanks)
2442
+ delete next[key];
2443
+ return next;
2444
+ }
2445
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2446
+ function isAmbientEconomyEnv(env) {
2447
+ if (typeof process !== "undefined" && env === process.env)
2448
+ return true;
2449
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
2450
+ }
2451
+ function economyResolverInputs(env, credentials = {}) {
2452
+ const normalised = economyResolverEnv(env);
2453
+ if (normalised === env)
2454
+ return { env: normalised, credentials };
2455
+ const keychain = { ...credentials.keychain };
2456
+ if (keychain.enabled === undefined && keychain.run === undefined) {
2457
+ keychain.enabled = isAmbientEconomyEnv(env);
2458
+ }
2459
+ return { env: normalised, credentials: { ...credentials, keychain } };
2460
+ }
2461
+ function resolveEconomyStorageClient(name, env, overrides = {}) {
2462
+ const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
2463
+ const wired = createClientTransport(name, resolverEnv, {
2464
+ credentials,
2465
+ ...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2466
+ ...overrides.headers ? { headers: overrides.headers } : {},
2467
+ ...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2468
+ ...overrides.retry !== undefined ? { retry: overrides.retry } : {},
2469
+ ...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2470
+ });
2471
+ return {
2472
+ transport: "http",
2473
+ client: createHasnaStorageClient(name, wired.client),
2474
+ resolution: wired.resolution
2475
+ };
2476
+ }
2477
+ function localEconomyNotice() {
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.`;
2479
+ }
2480
+ function credentialRequiredError(env, cause) {
2481
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2482
+ const clientEnv = env;
2483
+ const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
2484
+ const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
2485
+ return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
2486
+ }
2487
+ var cache;
2488
+ var localNoticePrinted = false;
2489
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
2490
+ const clientEnv = env;
2491
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
2492
+ return { active: false, client: null };
2493
+ }
2494
+ let resolved;
2495
+ try {
2496
+ resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
2497
+ } catch (error) {
2498
+ if (error instanceof ClientTransportConfigurationError) {
2499
+ throw credentialRequiredError(env, error);
2500
+ }
2501
+ throw error;
2502
+ }
2503
+ return { active: true, client: resolved.client };
2504
+ }
2505
+ function economyCloudStorage(env = process.env) {
2506
+ if (cache === undefined) {
2507
+ cache = resolveEconomyCloudStorage(env);
2508
+ if (!cache.active && !localNoticePrinted) {
2509
+ localNoticePrinted = true;
2510
+ if (typeof process !== "undefined")
2511
+ process.stderr.write(`${localEconomyNotice()}
2512
+ `);
2513
+ }
2514
+ }
2515
+ return cache;
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
+ }
1416
3458
  // package.json
1417
3459
  var package_default = {
1418
3460
  name: "@hasna/economy",
1419
- version: "0.3.28",
1420
- description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3461
+ version: "0.5.0",
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",
1421
3463
  repository: {
1422
3464
  type: "git",
1423
- url: "git+https://github.com/hasna/economy.git"
3465
+ url: "https://github.com/hasna/apps.git",
3466
+ directory: "apps/economy"
1424
3467
  },
1425
3468
  bugs: {
1426
- url: "https://github.com/hasna/economy/issues"
3469
+ url: "https://github.com/hasna/apps/issues"
1427
3470
  },
1428
- homepage: "https://github.com/hasna/economy#readme",
3471
+ homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
1429
3472
  type: "module",
1430
3473
  main: "dist/index.js",
1431
3474
  types: "dist/index.d.ts",
@@ -1448,22 +3491,21 @@ var package_default = {
1448
3491
  files: [
1449
3492
  "dist",
1450
3493
  "docs",
1451
- "dashboard/dist",
1452
3494
  "postinstall.js",
1453
3495
  "LICENSE",
1454
3496
  "README.md",
1455
3497
  "CHANGELOG.md",
1456
3498
  "SECURITY.md",
1457
3499
  "CONTRIBUTING.md",
1458
- "CODE_OF_CONDUCT.md"
3500
+ "CODE_OF_CONDUCT.md",
3501
+ "!dist/lib/test-hermetic-*"
1459
3502
  ],
1460
3503
  scripts: {
1461
- build: "cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && 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",
1462
- "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
1463
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
1464
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
1465
- "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
1466
- "build:dashboard": "cd dashboard && bun run build",
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]"',
1467
3509
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
1468
3510
  prepack: "bun run build && bun run artifact-scan",
1469
3511
  typecheck: "tsc --noEmit",
@@ -1493,21 +3535,18 @@ var package_default = {
1493
3535
  access: "public"
1494
3536
  },
1495
3537
  dependencies: {
1496
- "@hasna/accounts": "0.2.23",
1497
- "@hasna/agent-registry": "^0.1.0",
1498
- "@hasna/contracts": "0.13.4",
3538
+ "@hasna/contracts": "1.0.2",
1499
3539
  "@hasna/events": "^0.1.16",
1500
- "@hasna/mcp-harness": "^0.1.0",
1501
- "@hasna/paths": "0.1.0",
1502
3540
  "@hasna/projects": "1.0.0",
1503
3541
  "@modelcontextprotocol/sdk": "^1.12.1",
1504
3542
  chalk: "^5.4.1",
1505
3543
  commander: "^13.1.0",
1506
3544
  pg: "8.13.1",
1507
- zod: "^3.24.2"
3545
+ zod: "^3.24.2",
3546
+ "@hasna/trash": "0.2.3"
1508
3547
  },
1509
3548
  devDependencies: {
1510
- "@types/bun": "latest",
3549
+ "@types/bun": "1.3.14",
1511
3550
  "@types/pg": "^8.20.0",
1512
3551
  "bun-types": "latest",
1513
3552
  typescript: "^5.7.2"
@@ -1547,7 +3586,7 @@ if (args.includes("--version") || args.includes("-V")) {
1547
3586
  if (args.includes("--help") || args.includes("-h")) {
1548
3587
  console.log(`Usage: economy-otel [options]
1549
3588
 
1550
- OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into economy.db
3589
+ OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into Economy
1551
3590
 
1552
3591
  Options:
1553
3592
  -p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
@@ -1558,6 +3597,13 @@ Environment:
1558
3597
  ECONOMY_OTEL_PORT Override the default port (4318)
1559
3598
  ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
1560
3599
 
3600
+ Storage (decided at startup, before the listener binds):
3601
+ A credential resolved by @hasna/contracts (Keychain item
3602
+ hasna.credentials.economy.api-key, ~/.hasna/economy/config/credentials,
3603
+ HASNA_ECONOMY_API_KEY) forwards every accepted payload to the hosted
3604
+ API's /v1/ingest. HASNA_ECONOMY_LOCAL=1 writes the on-box SQLite store
3605
+ instead. With neither the sidecar fails closed (exit 1, no listener).
3606
+
1561
3607
  Endpoints:
1562
3608
  POST /v1/metrics OTLP JSON metrics
1563
3609
  POST /ingest Simplified single-event JSON
@@ -1565,7 +3611,28 @@ Endpoints:
1565
3611
  process.exit(0);
1566
3612
  }
1567
3613
  var port = resolvePort(args);
1568
- var db = openDatabase();
3614
+ function resolveStorage() {
3615
+ try {
3616
+ return economyCloudStorage();
3617
+ } catch (error) {
3618
+ console.error(`economy-otel: ${error instanceof Error ? error.message : String(error)}`);
3619
+ process.exit(1);
3620
+ }
3621
+ }
3622
+ var storage = resolveStorage();
3623
+ var localDb = storage.active ? null : openDatabase();
3624
+ async function forwardRows(cloud, rows) {
3625
+ const scratch = openDatabase(":memory:", true);
3626
+ try {
3627
+ const result = await ingestOtelRows(scratch, rows);
3628
+ const pushed = await pushIngestRows(cloud, scratch);
3629
+ return { ...result, forwarded: pushed.total };
3630
+ } finally {
3631
+ try {
3632
+ scratch.close();
3633
+ } catch {}
3634
+ }
3635
+ }
1569
3636
  var server = Bun.serve({
1570
3637
  port,
1571
3638
  hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
@@ -1590,7 +3657,16 @@ var server = Bun.serve({
1590
3657
  if (rows.length === 0) {
1591
3658
  return Response.json({ ingested: 0, message: "no matching metrics" });
1592
3659
  }
1593
- const result = await ingestOtelRows(db, rows);
3660
+ if (storage.active) {
3661
+ try {
3662
+ const result2 = await forwardRows(storage, rows);
3663
+ return Response.json({ ingested: result2.requests, sessions: result2.sessions, forwarded: result2.forwarded });
3664
+ } catch (error) {
3665
+ const reason = error instanceof Error ? error.message : String(error);
3666
+ return Response.json({ error: `forward to the economy API failed: ${reason}` }, { status: 502 });
3667
+ }
3668
+ }
3669
+ const result = await ingestOtelRows(localDb, rows);
1594
3670
  return Response.json({ ingested: result.requests, sessions: result.sessions });
1595
3671
  }
1596
3672
  });