@blockrun/clawrouter 0.12.282 → 0.12.284

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.js CHANGED
@@ -32315,6 +32315,11 @@ function getSharedSpendControl() {
32315
32315
  host.__clawrouterSharedSpendControl ??= new SpendControl();
32316
32316
  return host.__clawrouterSharedSpendControl;
32317
32317
  }
32318
+ function parseQuotedAmountMicros(selected) {
32319
+ const raw = selected.amount ?? selected.maxAmountRequired;
32320
+ if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) return void 0;
32321
+ return BigInt(raw);
32322
+ }
32318
32323
  function parseQuotedAmountUsd(selected) {
32319
32324
  const raw = selected.amount ?? selected.maxAmountRequired;
32320
32325
  if (typeof raw !== "string" || !CANONICAL_AMOUNT.test(raw)) {
@@ -33080,6 +33085,16 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33080
33085
  return est === void 0 ? void 0 : Number(est);
33081
33086
  };
33082
33087
  const needMicros = estimateMicros();
33088
+ const notifyDeclinedPayment = () => {
33089
+ if (!options?.onPaymentDeclined) return;
33090
+ try {
33091
+ options.onPaymentDeclined();
33092
+ } catch (error) {
33093
+ console.error(
33094
+ `[ClawRouter] onPaymentDeclined callback failed: ${error instanceof Error ? error.message : String(error)}`
33095
+ );
33096
+ }
33097
+ };
33083
33098
  const cached = !options?.skipPreAuth ? cache2.get(cacheKey2) : void 0;
33084
33099
  const preAuthCovers = cached !== void 0 && Date.now() - cached.cachedAt < ttlMs && cached.coverMicros !== void 0 && needMicros !== void 0 && needMicros <= cached.coverMicros;
33085
33100
  if (preAuthCovers) {
@@ -33098,6 +33113,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33098
33113
  return response2;
33099
33114
  }
33100
33115
  paymentInFlight = false;
33116
+ notifyDeclinedPayment();
33101
33117
  cache2.delete(cacheKey2);
33102
33118
  } catch (err) {
33103
33119
  cache2.delete(cacheKey2);
@@ -33142,6 +33158,7 @@ function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, op
33142
33158
  clonedRequest.headers.set(key3, value);
33143
33159
  }
33144
33160
  const paidResponse = await baseFetch(clonedRequest);
33161
+ if (paidResponse.status === 402) notifyDeclinedPayment();
33145
33162
  notifyAcceptedPayment(paidResponse, payload);
33146
33163
  return paidResponse;
33147
33164
  };
@@ -64789,6 +64806,132 @@ var init_api_key = __esm({
64789
64806
  }
64790
64807
  });
64791
64808
 
64809
+ // src/session-spend.ts
64810
+ function parseMicros(raw) {
64811
+ if (raw === void 0 || !/^\d+$/.test(raw)) return void 0;
64812
+ return BigInt(raw);
64813
+ }
64814
+ function usdToMicros(usd2) {
64815
+ return BigInt(Math.round(usd2 * 1e6));
64816
+ }
64817
+ function microsToUsd(micros) {
64818
+ return Number(micros) / 1e6;
64819
+ }
64820
+ var SessionSpend, CostCapError;
64821
+ var init_session_spend = __esm({
64822
+ "src/session-spend.ts"() {
64823
+ "use strict";
64824
+ init_spend_control();
64825
+ SessionSpend = class _SessionSpend {
64826
+ constructor(store, sessionId, capMicros, holdMicros) {
64827
+ this.store = store;
64828
+ this.sessionId = sessionId;
64829
+ this.capMicros = capMicros;
64830
+ this.heldMicros = holdMicros;
64831
+ if (holdMicros > 0n) store.adjustSessionHold(sessionId, holdMicros);
64832
+ }
64833
+ store;
64834
+ sessionId;
64835
+ capMicros;
64836
+ /** Budget this request still holds against the session (strict mode only). */
64837
+ heldMicros;
64838
+ /** Sum of payments signed by this request that still count. */
64839
+ signedMicros = 0n;
64840
+ /** The most recent signature not yet settled or voided — the one a decline undoes. */
64841
+ lastSignedMicros = 0n;
64842
+ released = false;
64843
+ /**
64844
+ * Strict mode: admit the request only if the session's spend, plus what its
64845
+ * other in-flight requests hold, plus this request's estimate fits the cap —
64846
+ * and hold the estimate in the same step.
64847
+ */
64848
+ static admit(store, sessionId, capMicros, estimateMicros) {
64849
+ const spentMicros = store.getSessionCostMicros(sessionId);
64850
+ const heldMicros = store.getSessionHoldMicros(sessionId);
64851
+ if (spentMicros + heldMicros + estimateMicros > capMicros) {
64852
+ return { admitted: false, spentMicros, heldMicros, estimateMicros };
64853
+ }
64854
+ return {
64855
+ admitted: true,
64856
+ spend: new _SessionSpend(store, sessionId, capMicros, estimateMicros)
64857
+ };
64858
+ }
64859
+ /** Graceful mode: track signed spend, enforce nothing. */
64860
+ static track(store, sessionId) {
64861
+ return new _SessionSpend(store, sessionId, void 0, 0n);
64862
+ }
64863
+ get isStrict() {
64864
+ return this.capMicros !== void 0;
64865
+ }
64866
+ /**
64867
+ * Before a payment is created: may this request sign `quoteMicros`?
64868
+ * Spend already recorded (including this request's own earlier signatures)
64869
+ * plus what OTHER in-flight requests hold, plus the quote, must fit the cap.
64870
+ * Always true in graceful mode.
64871
+ */
64872
+ coverPayment(quoteMicros) {
64873
+ if (this.capMicros === void 0) return true;
64874
+ const spent = this.store.getSessionCostMicros(this.sessionId);
64875
+ const heldByOthers = this.store.getSessionHoldMicros(this.sessionId) - this.heldMicros;
64876
+ if (spent + heldByOthers + quoteMicros > this.capMicros) return false;
64877
+ if (quoteMicros > this.heldMicros) {
64878
+ this.store.adjustSessionHold(this.sessionId, quoteMicros - this.heldMicros);
64879
+ this.heldMicros = quoteMicros;
64880
+ }
64881
+ return true;
64882
+ }
64883
+ /** A payment was signed: it is spend now, and replaces part of the hold. */
64884
+ recordSigned(amountMicros) {
64885
+ if (amountMicros <= 0n) return;
64886
+ this.signedMicros += amountMicros;
64887
+ this.lastSignedMicros = amountMicros;
64888
+ this.store.addSessionCost(this.sessionId, amountMicros);
64889
+ this.shrinkHold(amountMicros);
64890
+ }
64891
+ /**
64892
+ * The gateway answered the payment just signed with a 402: nothing settled.
64893
+ * Payments within one request are signed and sent one at a time, so the
64894
+ * decline always belongs to the latest signature. Undone at most once.
64895
+ */
64896
+ voidLastSigned() {
64897
+ const undo = this.lastSignedMicros;
64898
+ this.lastSignedMicros = 0n;
64899
+ if (undo <= 0n) return;
64900
+ this.signedMicros -= undo;
64901
+ this.store.addSessionCost(this.sessionId, -undo);
64902
+ }
64903
+ /**
64904
+ * An attempt succeeded. When nothing was signed for this request (the API
64905
+ * key rail), the local estimate is the only record of the spend. When a
64906
+ * payment was signed, it is already recorded at its real amount.
64907
+ */
64908
+ recordSuccess(estimateMicros) {
64909
+ if (this.signedMicros > 0n || estimateMicros <= 0n) return;
64910
+ this.store.addSessionCost(this.sessionId, estimateMicros);
64911
+ this.shrinkHold(estimateMicros);
64912
+ }
64913
+ /** The request is over: return whatever it still holds. Idempotent. */
64914
+ release() {
64915
+ if (this.released) return;
64916
+ this.released = true;
64917
+ this.shrinkHold(this.heldMicros);
64918
+ }
64919
+ shrinkHold(byMicros) {
64920
+ const drop = byMicros < this.heldMicros ? byMicros : this.heldMicros;
64921
+ if (drop <= 0n) return;
64922
+ this.heldMicros -= drop;
64923
+ this.store.adjustSessionHold(this.sessionId, -drop);
64924
+ }
64925
+ };
64926
+ CostCapError = class extends SpendPolicyError {
64927
+ constructor(reason) {
64928
+ super(reason);
64929
+ this.name = "CostCapError";
64930
+ }
64931
+ };
64932
+ }
64933
+ });
64934
+
64792
64935
  // src/compression/types.ts
64793
64936
  var DEFAULT_COMPRESSION_CONFIG;
64794
64937
  var init_types2 = __esm({
@@ -65636,6 +65779,9 @@ var init_session = __esm({
65636
65779
  };
65637
65780
  SessionStore = class {
65638
65781
  sessions = /* @__PURE__ */ new Map();
65782
+ // Kept apart from `sessions`: a hold belongs to requests still running, so it
65783
+ // must not vanish when the idle-session sweep drops an entry mid-request.
65784
+ sessionHolds = /* @__PURE__ */ new Map();
65639
65785
  config;
65640
65786
  cleanupInterval = null;
65641
65787
  constructor(config = {}) {
@@ -65811,14 +65957,34 @@ var init_session = __esm({
65811
65957
  this.sessions.set(sessionId, entry);
65812
65958
  }
65813
65959
  entry.sessionCostMicros += additionalMicros;
65960
+ if (entry.sessionCostMicros < 0n) entry.sessionCostMicros = 0n;
65814
65961
  }
65815
65962
  /**
65816
65963
  * Get the total accumulated cost for a session in USD.
65817
65964
  */
65818
65965
  getSessionCostUsd(sessionId) {
65819
- const entry = this.sessions.get(sessionId);
65820
- if (!entry) return 0;
65821
- return Number(entry.sessionCostMicros) / 1e6;
65966
+ return Number(this.getSessionCostMicros(sessionId)) / 1e6;
65967
+ }
65968
+ /**
65969
+ * Get the total accumulated cost for a session in USDC micros.
65970
+ */
65971
+ getSessionCostMicros(sessionId) {
65972
+ return this.sessions.get(sessionId)?.sessionCostMicros ?? 0n;
65973
+ }
65974
+ /**
65975
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
65976
+ * against the strict cap but not yet spent. See session-spend.ts.
65977
+ */
65978
+ getSessionHoldMicros(sessionId) {
65979
+ return this.sessionHolds.get(sessionId) ?? 0n;
65980
+ }
65981
+ /**
65982
+ * Grow (positive) or release (negative) a session's in-flight hold.
65983
+ */
65984
+ adjustSessionHold(sessionId, deltaMicros) {
65985
+ const next = (this.sessionHolds.get(sessionId) ?? 0n) + deltaMicros;
65986
+ if (next > 0n) this.sessionHolds.set(sessionId, next);
65987
+ else this.sessionHolds.delete(sessionId);
65822
65988
  }
65823
65989
  /**
65824
65990
  * Stop the cleanup interval.
@@ -96521,6 +96687,19 @@ async function startProxy(options) {
96521
96687
  }
96522
96688
  const account = walletKey ? privateKeyToAccount(walletKey) : void 0;
96523
96689
  const x402 = authMode === "wallet" ? new x402Client() : void 0;
96690
+ x402?.onBeforePaymentCreation(async (context) => {
96691
+ const spend = paymentStore.getStore()?.sessionSpend;
96692
+ if (!spend?.isStrict) return;
96693
+ const quote = parseQuotedAmountMicros(context.selectedRequirements);
96694
+ if (quote === void 0) {
96695
+ throw new CostCapError("ClawRouter cost cap: unreadable payment amount in the 402 quote");
96696
+ }
96697
+ if (!spend.coverPayment(quote)) {
96698
+ throw new CostCapError(
96699
+ `ClawRouter cost cap exceeded: signing $${microsToUsd(quote).toFixed(6)} would push this session past its maxCostPerRun limit`
96700
+ );
96701
+ }
96702
+ });
96524
96703
  if (x402 && account) {
96525
96704
  const evmPublicClient = createPublicClient({ chain: base, transport: http() });
96526
96705
  const evmSigner = toClientEvmSigner(account, evmPublicClient);
@@ -96563,6 +96742,11 @@ async function startProxy(options) {
96563
96742
  if (store) store.amountUsd = amountUsd;
96564
96743
  console.log(`[ClawRouter] Payment signed on ${chain3} (${network}) \u2014 $${amountUsd.toFixed(6)}`);
96565
96744
  });
96745
+ x402?.onAfterPaymentCreation(async (context) => {
96746
+ const spend = paymentStore.getStore()?.sessionSpend;
96747
+ const amount = parseQuotedAmountMicros(context.selectedRequirements);
96748
+ if (spend && amount !== void 0) spend.recordSigned(amount);
96749
+ });
96566
96750
  const payFetch = authMode === "api-key" ? withSpendPolicy(
96567
96751
  createApiKeyFetch(apiKey, fetch, apiBase),
96568
96752
  options.spendControl ?? getSharedSpendControl(),
@@ -96581,7 +96765,10 @@ async function startProxy(options) {
96581
96765
  // Only the wallet rail settles per call. The API-key rail is billed
96582
96766
  // server-side against account credit, so there is no per-request
96583
96767
  // settlement for an observer to report.
96584
- onPayment: options.onPayment
96768
+ onPayment: options.onPayment,
96769
+ // A 402 answer to a signed payment means the gateway declined it and
96770
+ // nothing settled — take it back off the session's budget.
96771
+ onPaymentDeclined: () => paymentStore.getStore()?.sessionSpend?.voidLastSigned()
96585
96772
  });
96586
96773
  let balanceMonitor;
96587
96774
  if (options._balanceMonitorOverride) {
@@ -97865,6 +98052,17 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
97865
98052
  return { success: true, response };
97866
98053
  } catch (err) {
97867
98054
  const errorMsg = err instanceof Error ? err.message : String(err);
98055
+ if (err instanceof CostCapError) {
98056
+ return {
98057
+ success: false,
98058
+ errorBody: JSON.stringify({
98059
+ error: { message: errorMsg, type: "cost_cap_exceeded", code: "cost_cap_exceeded" }
98060
+ }),
98061
+ errorStatus: 429,
98062
+ isProviderError: false,
98063
+ errorCategory: "payment_error"
98064
+ };
98065
+ }
97868
98066
  if (err instanceof SpendPolicyError) {
97869
98067
  return {
97870
98068
  success: false,
@@ -98944,14 +99142,23 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98944
99142
  }
98945
99143
  }
98946
99144
  }
99145
+ let sessionSpend;
98947
99146
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
98948
- const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98949
99147
  const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
98950
- const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
98951
- const projectedCostUsd = runCostUsd + thisReqEstUsd;
98952
- if (projectedCostUsd > options.maxCostPerRunUsd) {
99148
+ const admission = SessionSpend.admit(
99149
+ sessionStore,
99150
+ effectiveSessionId,
99151
+ usdToMicros(options.maxCostPerRunUsd),
99152
+ parseMicros(thisReqEstStr) ?? 0n
99153
+ );
99154
+ if (!admission.admitted) {
99155
+ const spentUsd = microsToUsd(admission.spentMicros);
99156
+ const heldUsd = microsToUsd(admission.heldMicros);
99157
+ const estUsd = microsToUsd(admission.estimateMicros);
99158
+ const projectedCostUsd = spentUsd + heldUsd + estUsd;
99159
+ const breakdown = `spent $${spentUsd.toFixed(4)}${heldUsd > 0 ? ` + in flight $${heldUsd.toFixed(4)}` : ""} + est $${estUsd.toFixed(4)}`;
98953
99160
  console.log(
98954
- `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
99161
+ `[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (${breakdown}) > $${options.maxCostPerRunUsd} limit`
98955
99162
  );
98956
99163
  res.writeHead(429, {
98957
99164
  "Content-Type": "application/json",
@@ -98960,7 +99167,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98960
99167
  res.end(
98961
99168
  JSON.stringify({
98962
99169
  error: {
98963
- message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
99170
+ message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (${breakdown}) would exceed limit $${options.maxCostPerRunUsd}`,
98964
99171
  type: "cost_cap_exceeded",
98965
99172
  code: "cost_cap_exceeded"
98966
99173
  }
@@ -98969,7 +99176,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
98969
99176
  deduplicator.removeInflight(dedupKey);
98970
99177
  return;
98971
99178
  }
99179
+ sessionSpend = admission.spend;
99180
+ } else if (options.maxCostPerRunUsd && effectiveSessionId) {
99181
+ sessionSpend = SessionSpend.track(sessionStore, effectiveSessionId);
98972
99182
  }
99183
+ const requestStore = paymentStore.getStore();
99184
+ if (requestStore) requestStore.sessionSpend = sessionSpend;
98973
99185
  if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
98974
99186
  const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
98975
99187
  const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
@@ -99365,11 +99577,9 @@ data: [DONE]
99365
99577
  actualModelUsed = tryModel;
99366
99578
  recordOutcome(tryModel, requestKind, true);
99367
99579
  console.log(`[ClawRouter] Success with model: ${tryModel}`);
99368
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99369
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99370
- if (costEst) {
99371
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99372
- }
99580
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99581
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99582
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99373
99583
  }
99374
99584
  break;
99375
99585
  }
@@ -99443,11 +99653,9 @@ data: [DONE]
99443
99653
  upstream = retryResult.response;
99444
99654
  actualModelUsed = tryModel;
99445
99655
  console.log(`[ClawRouter] Explicit-pin retry succeeded for: ${tryModel}`);
99446
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99447
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99448
- if (costEst) {
99449
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99450
- }
99656
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99657
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99658
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99451
99659
  }
99452
99660
  break;
99453
99661
  }
@@ -99514,11 +99722,9 @@ data: [DONE]
99514
99722
  upstream = retryResult.response;
99515
99723
  actualModelUsed = tryModel;
99516
99724
  console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
99517
- if (options.maxCostPerRunUsd && effectiveSessionId && !FREE_MODELS.has(tryModel)) {
99518
- const costEst = estimateAmount(tryModel, body.length, maxTokens);
99519
- if (costEst) {
99520
- sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
99521
- }
99725
+ if (sessionSpend && !FREE_MODELS.has(tryModel)) {
99726
+ const costEst = parseMicros(estimateAmount(tryModel, body.length, maxTokens));
99727
+ if (costEst !== void 0) sessionSpend.recordSuccess(costEst);
99522
99728
  }
99523
99729
  break;
99524
99730
  }
@@ -99612,6 +99818,7 @@ data: [DONE]
99612
99818
  );
99613
99819
  const rawErrBody = lastError?.body || structuredMessage;
99614
99820
  const errStatus = lastError?.status || 502;
99821
+ const costCapRefusal = errStatus === 429 && rawErrBody.includes('"cost_cap_exceeded"');
99615
99822
  const transformedErr = transformPaymentError(rawErrBody);
99616
99823
  requestHadError = true;
99617
99824
  if (headersSentEarly) {
@@ -99648,7 +99855,8 @@ data: [DONE]
99648
99855
  "Content-Type": "application/json",
99649
99856
  "x-context-used-kb": String(originalContextSizeKB),
99650
99857
  "x-context-limit-kb": String(CONTEXT_LIMIT_KB),
99651
- ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {}
99858
+ ...lastError?.requestId ? { "x-blockrun-request-id": lastError.requestId } : {},
99859
+ ...costCapRefusal ? { "X-ClawRouter-Cost-Cap-Exceeded": "1" } : {}
99652
99860
  });
99653
99861
  res.end(transformedErr);
99654
99862
  deduplicator.complete(dedupKey, {
@@ -100082,6 +100290,8 @@ data: [DONE]
100082
100290
  throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
100083
100291
  }
100084
100292
  throw err;
100293
+ } finally {
100294
+ sessionSpend?.release();
100085
100295
  }
100086
100296
  const logModel = routingDecision?.model ?? modelId;
100087
100297
  if (logModel) {
@@ -100168,6 +100378,7 @@ var init_proxy = __esm({
100168
100378
  init_auth();
100169
100379
  init_api_key();
100170
100380
  init_spend_control();
100381
+ init_session_spend();
100171
100382
  init_compression();
100172
100383
  init_errors5();
100173
100384
  init_version4();
@@ -101641,7 +101852,8 @@ __export(reconcile_exports, {
101641
101852
  joinRows: () => joinRows,
101642
101853
  loadGatewayRows: () => loadGatewayRows,
101643
101854
  loadLocalRows: () => loadLocalRows,
101644
- reconcile: () => reconcile
101855
+ reconcile: () => reconcile,
101856
+ reconcileExitCode: () => reconcileExitCode
101645
101857
  });
101646
101858
  import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
101647
101859
  import { join as join13 } from "path";
@@ -101688,16 +101900,36 @@ async function loadLocalRows(since) {
101688
101900
  async function loadGatewayRows(apiKey, from15, maxPages = 40) {
101689
101901
  const rows = [];
101690
101902
  const unavailableDays = /* @__PURE__ */ new Set();
101903
+ const requested = /* @__PURE__ */ new Set();
101691
101904
  let cursor;
101692
101905
  for (let page = 0; page < maxPages; page++) {
101693
101906
  const result = await fetchUsagePage(apiKey, { from: from15, limit: 500, cursor });
101694
- if (!result) return page === 0 ? void 0 : { rows, unavailableDays: [...unavailableDays] };
101907
+ if (!result) {
101908
+ if (page === 0) return void 0;
101909
+ return {
101910
+ rows,
101911
+ unavailableDays: [...unavailableDays],
101912
+ incomplete: `page ${page + 1} of the ledger could not be read`
101913
+ };
101914
+ }
101695
101915
  rows.push(...result.rows);
101696
101916
  result.unavailableDays.forEach((d) => unavailableDays.add(d));
101697
- if (!result.nextCursor) break;
101917
+ if (!result.nextCursor) return { rows, unavailableDays: [...unavailableDays] };
101918
+ if (requested.has(result.nextCursor)) {
101919
+ return {
101920
+ rows,
101921
+ unavailableDays: [...unavailableDays],
101922
+ incomplete: `the ledger's page cursor stopped advancing after page ${page + 1}`
101923
+ };
101924
+ }
101698
101925
  cursor = result.nextCursor;
101926
+ requested.add(cursor);
101699
101927
  }
101700
- return { rows, unavailableDays: [...unavailableDays] };
101928
+ return {
101929
+ rows,
101930
+ unavailableDays: [...unavailableDays],
101931
+ incomplete: `the ledger has more than ${maxPages} pages for this window; try fewer --days`
101932
+ };
101701
101933
  }
101702
101934
  function joinRows(localRows, gatewayRows, unkeyed = 0, unavailableDays = []) {
101703
101935
  const localById = new Map(localRows.map((r2) => [r2.requestId, r2]));
@@ -101740,7 +101972,14 @@ async function reconcile(apiKey, days) {
101740
101972
  loadGatewayRows(apiKey, since.toISOString())
101741
101973
  ]);
101742
101974
  if (!gateway) throw new Error("Could not read the BlockRun usage ledger (GET /v1/usage).");
101743
- return joinRows(localRows, gateway.rows, unkeyed, gateway.unavailableDays);
101975
+ const result = joinRows(localRows, gateway.rows, unkeyed, gateway.unavailableDays);
101976
+ if (gateway.incomplete) result.ledgerIncomplete = gateway.incomplete;
101977
+ return result;
101978
+ }
101979
+ function reconcileExitCode(r2) {
101980
+ if (r2.chargedNotRecorded.length > 0) return 2;
101981
+ if (r2.ledgerIncomplete) return 1;
101982
+ return 0;
101744
101983
  }
101745
101984
  function formatReconcile(r2, days) {
101746
101985
  const out = [];
@@ -101748,6 +101987,13 @@ function formatReconcile(r2, days) {
101748
101987
  out.push(`
101749
101988
  BlockRun reconciliation \u2014 last ${days} day${days === 1 ? "" : "s"}
101750
101989
  `);
101990
+ if (r2.ledgerIncomplete) {
101991
+ out.push(` \u26A0 Partial ledger: ${r2.ledgerIncomplete}.`);
101992
+ out.push(` Only the ledger pages that were read are compared: the gateway total is`);
101993
+ out.push(` short, charges on the unread pages are not checked, and calls billed on`);
101994
+ out.push(` those pages show up as recorded locally with no settled ledger row.
101995
+ `);
101996
+ }
101751
101997
  out.push(
101752
101998
  ` Gateway charged: ${usd(r2.gatewayTotalUsd)} (${r2.matched.length + r2.chargedNotRecorded.length} settled calls)`
101753
101999
  );
@@ -123261,7 +123507,7 @@ var init_client5 = __esm({
123261
123507
  function resolveSpendControl(deps) {
123262
123508
  return deps?.spendControl ?? getSharedSpendControl();
123263
123509
  }
123264
- function usdToMicros(usd2) {
123510
+ function usdToMicros2(usd2) {
123265
123511
  return String(Math.ceil(usd2 * 1e6));
123266
123512
  }
123267
123513
  async function signUnderSpendPolicy(deps, quote, action, sign4) {
@@ -123527,7 +123773,7 @@ Re-call with confirm:true to sign and submit.`,
123527
123773
  payTo: negRisk ? NEG_RISK_CTF_EXCHANGE_V2 : CTF_EXCHANGE_V2,
123528
123774
  network: `eip155:${POLYGON_CHAIN_ID}`,
123529
123775
  asset: PUSD_COLLATERAL,
123530
- amount: usdToMicros(notional)
123776
+ amount: usdToMicros2(notional)
123531
123777
  };
123532
123778
  let r2;
123533
123779
  try {
@@ -235914,10 +236160,10 @@ async function cmdReconcile(days) {
235914
236160
  process.exit(1);
235915
236161
  }
235916
236162
  try {
235917
- const { reconcile: reconcile2, formatReconcile: formatReconcile2 } = await Promise.resolve().then(() => (init_reconcile(), reconcile_exports));
236163
+ const { reconcile: reconcile2, formatReconcile: formatReconcile2, reconcileExitCode: reconcileExitCode2 } = await Promise.resolve().then(() => (init_reconcile(), reconcile_exports));
235918
236164
  const result = await reconcile2(resolved.key, days);
235919
236165
  console.log(formatReconcile2(result, days));
235920
- process.exitCode = result.chargedNotRecorded.length > 0 ? 2 : 0;
236166
+ process.exitCode = reconcileExitCode2(result);
235921
236167
  } catch (error) {
235922
236168
  console.error(`\u2717 ${error instanceof Error ? error.message : String(error)}`);
235923
236169
  process.exit(1);
package/dist/index.d.ts CHANGED
@@ -966,6 +966,7 @@ declare const DEFAULT_SESSION_CONFIG: SessionConfig;
966
966
  */
967
967
  declare class SessionStore {
968
968
  private sessions;
969
+ private sessionHolds;
969
970
  private config;
970
971
  private cleanupInterval;
971
972
  constructor(config?: Partial<SessionConfig>);
@@ -1036,6 +1037,19 @@ declare class SessionStore {
1036
1037
  * Get the total accumulated cost for a session in USD.
1037
1038
  */
1038
1039
  getSessionCostUsd(sessionId: string): number;
1040
+ /**
1041
+ * Get the total accumulated cost for a session in USDC micros.
1042
+ */
1043
+ getSessionCostMicros(sessionId: string): bigint;
1044
+ /**
1045
+ * Budget held by a session's in-flight requests (USDC micros) — admitted
1046
+ * against the strict cap but not yet spent. See session-spend.ts.
1047
+ */
1048
+ getSessionHoldMicros(sessionId: string): bigint;
1049
+ /**
1050
+ * Grow (positive) or release (negative) a session's in-flight hold.
1051
+ */
1052
+ adjustSessionHold(sessionId: string, deltaMicros: bigint): void;
1039
1053
  /**
1040
1054
  * Stop the cleanup interval.
1041
1055
  */