@hasna/economy 0.5.10 → 0.5.11

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/dist/cli/index.js CHANGED
@@ -1,23 +1,36 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
+ import {
4
+ buildStatusLine,
5
+ gatherStatusData
6
+ } from "../chunks/index-akxt4n8x.js";
7
+ import"../chunks/index-w3bvv0ns.js";
8
+ import {
9
+ JournalMaintenanceError,
10
+ PendingIngestError,
11
+ backfillMachineId,
12
+ billingSyncToCloud,
13
+ getIngestCachePath,
14
+ ingestJournalOperationSchema,
15
+ recalculateZeroCostRequests,
16
+ runIngestJournalOperation,
17
+ syncAllToCloud,
18
+ syncHasFailures,
19
+ syncOutcomeLines
20
+ } from "../chunks/index-yv28hyk7.js";
3
21
  import {
4
22
  syncAnthropicBilling,
5
23
  syncGeminiBilling,
6
24
  syncOpenAIBilling
7
25
  } from "../chunks/index-gtjqy0kd.js";
8
- import {
9
- buildStatusLine,
10
- gatherStatusData
11
- } from "../chunks/index-w4r6xs02.js";
12
- import"../chunks/index-w3bvv0ns.js";
13
26
  import {
14
27
  agentPaths,
15
28
  syncAll
16
- } from "../chunks/index-t777mc83.js";
29
+ } from "../chunks/index-mgnzgf1m.js";
17
30
  import {
18
31
  getStore,
19
32
  isCloudStore
20
- } from "../chunks/index-twn8ncdp.js";
33
+ } from "../chunks/index-v35tm3xg.js";
21
34
  import {
22
35
  billingDriftCheck,
23
36
  comparisonValueChange,
@@ -25,13 +38,9 @@ import {
25
38
  economyTransportReport,
26
39
  formatComparisonValue,
27
40
  packageMetadata
28
- } from "../chunks/index-4v5r3c22.js";
41
+ } from "../chunks/index-b36bq51x.js";
29
42
  import {
30
43
  AGENTS,
31
- MAX_INGEST_BYTES,
32
- MAX_INGEST_ROWS,
33
- cacheDir,
34
- canonicalIngestBodyHash,
35
44
  dedupeRequests,
36
45
  ensurePricingSeeded,
37
46
  formatBillingValue,
@@ -44,10 +53,7 @@ import {
44
53
  getPricingFromDb,
45
54
  parseAgent,
46
55
  queryZeroCostTokenizedModels,
47
- rollupSession,
48
- setIngestState,
49
- stableJson,
50
- validateIngest
56
+ setIngestState
51
57
  } from "../chunks/index-zgvmrqyc.js";
52
58
  import {
53
59
  __require
@@ -1359,23 +1365,23 @@ function registerExtendedCommands(program) {
1359
1365
  console.log();
1360
1366
  });
1361
1367
  program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
1362
- const { runTui } = await import("../chunks/tui-5s5w6yjd.js");
1368
+ const { runTui } = await import("../chunks/tui-hxjczmv5.js");
1363
1369
  await runTui({
1364
1370
  watch: Boolean(opts.watch),
1365
1371
  interval: Number(opts.interval ?? 30)
1366
1372
  });
1367
1373
  });
1368
1374
  program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
1369
- const { printWaybarJson } = await import("../chunks/tui-5s5w6yjd.js");
1375
+ const { printWaybarJson } = await import("../chunks/tui-hxjczmv5.js");
1370
1376
  await printWaybarJson();
1371
1377
  });
1372
1378
  const barCmd = program.command("bar").description("Status bar helpers (Linux)");
1373
1379
  barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
1374
- const { runTui } = await import("../chunks/tui-5s5w6yjd.js");
1380
+ const { runTui } = await import("../chunks/tui-hxjczmv5.js");
1375
1381
  await runTui({ watch: Boolean(opts.watch) });
1376
1382
  });
1377
1383
  barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
1378
- const { printWaybarJson } = await import("../chunks/tui-5s5w6yjd.js");
1384
+ const { printWaybarJson } = await import("../chunks/tui-hxjczmv5.js");
1379
1385
  await printWaybarJson();
1380
1386
  });
1381
1387
  program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
@@ -1433,755 +1439,7 @@ function registerFleetCommands(program) {
1433
1439
  }
1434
1440
 
1435
1441
  // src/lib/ingest-journal-client.ts
1436
- import { z as z2 } from "zod";
1437
-
1438
- // src/lib/ingest-journal.ts
1439
- import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
1440
- import { createHash } from "crypto";
1441
- import { join as join2, resolve } from "path";
1442
- import { HasnaHttpError } from "@hasna/contracts/client";
1443
- var MAX_COMMANDS = 1000;
1444
- var MAX_BYTES = 64 * 1024 * 1024;
1445
- var RECEIPT_RESERVE = 4096;
1446
- var MAINTENANCE_RESERVE = 16384;
1447
- var sha = (s) => createHash("sha256").update(s).digest("hex");
1448
-
1449
- class PendingIngestError extends Error {
1450
- pending_keys;
1451
- code = "INGEST_RECONCILIATION_REQUIRED";
1452
- constructor(pending_keys) {
1453
- super("Pending ingestion requires hosted receipt reconciliation; no command was replayed");
1454
- this.pending_keys = pending_keys;
1455
- this.name = "PendingIngestError";
1456
- }
1457
- }
1458
- function definitiveIngestRefusal(status, body) {
1459
- const reasons = { 400: "invalid_ingest", 409: "ingest_conflict", 413: "ingest_too_large", 422: "unprocessable_ingest" };
1460
- if (!Object.hasOwn(reasons, status))
1461
- return null;
1462
- const legacy = body?.expected === "legacy_aggregate_requires_migration";
1463
- return {
1464
- status,
1465
- code: legacy ? "legacy_aggregate_requires_migration" : "ingest_refused",
1466
- reason: legacy ? "legacy_aggregate_requires_migration" : reasons[status]
1467
- };
1468
- }
1469
- function getIngestJournalPath() {
1470
- return process.env.HASNA_ECONOMY_INGEST_JOURNAL ?? join2(cacheDir({ app: "economy", home: process.env.HOME || process.env.USERPROFILE }), "pending-ingest");
1471
- }
1472
- function targetId(cloud) {
1473
- const url = new URL(cloud.client.transport.baseUrl);
1474
- if (url.username || url.password || url.search || url.hash)
1475
- throw new Error("Ingest authority must not contain URL credentials or capabilities");
1476
- return sha(url.toString().replace(/\/+$/, ""));
1477
- }
1478
- function secureDirectory(path) {
1479
- mkdirSync(path, { recursive: true, mode: 448 });
1480
- const stat = lstatSync(path);
1481
- if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
1482
- throw new Error("Ingest journal must be a private owner directory");
1483
- }
1484
- function readJson(path, max) {
1485
- const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
1486
- try {
1487
- const st = fstatSync(fd);
1488
- if (!st.isFile() || st.size > max || (st.mode & 63) !== 0 || process.getuid && st.uid !== process.getuid())
1489
- throw new Error("Unsafe ingest journal record");
1490
- return JSON.parse(readFileSync(fd, "utf8"));
1491
- } finally {
1492
- closeSync(fd);
1493
- }
1494
- }
1495
- function createExact(path, value) {
1496
- const bytes = JSON.stringify(value);
1497
- let fd;
1498
- try {
1499
- fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
1500
- } catch (error) {
1501
- if (error.code === "EEXIST")
1502
- return false;
1503
- throw error;
1504
- }
1505
- try {
1506
- writeFileSync(fd, bytes);
1507
- fsyncSync(fd);
1508
- } finally {
1509
- closeSync(fd);
1510
- }
1511
- if (JSON.stringify(readJson(path, MAX_INGEST_BYTES + 4096)) !== bytes)
1512
- throw new Error("Ingest journal readback failed");
1513
- const directory = openSync(resolve(path, ".."), constants.O_RDONLY);
1514
- try {
1515
- fsyncSync(directory);
1516
- } finally {
1517
- closeSync(directory);
1518
- }
1519
- return true;
1520
- }
1521
- function unwrap(raw) {
1522
- return raw && typeof raw === "object" && "data" in raw ? raw.data : raw;
1523
- }
1524
- function verifyIngestReceipt(raw, command) {
1525
- const receipt = unwrap(raw);
1526
- 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")
1527
- throw new Error("Invalid ingest receipt");
1528
- let sum = 0;
1529
- for (const [table, count] of Object.entries(receipt.ingested)) {
1530
- if (!Array.isArray(command[table]) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0 || count > command[table].length)
1531
- throw new Error("Invalid ingest receipt");
1532
- sum += count;
1533
- }
1534
- if (sum !== receipt.total)
1535
- throw new Error("Invalid ingest receipt");
1536
- const projected = {
1537
- ingested: Object.fromEntries(Object.entries(receipt.ingested)),
1538
- total: receipt.total,
1539
- operation_key: receipt.operation_key,
1540
- replayed: receipt.replayed,
1541
- stale: receipt.stale ?? 0,
1542
- unchanged: receipt.unchanged ?? 0
1543
- };
1544
- const rows = Object.values(command).reduce((n, v) => n + (Array.isArray(v) ? v.length : 0), 0);
1545
- if (![projected.stale, projected.unchanged].every((n) => Number.isSafeInteger(n) && n >= 0 && n <= rows) || Buffer.byteLength(JSON.stringify(projected)) > RECEIPT_RESERVE - 256)
1546
- throw new Error("Invalid ingest receipt");
1547
- return projected;
1548
- }
1549
- function sameReceipt(left, right) {
1550
- const meaning = (value) => ({ ingested: value?.ingested, total: value?.total, operation_key: value?.operation_key, stale: value?.stale ?? 0, unchanged: value?.unchanged ?? 0 });
1551
- return stableJson(meaning(left)) === stableJson(meaning(right));
1552
- }
1553
- async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
1554
- if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
1555
- throw new RangeError("Receipt lookup requires a positive finite timeout");
1556
- const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
1557
- const deadline = performance.now() + timeoutMs;
1558
- let failures = 0, lastError;
1559
- for (;; ) {
1560
- const remaining = Math.floor(deadline - performance.now());
1561
- if (remaining <= 0)
1562
- throw lastError ?? new Error("Receipt lookup deadline expired");
1563
- let response;
1564
- try {
1565
- const abort = new AbortController;
1566
- let timer;
1567
- try {
1568
- const expired = new Promise((_, reject) => {
1569
- timer = setTimeout(() => {
1570
- abort.abort();
1571
- reject(new Error("Receipt lookup deadline expired"));
1572
- }, remaining);
1573
- });
1574
- response = await Promise.race([
1575
- cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
1576
- expired
1577
- ]);
1578
- } finally {
1579
- clearTimeout(timer);
1580
- }
1581
- } catch (error) {
1582
- if (!(error instanceof HasnaHttpError) || error.status !== 503)
1583
- throw error;
1584
- lastError = error;
1585
- failures++;
1586
- const delay = 100 * failures;
1587
- if (failures >= 3 || deadline - performance.now() <= delay)
1588
- throw error;
1589
- await new Promise((resolve2) => setTimeout(resolve2, delay));
1590
- continue;
1591
- }
1592
- if (performance.now() >= deadline)
1593
- throw new Error("Receipt lookup deadline expired");
1594
- const raw = unwrap(response);
1595
- if (!raw || raw.body_hash !== bodyHash)
1596
- throw new Error("Invalid reconciliation response");
1597
- if (raw.status === "absent")
1598
- return null;
1599
- if (raw.status !== "committed")
1600
- throw new Error("Invalid reconciliation response");
1601
- return raw.receipt;
1602
- }
1603
- }
1604
-
1605
- class IngestJournal {
1606
- directory;
1607
- target;
1608
- inventoryBytes = 0;
1609
- freshlyStaged = new Set;
1610
- constructor(cloud, path = getIngestJournalPath()) {
1611
- this.target = targetId(cloud);
1612
- secureDirectory(path);
1613
- this.directory = join2(realpathSync(path), this.target);
1614
- secureDirectory(this.directory);
1615
- }
1616
- names() {
1617
- const names = [];
1618
- const directory = opendirSync(this.directory);
1619
- try {
1620
- for (let entry;entry = directory.readSync(); ) {
1621
- if (entry.name === ".lease" || entry.name === ".maintenance" || entry.name === ".authority.json" || entry.name === ".recovery")
1622
- continue;
1623
- names.push(entry.name);
1624
- if (names.length > MAX_COMMANDS * 2)
1625
- throw new Error("Ingest journal capacity reached; preserve and reconcile records");
1626
- }
1627
- } finally {
1628
- directory.closeSync();
1629
- }
1630
- this.inventoryBytes = 0;
1631
- const set = new Set(names);
1632
- for (const name of names) {
1633
- if (!/^[a-f0-9]{64}\.(intent|receipt|refusal)\.json$/.test(name))
1634
- throw new Error("Unexpected ingest journal entry");
1635
- if (!name.endsWith(".intent.json") && !set.has(name.replace(/\.(receipt|refusal)\./, ".intent.")))
1636
- throw new Error("Orphaned ingest outcome requires maintenance recovery");
1637
- if (name.endsWith(".refusal.json") && set.has(name.replace(".refusal.", ".receipt.")))
1638
- throw new Error("Conflicting ingest receipt and refusal");
1639
- const stat = lstatSync(join2(this.directory, name));
1640
- if (!stat.isFile() || stat.isSymbolicLink() || (stat.mode & 63) !== 0)
1641
- throw new Error("Unsafe ingest journal record");
1642
- if (stat.size > MAX_INGEST_BYTES + 4096)
1643
- throw new Error("Oversized ingest journal record");
1644
- this.inventoryBytes += stat.size;
1645
- if (name.endsWith(".refusal.json"))
1646
- this.refusal(this.intent(name.replace(".refusal.", ".intent.")).command);
1647
- }
1648
- return names;
1649
- }
1650
- file(key, kind) {
1651
- return join2(this.directory, `${sha(key)}.${kind}.json`);
1652
- }
1653
- intent(name) {
1654
- const record = readJson(join2(this.directory, name), MAX_INGEST_BYTES + 4096);
1655
- 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`)
1656
- throw new Error("Invalid ingest journal intent");
1657
- validateIngest(record.command);
1658
- return record;
1659
- }
1660
- status() {
1661
- const names = this.names(), intents = names.filter((n) => n.endsWith(".intent.json"));
1662
- const set = new Set(names);
1663
- const completed = intents.filter((name) => set.has(name.replace(".intent.", ".receipt."))).length;
1664
- const rejected = intents.filter((name) => set.has(name.replace(".intent.", ".refusal."))).length;
1665
- const bytes = this.inventoryBytes;
1666
- 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 };
1667
- }
1668
- canStage(command, status = this.status()) {
1669
- const name = `${sha(command.idempotency_key)}.intent.json`;
1670
- try {
1671
- if (this.intent(name).body_hash !== canonicalIngestBodyHash(command))
1672
- throw new Error("Ingest command key is already bound to different content");
1673
- return true;
1674
- } catch (error) {
1675
- if (error.code !== "ENOENT")
1676
- throw error;
1677
- }
1678
- const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
1679
- return status.commands < MAX_COMMANDS && status.reserved_bytes + Buffer.byteLength(JSON.stringify(record)) + RECEIPT_RESERVE <= MAX_BYTES;
1680
- }
1681
- receipt(command) {
1682
- if (this.refusal(command))
1683
- throw new Error("Conflicting ingest receipt and refusal");
1684
- const record = readJson(this.file(command.idempotency_key, "receipt"), RECEIPT_RESERVE);
1685
- if (record.version !== 1 || record.target !== this.target || record.body_hash !== canonicalIngestBodyHash(command))
1686
- throw new Error("Invalid receipt journal binding");
1687
- return verifyIngestReceipt(record.receipt, command);
1688
- }
1689
- stage(command) {
1690
- validateIngest(command);
1691
- const path = this.file(command.idempotency_key, "intent");
1692
- const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), command };
1693
- if (!this.canStage(command))
1694
- throw new Error("Ingest journal capacity reached; run economy ingest journal maintain; originals preserved");
1695
- if (createExact(path, record)) {
1696
- this.freshlyStaged.add(command.idempotency_key);
1697
- return true;
1698
- }
1699
- this.freshlyStaged.delete(command.idempotency_key);
1700
- const old = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1701
- if (old.body_hash !== record.body_hash)
1702
- throw new Error("Ingest command key is already bound to different content");
1703
- return false;
1704
- }
1705
- accept(command, raw) {
1706
- if (this.refusal(command))
1707
- throw new Error("A refused command cannot accept a receipt");
1708
- this.freshlyStaged.delete(command.idempotency_key);
1709
- const receipt = verifyIngestReceipt(raw, command);
1710
- const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1711
- if (intent.body_hash !== canonicalIngestBodyHash(command))
1712
- throw new Error("Ingest receipt content conflicts with exact intent");
1713
- const record = { version: 1, target: this.target, body_hash: canonicalIngestBodyHash(command), receipt };
1714
- const path = this.file(command.idempotency_key, "receipt");
1715
- if (!this.names().includes(`${sha(command.idempotency_key)}.receipt.json`) && this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
1716
- throw new Error("Receipt capacity exhausted; exact intent retained for reconciliation");
1717
- if (!createExact(path, record)) {
1718
- const old = readJson(path, MAX_INGEST_BYTES);
1719
- if (old.body_hash !== record.body_hash || old.target !== this.target || !sameReceipt(old.receipt, receipt))
1720
- throw new Error("Ingest receipt journal conflict");
1721
- }
1722
- return receipt;
1723
- }
1724
- recordRefusal(command, refusal) {
1725
- if (!this.freshlyStaged.delete(command.idempotency_key))
1726
- throw new Error("Refusal requires this invocation's fresh command");
1727
- const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`);
1728
- if (intent.body_hash !== canonicalIngestBodyHash(command))
1729
- throw new Error("Refusal conflicts with exact intent");
1730
- const expected = definitiveIngestRefusal(refusal.status, refusal.code === "legacy_aggregate_requires_migration" ? { expected: refusal.code } : null);
1731
- if (!expected || stableJson(expected) !== stableJson(refusal))
1732
- throw new Error("Invalid definitive refusal");
1733
- const names = this.names();
1734
- if (names.includes(`${sha(command.idempotency_key)}.receipt.json`))
1735
- throw new Error("A completed command cannot be refused");
1736
- const record = { version: 1, target: this.target, body_hash: intent.body_hash, refusal };
1737
- if (Buffer.byteLength(JSON.stringify(record)) > RECEIPT_RESERVE || this.inventoryBytes + Buffer.byteLength(JSON.stringify(record)) > MAX_BYTES)
1738
- throw new Error("Refusal capacity exhausted; exact intent retained");
1739
- if (!createExact(this.file(command.idempotency_key, "refusal"), record))
1740
- throw new Error("Existing refusal requires inspection");
1741
- this.refusal(command);
1742
- }
1743
- refusal(command) {
1744
- let record;
1745
- try {
1746
- record = readJson(this.file(command.idempotency_key, "refusal"), RECEIPT_RESERVE);
1747
- } catch (error) {
1748
- if (error.code === "ENOENT")
1749
- return null;
1750
- throw error;
1751
- }
1752
- const intent = this.intent(`${sha(command.idempotency_key)}.intent.json`), r = record?.refusal;
1753
- const expected = definitiveIngestRefusal(r?.status, r?.code === "legacy_aggregate_requires_migration" ? { expected: r.code } : null);
1754
- if (record?.version !== 1 || record.target !== this.target || record.body_hash !== intent.body_hash || intent.body_hash !== canonicalIngestBodyHash(command) || !expected || stableJson(expected) !== stableJson(r))
1755
- throw new Error("Invalid refusal journal binding");
1756
- try {
1757
- lstatSync(this.file(command.idempotency_key, "receipt"));
1758
- throw new Error("Conflicting ingest receipt and refusal");
1759
- } catch (error) {
1760
- if (error.code !== "ENOENT")
1761
- throw error;
1762
- }
1763
- return expected;
1764
- }
1765
- async reconcileCommand(cloud, command, timeoutMs = 1e4) {
1766
- const receipt = await this.lookupReceipt(cloud, command, timeoutMs);
1767
- return receipt ? this.accept(command, receipt) : null;
1768
- }
1769
- assertCloud(cloud) {
1770
- if (targetId(cloud) !== this.target)
1771
- throw new Error("Ingest journal authority changed");
1772
- }
1773
- async lookupReceipt(cloud, command, timeoutMs = 1e4) {
1774
- this.assertCloud(cloud);
1775
- const receipt = await lookupJournalReceipt(cloud, command.idempotency_key, canonicalIngestBodyHash(command), timeoutMs);
1776
- return receipt === null ? null : verifyIngestReceipt(receipt, command);
1777
- }
1778
- async reconcilePending(cloud) {
1779
- const names = this.names(), unresolved = [];
1780
- const deadline = performance.now() + 30000;
1781
- for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
1782
- const intent = this.intent(name);
1783
- if (names.includes(`${sha(intent.command.idempotency_key)}.refusal.json`)) {
1784
- this.refusal(intent.command);
1785
- continue;
1786
- }
1787
- if (names.includes(`${sha(intent.command.idempotency_key)}.receipt.json`)) {
1788
- const record = readJson(this.file(intent.command.idempotency_key, "receipt"), MAX_INGEST_BYTES);
1789
- if (record.target !== this.target || record.body_hash !== intent.body_hash)
1790
- throw new Error("Invalid receipt journal binding");
1791
- verifyIngestReceipt(record.receipt, intent.command);
1792
- continue;
1793
- }
1794
- if (performance.now() >= deadline) {
1795
- unresolved.push(intent.command.idempotency_key);
1796
- continue;
1797
- }
1798
- try {
1799
- if (!await this.reconcileCommand(cloud, intent.command, Math.max(1, Math.min(1e4, deadline - performance.now()))))
1800
- unresolved.push(intent.command.idempotency_key);
1801
- } catch {
1802
- unresolved.push(intent.command.idempotency_key);
1803
- }
1804
- }
1805
- if (unresolved.length)
1806
- throw new PendingIngestError(unresolved);
1807
- }
1808
- }
1809
-
1810
- // src/lib/ingest-journal-maintenance.ts
1811
- import { constants as constants2, closeSync as closeSync2, fstatSync as fstatSync2, lstatSync as lstatSync2, openSync as openSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1812
- import { createHash as createHash2 } from "crypto";
1813
- import { join as join3 } from "path";
1814
- var uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
1815
- var digest = /^[a-f0-9]{64}$/;
1816
- var sha2 = (bytes) => createHash2("sha256").update(bytes).digest("hex");
1817
-
1818
- class JournalMaintenanceError extends Error {
1819
- reason;
1820
- target;
1821
- code = "INGEST_JOURNAL_MAINTENANCE_REQUIRED";
1822
- constructor(reason, target) {
1823
- super(`Ingest journal maintenance required (${reason}); run economy ingest journal maintain; originals or Trash recovery state preserved`);
1824
- this.reason = reason;
1825
- this.target = target;
1826
- this.name = "JournalMaintenanceError";
1827
- }
1828
- }
1829
- function exists(path) {
1830
- try {
1831
- lstatSync2(path);
1832
- return true;
1833
- } catch (error) {
1834
- if (error.code === "ENOENT")
1835
- return false;
1836
- throw error;
1837
- }
1838
- }
1839
- function snapshot(path) {
1840
- const fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK);
1841
- try {
1842
- const stat = fstatSync2(fd);
1843
- if (!stat.isFile() || stat.nlink !== 1 || stat.size > 4 * 1024 * 1024 + 4096 || (stat.mode & 63) !== 0 || process.getuid && stat.uid !== process.getuid())
1844
- throw new Error("Unsafe maintenance source");
1845
- const bytes = readFileSync2(fd), after = fstatSync2(fd);
1846
- if (after.ctimeMs !== stat.ctimeMs || bytes.length !== stat.size)
1847
- throw new Error("Maintenance source changed");
1848
- return { sha256: sha2(bytes), sizeBytes: stat.size, mode: stat.mode & 511, device: String(stat.dev), inode: String(stat.ino), ctime: stat.ctimeMs };
1849
- } finally {
1850
- closeSync2(fd);
1851
- }
1852
- }
1853
- function sameSource(path, expected) {
1854
- const { capture_sha256: _capture, ...original } = expected;
1855
- if (JSON.stringify(snapshot(path)) !== JSON.stringify(original))
1856
- throw new Error("Maintenance source changed; preserve for inspection");
1857
- }
1858
- function validAnchor(value, target) {
1859
- const a = value;
1860
- if (a?.version !== 1 || a.target !== target || !/^[A-Za-z0-9._:-]{1,200}$/.test(a.key) || !digest.test(a.body_hash) || !digest.test(a.receipt?.operation_key))
1861
- throw new Error("Invalid journal authority anchor");
1862
- return a;
1863
- }
1864
- async function confirm(cloud, anchor, deadline) {
1865
- if (Date.now() >= deadline)
1866
- throw new Error("Maintenance deadline reached");
1867
- const current = await lookupJournalReceipt(cloud, anchor.key, anchor.body_hash, Math.max(1, Math.min(1e4, deadline - Date.now())));
1868
- if (current === null || !sameReceipt(current, anchor.receipt))
1869
- throw new Error("Exact Economy receipt is unavailable or changed");
1870
- }
1871
- async function verifyJournalAuthority(journal, cloud) {
1872
- journal.assertCloud(cloud);
1873
- const path = join3(journal.directory, ".authority.json");
1874
- if (!exists(path)) {
1875
- const names = journal.names();
1876
- const name = names.find((n) => n.endsWith(".intent.json") && names.includes(n.replace(".intent.", ".receipt.")));
1877
- if (!name)
1878
- return;
1879
- const intent = journal.intent(name), receipt = journal.receipt(intent.command);
1880
- createExact(path, { version: 1, target: journal.target, key: intent.command.idempotency_key, body_hash: intent.body_hash, receipt });
1881
- }
1882
- await confirm(cloud, validAnchor(readJson(path, 4096), journal.target), Date.now() + 1e4);
1883
- }
1884
- function recoveryFactory(cloud, env = process.env) {
1885
- return async (operationRoot, checkpoint, deadline) => {
1886
- if (env.HASNA_ECONOMY_INGEST_RECOVERY !== "trash")
1887
- throw new Error("recovery_not_configured");
1888
- const economy = new URL(cloud.client.transport.baseUrl);
1889
- const fleet = economy.origin === "https://api.hasna.com" && /^\/economy(?:\/v1)?\/?$/.test(economy.pathname);
1890
- if (!fleet && !env.HASNA_TRASH_API_URL)
1891
- throw new Error("self_host_trash_authority_required");
1892
- const sdk = await import("@hasna/trash/sdk");
1893
- if (sdk.VERSION !== "0.2.3")
1894
- throw new Error("trash_version_mismatch");
1895
- const boundedFetch = (input, init) => {
1896
- const remaining = deadline - Date.now();
1897
- if (remaining <= 0)
1898
- throw new Error("maintenance_deadline");
1899
- const signal = AbortSignal.timeout(remaining);
1900
- return fetch(input, { ...init, signal: init?.signal ? AbortSignal.any([signal, init.signal]) : signal, redirect: "error" });
1901
- };
1902
- return Object.assign(sdk.createTrash({ env, operationRoot, checkpoint, fetchImpl: boundedFetch, transferFetch: boundedFetch, lockTimeoutMs: 100 }), { snapshot: sdk.snapshotIdentity });
1903
- };
1904
- }
1905
-
1906
- class IngestJournalMaintenance {
1907
- journal;
1908
- cloud;
1909
- factory;
1910
- control;
1911
- operationRoot;
1912
- constructor(journal, cloud, factory = recoveryFactory(cloud)) {
1913
- this.journal = journal;
1914
- this.cloud = cloud;
1915
- this.factory = factory;
1916
- journal.assertCloud(cloud);
1917
- this.control = join3(journal.directory, ".maintenance");
1918
- this.operationRoot = join3(journal.directory, ".recovery");
1919
- }
1920
- manifest() {
1921
- secureDirectory(this.control);
1922
- const names = readdirSync(this.control);
1923
- if (names.some((n) => !["manifest.json", "intent.operation.json", "receipt.operation.json"].includes(n)))
1924
- throw new Error("Unexpected maintenance entry");
1925
- let size = 0;
1926
- for (const name of names)
1927
- size += lstatSync2(join3(this.control, name)).size;
1928
- if (size > MAINTENANCE_RESERVE)
1929
- throw new Error("Maintenance metadata exceeds reserve");
1930
- const m = validAnchor(readJson(join3(this.control, "manifest.json"), 8192), this.journal.target);
1931
- for (const part of [m.intent, m.receipt_file])
1932
- if (!part || !digest.test(part.sha256) || !digest.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))
1933
- throw new Error("Invalid maintenance snapshot");
1934
- return m;
1935
- }
1936
- pending(backend) {
1937
- const pending = backend.pending(4);
1938
- if (pending.length > 3 || pending.some((p) => !uuid.test(p.id) || p.phase === "unreadable"))
1939
- throw new Error("Unexpected Trash recovery state");
1940
- return pending;
1941
- }
1942
- async backend(deadline) {
1943
- const backend = await this.factory(this.operationRoot, async (point, operation) => {
1944
- if (operation.path === this.control)
1945
- return;
1946
- const m = this.manifest();
1947
- const kind = operation.path === this.journal.file(m.key, "receipt") ? "receipt" : operation.path === this.journal.file(m.key, "intent") ? "intent" : null;
1948
- if (!kind || operation.kind !== "capture" || !uuid.test(operation.id) || operation.id !== operation.entryId)
1949
- throw new Error("Unexpected Trash operation");
1950
- const expected = kind === "receipt" ? m.receipt_file : m.intent;
1951
- if (point === "afterSnapshot") {
1952
- if (!operation.receipt || operation.receipt.sha256 !== expected.capture_sha256 || operation.receipt.sizeBytes !== expected.sizeBytes || operation.receipt.mode !== expected.mode)
1953
- throw new Error("Trash snapshot mismatch");
1954
- sameSource(operation.path, expected);
1955
- const path = join3(this.control, `${kind}.operation.json`);
1956
- createExact(path, { id: operation.id });
1957
- if (readJson(path, 1024).id !== operation.id)
1958
- throw new Error("Conflicting Trash operation");
1959
- }
1960
- if (point === "beforeStage") {
1961
- await confirm(this.cloud, m, deadline);
1962
- sameSource(operation.path, expected);
1963
- }
1964
- }, deadline);
1965
- const status = await backend.doctor();
1966
- if (!status.station || !uuid.test(status.station.id) || !Number.isInteger(status.retentionDays) || status.retentionDays < 1)
1967
- throw new Error("Trash station authority unavailable");
1968
- return backend;
1969
- }
1970
- async entry(backend, id, path, expected) {
1971
- const e = await backend.api.get(id);
1972
- 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")
1973
- throw new Error("Trash preservation readback refused");
1974
- if (exists(path))
1975
- throw new Error("Retired source reappeared; preserve for inspection");
1976
- }
1977
- async pairFile(backend, m, kind, deadline) {
1978
- await confirm(this.cloud, m, deadline);
1979
- const path = this.journal.file(m.key, kind), expected = kind === "receipt" ? m.receipt_file : m.intent;
1980
- const saved = join3(this.control, `${kind}.operation.json`);
1981
- let id;
1982
- if (exists(saved))
1983
- id = readJson(saved, 1024).id;
1984
- const pending = this.pending(backend);
1985
- const matches = pending.filter((p) => p.path === path);
1986
- if (matches.length > 1 || id && !uuid.test(id))
1987
- throw new Error("Conflicting Trash operation");
1988
- if (matches[0]) {
1989
- if (id && id !== matches[0].id)
1990
- throw new Error("Conflicting Trash operation");
1991
- id = matches[0].id;
1992
- const operation = backend.journal.read(id);
1993
- if (operation.kind !== "capture" || operation.path !== path || operation.entryId !== id)
1994
- throw new Error("Invalid Trash operation binding");
1995
- if (exists(path))
1996
- sameSource(path, expected);
1997
- await backend.recover(id, { takeAbandonedLock: true });
1998
- } else if (!id) {
1999
- sameSource(path, expected);
2000
- const status = await backend.doctor();
2001
- const e = await backend.put(path, { retentionDays: status.retentionDays });
2002
- id = e.id;
2003
- if (!exists(saved) || readJson(saved, 1024).id !== id)
2004
- throw new Error("Trash did not persist its capture binding");
2005
- }
2006
- await this.entry(backend, id, path, expected);
2007
- }
2008
- async recover() {
2009
- if (!exists(this.control) && !exists(this.operationRoot))
2010
- return;
2011
- if (exists(this.operationRoot))
2012
- secureDirectory(this.operationRoot);
2013
- if (!exists(this.control) && readdirSync(this.operationRoot).every((n) => n === ".locks"))
2014
- return;
2015
- const deadline = Date.now() + 30000;
2016
- try {
2017
- await this.finish(await this.backend(deadline), deadline);
2018
- } catch (error) {
2019
- if (error instanceof JournalMaintenanceError)
2020
- throw error;
2021
- throw new JournalMaintenanceError("recovery_unresolved", this.journal.target);
2022
- }
2023
- }
2024
- async finish(backend, deadline) {
2025
- const pending = this.pending(backend);
2026
- const control = pending.filter((p) => p.path === this.control);
2027
- if (control.length) {
2028
- if (control.length !== 1 || pending.length !== 1)
2029
- throw new Error("Conflicting maintenance cleanup");
2030
- const op = backend.journal.read(control[0].id);
2031
- if (op.kind !== "capture" || op.path !== this.control)
2032
- throw new Error("Invalid maintenance cleanup");
2033
- await backend.recover(op.id, { takeAbandonedLock: true });
2034
- if (exists(this.control) || this.pending(backend).length)
2035
- throw new Error("Maintenance cleanup unresolved");
2036
- return;
2037
- }
2038
- if (!exists(this.control)) {
2039
- if (pending.length)
2040
- throw new Error("Orphaned maintenance operation");
2041
- return;
2042
- }
2043
- if (readdirSync(this.control).length === 0) {
2044
- if (pending.length)
2045
- throw new Error("Orphaned maintenance operation");
2046
- const status2 = await backend.doctor();
2047
- await backend.put(this.control, { retentionDays: status2.retentionDays });
2048
- return;
2049
- }
2050
- const m = this.manifest();
2051
- if (pending.some((p) => p.path !== this.journal.file(m.key, "receipt") && p.path !== this.journal.file(m.key, "intent")))
2052
- throw new Error("Unexpected recovery path");
2053
- await this.pairFile(backend, m, "receipt", deadline);
2054
- await this.pairFile(backend, m, "intent", deadline);
2055
- if (this.pending(backend).length)
2056
- throw new Error("Unfinished pair capture");
2057
- await confirm(this.cloud, m, deadline);
2058
- const status = await backend.doctor();
2059
- await backend.put(this.control, { retentionDays: status.retentionDays });
2060
- if (exists(this.control) || this.pending(backend).length)
2061
- throw new Error("Maintenance cleanup unresolved");
2062
- }
2063
- async maintain(limit = 32) {
2064
- if (!Number.isInteger(limit) || limit < 1 || limit > 32)
2065
- throw new Error("Journal maintenance limit must be 1\u201332");
2066
- await this.recover();
2067
- await verifyJournalAuthority(this.journal, this.cloud);
2068
- const sweepDeadline = Date.now() + 30000;
2069
- let retired = 0, bytes = 0;
2070
- let backend;
2071
- try {
2072
- const names = this.journal.names();
2073
- for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
2074
- if (retired >= limit || Date.now() >= sweepDeadline || retired > 0 && bytes >= 1048576)
2075
- break;
2076
- if (!names.includes(name.replace(".intent.", ".receipt.")))
2077
- continue;
2078
- const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
2079
- const deadline = Date.now() + 30000;
2080
- backend = await this.backend(deadline);
2081
- const m = {
2082
- version: 1,
2083
- target: this.journal.target,
2084
- key: intent.command.idempotency_key,
2085
- body_hash: intent.body_hash,
2086
- receipt,
2087
- intent: snapshot(join3(this.journal.directory, name)),
2088
- receipt_file: snapshot(this.journal.file(intent.command.idempotency_key, "receipt"))
2089
- };
2090
- await confirm(this.cloud, m, deadline);
2091
- m.intent.capture_sha256 = backend.snapshot(this.journal.file(m.key, "intent")).sha256;
2092
- m.receipt_file.capture_sha256 = backend.snapshot(this.journal.file(m.key, "receipt")).sha256;
2093
- secureDirectory(this.control);
2094
- if (!createExact(join3(this.control, "manifest.json"), m))
2095
- throw new Error("Existing maintenance transaction");
2096
- await this.finish(backend, deadline);
2097
- retired++;
2098
- bytes += m.intent.sizeBytes + m.receipt_file.sizeBytes;
2099
- }
2100
- return { retired, remaining: this.journal.status().completed };
2101
- } catch (error) {
2102
- if (error instanceof JournalMaintenanceError)
2103
- throw error;
2104
- 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";
2105
- throw new JournalMaintenanceError(reason, this.journal.target);
2106
- }
2107
- }
2108
- async admit(command) {
2109
- const status = this.journal.status();
2110
- const fits = this.journal.canStage(command, status);
2111
- if (status.commands < 64 && status.reserved_bytes < 50331648 && fits)
2112
- return;
2113
- try {
2114
- await this.maintain();
2115
- } catch (error) {
2116
- const deferrable = error instanceof JournalMaintenanceError && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch", "recovery_authority_unavailable"].includes(error.reason);
2117
- if (!deferrable || exists(this.control) || !this.journal.canStage(command))
2118
- throw error;
2119
- }
2120
- if (!this.journal.canStage(command))
2121
- throw new JournalMaintenanceError("capacity", this.journal.target);
2122
- }
2123
- }
2124
-
2125
- // src/lib/ingest-journal-operation.ts
2126
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "fs";
2127
1442
  import { z } from "zod";
2128
-
2129
- // src/lib/ingest-journal-lock.ts
2130
- import { constants as constants3, closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3 } from "fs";
2131
- import { join as join4 } from "path";
2132
- async function withIngestJournalLock(directory, action) {
2133
- if (process.platform !== "linux" && process.platform !== "darwin")
2134
- throw new Error("Ingest journal locking requires Linux or macOS");
2135
- const { dlopen, FFIType } = await import("bun:ffi");
2136
- const library = dlopen(process.platform === "darwin" ? "/usr/lib/libSystem.B.dylib" : "libc.so.6", {
2137
- flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 }
2138
- });
2139
- const path = join4(directory, ".lease");
2140
- let fd;
2141
- try {
2142
- fd = openSync3(path, constants3.O_RDWR | constants3.O_CREAT | constants3.O_NOFOLLOW | constants3.O_NONBLOCK, 384);
2143
- const stat = fstatSync3(fd), current = lstatSync3(path);
2144
- 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())
2145
- throw new Error("Unsafe ingest journal lease");
2146
- if (library.symbols.flock(fd, 6) !== 0) {
2147
- const error = new Error("Ingest journal is busy; retry after the current operation finishes");
2148
- Object.assign(error, { code: "INGEST_JOURNAL_BUSY" });
2149
- throw error;
2150
- }
2151
- return await action();
2152
- } finally {
2153
- if (fd !== undefined)
2154
- closeSync3(fd);
2155
- library.close();
2156
- }
2157
- }
2158
-
2159
- // src/lib/ingest-journal-operation.ts
2160
- var ingestJournalOperationSchema = z.discriminatedUnion("action", [
2161
- z.object({ action: z.literal("status") }).strict(),
2162
- z.object({ action: z.literal("reconcile") }).strict(),
2163
- z.object({ action: z.literal("maintain"), confirm: z.literal(true), limit: z.number().int().min(1).max(32).default(32) }).strict()
2164
- ]);
2165
- async function runIngestJournalOperation(cloud, input) {
2166
- const options = ingestJournalOperationSchema.parse(input);
2167
- const journal = new IngestJournal(cloud);
2168
- return withIngestJournalLock(journal.directory, async () => {
2169
- const maintenance = new IngestJournalMaintenance(journal, cloud);
2170
- const unfinished = existsSync2(maintenance.control) || existsSync2(maintenance.operationRoot) && readdirSync2(maintenance.operationRoot).some((n) => n !== ".locks");
2171
- if (options.action === "status" && unfinished)
2172
- throw new JournalMaintenanceError("recovery_pending", journal.target);
2173
- if (options.action !== "status") {
2174
- await maintenance.recover();
2175
- if (options.action === "reconcile")
2176
- await journal.reconcilePending(cloud);
2177
- await verifyJournalAuthority(journal, cloud);
2178
- }
2179
- const sweep = options.action === "maintain" ? await maintenance.maintain(options.limit) : { retired: 0 };
2180
- return { schema: "economy.ingest-journal.v1", action: options.action, ...journal.status(), ...sweep };
2181
- });
2182
- }
2183
-
2184
- // src/lib/ingest-journal-client.ts
2185
1443
  async function ingestJournal(input) {
2186
1444
  const operation = ingestJournalOperationSchema.parse(input);
2187
1445
  const cloud = economyCloudStorage();
@@ -2190,7 +1448,7 @@ async function ingestJournal(input) {
2190
1448
  return runIngestJournalOperation(cloud, operation);
2191
1449
  }
2192
1450
  function ingestJournalError(error) {
2193
- if (error instanceof z2.ZodError)
1451
+ if (error instanceof z.ZodError)
2194
1452
  return { error: {
2195
1453
  code: "INVALID_JOURNAL_OPERATION",
2196
1454
  message: "Use status or reconcile without controls, or maintain with confirm=true and limit 1\u201332.",
@@ -2367,300 +1625,6 @@ function registerBriefCommand(program, deps = {}) {
2367
1625
  });
2368
1626
  }
2369
1627
 
2370
- // src/lib/cloud-ingest.ts
2371
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, renameSync, writeFileSync as writeFileSync2 } from "fs";
2372
- import { dirname, join as join5 } from "path";
2373
-
2374
- // src/lib/sync-maintenance.ts
2375
- function backfillMachineId(db) {
2376
- const machine = getMachineId();
2377
- const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE (machine_id = '' OR machine_id IS NULL)
2378
- AND NOT EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.request_id = requests.id)`).run(machine);
2379
- const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
2380
- return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
2381
- }
2382
- async function recalculateZeroCostRequests(db) {
2383
- const { assessCostFromDb } = await import("../chunks/pricing-bn88mhqk.js");
2384
- const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens
2385
- FROM requests
2386
- WHERE cost_usd = 0
2387
- AND NOT EXISTS (SELECT 1 FROM economy_legacy_active a WHERE a.request_id = requests.id)
2388
- AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
2389
- let fixed = 0;
2390
- for (const r of zeroRows) {
2391
- const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
2392
- const assessment = assessCostFromDb(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
2393
- const cost = assessment.usd;
2394
- if (cost !== null && cost > 0) {
2395
- db.prepare(`UPDATE requests SET cost_usd = ?, cost_basis = CASE WHEN cost_basis = 'unknown' THEN 'estimated' ELSE cost_basis END WHERE id = ?`).run(cost, r.id);
2396
- fixed++;
2397
- }
2398
- }
2399
- if (fixed > 0) {
2400
- const touchedSessions = new Set(zeroRows.map((r) => {
2401
- const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
2402
- return row?.session_id;
2403
- }).filter(Boolean));
2404
- for (const sid of touchedSessions)
2405
- rollupSession(db, sid);
2406
- }
2407
- return { fixed, total: zeroRows.length, buckets: queryZeroCostTokenizedModels(db, 8) };
2408
- }
2409
-
2410
- // src/lib/ingest-batches.ts
2411
- import { createHash as createHash3 } from "crypto";
2412
- function* ingestBatches(body) {
2413
- let rows = {};
2414
- let count = 0;
2415
- let bytes = 0;
2416
- const finish = () => {
2417
- const digest2 = createHash3("sha256").update(stableJson(rows)).digest("hex");
2418
- const batch = { ...rows, schema_version: 1, idempotency_key: `scan-${digest2}` };
2419
- rows = {};
2420
- count = 0;
2421
- bytes = 0;
2422
- if (Buffer.byteLength(JSON.stringify(batch)) > MAX_INGEST_BYTES)
2423
- throw new Error("Ingest batch exceeds the server byte limit");
2424
- return batch;
2425
- };
2426
- const rowLimit = Math.min(MAX_INGEST_ROWS, 500);
2427
- const byteLimit = MAX_INGEST_BYTES - 4096;
2428
- for (const table2 of Object.keys(body).sort()) {
2429
- const sorted = body[table2].map((row) => ({ row, canonical: stableJson(row) })).sort((a, b) => a.canonical < b.canonical ? -1 : a.canonical > b.canonical ? 1 : 0);
2430
- for (const { row, canonical } of sorted) {
2431
- const size = Buffer.byteLength(canonical) + 1;
2432
- if (size > byteLimit)
2433
- throw new Error("One ingestion row exceeds the server byte limit");
2434
- if (count > 0 && (count >= rowLimit || bytes + size > byteLimit))
2435
- yield finish();
2436
- (rows[table2] ??= []).push(row);
2437
- count++;
2438
- bytes += size;
2439
- }
2440
- }
2441
- if (count > 0)
2442
- yield finish();
2443
- }
2444
-
2445
- class IngestBatchFailure extends Error {
2446
- pending_key;
2447
- completed;
2448
- pending_command;
2449
- constructor(pending_key, completed, pending_command) {
2450
- super("Ingest batch did not return a verified receipt; retain the exact staged command and reconcile before retrying");
2451
- this.pending_key = pending_key;
2452
- this.completed = completed;
2453
- this.pending_command = pending_command;
2454
- this.name = "IngestBatchFailure";
2455
- }
2456
- }
2457
-
2458
- // src/lib/cloud-ingest.ts
2459
- import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
2460
- var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
2461
- function getIngestCachePath() {
2462
- if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
2463
- return process.env["HASNA_ECONOMY_INGEST_CACHE"];
2464
- const home = process.env["HOME"] || process.env["USERPROFILE"] || undefined;
2465
- return join5(cacheDir({ app: "economy", home }), "ingest-cache.json");
2466
- }
2467
- function readIngestCache(path) {
2468
- let text;
2469
- try {
2470
- text = readFileSync3(path, "utf8");
2471
- } catch {
2472
- return [];
2473
- }
2474
- try {
2475
- const parsed = JSON.parse(text);
2476
- const rows = Array.isArray(parsed?.ingest_state) ? parsed.ingest_state : [];
2477
- return rows.filter((row) => typeof row === "object" && row !== null && typeof row.source === "string" && typeof row.key === "string" && typeof row.value === "string");
2478
- } catch {
2479
- return [];
2480
- }
2481
- }
2482
- function writeIngestCache(path, rows) {
2483
- mkdirSync2(dirname(path), { recursive: true });
2484
- const tmp = `${path}.${process.pid}.tmp`;
2485
- writeFileSync2(tmp, JSON.stringify({ version: 1, ingest_state: rows }), { mode: 384 });
2486
- renameSync(tmp, path);
2487
- }
2488
- function loadIngestState(scratch, rows) {
2489
- scratch.prepare(`DELETE FROM ingest_state`).run();
2490
- const insert = scratch.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
2491
- for (const row of rows)
2492
- insert.run(row.source, row.key, row.value);
2493
- }
2494
- function dumpIngestState(scratch) {
2495
- return scratch.prepare(`SELECT source, key, value FROM ingest_state`).all();
2496
- }
2497
- function exportIngestRows(db) {
2498
- const body = {};
2499
- for (const table2 of CLOUD_INGEST_TABLES) {
2500
- const rows = db.prepare(`SELECT * FROM ${table2}`).all();
2501
- if (rows.length > 0)
2502
- body[table2] = rows;
2503
- }
2504
- return body;
2505
- }
2506
-
2507
- class IngestRefused extends Error {
2508
- status;
2509
- code;
2510
- reason;
2511
- constructor(status, code, reason = code) {
2512
- super(`Ingestion refused: ${reason}; exact command intent retained`);
2513
- this.status = status;
2514
- this.code = code;
2515
- this.reason = reason;
2516
- this.name = "IngestRefused";
2517
- }
2518
- }
2519
- async function postIngest(cloud, body) {
2520
- const journal = new IngestJournal(cloud);
2521
- return withIngestJournalLock(journal.directory, async () => {
2522
- const maintenance = new IngestJournalMaintenance(journal, cloud);
2523
- await maintenance.recover();
2524
- await journal.reconcilePending(cloud);
2525
- await verifyJournalAuthority(journal, cloud);
2526
- const ingested = {};
2527
- const completed = [];
2528
- let total = 0;
2529
- for (const batch of ingestBatches(body)) {
2530
- try {
2531
- const refused = journal.refusal(batch);
2532
- if (refused)
2533
- throw new IngestRefused(refused.status, refused.code, refused.reason);
2534
- await maintenance.admit(batch);
2535
- const known = await journal.lookupReceipt(cloud, batch);
2536
- const fresh = journal.stage(batch);
2537
- const sendFresh = async () => {
2538
- let response;
2539
- try {
2540
- response = await cloud.client.transport.post("/ingest", batch, { retry: false });
2541
- } catch (error) {
2542
- const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
2543
- if (refusal) {
2544
- journal.recordRefusal(batch, refusal);
2545
- throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
2546
- }
2547
- throw error;
2548
- }
2549
- return journal.accept(batch, response);
2550
- };
2551
- const payload = known ? journal.accept(batch, known) : fresh ? await sendFresh() : await journal.reconcileCommand(cloud, batch);
2552
- await verifyJournalAuthority(journal, cloud);
2553
- if (!payload)
2554
- throw new PendingIngestError([batch.idempotency_key]);
2555
- verifyIngestReceipt(payload, batch);
2556
- const counts = payload["ingested"];
2557
- if (!counts || typeof counts !== "object" || Array.isArray(counts) || typeof payload["total"] !== "number" || !Number.isSafeInteger(payload["total"]) || payload["total"] < 0)
2558
- throw new Error("Invalid ingest receipt");
2559
- let sum = 0;
2560
- for (const [table2, count] of Object.entries(counts)) {
2561
- if (!CLOUD_INGEST_TABLES.includes(table2) || typeof count !== "number" || !Number.isSafeInteger(count) || count < 0)
2562
- throw new Error("Invalid ingest receipt");
2563
- sum += count;
2564
- ingested[table2] = (ingested[table2] ?? 0) + count;
2565
- }
2566
- if (sum !== payload["total"])
2567
- throw new Error("Invalid ingest receipt");
2568
- total += sum;
2569
- const operation = payload["operation_key"];
2570
- if (operation !== undefined && (typeof operation !== "string" || !/^[a-f0-9]{64}$/.test(operation)))
2571
- throw new Error("Invalid ingest receipt");
2572
- completed.push({
2573
- idempotency_key: batch.idempotency_key,
2574
- ...typeof operation === "string" ? { operation_key: operation } : {},
2575
- replayed: payload["replayed"] === true
2576
- });
2577
- } catch (error) {
2578
- if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
2579
- throw error;
2580
- if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
2581
- const body2 = error.body;
2582
- throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
2583
- }
2584
- throw new IngestBatchFailure(batch.idempotency_key, completed, batch);
2585
- }
2586
- }
2587
- return { ingested, total, receipts: completed };
2588
- });
2589
- }
2590
- async function syncAllToCloud(cloud, opts = {}) {
2591
- await runIngestJournalOperation(cloud, { action: "reconcile" });
2592
- const { openDatabase } = await import("../chunks/sqlite-store-r219qrjt.js");
2593
- const scratch = openDatabase(":memory:", true);
2594
- ensurePricingSeeded(scratch);
2595
- const cachePath = opts.cachePath ?? getIngestCachePath();
2596
- try {
2597
- loadIngestState(scratch, opts.force ? [] : readIngestCache(cachePath));
2598
- const result = await syncAll(scratch, { ...opts, noCrossAppLocalReads: true });
2599
- if (opts.backfillMachine)
2600
- backfillMachineId(scratch);
2601
- if (opts.recalculate)
2602
- await recalculateZeroCostRequests(scratch);
2603
- const body = exportIngestRows(scratch);
2604
- if (Object.keys(body).length === 0) {
2605
- writeIngestCache(cachePath, dumpIngestState(scratch));
2606
- return { ...result, posted: false };
2607
- }
2608
- const response = await postIngest(cloud, body);
2609
- writeIngestCache(cachePath, dumpIngestState(scratch));
2610
- return { ...result, posted: true, ...response };
2611
- } finally {
2612
- try {
2613
- scratch.close();
2614
- } catch {}
2615
- }
2616
- }
2617
- async function billingSyncToCloud(cloud, opts = {}) {
2618
- await runIngestJournalOperation(cloud, { action: "reconcile" });
2619
- const { openDatabase } = await import("../chunks/sqlite-store-r219qrjt.js");
2620
- const scratch = openDatabase(":memory:", true);
2621
- try {
2622
- const days = opts.days ?? 31;
2623
- const providers = {};
2624
- const attempts = [
2625
- ["anthropic", () => syncAnthropicBilling(scratch, { days })],
2626
- ["openai", () => syncOpenAIBilling(scratch, { days })],
2627
- ["gemini", () => syncGeminiBilling(scratch, { days })]
2628
- ];
2629
- const anySelected = opts.anthropic || opts.openai || opts.gemini;
2630
- let totalUsd = 0;
2631
- for (const [provider, run] of attempts) {
2632
- if (anySelected && !opts[provider])
2633
- continue;
2634
- {
2635
- try {
2636
- const r = await run();
2637
- providers[provider] = { ok: true, days: r.days, totalUsd: r.totalUsd };
2638
- totalUsd += r.totalUsd;
2639
- } catch {
2640
- providers[provider] = { ok: false, error: "provider_billing_collection_failed" };
2641
- }
2642
- }
2643
- }
2644
- const body = exportIngestRows(scratch);
2645
- if (Object.keys(body).length === 0)
2646
- return { providers, totalUsd, posted: false };
2647
- const response = await postIngest(cloud, body);
2648
- return { providers, totalUsd, posted: true, ...response };
2649
- } finally {
2650
- try {
2651
- scratch.close();
2652
- } catch {}
2653
- }
2654
- }
2655
-
2656
- // src/lib/sync-report.ts
2657
- function syncHasFailures(result) {
2658
- return result.collections?.some((row) => row.status === "failed" || row.status === "unsupported") ?? false;
2659
- }
2660
- function syncOutcomeLines(result) {
2661
- return (result.collections ?? []).map((row) => `${row.source}: ${row.status}${row.error_code ? ` (${row.error_code})` : ""}`);
2662
- }
2663
-
2664
1628
  // src/lib/autosync-gate.ts
2665
1629
  var AUTOSYNC_STATE_SOURCE = "autosync";
2666
1630
  var AUTOSYNC_STATE_KEY = "last_run";
@@ -2698,7 +1662,7 @@ async function autoSyncDue(db, env = process.env) {
2698
1662
  }
2699
1663
 
2700
1664
  // src/lib/contracts-client/transport.ts
2701
- import { HasnaHttpError as HasnaHttpError3 } from "@hasna/contracts/client";
1665
+ import { HasnaHttpError } from "@hasna/contracts/client";
2702
1666
 
2703
1667
  // src/cli/index.ts
2704
1668
  var program = new Command;
@@ -3242,13 +2206,15 @@ program.command("accounts [period]").description("List account usage by email ad
3242
2206
  printHiddenRowsHint(rows.length, visibleRows.length, "Use --limit <n>, --verbose, or --json.");
3243
2207
  console.log();
3244
2208
  });
3245
- program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
3246
- const { watchCosts } = await import("../chunks/watch-7pf8xj1q.js");
2209
+ program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--sync-interval <seconds>", "Minimum collection interval and fallback scan interval for --daemon", "60").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").option("--no-notifications", "Disable desktop notifications, including high-cost notices").action(async (opts) => {
2210
+ const { watchCosts } = await import("../chunks/watch-ewed2srf.js");
3247
2211
  await watchCosts({
3248
2212
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
3249
2213
  daemon: Boolean(opts.daemon),
2214
+ syncInterval: parsePositiveCliInteger(opts.syncInterval ?? "60", "--sync-interval"),
3250
2215
  agent: parseOptionalCliAgent(opts.agent),
3251
- notify: opts.notify ? parsePositiveCliNumber(opts.notify, "--notify") : undefined
2216
+ notify: opts.notify ? parsePositiveCliNumber(opts.notify, "--notify") : undefined,
2217
+ notifications: opts.notifications
3252
2218
  });
3253
2219
  });
3254
2220
  var budgetCmd = program.command("budget").description("Manage spending budgets");
@@ -3479,7 +2445,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
3479
2445
  });
3480
2446
  program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
3481
2447
  const port = parseCliPort(opts.port ?? "3456", "--port");
3482
- const { startServer } = await import("../chunks/serve-bqn5j46v.js");
2448
+ const { startServer } = await import("../chunks/serve-kk3n0p7j.js");
3483
2449
  startServer(port);
3484
2450
  });
3485
2451
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
@@ -3594,8 +2560,8 @@ program.command("export").description("Export data as CSV").option("--type <type
3594
2560
  }
3595
2561
  }
3596
2562
  if (opts.output) {
3597
- const { writeFileSync: writeFileSync3 } = await import("fs");
3598
- writeFileSync3(opts.output, csv);
2563
+ const { writeFileSync } = await import("fs");
2564
+ writeFileSync(opts.output, csv);
3599
2565
  console.log(chalk3.green(`\u2713 Exported to ${opts.output}`));
3600
2566
  } else {
3601
2567
  process.stdout.write(csv);
@@ -3900,7 +2866,7 @@ registerJournalCommands(program);
3900
2866
  registerFleetCommands(program);
3901
2867
  registerEventsCommands(program, { source: "economy" });
3902
2868
  function reportCliError(err) {
3903
- if (err instanceof HasnaHttpError3) {
2869
+ if (err instanceof HasnaHttpError) {
3904
2870
  if (err.status === 404) {
3905
2871
  fail(`economy: the cloud API has no endpoint for this command (${err.method} ${err.path} -> 404). ` + `The economy API server is likely running an older build \u2014 it needs a redeploy of the current version.`);
3906
2872
  }