@hasna/economy 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +12 -9
- package/dist/chunks/agent-registry-store-tpnhx3wj.js +39 -0
- package/dist/chunks/billing-bkpxy4kv.js +13 -0
- package/dist/chunks/config-j9mv3ehk.js +15 -0
- package/dist/chunks/index-2hwzmfgv.js +3407 -0
- package/dist/chunks/index-49f12css.js +124 -0
- package/dist/chunks/index-d480p2wh.js +1322 -0
- package/dist/chunks/index-ez9thsr7.js +3440 -0
- package/dist/chunks/index-hesxq90g.js +62 -0
- package/dist/chunks/index-k540hfww.js +258 -0
- package/dist/chunks/index-p35cyhc4.js +4 -0
- package/dist/chunks/index-r1jv3kpy.js +451 -0
- package/dist/chunks/index-w3bvv0ns.js +26 -0
- package/dist/chunks/index-xsah93s2.js +2085 -0
- package/dist/chunks/index-zpvea08j.js +3372 -0
- package/dist/chunks/menubar-6xtkt17h.js +296 -0
- package/dist/chunks/open-projects-6z4pq6mx.js +39 -0
- package/dist/chunks/pricing-f80p81gr.js +29 -0
- package/dist/chunks/pricing-vf2pycmh.js +30 -0
- package/dist/chunks/pricing-y21mcjsa.js +30 -0
- package/dist/chunks/serve-2knem98x.js +4655 -0
- package/dist/chunks/sqlite-store-501f32ts.js +258 -0
- package/dist/chunks/sqlite-store-fq4natnf.js +256 -0
- package/dist/chunks/sqlite-store-st99garx.js +258 -0
- package/dist/chunks/third-party-sqlite-5e85fxa2.js +11 -0
- package/dist/chunks/tui-khmnkat2.js +22 -0
- package/dist/chunks/watch-4y064yys.js +196 -0
- package/dist/chunks/webhooks-k4fh2t9c.js +109 -0
- package/dist/cli/commands/brief.d.ts.map +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/commands/journal.d.ts +3 -0
- package/dist/cli/commands/journal.d.ts.map +1 -0
- package/dist/cli/commands/menubar.d.ts +97 -4
- package/dist/cli/commands/menubar.d.ts.map +1 -1
- package/dist/cli/commands/tui.d.ts +1 -1
- package/dist/cli/commands/tui.d.ts.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/index.js +1358 -10114
- package/dist/db/cloud.d.ts +7 -9
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +14 -12
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/ingest-concurrency-worker.d.ts +2 -0
- package/dist/db/ingest-concurrency-worker.d.ts.map +1 -0
- package/dist/db/ingest-errors.d.ts +9 -0
- package/dist/db/ingest-errors.d.ts.map +1 -0
- package/dist/db/ingest-schema.d.ts +3 -0
- package/dist/db/ingest-schema.d.ts.map +1 -0
- package/dist/db/ingest-specs.d.ts +8 -0
- package/dist/db/ingest-specs.d.ts.map +1 -0
- package/dist/db/ingest-test-contract.d.ts +3 -0
- package/dist/db/ingest-test-contract.d.ts.map +1 -0
- package/dist/db/ingest-validation.d.ts +12 -0
- package/dist/db/ingest-validation.d.ts.map +1 -0
- package/dist/db/ingest.d.ts +27 -0
- package/dist/db/ingest.d.ts.map +1 -0
- package/dist/db/legacy-schema.d.ts +7 -0
- package/dist/db/legacy-schema.d.ts.map +1 -0
- package/dist/db/observation-contract.d.ts +3 -0
- package/dist/db/observation-contract.d.ts.map +1 -0
- package/dist/db/observation-provenance.d.ts +23 -0
- package/dist/db/observation-provenance.d.ts.map +1 -0
- package/dist/db/observation-schema.d.ts +24 -0
- package/dist/db/observation-schema.d.ts.map +1 -0
- package/dist/db/pg-connection.d.ts +10 -0
- package/dist/db/pg-connection.d.ts.map +1 -0
- package/dist/db/pg-legacy-schema.d.ts +12 -0
- package/dist/db/pg-legacy-schema.d.ts.map +1 -0
- package/dist/db/pg-migrate.d.ts +4 -9
- package/dist/db/pg-migrate.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/pg-protocol.d.ts +28 -0
- package/dist/db/pg-protocol.d.ts.map +1 -0
- package/dist/db/sqlite-store.d.ts +4 -0
- package/dist/db/sqlite-store.d.ts.map +1 -0
- package/dist/db/sync-pg.d.ts +17 -21
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/db/third-party-sqlite.d.ts +10 -0
- package/dist/db/third-party-sqlite.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1171 -2465
- package/dist/ingest/claude-quota.d.ts.map +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex-quota.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts +25 -0
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/ingest/collection-outcome.d.ts +17 -0
- package/dist/ingest/collection-outcome.d.ts.map +1 -0
- package/dist/ingest/cursor.d.ts.map +1 -1
- package/dist/ingest/gemini.d.ts.map +1 -1
- package/dist/ingest/hermes.d.ts +1 -1
- package/dist/ingest/hermes.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/ingest/opencode.d.ts +1 -1
- package/dist/ingest/opencode.d.ts.map +1 -1
- package/dist/ingest/pi.d.ts +1 -1
- package/dist/ingest/pi.d.ts.map +1 -1
- package/dist/ingest/quota-observation.d.ts +16 -0
- package/dist/ingest/quota-observation.d.ts.map +1 -0
- package/dist/ingest/source-time.d.ts +4 -0
- package/dist/ingest/source-time.d.ts.map +1 -0
- package/dist/lib/accounts-store.d.ts +39 -13
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +1 -1
- package/dist/lib/autosync-gate.d.ts +12 -5
- package/dist/lib/autosync-gate.d.ts.map +1 -1
- package/dist/lib/billing-diff.d.ts +18 -40
- package/dist/lib/billing-diff.d.ts.map +1 -1
- package/dist/lib/brief.d.ts +2 -0
- package/dist/lib/brief.d.ts.map +1 -1
- package/dist/lib/budget-decimal.d.ts +14 -0
- package/dist/lib/budget-decimal.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/ingest-batches.d.ts +21 -0
- package/dist/lib/ingest-batches.d.ts.map +1 -0
- package/dist/lib/ingest-journal-client.d.ts +37 -0
- package/dist/lib/ingest-journal-client.d.ts.map +1 -0
- package/dist/lib/ingest-journal-lock.d.ts +5 -0
- package/dist/lib/ingest-journal-lock.d.ts.map +1 -0
- package/dist/lib/ingest-journal-maintenance.d.ts +92 -0
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -0
- package/dist/lib/ingest-journal-operation.d.ts +55 -0
- package/dist/lib/ingest-journal-operation.d.ts.map +1 -0
- package/dist/lib/ingest-journal.d.ts +73 -0
- package/dist/lib/ingest-journal.d.ts.map +1 -0
- package/dist/lib/money-display.d.ts +13 -0
- package/dist/lib/money-display.d.ts.map +1 -0
- package/dist/lib/periods.d.ts +12 -0
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/pricing.d.ts +14 -0
- package/dist/lib/pricing.d.ts.map +1 -1
- package/dist/lib/savings.d.ts +64 -7
- package/dist/lib/savings.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +25 -21
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/summary-contract.d.ts +32 -0
- package/dist/lib/summary-contract.d.ts.map +1 -0
- package/dist/lib/summary-test-contract.d.ts +3 -0
- package/dist/lib/summary-test-contract.d.ts.map +1 -0
- package/dist/lib/sync-all.d.ts +4 -0
- package/dist/lib/sync-all.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts.map +1 -1
- package/dist/lib/sync-report.d.ts +4 -0
- package/dist/lib/sync-report.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +9 -0
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -0
- package/dist/mcp/agent-registry-store.d.ts +30 -0
- package/dist/mcp/agent-registry-store.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +9 -16
- package/dist/mcp/agent-registry.d.ts.map +1 -1
- package/dist/mcp/index.js +2291 -3458
- package/dist/mcp/ingest-journal-tool.d.ts +2 -0
- package/dist/mcp/ingest-journal-tool.d.ts.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/otel/index.js +1960 -289
- package/dist/server/human-auth.d.ts +20 -0
- package/dist/server/human-auth.d.ts.map +1 -0
- package/dist/server/index.js +10639 -6720
- package/dist/server/legacy-inventory.d.ts +52 -0
- package/dist/server/legacy-inventory.d.ts.map +1 -0
- package/dist/server/legacy-operations.d.ts +64 -0
- package/dist/server/legacy-operations.d.ts.map +1 -0
- package/dist/server/legacy-plan.d.ts +55 -0
- package/dist/server/legacy-plan.d.ts.map +1 -0
- package/dist/server/legacy-preservation.d.ts +58 -0
- package/dist/server/legacy-preservation.d.ts.map +1 -0
- package/dist/server/pg-sync-worker.js +195 -21
- package/dist/server/serve.d.ts +20 -16
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/service-admission.d.ts +10 -0
- package/dist/server/service-admission.d.ts.map +1 -0
- package/dist/server/service-dispatcher.d.ts +45 -0
- package/dist/server/service-dispatcher.d.ts.map +1 -0
- package/dist/server/service-operations.d.ts +5 -0
- package/dist/server/service-operations.d.ts.map +1 -0
- package/dist/server/service-protocol.d.ts +50 -0
- package/dist/server/service-protocol.d.ts.map +1 -0
- package/dist/server/service-worker.d.ts +2 -0
- package/dist/server/service-worker.d.ts.map +1 -0
- package/dist/server/service-worker.js +8038 -0
- package/dist/types/index.d.ts +66 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/README.md +1 -1
- package/docs/cli.md +1 -1
- package/docs/configuration.md +5 -5
- package/docs/cost-reporting.md +49 -0
- package/docs/historical-data-reconciliation.md +203 -0
- package/docs/ingest-journal.md +108 -0
- package/docs/ingestion.md +60 -4
- package/docs/mcp.md +2 -2
- package/docs/native-budget-save.md +84 -0
- package/docs/native-human-auth.md +102 -0
- package/docs/postgresql-legacy-schema-repair.md +30 -0
- package/docs/rest-api.md +2 -2
- package/package.json +25 -13
- package/dist/lib/test-hermetic-accounts.d.ts +0 -7
- package/dist/lib/test-hermetic-accounts.d.ts.map +0 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +0 -4
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +0 -1
package/dist/otel/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
|
290
|
-
|
|
1156
|
+
return { usd: null, pricing: "missing" };
|
|
1157
|
+
if (Object.values(pricing).some((value) => value !== undefined && (!Number.isFinite(value) || value < 0))) {
|
|
1158
|
+
throw new RangeError("Model pricing rates must be finite and nonnegative");
|
|
1159
|
+
}
|
|
1160
|
+
const usd = computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours);
|
|
1161
|
+
if (!Number.isFinite(usd))
|
|
1162
|
+
throw new RangeError("Cost estimate exceeds the supported numeric range");
|
|
1163
|
+
return { usd, pricing: configured ? "configured" : "catalog" };
|
|
291
1164
|
}
|
|
292
1165
|
function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens, cacheStorageTokenHours) {
|
|
293
1166
|
if (isFreeModel(model))
|
|
@@ -308,7 +1181,7 @@ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cache
|
|
|
308
1181
|
}
|
|
309
1182
|
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
310
1183
|
}
|
|
311
|
-
var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS;
|
|
1184
|
+
var DEFAULT_PRICING, LEGACY_DEFAULT_PRICING, ADDITIONAL_LEGACY_DEFAULT_PRICING, REMOVED_DEFAULT_PRICING, FREE_PRICING, GEMINI_PROMPT_TIERS, OPENAI_PROMPT_TIERS, QWEN_PROMPT_TIERS, MINIMAX_PROMPT_TIERS, XAI_PROMPT_TIERS, UnknownModelPricingError;
|
|
312
1185
|
var init_pricing = __esm(() => {
|
|
313
1186
|
init_database();
|
|
314
1187
|
DEFAULT_PRICING = {
|
|
@@ -587,6 +1460,17 @@ var init_pricing = __esm(() => {
|
|
|
587
1460
|
cacheReadPer1M: 0
|
|
588
1461
|
}
|
|
589
1462
|
};
|
|
1463
|
+
UnknownModelPricingError = class UnknownModelPricingError extends Error {
|
|
1464
|
+
code = "MODEL_PRICING_UNAVAILABLE";
|
|
1465
|
+
constructor() {
|
|
1466
|
+
super("MODEL_PRICING_UNAVAILABLE: Pricing unavailable for model; configure its rates before estimating cost");
|
|
1467
|
+
this.name = "UnknownModelPricingError";
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
});
|
|
1471
|
+
// src/lib/summary-contract.ts
|
|
1472
|
+
var init_summary_contract = __esm(() => {
|
|
1473
|
+
init_pricing();
|
|
590
1474
|
});
|
|
591
1475
|
|
|
592
1476
|
// src/db/database.ts
|
|
@@ -648,6 +1532,9 @@ function pathsResolverResolve(kind, options) {
|
|
|
648
1532
|
function dataDir(options) {
|
|
649
1533
|
return pathsResolverResolve("data", options);
|
|
650
1534
|
}
|
|
1535
|
+
function cacheDir(options) {
|
|
1536
|
+
return pathsResolverResolve("cache", options);
|
|
1537
|
+
}
|
|
651
1538
|
function normalizeMachineId(value) {
|
|
652
1539
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
653
1540
|
return id && id.length > 0 ? id : null;
|
|
@@ -728,59 +1615,6 @@ function getDbPath() {
|
|
|
728
1615
|
return process.env["ECONOMY_DB"];
|
|
729
1616
|
return join(getDataDir(), "economy.db");
|
|
730
1617
|
}
|
|
731
|
-
function isSqliteBusyError(error) {
|
|
732
|
-
const candidate = error;
|
|
733
|
-
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
734
|
-
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
735
|
-
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
736
|
-
}
|
|
737
|
-
function retryDelayMs(attempt) {
|
|
738
|
-
return Math.min(1000, 50 * 2 ** attempt);
|
|
739
|
-
}
|
|
740
|
-
function withSqliteBusyRetry(operation, context) {
|
|
741
|
-
const maxAttempts = 8;
|
|
742
|
-
let lastError;
|
|
743
|
-
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
744
|
-
try {
|
|
745
|
-
return operation();
|
|
746
|
-
} catch (error) {
|
|
747
|
-
if (!isSqliteBusyError(error))
|
|
748
|
-
throw error;
|
|
749
|
-
lastError = error;
|
|
750
|
-
if (attempt === maxAttempts - 1)
|
|
751
|
-
break;
|
|
752
|
-
Bun.sleepSync(retryDelayMs(attempt));
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
|
|
756
|
-
}
|
|
757
|
-
function openDatabase(dbPath, skipSeed = false) {
|
|
758
|
-
const path = dbPath ?? getDbPath();
|
|
759
|
-
if (path !== ":memory:") {
|
|
760
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
761
|
-
if (dir && !existsSync(dir))
|
|
762
|
-
mkdirSync(dir, { recursive: true });
|
|
763
|
-
}
|
|
764
|
-
const db = withSqliteBusyRetry(() => {
|
|
765
|
-
const opened = new SqliteAdapter(path);
|
|
766
|
-
try {
|
|
767
|
-
opened.exec("PRAGMA busy_timeout = 10000");
|
|
768
|
-
opened.exec("PRAGMA journal_mode = WAL");
|
|
769
|
-
opened.exec("PRAGMA foreign_keys = ON");
|
|
770
|
-
initSchema(opened);
|
|
771
|
-
return opened;
|
|
772
|
-
} catch (error) {
|
|
773
|
-
try {
|
|
774
|
-
opened.close();
|
|
775
|
-
} catch {}
|
|
776
|
-
throw error;
|
|
777
|
-
}
|
|
778
|
-
}, `opening ${path}`);
|
|
779
|
-
if (!skipSeed) {
|
|
780
|
-
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
781
|
-
}
|
|
782
|
-
return db;
|
|
783
|
-
}
|
|
784
1618
|
function quoteSqlIdent(identifier) {
|
|
785
1619
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
786
1620
|
}
|
|
@@ -802,6 +1636,7 @@ function addColumnIfMissing(db, table, column, definition) {
|
|
|
802
1636
|
}
|
|
803
1637
|
}
|
|
804
1638
|
function initSchema(db) {
|
|
1639
|
+
db.exec(INGEST_SCHEMA_SQL);
|
|
805
1640
|
db.exec(`
|
|
806
1641
|
CREATE TABLE IF NOT EXISTS cost_centers (
|
|
807
1642
|
id TEXT PRIMARY KEY,
|
|
@@ -993,6 +1828,7 @@ function initSchema(db) {
|
|
|
993
1828
|
addColumnIfMissing(db, "requests", "cost_center_id", `TEXT`);
|
|
994
1829
|
addColumnIfMissing(db, "sessions", "cost_center_id", `TEXT`);
|
|
995
1830
|
addColumnIfMissing(db, "budgets", "cost_center_id", `TEXT`);
|
|
1831
|
+
addColumnIfMissing(db, "budgets", "metric", `TEXT NOT NULL DEFAULT 'legacy_request_cost_usd' CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`);
|
|
996
1832
|
if (addColumnIfMissing(db, "requests", "cache_create_5m_tokens", "INTEGER DEFAULT 0")) {
|
|
997
1833
|
db.exec(`UPDATE requests SET cache_create_5m_tokens = cache_create_tokens WHERE cache_create_5m_tokens = 0`);
|
|
998
1834
|
}
|
|
@@ -1026,6 +1862,12 @@ function initSchema(db) {
|
|
|
1026
1862
|
CREATE INDEX IF NOT EXISTS idx_budgets_cost_center ON budgets(cost_center_id);
|
|
1027
1863
|
CREATE INDEX IF NOT EXISTS idx_cost_centers_kind ON cost_centers(kind);
|
|
1028
1864
|
`);
|
|
1865
|
+
for (const [table, columns] of Object.entries(OBSERVATION_COLUMNS)) {
|
|
1866
|
+
for (const [column, type] of Object.entries(columns))
|
|
1867
|
+
addColumnIfMissing(db, table, column, type);
|
|
1868
|
+
}
|
|
1869
|
+
db.exec(SCAN_RECEIPTS_SQL);
|
|
1870
|
+
db.exec(LEGACY_SQLITE_SQL);
|
|
1029
1871
|
}
|
|
1030
1872
|
function upsertCostCenter(db, center) {
|
|
1031
1873
|
const updatedAt = center.updated_at ?? center.created_at;
|
|
@@ -1036,7 +1878,7 @@ function upsertCostCenter(db, center) {
|
|
|
1036
1878
|
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
1037
1879
|
}
|
|
1038
1880
|
function upsertRequest(db, req) {
|
|
1039
|
-
const now = req.updated_at ??
|
|
1881
|
+
const now = req.updated_at ?? req.timestamp;
|
|
1040
1882
|
db.prepare(`
|
|
1041
1883
|
INSERT OR REPLACE INTO requests
|
|
1042
1884
|
(id, agent, session_id, model, input_tokens, output_tokens,
|
|
@@ -1048,7 +1890,7 @@ function upsertRequest(db, req) {
|
|
|
1048
1890
|
`).run(req.id, req.agent, req.session_id, req.model, req.input_tokens, req.output_tokens, req.cache_read_tokens, req.cache_create_tokens, req.cache_create_5m_tokens ?? req.cache_create_tokens, req.cache_create_1h_tokens ?? 0, req.cost_usd, req.cost_basis ?? "estimated", req.duration_ms, req.timestamp, req.source_request_id, req.machine_id ?? "", req.cost_center_id ?? null, req.attribution_tag ?? process.env["ECONOMY_TAG"] ?? "", req.account_key ?? "", req.account_tool ?? "", req.account_name ?? "", req.account_email ?? "", req.account_source ?? "", now, req.synced_at ?? "");
|
|
1049
1891
|
}
|
|
1050
1892
|
function upsertSession(db, session) {
|
|
1051
|
-
const now = session.updated_at ??
|
|
1893
|
+
const now = session.updated_at ?? session.ended_at ?? session.started_at;
|
|
1052
1894
|
db.prepare(`
|
|
1053
1895
|
INSERT OR REPLACE INTO sessions
|
|
1054
1896
|
(id, agent, project_path, project_name, started_at, ended_at,
|
|
@@ -1060,30 +1902,30 @@ function upsertSession(db, session) {
|
|
|
1060
1902
|
function rollupSession(db, sessionId) {
|
|
1061
1903
|
db.prepare(`
|
|
1062
1904
|
UPDATE sessions SET
|
|
1063
|
-
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM
|
|
1064
|
-
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM
|
|
1065
|
-
request_count = (SELECT COUNT(*) FROM
|
|
1066
|
-
ended_at = (SELECT MAX(timestamp) FROM
|
|
1905
|
+
total_cost_usd = (SELECT COALESCE(SUM(cost_usd), 0) FROM economy_effective_requests WHERE session_id = ?),
|
|
1906
|
+
total_tokens = (SELECT COALESCE(SUM(input_tokens + output_tokens + cache_read_tokens + cache_create_tokens), 0) FROM economy_effective_requests WHERE session_id = ?),
|
|
1907
|
+
request_count = (SELECT COUNT(*) FROM economy_effective_requests WHERE session_id = ?),
|
|
1908
|
+
ended_at = (SELECT MAX(timestamp) FROM economy_effective_requests WHERE session_id = ?),
|
|
1067
1909
|
started_at = CASE WHEN started_at = '' OR started_at IS NULL
|
|
1068
|
-
THEN (SELECT MIN(timestamp) FROM
|
|
1910
|
+
THEN (SELECT MIN(timestamp) FROM economy_effective_requests WHERE session_id = ?)
|
|
1069
1911
|
ELSE started_at END,
|
|
1070
1912
|
account_key = CASE WHEN account_key = '' OR account_key IS NULL
|
|
1071
|
-
THEN COALESCE((SELECT account_key FROM
|
|
1913
|
+
THEN COALESCE((SELECT account_key FROM economy_effective_requests WHERE session_id = ? AND account_key != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1072
1914
|
ELSE account_key END,
|
|
1073
1915
|
account_tool = CASE WHEN account_tool = '' OR account_tool IS NULL
|
|
1074
|
-
THEN COALESCE((SELECT account_tool FROM
|
|
1916
|
+
THEN COALESCE((SELECT account_tool FROM economy_effective_requests WHERE session_id = ? AND account_tool != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1075
1917
|
ELSE account_tool END,
|
|
1076
1918
|
account_name = CASE WHEN account_name = '' OR account_name IS NULL
|
|
1077
|
-
THEN COALESCE((SELECT account_name FROM
|
|
1919
|
+
THEN COALESCE((SELECT account_name FROM economy_effective_requests WHERE session_id = ? AND account_name != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1078
1920
|
ELSE account_name END,
|
|
1079
1921
|
account_email = CASE WHEN account_email = '' OR account_email IS NULL
|
|
1080
|
-
THEN COALESCE((SELECT account_email FROM
|
|
1922
|
+
THEN COALESCE((SELECT account_email FROM economy_effective_requests WHERE session_id = ? AND account_email != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1081
1923
|
ELSE account_email END,
|
|
1082
1924
|
account_source = CASE WHEN account_source = '' OR account_source IS NULL
|
|
1083
|
-
THEN COALESCE((SELECT account_source FROM
|
|
1925
|
+
THEN COALESCE((SELECT account_source FROM economy_effective_requests WHERE session_id = ? AND account_source != '' ORDER BY timestamp DESC LIMIT 1), '')
|
|
1084
1926
|
ELSE account_source END,
|
|
1085
1927
|
cost_center_id = CASE WHEN cost_center_id = '' OR cost_center_id IS NULL
|
|
1086
|
-
THEN COALESCE((SELECT cost_center_id FROM
|
|
1928
|
+
THEN COALESCE((SELECT cost_center_id FROM economy_effective_requests WHERE session_id = ? AND cost_center_id IS NOT NULL AND cost_center_id != '' ORDER BY timestamp DESC LIMIT 1), NULL)
|
|
1087
1929
|
ELSE cost_center_id END
|
|
1088
1930
|
WHERE id = ?
|
|
1089
1931
|
`).run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
|
|
@@ -1121,7 +1963,13 @@ function seedModelPricing(db, defaults) {
|
|
|
1121
1963
|
}
|
|
1122
1964
|
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
1123
1965
|
var init_database = __esm(() => {
|
|
1124
|
-
|
|
1966
|
+
init_observation_schema();
|
|
1967
|
+
init_observation_provenance();
|
|
1968
|
+
init_ingest_validation();
|
|
1969
|
+
init_legacy_schema();
|
|
1970
|
+
init_ingest();
|
|
1971
|
+
init_ingest_errors();
|
|
1972
|
+
init_summary_contract();
|
|
1125
1973
|
PATHS_RESOLVER_KIND_ENV = {
|
|
1126
1974
|
config: "HASNA_CONFIG_HOME",
|
|
1127
1975
|
data: "HASNA_DATA_HOME",
|
|
@@ -1131,28 +1979,73 @@ var init_database = __esm(() => {
|
|
|
1131
1979
|
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1132
1980
|
});
|
|
1133
1981
|
|
|
1982
|
+
// src/db/sqlite-store.ts
|
|
1983
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1984
|
+
function isSqliteBusyError(error) {
|
|
1985
|
+
const candidate = error;
|
|
1986
|
+
const code = typeof candidate.code === "string" ? candidate.code : "";
|
|
1987
|
+
const message = typeof candidate.message === "string" ? candidate.message : String(error);
|
|
1988
|
+
return code === "SQLITE_BUSY" || code === "SQLITE_BUSY_RECOVERY" || /database is locked|SQLITE_BUSY/i.test(message);
|
|
1989
|
+
}
|
|
1990
|
+
function retryDelayMs(attempt) {
|
|
1991
|
+
return Math.min(1000, 50 * 2 ** attempt);
|
|
1992
|
+
}
|
|
1993
|
+
function withSqliteBusyRetry(operation, context) {
|
|
1994
|
+
const maxAttempts = 8;
|
|
1995
|
+
let lastError;
|
|
1996
|
+
for (let attempt = 0;attempt < maxAttempts; attempt++) {
|
|
1997
|
+
try {
|
|
1998
|
+
return operation();
|
|
1999
|
+
} catch (error) {
|
|
2000
|
+
if (!isSqliteBusyError(error))
|
|
2001
|
+
throw error;
|
|
2002
|
+
lastError = error;
|
|
2003
|
+
if (attempt === maxAttempts - 1)
|
|
2004
|
+
break;
|
|
2005
|
+
Bun.sleepSync(retryDelayMs(attempt));
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
throw new Error(`SQLite database is locked after ${maxAttempts} attempts while ${context}. Another economy sync/merge may be recovering the database; retry shortly.`, { cause: lastError });
|
|
2009
|
+
}
|
|
2010
|
+
function openDatabase(dbPath, skipSeed = false) {
|
|
2011
|
+
const path = dbPath ?? getDbPath();
|
|
2012
|
+
if (path !== ":memory:") {
|
|
2013
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
2014
|
+
if (dir && !existsSync2(dir))
|
|
2015
|
+
mkdirSync2(dir, { recursive: true });
|
|
2016
|
+
}
|
|
2017
|
+
const db = withSqliteBusyRetry(() => {
|
|
2018
|
+
const opened = new SqliteAdapter(path);
|
|
2019
|
+
try {
|
|
2020
|
+
opened.exec("PRAGMA busy_timeout = 10000");
|
|
2021
|
+
opened.exec("PRAGMA journal_mode = WAL");
|
|
2022
|
+
opened.exec("PRAGMA foreign_keys = ON");
|
|
2023
|
+
initSchema(opened);
|
|
2024
|
+
return opened;
|
|
2025
|
+
} catch (error) {
|
|
2026
|
+
try {
|
|
2027
|
+
opened.close();
|
|
2028
|
+
} catch {}
|
|
2029
|
+
throw error;
|
|
2030
|
+
}
|
|
2031
|
+
}, `opening ${path}`);
|
|
2032
|
+
if (!skipSeed) {
|
|
2033
|
+
Promise.resolve().then(() => (init_pricing(), exports_pricing)).then(({ ensurePricingSeeded: ensurePricingSeeded2 }) => ensurePricingSeeded2(db)).catch(() => {});
|
|
2034
|
+
}
|
|
2035
|
+
return db;
|
|
2036
|
+
}
|
|
2037
|
+
var init_sqlite_store = __esm(() => {
|
|
2038
|
+
init_sqlite_adapter();
|
|
2039
|
+
init_database();
|
|
2040
|
+
init_database();
|
|
2041
|
+
});
|
|
2042
|
+
|
|
1134
2043
|
// src/otel/index.ts
|
|
1135
|
-
|
|
2044
|
+
init_sqlite_store();
|
|
1136
2045
|
|
|
1137
2046
|
// src/ingest/otel.ts
|
|
1138
2047
|
init_database();
|
|
1139
|
-
|
|
1140
|
-
// src/lib/agents.ts
|
|
1141
|
-
var AGENTS = [
|
|
1142
|
-
"claude",
|
|
1143
|
-
"takumi",
|
|
1144
|
-
"codex",
|
|
1145
|
-
"gemini",
|
|
1146
|
-
"opencode",
|
|
1147
|
-
"cursor",
|
|
1148
|
-
"pi",
|
|
1149
|
-
"hermes"
|
|
1150
|
-
];
|
|
1151
|
-
function isAgent(value) {
|
|
1152
|
-
return AGENTS.includes(value);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
// src/ingest/otel.ts
|
|
2048
|
+
init_agents();
|
|
1156
2049
|
function attrValue(attr) {
|
|
1157
2050
|
if (!attr?.value)
|
|
1158
2051
|
return;
|
|
@@ -1478,6 +2371,10 @@ async function ingestOtelRows(db, rows) {
|
|
|
1478
2371
|
init_database();
|
|
1479
2372
|
init_pricing();
|
|
1480
2373
|
|
|
2374
|
+
// src/ingest/collection-outcome.ts
|
|
2375
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2376
|
+
var context = new AsyncLocalStorage;
|
|
2377
|
+
|
|
1481
2378
|
// src/ingest/claude.ts
|
|
1482
2379
|
init_database();
|
|
1483
2380
|
init_pricing();
|
|
@@ -1489,172 +2386,6 @@ import { homedir as homedir2 } from "os";
|
|
|
1489
2386
|
import { join as join2 } from "path";
|
|
1490
2387
|
import { z } from "zod";
|
|
1491
2388
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
1492
|
-
var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
1493
|
-
var profileSchema = z.object({
|
|
1494
|
-
name: z.string(),
|
|
1495
|
-
tool: z.string(),
|
|
1496
|
-
email: z.string().optional(),
|
|
1497
|
-
displayName: z.string().optional(),
|
|
1498
|
-
dir: z.string(),
|
|
1499
|
-
description: z.string().optional(),
|
|
1500
|
-
createdAt: z.string(),
|
|
1501
|
-
lastUsedAt: z.string().optional()
|
|
1502
|
-
});
|
|
1503
|
-
var toolDefSchema = z.object({
|
|
1504
|
-
id: z.string(),
|
|
1505
|
-
label: z.string(),
|
|
1506
|
-
envVar: z.string(),
|
|
1507
|
-
defaultDir: z.string(),
|
|
1508
|
-
bin: z.string()
|
|
1509
|
-
});
|
|
1510
|
-
var storeSchema = z.object({
|
|
1511
|
-
version: z.literal(1),
|
|
1512
|
-
current: z.record(z.string(), z.string()).default({}),
|
|
1513
|
-
applied: z.record(z.string(), z.string()).default({}),
|
|
1514
|
-
toolLocks: z.record(z.string(), z.string()).default({}),
|
|
1515
|
-
profiles: z.array(profileSchema).default([]),
|
|
1516
|
-
tools: z.array(toolDefSchema).default([])
|
|
1517
|
-
});
|
|
1518
|
-
var BUILTIN_TOOLS = [
|
|
1519
|
-
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
1520
|
-
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
1521
|
-
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
|
|
1522
|
-
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
|
|
1523
|
-
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
|
|
1524
|
-
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
|
|
1525
|
-
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
|
|
1526
|
-
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
|
|
1527
|
-
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
|
|
1528
|
-
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
|
|
1529
|
-
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
|
|
1530
|
-
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
|
|
1531
|
-
];
|
|
1532
|
-
|
|
1533
|
-
// src/ingest/claude.ts
|
|
1534
|
-
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
|
|
1535
|
-
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
1536
|
-
|
|
1537
|
-
// src/ingest/codex.ts
|
|
1538
|
-
import { homedir as homedir4 } from "os";
|
|
1539
|
-
import { join as join4, basename as basename2 } from "path";
|
|
1540
|
-
init_database();
|
|
1541
|
-
init_pricing();
|
|
1542
|
-
var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
|
|
1543
|
-
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
|
|
1544
|
-
var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
|
|
1545
|
-
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
|
|
1546
|
-
|
|
1547
|
-
// src/ingest/gemini.ts
|
|
1548
|
-
init_database();
|
|
1549
|
-
init_pricing();
|
|
1550
|
-
import { homedir as homedir5 } from "os";
|
|
1551
|
-
import { join as join5, basename as basename3 } from "path";
|
|
1552
|
-
var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
|
|
1553
|
-
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
|
|
1554
|
-
|
|
1555
|
-
// src/ingest/opencode.ts
|
|
1556
|
-
init_database();
|
|
1557
|
-
init_pricing();
|
|
1558
|
-
import { homedir as homedir6 } from "os";
|
|
1559
|
-
import { join as join6 } from "path";
|
|
1560
|
-
var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
|
|
1561
|
-
|
|
1562
|
-
// src/ingest/cursor.ts
|
|
1563
|
-
init_database();
|
|
1564
|
-
|
|
1565
|
-
// src/ingest/pi.ts
|
|
1566
|
-
init_database();
|
|
1567
|
-
import { homedir as homedir7 } from "os";
|
|
1568
|
-
import { join as join7 } from "path";
|
|
1569
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
|
|
1570
|
-
|
|
1571
|
-
// src/ingest/hermes.ts
|
|
1572
|
-
init_database();
|
|
1573
|
-
import { homedir as homedir8 } from "os";
|
|
1574
|
-
import { join as join8 } from "path";
|
|
1575
|
-
var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
|
|
1576
|
-
|
|
1577
|
-
// src/ingest/loops.ts
|
|
1578
|
-
init_database();
|
|
1579
|
-
init_pricing();
|
|
1580
|
-
|
|
1581
|
-
// src/ingest/claude-quota.ts
|
|
1582
|
-
init_database();
|
|
1583
|
-
|
|
1584
|
-
// src/lib/paths.ts
|
|
1585
|
-
import { homedir as homedir9 } from "os";
|
|
1586
|
-
import { join as join9 } from "path";
|
|
1587
|
-
function getHomeDir2() {
|
|
1588
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
1589
|
-
}
|
|
1590
|
-
function agentPaths() {
|
|
1591
|
-
const home = getHomeDir2();
|
|
1592
|
-
return {
|
|
1593
|
-
claudeProjects: join9(home, ".claude", "projects"),
|
|
1594
|
-
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
1595
|
-
takumiProjects: join9(home, ".takumi", "projects"),
|
|
1596
|
-
codexDir: join9(home, ".codex"),
|
|
1597
|
-
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
1598
|
-
codexAuth: join9(home, ".codex", "auth.json"),
|
|
1599
|
-
codexConfig: join9(home, ".codex", "config.toml"),
|
|
1600
|
-
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
1601
|
-
geminiHistory: join9(home, ".gemini", "history"),
|
|
1602
|
-
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
1603
|
-
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
1604
|
-
hermesDir: join9(home, ".hermes"),
|
|
1605
|
-
hermesDb: join9(home, ".hermes", "state.db")
|
|
1606
|
-
};
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
// src/ingest/claude-quota.ts
|
|
1610
|
-
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
1611
|
-
|
|
1612
|
-
// src/ingest/codex-quota.ts
|
|
1613
|
-
init_database();
|
|
1614
|
-
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
1615
|
-
|
|
1616
|
-
// src/lib/sync-all.ts
|
|
1617
|
-
init_database();
|
|
1618
|
-
|
|
1619
|
-
// src/ingest/billing.ts
|
|
1620
|
-
init_database();
|
|
1621
|
-
|
|
1622
|
-
// src/lib/sync-maintenance.ts
|
|
1623
|
-
init_database();
|
|
1624
|
-
|
|
1625
|
-
// src/lib/cloud-ingest.ts
|
|
1626
|
-
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
|
|
1627
|
-
function exportIngestRows(db) {
|
|
1628
|
-
const body = {};
|
|
1629
|
-
for (const table of CLOUD_INGEST_TABLES) {
|
|
1630
|
-
const rows = db.prepare(`SELECT * FROM ${table}`).all();
|
|
1631
|
-
if (rows.length > 0)
|
|
1632
|
-
body[table] = rows;
|
|
1633
|
-
}
|
|
1634
|
-
return body;
|
|
1635
|
-
}
|
|
1636
|
-
function unwrapEnvelope(raw) {
|
|
1637
|
-
if (raw && typeof raw === "object" && "data" in raw) {
|
|
1638
|
-
const data = raw.data;
|
|
1639
|
-
return data && typeof data === "object" ? data : {};
|
|
1640
|
-
}
|
|
1641
|
-
return raw ?? {};
|
|
1642
|
-
}
|
|
1643
|
-
async function postIngest(cloud, body) {
|
|
1644
|
-
const raw = await cloud.client.transport.post("/ingest", body);
|
|
1645
|
-
const payload = unwrapEnvelope(raw);
|
|
1646
|
-
return {
|
|
1647
|
-
ingested: payload["ingested"] ?? {},
|
|
1648
|
-
total: Number(payload["total"] ?? 0)
|
|
1649
|
-
};
|
|
1650
|
-
}
|
|
1651
|
-
async function pushIngestRows(cloud, scratch) {
|
|
1652
|
-
const body = exportIngestRows(scratch);
|
|
1653
|
-
if (Object.keys(body).length === 0)
|
|
1654
|
-
return { posted: false, ingested: {}, total: 0 };
|
|
1655
|
-
const response = await postIngest(cloud, body);
|
|
1656
|
-
return { posted: true, ingested: response.ingested, total: response.total };
|
|
1657
|
-
}
|
|
1658
2389
|
|
|
1659
2390
|
// src/lib/cloud-storage.ts
|
|
1660
2391
|
import {
|
|
@@ -1670,18 +2401,6 @@ import {
|
|
|
1670
2401
|
} from "@hasna/contracts/client";
|
|
1671
2402
|
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
1672
2403
|
var ECONOMY_APP = "economy";
|
|
1673
|
-
var RETIRED_STORAGE_MODE_KEYS = [
|
|
1674
|
-
"HASNA_ECONOMY_STORAGE_MODE",
|
|
1675
|
-
"HASNA_ECONOMY_MODE",
|
|
1676
|
-
"ECONOMY_STORAGE_MODE",
|
|
1677
|
-
"ECONOMY_MODE"
|
|
1678
|
-
];
|
|
1679
|
-
function assertNoRetiredStorageMode(env) {
|
|
1680
|
-
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
1681
|
-
if (!legacyKey)
|
|
1682
|
-
return;
|
|
1683
|
-
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
|
|
1684
|
-
}
|
|
1685
2404
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
1686
2405
|
function envFlagSet(env, key) {
|
|
1687
2406
|
const raw = env[key];
|
|
@@ -1756,7 +2475,7 @@ function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
|
1756
2475
|
};
|
|
1757
2476
|
}
|
|
1758
2477
|
function localEconomyNotice() {
|
|
1759
|
-
return `economy:
|
|
2478
|
+
return `economy: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
1760
2479
|
}
|
|
1761
2480
|
function credentialRequiredError(env, cause) {
|
|
1762
2481
|
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
@@ -1768,7 +2487,6 @@ function credentialRequiredError(env, cause) {
|
|
|
1768
2487
|
var cache;
|
|
1769
2488
|
var localNoticePrinted = false;
|
|
1770
2489
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
1771
|
-
assertNoRetiredStorageMode(env);
|
|
1772
2490
|
const clientEnv = env;
|
|
1773
2491
|
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
1774
2492
|
return { active: false, client: null };
|
|
@@ -1796,19 +2514,961 @@ function economyCloudStorage(env = process.env) {
|
|
|
1796
2514
|
}
|
|
1797
2515
|
return cache;
|
|
1798
2516
|
}
|
|
2517
|
+
|
|
2518
|
+
// src/lib/accounts-store.ts
|
|
2519
|
+
var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
2520
|
+
var profileSchema = z.object({
|
|
2521
|
+
name: z.string(),
|
|
2522
|
+
tool: z.string(),
|
|
2523
|
+
email: z.string().optional(),
|
|
2524
|
+
displayName: z.string().optional(),
|
|
2525
|
+
dir: z.string(),
|
|
2526
|
+
description: z.string().optional(),
|
|
2527
|
+
createdAt: z.string(),
|
|
2528
|
+
lastUsedAt: z.string().optional()
|
|
2529
|
+
});
|
|
2530
|
+
var toolDefSchema = z.object({
|
|
2531
|
+
id: z.string(),
|
|
2532
|
+
label: z.string(),
|
|
2533
|
+
envVar: z.string(),
|
|
2534
|
+
defaultDir: z.string(),
|
|
2535
|
+
bin: z.string()
|
|
2536
|
+
});
|
|
2537
|
+
var storeSchema = z.object({
|
|
2538
|
+
version: z.literal(1),
|
|
2539
|
+
current: z.record(z.string(), z.string()).default({}),
|
|
2540
|
+
applied: z.record(z.string(), z.string()).default({}),
|
|
2541
|
+
toolLocks: z.record(z.string(), z.string()).default({}),
|
|
2542
|
+
profiles: z.array(profileSchema).default([]),
|
|
2543
|
+
tools: z.array(toolDefSchema).default([])
|
|
2544
|
+
});
|
|
2545
|
+
var BUILTIN_TOOLS = [
|
|
2546
|
+
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
2547
|
+
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
2548
|
+
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
|
|
2549
|
+
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
|
|
2550
|
+
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
|
|
2551
|
+
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
|
|
2552
|
+
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
|
|
2553
|
+
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
|
|
2554
|
+
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
|
|
2555
|
+
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
|
|
2556
|
+
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
|
|
2557
|
+
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
|
|
2558
|
+
];
|
|
2559
|
+
|
|
2560
|
+
// src/ingest/claude.ts
|
|
2561
|
+
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
|
|
2562
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
2563
|
+
|
|
2564
|
+
// src/ingest/codex.ts
|
|
2565
|
+
init_database();
|
|
2566
|
+
init_pricing();
|
|
2567
|
+
import { homedir as homedir4 } from "os";
|
|
2568
|
+
import { join as join4, basename as basename2 } from "path";
|
|
2569
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
|
|
2570
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
|
|
2571
|
+
var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
|
|
2572
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
|
|
2573
|
+
|
|
2574
|
+
// src/ingest/gemini.ts
|
|
2575
|
+
init_database();
|
|
2576
|
+
init_pricing();
|
|
2577
|
+
import { homedir as homedir5 } from "os";
|
|
2578
|
+
import { join as join5, basename as basename3 } from "path";
|
|
2579
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
|
|
2580
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
|
|
2581
|
+
|
|
2582
|
+
// src/ingest/opencode.ts
|
|
2583
|
+
init_database();
|
|
2584
|
+
init_pricing();
|
|
2585
|
+
import { homedir as homedir6 } from "os";
|
|
2586
|
+
import { join as join6 } from "path";
|
|
2587
|
+
var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
|
|
2588
|
+
|
|
2589
|
+
// src/ingest/quota-observation.ts
|
|
2590
|
+
init_database();
|
|
2591
|
+
init_observation_provenance();
|
|
2592
|
+
|
|
2593
|
+
// src/ingest/pi.ts
|
|
2594
|
+
init_database();
|
|
2595
|
+
import { homedir as homedir7 } from "os";
|
|
2596
|
+
import { join as join7 } from "path";
|
|
2597
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
|
|
2598
|
+
|
|
2599
|
+
// src/ingest/hermes.ts
|
|
2600
|
+
init_database();
|
|
2601
|
+
import { homedir as homedir8 } from "os";
|
|
2602
|
+
import { join as join8 } from "path";
|
|
2603
|
+
var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
|
|
2604
|
+
|
|
2605
|
+
// src/ingest/loops.ts
|
|
2606
|
+
init_database();
|
|
2607
|
+
init_pricing();
|
|
2608
|
+
|
|
2609
|
+
// src/ingest/codex-quota.ts
|
|
2610
|
+
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
2611
|
+
|
|
2612
|
+
// src/lib/sync-all.ts
|
|
2613
|
+
init_database();
|
|
2614
|
+
init_observation_provenance();
|
|
2615
|
+
|
|
2616
|
+
// src/ingest/billing.ts
|
|
2617
|
+
init_database();
|
|
2618
|
+
|
|
2619
|
+
// src/lib/sync-maintenance.ts
|
|
2620
|
+
init_database();
|
|
2621
|
+
|
|
2622
|
+
// src/lib/ingest-batches.ts
|
|
2623
|
+
init_ingest_validation();
|
|
2624
|
+
import { createHash as createHash3 } from "crypto";
|
|
2625
|
+
function* ingestBatches(body) {
|
|
2626
|
+
let rows = {};
|
|
2627
|
+
let count = 0;
|
|
2628
|
+
let bytes = 0;
|
|
2629
|
+
const finish = () => {
|
|
2630
|
+
const digest2 = createHash3("sha256").update(stableJson(rows)).digest("hex");
|
|
2631
|
+
const batch = { ...rows, schema_version: 1, idempotency_key: `scan-${digest2}` };
|
|
2632
|
+
rows = {};
|
|
2633
|
+
count = 0;
|
|
2634
|
+
bytes = 0;
|
|
2635
|
+
if (Buffer.byteLength(JSON.stringify(batch)) > MAX_INGEST_BYTES)
|
|
2636
|
+
throw new Error("Ingest batch exceeds the server byte limit");
|
|
2637
|
+
return batch;
|
|
2638
|
+
};
|
|
2639
|
+
const rowLimit = Math.min(MAX_INGEST_ROWS, 500);
|
|
2640
|
+
const byteLimit = MAX_INGEST_BYTES - 4096;
|
|
2641
|
+
for (const table of Object.keys(body).sort()) {
|
|
2642
|
+
const sorted = body[table].map((row) => ({ row, canonical: stableJson(row) })).sort((a, b) => a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0);
|
|
2643
|
+
for (const { row, canonical } of sorted) {
|
|
2644
|
+
const size = Buffer.byteLength(canonical) + 1;
|
|
2645
|
+
if (size > byteLimit)
|
|
2646
|
+
throw new Error("One ingestion row exceeds the server byte limit");
|
|
2647
|
+
if (count > 0 && (count >= rowLimit || bytes + size > byteLimit))
|
|
2648
|
+
yield finish();
|
|
2649
|
+
(rows[table] ??= []).push(row);
|
|
2650
|
+
count++;
|
|
2651
|
+
bytes += size;
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
if (count > 0)
|
|
2655
|
+
yield finish();
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
class IngestBatchFailure extends Error {
|
|
2659
|
+
pending_key;
|
|
2660
|
+
completed;
|
|
2661
|
+
pending_command;
|
|
2662
|
+
constructor(pending_key, completed, pending_command) {
|
|
2663
|
+
super("Ingest batch did not return a verified receipt; retain the exact staged command and reconcile before retrying");
|
|
2664
|
+
this.pending_key = pending_key;
|
|
2665
|
+
this.completed = completed;
|
|
2666
|
+
this.pending_command = pending_command;
|
|
2667
|
+
this.name = "IngestBatchFailure";
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/lib/ingest-journal.ts
|
|
2672
|
+
init_ingest_validation();
|
|
2673
|
+
init_database();
|
|
2674
|
+
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
2675
|
+
import { createHash as createHash4 } from "crypto";
|
|
2676
|
+
import { join as join9, resolve as resolve2 } from "path";
|
|
2677
|
+
var MAX_COMMANDS = 1000;
|
|
2678
|
+
var MAX_BYTES = 64 * 1024 * 1024;
|
|
2679
|
+
var RECEIPT_RESERVE = 4096;
|
|
2680
|
+
var MAINTENANCE_RESERVE = 16384;
|
|
2681
|
+
var sha = (s) => createHash4("sha256").update(s).digest("hex");
|
|
2682
|
+
|
|
2683
|
+
class PendingIngestError extends Error {
|
|
2684
|
+
pending_keys;
|
|
2685
|
+
code = "INGEST_RECONCILIATION_REQUIRED";
|
|
2686
|
+
constructor(pending_keys) {
|
|
2687
|
+
super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
|
|
2688
|
+
this.pending_keys = pending_keys;
|
|
2689
|
+
this.name = "PendingIngestError";
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function definitiveIngestRefusal(status, body) {
|
|
2693
|
+
const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
|
|
2694
|
+
if (!Object.hasOwn(reasons, status))
|
|
2695
|
+
return null;
|
|
2696
|
+
const legacy = body?.expected === "legacy_aggregate_requires_migration";
|
|
2697
|
+
return {
|
|
2698
|
+
status,
|
|
2699
|
+
code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
|
|
2700
|
+
reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
function getIngestJournalPath() {
|
|
2704
|
+
return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join9(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
|
|
2705
|
+
}
|
|
2706
|
+
function targetId(cloud) {
|
|
2707
|
+
const url = new URL(cloud.client.transport.baseUrl);
|
|
2708
|
+
if (url.username || url.password || url.search || url.hash)
|
|
2709
|
+
throw new Error("Ingest authority must not contain URL credentials or capabilities");
|
|
2710
|
+
return sha(url.toString().replace(/\/+$/, ""));
|
|
2711
|
+
}
|
|
2712
|
+
function secureDirectory(path) {
|
|
2713
|
+
mkdirSync3(path, { recursive: true, mode: 448 });
|
|
2714
|
+
const stat = lstatSync(path);
|
|
2715
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
2716
|
+
throw new Error("Ingest journal must be a private owner directory");
|
|
2717
|
+
}
|
|
2718
|
+
function readJson(path, max) {
|
|
2719
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
2720
|
+
try {
|
|
2721
|
+
const st = fstatSync(fd);
|
|
2722
|
+
if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
|
|
2723
|
+
throw new Error("Unsafe ingest journal record");
|
|
2724
|
+
return JSON.parse(readFileSync(fd, "utf8"));
|
|
2725
|
+
} finally {
|
|
2726
|
+
closeSync(fd);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
function createExact(path, value) {
|
|
2730
|
+
const bytes = JSON.stringify(value);
|
|
2731
|
+
let fd;
|
|
2732
|
+
try {
|
|
2733
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
|
|
2734
|
+
} catch (error) {
|
|
2735
|
+
if (error.code === "EEXIST")
|
|
2736
|
+
return false;
|
|
2737
|
+
throw error;
|
|
2738
|
+
}
|
|
2739
|
+
try {
|
|
2740
|
+
writeFileSync(fd, bytes);
|
|
2741
|
+
fsyncSync(fd);
|
|
2742
|
+
} finally {
|
|
2743
|
+
closeSync(fd);
|
|
2744
|
+
}
|
|
2745
|
+
if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
|
|
2746
|
+
throw new Error("Ingest journal readback failed");
|
|
2747
|
+
const directory = openSync(resolve2(path, ".."), constants.O_RDONLY);
|
|
2748
|
+
try {
|
|
2749
|
+
fsyncSync(directory);
|
|
2750
|
+
} finally {
|
|
2751
|
+
closeSync(directory);
|
|
2752
|
+
}
|
|
2753
|
+
return true;
|
|
2754
|
+
}
|
|
2755
|
+
function unwrap(raw) {
|
|
2756
|
+
return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
|
|
2757
|
+
}
|
|
2758
|
+
function verifyIngestReceipt(raw, command) {
|
|
2759
|
+
const receipt = unwrap(raw);
|
|
2760
|
+
if (!receipt || typeof receipt !== "object" || !receipt.ingested || typeof receipt.ingested !== "object" || Array.isArray(receipt.ingested) || !Number.isSafeInteger(receipt.total) || receipt.total < 0 || typeof receipt.operation_key !== "string" || !/^[a-f0-9]{64}$/.test(receipt.operation_key) || typeof receipt.replayed !== "boolean")
|
|
2761
|
+
throw new Error("Invalid ingest receipt");
|
|
2762
|
+
let sum = 0;
|
|
2763
|
+
for (const [table, count] of Object.entries(receipt.ingested)) {
|
|
2764
|
+
if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
|
|
2765
|
+
throw new Error("Invalid ingest receipt");
|
|
2766
|
+
sum += count;
|
|
2767
|
+
}
|
|
2768
|
+
if (sum !== receipt.total)
|
|
2769
|
+
throw new Error("Invalid ingest receipt");
|
|
2770
|
+
const projected = {
|
|
2771
|
+
ingested: Object.fromEntries(Object.entries(receipt.ingested)),
|
|
2772
|
+
total: receipt.total,
|
|
2773
|
+
operation_key: receipt.operation_key,
|
|
2774
|
+
replayed: receipt.replayed,
|
|
2775
|
+
stale: receipt.stale ?? 0,
|
|
2776
|
+
unchanged: receipt.unchanged ?? 0
|
|
2777
|
+
};
|
|
2778
|
+
const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
|
|
2779
|
+
if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
|
|
2780
|
+
throw new Error("Invalid ingest receipt");
|
|
2781
|
+
return projected;
|
|
2782
|
+
}
|
|
2783
|
+
function sameReceipt(left, right) {
|
|
2784
|
+
const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
|
|
2785
|
+
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
2786
|
+
}
|
|
2787
|
+
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
2788
|
+
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
2789
|
+
const raw = unwrap(await cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs }));
|
|
2790
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
2791
|
+
throw new Error("Invalid reconciliation response");
|
|
2792
|
+
if (raw.status === "absent")
|
|
2793
|
+
return null;
|
|
2794
|
+
if (raw.status !== "committed")
|
|
2795
|
+
throw new Error("Invalid reconciliation response");
|
|
2796
|
+
return raw.receipt;
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2799
|
+
class IngestJournal {
|
|
2800
|
+
directory;
|
|
2801
|
+
target;
|
|
2802
|
+
inventoryBytes = 0;
|
|
2803
|
+
freshlyStaged = new Set;
|
|
2804
|
+
constructor(cloud, path = getIngestJournalPath()) {
|
|
2805
|
+
this.target = targetId(cloud);
|
|
2806
|
+
secureDirectory(path);
|
|
2807
|
+
this.directory = join9(realpathSync(path), this.target);
|
|
2808
|
+
secureDirectory(this.directory);
|
|
2809
|
+
}
|
|
2810
|
+
names() {
|
|
2811
|
+
const names = [];
|
|
2812
|
+
const directory = opendirSync(this.directory);
|
|
2813
|
+
try {
|
|
2814
|
+
for (let entry;entry = directory.readSync(); ) {
|
|
2815
|
+
if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
|
|
2816
|
+
continue;
|
|
2817
|
+
names.push(entry.name);
|
|
2818
|
+
if (names.length > MAX_COMMANDS * 2)
|
|
2819
|
+
throw new Error("Ingest journal capacity reached; preserve and reconcile records");
|
|
2820
|
+
}
|
|
2821
|
+
} finally {
|
|
2822
|
+
directory.closeSync();
|
|
2823
|
+
}
|
|
2824
|
+
this.inventoryBytes = 0;
|
|
2825
|
+
const set = new Set(names);
|
|
2826
|
+
for (const name of names) {
|
|
2827
|
+
if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
|
|
2828
|
+
throw new Error("Unexpected ingest journal entry");
|
|
2829
|
+
if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
|
|
2830
|
+
throw new Error("Orphaned ingest outcome requires maintenance recovery");
|
|
2831
|
+
if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
|
|
2832
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
2833
|
+
const stat = lstatSync(join9(this.directory, name));
|
|
2834
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
|
|
2835
|
+
throw new Error("Unsafe ingest journal record");
|
|
2836
|
+
if (stat.size > MAX_INGEST_BYTES + 4096)
|
|
2837
|
+
throw new Error("Oversized ingest journal record");
|
|
2838
|
+
this.inventoryBytes += stat.size;
|
|
2839
|
+
if (name.endsWith(".refusal.json"))
|
|
2840
|
+
this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
|
|
2841
|
+
}
|
|
2842
|
+
return names;
|
|
2843
|
+
}
|
|
2844
|
+
file(key, kind) {
|
|
2845
|
+
return join9(this.directory, `${sha(key)}.${kind}.json`);
|
|
2846
|
+
}
|
|
2847
|
+
intent(name) {
|
|
2848
|
+
const record = readJson(join9(this.directory, name), MAX_INGEST_BYTES + 4096);
|
|
2849
|
+
if (record.version !== 1 || record.target !== this.target || !record.command || typeof record.command.idempotency_key !== "string" || record.body_hash !== canonicalIngestBodyHash(record.command) || name !== `${sha(record.command.idempotency_key)}.intent.json`)
|
|
2850
|
+
throw new Error("Invalid ingest journal intent");
|
|
2851
|
+
validateIngest(record.command);
|
|
2852
|
+
return record;
|
|
2853
|
+
}
|
|
2854
|
+
status() {
|
|
2855
|
+
const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
|
|
2856
|
+
const set = new Set(names);
|
|
2857
|
+
const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
|
|
2858
|
+
const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
|
|
2859
|
+
const bytes = this.inventoryBytes;
|
|
2860
|
+
return { target: this.target, commands: intents.length, completed, rejected, pending: intents.length - completed - rejected, bytes, reserved_bytes: bytes + (intents.length - completed - rejected) * RECEIPT_RESERVE + MAINTENANCE_RESERVE };
|
|
2861
|
+
}
|
|
2862
|
+
canStage(command, status = this.status()) {
|
|
2863
|
+
const name = `${sha(command.idempotency_key)}.intent.json`;
|
|
2864
|
+
try {
|
|
2865
|
+
if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
|
|
2866
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
2867
|
+
return true;
|
|
2868
|
+
} catch (error) {
|
|
2869
|
+
if (error.code !== "ENOENT")
|
|
2870
|
+
throw error;
|
|
2871
|
+
}
|
|
2872
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
2873
|
+
return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
|
|
2874
|
+
}
|
|
2875
|
+
receipt(command) {
|
|
2876
|
+
if (this.refusal(command))
|
|
2877
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
2878
|
+
const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
|
|
2879
|
+
if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
|
|
2880
|
+
throw new Error("Invalid receipt journal binding");
|
|
2881
|
+
return verifyIngestReceipt(record.receipt, command);
|
|
2882
|
+
}
|
|
2883
|
+
stage(command) {
|
|
2884
|
+
validateIngest(command);
|
|
2885
|
+
const path = this.file(command.idempotency_key, "intent");
|
|
2886
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
|
|
2887
|
+
if (!this.canStage(command))
|
|
2888
|
+
throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
|
|
2889
|
+
if (createExact(path, record)) {
|
|
2890
|
+
this.freshlyStaged.add(command.idempotency_key);
|
|
2891
|
+
return true;
|
|
2892
|
+
}
|
|
2893
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
2894
|
+
const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
2895
|
+
if (old.body_hash !== record.body_hash)
|
|
2896
|
+
throw new Error("Ingest command key is already bound to different content");
|
|
2897
|
+
return false;
|
|
2898
|
+
}
|
|
2899
|
+
accept(command, raw) {
|
|
2900
|
+
if (this.refusal(command))
|
|
2901
|
+
throw new Error("A refused command cannot accept a receipt");
|
|
2902
|
+
this.freshlyStaged.delete(command.idempotency_key);
|
|
2903
|
+
const receipt = verifyIngestReceipt(raw, command);
|
|
2904
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
2905
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
2906
|
+
throw new Error("Ingest receipt content conflicts with exact intent");
|
|
2907
|
+
const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
|
|
2908
|
+
const path = this.file(command.idempotency_key, "receipt");
|
|
2909
|
+
if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
2910
|
+
throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
|
|
2911
|
+
if (!createExact(path, record)) {
|
|
2912
|
+
const old = readJson(path, MAX_INGEST_BYTES);
|
|
2913
|
+
if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
|
|
2914
|
+
throw new Error("Ingest receipt journal conflict");
|
|
2915
|
+
}
|
|
2916
|
+
return receipt;
|
|
2917
|
+
}
|
|
2918
|
+
recordRefusal(command, refusal) {
|
|
2919
|
+
if (!this.freshlyStaged.delete(command.idempotency_key))
|
|
2920
|
+
throw new Error("Refusal requires this invocation's fresh command");
|
|
2921
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
|
|
2922
|
+
if (intent.body_hash !== canonicalIngestBodyHash(command))
|
|
2923
|
+
throw new Error("Refusal conflicts with exact intent");
|
|
2924
|
+
const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
|
|
2925
|
+
if (!expected || stableJson(expected) !== stableJson(refusal))
|
|
2926
|
+
throw new Error("Invalid definitive refusal");
|
|
2927
|
+
const names = this.names();
|
|
2928
|
+
if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
|
|
2929
|
+
throw new Error("A completed command cannot be refused");
|
|
2930
|
+
const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
|
|
2931
|
+
if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
|
|
2932
|
+
throw new Error("Refusal capacity exhausted; exact intent retained");
|
|
2933
|
+
if (!createExact(this.file(command.idempotency_key, "refusal"), record))
|
|
2934
|
+
throw new Error("Existing refusal requires inspection");
|
|
2935
|
+
this.refusal(command);
|
|
2936
|
+
}
|
|
2937
|
+
refusal(command) {
|
|
2938
|
+
let record;
|
|
2939
|
+
try {
|
|
2940
|
+
record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
|
|
2941
|
+
} catch (error) {
|
|
2942
|
+
if (error.code === "ENOENT")
|
|
2943
|
+
return null;
|
|
2944
|
+
throw error;
|
|
2945
|
+
}
|
|
2946
|
+
const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
|
|
2947
|
+
const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
|
|
2948
|
+
if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
|
|
2949
|
+
throw new Error("Invalid refusal journal binding");
|
|
2950
|
+
try {
|
|
2951
|
+
lstatSync(this.file(command.idempotency_key, "receipt"));
|
|
2952
|
+
throw new Error("Conflicting ingest receipt and refusal");
|
|
2953
|
+
} catch (error) {
|
|
2954
|
+
if (error.code !== "ENOENT")
|
|
2955
|
+
throw error;
|
|
2956
|
+
}
|
|
2957
|
+
return expected;
|
|
2958
|
+
}
|
|
2959
|
+
async reconcileCommand(cloud, command, timeoutMs = 1e4) {
|
|
2960
|
+
const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
|
|
2961
|
+
return receipt ? this.accept(command, receipt) : null;
|
|
2962
|
+
}
|
|
2963
|
+
assertCloud(cloud) {
|
|
2964
|
+
if (targetId(cloud) !== this.target)
|
|
2965
|
+
throw new Error("Ingest journal authority changed");
|
|
2966
|
+
}
|
|
2967
|
+
async lookupReceipt(cloud, command, timeoutMs = 1e4) {
|
|
2968
|
+
this.assertCloud(cloud);
|
|
2969
|
+
const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
|
|
2970
|
+
return receipt === null ? null : verifyIngestReceipt(receipt, command);
|
|
2971
|
+
}
|
|
2972
|
+
async reconcilePending(cloud) {
|
|
2973
|
+
const names = this.names(), unresolved = [];
|
|
2974
|
+
const deadline = performance.now() + 30000;
|
|
2975
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2976
|
+
const intent = this.intent(name);
|
|
2977
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
|
|
2978
|
+
this.refusal(intent.command);
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
|
|
2982
|
+
const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
|
|
2983
|
+
if (record.target !== this.target || record.body_hash !== intent.body_hash)
|
|
2984
|
+
throw new Error("Invalid receipt journal binding");
|
|
2985
|
+
verifyIngestReceipt(record.receipt, intent.command);
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
if (performance.now() >= deadline) {
|
|
2989
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
try {
|
|
2993
|
+
if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
|
|
2994
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2995
|
+
} catch {
|
|
2996
|
+
unresolved.push(intent.command.idempotency_key);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
if (unresolved.length)
|
|
3000
|
+
throw new PendingIngestError(unresolved);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/lib/ingest-journal-maintenance.ts
|
|
3005
|
+
import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
3006
|
+
import { createHash as createHash5 } from "crypto";
|
|
3007
|
+
import { join as join10 } from "path";
|
|
3008
|
+
var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
3009
|
+
var digest2 = /^[a-f0-9]{64}$/;
|
|
3010
|
+
var sha2 = (bytes) => createHash5("sha256").update(bytes).digest("hex");
|
|
3011
|
+
|
|
3012
|
+
class JournalMaintenanceError extends Error {
|
|
3013
|
+
reason;
|
|
3014
|
+
target;
|
|
3015
|
+
code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
|
|
3016
|
+
constructor(reason, target) {
|
|
3017
|
+
super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
|
|
3018
|
+
this.reason = reason;
|
|
3019
|
+
this.target = target;
|
|
3020
|
+
this.name = "JournalMaintenanceError";
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
function exists(path) {
|
|
3024
|
+
try {
|
|
3025
|
+
lstatSync2(path);
|
|
3026
|
+
return true;
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
if (error.code === "ENOENT")
|
|
3029
|
+
return false;
|
|
3030
|
+
throw error;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
function snapshot(path) {
|
|
3034
|
+
const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
|
|
3035
|
+
try {
|
|
3036
|
+
const stat = fstatSync2(fd);
|
|
3037
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
3038
|
+
throw new Error("Unsafe maintenance source");
|
|
3039
|
+
const bytes = readFileSync2(fd), after = fstatSync2(fd);
|
|
3040
|
+
if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
|
|
3041
|
+
throw new Error("Maintenance source changed");
|
|
3042
|
+
return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
|
|
3043
|
+
} finally {
|
|
3044
|
+
closeSync2(fd);
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
function sameSource(path, expected) {
|
|
3048
|
+
const { capture_sha256: _capture, ...original } = expected;
|
|
3049
|
+
if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
|
|
3050
|
+
throw new Error("Maintenance source changed; preserve for inspection");
|
|
3051
|
+
}
|
|
3052
|
+
function validAnchor(value, target) {
|
|
3053
|
+
const a = value;
|
|
3054
|
+
if (a?.version !== 1 || a.target !== target || !/^[A-Za-z0-9._:-]{1,200}$/.test(a.key) || !digest2.test(a.body_hash) || !digest2.test(a.receipt?.operation_key))
|
|
3055
|
+
throw new Error("Invalid journal authority anchor");
|
|
3056
|
+
return a;
|
|
3057
|
+
}
|
|
3058
|
+
async function confirm(cloud, anchor, deadline) {
|
|
3059
|
+
if (Date.now() >= deadline)
|
|
3060
|
+
throw new Error("Maintenance deadline reached");
|
|
3061
|
+
const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
|
|
3062
|
+
if (current === null || !sameReceipt(current, anchor.receipt))
|
|
3063
|
+
throw new Error("Exact Economy receipt is unavailable or changed");
|
|
3064
|
+
}
|
|
3065
|
+
async function verifyJournalAuthority(journal, cloud) {
|
|
3066
|
+
journal.assertCloud(cloud);
|
|
3067
|
+
const path = join10(journal.directory, ".authority.json");
|
|
3068
|
+
if (!exists(path)) {
|
|
3069
|
+
const names = journal.names();
|
|
3070
|
+
const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
|
|
3071
|
+
if (!name)
|
|
3072
|
+
return;
|
|
3073
|
+
const intent = journal.intent(name), receipt = journal.receipt(intent.command);
|
|
3074
|
+
createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
|
|
3075
|
+
}
|
|
3076
|
+
await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
|
|
3077
|
+
}
|
|
3078
|
+
function recoveryFactory(cloud, env = process.env) {
|
|
3079
|
+
return async (operationRoot, checkpoint, deadline) => {
|
|
3080
|
+
if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
|
|
3081
|
+
throw new Error("recovery_not_configured");
|
|
3082
|
+
const economy = new URL(cloud.client.transport.baseUrl);
|
|
3083
|
+
const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
|
|
3084
|
+
if (!fleet && !env.HASNA_TRASH_API_URL)
|
|
3085
|
+
throw new Error("self_host_trash_authority_required");
|
|
3086
|
+
const sdk = await import("@hasna/trash/sdk");
|
|
3087
|
+
if (sdk.VERSION !== "0.2.3")
|
|
3088
|
+
throw new Error("trash_version_mismatch");
|
|
3089
|
+
const boundedFetch = (input, init) => {
|
|
3090
|
+
const remaining = deadline - Date.now();
|
|
3091
|
+
if (remaining <= 0)
|
|
3092
|
+
throw new Error("maintenance_deadline");
|
|
3093
|
+
const signal = AbortSignal.timeout(remaining);
|
|
3094
|
+
return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
|
|
3095
|
+
};
|
|
3096
|
+
return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
|
|
3097
|
+
};
|
|
3098
|
+
}
|
|
3099
|
+
|
|
3100
|
+
class IngestJournalMaintenance {
|
|
3101
|
+
journal;
|
|
3102
|
+
cloud;
|
|
3103
|
+
factory;
|
|
3104
|
+
control;
|
|
3105
|
+
operationRoot;
|
|
3106
|
+
constructor(journal, cloud, factory = recoveryFactory(cloud)) {
|
|
3107
|
+
this.journal = journal;
|
|
3108
|
+
this.cloud = cloud;
|
|
3109
|
+
this.factory = factory;
|
|
3110
|
+
journal.assertCloud(cloud);
|
|
3111
|
+
this.control = join10(journal.directory, ".maintenance");
|
|
3112
|
+
this.operationRoot = join10(journal.directory, ".recovery");
|
|
3113
|
+
}
|
|
3114
|
+
manifest() {
|
|
3115
|
+
secureDirectory(this.control);
|
|
3116
|
+
const names = readdirSync2(this.control);
|
|
3117
|
+
if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
|
|
3118
|
+
throw new Error("Unexpected maintenance entry");
|
|
3119
|
+
let size = 0;
|
|
3120
|
+
for (const name of names)
|
|
3121
|
+
size += lstatSync2(join10(this.control, name)).size;
|
|
3122
|
+
if (size > MAINTENANCE_RESERVE)
|
|
3123
|
+
throw new Error("Maintenance metadata exceeds reserve");
|
|
3124
|
+
const m = validAnchor(readJson(join10(this.control, "manifest.json"), 8192), this.journal.target);
|
|
3125
|
+
for (const part of [m.intent, m.receipt_file])
|
|
3126
|
+
if (!part || !digest2.test(part.sha256) || !digest2.test(part.capture_sha256 ?? "") || !Number.isSafeInteger(part.sizeBytes) || part.sizeBytes < 1 || part.sizeBytes > 4198400 || part.mode !== 384 || !/^\d+$/.test(part.device) || !/^\d+$/.test(part.inode) || !Number.isFinite(part.ctime))
|
|
3127
|
+
throw new Error("Invalid maintenance snapshot");
|
|
3128
|
+
return m;
|
|
3129
|
+
}
|
|
3130
|
+
pending(backend) {
|
|
3131
|
+
const pending = backend.pending(4);
|
|
3132
|
+
if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
|
|
3133
|
+
throw new Error("Unexpected Trash recovery state");
|
|
3134
|
+
return pending;
|
|
3135
|
+
}
|
|
3136
|
+
async backend(deadline) {
|
|
3137
|
+
const backend = await this.factory(this.operationRoot, async (point, operation) => {
|
|
3138
|
+
if (operation.path === this.control)
|
|
3139
|
+
return;
|
|
3140
|
+
const m = this.manifest();
|
|
3141
|
+
const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
|
|
3142
|
+
if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
|
|
3143
|
+
throw new Error("Unexpected Trash operation");
|
|
3144
|
+
const expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
3145
|
+
if (point === "afterSnapshot") {
|
|
3146
|
+
if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
|
|
3147
|
+
throw new Error("Trash snapshot mismatch");
|
|
3148
|
+
sameSource(operation.path, expected);
|
|
3149
|
+
const path = join10(this.control, `${kind}.operation.json`);
|
|
3150
|
+
createExact(path, { id: operation.id });
|
|
3151
|
+
if (readJson(path, 1024).id !== operation.id)
|
|
3152
|
+
throw new Error("Conflicting Trash operation");
|
|
3153
|
+
}
|
|
3154
|
+
if (point === "beforeStage") {
|
|
3155
|
+
await confirm(this.cloud, m, deadline);
|
|
3156
|
+
sameSource(operation.path, expected);
|
|
3157
|
+
}
|
|
3158
|
+
}, deadline);
|
|
3159
|
+
const status = await backend.doctor();
|
|
3160
|
+
if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
|
|
3161
|
+
throw new Error("Trash station authority unavailable");
|
|
3162
|
+
return backend;
|
|
3163
|
+
}
|
|
3164
|
+
async entry(backend, id, path, expected) {
|
|
3165
|
+
const e = await backend.api.get(id);
|
|
3166
|
+
if (e.id !== id || e.originalPath !== path || e.kind !== "file" || e.sha256 !== expected.capture_sha256 || e.sizeBytes !== expected.sizeBytes || e.mode !== expected.mode || e.state !== "trashed")
|
|
3167
|
+
throw new Error("Trash preservation readback refused");
|
|
3168
|
+
if (exists(path))
|
|
3169
|
+
throw new Error("Retired source reappeared; preserve for inspection");
|
|
3170
|
+
}
|
|
3171
|
+
async pairFile(backend, m, kind, deadline) {
|
|
3172
|
+
await confirm(this.cloud, m, deadline);
|
|
3173
|
+
const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
|
|
3174
|
+
const saved = join10(this.control, `${kind}.operation.json`);
|
|
3175
|
+
let id;
|
|
3176
|
+
if (exists(saved))
|
|
3177
|
+
id = readJson(saved, 1024).id;
|
|
3178
|
+
const pending = this.pending(backend);
|
|
3179
|
+
const matches = pending.filter((p) => p.path === path);
|
|
3180
|
+
if (matches.length > 1 || id && !uuid.test(id))
|
|
3181
|
+
throw new Error("Conflicting Trash operation");
|
|
3182
|
+
if (matches[0]) {
|
|
3183
|
+
if (id && id !== matches[0].id)
|
|
3184
|
+
throw new Error("Conflicting Trash operation");
|
|
3185
|
+
id = matches[0].id;
|
|
3186
|
+
const operation = backend.journal.read(id);
|
|
3187
|
+
if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
|
|
3188
|
+
throw new Error("Invalid Trash operation binding");
|
|
3189
|
+
if (exists(path))
|
|
3190
|
+
sameSource(path, expected);
|
|
3191
|
+
await backend.recover(id, { takeAbandonedLock: true });
|
|
3192
|
+
} else if (!id) {
|
|
3193
|
+
sameSource(path, expected);
|
|
3194
|
+
const status = await backend.doctor();
|
|
3195
|
+
const e = await backend.put(path, { retentionDays: status.retentionDays });
|
|
3196
|
+
id = e.id;
|
|
3197
|
+
if (!exists(saved) || readJson(saved, 1024).id !== id)
|
|
3198
|
+
throw new Error("Trash did not persist its capture binding");
|
|
3199
|
+
}
|
|
3200
|
+
await this.entry(backend, id, path, expected);
|
|
3201
|
+
}
|
|
3202
|
+
async recover() {
|
|
3203
|
+
if (!exists(this.control) && !exists(this.operationRoot))
|
|
3204
|
+
return;
|
|
3205
|
+
if (exists(this.operationRoot))
|
|
3206
|
+
secureDirectory(this.operationRoot);
|
|
3207
|
+
if (!exists(this.control) && readdirSync2(this.operationRoot).every((n) => n === ".locks"))
|
|
3208
|
+
return;
|
|
3209
|
+
const deadline = Date.now() + 30000;
|
|
3210
|
+
try {
|
|
3211
|
+
await this.finish(await this.backend(deadline), deadline);
|
|
3212
|
+
} catch (error) {
|
|
3213
|
+
if (error instanceof JournalMaintenanceError)
|
|
3214
|
+
throw error;
|
|
3215
|
+
throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async finish(backend, deadline) {
|
|
3219
|
+
const pending = this.pending(backend);
|
|
3220
|
+
const control = pending.filter((p) => p.path === this.control);
|
|
3221
|
+
if (control.length) {
|
|
3222
|
+
if (control.length !== 1 || pending.length !== 1)
|
|
3223
|
+
throw new Error("Conflicting maintenance cleanup");
|
|
3224
|
+
const op = backend.journal.read(control[0].id);
|
|
3225
|
+
if (op.kind !== "capture" || op.path !== this.control)
|
|
3226
|
+
throw new Error("Invalid maintenance cleanup");
|
|
3227
|
+
await backend.recover(op.id, { takeAbandonedLock: true });
|
|
3228
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
3229
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
3230
|
+
return;
|
|
3231
|
+
}
|
|
3232
|
+
if (!exists(this.control)) {
|
|
3233
|
+
if (pending.length)
|
|
3234
|
+
throw new Error("Orphaned maintenance operation");
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
if (readdirSync2(this.control).length === 0) {
|
|
3238
|
+
if (pending.length)
|
|
3239
|
+
throw new Error("Orphaned maintenance operation");
|
|
3240
|
+
const status2 = await backend.doctor();
|
|
3241
|
+
await backend.put(this.control, { retentionDays: status2.retentionDays });
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
const m = this.manifest();
|
|
3245
|
+
if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
|
|
3246
|
+
throw new Error("Unexpected recovery path");
|
|
3247
|
+
await this.pairFile(backend, m, "receipt", deadline);
|
|
3248
|
+
await this.pairFile(backend, m, "intent", deadline);
|
|
3249
|
+
if (this.pending(backend).length)
|
|
3250
|
+
throw new Error("Unfinished pair capture");
|
|
3251
|
+
await confirm(this.cloud, m, deadline);
|
|
3252
|
+
const status = await backend.doctor();
|
|
3253
|
+
await backend.put(this.control, { retentionDays: status.retentionDays });
|
|
3254
|
+
if (exists(this.control) || this.pending(backend).length)
|
|
3255
|
+
throw new Error("Maintenance cleanup unresolved");
|
|
3256
|
+
}
|
|
3257
|
+
async maintain(limit = 32) {
|
|
3258
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 32)
|
|
3259
|
+
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
3260
|
+
await this.recover();
|
|
3261
|
+
await verifyJournalAuthority(this.journal, this.cloud);
|
|
3262
|
+
const deadline = Date.now() + 30000;
|
|
3263
|
+
let retired = 0, bytes = 0;
|
|
3264
|
+
let backend;
|
|
3265
|
+
try {
|
|
3266
|
+
const names = this.journal.names();
|
|
3267
|
+
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
3268
|
+
if (retired >= limit || Date.now() >= deadline || retired > 0 && bytes >= 1048576)
|
|
3269
|
+
break;
|
|
3270
|
+
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
3271
|
+
continue;
|
|
3272
|
+
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
3273
|
+
backend ??= await this.backend(deadline);
|
|
3274
|
+
const m = {
|
|
3275
|
+
version: 1,
|
|
3276
|
+
target: this.journal.target,
|
|
3277
|
+
key: intent.command.idempotency_key,
|
|
3278
|
+
body_hash: intent.body_hash,
|
|
3279
|
+
receipt,
|
|
3280
|
+
intent: snapshot(join10(this.journal.directory, name)),
|
|
3281
|
+
receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
|
|
3282
|
+
};
|
|
3283
|
+
await confirm(this.cloud, m, deadline);
|
|
3284
|
+
m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
|
|
3285
|
+
m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
|
|
3286
|
+
secureDirectory(this.control);
|
|
3287
|
+
if (!createExact(join10(this.control, "manifest.json"), m))
|
|
3288
|
+
throw new Error("Existing maintenance transaction");
|
|
3289
|
+
await this.finish(backend, deadline);
|
|
3290
|
+
retired++;
|
|
3291
|
+
bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
|
|
3292
|
+
}
|
|
3293
|
+
return { retired, remaining: this.journal.status().completed };
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
if (error instanceof JournalMaintenanceError)
|
|
3296
|
+
throw error;
|
|
3297
|
+
const reason = error instanceof Error && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch"].includes(error.message) ? error.message : backend ? "recovery_unresolved" : "recovery_authority_unavailable";
|
|
3298
|
+
throw new JournalMaintenanceError(reason, this.journal.target);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
async admit(command) {
|
|
3302
|
+
const status = this.journal.status();
|
|
3303
|
+
const fits = this.journal.canStage(command, status);
|
|
3304
|
+
if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
|
|
3305
|
+
return;
|
|
3306
|
+
try {
|
|
3307
|
+
await this.maintain();
|
|
3308
|
+
} catch (error) {
|
|
3309
|
+
if (exists(this.control) || !this.journal.canStage(command))
|
|
3310
|
+
throw error;
|
|
3311
|
+
}
|
|
3312
|
+
if (!this.journal.canStage(command))
|
|
3313
|
+
throw new JournalMaintenanceError("capacity", this.journal.target);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
// src/lib/ingest-journal-lock.ts
|
|
3318
|
+
import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
|
|
3319
|
+
import { join as join11 } from "path";
|
|
3320
|
+
async function withIngestJournalLock(directory, action) {
|
|
3321
|
+
if (process.platform !== "linux" && process.platform !== "darwin")
|
|
3322
|
+
throw new Error("Ingest journal locking requires Linux or macOS");
|
|
3323
|
+
const { dlopen, FFIType } = await import("bun:ffi");
|
|
3324
|
+
const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
|
|
3325
|
+
flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
|
|
3326
|
+
});
|
|
3327
|
+
const path = join11(directory, ".lease");
|
|
3328
|
+
let fd;
|
|
3329
|
+
try {
|
|
3330
|
+
fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
|
|
3331
|
+
const stat = fstatSync3(fd), current = lstatSync3(path);
|
|
3332
|
+
if (!stat.isFile() || stat.size !== 0 || stat.nlink !== 1 || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
|
|
3333
|
+
throw new Error("Unsafe ingest journal lease");
|
|
3334
|
+
if (library.symbols.flock(fd, 6) !== 0) {
|
|
3335
|
+
const error = new Error("Ingest journal is busy; retry after the current operation finishes");
|
|
3336
|
+
Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
|
|
3337
|
+
throw error;
|
|
3338
|
+
}
|
|
3339
|
+
return await action();
|
|
3340
|
+
} finally {
|
|
3341
|
+
if (fd !== undefined)
|
|
3342
|
+
closeSync3(fd);
|
|
3343
|
+
library.close();
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
// src/lib/ingest-journal-operation.ts
|
|
3348
|
+
import { z as z2 } from "zod";
|
|
3349
|
+
var ingestJournalOperationSchema = z2.discriminatedUnion("action", [
|
|
3350
|
+
z2.object({ action: z2.literal("status") }).strict(),
|
|
3351
|
+
z2.object({ action: z2.literal("reconcile") }).strict(),
|
|
3352
|
+
z2.object({ action: z2.literal("maintain"), confirm: z2.literal(true), limit: z2.number().int().min(1).max(32).default(32) }).strict()
|
|
3353
|
+
]);
|
|
3354
|
+
|
|
3355
|
+
// src/lib/cloud-ingest.ts
|
|
3356
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
3357
|
+
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
3358
|
+
function exportIngestRows(db) {
|
|
3359
|
+
const body = {};
|
|
3360
|
+
for (const table of CLOUD_INGEST_TABLES) {
|
|
3361
|
+
const rows = db.prepare(`SELECT * FROM ${table}`).all();
|
|
3362
|
+
if (rows.length > 0)
|
|
3363
|
+
body[table] = rows;
|
|
3364
|
+
}
|
|
3365
|
+
return body;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
class IngestRefused extends Error {
|
|
3369
|
+
status;
|
|
3370
|
+
code;
|
|
3371
|
+
reason;
|
|
3372
|
+
constructor(status, code, reason = code) {
|
|
3373
|
+
super(`Ingestion refused: ${reason}; exact command intent retained`);
|
|
3374
|
+
this.status = status;
|
|
3375
|
+
this.code = code;
|
|
3376
|
+
this.reason = reason;
|
|
3377
|
+
this.name = "IngestRefused";
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
async function postIngest(cloud, body) {
|
|
3381
|
+
const journal = new IngestJournal(cloud);
|
|
3382
|
+
return withIngestJournalLock(journal.directory, async () => {
|
|
3383
|
+
const maintenance = new IngestJournalMaintenance(journal, cloud);
|
|
3384
|
+
await maintenance.recover();
|
|
3385
|
+
await journal.reconcilePending(cloud);
|
|
3386
|
+
await verifyJournalAuthority(journal, cloud);
|
|
3387
|
+
const ingested = {};
|
|
3388
|
+
const completed = [];
|
|
3389
|
+
let total = 0;
|
|
3390
|
+
for (const batch of ingestBatches(body)) {
|
|
3391
|
+
try {
|
|
3392
|
+
const refused = journal.refusal(batch);
|
|
3393
|
+
if (refused)
|
|
3394
|
+
throw new IngestRefused(refused.status, refused.code, refused.reason);
|
|
3395
|
+
await maintenance.admit(batch);
|
|
3396
|
+
const known = await journal.lookupReceipt(cloud, batch);
|
|
3397
|
+
const fresh = journal.stage(batch);
|
|
3398
|
+
const sendFresh = async () => {
|
|
3399
|
+
let response;
|
|
3400
|
+
try {
|
|
3401
|
+
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
3402
|
+
} catch (error) {
|
|
3403
|
+
const refusal = error instanceof HasnaHttpError ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
3404
|
+
if (refusal) {
|
|
3405
|
+
journal.recordRefusal(batch, refusal);
|
|
3406
|
+
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
3407
|
+
}
|
|
3408
|
+
throw error;
|
|
3409
|
+
}
|
|
3410
|
+
return journal.accept(batch, response);
|
|
3411
|
+
};
|
|
3412
|
+
const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
|
|
3413
|
+
await verifyJournalAuthority(journal, cloud);
|
|
3414
|
+
if (!payload)
|
|
3415
|
+
throw new PendingIngestError([batch.idempotency_key]);
|
|
3416
|
+
verifyIngestReceipt(payload, batch);
|
|
3417
|
+
const counts = payload["ingested"];
|
|
3418
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
|
|
3419
|
+
throw new Error("Invalid ingest receipt");
|
|
3420
|
+
let sum = 0;
|
|
3421
|
+
for (const [table, count] of Object.entries(counts)) {
|
|
3422
|
+
if (!CLOUD_INGEST_TABLES.includes(table) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)
|
|
3423
|
+
throw new Error("Invalid ingest receipt");
|
|
3424
|
+
sum += count;
|
|
3425
|
+
ingested[table] = (ingested[table] ?? 0) + count;
|
|
3426
|
+
}
|
|
3427
|
+
if (sum !== payload["total"])
|
|
3428
|
+
throw new Error("Invalid ingest receipt");
|
|
3429
|
+
total += sum;
|
|
3430
|
+
const operation = payload["operation_key"];
|
|
3431
|
+
if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
|
|
3432
|
+
throw new Error("Invalid ingest receipt");
|
|
3433
|
+
completed.push({
|
|
3434
|
+
idempotency_key: batch.idempotency_key,
|
|
3435
|
+
...typeof operation === "string" ? { operation_key: operation } : {},
|
|
3436
|
+
replayed: payload["replayed"] === true
|
|
3437
|
+
});
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
3440
|
+
throw error;
|
|
3441
|
+
if (error instanceof HasnaHttpError && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
3442
|
+
const body2 = error.body;
|
|
3443
|
+
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
3444
|
+
}
|
|
3445
|
+
throw new IngestBatchFailure(batch.idempotency_key, completed, batch);
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
return { ingested, total, receipts: completed };
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
async function pushIngestRows(cloud, scratch) {
|
|
3452
|
+
const body = exportIngestRows(scratch);
|
|
3453
|
+
if (Object.keys(body).length === 0)
|
|
3454
|
+
return { posted: false, ingested: {}, total: 0, receipts: [] };
|
|
3455
|
+
const response = await postIngest(cloud, body);
|
|
3456
|
+
return { posted: true, ...response };
|
|
3457
|
+
}
|
|
1799
3458
|
// package.json
|
|
1800
3459
|
var package_default = {
|
|
1801
3460
|
name: "@hasna/economy",
|
|
1802
|
-
version: "0.
|
|
3461
|
+
version: "0.5.1",
|
|
1803
3462
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
1804
3463
|
repository: {
|
|
1805
3464
|
type: "git",
|
|
1806
|
-
url: "
|
|
3465
|
+
url: "https://github.com/hasna/apps.git",
|
|
3466
|
+
directory: "apps/economy"
|
|
1807
3467
|
},
|
|
1808
3468
|
bugs: {
|
|
1809
|
-
url: "https://github.com/hasna/
|
|
3469
|
+
url: "https://github.com/hasna/apps/issues"
|
|
1810
3470
|
},
|
|
1811
|
-
homepage: "https://github.com/hasna/economy#readme",
|
|
3471
|
+
homepage: "https://github.com/hasna/apps/tree/main/apps/economy#readme",
|
|
1812
3472
|
type: "module",
|
|
1813
3473
|
main: "dist/index.js",
|
|
1814
3474
|
types: "dist/index.d.ts",
|
|
@@ -1837,14 +3497,15 @@ var package_default = {
|
|
|
1837
3497
|
"CHANGELOG.md",
|
|
1838
3498
|
"SECURITY.md",
|
|
1839
3499
|
"CONTRIBUTING.md",
|
|
1840
|
-
"CODE_OF_CONDUCT.md"
|
|
3500
|
+
"CODE_OF_CONDUCT.md",
|
|
3501
|
+
"!dist/lib/test-hermetic-*"
|
|
1841
3502
|
],
|
|
1842
3503
|
scripts: {
|
|
1843
|
-
build:
|
|
1844
|
-
"build:cli":
|
|
1845
|
-
"build:mcp":
|
|
1846
|
-
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
1847
|
-
"build:lib":
|
|
3504
|
+
build: 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]" && tsc --emitDeclarationOnly --outDir dist',
|
|
3505
|
+
"build:cli": 'bun build src/cli/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
3506
|
+
"build:mcp": 'bun build src/mcp/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
3507
|
+
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/server/service-worker.ts --outdir dist/server --target bun --packages external",
|
|
3508
|
+
"build:lib": 'bun build src/index.ts --outdir dist --root src --target bun --packages external --splitting --chunk-naming "chunks/[name]-[hash].[ext]"',
|
|
1848
3509
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
1849
3510
|
prepack: "bun run build && bun run artifact-scan",
|
|
1850
3511
|
typecheck: "tsc --noEmit",
|
|
@@ -1881,7 +3542,8 @@ var package_default = {
|
|
|
1881
3542
|
chalk: "^5.4.1",
|
|
1882
3543
|
commander: "^13.1.0",
|
|
1883
3544
|
pg: "8.13.1",
|
|
1884
|
-
zod: "^3.24.2"
|
|
3545
|
+
zod: "^3.24.2",
|
|
3546
|
+
"@hasna/trash": "0.2.3"
|
|
1885
3547
|
},
|
|
1886
3548
|
devDependencies: {
|
|
1887
3549
|
"@types/bun": "1.3.14",
|
|
@@ -1891,7 +3553,16 @@ var package_default = {
|
|
|
1891
3553
|
},
|
|
1892
3554
|
trustedDependencies: [
|
|
1893
3555
|
"@hasna/projects"
|
|
1894
|
-
]
|
|
3556
|
+
],
|
|
3557
|
+
overrides: {
|
|
3558
|
+
"fast-uri": "3.1.8",
|
|
3559
|
+
hono: "4.13.7",
|
|
3560
|
+
"ip-address": "10.7.1",
|
|
3561
|
+
nanoid: "5.1.16",
|
|
3562
|
+
"path-to-regexp": "8.4.0",
|
|
3563
|
+
undici: "6.28.1",
|
|
3564
|
+
"express-rate-limit": "8.7.0"
|
|
3565
|
+
}
|
|
1895
3566
|
};
|
|
1896
3567
|
|
|
1897
3568
|
// src/lib/package-metadata.ts
|