@hasna/economy 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
  3. package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
  4. package/dist/chunks/{index-d480p2wh.js → index-1vz1arz5.js} +14 -13
  5. package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
  6. package/dist/chunks/{index-49f12css.js → index-bn04v378.js} +2 -2
  7. package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
  8. package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
  9. package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
  10. package/dist/chunks/{index-r1jv3kpy.js → index-mhrkwvtk.js} +3 -3
  11. package/dist/chunks/{index-xsah93s2.js → index-s9mvven6.js} +2 -2
  12. package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
  13. package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
  14. package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
  15. package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
  16. package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
  17. package/dist/chunks/{serve-2knem98x.js → serve-8yqe7nhs.js} +44 -11
  18. package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-13nhr1z0.js} +2 -2
  19. package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-gamn65xp.js} +2 -2
  20. package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
  21. package/dist/chunks/{tui-khmnkat2.js → tui-fe94bvp9.js} +4 -4
  22. package/dist/chunks/{watch-4y064yys.js → watch-ct8dvrpr.js} +5 -5
  23. package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
  24. package/dist/cli/index.js +27 -27
  25. package/dist/db/database.d.ts +2 -2
  26. package/dist/db/database.d.ts.map +1 -1
  27. package/dist/db/legacy-large-schema.d.ts +6 -0
  28. package/dist/db/legacy-large-schema.d.ts.map +1 -0
  29. package/dist/db/pg-migrations.d.ts.map +1 -1
  30. package/dist/db/sync-pg.d.ts.map +1 -1
  31. package/dist/index.js +15 -14
  32. package/dist/lib/analytics.d.ts +1 -1
  33. package/dist/lib/analytics.d.ts.map +1 -1
  34. package/dist/lib/periods.d.ts +20 -1
  35. package/dist/lib/periods.d.ts.map +1 -1
  36. package/dist/lib/spikes.d.ts +3 -3
  37. package/dist/lib/spikes.d.ts.map +1 -1
  38. package/dist/mcp/index.js +18 -17
  39. package/dist/otel/index.js +1 -1
  40. package/dist/server/index.js +213 -74
  41. package/dist/server/legacy-limits.d.ts +51 -0
  42. package/dist/server/legacy-limits.d.ts.map +1 -0
  43. package/dist/server/legacy-operations.d.ts +2 -1
  44. package/dist/server/legacy-operations.d.ts.map +1 -1
  45. package/dist/server/legacy-plan.d.ts +2 -1
  46. package/dist/server/legacy-plan.d.ts.map +1 -1
  47. package/dist/server/legacy-preservation.d.ts +2 -1
  48. package/dist/server/legacy-preservation.d.ts.map +1 -1
  49. package/dist/server/service-operations.d.ts.map +1 -1
  50. package/dist/server/service-worker.js +78 -33
  51. package/docs/historical-data-reconciliation.md +38 -0
  52. package/package.json +1 -1
@@ -1118,12 +1118,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
1118
1118
  function ymd(date2) {
1119
1119
  return date2.toISOString().substring(0, 10);
1120
1120
  }
1121
- function calendarPeriodBounds(period, now = new Date) {
1121
+ function resolveReportingInterval(interval, now = new Date) {
1122
1122
  if (!Number.isFinite(now.getTime()))
1123
1123
  throw new RangeError("Invalid reporting clock");
1124
- const start = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
1124
+ const start = new Date(now);
1125
+ start.setUTCHours(0, 0, 0, 0);
1125
1126
  const end = new Date(start);
1126
- switch (period) {
1127
+ if (interval.kind === "rolling") {
1128
+ if (!Number.isSafeInteger(interval.count) || interval.count < 1)
1129
+ throw new RangeError("Invalid reporting window");
1130
+ if (interval.unit === "hour") {
1131
+ start.setTime(now.getTime() - interval.count * 3600000);
1132
+ if (!Number.isFinite(start.getTime()))
1133
+ throw new RangeError("Invalid reporting window");
1134
+ return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
1135
+ }
1136
+ if (interval.unit !== "day")
1137
+ throw new RangeError("Invalid reporting unit");
1138
+ start.setUTCDate(start.getUTCDate() - interval.count + 1);
1139
+ end.setUTCDate(end.getUTCDate() + 1);
1140
+ if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
1141
+ throw new RangeError("Invalid reporting window");
1142
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1143
+ }
1144
+ if (interval.kind !== "calendar")
1145
+ throw new RangeError("Invalid reporting interval");
1146
+ switch (interval.period) {
1127
1147
  case "today":
1128
1148
  end.setUTCDate(end.getUTCDate() + 1);
1129
1149
  break;
@@ -1146,11 +1166,15 @@ function calendarPeriodBounds(period, now = new Date) {
1146
1166
  end.setUTCFullYear(end.getUTCFullYear() + 1);
1147
1167
  break;
1148
1168
  case "all":
1149
- return { time_zone: "UTC", start_date: null, end_date_exclusive: null };
1169
+ return { time_zone: "UTC", start: null, end_exclusive: null };
1150
1170
  default:
1151
1171
  throw new RangeError("Invalid reporting period");
1152
1172
  }
1153
- return { time_zone: "UTC", start_date: ymd(start), end_date_exclusive: ymd(end) };
1173
+ return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
1174
+ }
1175
+ function calendarPeriodBounds(period, now = new Date) {
1176
+ const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
1177
+ return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
1154
1178
  }
1155
1179
  function calendarPeriodWhere(period, column, now = new Date) {
1156
1180
  if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
@@ -2209,20 +2233,22 @@ function queryAccountBreakdownSince(db, since) {
2209
2233
  ORDER BY api_equivalent_usd DESC
2210
2234
  `).all(since, since);
2211
2235
  }
2212
- function queryDailyBreakdown(db, days = 30, machine) {
2236
+ function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
2237
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
2213
2238
  const machineClause = machine ? " AND machine_id = ?" : "";
2214
- const params = machine ? [`-${days}`, machine] : [`-${days}`];
2239
+ const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
2215
2240
  return db.prepare(`
2216
2241
  SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
2217
2242
  FROM economy_effective_requests
2218
- WHERE timestamp >= DATE('now', ? || ' days')${machineClause}
2243
+ WHERE timestamp >= ? AND timestamp < ?${machineClause}
2219
2244
  GROUP BY DATE(timestamp), agent
2220
2245
  ORDER BY date ASC
2221
2246
  `).all(...params);
2222
2247
  }
2223
- function queryHourlyBreakdown(db, machine, hours) {
2224
- const clauses = hours == null ? [`DATE(timestamp) = DATE('now')`] : [`DATETIME(timestamp) >= DATETIME('now', ?)`];
2225
- const params = hours == null ? [] : [`-${hours} hours`];
2248
+ function queryHourlyBreakdown(db, machine, hours, now = new Date) {
2249
+ const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
2250
+ const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
2251
+ const params = [bounds.start, bounds.end_exclusive];
2226
2252
  if (machine) {
2227
2253
  clauses.push("machine_id = ?");
2228
2254
  params.push(machine);
@@ -3143,15 +3169,16 @@ function translateSqliteDates(sql) {
3143
3169
  s = s.replace(/DATE\('now',\s*(\$\d+)\s*\|\|\s*' days'\)/gi, (_m, p) => isoDate(`now() + ((${p}) || ' days')::interval`));
3144
3170
  s = s.replace(/DATETIME\('now',\s*(\$\d+)\)/gi, (_m, p) => `(now() + (${p})::interval)`);
3145
3171
  s = s.replace(/DATETIME\('now',\s*'([^']+)'\)/gi, (_m, lit) => `(now() + interval '${lit}')`);
3172
+ s = s.replace(/STRFTIME\('%Y-%m-%dT%H:%M:%fZ',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, col) => `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`);
3146
3173
  const strftimeMap = { "%H": "HH24", "%Y": "YYYY", "%m": "MM", "%d": "DD", "%M": "MI", "%S": "SS" };
3147
3174
  s = s.replace(/STRFTIME\('([^']+)',\s*([A-Za-z_][A-Za-z0-9_]*)\)/gi, (_m, fmt, col) => {
3148
3175
  const pgFmt = Object.entries(strftimeMap).reduce((acc, [k, v]) => acc.split(k).join(v), fmt);
3149
- return `to_char(CAST("${col}" AS timestamptz), '${pgFmt}')`;
3176
+ return `to_char(CAST("${col}" AS timestamptz) AT TIME ZONE 'UTC', '${pgFmt}')`;
3150
3177
  });
3151
3178
  s = s.replace(/\bDATE\('now'\)/gi, isoDate("now()"));
3152
3179
  s = s.replace(/\bDATETIME\('now'\)/gi, "now()");
3153
3180
  s = s.replace(/\bDATETIME\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `CAST("${col}" AS timestamptz)`);
3154
- s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/gi, (_m, col) => `substr("${col}", 1, 10)`);
3181
+ s = s.replace(/\bDATE\(\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\s*\)/gi, (_m, col) => `substr(${col.split(".").map((part) => `"${part}"`).join(".")}, 1, 10)`);
3155
3182
  return s;
3156
3183
  }
3157
3184
  function resolveWorkerPath() {
@@ -3360,6 +3387,30 @@ function resolvePgConnection(dsn) {
3360
3387
  }
3361
3388
  var init_pg_connection = () => {};
3362
3389
 
3390
+ // src/db/legacy-large-schema.ts
3391
+ var LEGACY_LARGE_SESSION_PG_SQL = `
3392
+ CREATE INDEX idx_legacy_targets_session_operation
3393
+ ON legacy_reconciliation_targets(session_id,operation_key);
3394
+ CREATE OR REPLACE FUNCTION economy_legacy_target_guard() RETURNS trigger LANGUAGE plpgsql AS $body$
3395
+ BEGIN
3396
+ IF EXISTS(
3397
+ SELECT 1 FROM (SELECT DISTINCT session_id,operation_key FROM inserted_legacy_targets) n
3398
+ JOIN economy_legacy_active a ON a.session_id=n.session_id AND a.operation_key<>n.operation_key
3399
+ ) OR EXISTS(
3400
+ SELECT 1 FROM inserted_legacy_targets n
3401
+ JOIN economy_legacy_active a ON a.request_id=n.request_id AND a.operation_key<>n.operation_key
3402
+ ) THEN
3403
+ RAISE EXCEPTION 'legacy reconciliation is already active for this session' USING ERRCODE='23000';
3404
+ END IF;
3405
+ RETURN NULL;
3406
+ END
3407
+ $body$;
3408
+ DROP TRIGGER legacy_target_no_overlap ON legacy_reconciliation_targets;
3409
+ CREATE TRIGGER legacy_target_no_overlap AFTER INSERT ON legacy_reconciliation_targets
3410
+ REFERENCING NEW TABLE AS inserted_legacy_targets
3411
+ FOR EACH STATEMENT EXECUTE FUNCTION economy_legacy_target_guard();
3412
+ `;
3413
+
3363
3414
  // src/db/pg-migrations.ts
3364
3415
  var PG_MIGRATIONS;
3365
3416
  var init_pg_migrations = __esm(() => {
@@ -3595,7 +3646,8 @@ var init_pg_migrations = __esm(() => {
3595
3646
  ...OBSERVATION_PG_MIGRATIONS,
3596
3647
  `ALTER TABLE budgets ADD COLUMN IF NOT EXISTS metric TEXT NOT NULL DEFAULT 'legacy_request_cost_usd'
3597
3648
  CHECK (metric IN ('legacy_request_cost_usd', 'metered_api_usd'))`,
3598
- LEGACY_PG_SQL
3649
+ LEGACY_PG_SQL,
3650
+ LEGACY_LARGE_SESSION_PG_SQL
3599
3651
  ];
3600
3652
  });
3601
3653
 
@@ -4004,12 +4056,21 @@ var init_config = __esm(() => {
4004
4056
  function detectCostSpikes(dailyTotals, opts) {
4005
4057
  const windowDays = opts?.windowDays ?? 7;
4006
4058
  const multiplier = opts?.multiplier ?? 2;
4059
+ if (!Number.isSafeInteger(windowDays) || windowDays < 1)
4060
+ throw new RangeError("Invalid spike window");
4061
+ if (!Number.isFinite(multiplier) || multiplier <= 0)
4062
+ throw new RangeError("Invalid spike multiplier");
4007
4063
  const sorted = [...dailyTotals].sort((a, b) => a.date.localeCompare(b.date));
4008
4064
  const spikes = [];
4009
4065
  for (let i = windowDays;i < sorted.length; i++) {
4010
4066
  const window = sorted.slice(i - windowDays, i);
4011
- const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
4012
4067
  const current = sorted[i];
4068
+ const previous = new Date(current.date + "T00:00:00.000Z");
4069
+ previous.setUTCDate(previous.getUTCDate() - 1);
4070
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: windowDays }, previous);
4071
+ if (window[0].date !== bounds.start || window[window.length - 1].date !== previous.toISOString().slice(0, 10) || new Set(window.map((day) => day.date)).size !== windowDays)
4072
+ continue;
4073
+ const avg = window.reduce((s, d) => s + d.cost_usd, 0) / window.length;
4013
4074
  if (avg > 0 && current.cost_usd > avg * multiplier) {
4014
4075
  spikes.push({
4015
4076
  date: current.date,
@@ -4021,21 +4082,23 @@ function detectCostSpikes(dailyTotals, opts) {
4021
4082
  }
4022
4083
  return spikes;
4023
4084
  }
4024
- function queryRecentSpikes(db, days = 14) {
4085
+ function queryRecentSpikes(db, days = 14, now = new Date) {
4086
+ const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
4025
4087
  const rows = db.prepare(`
4026
4088
  SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd), 0) as cost_usd
4027
4089
  FROM economy_effective_requests
4028
- WHERE timestamp >= DATE('now', ? || ' days')
4090
+ WHERE timestamp >= ? AND timestamp < ?
4029
4091
  GROUP BY DATE(timestamp)
4030
4092
  ORDER BY date ASC
4031
- `).all(-days);
4093
+ `).all(bounds.start, bounds.end_exclusive);
4032
4094
  return detectCostSpikes(rows);
4033
4095
  }
4034
- function getTodaySpike(db) {
4035
- const today = new Date().toISOString().substring(0, 10);
4036
- const spikes = queryRecentSpikes(db, 14);
4096
+ function getTodaySpike(db, now = new Date) {
4097
+ const today = resolveReportingInterval({ kind: "calendar", period: "today" }, now).start;
4098
+ const spikes = queryRecentSpikes(db, 14, now);
4037
4099
  return spikes.find((s) => s.date === today) ?? null;
4038
4100
  }
4101
+ var init_spikes = () => {};
4039
4102
 
4040
4103
  // src/lib/webhooks.ts
4041
4104
  var exports_webhooks = {};
@@ -4102,6 +4165,7 @@ async function fireWebhook(url, payload) {
4102
4165
  var init_webhooks = __esm(() => {
4103
4166
  init_config();
4104
4167
  init_database();
4168
+ init_spikes();
4105
4169
  });
4106
4170
 
4107
4171
  // src/db/pg-migrate.ts
@@ -4508,6 +4572,42 @@ var init_legacy_inventory = __esm(() => {
4508
4572
  allCandidates = LEGACY_FAMILIES.map((family) => `(${predicates[family]})`).join(" OR ");
4509
4573
  });
4510
4574
 
4575
+ // src/server/legacy-limits.ts
4576
+ function legacyMode(kind, schema) {
4577
+ if (schema === LEGACY_STANDARD[kind])
4578
+ return LEGACY_STANDARD;
4579
+ if (schema === LEGACY_SINGLE_SESSION[kind])
4580
+ return LEGACY_SINGLE_SESSION;
4581
+ throw Error(`Unsupported legacy ${kind} schema`);
4582
+ }
4583
+ var LEGACY_STANDARD, LEGACY_SINGLE_SESSION;
4584
+ var init_legacy_limits = __esm(() => {
4585
+ LEGACY_STANDARD = {
4586
+ selection: "economy.legacy-selection.v1",
4587
+ plan: "economy.legacy-plan.v1",
4588
+ capsule: "economy.legacy-preservation.v1",
4589
+ receipt: "economy.legacy-operation-receipt.v1",
4590
+ items: 100,
4591
+ sessions: 100,
4592
+ members: 1e4,
4593
+ inputBytes: 256 * 1024,
4594
+ originalBytes: 16 * 1024 * 1024,
4595
+ capsuleBytes: 32 * 1024 * 1024
4596
+ };
4597
+ LEGACY_SINGLE_SESSION = {
4598
+ selection: "economy.legacy-session-selection.v1",
4599
+ plan: "economy.legacy-session-plan.v1",
4600
+ capsule: "economy.legacy-session-preservation.v1",
4601
+ receipt: "economy.legacy-session-operation-receipt.v1",
4602
+ items: 50000,
4603
+ sessions: 1,
4604
+ members: 50000,
4605
+ inputBytes: 32 * 1024 * 1024,
4606
+ originalBytes: 64 * 1024 * 1024,
4607
+ capsuleBytes: 128 * 1024 * 1024
4608
+ };
4609
+ });
4610
+
4511
4611
  // src/server/legacy-plan.ts
4512
4612
  var exports_legacy_plan = {};
4513
4613
  __export(exports_legacy_plan, {
@@ -4543,10 +4643,11 @@ function digest2(value) {
4543
4643
  }
4544
4644
  function parseLegacySelection(value) {
4545
4645
  const input = object(value, ["schema", "items"], "selection");
4546
- if (input.schema !== "economy.legacy-selection.v1")
4547
- throw Error("schema must be economy.legacy-selection.v1");
4548
- if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > 100)
4549
- throw Error("items must contain 1-100 explicit request selections");
4646
+ const mode = legacyMode("selection", input.schema);
4647
+ if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > mode.items)
4648
+ throw Error(`items must contain 1-${mode.items} explicit request selections`);
4649
+ if (Buffer.byteLength(JSON.stringify(input)) > mode.inputBytes)
4650
+ throw Error(`Selection exceeds its ${mode.inputBytes}-byte artifact bound`);
4550
4651
  const ids = new Set;
4551
4652
  const items = input.items.map((item) => {
4552
4653
  const row = object(item, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref"], "item");
@@ -4569,7 +4670,7 @@ function parseLegacySelection(value) {
4569
4670
  evidence_ref: text(row.evidence_ref, 512, "evidence_ref")
4570
4671
  };
4571
4672
  }).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
4572
- return { schema: "economy.legacy-selection.v1", items };
4673
+ return { schema: mode.selection, items };
4573
4674
  }
4574
4675
  function readLegacySelectionFile(path) {
4575
4676
  if (!isAbsolute(path) || path.includes("\x00"))
@@ -4582,9 +4683,10 @@ function readLegacySelectionFile(path) {
4582
4683
  }
4583
4684
  try {
4584
4685
  const before = fstatSync(fd);
4686
+ const sizeError = "--input must be a regular JSON file of 1-262144 bytes for standard selection, or at most 33554432 bytes for explicit single-session selection";
4585
4687
  if (!before.isFile() || before.size < 1 || before.size > INPUT_MAX)
4586
- throw Error("--input must be a regular JSON file of 1-262144 bytes");
4587
- const bytes = Buffer.alloc(INPUT_MAX + 1);
4688
+ throw Error(sizeError);
4689
+ const bytes = Buffer.alloc(Math.min(INPUT_MAX + 1, before.size + 1));
4588
4690
  let offset = 0, n;
4589
4691
  while (offset < bytes.length && (n = readSync(fd, bytes, offset, bytes.length - offset, null)) > 0)
4590
4692
  offset += n;
@@ -4595,9 +4697,12 @@ function readLegacySelectionFile(path) {
4595
4697
  try {
4596
4698
  value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, offset)));
4597
4699
  } catch {
4598
- throw Error("--input must contain valid UTF-8 JSON");
4700
+ throw Error(before.size > LEGACY_STANDARD.inputBytes ? sizeError : "--input must contain valid UTF-8 JSON");
4599
4701
  }
4600
- return parseLegacySelection(value);
4702
+ const selection = parseLegacySelection(value);
4703
+ if (offset > legacyMode("selection", selection.schema).inputBytes)
4704
+ throw Error(sizeError);
4705
+ return selection;
4601
4706
  } finally {
4602
4707
  closeSync(fd);
4603
4708
  }
@@ -4612,6 +4717,7 @@ async function prepareLegacyPlan(client, input) {
4612
4717
  }
4613
4718
  async function withLegacyPlanSnapshot(client, input, read) {
4614
4719
  const selection = parseLegacySelection(input);
4720
+ const mode = legacyMode("selection", selection.schema);
4615
4721
  let began = false;
4616
4722
  try {
4617
4723
  await client.query("BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY");
@@ -4646,14 +4752,16 @@ async function withLegacyPlanSnapshot(client, input, read) {
4646
4752
  return { ...item, session_id: identifier2(row.session_id, "session_id") };
4647
4753
  });
4648
4754
  const sessionIds = [...new Set(items.map((x) => x.session_id))].sort();
4755
+ if (mode.sessions === 1 && sessionIds.length !== 1)
4756
+ throw Error("Single-session reconciliation requires exactly one session");
4649
4757
  const sessions = (await client.query(`SELECT id, encode(sha256(convert_to(row_to_json(sessions)::text,'UTF8')),'hex') AS original_sha256
4650
4758
  FROM sessions WHERE id=ANY($1::text[]) ORDER BY id`, [sessionIds])).rows;
4651
4759
  if (sessions.length !== sessionIds.length)
4652
4760
  throw Error("A selected request has no session; resolve that inconsistency before reconciliation");
4653
4761
  const members = (await client.query(`SELECT id, session_id, encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256
4654
- FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${MEMBERS_MAX + 1}`, [sessionIds])).rows;
4655
- if (members.length > MEMBERS_MAX)
4656
- throw Error("Selected sessions exceed 10000 requests; split the selection by session or use a separately reviewed large-session procedure");
4762
+ FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C", id COLLATE "C" LIMIT ${mode.members + 1}`, [sessionIds])).rows;
4763
+ if (members.length > mode.members)
4764
+ throw Error(`Selected sessions exceed ${mode.members} requests; complete-session reconciliation cannot truncate membership`);
4657
4765
  const populations = sessions.map((session) => {
4658
4766
  const rows = members.filter((row) => row.session_id === session.id);
4659
4767
  if (!rows.length)
@@ -4663,7 +4771,7 @@ async function withLegacyPlanSnapshot(client, input, read) {
4663
4771
  return { id: session.id, original_sha256, request_count: rows.length, members_sha256 };
4664
4772
  });
4665
4773
  const body = {
4666
- schema: "economy.legacy-plan.v1",
4774
+ schema: mode.plan,
4667
4775
  mode: "read-only",
4668
4776
  original_encoding: "postgres-row-json-text-v1",
4669
4777
  member_encoding: "sha256-json-id-digest-pairs-utf8-c-order-v1",
@@ -4707,10 +4815,11 @@ async function runLegacyPlan(selection) {
4707
4815
  await pool.end();
4708
4816
  }
4709
4817
  }
4710
- var INPUT_MAX, MEMBERS_MAX = 1e4, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
4818
+ var INPUT_MAX, sha256 = (text) => createHash6("sha256").update(text).digest("hex");
4711
4819
  var init_legacy_plan = __esm(() => {
4712
4820
  init_legacy_inventory();
4713
- INPUT_MAX = 256 * 1024;
4821
+ init_legacy_limits();
4822
+ INPUT_MAX = LEGACY_SINGLE_SESSION.inputBytes;
4714
4823
  });
4715
4824
 
4716
4825
  // src/server/legacy-preservation.ts
@@ -4731,15 +4840,16 @@ import { closeSync as closeSync2, constants as constants2, fstatSync as fstatSyn
4731
4840
  import { basename as basename4, dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
4732
4841
  function parseLegacyPlan(value) {
4733
4842
  const p = record(value);
4843
+ const mode = legacyMode("plan", p.schema);
4734
4844
  fields(p, ["schema", "mode", "original_encoding", "member_encoding", "snapshot", "items", "sessions", "mutations_performed", "limitations", "plan_sha256"]);
4735
- if (p.schema !== "economy.legacy-plan.v1" || p.mode !== "read-only" || p.original_encoding !== "postgres-row-json-text-v1" || p.member_encoding !== "sha256-json-id-digest-pairs-utf8-c-order-v1" || p.mutations_performed !== 0)
4845
+ if (p.mode !== "read-only" || p.original_encoding !== "postgres-row-json-text-v1" || p.member_encoding !== "sha256-json-id-digest-pairs-utf8-c-order-v1" || p.mutations_performed !== 0)
4736
4846
  fail("Unsupported legacy plan contract");
4737
4847
  const { plan_sha256, ...body } = p;
4738
4848
  if (hash(plan_sha256) !== sha2(JSON.stringify(body)))
4739
4849
  fail("Legacy plan digest mismatch");
4740
4850
  if (!Array.isArray(p.items))
4741
4851
  fail("Legacy plan items must be an array");
4742
- const selection = parseLegacySelection({ schema: "economy.legacy-selection.v1", items: p.items.map((item) => {
4852
+ const selection = parseLegacySelection({ schema: mode.selection, items: p.items.map((item) => {
4743
4853
  const row = record(item);
4744
4854
  fields(row, ["id", "family", "original_sha256", "disposition", "reason", "evidence_ref", "session_id"]);
4745
4855
  identifier3(row.session_id);
@@ -4767,7 +4877,7 @@ function parseLegacyPlan(value) {
4767
4877
  if (typeof t.relation_oid !== "string" || !/^[0-9]+$/.test(t.relation_oid))
4768
4878
  fail("Invalid relation locator");
4769
4879
  }
4770
- if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > 100)
4880
+ if (!Array.isArray(p.sessions) || p.sessions.length < 1 || p.sessions.length > mode.sessions)
4771
4881
  fail("Invalid legacy session population");
4772
4882
  const ids = new Set;
4773
4883
  let members = 0;
@@ -4784,7 +4894,7 @@ function parseLegacyPlan(value) {
4784
4894
  fail("Invalid legacy member count");
4785
4895
  members += s.request_count;
4786
4896
  }
4787
- if (members > MAX_MEMBERS || p.items.some((x) => !ids.has(x.session_id)) || ids.size !== new Set(p.items.map((x) => x.session_id)).size)
4897
+ if (members > mode.members || p.items.some((x) => !ids.has(x.session_id)) || ids.size !== new Set(p.items.map((x) => x.session_id)).size)
4788
4898
  fail("Incomplete or excessive legacy session membership");
4789
4899
  if (!Array.isArray(p.limitations) || p.limitations.length > 20 || p.limitations.some((x) => typeof x !== "string" || x.length > 2048))
4790
4900
  fail("Invalid legacy plan limitations");
@@ -4801,16 +4911,28 @@ function assertLegacyPlanCurrent(expected, current) {
4801
4911
  }
4802
4912
  function parseLegacyCapsule(value) {
4803
4913
  const c = record(value);
4914
+ const mode = legacyMode("capsule", c.schema);
4804
4915
  fields(c, ["schema", "plan", "captured_at", "requests", "sessions", "capsule_sha256"]);
4805
- if (c.schema !== "economy.legacy-preservation.v1" || typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
4916
+ if (typeof c.captured_at !== "string" || c.captured_at.length > 100 || !c.captured_at)
4806
4917
  fail("Invalid preservation capsule");
4807
4918
  const { capsule_sha256, ...body } = c;
4808
4919
  const bytes = JSON.stringify(body);
4809
- if (Buffer.byteLength(bytes) > MAX_BYTES || hash(capsule_sha256) !== sha2(bytes))
4920
+ if (Buffer.byteLength(bytes) > mode.capsuleBytes || hash(capsule_sha256) !== sha2(bytes))
4810
4921
  fail("Preservation capsule bound or digest mismatch");
4811
4922
  const plan = parseLegacyPlan(c.plan);
4812
- if (!Array.isArray(c.requests) || c.requests.length > MAX_MEMBERS || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
4923
+ if (plan.schema !== mode.plan)
4924
+ fail("Preservation capsule and plan use different artifact modes");
4925
+ if (!Array.isArray(c.requests) || c.requests.length > mode.members || !Array.isArray(c.sessions) || c.sessions.length !== plan.sessions.length)
4813
4926
  fail("Invalid original populations");
4927
+ let originalBytes = 0;
4928
+ const boundOriginal = (original) => {
4929
+ if (typeof original !== "string")
4930
+ fail("Invalid original row text");
4931
+ originalBytes += Buffer.byteLength(original);
4932
+ if (originalBytes > mode.originalBytes)
4933
+ fail("Preserved original row text exceeds its artifact mode bound");
4934
+ return original;
4935
+ };
4814
4936
  const requests = new Map;
4815
4937
  for (const value2 of c.requests) {
4816
4938
  const row = record(value2);
@@ -4818,7 +4940,7 @@ function parseLegacyCapsule(value) {
4818
4940
  const id = identifier3(row.id);
4819
4941
  if (requests.has(id) || typeof row.original !== "string")
4820
4942
  fail("Invalid or duplicate request original");
4821
- const original = record(JSON.parse(row.original));
4943
+ const original = record(JSON.parse(boundOriginal(row.original)));
4822
4944
  if (original.id !== id)
4823
4945
  fail("Request original identity mismatch");
4824
4946
  requests.set(id, { session: identifier3(original.session_id), digest: sha2(row.original) });
@@ -4832,7 +4954,7 @@ function parseLegacyCapsule(value) {
4832
4954
  fail("Invalid or duplicate session original");
4833
4955
  sessions.add(id);
4834
4956
  const expected = plan.sessions.find((s) => s.id === id) ?? fail("Session original does not match the plan");
4835
- if (record(JSON.parse(row.original)).id !== id || sha2(row.original) !== expected.original_sha256)
4957
+ if (record(JSON.parse(boundOriginal(row.original))).id !== id || sha2(row.original) !== expected.original_sha256)
4836
4958
  fail("Session original does not match the plan");
4837
4959
  const members = [...requests].filter(([, r]) => r.session === id).sort(([a], [b]) => cOrder(a, b)).map(([rid, r]) => [rid, r.digest]);
4838
4960
  if (members.length !== expected.request_count || sha2(JSON.stringify(members)) !== expected.members_sha256)
@@ -4849,18 +4971,19 @@ function parseLegacyCapsule(value) {
4849
4971
  }
4850
4972
  async function preserveLegacyOriginals(client, input) {
4851
4973
  const expected = parseLegacyPlan(input);
4852
- const selection = { schema: "economy.legacy-selection.v1", items: expected.items.map(({ session_id, ...item }) => item) };
4974
+ const mode = legacyMode("plan", expected.schema);
4975
+ const selection = { schema: mode.selection, items: expected.items.map(({ session_id, ...item }) => item) };
4853
4976
  return withLegacyPlanSnapshot(client, selection, async (current) => {
4854
4977
  assertLegacyPlanCurrent(expected, current);
4855
4978
  const ids = current.sessions.map((x) => x.id);
4856
4979
  const sizes = (await client.query(`SELECT
4857
4980
  (SELECT COALESCE(SUM(octet_length(row_to_json(requests)::text)),0) FROM requests WHERE session_id=ANY($1::text[]))::text AS request_bytes,
4858
4981
  (SELECT COALESCE(SUM(octet_length(row_to_json(sessions)::text)),0) FROM sessions WHERE id=ANY($1::text[]))::text AS session_bytes`, [ids])).rows[0];
4859
- if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(MAX_BYTES / 2))
4860
- fail("Preservation exceeds 16 MiB of original row text; split by session");
4861
- const requests = (await client.query('SELECT id,row_to_json(requests)::text AS original FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT 10001', [ids])).rows;
4862
- const sessions = (await client.query('SELECT id,row_to_json(sessions)::text AS original FROM sessions WHERE id=ANY($1::text[]) ORDER BY id COLLATE "C" LIMIT 101', [ids])).rows;
4863
- const body = { schema: "economy.legacy-preservation.v1", plan: expected, captured_at: current.snapshot.at, requests, sessions };
4982
+ if (!sizes || !/^[0-9]+$/.test(sizes.request_bytes) || !/^[0-9]+$/.test(sizes.session_bytes) || BigInt(sizes.request_bytes) + BigInt(sizes.session_bytes) > BigInt(mode.originalBytes))
4983
+ fail(`Preservation exceeds ${mode.originalBytes / 1024 / 1024} MiB of original row text; complete sessions cannot be truncated`);
4984
+ const requests = (await client.query(`SELECT id,row_to_json(requests)::text AS original FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
4985
+ const sessions = (await client.query(`SELECT id,row_to_json(sessions)::text AS original FROM sessions WHERE id=ANY($1::text[]) ORDER BY id COLLATE "C" LIMIT ${mode.sessions + 1}`, [ids])).rows;
4986
+ const body = { schema: mode.capsule, plan: expected, captured_at: current.snapshot.at, requests, sessions };
4864
4987
  return parseLegacyCapsule({ ...body, capsule_sha256: sha2(JSON.stringify(body)) });
4865
4988
  });
4866
4989
  }
@@ -4891,11 +5014,16 @@ function readLegacyBytes(path, maxBytes = MAX_BYTES) {
4891
5014
  }
4892
5015
  function readLegacyArtifact(path, maxBytes = MAX_BYTES) {
4893
5016
  const bytes = readLegacyBytes(path, maxBytes);
5017
+ let value;
4894
5018
  try {
4895
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
5019
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
4896
5020
  } catch {
4897
5021
  fail("Artifact must contain valid UTF-8 JSON");
4898
5022
  }
5023
+ const limit = value?.schema === LEGACY_SINGLE_SESSION.plan || value?.schema === LEGACY_SINGLE_SESSION.capsule ? LEGACY_SINGLE_SESSION.capsuleBytes : LEGACY_STANDARD.capsuleBytes;
5024
+ if (bytes.length > limit)
5025
+ fail("Artifact exceeds its schema file bound");
5026
+ return value;
4899
5027
  }
4900
5028
  function writeLegacyCapsule(path, input) {
4901
5029
  const capsule = parseLegacyCapsule(input);
@@ -4906,7 +5034,7 @@ function writeLegacyCapsule(path, input) {
4906
5034
  fail("Output parent must be an owner-only directory");
4907
5035
  const bytes = Buffer.from(JSON.stringify(capsule) + `
4908
5036
  `);
4909
- if (bytes.length > MAX_BYTES)
5037
+ if (bytes.length > legacyMode("capsule", capsule.schema).capsuleBytes)
4910
5038
  fail("Preservation capsule exceeds the file bound");
4911
5039
  const fd = openSync2(path, constants2.O_WRONLY | constants2.O_CREAT | constants2.O_EXCL | constants2.O_NOFOLLOW, 384);
4912
5040
  try {
@@ -5037,7 +5165,7 @@ async function runLegacyPreservation(command, options) {
5037
5165
  await pool.end();
5038
5166
  }
5039
5167
  }
5040
- var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
5168
+ var MAX_BYTES, sha2 = (s) => createHash7("sha256").update(s).digest("hex"), fail = (message) => {
5041
5169
  throw Error(message);
5042
5170
  }, record = (v) => v !== null && typeof v === "object" && !Array.isArray(v) ? v : fail("Expected a JSON object"), hash = (v) => typeof v === "string" && /^[a-f0-9]{64}$/.test(v) ? v : fail("Expected a lowercase SHA-256 digest"), identifier3 = (v) => typeof v === "string" && v.length > 0 && v.length <= 1024 && !v.includes("\x00") ? v : fail("Expected a nonempty identifier of at most 1024 characters without NUL"), fields = (o, names) => {
5043
5171
  if (Object.keys(o).sort().join(",") !== [...names].sort().join(","))
@@ -5045,7 +5173,8 @@ var MAX_BYTES, MAX_MEMBERS = 1e4, sha2 = (s) => createHash7("sha256").update(s).
5045
5173
  }, cOrder = (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b)), same = (a, b) => JSON.stringify(a) === JSON.stringify(b), quote3 = (s) => '"' + s.replaceAll('"', '""') + '"';
5046
5174
  var init_legacy_preservation = __esm(() => {
5047
5175
  init_legacy_plan();
5048
- MAX_BYTES = 32 * 1024 * 1024;
5176
+ init_legacy_limits();
5177
+ MAX_BYTES = LEGACY_SINGLE_SESSION.capsuleBytes;
5049
5178
  });
5050
5179
 
5051
5180
  // src/server/legacy-operations.ts
@@ -5109,6 +5238,7 @@ function matchIntent(intent, capsule, target) {
5109
5238
  }
5110
5239
  async function verifyOriginals(client, capsule) {
5111
5240
  const p = capsule.plan;
5241
+ const mode = legacyMode("capsule", capsule.schema);
5112
5242
  const context2 = (await client.query("SELECT current_database() AS database,(SELECT oid::text FROM pg_database WHERE datname=current_database()) AS database_oid")).rows[0];
5113
5243
  if (context2?.database !== p.snapshot.database || context2?.database_oid !== p.snapshot.database_oid)
5114
5244
  fail2("Legacy operation targets a different database");
@@ -5122,7 +5252,7 @@ async function verifyOriginals(client, capsule) {
5122
5252
  fail2("Legacy session originals changed; preserve later work and prepare a new review");
5123
5253
  const rows = (await client.query(`SELECT id,session_id,encode(sha256(convert_to(row_to_json(requests)::text,'UTF8')),'hex') AS original_sha256,
5124
5254
  CASE ${LEGACY_FAMILIES.map((f) => "WHEN " + LEGACY_PREDICATES[f] + " THEN '" + f + "'").join(" ")} END AS family
5125
- FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT 10001`, [ids])).rows;
5255
+ FROM requests WHERE session_id=ANY($1::text[]) ORDER BY session_id COLLATE "C",id COLLATE "C" LIMIT ${mode.members + 1}`, [ids])).rows;
5126
5256
  if (rows.length !== capsule.requests.length)
5127
5257
  fail2("Legacy request membership changed; preserve later work and prepare a new review");
5128
5258
  const originals = new Map(capsule.requests.map((r) => [r.id, sha3(r.original)]));
@@ -5176,13 +5306,14 @@ async function observed(client, run) {
5176
5306
  }
5177
5307
  function validateReceipt(value) {
5178
5308
  const v = value;
5179
- if (!v || v.schema !== "economy.legacy-operation-receipt.v1" || !["apply", "rollback"].includes(v.action))
5309
+ if (!v || !["apply", "rollback"].includes(v.action))
5180
5310
  fail2("Invalid stored legacy receipt");
5311
+ const mode = legacyMode("receipt", v.schema);
5181
5312
  key(v.operation_key);
5182
5313
  key(v.apply_key);
5183
5314
  digest3(v.plan_sha256);
5184
5315
  digest3(v.capsule_sha256);
5185
- if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <= 1e4) || v.target_requests > 100 || v.sessions > 100 || typeof v.created_at !== "string" || !Number.isFinite(Date.parse(v.created_at)))
5316
+ if (v.source_rows_changed !== 0 || ![v.target_requests, v.preserved_requests, v.sessions].every((n) => Number.isSafeInteger(n) && n > 0 && n <= mode.members) || v.target_requests > mode.items || v.sessions > mode.sessions || typeof v.created_at !== "string" || !Number.isFinite(Date.parse(v.created_at)))
5186
5317
  fail2("Invalid stored legacy receipt fields");
5187
5318
  const names = ["schema", "action", "operation_key", "apply_key", "plan_sha256", "capsule_sha256", "target_requests", "preserved_requests", "sessions", "source_rows_changed", "created_at"];
5188
5319
  if (Object.keys(v).sort().join(",") !== names.sort().join(","))
@@ -5207,7 +5338,7 @@ async function reply(client, row, expectedHash) {
5207
5338
  }
5208
5339
  function receipt(action, intent, capsule) {
5209
5340
  return {
5210
- schema: "economy.legacy-operation-receipt.v1",
5341
+ schema: legacyMode("capsule", capsule.schema).receipt,
5211
5342
  action,
5212
5343
  operation_key: intent.operation_key,
5213
5344
  apply_key: action === "apply" ? intent.operation_key : intent.apply_key,
@@ -5247,8 +5378,9 @@ async function applyLegacyReconciliation(client, input, preserved, target) {
5247
5378
  const saved = (await client.query("SELECT capsule_json,capsule_sha256 FROM legacy_reconciliations WHERE operation_key=$1", [intent.operation_key])).rows[0];
5248
5379
  if (saved?.capsule_json !== json3 || saved?.capsule_sha256 !== capsule.capsule_sha256 || parseLegacyCapsule(JSON.parse(saved.capsule_json)).capsule_sha256 !== capsule.capsule_sha256)
5249
5380
  fail2("Durable preservation readback failed");
5250
- for (const item of capsule.plan.items)
5251
- await client.query("INSERT INTO legacy_reconciliation_targets VALUES($1,$2,$3,$4,$5)", [intent.operation_key, item.id, item.session_id, item.disposition, item.original_sha256]);
5381
+ await client.query(`INSERT INTO legacy_reconciliation_targets(operation_key,request_id,session_id,disposition,original_sha256)
5382
+ SELECT $1, x.id, x.session_id, x.disposition, x.original_sha256
5383
+ FROM jsonb_to_recordset($2::jsonb) AS x(id text,session_id text,disposition text,original_sha256 text)`, [intent.operation_key, JSON.stringify(capsule.plan.items.map(({ id, session_id, disposition, original_sha256 }) => ({ id, session_id, disposition, original_sha256 })))]);
5252
5384
  const stored = await lookup(client, "apply", intent.operation_key) ?? fail2("Legacy receipt readback failed");
5253
5385
  if (stored.response_json !== responseJson)
5254
5386
  fail2("Legacy receipt readback failed");
@@ -5360,6 +5492,7 @@ var sha3 = (s) => createHash8("sha256").update(s).digest("hex"), fail2 = (s) =>
5360
5492
  WHERE c.oid IN ('requests'::regclass,'sessions'::regclass) AND a.attnum>0 AND NOT a.attisdropped
5361
5493
  GROUP BY c.oid,c.relname,n.nspname ORDER BY c.relname`, same2 = (a, b) => JSON.stringify(a) === JSON.stringify(b);
5362
5494
  var init_legacy_operations = __esm(() => {
5495
+ init_legacy_limits();
5363
5496
  init_pg_connection();
5364
5497
  init_legacy_inventory();
5365
5498
  init_legacy_preservation();
@@ -5396,7 +5529,7 @@ function isAuthorizedRequest(req, path) {
5396
5529
  // package.json
5397
5530
  var package_default = {
5398
5531
  name: "@hasna/economy",
5399
- version: "0.5.1",
5532
+ version: "0.5.3",
5400
5533
  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",
5401
5534
  repository: {
5402
5535
  type: "git",
@@ -11535,7 +11668,8 @@ function buildBrief(db, opts = {}) {
11535
11668
  }
11536
11669
 
11537
11670
  // src/lib/analytics.ts
11538
- function queryProjectDetail(db, nameOrPath) {
11671
+ function queryProjectDetail(db, nameOrPath, now = new Date) {
11672
+ const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
11539
11673
  const like = `%${nameOrPath}%`;
11540
11674
  const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
11541
11675
  FROM economy_effective_sessions
@@ -11549,8 +11683,8 @@ function queryProjectDetail(db, nameOrPath) {
11549
11683
  const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
11550
11684
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
11551
11685
  WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
11552
- AND r.timestamp >= DATE('now', '-14 days')
11553
- GROUP BY date ORDER BY date ASC`).all(like, like);
11686
+ AND r.timestamp >= ? AND r.timestamp < ?
11687
+ GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
11554
11688
  const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
11555
11689
  FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
11556
11690
  WHERE s.project_name LIKE ? OR s.project_path LIKE ?
@@ -11589,18 +11723,17 @@ function queryRangeStats(db, from, to) {
11589
11723
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
11590
11724
  }
11591
11725
  function queryForecast(db, now = new Date) {
11592
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
11593
- const dayOfMonth = now.getDate();
11594
- const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
11595
- const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(monthStart);
11726
+ const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
11727
+ const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
11728
+ const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
11729
+ const dayOfMonth = now.getUTCDate();
11730
+ const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
11596
11731
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
11597
11732
  const projected = dailyAvg * daysInMonth;
11598
- const sevenDaysAgo = new Date(now);
11599
- sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
11600
- const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
11733
+ const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
11601
11734
  const last7DailyAvg = last7.cost / 7;
11602
11735
  const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
11603
- FROM economy_effective_requests WHERE DATE(timestamp) >= ? GROUP BY date ORDER BY cost_usd ASC`).all(monthStart);
11736
+ FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
11604
11737
  return {
11605
11738
  day_of_month: dayOfMonth,
11606
11739
  days_in_month: daysInMonth,
@@ -11777,7 +11910,13 @@ function createDataHandler(db, actorId) {
11777
11910
  if (path === "/api/daily" && method === "GET") {
11778
11911
  const days = Number(url.searchParams.get("days") ?? 30);
11779
11912
  const machine = url.searchParams.get("machine") ?? undefined;
11780
- return ok(queryDailyBreakdown(db, days, machine));
11913
+ const now = new Date;
11914
+ try {
11915
+ resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
11916
+ } catch {
11917
+ return err("days must be a positive integer within the supported UTC date range");
11918
+ }
11919
+ return ok(queryDailyBreakdown(db, days, machine, now));
11781
11920
  }
11782
11921
  if (path === "/api/hourly" && method === "GET") {
11783
11922
  const machine = url.searchParams.get("machine") ?? undefined;