taximeter 0.1.2 → 0.2.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.
@@ -81,7 +81,8 @@ var policySchema = z2.strictObject({
81
81
  denyHosts: z2.array(labelSchema).default([]),
82
82
  allowPayTo: z2.array(labelSchema).default([]),
83
83
  maxSinglePayment: amountSchema.nullable().default("1000000"),
84
- maxSingleAsset: labelSchema.default("USDC")
84
+ maxSingleAsset: labelSchema.default("USDC"),
85
+ unknownAsset: z2.enum(["allow", "deny"]).default("deny")
85
86
  });
86
87
  var portsSchema = z2.strictObject({
87
88
  proxy: z2.number().int().min(0).max(65535).default(8402),
@@ -109,7 +110,8 @@ var configPatchSchema = z2.strictObject({
109
110
  denyHosts: z2.array(labelSchema).optional(),
110
111
  allowPayTo: z2.array(labelSchema).optional(),
111
112
  maxSinglePayment: amountSchema.nullable().optional(),
112
- maxSingleAsset: labelSchema.optional()
113
+ maxSingleAsset: labelSchema.optional(),
114
+ unknownAsset: z2.enum(["allow", "deny"]).optional()
113
115
  }).optional(),
114
116
  ports: z2.strictObject({
115
117
  proxy: z2.number().int().min(0).max(65535).optional(),
@@ -320,6 +322,23 @@ function spentForBudget(proposed, events, budget, scope, now) {
320
322
  return spent.toString();
321
323
  }
322
324
  function evaluate(proposed, events, config, now) {
325
+ return evaluateWithState(
326
+ proposed,
327
+ {
328
+ reserved: events.find(
329
+ (event) => event.paymentKey === proposed.paymentKey && countsAsSpend(event)
330
+ ),
331
+ spent: {
332
+ perTask: config.budgets.perTask ? spentForBudget(proposed, events, config.budgets.perTask, "perTask", now) : "0",
333
+ perAgent: config.budgets.perAgent ? spentForBudget(proposed, events, config.budgets.perAgent, "perAgent", now) : "0",
334
+ global: config.budgets.global ? spentForBudget(proposed, events, config.budgets.global, "global", now) : "0"
335
+ }
336
+ },
337
+ config,
338
+ now
339
+ );
340
+ }
341
+ function evaluateWithState(proposed, state, config, now) {
323
342
  const { policy } = config;
324
343
  const host = proposed.host.toLowerCase();
325
344
  if (policy.denyHosts.some((value) => value.toLowerCase() === host)) return deny("host_denied");
@@ -327,16 +346,16 @@ function evaluate(proposed, events, config, now) {
327
346
  return deny("host_not_allowed");
328
347
  if (policy.allowPayTo.length > 0 && !policy.allowPayTo.some((value) => value.toLowerCase() === proposed.payTo.toLowerCase()))
329
348
  return deny("recipient_not_allowed");
349
+ if (policy.unknownAsset === "deny" && !assetMetadata(proposed.network, proposed.asset).decimalsKnown)
350
+ return deny("unknown_asset");
330
351
  if (policy.maxSinglePayment !== null && matchesAsset(policy.maxSingleAsset, proposed) && BigInt(proposed.amount) > BigInt(policy.maxSinglePayment))
331
352
  return deny("max_single_payment", policy.maxSinglePayment);
332
- const reserved = events.find(
333
- (event) => event.paymentKey === proposed.paymentKey && countsAsSpend(event)
334
- );
353
+ const reserved = state.reserved && countsAsSpend(state.reserved) ? state.reserved : void 0;
335
354
  for (const scope of ["perTask", "perAgent", "global"]) {
336
355
  const budget = config.budgets[scope];
337
356
  if (!budget || !matchesAsset(budget.asset, proposed) || budget.network && budget.network !== proposed.network)
338
357
  continue;
339
- const spent = spentForBudget(proposed, events, budget, scope, now);
358
+ const spent = state.spent[scope];
340
359
  const reservedTime = reserved ? Date.parse(reserved.attemptedAt ?? reserved.ts) : -Infinity;
341
360
  const inWindow = reservedTime <= now && (!budget.window || reservedTime >= now - windows[budget.window]);
342
361
  const increment = reserved && (reserved.settlementStatus === "confirmed" || inWindow) ? 0n : BigInt(proposed.amount);
@@ -647,8 +666,9 @@ var Meter = class {
647
666
  if (!proposed) return {};
648
667
  try {
649
668
  return this.ledger.transaction(() => {
650
- const events = this.ledger.view();
651
- const decision = evaluate(proposed, events, this.config, Date.now());
669
+ const now = Date.now();
670
+ const state = this.ledger.policyState(proposed, this.config.budgets, now);
671
+ const decision = evaluateWithState(proposed, state, this.config, now);
652
672
  if (!decision.allowed) {
653
673
  this.ledger.append(
654
674
  paymentEventSchema.parse({
@@ -660,9 +680,7 @@ var Meter = class {
660
680
  );
661
681
  return { body: decision.body };
662
682
  }
663
- const previous = events.find(
664
- (event) => event.status === "observed" && event.paymentKey === proposed.paymentKey
665
- );
683
+ const previous = state.reserved;
666
684
  const payment = previous ?? proposed;
667
685
  if (!previous) this.ledger.append(payment);
668
686
  const ts = (/* @__PURE__ */ new Date()).toISOString();
@@ -794,34 +812,354 @@ body{font:14px Arial,Helvetica,sans-serif;color:#182b27;background:#fff;margin:4
794
812
  // src/ledger/store.ts
795
813
  import { mkdirSync } from "fs";
796
814
  import { dirname as dirname2 } from "path";
797
- import Database from "better-sqlite3";
815
+ import Database2 from "better-sqlite3";
798
816
  import { v7 as v73 } from "uuid";
799
- import { z as z7 } from "zod";
817
+ import { z as z8 } from "zod";
800
818
 
801
819
  // migrations/001.sql
802
820
  var __default = "CREATE TABLE schema_version (version INTEGER PRIMARY KEY);\nINSERT INTO schema_version VALUES (1);\nCREATE TABLE events (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n taskId TEXT,\n agentId TEXT,\n host TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('observed', 'blocked')),\n paymentKey TEXT NOT NULL,\n payload TEXT NOT NULL CHECK (json_valid(payload))\n);\nCREATE INDEX events_ts ON events(ts);\nCREATE INDEX events_task ON events(taskId);\nCREATE INDEX events_agent ON events(agentId);\nCREATE INDEX events_host ON events(host);\nCREATE UNIQUE INDEX events_payment ON events(paymentKey) WHERE status = 'observed';\nCREATE TABLE outcomes (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n paymentId TEXT NOT NULL REFERENCES events(id),\n payload TEXT NOT NULL CHECK (json_valid(payload))\n);\nCREATE INDEX outcomes_payment ON outcomes(paymentId);\nCREATE TABLE diagnostics (\n id TEXT PRIMARY KEY,\n ts TEXT NOT NULL,\n payload TEXT NOT NULL CHECK (json_valid(payload))\n);\nCREATE TRIGGER events_no_update BEFORE UPDATE ON events BEGIN SELECT RAISE(ABORT, 'append-only events'); END;\nCREATE TRIGGER events_no_delete BEFORE DELETE ON events BEGIN SELECT RAISE(ABORT, 'append-only events'); END;\nCREATE TRIGGER outcomes_no_update BEFORE UPDATE ON outcomes BEGIN SELECT RAISE(ABORT, 'append-only outcomes'); END;\nCREATE TRIGGER outcomes_no_delete BEFORE DELETE ON outcomes BEGIN SELECT RAISE(ABORT, 'append-only outcomes'); END;\nCREATE TRIGGER diagnostics_no_update BEFORE UPDATE ON diagnostics BEGIN SELECT RAISE(ABORT, 'append-only diagnostics'); END;\nCREATE TRIGGER diagnostics_no_delete BEFORE DELETE ON diagnostics BEGIN SELECT RAISE(ABORT, 'append-only diagnostics'); END;\n";
803
821
 
822
+ // migrations/002.sql
823
+ var __default2 = "CREATE TABLE cache_cursors (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n eventRowid INTEGER NOT NULL CHECK (eventRowid >= 0),\n outcomeRowid INTEGER NOT NULL CHECK (outcomeRowid >= 0)\n);\nINSERT INTO cache_cursors VALUES (1, 0, 0);\n\nCREATE TABLE cache_payments (\n paymentId TEXT PRIMARY KEY REFERENCES events(id),\n paymentKey TEXT NOT NULL UNIQUE,\n payload TEXT NOT NULL CHECK (json_valid(payload)),\n maxAttemptedAt TEXT\n);\n\nCREATE TABLE cache_attempts (\n paymentId TEXT NOT NULL REFERENCES cache_payments(paymentId),\n attemptId TEXT NOT NULL,\n firstTs TEXT NOT NULL,\n firstId TEXT NOT NULL,\n winnerTs TEXT NOT NULL,\n winnerId TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('unknown', 'confirmed', 'failed')),\n payload TEXT NOT NULL CHECK (json_valid(payload)),\n PRIMARY KEY (paymentId, attemptId)\n);\nCREATE INDEX cache_attempt_selection ON cache_attempts (\n paymentId, status, firstTs, firstId COLLATE NOCASE, firstId DESC\n);\n\nCREATE TABLE cache_prefix (\n partitionKey TEXT NOT NULL,\n prefix TEXT NOT NULL,\n amount TEXT NOT NULL CHECK (amount <> '' AND amount NOT GLOB '*[^0-9]*'),\n PRIMARY KEY (partitionKey, prefix)\n) WITHOUT ROWID;\n\nINSERT INTO schema_version VALUES (2);\n";
824
+
825
+ // src/ledger/backup.ts
826
+ import { mkdtempSync, renameSync, writeFileSync } from "fs";
827
+ import { join as join2, resolve as resolve2 } from "path";
828
+ import Database from "better-sqlite3";
829
+ function backupLedger(path) {
830
+ const directory = mkdtempSync(`${resolve2(path)}.backup-v1-`);
831
+ const partial = join2(directory, "ledger.partial.db");
832
+ const destination = join2(directory, "ledger.db");
833
+ writeFileSync(partial, "", { flag: "wx", mode: 384 });
834
+ const reader = new Database(path, { readonly: true, fileMustExist: true });
835
+ try {
836
+ reader.pragma("synchronous = FULL");
837
+ reader.prepare("VACUUM INTO ?").run(partial);
838
+ } finally {
839
+ reader.close();
840
+ }
841
+ renameSync(partial, destination);
842
+ return destination;
843
+ }
844
+
845
+ // src/ledger/cache.ts
846
+ import { z as z7 } from "zod";
847
+ var cursorSchema = z7.object({
848
+ eventRowid: z7.number().int().nonnegative(),
849
+ outcomeRowid: z7.number().int().nonnegative()
850
+ });
851
+ var sourceSchema = z7.object({
852
+ sourceRowid: z7.number().int().positive(),
853
+ id: z7.string(),
854
+ payload: z7.string()
855
+ });
856
+ var payloadSchema = z7.object({ payload: z7.string() });
857
+ var paymentRowSchema = z7.object({
858
+ payload: z7.string(),
859
+ originalPayload: z7.string(),
860
+ maxAttemptedAt: z7.iso.datetime().nullable()
861
+ });
862
+ var attemptRowSchema = z7.object({
863
+ firstTs: z7.iso.datetime(),
864
+ firstId: z7.uuidv7(),
865
+ winnerTs: z7.iso.datetime(),
866
+ winnerId: z7.uuidv7(),
867
+ status: z7.enum(["unknown", "confirmed", "failed"])
868
+ });
869
+ var prefixRowSchema = z7.object({
870
+ prefix: z7.string().regex(/^[0-9a-f]{0,16}$/),
871
+ amount: integerStringSchema
872
+ });
873
+ var scopeSchema = z7.enum(["perTask", "perAgent", "global"]);
874
+ var windowSchema = z7.enum(["1h", "24h", "7d", "30d"]);
875
+ var timeSchema = z7.number().int().min(-864e13).max(864e13);
876
+ var windows2 = {
877
+ "1h": 36e5,
878
+ "24h": 864e5,
879
+ "7d": 6048e5,
880
+ "30d": 2592e6
881
+ };
882
+ var digits = "0123456789abcdef";
883
+ var bias = 1n << 63n;
884
+ function timeKey(milliseconds) {
885
+ const value = z7.number().int().parse(milliseconds);
886
+ const encoded = bias + BigInt(value);
887
+ if (encoded < 0n || encoded >= 1n << 64n) throw new Error("Timestamp outside cache index range");
888
+ return encoded.toString(16).padStart(16, "0");
889
+ }
890
+ function partition(payment, scope) {
891
+ const attribution = scope === "perTask" ? payment.taskId : scope === "perAgent" ? payment.agentId : null;
892
+ return JSON.stringify([scope, attribution ?? null, payment.network, payment.asset.toLowerCase()]);
893
+ }
894
+ function order2(left, right) {
895
+ return left.ts.localeCompare(right.ts) || left.id.localeCompare(right.id);
896
+ }
897
+ var LedgerCache = class {
898
+ constructor(database) {
899
+ this.database = database;
900
+ }
901
+ database;
902
+ statements = /* @__PURE__ */ new Map();
903
+ statement(sql) {
904
+ let statement = this.statements.get(sql);
905
+ if (!statement) {
906
+ statement = this.database.prepare(sql);
907
+ this.statements.set(sql, statement);
908
+ }
909
+ return statement;
910
+ }
911
+ /** The caller holds an immediate transaction throughout synchronization and budget gating. */
912
+ synchronize() {
913
+ const cursor = cursorSchema.parse(
914
+ this.statement("SELECT eventRowid, outcomeRowid FROM cache_cursors WHERE id = 1").get()
915
+ );
916
+ let eventRowid = cursor.eventRowid;
917
+ let outcomeRowid = cursor.outcomeRowid;
918
+ const eventQuery = this.statement(
919
+ "SELECT rowid AS sourceRowid, id, payload FROM events WHERE rowid > ? ORDER BY rowid LIMIT 256"
920
+ );
921
+ for (let batch = eventQuery.all(eventRowid); batch.length > 0; batch = eventQuery.all(eventRowid)) {
922
+ for (const input of batch) {
923
+ const row = sourceSchema.parse(input);
924
+ const payment = paymentEventSchema.parse(JSON.parse(row.payload));
925
+ if (payment.id !== row.id)
926
+ throw new Error("Payment row identity does not match its payload");
927
+ if (payment.status === "observed") {
928
+ const derived = paymentEventSchema.parse({
929
+ ...payment,
930
+ settlement_unknown: payment.settlementStatus === "unknown"
931
+ });
932
+ const inserted = this.statement(
933
+ "INSERT OR IGNORE INTO cache_payments (paymentId, paymentKey, payload, maxAttemptedAt) VALUES (?, ?, ?, NULL)"
934
+ ).run(payment.id, payment.paymentKey, JSON.stringify(derived));
935
+ if (inserted.changes > 0) this.changeContribution(void 0, derived);
936
+ }
937
+ eventRowid = row.sourceRowid;
938
+ }
939
+ }
940
+ const outcomeQuery = this.statement(
941
+ "SELECT rowid AS sourceRowid, id, payload FROM outcomes WHERE rowid > ? ORDER BY rowid LIMIT 256"
942
+ );
943
+ for (let batch = outcomeQuery.all(outcomeRowid); batch.length > 0; batch = outcomeQuery.all(outcomeRowid)) {
944
+ for (const input of batch) {
945
+ const row = sourceSchema.parse(input);
946
+ const outcome = outcomeSchema.parse(JSON.parse(row.payload));
947
+ if (outcome.id !== row.id)
948
+ throw new Error("Outcome row identity does not match its payload");
949
+ this.applyOutcome(outcome);
950
+ outcomeRowid = row.sourceRowid;
951
+ }
952
+ }
953
+ if (eventRowid !== cursor.eventRowid || outcomeRowid !== cursor.outcomeRowid) {
954
+ this.statement("UPDATE cache_cursors SET eventRowid = ?, outcomeRowid = ? WHERE id = 1").run(
955
+ eventRowid,
956
+ outcomeRowid
957
+ );
958
+ }
959
+ }
960
+ rebuild() {
961
+ this.database.exec(
962
+ "DELETE FROM cache_prefix; DELETE FROM cache_attempts; DELETE FROM cache_payments; UPDATE cache_cursors SET eventRowid = 0, outcomeRowid = 0 WHERE id = 1"
963
+ );
964
+ this.synchronize();
965
+ }
966
+ reservation(paymentKey) {
967
+ const key = z7.string().min(1).parse(paymentKey);
968
+ const row = payloadSchema.optional().parse(this.statement("SELECT payload FROM cache_payments WHERE paymentKey = ?").get(key));
969
+ return row ? paymentEventSchema.parse(JSON.parse(row.payload)) : void 0;
970
+ }
971
+ spent(proposed, inputScope, inputWindow, now) {
972
+ const payment = paymentEventSchema.parse(proposed);
973
+ const scope = scopeSchema.parse(inputScope);
974
+ const window = windowSchema.optional().parse(inputWindow);
975
+ const end = timeSchema.parse(now);
976
+ const key = partition(payment, scope);
977
+ const upper = this.prefixTotal(key, end);
978
+ const lower = window ? this.prefixTotal(key, end - windows2[window] - 1) : 0n;
979
+ const result = upper - lower;
980
+ if (result < 0n) throw new Error("Cached budget total is inconsistent");
981
+ return result.toString();
982
+ }
983
+ applyOutcome(outcome) {
984
+ const row = paymentRowSchema.optional().parse(
985
+ this.statement(
986
+ "SELECT c.payload, c.maxAttemptedAt, e.payload AS originalPayload FROM cache_payments c JOIN events e ON e.id = c.paymentId WHERE c.paymentId = ?"
987
+ ).get(outcome.paymentId)
988
+ );
989
+ if (!row) return;
990
+ const previous = paymentEventSchema.parse(JSON.parse(row.payload));
991
+ const original = paymentEventSchema.parse(JSON.parse(row.originalPayload));
992
+ const attemptId = outcome.attemptId ?? outcome.paymentId;
993
+ const attempt = attemptRowSchema.optional().parse(
994
+ this.statement(
995
+ "SELECT firstTs, firstId, winnerTs, winnerId, status FROM cache_attempts WHERE paymentId = ? AND attemptId = ?"
996
+ ).get(outcome.paymentId, attemptId)
997
+ );
998
+ if (!attempt) {
999
+ this.statement(
1000
+ "INSERT INTO cache_attempts (paymentId, attemptId, firstTs, firstId, winnerTs, winnerId, status, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
1001
+ ).run(
1002
+ outcome.paymentId,
1003
+ attemptId,
1004
+ outcome.ts,
1005
+ outcome.id,
1006
+ outcome.ts,
1007
+ outcome.id,
1008
+ outcome.status,
1009
+ JSON.stringify(outcome)
1010
+ );
1011
+ } else {
1012
+ if (order2(outcome, { ts: attempt.firstTs, id: attempt.firstId }) < 0) {
1013
+ this.statement(
1014
+ "UPDATE cache_attempts SET firstTs = ?, firstId = ? WHERE paymentId = ? AND attemptId = ?"
1015
+ ).run(outcome.ts, outcome.id, outcome.paymentId, attemptId);
1016
+ }
1017
+ const newer = order2(outcome, { ts: attempt.winnerTs, id: attempt.winnerId }) > 0;
1018
+ const wins = attempt.status === "confirmed" ? outcome.status === "confirmed" && newer : outcome.status === "confirmed" || newer;
1019
+ if (wins) {
1020
+ this.statement(
1021
+ "UPDATE cache_attempts SET winnerTs = ?, winnerId = ?, status = ?, payload = ? WHERE paymentId = ? AND attemptId = ?"
1022
+ ).run(
1023
+ outcome.ts,
1024
+ outcome.id,
1025
+ outcome.status,
1026
+ JSON.stringify(outcome),
1027
+ outcome.paymentId,
1028
+ attemptId
1029
+ );
1030
+ }
1031
+ }
1032
+ const maxAttemptedAt = outcome.attemptedAt && (!row.maxAttemptedAt || outcome.attemptedAt > row.maxAttemptedAt) ? outcome.attemptedAt : row.maxAttemptedAt;
1033
+ const selected = this.selectedOutcome(outcome.paymentId);
1034
+ const status = original.settlementStatus === "confirmed" ? "confirmed" : selected?.status ?? original.settlementStatus;
1035
+ const current = paymentEventSchema.parse({
1036
+ ...original,
1037
+ ...maxAttemptedAt ? { attemptedAt: maxAttemptedAt } : {},
1038
+ settlementStatus: status,
1039
+ settlement_unknown: status === "unknown",
1040
+ ...selected?.txHash ? { txHash: selected.txHash } : {}
1041
+ });
1042
+ this.changeContribution(previous, current);
1043
+ this.statement(
1044
+ "UPDATE cache_payments SET payload = ?, maxAttemptedAt = ? WHERE paymentId = ?"
1045
+ ).run(JSON.stringify(current), maxAttemptedAt, outcome.paymentId);
1046
+ }
1047
+ selectedOutcome(paymentId) {
1048
+ for (const status of ["confirmed", "unknown"]) {
1049
+ const row2 = payloadSchema.optional().parse(
1050
+ this.statement(
1051
+ "SELECT payload FROM cache_attempts WHERE paymentId = ? AND status = ? ORDER BY firstTs, firstId COLLATE NOCASE, firstId DESC LIMIT 1"
1052
+ ).get(paymentId, status)
1053
+ );
1054
+ if (row2) return outcomeSchema.parse(JSON.parse(row2.payload));
1055
+ }
1056
+ const row = payloadSchema.optional().parse(
1057
+ this.statement(
1058
+ "SELECT payload FROM cache_attempts WHERE paymentId = ? AND status = 'failed' ORDER BY firstTs DESC, firstId COLLATE NOCASE DESC, firstId ASC LIMIT 1"
1059
+ ).get(paymentId)
1060
+ );
1061
+ return row ? outcomeSchema.parse(JSON.parse(row.payload)) : void 0;
1062
+ }
1063
+ changeContribution(previous, current) {
1064
+ const changes = /* @__PURE__ */ new Map();
1065
+ const add = (payment, sign) => {
1066
+ if (!countsAsSpend(payment)) return;
1067
+ const amount = BigInt(payment.amount) * sign;
1068
+ if (amount === 0n) return;
1069
+ const timestamp = timeKey(Date.parse(payment.attemptedAt ?? payment.ts));
1070
+ for (const scope of ["perTask", "perAgent", "global"]) {
1071
+ const key = partition(payment, scope);
1072
+ const nodes = changes.get(key) ?? /* @__PURE__ */ new Map();
1073
+ for (let length = 0; length <= timestamp.length; length += 1) {
1074
+ const prefix = timestamp.slice(0, length);
1075
+ nodes.set(prefix, (nodes.get(prefix) ?? 0n) + amount);
1076
+ }
1077
+ changes.set(key, nodes);
1078
+ }
1079
+ };
1080
+ if (previous) add(previous, -1n);
1081
+ add(current, 1n);
1082
+ for (const [key, nodes] of changes) {
1083
+ const pending = [...nodes].filter(([, amount]) => amount !== 0n);
1084
+ if (pending.length === 0) continue;
1085
+ const existing = /* @__PURE__ */ new Map();
1086
+ for (const input of this.statement(
1087
+ "SELECT prefix, amount FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
1088
+ ).iterate(key, JSON.stringify(pending.map(([prefix]) => prefix)))) {
1089
+ const row = prefixRowSchema.parse(input);
1090
+ existing.set(row.prefix, BigInt(row.amount));
1091
+ }
1092
+ for (const [prefix, delta] of pending) {
1093
+ const next = (existing.get(prefix) ?? 0n) + delta;
1094
+ if (next < 0n) throw new Error("Cached monetary contribution underflow");
1095
+ if (next === 0n) {
1096
+ this.statement("DELETE FROM cache_prefix WHERE partitionKey = ? AND prefix = ?").run(
1097
+ key,
1098
+ prefix
1099
+ );
1100
+ } else {
1101
+ this.statement(
1102
+ "INSERT INTO cache_prefix (partitionKey, prefix, amount) VALUES (?, ?, ?) ON CONFLICT(partitionKey, prefix) DO UPDATE SET amount = excluded.amount"
1103
+ ).run(key, prefix, next.toString());
1104
+ }
1105
+ }
1106
+ }
1107
+ }
1108
+ prefixTotal(key, time) {
1109
+ const timestamp = timeKey(time);
1110
+ const prefixes = [];
1111
+ for (let index = 0; index < timestamp.length; index += 1) {
1112
+ const digit = timestamp[index];
1113
+ if (digit === void 0) throw new Error("Missing timestamp digit");
1114
+ const limit = digits.indexOf(digit);
1115
+ for (let child = 0; child < limit; child += 1) {
1116
+ prefixes.push(`${timestamp.slice(0, index)}${digits[child]}`);
1117
+ }
1118
+ }
1119
+ prefixes.push(timestamp);
1120
+ let sum = 0n;
1121
+ for (const input of this.statement(
1122
+ "SELECT prefix, amount FROM cache_prefix WHERE partitionKey = ? AND prefix IN (SELECT value FROM json_each(?))"
1123
+ ).iterate(key, JSON.stringify(prefixes))) {
1124
+ sum += BigInt(prefixRowSchema.parse(input).amount);
1125
+ }
1126
+ return sum;
1127
+ }
1128
+ };
1129
+
804
1130
  // src/ledger/store.ts
805
- var rowSchema = z7.object({ payload: z7.string() });
1131
+ var rowSchema = z8.object({ payload: z8.string() });
806
1132
  var Ledger = class {
807
1133
  database;
1134
+ cache;
808
1135
  constructor(input) {
809
- const path = z7.string().min(1).parse(input);
1136
+ const path = z8.string().min(1).parse(input);
810
1137
  if (path !== ":memory:") mkdirSync(dirname2(path), { recursive: true, mode: 448 });
811
- this.database = new Database(path);
1138
+ this.database = new Database2(path);
812
1139
  try {
813
1140
  this.database.pragma("journal_mode = WAL");
814
1141
  this.database.pragma("foreign_keys = ON");
815
1142
  this.database.pragma("busy_timeout = 5000");
816
- this.database.transaction(() => {
1143
+ this.cache = this.database.transaction(() => {
817
1144
  const table = this.database.prepare(
818
1145
  "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_version'"
819
1146
  ).get();
820
1147
  if (!table) this.database.exec(__default);
821
- const version3 = z7.object({ version: z7.literal(1) }).parse(
1148
+ const version3 = z8.object({ version: z8.union([z8.literal(1), z8.literal(2)]) }).parse(
822
1149
  this.database.prepare("SELECT MAX(version) AS version FROM schema_version").get()
823
1150
  );
824
- return version3.version;
1151
+ if (version3.version === 1) {
1152
+ if (table && path !== ":memory:") {
1153
+ process.stderr.write("Migrating ledger\u2026\n");
1154
+ const backup = backupLedger(path);
1155
+ process.stderr.write(`Ledger backup saved: ${backup}
1156
+ `);
1157
+ }
1158
+ this.database.exec(__default2);
1159
+ }
1160
+ const cache = new LedgerCache(this.database);
1161
+ cache.synchronize();
1162
+ return cache;
825
1163
  }).immediate();
826
1164
  } catch (error) {
827
1165
  this.database.close();
@@ -833,22 +1171,49 @@ var Ledger = class {
833
1171
  }
834
1172
  append(input) {
835
1173
  const event = paymentEventSchema.parse(input);
836
- return this.database.prepare(
837
- "INSERT OR IGNORE INTO events (id, ts, taskId, agentId, host, status, paymentKey, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
838
- ).run(
839
- event.id,
840
- event.ts,
841
- event.taskId ?? null,
842
- event.agentId ?? null,
843
- event.host,
844
- event.status,
845
- event.paymentKey,
846
- JSON.stringify(event)
847
- ).changes > 0;
1174
+ return this.transaction(() => {
1175
+ const inserted = this.database.prepare(
1176
+ "INSERT OR IGNORE INTO events (id, ts, taskId, agentId, host, status, paymentKey, payload) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
1177
+ ).run(
1178
+ event.id,
1179
+ event.ts,
1180
+ event.taskId ?? null,
1181
+ event.agentId ?? null,
1182
+ event.host,
1183
+ event.status,
1184
+ event.paymentKey,
1185
+ JSON.stringify(event)
1186
+ ).changes > 0;
1187
+ this.cache.synchronize();
1188
+ return inserted;
1189
+ });
848
1190
  }
849
1191
  appendOutcome(input) {
850
1192
  const outcome = outcomeSchema.parse(input);
851
- return this.database.prepare("INSERT OR IGNORE INTO outcomes (id, ts, paymentId, payload) VALUES (?, ?, ?, ?)").run(outcome.id, outcome.ts, outcome.paymentId, JSON.stringify(outcome)).changes > 0;
1193
+ return this.transaction(() => {
1194
+ const inserted = this.database.prepare(
1195
+ "INSERT OR IGNORE INTO outcomes (id, ts, paymentId, payload) VALUES (?, ?, ?, ?)"
1196
+ ).run(outcome.id, outcome.ts, outcome.paymentId, JSON.stringify(outcome)).changes > 0;
1197
+ this.cache.synchronize();
1198
+ return inserted;
1199
+ });
1200
+ }
1201
+ policyState(proposed, budgets, now) {
1202
+ return this.transaction(() => {
1203
+ this.cache.synchronize();
1204
+ return {
1205
+ reserved: this.cache.reservation(proposed.paymentKey),
1206
+ spent: {
1207
+ perTask: budgets.perTask ? this.cache.spent(proposed, "perTask", budgets.perTask.window, now) : "0",
1208
+ perAgent: budgets.perAgent ? this.cache.spent(proposed, "perAgent", budgets.perAgent.window, now) : "0",
1209
+ global: budgets.global ? this.cache.spent(proposed, "global", budgets.global.window, now) : "0"
1210
+ }
1211
+ };
1212
+ });
1213
+ }
1214
+ /** Rebuild disposable projections without modifying the append-only audit log. */
1215
+ rebuildCache() {
1216
+ this.transaction(() => this.cache.rebuild());
852
1217
  }
853
1218
  events() {
854
1219
  return this.database.prepare("SELECT payload FROM events ORDER BY ts, id").all().map((row) => paymentEventSchema.parse(JSON.parse(rowSchema.parse(row).payload)));
@@ -885,10 +1250,10 @@ import {
885
1250
  } from "http";
886
1251
  import { request as httpsRequest } from "https";
887
1252
  import { connect, Socket } from "net";
888
- import { z as z8 } from "zod";
889
- var incomingHeadersSchema = z8.record(
890
- z8.string(),
891
- z8.union([z8.string(), z8.array(z8.string()), z8.undefined()])
1253
+ import { z as z9 } from "zod";
1254
+ var incomingHeadersSchema = z9.record(
1255
+ z9.string(),
1256
+ z9.union([z9.string(), z9.array(z9.string()), z9.undefined()])
892
1257
  );
893
1258
  var sockets = /* @__PURE__ */ new WeakMap();
894
1259
  function normalizedHeaders(input) {
@@ -922,7 +1287,7 @@ function forwardingHeaders(message, host, upgrade = false) {
922
1287
  return result;
923
1288
  }
924
1289
  function targetFor(request, config) {
925
- const reference = z8.string().min(1).parse(request.url);
1290
+ const reference = z9.string().min(1).parse(request.url);
926
1291
  const absolute = /^https?:\/\//i.test(reference);
927
1292
  const url = new URL(
928
1293
  httpUrlSchema.parse(
@@ -931,13 +1296,13 @@ function targetFor(request, config) {
931
1296
  );
932
1297
  const suffix = absolute ? reference.replace(/^https?:\/\/[^/?#]*/i, "") : reference.startsWith("/") ? reference : `${url.pathname}${url.search}`;
933
1298
  const withoutFragment = suffix.split("#", 1)[0] ?? "";
934
- const path = z8.string().min(1).parse(withoutFragment.startsWith("/") ? withoutFragment : `/${withoutFragment}`);
1299
+ const path = z9.string().min(1).parse(withoutFragment.startsWith("/") ? withoutFragment : `/${withoutFragment}`);
935
1300
  return { url, path, resource: httpUrlSchema.parse(`${url.origin}${path}`) };
936
1301
  }
937
1302
  function wire(request, target) {
938
1303
  return {
939
1304
  url: target.resource,
940
- method: z8.string().parse(request.method),
1305
+ method: z9.string().parse(request.method),
941
1306
  headers: normalizedHeaders(request.headers)
942
1307
  };
943
1308
  }
@@ -986,7 +1351,7 @@ function createProxy(options) {
986
1351
  if (status === 402 && !needsBody) meter.observe(input, upstream);
987
1352
  if (needsBody)
988
1353
  reply.on("data", (chunk) => {
989
- const bytes = z8.instanceof(Buffer).parse(chunk);
1354
+ const bytes = z9.instanceof(Buffer).parse(chunk);
990
1355
  length += bytes.length;
991
1356
  if (length <= 65536) chunks.push(bytes);
992
1357
  else chunks.length = 0;
@@ -1027,9 +1392,9 @@ function createProxy(options) {
1027
1392
  });
1028
1393
  server.on("connect", (request, client, head) => {
1029
1394
  try {
1030
- const address = z8.string().regex(/^(?:\[[0-9a-fA-F:]+\]|[^\s/:?#@]+):[0-9]+$/).parse(request.url);
1395
+ const address = z9.string().regex(/^(?:\[[0-9a-fA-F:]+\]|[^\s/:?#@]+):[0-9]+$/).parse(request.url);
1031
1396
  const target = new URL(`http://${address}`);
1032
- const port = z8.coerce.number().int().min(1).max(65535).parse(target.port || "80");
1397
+ const port = z9.coerce.number().int().min(1).max(65535).parse(target.port || "80");
1033
1398
  const host = target.hostname.replace(/^\[|\]$/g, "");
1034
1399
  meter.diagnose(
1035
1400
  "tls_unmetered",
@@ -1132,18 +1497,18 @@ ${JSON.stringify(intake.body)}`
1132
1497
  async function closeProxy(server) {
1133
1498
  for (const socket of sockets.get(server) ?? []) socket.destroy();
1134
1499
  await new Promise(
1135
- (resolve2, reject) => server.close((error) => error ? reject(error) : resolve2())
1500
+ (resolve3, reject) => server.close((error) => error ? reject(error) : resolve3())
1136
1501
  );
1137
1502
  }
1138
1503
 
1139
1504
  // src/version.ts
1140
- import { z as z9 } from "zod";
1505
+ import { z as z10 } from "zod";
1141
1506
 
1142
1507
  // package.json
1143
- var version = "0.1.2";
1508
+ var version = "0.2.1";
1144
1509
 
1145
1510
  // src/version.ts
1146
- var version2 = z9.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/).parse(version);
1511
+ var version2 = z10.string().regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/).parse(version);
1147
1512
 
1148
1513
  export {
1149
1514
  integerStringSchema,
package/dist/cli/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  totalSchema,
23
23
  totals,
24
24
  version
25
- } from "../chunk-DWBUWM3G.js";
25
+ } from "../chunk-DF4YCPFO.js";
26
26
 
27
27
  // src/cli/index.ts
28
28
  import { CommanderError } from "commander";