@hasna/economy 0.5.8 → 0.5.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/chunks/{index-h2mcj7gv.js → index-4v5r3c22.js} +1 -1
- package/dist/chunks/{index-9za4cmq2.js → index-t777mc83.js} +1 -1
- package/dist/chunks/{index-a1gh3d6t.js → index-twn8ncdp.js} +1 -1
- package/dist/chunks/{index-gf3kan31.js → index-w4r6xs02.js} +1 -1
- package/dist/chunks/{menubar-6xtkt17h.js → menubar-zgazgppv.js} +1 -1
- package/dist/chunks/{serve-14cgmx02.js → serve-bqn5j46v.js} +2 -2
- package/dist/chunks/{tui-s6tpce3h.js → tui-5s5w6yjd.js} +3 -3
- package/dist/chunks/{watch-cv5va8x6.js → watch-7pf8xj1q.js} +3 -3
- package/dist/cli/index.js +75 -32
- package/dist/index.js +56 -13
- package/dist/lib/ingest-journal-maintenance.d.ts.map +1 -1
- package/dist/lib/ingest-journal.d.ts.map +1 -1
- package/dist/mcp/index.js +59 -16
- package/dist/otel/index.js +59 -16
- package/dist/server/index.js +1 -1
- package/dist/server/service-worker.js +1 -1
- package/docs/ingest-journal.md +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Retry temporarily unavailable receipt reads within the original deadline without replaying ingestion. Fix macOS installation to verify the release signature using an inline signing requirement.
|
|
8
|
+
|
|
9
|
+
## 0.5.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Give each journal recovery transaction its own bounded deadline so slow storage does not interrupt healthy collection, and stop ingestion whenever cleanup leaves unresolved recovery state.
|
|
14
|
+
|
|
3
15
|
## 0.5.8
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -86,7 +86,7 @@ function comparisonValueChange(left, right) {
|
|
|
86
86
|
// package.json
|
|
87
87
|
var package_default = {
|
|
88
88
|
name: "@hasna/economy",
|
|
89
|
-
version: "0.5.
|
|
89
|
+
version: "0.5.10",
|
|
90
90
|
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",
|
|
91
91
|
repository: {
|
|
92
92
|
type: "git",
|
|
@@ -164,7 +164,7 @@ function createMenubarManager(host = runtime) {
|
|
|
164
164
|
refuse("BUNDLE_VERSION", "Release version or build is invalid.");
|
|
165
165
|
if (!host.run("/usr/bin/lipo", ["-archs", join(path, "Contents/MacOS", EXECUTABLE)]).split(/\s+/).includes(host.architecture))
|
|
166
166
|
refuse("BUNDLE_ARCHITECTURE", "The app does not support this Mac architecture.");
|
|
167
|
-
host.run("/usr/bin/codesign", ["--verify", "--deep", "--strict", "-R",
|
|
167
|
+
host.run("/usr/bin/codesign", ["--verify", "--deep", "--strict", "-R", `=anchor apple generic and certificate leaf[subject.OU] = "${TEAM_ID}" and identifier "${BUNDLE_ID}"`, path]);
|
|
168
168
|
host.run("/usr/sbin/spctl", ["--assess", "--type", "execute", path]);
|
|
169
169
|
const after = fingerprintMacApp(path);
|
|
170
170
|
if (before.sha256 !== after.sha256)
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./index-w3bvv0ns.js";
|
|
6
6
|
import {
|
|
7
7
|
syncAll
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-t777mc83.js";
|
|
9
9
|
import {
|
|
10
10
|
buildBrief,
|
|
11
11
|
packageMetadata,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
queryProjectDetail,
|
|
17
17
|
queryRangeStats,
|
|
18
18
|
querySavingsSummary
|
|
19
|
-
} from "./index-
|
|
19
|
+
} from "./index-4v5r3c22.js";
|
|
20
20
|
import {
|
|
21
21
|
AGENTS,
|
|
22
22
|
INGEST_SCHEMA_SQL,
|
|
@@ -6,10 +6,10 @@ import {
|
|
|
6
6
|
printStatusLine,
|
|
7
7
|
printWaybarJson,
|
|
8
8
|
runTui
|
|
9
|
-
} from "./index-
|
|
9
|
+
} from "./index-w4r6xs02.js";
|
|
10
10
|
import"./index-w3bvv0ns.js";
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
import"./index-twn8ncdp.js";
|
|
12
|
+
import"./index-4v5r3c22.js";
|
|
13
13
|
import"./index-zgvmrqyc.js";
|
|
14
14
|
import"./index-p35cyhc4.js";
|
|
15
15
|
export {
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import {
|
|
3
3
|
agentPaths,
|
|
4
4
|
syncAll
|
|
5
|
-
} from "./index-
|
|
5
|
+
} from "./index-t777mc83.js";
|
|
6
6
|
import {
|
|
7
7
|
getStore,
|
|
8
8
|
isCloudStore
|
|
9
|
-
} from "./index-
|
|
10
|
-
import"./index-
|
|
9
|
+
} from "./index-twn8ncdp.js";
|
|
10
|
+
import"./index-4v5r3c22.js";
|
|
11
11
|
import {
|
|
12
12
|
formatSummaryValue
|
|
13
13
|
} from "./index-zgvmrqyc.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -8,16 +8,16 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
buildStatusLine,
|
|
10
10
|
gatherStatusData
|
|
11
|
-
} from "../chunks/index-
|
|
11
|
+
} from "../chunks/index-w4r6xs02.js";
|
|
12
12
|
import"../chunks/index-w3bvv0ns.js";
|
|
13
13
|
import {
|
|
14
14
|
agentPaths,
|
|
15
15
|
syncAll
|
|
16
|
-
} from "../chunks/index-
|
|
16
|
+
} from "../chunks/index-t777mc83.js";
|
|
17
17
|
import {
|
|
18
18
|
getStore,
|
|
19
19
|
isCloudStore
|
|
20
|
-
} from "../chunks/index-
|
|
20
|
+
} from "../chunks/index-twn8ncdp.js";
|
|
21
21
|
import {
|
|
22
22
|
billingDriftCheck,
|
|
23
23
|
comparisonValueChange,
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
economyTransportReport,
|
|
26
26
|
formatComparisonValue,
|
|
27
27
|
packageMetadata
|
|
28
|
-
} from "../chunks/index-
|
|
28
|
+
} from "../chunks/index-4v5r3c22.js";
|
|
29
29
|
import {
|
|
30
30
|
AGENTS,
|
|
31
31
|
MAX_INGEST_BYTES,
|
|
@@ -1359,23 +1359,23 @@ function registerExtendedCommands(program) {
|
|
|
1359
1359
|
console.log();
|
|
1360
1360
|
});
|
|
1361
1361
|
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-
|
|
1362
|
+
const { runTui } = await import("../chunks/tui-5s5w6yjd.js");
|
|
1363
1363
|
await runTui({
|
|
1364
1364
|
watch: Boolean(opts.watch),
|
|
1365
1365
|
interval: Number(opts.interval ?? 30)
|
|
1366
1366
|
});
|
|
1367
1367
|
});
|
|
1368
1368
|
program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
|
|
1369
|
-
const { printWaybarJson } = await import("../chunks/tui-
|
|
1369
|
+
const { printWaybarJson } = await import("../chunks/tui-5s5w6yjd.js");
|
|
1370
1370
|
await printWaybarJson();
|
|
1371
1371
|
});
|
|
1372
1372
|
const barCmd = program.command("bar").description("Status bar helpers (Linux)");
|
|
1373
1373
|
barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
|
|
1374
|
-
const { runTui } = await import("../chunks/tui-
|
|
1374
|
+
const { runTui } = await import("../chunks/tui-5s5w6yjd.js");
|
|
1375
1375
|
await runTui({ watch: Boolean(opts.watch) });
|
|
1376
1376
|
});
|
|
1377
1377
|
barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
|
|
1378
|
-
const { printWaybarJson } = await import("../chunks/tui-
|
|
1378
|
+
const { printWaybarJson } = await import("../chunks/tui-5s5w6yjd.js");
|
|
1379
1379
|
await printWaybarJson();
|
|
1380
1380
|
});
|
|
1381
1381
|
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) => {
|
|
@@ -1439,6 +1439,7 @@ import { z as z2 } from "zod";
|
|
|
1439
1439
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
1440
1440
|
import { createHash } from "crypto";
|
|
1441
1441
|
import { join as join2, resolve } from "path";
|
|
1442
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
1442
1443
|
var MAX_COMMANDS = 1000;
|
|
1443
1444
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
1444
1445
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -1550,15 +1551,55 @@ function sameReceipt(left, right) {
|
|
|
1550
1551
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
1551
1552
|
}
|
|
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");
|
|
1553
1556
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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
|
+
}
|
|
1562
1603
|
}
|
|
1563
1604
|
|
|
1564
1605
|
class IngestJournal {
|
|
@@ -2024,18 +2065,19 @@ class IngestJournalMaintenance {
|
|
|
2024
2065
|
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
2025
2066
|
await this.recover();
|
|
2026
2067
|
await verifyJournalAuthority(this.journal, this.cloud);
|
|
2027
|
-
const
|
|
2068
|
+
const sweepDeadline = Date.now() + 30000;
|
|
2028
2069
|
let retired = 0, bytes = 0;
|
|
2029
2070
|
let backend;
|
|
2030
2071
|
try {
|
|
2031
2072
|
const names = this.journal.names();
|
|
2032
2073
|
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2033
|
-
if (retired >= limit || Date.now() >=
|
|
2074
|
+
if (retired >= limit || Date.now() >= sweepDeadline || retired > 0 && bytes >= 1048576)
|
|
2034
2075
|
break;
|
|
2035
2076
|
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
2036
2077
|
continue;
|
|
2037
2078
|
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
2038
|
-
|
|
2079
|
+
const deadline = Date.now() + 30000;
|
|
2080
|
+
backend = await this.backend(deadline);
|
|
2039
2081
|
const m = {
|
|
2040
2082
|
version: 1,
|
|
2041
2083
|
target: this.journal.target,
|
|
@@ -2071,7 +2113,8 @@ class IngestJournalMaintenance {
|
|
|
2071
2113
|
try {
|
|
2072
2114
|
await this.maintain();
|
|
2073
2115
|
} catch (error) {
|
|
2074
|
-
|
|
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))
|
|
2075
2118
|
throw error;
|
|
2076
2119
|
}
|
|
2077
2120
|
if (!this.journal.canStage(command))
|
|
@@ -2413,7 +2456,7 @@ class IngestBatchFailure extends Error {
|
|
|
2413
2456
|
}
|
|
2414
2457
|
|
|
2415
2458
|
// src/lib/cloud-ingest.ts
|
|
2416
|
-
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
2459
|
+
import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
|
|
2417
2460
|
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
2418
2461
|
function getIngestCachePath() {
|
|
2419
2462
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
@@ -2496,7 +2539,7 @@ async function postIngest(cloud, body) {
|
|
|
2496
2539
|
try {
|
|
2497
2540
|
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
2498
2541
|
} catch (error) {
|
|
2499
|
-
const refusal = error instanceof
|
|
2542
|
+
const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
2500
2543
|
if (refusal) {
|
|
2501
2544
|
journal.recordRefusal(batch, refusal);
|
|
2502
2545
|
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
@@ -2534,7 +2577,7 @@ async function postIngest(cloud, body) {
|
|
|
2534
2577
|
} catch (error) {
|
|
2535
2578
|
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
2536
2579
|
throw error;
|
|
2537
|
-
if (error instanceof
|
|
2580
|
+
if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
2538
2581
|
const body2 = error.body;
|
|
2539
2582
|
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
2540
2583
|
}
|
|
@@ -2655,7 +2698,7 @@ async function autoSyncDue(db, env = process.env) {
|
|
|
2655
2698
|
}
|
|
2656
2699
|
|
|
2657
2700
|
// src/lib/contracts-client/transport.ts
|
|
2658
|
-
import { HasnaHttpError as
|
|
2701
|
+
import { HasnaHttpError as HasnaHttpError3 } from "@hasna/contracts/client";
|
|
2659
2702
|
|
|
2660
2703
|
// src/cli/index.ts
|
|
2661
2704
|
var program = new Command;
|
|
@@ -3200,7 +3243,7 @@ program.command("accounts [period]").description("List account usage by email ad
|
|
|
3200
3243
|
console.log();
|
|
3201
3244
|
});
|
|
3202
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) => {
|
|
3203
|
-
const { watchCosts } = await import("../chunks/watch-
|
|
3246
|
+
const { watchCosts } = await import("../chunks/watch-7pf8xj1q.js");
|
|
3204
3247
|
await watchCosts({
|
|
3205
3248
|
interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
|
|
3206
3249
|
daemon: Boolean(opts.daemon),
|
|
@@ -3436,7 +3479,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
|
|
|
3436
3479
|
});
|
|
3437
3480
|
program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
|
|
3438
3481
|
const port = parseCliPort(opts.port ?? "3456", "--port");
|
|
3439
|
-
const { startServer } = await import("../chunks/serve-
|
|
3482
|
+
const { startServer } = await import("../chunks/serve-bqn5j46v.js");
|
|
3440
3483
|
startServer(port);
|
|
3441
3484
|
});
|
|
3442
3485
|
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) => {
|
|
@@ -3660,23 +3703,23 @@ program.command("efficiency").description("Show output/input token ratio per mod
|
|
|
3660
3703
|
});
|
|
3661
3704
|
var menubarCmd = program.command("menubar").description("Manage the signed HasnaEconomy macOS menu-bar app");
|
|
3662
3705
|
menubarCmd.command("inspect").description("Verify a selected release bundle and report its content hash as JSON").requiredOption("--app <absolute-path>", "Path to the signed and notarized HasnaEconomy.app artifact").action(async (opts) => {
|
|
3663
|
-
const { menubarInspect } = await import("../chunks/menubar-
|
|
3706
|
+
const { menubarInspect } = await import("../chunks/menubar-zgazgppv.js");
|
|
3664
3707
|
menubarInspect(opts);
|
|
3665
3708
|
});
|
|
3666
3709
|
menubarCmd.command("install").description("Install an explicitly selected release; refuse an occupied destination").requiredOption("--app <absolute-path>", "Path to the signed and notarized HasnaEconomy.app artifact").requiredOption("--app-version <version>", "Expected reviewed release version").requiredOption("--sha256 <hash>", "Expected hasna-economy-app-tree-v1 SHA-256 from the release receipt").action(async (opts) => {
|
|
3667
|
-
const { menubarInstall } = await import("../chunks/menubar-
|
|
3710
|
+
const { menubarInstall } = await import("../chunks/menubar-zgazgppv.js");
|
|
3668
3711
|
await menubarInstall(opts);
|
|
3669
3712
|
});
|
|
3670
3713
|
menubarCmd.command("uninstall").description("Capture the stopped HasnaEconomy app through Trash; preserve its recovery receipt").action(async () => {
|
|
3671
|
-
const { menubarUninstall } = await import("../chunks/menubar-
|
|
3714
|
+
const { menubarUninstall } = await import("../chunks/menubar-zgazgppv.js");
|
|
3672
3715
|
menubarUninstall();
|
|
3673
3716
|
});
|
|
3674
3717
|
menubarCmd.command("start").description("Verify HasnaEconomy and request launch").action(async () => {
|
|
3675
|
-
const { menubarStart } = await import("../chunks/menubar-
|
|
3718
|
+
const { menubarStart } = await import("../chunks/menubar-zgazgppv.js");
|
|
3676
3719
|
menubarStart();
|
|
3677
3720
|
});
|
|
3678
3721
|
menubarCmd.command("stop").description("Explicitly request that the installed HasnaEconomy app quit").action(async () => {
|
|
3679
|
-
const { menubarStop } = await import("../chunks/menubar-
|
|
3722
|
+
const { menubarStop } = await import("../chunks/menubar-zgazgppv.js");
|
|
3680
3723
|
menubarStop();
|
|
3681
3724
|
});
|
|
3682
3725
|
var goalCmd = program.command("goal").description("Manage spending goals");
|
|
@@ -3857,7 +3900,7 @@ registerJournalCommands(program);
|
|
|
3857
3900
|
registerFleetCommands(program);
|
|
3858
3901
|
registerEventsCommands(program, { source: "economy" });
|
|
3859
3902
|
function reportCliError(err) {
|
|
3860
|
-
if (err instanceof
|
|
3903
|
+
if (err instanceof HasnaHttpError3) {
|
|
3861
3904
|
if (err.status === 404) {
|
|
3862
3905
|
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.`);
|
|
3863
3906
|
}
|
package/dist/index.js
CHANGED
|
@@ -111,7 +111,7 @@ import { randomUUID } from "crypto";
|
|
|
111
111
|
// package.json
|
|
112
112
|
var package_default = {
|
|
113
113
|
name: "@hasna/economy",
|
|
114
|
-
version: "0.5.
|
|
114
|
+
version: "0.5.10",
|
|
115
115
|
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",
|
|
116
116
|
repository: {
|
|
117
117
|
type: "git",
|
|
@@ -1760,6 +1760,7 @@ import { z as z2 } from "zod";
|
|
|
1760
1760
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
1761
1761
|
import { createHash } from "crypto";
|
|
1762
1762
|
import { join, resolve } from "path";
|
|
1763
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
1763
1764
|
var MAX_COMMANDS = 1000;
|
|
1764
1765
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
1765
1766
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -1871,15 +1872,55 @@ function sameReceipt(left, right) {
|
|
|
1871
1872
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
1872
1873
|
}
|
|
1873
1874
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
1875
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
1876
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
1874
1877
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
1875
|
-
const
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1878
|
+
const deadline = performance.now() + timeoutMs;
|
|
1879
|
+
let failures = 0, lastError;
|
|
1880
|
+
for (;; ) {
|
|
1881
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
1882
|
+
if (remaining <= 0)
|
|
1883
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
1884
|
+
let response;
|
|
1885
|
+
try {
|
|
1886
|
+
const abort = new AbortController;
|
|
1887
|
+
let timer;
|
|
1888
|
+
try {
|
|
1889
|
+
const expired = new Promise((_, reject) => {
|
|
1890
|
+
timer = setTimeout(() => {
|
|
1891
|
+
abort.abort();
|
|
1892
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
1893
|
+
}, remaining);
|
|
1894
|
+
});
|
|
1895
|
+
response = await Promise.race([
|
|
1896
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
1897
|
+
expired
|
|
1898
|
+
]);
|
|
1899
|
+
} finally {
|
|
1900
|
+
clearTimeout(timer);
|
|
1901
|
+
}
|
|
1902
|
+
} catch (error) {
|
|
1903
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
1904
|
+
throw error;
|
|
1905
|
+
lastError = error;
|
|
1906
|
+
failures++;
|
|
1907
|
+
const delay = 100 * failures;
|
|
1908
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
1909
|
+
throw error;
|
|
1910
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
if (performance.now() >= deadline)
|
|
1914
|
+
throw new Error("Receipt lookup deadline expired");
|
|
1915
|
+
const raw = unwrap(response);
|
|
1916
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
1917
|
+
throw new Error("Invalid reconciliation response");
|
|
1918
|
+
if (raw.status === "absent")
|
|
1919
|
+
return null;
|
|
1920
|
+
if (raw.status !== "committed")
|
|
1921
|
+
throw new Error("Invalid reconciliation response");
|
|
1922
|
+
return raw.receipt;
|
|
1923
|
+
}
|
|
1883
1924
|
}
|
|
1884
1925
|
|
|
1885
1926
|
class IngestJournal {
|
|
@@ -2345,18 +2386,19 @@ class IngestJournalMaintenance {
|
|
|
2345
2386
|
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
2346
2387
|
await this.recover();
|
|
2347
2388
|
await verifyJournalAuthority(this.journal, this.cloud);
|
|
2348
|
-
const
|
|
2389
|
+
const sweepDeadline = Date.now() + 30000;
|
|
2349
2390
|
let retired = 0, bytes = 0;
|
|
2350
2391
|
let backend;
|
|
2351
2392
|
try {
|
|
2352
2393
|
const names = this.journal.names();
|
|
2353
2394
|
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
2354
|
-
if (retired >= limit || Date.now() >=
|
|
2395
|
+
if (retired >= limit || Date.now() >= sweepDeadline || retired > 0 && bytes >= 1048576)
|
|
2355
2396
|
break;
|
|
2356
2397
|
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
2357
2398
|
continue;
|
|
2358
2399
|
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
2359
|
-
|
|
2400
|
+
const deadline = Date.now() + 30000;
|
|
2401
|
+
backend = await this.backend(deadline);
|
|
2360
2402
|
const m = {
|
|
2361
2403
|
version: 1,
|
|
2362
2404
|
target: this.journal.target,
|
|
@@ -2392,7 +2434,8 @@ class IngestJournalMaintenance {
|
|
|
2392
2434
|
try {
|
|
2393
2435
|
await this.maintain();
|
|
2394
2436
|
} catch (error) {
|
|
2395
|
-
|
|
2437
|
+
const deferrable = error instanceof JournalMaintenanceError && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch", "recovery_authority_unavailable"].includes(error.reason);
|
|
2438
|
+
if (!deferrable || exists(this.control) || !this.journal.canStage(command))
|
|
2396
2439
|
throw error;
|
|
2397
2440
|
}
|
|
2398
2441
|
if (!this.journal.canStage(command))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest-journal-maintenance.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal-maintenance.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"ingest-journal-maintenance.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal-maintenance.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAA;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,EAAe,aAAa,EAAqF,MAAM,qBAAqB,CAAA;AAKnJ,qBAAa,uBAAwB,SAAQ,KAAK;IAEpC,QAAQ,CAAC,MAAM,EAAE,MAAM;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM;IAD5D,QAAQ,CAAC,IAAI,yCAAwC;gBAChC,MAAM,EAAE,MAAM,EAAW,MAAM,EAAE,MAAM;CAI7D;AACD,UAAU,KAAK;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AACrJ,UAAU,SAAS;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE;AAC7J,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC1C,MAAM,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE;YAAE,EAAE,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QAAC,aAAa,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC5E,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC3E,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,CAAA;KAAE,CAAA;IACxC,GAAG,EAAE;QAAE,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;KAAE,CAAA;IACxC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,KAAK,CAAC,CAAA;IACvE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,iBAAiB,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC/E;AACD,MAAM,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,eAAe,CAAC,CAAA;AAkCrK;iFACiF;AACjF,wBAAsB,sBAAsB,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAWpH;AAED;uEACuE;AACvE,wBAAgB,eAAe,CAAC,KAAK,EAAE,yBAAyB,EAAE,GAAG,oBAAc,GAAG,sBAAsB,CAgB3G;AAED;;oEAEoE;AACpE,qBAAa,wBAAwB;IAGvB,QAAQ,CAAC,OAAO,EAAE,aAAa;IAAE,QAAQ,CAAC,KAAK,EAAE,yBAAyB;IACpF,QAAQ,CAAC,OAAO;IAHlB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;gBACT,OAAO,EAAE,aAAa,EAAW,KAAK,EAAE,yBAAyB,EAC3E,OAAO,yBAAyB;IAK3C,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,OAAO;YAKD,OAAO;YAsBP,KAAK;YAML,QAAQ;IA2BtB,gFAAgF;IAC1E,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;YAQhB,MAAM;IA+Bd,QAAQ,CAAC,KAAK,SAAK,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAoCrE,KAAK,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;CAcjD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest-journal.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ingest-journal.d.ts","sourceRoot":"","sources":["../../src/lib/ingest-journal.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -86,7 +86,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
86
86
|
// package.json
|
|
87
87
|
var package_default = {
|
|
88
88
|
name: "@hasna/economy",
|
|
89
|
-
version: "0.5.
|
|
89
|
+
version: "0.5.10",
|
|
90
90
|
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",
|
|
91
91
|
repository: {
|
|
92
92
|
type: "git",
|
|
@@ -758,6 +758,7 @@ import { z as z3 } from "zod";
|
|
|
758
758
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
759
759
|
import { createHash } from "crypto";
|
|
760
760
|
import { join as join2, resolve } from "path";
|
|
761
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
761
762
|
var MAX_COMMANDS = 1000;
|
|
762
763
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
763
764
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -869,15 +870,55 @@ function sameReceipt(left, right) {
|
|
|
869
870
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
870
871
|
}
|
|
871
872
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
873
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
874
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
872
875
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
873
|
-
const
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
876
|
+
const deadline = performance.now() + timeoutMs;
|
|
877
|
+
let failures = 0, lastError;
|
|
878
|
+
for (;; ) {
|
|
879
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
880
|
+
if (remaining <= 0)
|
|
881
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
882
|
+
let response;
|
|
883
|
+
try {
|
|
884
|
+
const abort = new AbortController;
|
|
885
|
+
let timer;
|
|
886
|
+
try {
|
|
887
|
+
const expired = new Promise((_, reject) => {
|
|
888
|
+
timer = setTimeout(() => {
|
|
889
|
+
abort.abort();
|
|
890
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
891
|
+
}, remaining);
|
|
892
|
+
});
|
|
893
|
+
response = await Promise.race([
|
|
894
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
895
|
+
expired
|
|
896
|
+
]);
|
|
897
|
+
} finally {
|
|
898
|
+
clearTimeout(timer);
|
|
899
|
+
}
|
|
900
|
+
} catch (error) {
|
|
901
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
902
|
+
throw error;
|
|
903
|
+
lastError = error;
|
|
904
|
+
failures++;
|
|
905
|
+
const delay = 100 * failures;
|
|
906
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
907
|
+
throw error;
|
|
908
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (performance.now() >= deadline)
|
|
912
|
+
throw new Error("Receipt lookup deadline expired");
|
|
913
|
+
const raw = unwrap(response);
|
|
914
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
915
|
+
throw new Error("Invalid reconciliation response");
|
|
916
|
+
if (raw.status === "absent")
|
|
917
|
+
return null;
|
|
918
|
+
if (raw.status !== "committed")
|
|
919
|
+
throw new Error("Invalid reconciliation response");
|
|
920
|
+
return raw.receipt;
|
|
921
|
+
}
|
|
881
922
|
}
|
|
882
923
|
|
|
883
924
|
class IngestJournal {
|
|
@@ -1343,18 +1384,19 @@ class IngestJournalMaintenance {
|
|
|
1343
1384
|
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
1344
1385
|
await this.recover();
|
|
1345
1386
|
await verifyJournalAuthority(this.journal, this.cloud);
|
|
1346
|
-
const
|
|
1387
|
+
const sweepDeadline = Date.now() + 30000;
|
|
1347
1388
|
let retired = 0, bytes = 0;
|
|
1348
1389
|
let backend;
|
|
1349
1390
|
try {
|
|
1350
1391
|
const names = this.journal.names();
|
|
1351
1392
|
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
1352
|
-
if (retired >= limit || Date.now() >=
|
|
1393
|
+
if (retired >= limit || Date.now() >= sweepDeadline || retired > 0 && bytes >= 1048576)
|
|
1353
1394
|
break;
|
|
1354
1395
|
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
1355
1396
|
continue;
|
|
1356
1397
|
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
1357
|
-
|
|
1398
|
+
const deadline = Date.now() + 30000;
|
|
1399
|
+
backend = await this.backend(deadline);
|
|
1358
1400
|
const m = {
|
|
1359
1401
|
version: 1,
|
|
1360
1402
|
target: this.journal.target,
|
|
@@ -1390,7 +1432,8 @@ class IngestJournalMaintenance {
|
|
|
1390
1432
|
try {
|
|
1391
1433
|
await this.maintain();
|
|
1392
1434
|
} catch (error) {
|
|
1393
|
-
|
|
1435
|
+
const deferrable = error instanceof JournalMaintenanceError && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch", "recovery_authority_unavailable"].includes(error.reason);
|
|
1436
|
+
if (!deferrable || exists(this.control) || !this.journal.canStage(command))
|
|
1394
1437
|
throw error;
|
|
1395
1438
|
}
|
|
1396
1439
|
if (!this.journal.canStage(command))
|
|
@@ -3782,7 +3825,7 @@ class IngestBatchFailure extends Error {
|
|
|
3782
3825
|
}
|
|
3783
3826
|
|
|
3784
3827
|
// src/lib/cloud-ingest.ts
|
|
3785
|
-
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
3828
|
+
import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
|
|
3786
3829
|
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
3787
3830
|
function getIngestCachePath() {
|
|
3788
3831
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
@@ -3865,7 +3908,7 @@ async function postIngest(cloud, body) {
|
|
|
3865
3908
|
try {
|
|
3866
3909
|
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
3867
3910
|
} catch (error) {
|
|
3868
|
-
const refusal = error instanceof
|
|
3911
|
+
const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
3869
3912
|
if (refusal) {
|
|
3870
3913
|
journal.recordRefusal(batch, refusal);
|
|
3871
3914
|
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
@@ -3903,7 +3946,7 @@ async function postIngest(cloud, body) {
|
|
|
3903
3946
|
} catch (error) {
|
|
3904
3947
|
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
3905
3948
|
throw error;
|
|
3906
|
-
if (error instanceof
|
|
3949
|
+
if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
3907
3950
|
const body2 = error.body;
|
|
3908
3951
|
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
3909
3952
|
}
|
package/dist/otel/index.js
CHANGED
|
@@ -2696,6 +2696,7 @@ init_database();
|
|
|
2696
2696
|
import { constants, closeSync, fsyncSync, fstatSync, lstatSync, mkdirSync as mkdirSync3, openSync, readFileSync, opendirSync, realpathSync, writeFileSync } from "fs";
|
|
2697
2697
|
import { createHash as createHash4 } from "crypto";
|
|
2698
2698
|
import { join as join8, resolve as resolve2 } from "path";
|
|
2699
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
2699
2700
|
var MAX_COMMANDS = 1000;
|
|
2700
2701
|
var MAX_BYTES = 64 * 1024 * 1024;
|
|
2701
2702
|
var RECEIPT_RESERVE = 4096;
|
|
@@ -2807,15 +2808,55 @@ function sameReceipt(left, right) {
|
|
|
2807
2808
|
return stableJson(meaning(left)) === stableJson(meaning(right));
|
|
2808
2809
|
}
|
|
2809
2810
|
async function lookupJournalReceipt(cloud, key, bodyHash, timeoutMs = 1e4) {
|
|
2811
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
2812
|
+
throw new RangeError("Receipt lookup requires a positive finite timeout");
|
|
2810
2813
|
const query = new URLSearchParams({ idempotency_key: key, body_hash: bodyHash });
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2814
|
+
const deadline = performance.now() + timeoutMs;
|
|
2815
|
+
let failures = 0, lastError;
|
|
2816
|
+
for (;; ) {
|
|
2817
|
+
const remaining = Math.floor(deadline - performance.now());
|
|
2818
|
+
if (remaining <= 0)
|
|
2819
|
+
throw lastError ?? new Error("Receipt lookup deadline expired");
|
|
2820
|
+
let response;
|
|
2821
|
+
try {
|
|
2822
|
+
const abort = new AbortController;
|
|
2823
|
+
let timer;
|
|
2824
|
+
try {
|
|
2825
|
+
const expired = new Promise((_, reject) => {
|
|
2826
|
+
timer = setTimeout(() => {
|
|
2827
|
+
abort.abort();
|
|
2828
|
+
reject(new Error("Receipt lookup deadline expired"));
|
|
2829
|
+
}, remaining);
|
|
2830
|
+
});
|
|
2831
|
+
response = await Promise.race([
|
|
2832
|
+
cloud.client.transport.get(`/ingest/receipts?${query}`, { retry: false, timeoutMs: remaining, signal: abort.signal }),
|
|
2833
|
+
expired
|
|
2834
|
+
]);
|
|
2835
|
+
} finally {
|
|
2836
|
+
clearTimeout(timer);
|
|
2837
|
+
}
|
|
2838
|
+
} catch (error) {
|
|
2839
|
+
if (!(error instanceof HasnaHttpError) || error.status !== 503)
|
|
2840
|
+
throw error;
|
|
2841
|
+
lastError = error;
|
|
2842
|
+
failures++;
|
|
2843
|
+
const delay = 100 * failures;
|
|
2844
|
+
if (failures >= 3 || deadline - performance.now() <= delay)
|
|
2845
|
+
throw error;
|
|
2846
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
if (performance.now() >= deadline)
|
|
2850
|
+
throw new Error("Receipt lookup deadline expired");
|
|
2851
|
+
const raw = unwrap(response);
|
|
2852
|
+
if (!raw || raw.body_hash !== bodyHash)
|
|
2853
|
+
throw new Error("Invalid reconciliation response");
|
|
2854
|
+
if (raw.status === "absent")
|
|
2855
|
+
return null;
|
|
2856
|
+
if (raw.status !== "committed")
|
|
2857
|
+
throw new Error("Invalid reconciliation response");
|
|
2858
|
+
return raw.receipt;
|
|
2859
|
+
}
|
|
2819
2860
|
}
|
|
2820
2861
|
|
|
2821
2862
|
class IngestJournal {
|
|
@@ -3281,18 +3322,19 @@ class IngestJournalMaintenance {
|
|
|
3281
3322
|
throw new Error("Journal maintenance limit must be 1\u201332");
|
|
3282
3323
|
await this.recover();
|
|
3283
3324
|
await verifyJournalAuthority(this.journal, this.cloud);
|
|
3284
|
-
const
|
|
3325
|
+
const sweepDeadline = Date.now() + 30000;
|
|
3285
3326
|
let retired = 0, bytes = 0;
|
|
3286
3327
|
let backend;
|
|
3287
3328
|
try {
|
|
3288
3329
|
const names = this.journal.names();
|
|
3289
3330
|
for (const name of names.filter((n) => n.endsWith(".intent.json"))) {
|
|
3290
|
-
if (retired >= limit || Date.now() >=
|
|
3331
|
+
if (retired >= limit || Date.now() >= sweepDeadline || retired > 0 && bytes >= 1048576)
|
|
3291
3332
|
break;
|
|
3292
3333
|
if (!names.includes(name.replace(".intent.", ".receipt.")))
|
|
3293
3334
|
continue;
|
|
3294
3335
|
const intent = this.journal.intent(name), receipt = this.journal.receipt(intent.command);
|
|
3295
|
-
|
|
3336
|
+
const deadline = Date.now() + 30000;
|
|
3337
|
+
backend = await this.backend(deadline);
|
|
3296
3338
|
const m = {
|
|
3297
3339
|
version: 1,
|
|
3298
3340
|
target: this.journal.target,
|
|
@@ -3328,7 +3370,8 @@ class IngestJournalMaintenance {
|
|
|
3328
3370
|
try {
|
|
3329
3371
|
await this.maintain();
|
|
3330
3372
|
} catch (error) {
|
|
3331
|
-
|
|
3373
|
+
const deferrable = error instanceof JournalMaintenanceError && ["recovery_not_configured", "self_host_trash_authority_required", "trash_version_mismatch", "recovery_authority_unavailable"].includes(error.reason);
|
|
3374
|
+
if (!deferrable || exists(this.control) || !this.journal.canStage(command))
|
|
3332
3375
|
throw error;
|
|
3333
3376
|
}
|
|
3334
3377
|
if (!this.journal.canStage(command))
|
|
@@ -3375,7 +3418,7 @@ var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
|
|
|
3375
3418
|
]);
|
|
3376
3419
|
|
|
3377
3420
|
// src/lib/cloud-ingest.ts
|
|
3378
|
-
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
3421
|
+
import { HasnaHttpError as HasnaHttpError2 } from "@hasna/contracts/client";
|
|
3379
3422
|
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions", "scan_receipts"];
|
|
3380
3423
|
function exportIngestRows(db) {
|
|
3381
3424
|
const body = {};
|
|
@@ -3422,7 +3465,7 @@ async function postIngest(cloud, body) {
|
|
|
3422
3465
|
try {
|
|
3423
3466
|
response = await cloud.client.transport.post("/ingest", batch, { retry: false });
|
|
3424
3467
|
} catch (error) {
|
|
3425
|
-
const refusal = error instanceof
|
|
3468
|
+
const refusal = error instanceof HasnaHttpError2 ? definitiveIngestRefusal(error.status, error.body) : null;
|
|
3426
3469
|
if (refusal) {
|
|
3427
3470
|
journal.recordRefusal(batch, refusal);
|
|
3428
3471
|
throw new IngestRefused(refusal.status, refusal.code, refusal.reason);
|
|
@@ -3460,7 +3503,7 @@ async function postIngest(cloud, body) {
|
|
|
3460
3503
|
} catch (error) {
|
|
3461
3504
|
if (error instanceof PendingIngestError || error instanceof JournalMaintenanceError || error instanceof IngestRefused)
|
|
3462
3505
|
throw error;
|
|
3463
|
-
if (error instanceof
|
|
3506
|
+
if (error instanceof HasnaHttpError2 && error.status >= 400 && error.status < 500 && error.status !== 408) {
|
|
3464
3507
|
const body2 = error.body;
|
|
3465
3508
|
throw new IngestRefused(error.status, body2?.expected === "legacy_aggregate_requires_migration" ? "legacy_aggregate_requires_migration" : "ingest_refused");
|
|
3466
3509
|
}
|
|
@@ -3480,7 +3523,7 @@ async function pushIngestRows(cloud, scratch) {
|
|
|
3480
3523
|
// package.json
|
|
3481
3524
|
var package_default = {
|
|
3482
3525
|
name: "@hasna/economy",
|
|
3483
|
-
version: "0.5.
|
|
3526
|
+
version: "0.5.10",
|
|
3484
3527
|
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",
|
|
3485
3528
|
repository: {
|
|
3486
3529
|
type: "git",
|
package/dist/server/index.js
CHANGED
|
@@ -5578,7 +5578,7 @@ function isAuthorizedRequest(req, path) {
|
|
|
5578
5578
|
// package.json
|
|
5579
5579
|
var package_default = {
|
|
5580
5580
|
name: "@hasna/economy",
|
|
5581
|
-
version: "0.5.
|
|
5581
|
+
version: "0.5.10",
|
|
5582
5582
|
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",
|
|
5583
5583
|
repository: {
|
|
5584
5584
|
type: "git",
|
|
@@ -7454,7 +7454,7 @@ import { randomUUID } from "crypto";
|
|
|
7454
7454
|
// package.json
|
|
7455
7455
|
var package_default = {
|
|
7456
7456
|
name: "@hasna/economy",
|
|
7457
|
-
version: "0.5.
|
|
7457
|
+
version: "0.5.10",
|
|
7458
7458
|
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",
|
|
7459
7459
|
repository: {
|
|
7460
7460
|
type: "git",
|
package/docs/ingest-journal.md
CHANGED
|
@@ -35,7 +35,11 @@ closed. No operator input can relabel an uncertain intent as rejected.
|
|
|
35
35
|
|
|
36
36
|
A bounded sweep starts at 64 commands, 48 MiB, or insufficient next-command
|
|
37
37
|
headroom. It retires at most 32 pairs or approximately 1 MiB per pass; a single
|
|
38
|
-
large pair is handled individually.
|
|
38
|
+
large pair is handled individually. A sweep admits new transactions for up to
|
|
39
|
+
30 seconds; each admitted transaction has its own 30-second deadline for all
|
|
40
|
+
three captures. The last transaction may finish after admission closes, so this
|
|
41
|
+
phase can take up to 60 seconds. Prerequisite recovery and authority checks have
|
|
42
|
+
their own bounds.
|
|
39
43
|
The active journal remains bounded; Trash's recoverable remote retention and
|
|
40
44
|
unfinished transfer staging are governed by Trash's own policy.
|
|
41
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|