@dvmkit/sdk 0.1.5-rc.8 → 0.2.0-rc.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-BIP6G74V.js → chunk-2ATUAUAO.js} +8 -8
  3. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  4. package/dist/{chunk-EVBK675R.js → chunk-6GRIKOFB.js} +28 -23
  5. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-FT6HTUM4.js} +1 -1
  7. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  8. package/dist/{chunk-U6M3ATSG.js → chunk-JDT5LCJC.js} +40 -6
  9. package/dist/{chunk-FROTD5XQ.js → chunk-JLXYOV4Y.js} +1 -2
  10. package/dist/{chunk-M7LHFJ5K.js → chunk-KMZXTBLA.js} +2 -2
  11. package/dist/{chunk-6BQM7TOW.js → chunk-L67WTZX2.js} +3 -7
  12. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  13. package/dist/{chunk-JZWELPFH.js → chunk-MRAGS5VP.js} +1 -1
  14. package/dist/{chunk-2UUXIIOC.js → chunk-O2X2CCKH.js} +3 -3
  15. package/dist/{chunk-TQWGQCNV.js → chunk-OMIQMMME.js} +3 -3
  16. package/dist/{chunk-UB5FZ43T.js → chunk-PCUQZDZA.js} +475 -366
  17. package/dist/{chunk-KVEHHC7W.js → chunk-PHHAYRQV.js} +7 -9
  18. package/dist/{chunk-SSSZUVWM.js → chunk-QP53RWAD.js} +88 -38
  19. package/dist/{chunk-DMNLFNTW.js → chunk-QT4ONTST.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-MLRCSJYX.js → chunk-V7EVFLAK.js} +87 -90
  22. package/dist/{chunk-E4EVGPDX.js → chunk-XQXJKJ3P.js} +0 -2
  23. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-5ZEJRI46.js} +1 -1
  24. package/dist/{credit-menu-s5HmGCqx.d.ts → credit-menu-D4Gcdgc4.d.ts} +488 -644
  25. package/dist/{fx-D860pZvP.d.ts → fx-B0SLBe5x.d.ts} +38 -82
  26. package/dist/index.d.ts +11 -14
  27. package/dist/index.js +2 -2
  28. package/dist/internal/caller.d.ts +618 -1525
  29. package/dist/internal/caller.js +28 -60
  30. package/dist/internal/server.d.ts +36 -61
  31. package/dist/internal/server.js +11 -11
  32. package/dist/{job-store-BUGqvCfL.d.ts → job-store-B2uZvga4.d.ts} +70 -59
  33. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-CQBnQgsT.d.ts} +19 -27
  34. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-ZOH6C3N4.js} +2 -2
  35. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-IOJBF7DB.js} +1 -1
  36. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-5PIRYFVQ.js} +1 -1
  37. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-ISRHBMOU.js} +1 -1
  38. package/dist/{postgres-job-store-3RAXMNSY.js → postgres-job-store-OGQ6IT4U.js} +1 -1
  39. package/dist/{postgres-kv-store-JFBDP5IP.js → postgres-kv-store-D5E2EZ24.js} +1 -1
  40. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-IZFLTTAC.js} +1 -1
  41. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  42. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-FIDI3RNH.js} +1 -1
  43. package/dist/{revenue-reporter-ASZ7SHHH.js → revenue-reporter-NNCNRY4C.js} +1 -1
  44. package/dist/server/index.d.ts +53 -59
  45. package/dist/server/index.js +38 -37
  46. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  47. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-CXg7ziML.d.ts} +389 -551
  48. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-76TDAF34.js} +1 -1
  49. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-DXM7QXJQ.js} +3 -3
  50. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-O67H5M4N.js} +2 -2
  51. package/dist/testing/index.d.ts +5 -15
  52. package/dist/testing/index.js +4 -11
  53. package/dist/{usd-DoRuAckA.d.ts → usd-BNDg1715.d.ts} +14 -16
  54. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  55. package/dist/{x402-5H27DCBE.js → x402-7S2EFINY.js} +2 -2
  56. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryProcessedPaymentStore,
3
3
  ProcessedPaymentReplayError
4
- } from "./chunk-DMNLFNTW.js";
4
+ } from "./chunk-QT4ONTST.js";
5
5
  import {
6
6
  createX402FacilitatorAuthHeaders,
7
7
  createX402FacilitatorClient,
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-BQ2NMWKE.js";
11
11
  import {
12
12
  MemoryCreditLedger
13
- } from "./chunk-KVEHHC7W.js";
13
+ } from "./chunk-PHHAYRQV.js";
14
14
  import {
15
15
  SIGNED_ENVELOPE_FIELDS,
16
16
  SignedRequestError,
@@ -35,7 +35,7 @@ import {
35
35
  localMessage,
36
36
  providerMessage,
37
37
  toJobRecord
38
- } from "./chunk-U6M3ATSG.js";
38
+ } from "./chunk-JDT5LCJC.js";
39
39
  import {
40
40
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
41
41
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -100,7 +100,7 @@ import {
100
100
  pinDimension,
101
101
  withSpan,
102
102
  withTraceContext
103
- } from "./chunk-E4EVGPDX.js";
103
+ } from "./chunk-XQXJKJ3P.js";
104
104
  import {
105
105
  FxRateUnavailableError,
106
106
  InvalidCurrencyError,
@@ -109,7 +109,7 @@ import {
109
109
  fxRateFor,
110
110
  resolveFxSourceFromEnv,
111
111
  validateCurrency
112
- } from "./chunk-27V2ILSR.js";
112
+ } from "./chunk-4A2RAKCW.js";
113
113
  import {
114
114
  fiatToSatsCeil,
115
115
  parseUsdPrice,
@@ -130,7 +130,7 @@ import {
130
130
  isNonChannelBitcoinRail,
131
131
  netOwedSats,
132
132
  x402SettlementPending
133
- } from "./chunk-MLRCSJYX.js";
133
+ } from "./chunk-V7EVFLAK.js";
134
134
  import {
135
135
  withSdkInitLock
136
136
  } from "./chunk-S3XAHZQY.js";
@@ -141,7 +141,7 @@ import {
141
141
  advertisedMethods,
142
142
  challengeMeta,
143
143
  isTempoInsufficientFundsError
144
- } from "./chunk-M7LHFJ5K.js";
144
+ } from "./chunk-KMZXTBLA.js";
145
145
 
146
146
  // src/sdk/server/lock-pubkey-store.ts
147
147
  async function initLockPubkeyTable(db) {
@@ -883,7 +883,7 @@ async function buildCreditMenu(args) {
883
883
  funding,
884
884
  // Rendered when either half has something to say: a DVM configured for
885
885
  // sessions only would otherwise lose the whole block the moment the gate
886
- // closed, taking its capability signal with it (internal-review).
886
+ // closed, taking its capability signal with it.
887
887
  ...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
888
888
  tempo: {
889
889
  methods: tempoMethods,
@@ -896,7 +896,7 @@ async function buildCreditMenu(args) {
896
896
  // are the same misadvertisement in the other direction.
897
897
  ...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
898
898
  };
899
- if (args.funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
899
+ if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
900
900
  try {
901
901
  const fx = await args.fxFetcher.fetch();
902
902
  menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
@@ -905,7 +905,7 @@ async function buildCreditMenu(args) {
905
905
  "credit_menu_lightning_floor_unavailable",
906
906
  `credit menu degraded: could not express the Lightning funding floor in ${currency}, so the lightning rail is omitted from the funding menu until a rate is available.`
907
907
  );
908
- menu.funding = args.funding.filter((rail) => rail !== "lightning");
908
+ menu.funding = menu.funding.filter((rail) => rail !== "lightning");
909
909
  }
910
910
  }
911
911
  const echo = await selectPrimaryCredit({
@@ -1950,7 +1950,7 @@ async function createX402BatchSettlementServer(opts) {
1950
1950
  ...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
1951
1951
  // Every claim this fleet builds is capped at earned draws — the claim
1952
1952
  // timer, `claimAndSettle()`, and the withdraw-race rush claim all route
1953
- // through this selector (internal-review). A builder-supplied
1953
+ // through this selector. A builder-supplied
1954
1954
  // `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
1955
1955
  // a money invariant, not a policy knob. Upstream's refund job builds its
1956
1956
  // bundled claim from storage instead and never consults a selector, which
@@ -2071,14 +2071,13 @@ async function createX402BatchSettlementServer(opts) {
2071
2071
  // move money. The voucher base does not: a cooperatively drained and
2072
2072
  // reused channel can present the same cumulative maximum again under a
2073
2073
  // fresh authorization, which made the second legitimate deposit
2074
- // collide with the first row (internal-review). Conversely, keying by the
2074
+ // collide with the first row. Conversely, keying by the
2075
2075
  // credit effect would let one signed authorization open several rows.
2076
2076
  //
2077
2077
  // A cooperative refund belongs to one caller drain: after an operator
2078
2078
  // writes a failed drain off, the unchanged channel can legitimately
2079
2079
  // present the same voucher base under a fresh drain id. Keep that new
2080
2080
  // effect out of the old row's `(operation, payment_id)` replay slot
2081
- // (internal-review).
2082
2081
  paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
2083
2082
  amount: matched.amount
2084
2083
  };
@@ -3049,6 +3048,26 @@ function msatsToMethodAmount(msats, methodName, btcUsd) {
3049
3048
  const usd = msatsToUsd(msats, btcUsd);
3050
3049
  return usd.toFixed(decimalsFor(methodName));
3051
3050
  }
3051
+ function fiatMicroToMethodAmount(amountMicro, methodName) {
3052
+ if (!Number.isSafeInteger(amountMicro) || amountMicro < 0) {
3053
+ throw new Error("mpp-units: fiat amount must be a non-negative safe integer");
3054
+ }
3055
+ const decimals = decimalsFor(methodName);
3056
+ const microDecimals = 6;
3057
+ let atomic = BigInt(amountMicro);
3058
+ if (decimals < microDecimals) {
3059
+ const divisor = 10n ** BigInt(microDecimals - decimals);
3060
+ if (atomic % divisor !== 0n) {
3061
+ throw new Error(
3062
+ `mpp-units: fiat microunits cannot be represented exactly with ${decimals} decimals`
3063
+ );
3064
+ }
3065
+ atomic /= divisor;
3066
+ } else if (decimals > microDecimals) {
3067
+ atomic *= 10n ** BigInt(decimals - microDecimals);
3068
+ }
3069
+ return methodNativeAmountToDecimal(atomic, methodName);
3070
+ }
3052
3071
  function methodNativeAsset(methodName) {
3053
3072
  switch (methodName) {
3054
3073
  case "tempo":
@@ -3287,7 +3306,7 @@ var PAYMENT_PROOF_KEYS = ["cashu_token", "x402_payment", "tempo_credential"];
3287
3306
  function hasPaymentProof(content) {
3288
3307
  return PAYMENT_PROOF_KEYS.some((key) => Boolean(content[key]));
3289
3308
  }
3290
- async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRate, sessionIssuable = true, allowOneShotStablecoin = false) {
3309
+ async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRate, sessionIssuable = true, allowOneShotStablecoin = false, priceFiat) {
3291
3310
  return loggers.tempo.span(
3292
3311
  "tempo.issue_challenges",
3293
3312
  {
@@ -3305,7 +3324,7 @@ async function issueUpfrontChallenges(mpp, requiredMsats, resourcePath, btcUsdRa
3305
3324
  if (creditSurface && meta.intent === "charge" && !stablecoinCreditInstrumentAllowed(allowOneShotStablecoin, "tempo_charge")) {
3306
3325
  continue;
3307
3326
  }
3308
- const amount = msatsToMethodAmount(requiredMsats, meta.name, btcUsdRate);
3327
+ const amount = priceFiat?.currency === "usd" ? fiatMicroToMethodAmount(priceFiat.amountMicro, meta.name) : msatsToMethodAmount(requiredMsats, meta.name, btcUsdRate);
3309
3328
  const ch = await mpp.issueChallenge(meta.name, meta.intent, {
3310
3329
  amount,
3311
3330
  ...meta.intent === "session" && { suggestedDeposit: amount, unitType: "request" },
@@ -3611,7 +3630,7 @@ async function verifyUpfrontPayment(opts) {
3611
3630
  // Nothing settled, so the EIP-3009 nonce is unspent and the same
3612
3631
  // header is still payable — unlike the post-settlement branch
3613
3632
  // below, where saying this would be telling the caller to pay
3614
- // twice (internal-review).
3633
+ // twice.
3615
3634
  hint: `Nothing was settled \u2014 re-sign this request with fund.amount_micro set to expected_micro (${predicted}), re-attach this same X-PAYMENT header, and resubmit.`
3616
3635
  }
3617
3636
  );
@@ -3632,7 +3651,7 @@ async function verifyUpfrontPayment(opts) {
3632
3651
  x402Requirements
3633
3652
  });
3634
3653
  }
3635
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
3654
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
3636
3655
  const receipt = await verifyX402Payment(
3637
3656
  x402Payment,
3638
3657
  x402Config,
@@ -3669,7 +3688,7 @@ async function verifyUpfrontPayment(opts) {
3669
3688
  `fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
3670
3689
  // `expected_micro` states the valuation, but carries no corrective
3671
3690
  // hint here: the facilitator already settled, so re-presenting
3672
- // this credential would be a second payment (internal-review).
3691
+ // this credential would be a second payment.
3673
3692
  { extra: { expected_micro: fundedMicro } }
3674
3693
  );
3675
3694
  }
@@ -3783,13 +3802,14 @@ async function verifyUpfrontPayment(opts) {
3783
3802
  resourcePath ?? "/v1/job",
3784
3803
  rate,
3785
3804
  sessionIssuable,
3786
- opts.fundOnly?.allowOneShotStablecoin
3805
+ opts.fundOnly?.allowOneShotStablecoin,
3806
+ opts.priceFiat
3787
3807
  )
3788
3808
  }
3789
3809
  };
3790
3810
  }
3791
3811
  }
3792
- const expectedAmount = msatsToMethodAmount(requiredMsats, cred.challenge.method, rate);
3812
+ const expectedAmount = opts.priceFiat?.currency === "usd" ? fiatMicroToMethodAmount(opts.priceFiat.amountMicro, cred.challenge.method) : msatsToMethodAmount(requiredMsats, cred.challenge.method, rate);
3793
3813
  const sessionBasis = mppSessionBasis(cred);
3794
3814
  let tempoReceipt;
3795
3815
  const broadcast = async () => {
@@ -3862,7 +3882,8 @@ async function verifyUpfrontPayment(opts) {
3862
3882
  resourcePath ?? "/v1/job",
3863
3883
  rate,
3864
3884
  sessionIssuable,
3865
- opts.fundOnly?.allowOneShotStablecoin
3885
+ opts.fundOnly?.allowOneShotStablecoin,
3886
+ opts.priceFiat
3866
3887
  );
3867
3888
  return {
3868
3889
  paidMsats: 0,
@@ -3886,7 +3907,8 @@ async function verifyUpfrontPayment(opts) {
3886
3907
  resourcePath ?? "/v1/job",
3887
3908
  rate,
3888
3909
  sessionIssuable,
3889
- opts.fundOnly?.allowOneShotStablecoin
3910
+ opts.fundOnly?.allowOneShotStablecoin,
3911
+ opts.priceFiat
3890
3912
  );
3891
3913
  return {
3892
3914
  paidMsats: 0,
@@ -3952,7 +3974,8 @@ async function verifyUpfrontPayment(opts) {
3952
3974
  resourcePath ?? "/v1/job",
3953
3975
  rate,
3954
3976
  sessionIssuable,
3955
- opts.fundOnly?.allowOneShotStablecoin
3977
+ opts.fundOnly?.allowOneShotStablecoin,
3978
+ opts.priceFiat
3956
3979
  );
3957
3980
  return {
3958
3981
  paidMsats: 0,
@@ -4015,7 +4038,8 @@ async function verifyUpfrontPayment(opts) {
4015
4038
  resourcePath ?? "/v1/job",
4016
4039
  rate,
4017
4040
  sessionIssuable,
4018
- opts.fundOnly?.allowOneShotStablecoin
4041
+ opts.fundOnly?.allowOneShotStablecoin,
4042
+ opts.priceFiat
4019
4043
  ) : void 0;
4020
4044
  return {
4021
4045
  paidMsats: 0,
@@ -4170,16 +4194,16 @@ var PAYMENT_ERROR_COPY = {
4170
4194
  // Only the Cashu accumulator emits this code, and it evaluates each call's
4171
4195
  // own proofs — nothing sums a partial with a later one, on any path. So the
4172
4196
  // retry owes the full amount: implicit upfront the shortfall was forfeited
4173
- // (internal-review), mid-job it sits on a credit the per-site hint names
4174
- // (internal-review). Explicit upfront short deposits use `insufficient_credit`
4175
- // instead (internal-review). Telling a caller to "send the difference" produced a
4197
+ // mid-job it sits on a credit the per-site hint names
4198
+ // Explicit upfront short deposits use `insufficient_credit`
4199
+ // instead. Telling a caller to "send the difference" produced a
4176
4200
  // second short pay either way.
4177
4201
  payment_insufficient: {
4178
4202
  display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
4179
4203
  hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
4180
4204
  retryable: true
4181
4205
  },
4182
- // internal-review: the rail and the ledger leg commit together, but the job row is
4206
+ // the rail and the ledger leg commit together, but the job row is
4183
4207
  // written by a *second* transaction (`verifyAndCredit`). When that one can't
4184
4208
  // be made to land, the money is real and the caller's — it just isn't on the
4185
4209
  // job. Say so rather than answering a bare 500: the per-site body names the
@@ -4226,7 +4250,7 @@ var PAYMENT_ERROR_COPY = {
4226
4250
  hint: "Use a different prepaid balance or pay per call. To reclaim this balance after running jobs finish, inspect it with 'dvm wallet tempo-exit <dvm>'.",
4227
4251
  retryable: false
4228
4252
  },
4229
- // internal-review: a refund on this credit's payment channel reached the chain and
4253
+ // a refund on this credit's payment channel reached the chain and
4230
4254
  // never finished booking, so the escrow behind the balance may already have
4231
4255
  // been paid back. Spending it would buy work the service can never collect
4232
4256
  // for. The copy must not promise the money is gone *or* that it is safe to
@@ -4258,7 +4282,7 @@ var PAYMENT_ERROR_COPY = {
4258
4282
  hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
4259
4283
  retryable: true
4260
4284
  },
4261
- // internal-review: a per-rail floor, not the menu-wide minimum — today only the
4285
+ // a per-rail floor, not the menu-wide minimum — today only the
4262
4286
  // lightning rail carries one (the receive channel's routing floor). Refused
4263
4287
  // before any invoice is minted, so nothing is ever paid against this code;
4264
4288
  // `retryable: false` because re-sending the identical request can never
@@ -4288,7 +4312,7 @@ var PAYMENT_ERROR_COPY = {
4288
4312
  hint: "The balance is zero or fully held by in-flight jobs. Wait for running jobs to finish, then request the drain again with a fresh drain_id.",
4289
4313
  retryable: true
4290
4314
  },
4291
- // internal-review. Distinct from `nothing_to_drain` because the balance is real and
4315
+ // Distinct from `nothing_to_drain` because the balance is real and
4292
4316
  // waiting changes nothing: a reusable Bitcoin balance is a deposit the
4293
4317
  // service owes back in the asset it received, and what is left here converts
4294
4318
  // to less than one satoshi at the rate its own funding landed at. There is
@@ -4320,7 +4344,7 @@ var PAYMENT_ERROR_COPY = {
4320
4344
  hint: "Run 'dvm wallet tempo-exit <dvm>' to inspect the channel, request its on-chain exit if needed, and withdraw after the 15-minute grace period.",
4321
4345
  retryable: false
4322
4346
  },
4323
- // internal-review. The voucher verified and the ledger leg rolled back with the
4347
+ // The voucher verified and the ledger leg rolled back with the
4324
4348
  // failed submission, so nothing was charged and nothing about the payment is
4325
4349
  // wrong — which is exactly what the `payment_invalid` this replaced got
4326
4350
  // backwards, telling an agent to discard a credential that is still good. The
@@ -4397,7 +4421,7 @@ var PAYMENT_ERROR_COPY = {
4397
4421
  hint: "The token's proofs are pending at the mint; retry in a few seconds.",
4398
4422
  retryable: true
4399
4423
  },
4400
- // internal-review. Not failures — the two `lightning_*` states below are the normal
4424
+ // Not failures — the two `lightning_*` states below are the normal
4401
4425
  // shape of an interactive funding leg, which is why both are retryable and
4402
4426
  // neither `display` implies anything went wrong.
4403
4427
  lightning_settlement_pending: {
@@ -4417,7 +4441,7 @@ var PAYMENT_ERROR_COPY = {
4417
4441
  },
4418
4442
  replay_rejected: {
4419
4443
  display: "This payment was already submitted, so it wasn't charged again.",
4420
- // internal-review: this hint reaches a caller whose money already landed — the
4444
+ // this hint reaches a caller whose money already landed — the
4421
4445
  // accumulator holds these proofs under this request id. It used to read
4422
4446
  // "use a fresh token and a new request id for the next payment", which an
4423
4447
  // agent could follow straight into paying twice for the same charge.
@@ -4561,7 +4585,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4561
4585
  snapshot
4562
4586
  });
4563
4587
  }
4564
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
4588
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
4565
4589
  const receipt = await verifyX402Payment(
4566
4590
  body.content.x402_payment,
4567
4591
  opts.x402Config,
@@ -4696,7 +4720,10 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4696
4720
  }
4697
4721
  };
4698
4722
  }
4699
- const expectedAmount = msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
4723
+ const expectedAmount = snapshot.pendingPaymentFiatCurrency === "usd" && snapshot.pendingPaymentFiatMicro !== void 0 ? fiatMicroToMethodAmount(
4724
+ snapshot.pendingPaymentFiatMicro,
4725
+ credential.challenge.method
4726
+ ) : msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
4700
4727
  const expectedMeta = challengeMeta(credential.challenge);
4701
4728
  const creditNative = methodNativeAmountFromDecimal(
4702
4729
  expectedAmount,
@@ -4975,7 +5002,7 @@ async function processIncomingPayment(job, body, opts) {
4975
5002
  try {
4976
5003
  const requiredMsats = job.pendingPaymentMsats ?? 0;
4977
5004
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
4978
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
5005
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
4979
5006
  const receipt = await verifyX402Payment(
4980
5007
  body.content.x402_payment,
4981
5008
  opts.x402Config,
@@ -5041,7 +5068,7 @@ async function processIncomingPayment(job, body, opts) {
5041
5068
  status: 402
5042
5069
  };
5043
5070
  }
5044
- const expectedAmount = msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
5071
+ const expectedAmount = job.pendingPaymentFiatCurrency === "usd" && job.pendingPaymentFiatMicro !== void 0 ? fiatMicroToMethodAmount(job.pendingPaymentFiatMicro, credential.challenge.method) : msatsToMethodAmount(expectedMsats, credential.challenge.method, rate);
5045
5072
  const expectedMeta = challengeMeta(credential.challenge);
5046
5073
  await loggers.tempo.span(
5047
5074
  "tempo.verify_credential",
@@ -5279,7 +5306,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5279
5306
  // requestId is non-null here because acceptAccumulatorPayment returns
5280
5307
  // kind:"rejected" with code "missing_request_id" when unset (handled by
5281
5308
  // the rejected branch above). Thread it as the per-call settlement
5282
- // reference for revenue_events.tx_hash (internal-review). Unique per
5309
+ // reference for revenue_events.tx_hash. Unique per
5283
5310
  // (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
5284
5311
  // it serves as a stable id even though it isn't an on-chain hash.
5285
5312
  paymentTxHash: opts.requestId,
@@ -5428,7 +5455,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5428
5455
  amount_micro: priceFiat.amountMicro,
5429
5456
  // Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
5430
5457
  // the live fetch landed without this currency and it was carried forward
5431
- // from an earlier one (internal-review). The first is the rate source down, the
5458
+ // from an earlier one. The first is the rate source down, the
5432
5459
  // second is the rate source having quietly stopped carrying a currency
5433
5460
  // this DVM advertises — a config problem, and a different repair.
5434
5461
  ...denomination.carriedForward === true && { rate_carried_forward: true },
@@ -5659,7 +5686,7 @@ async function fundAndDraw(ctx, funding, tx) {
5659
5686
  creditId: creditId2,
5660
5687
  drawId: drawId2,
5661
5688
  addAmountMicro: fundedMicro,
5662
- // internal-review: never grow the draw past what the job cumulatively asked.
5689
+ // never grow the draw past what the job cumulatively asked.
5663
5690
  // The available-balance guard inside `growDraw` cannot do this — the
5664
5691
  // `fund` above raised the balance by exactly this amount — so a second
5665
5692
  // payment for one ask would otherwise be absorbed and settle as revenue.
@@ -5772,7 +5799,7 @@ function creditDepositPayload(dvmId, funding) {
5772
5799
  creditCurrency: funding.currency,
5773
5800
  fundedAt: funding.fundedAt,
5774
5801
  expiryMs: funding.expiryMs,
5775
- // internal-review: label a session-funded deposit as one, whether or not the chain
5802
+ // label a session-funded deposit as one, whether or not the chain
5776
5803
  // binding below could be read. `rail: "tempo"` alone does not say — a
5777
5804
  // `tempo/charge` one-shot funds a credit with no channel at all — so this is
5778
5805
  // what lets the platform tell a routine charge from a session whose binding
@@ -5780,7 +5807,7 @@ function creditDepositPayload(dvmId, funding) {
5780
5807
  ...funding.tempoSession && {
5781
5808
  tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
5782
5809
  },
5783
- // internal-review: the channel registration rides the deposit report, so it
5810
+ // the channel registration rides the deposit report, so it
5784
5811
  // commits with the voucher acceptance that created the channel's liability.
5785
5812
  ...funding.tempoChannel && funding.tempoSession && {
5786
5813
  tempoChannel: {
@@ -6047,7 +6074,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
6047
6074
  receivedProofs: [],
6048
6075
  error: {
6049
6076
  // A fixed message plus the reason enum, never `err.message`: that string
6050
- // is upstream's, and on this PR's own case it is the relay lock holder's
6077
+ // is upstream's and may include the relay lock holder's
6051
6078
  // backend pid and `client_addr`. The detail is logged where the throw is.
6052
6079
  body: paymentErrorBody("x402_settlement_submission_failed", {
6053
6080
  message: "x402 batch settlement submission failed",
@@ -6646,7 +6673,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6646
6673
  /** Client-generated idempotency key — a retry with the same id credits once. */
6647
6674
  fund_id: z.string().min(1),
6648
6675
  /**
6649
- * SHA-256 hex over the raw funding artifact (spec §2 condition 3).
6676
+ * SHA-256 hex over the raw funding artifact (the funding-commitment rule).
6650
6677
  * Required for every attach rail; absent on `method: "lightning"`, which
6651
6678
  * carries no caller-supplied artifact to hash — see {@link CreditRequest}.
6652
6679
  */
@@ -6670,7 +6697,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6670
6697
  * rail this build does not carry is answered by `validateDrainTarget`
6671
6698
  * with a typed `drain_method_unsupported` naming the rails that do
6672
6699
  * work — not by a Zod parse failure that says only "invalid enum". The
6673
- * caller of a removed rail (`lightning`, internal-review) and the caller of a
6700
+ * caller of a removed rail (`lightning`) and the caller of a
6674
6701
  * typo deserve the same actionable refusal.
6675
6702
  */
6676
6703
  method: z.string().min(1).optional(),
@@ -6798,12 +6825,12 @@ async function handleBalance(c, deps, args) {
6798
6825
  return c.json({
6799
6826
  op: "balance",
6800
6827
  // Expired credits are included: expiry ends spending, never ownership
6801
- // (spec §5), so stranded value must stay visible to whoever owns it.
6828
+ // (the caller-ownership contract), so stranded value must stay visible to whoever owns it.
6802
6829
  credits: all.map(
6803
6830
  (credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
6804
6831
  ),
6805
6832
  ...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
6806
- // internal-review: the funding menu the quote and 402 surfaces already advertise,
6833
+ // the funding menu the quote and 402 surfaces already advertise,
6807
6834
  // echoed here so one read answers "what do I hold, and what may I add".
6808
6835
  // Under the key `menu`, not `credit` — that one is the credit projection on
6809
6836
  // this route. Carried without the caller echo fields: `credits[]` above says
@@ -7088,7 +7115,7 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7088
7115
  expectedChannelId,
7089
7116
  // The close intent is re-asserted from inside the verifier, once the
7090
7117
  // credential is bound to this credit's own channel and before anything
7091
- // broadcasts (internal-review). Run above the call instead and a credential
7118
+ // broadcasts. Run above the call instead and a credential
7092
7119
  // signed for another channel touches the reservation on its way to a
7093
7120
  // refusal that was never going to close anything.
7094
7121
  onBound: async () => {
@@ -7394,7 +7421,7 @@ async function handleX402ChannelDrain(c, deps, args) {
7394
7421
  effectId: `${args.credit.creditId}:${args.drainId}`,
7395
7422
  // The voucher the caller presents is what actually gets revoked and paid,
7396
7423
  // so the settlement layer refuses one signed for any other channel before
7397
- // it touches anything (internal-review).
7424
+ // it touches anything.
7398
7425
  expectedChannelId: channelId,
7399
7426
  refundBoundNative,
7400
7427
  commit: async (_funding, tx) => {
@@ -7613,7 +7640,7 @@ function drainBody(record) {
7613
7640
  status: record.status,
7614
7641
  amount_micro: record.amountMicro,
7615
7642
  currency: record.currency,
7616
- // The in-kind obligation, where there is one (internal-review): a Bitcoin
7643
+ // The in-kind obligation, where there is one: a Bitcoin
7617
7644
  // reclaim returns the deposited sats pro rata, at the rate each funding
7618
7645
  // landed at, so this figure was fixed when the reclaim was requested and
7619
7646
  // does not move with the market. Published beside the fiat one rather than
@@ -7636,7 +7663,7 @@ function drainCopy(record) {
7636
7663
  // Named because it is the obligation, and because it is
7637
7664
  // fixed: a Bitcoin refund returns the share of the Bitcoin you
7638
7665
  // deposited that you never spent, so it does not move if the
7639
- // price does before you collect it (internal-review). The mint's own
7666
+ // price does before you collect it. The mint's own
7640
7667
  // redemption charge is named rather than glossed — it is a sat
7641
7668
  // or two at a fee-charging mint and nothing at a free one, and
7642
7669
  // a figure an agent relays to a human should not be a promise
@@ -8298,7 +8325,7 @@ var DRAIN_CONFLICT_COPY = {
8298
8325
  },
8299
8326
  // Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
8300
8327
  // channel rails, and two doors with two answers about one shortfall is the
8301
- // drift consolidating it was meant to prevent (internal-review). Tempo answers its
8328
+ // drift consolidating it was meant to prevent. Tempo answers its
8302
8329
  // own through `tempoFloorRefusal`, which reaches the same conclusion a step
8303
8330
  // earlier and adds the one rail-specific fact — its channel is already closed.
8304
8331
  refund_floor: {
@@ -8309,13 +8336,13 @@ var DRAIN_CONFLICT_COPY = {
8309
8336
  retryable: false,
8310
8337
  // True even though the ledger transaction that would have written the drain
8311
8338
  // row rolled back around this refusal. The wedged settlement row is recorded
8312
- // outside that transaction (internal-review) and holds this drain id as its
8339
+ // outside that transaction and holds this drain id as its
8313
8340
  // `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
8314
8341
  // drain under — so the caller has to keep it. Dropping it mints a fresh one
8315
8342
  // that the wedged-channel gate refuses as `settlement_pending` anyway, and
8316
8343
  // re-presenting *this* id is that gate's own documented exemption. Tempo
8317
8344
  // reaches the same `true` one artifact earlier, off a `credit_drains` row
8318
- // its close-intent reservation already committed (internal-review).
8345
+ // its close-intent reservation already committed.
8319
8346
  drainRegistered: true
8320
8347
  }
8321
8348
  };
@@ -8413,7 +8440,7 @@ function creditBody(credit, reconciliationPending = false) {
8413
8440
  remaining_micro: credit.availableMicro,
8414
8441
  expiry_ms: credit.expiryMs,
8415
8442
  expired: credit.expired,
8416
- // Reclaimable via op "drain" (internal-review) — true whenever spendable-or-
8443
+ // Reclaimable via op "drain" — true whenever spendable-or-
8417
8444
  // expired value remains. Expiry ends spending, never ownership.
8418
8445
  drainable: credit.status === "active" && credit.availableMicro > 0,
8419
8446
  ...reconciliationPending && { reconciliation_pending: true },
@@ -9287,13 +9314,13 @@ function buildContext(job, opts) {
9287
9314
  if (terminated("prompt")) {
9288
9315
  return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
9289
9316
  }
9317
+ job.status = "awaiting-input";
9290
9318
  providerMessage(job, "prompt", {
9291
9319
  id,
9292
9320
  text,
9293
9321
  ...promptOpts?.options && { options: promptOpts.options },
9294
9322
  ...promptOpts?.schema && { schema: promptOpts.schema }
9295
9323
  });
9296
- job.status = "awaiting-input";
9297
9324
  if (job.replayHighSeq !== null) {
9298
9325
  const response = job.messages.find(
9299
9326
  (m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
@@ -9407,7 +9434,7 @@ function buildContext(job, opts) {
9407
9434
  poolBasis: pool.basis,
9408
9435
  askCurrency,
9409
9436
  // `draw` here means the pool read fine and either the ASK wouldn't pin
9410
- // — an fx outage on a bare-msats ask, or a sub-micro amount — or it
9437
+ // an fx outage on a bare-msats ask, or a sub-micro amount — or it
9411
9438
  // pinned in a currency this pool isn't denominated in.
9412
9439
  ...pool.basis === "draw" && { poolCurrency: pool.currency }
9413
9440
  });
@@ -9494,7 +9521,7 @@ function buildContext(job, opts) {
9494
9521
  for (const m of opts.mpp.methods) {
9495
9522
  const meta = m;
9496
9523
  if (meta.intent === "session") continue;
9497
- const amount2 = msatsToMethodAmount(amountMsats, meta.name, rate);
9524
+ const amount2 = pendingFiat?.currency === "usd" ? fiatMicroToMethodAmount(pendingFiat.amountMicro, meta.name) : msatsToMethodAmount(amountMsats, meta.name, rate);
9498
9525
  const ch = await opts.mpp.issueChallenge(meta.name, meta.intent, {
9499
9526
  amount: amount2,
9500
9527
  description: reason
@@ -9542,8 +9569,8 @@ function buildContext(job, opts) {
9542
9569
  ask_msats: amountMsats
9543
9570
  });
9544
9571
  }
9545
- providerMessage(job, "payment-request", paymentRequestContent);
9546
9572
  job.status = "awaiting-input";
9573
+ providerMessage(job, "payment-request", paymentRequestContent);
9547
9574
  const paidMsatsAtYield = job.paidMsats;
9548
9575
  const p = new Promise((resolve, reject) => {
9549
9576
  let resolved = false;
@@ -9552,12 +9579,24 @@ function buildContext(job, opts) {
9552
9579
  unsubscribeNotify?.();
9553
9580
  unsubscribeNotify = void 0;
9554
9581
  };
9555
- const doResolve = (pc) => {
9582
+ const doResolve = async (pc) => {
9556
9583
  if (resolved) return;
9557
9584
  resolved = true;
9558
9585
  cleanup();
9559
9586
  creditedToHandlerMsats += amountMsats;
9560
9587
  creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
9588
+ if (job.pendingPayment?.resolve === doResolve) {
9589
+ job.pendingPayment = null;
9590
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
9591
+ if (job.status === "awaiting-input" && opts.onYield) {
9592
+ try {
9593
+ await opts.onYield();
9594
+ } catch (err) {
9595
+ reject(err);
9596
+ return;
9597
+ }
9598
+ }
9599
+ }
9561
9600
  resolve(pc);
9562
9601
  };
9563
9602
  const doReject = (err) => {
@@ -9574,11 +9613,17 @@ function buildContext(job, opts) {
9574
9613
  if (resolved) return;
9575
9614
  try {
9576
9615
  const counters = await getCounters();
9577
- if (!counters) return;
9616
+ if (!counters || resolved) return;
9578
9617
  const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
9579
9618
  if (satisfied) {
9619
+ const resume = async () => {
9620
+ job.paidMsats = counters.paidMsats;
9621
+ job.pendingPaymentMsats = counters.pendingPaymentMsats;
9622
+ await doResolve({ amount_msats: amountMsats });
9623
+ };
9580
9624
  if (opts.claimForProcessing) {
9581
- const won = await opts.claimForProcessing();
9625
+ const won = await opts.claimForProcessing(resume);
9626
+ if (resolved) return;
9582
9627
  if (!won) {
9583
9628
  resolved = true;
9584
9629
  cleanup();
@@ -9586,10 +9631,7 @@ function buildContext(job, opts) {
9586
9631
  return;
9587
9632
  }
9588
9633
  }
9589
- job.paidMsats = counters.paidMsats;
9590
- job.pendingPaymentMsats = counters.pendingPaymentMsats;
9591
- if (job.status === "awaiting-input") job.status = "processing";
9592
- doResolve({ amount_msats: amountMsats });
9634
+ await resume();
9593
9635
  }
9594
9636
  } catch (err) {
9595
9637
  log.error("requestPayment NOTIFY check failed", { error: String(err) });
@@ -9640,12 +9682,12 @@ function buildContext(job, opts) {
9640
9682
  runTerminal();
9641
9683
  },
9642
9684
  // `failOpts.refund` is a caller-fault annotation and nothing more
9643
- // (internal-review). No rail hands value backwards: a failed job's debit is the
9685
+ // No rail hands value backwards: a failed job's debit is the
9644
9686
  // credit draw the terminal funnel releases, and the caller reclaims that
9645
- // balance through the internal-review `drain` op. The Cashu branch that used to
9687
+ // balance through the `drain` op. The Cashu branch that used to
9646
9688
  // send proofs back here was dead — `job.receivedProofs` has been empty on
9647
9689
  // every path since the accumulator became the commit boundary for inbound
9648
- // proofs (internal-review) — and internal-review, the accumulator-debit rewrite that would
9690
+ // proofs — and the accumulator-debit rewrite that would
9649
9691
  // have revived it, was cancelled as superseded by the ledger.
9650
9692
  fail: (error) => {
9651
9693
  if (terminated("fail")) return;
@@ -9715,13 +9757,17 @@ var ReceiptIssuer = class {
9715
9757
  }
9716
9758
  /**
9717
9759
  * Build and sign the receipt for a terminal job. `seq` comes from
9718
- * `JobStore.claimReceiptSeq`, `issuedAt` defaults to now (tests pin it).
9760
+ * `JobStore.claimReceiptSeq`; `issuedAt` is epoch seconds and defaults to now.
9719
9761
  *
9720
9762
  * Free jobs get receipts too, with `paid.msats: 0` — they still consume a
9721
9763
  * sequence number and still attest an outcome. Whether feedback is
9722
9764
  * paid-only is the feedback layer's call, not this one's.
9723
9765
  *
9724
- * `credit` is the resolved draw block (internal-review) — present on every job
9766
+ * Timing and attribution come from the committed terminal row, not signing
9767
+ * time or handler input. Legacy rows without those durable inputs omit the
9768
+ * optional blocks rather than inventing historical evidence.
9769
+ *
9770
+ * `credit` is the resolved draw block — present on every job
9725
9771
  * whose payment funded/drew the ledger, absent otherwise. Signature-
9726
9772
  * compatible either way: `canonicalize` omits absent keys.
9727
9773
  */
@@ -9734,6 +9780,14 @@ var ReceiptIssuer = class {
9734
9780
  capability: record.capability,
9735
9781
  outcome: outcomeFor(record.status),
9736
9782
  reason: terminalReason(record),
9783
+ ...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
9784
+ timing: {
9785
+ accepted_at_ms: record.createdAt,
9786
+ terminal_at_ms: record.terminalAtMs,
9787
+ caller_wait_ms: record.callerWaitMs ?? 0
9788
+ }
9789
+ },
9790
+ ...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
9737
9791
  seq,
9738
9792
  issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
9739
9793
  requester_pubkey: record.requesterPubkey ?? null,
@@ -9752,7 +9806,7 @@ var ReceiptIssuer = class {
9752
9806
  return signReceipt(unsigned, this.secret);
9753
9807
  }
9754
9808
  /**
9755
- * Countersign one reclaim event (internal-review). Unlike job receipts there is
9809
+ * Countersign one reclaim event. Unlike job receipts there is
9756
9810
  * no store-allocated sequence — the drain's own `ledger_seq` (taken under
9757
9811
  * the credit row lock, shared with draws) already orders it in the
9758
9812
  * per-credit evidence chain.
@@ -10069,12 +10123,16 @@ var JobManager = class {
10069
10123
  cleanupTimers = /* @__PURE__ */ new Map();
10070
10124
  /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
10071
10125
  terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
10072
- /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
10126
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
10073
10127
  statusWatchers = /* @__PURE__ */ new Map();
10074
- /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
10128
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
10075
10129
  statusChecksInFlight = /* @__PURE__ */ new Set();
10076
- /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
10130
+ /** Job ids that were notified mid-re-read and must be re-checked. */
10077
10131
  statusChecksQueued = /* @__PURE__ */ new Set();
10132
+ // Every local input resume holds this queue through its durable wait update.
10133
+ // A prompt response and a payment NOTIFY must not mistake each other for a
10134
+ // different machine's successful claim.
10135
+ inputResumeTails = /* @__PURE__ */ new WeakMap();
10078
10136
  sessionId = randomBytes4(3).toString("hex");
10079
10137
  nextJobId = 1;
10080
10138
  accumulatorMonitor;
@@ -10082,7 +10140,7 @@ var JobManager = class {
10082
10140
  processingWatchdogMs;
10083
10141
  reactivationClaimGraceMs;
10084
10142
  /**
10085
- * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
10143
+ * Worker-heartbeat cadence. Also floors the clock
10086
10144
  * guard's compensation on the processing arm — see
10087
10145
  * {@link staleSweepCompensationCapMs}.
10088
10146
  */
@@ -10102,7 +10160,7 @@ var JobManager = class {
10102
10160
  jobRetentionResumeFrom;
10103
10161
  /**
10104
10162
  * A `Date.now()` reading that cannot regress within this manager's lifetime
10105
- * (internal-review). Anchored at construction, so a manager already running when
10163
+ * Anchored at construction, so a manager already running when
10106
10164
  * the clock stepped is covered. The stale-job sweep's two cutoffs, the
10107
10165
  * orphan-draw age gate and the reactivation claim grace all read it; see
10108
10166
  * {@link createMonotonicClock} for what it costs and where it is clamped.
@@ -10115,7 +10173,7 @@ var JobManager = class {
10115
10173
  jobRetentionSweepInFlight = false;
10116
10174
  heartbeatInFlight = false;
10117
10175
  /**
10118
- * The denomination of every credit this DVM opens (internal-review). Resolved once,
10176
+ * The denomination of every credit this DVM opens. Resolved once,
10119
10177
  * with the same boundary fallback `DVMServer` applies for direct JavaScript
10120
10178
  * callers that force an incomplete value past the descriptor contract.
10121
10179
  */
@@ -10130,7 +10188,7 @@ var JobManager = class {
10130
10188
  this.jobStore = new MemoryJobStore();
10131
10189
  } else {
10132
10190
  throw new Error(
10133
- "JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL). See internal-review / internal-review."
10191
+ "JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL)."
10134
10192
  );
10135
10193
  }
10136
10194
  if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
@@ -10138,7 +10196,7 @@ var JobManager = class {
10138
10196
  JSON.stringify({
10139
10197
  level: "warn",
10140
10198
  event: "receipts_disabled_store_unsupported",
10141
- message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods. See internal-review."
10199
+ message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods."
10142
10200
  })
10143
10201
  );
10144
10202
  }
@@ -10237,7 +10295,7 @@ var JobManager = class {
10237
10295
  }
10238
10296
  }
10239
10297
  }
10240
- if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
10298
+ if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
10241
10299
  this.terminalCostRecoveryTimer = setInterval(() => {
10242
10300
  void this.recoverTerminalCosts().catch((err) => {
10243
10301
  console.error(
@@ -10251,16 +10309,14 @@ var JobManager = class {
10251
10309
  this.terminalCostRecoveryTimer.unref();
10252
10310
  }
10253
10311
  }
10254
- /** Active in-memory jobs. */
10255
10312
  get activeJobs() {
10256
10313
  return this._activeJobs;
10257
10314
  }
10258
- /** The backing job store. */
10259
10315
  get store() {
10260
10316
  return this.jobStore;
10261
10317
  }
10262
10318
  /**
10263
- * True when this DVM will actually issue receipts (internal-review) — a key *and*
10319
+ * True when this DVM will actually issue receipts — a key *and*
10264
10320
  * a store that can allocate sequence numbers and persist bytes write-once.
10265
10321
  * `/v1/info#receipts` reads this rather than the key alone, so the flag can
10266
10322
  * never promise something `issueReceipt` silently declines to do.
@@ -10277,7 +10333,7 @@ var JobManager = class {
10277
10333
  return Object.keys(this.descriptor.capabilities);
10278
10334
  }
10279
10335
  /**
10280
- * Resolve a capability's static `price` to msats (internal-review).
10336
+ * Resolve a capability's static `price` to msats.
10281
10337
  *
10282
10338
  * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
10283
10339
  * Returns `undefined` for dynamic-priced capabilities (those that declare
@@ -10290,7 +10346,7 @@ var JobManager = class {
10290
10346
  }
10291
10347
  /**
10292
10348
  * Translate a capability's static `price` into the fiat envelope used by
10293
- * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
10349
+ * the per-capability `pricing.max` advertised on `/v1/info`.
10294
10350
  * Returns `undefined` for dynamic-priced capabilities and for unknown
10295
10351
  * names (the route handler validates the name before invoking this).
10296
10352
  */
@@ -10300,24 +10356,24 @@ var JobManager = class {
10300
10356
  return resolvePricingMax(cap.price);
10301
10357
  }
10302
10358
  /**
10303
- * Parse a request body through a capability's input schema (internal-review).
10359
+ * Parse a request body through a capability's input schema.
10304
10360
  *
10305
10361
  * Resolution order when a capability declares an `input` schema:
10306
10362
  * 1. `body.data` is preferred — agents pass structured fields directly
10307
- * (CLI builds this from `--param k=v`).
10363
+ * (CLI builds this from `--param k=v`).
10308
10364
  * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
10309
- * JSON-string contract.
10365
+ * JSON-string contract.
10310
10366
  * 3. Neither usable → throw `MissingStructuredInputError` so the route can
10311
- * return `invalid_input` with a hint pointing at `/v1/info`.
10367
+ * return `invalid_input` with a hint pointing at `/v1/info`.
10312
10368
  *
10313
10369
  * When the capability has no `input` schema, returns `body.input` raw
10314
10370
  * (primitive path).
10315
10371
  *
10316
- * The internal-review credit envelope is removed before the schema runs, on both
10317
- * branches (internal-review) — it rides the signed body but is protocol-level, so a
10372
+ * The credit envelope is removed before the schema runs, on both
10373
+ * branches — it rides the signed body but is protocol-level, so a
10318
10374
  * top-level `.strict()` capability schema would otherwise reject every
10319
10375
  * explicit draw as an unknown key, before the envelope was even extracted.
10320
- * The persisted `record.input` the internal-review reactivation path re-reads is the
10376
+ * The persisted `record.input` the reactivation path re-reads is the
10321
10377
  * pre-Zod wire form, so it comes through the second branch carrying them too.
10322
10378
  */
10323
10379
  parseInput(body, capability) {
@@ -10335,7 +10391,7 @@ var JobManager = class {
10335
10391
  throw new MissingStructuredInputError();
10336
10392
  }
10337
10393
  /**
10338
- * Look up the per-capability descriptor by name (internal-review). Throws when the
10394
+ * Look up the per-capability descriptor by name. Throws when the
10339
10395
  * name doesn't exist — the route layer validates body.capability first, so
10340
10396
  * reaching this with an unknown name is a programmer error.
10341
10397
  */
@@ -10361,7 +10417,7 @@ var JobManager = class {
10361
10417
  return this.descriptor.capabilities[name];
10362
10418
  }
10363
10419
  /**
10364
- * Allocate the id the next job will be created under (internal-review). The
10420
+ * Allocate the id the next job will be created under. The
10365
10421
  * submit path calls this BEFORE payment verification so the ledger draw
10366
10422
  * commits with its job linkage, then passes the id back through
10367
10423
  * `createJob`'s provenance. Ids allocated for requests whose payment is
@@ -10372,7 +10428,7 @@ var JobManager = class {
10372
10428
  return `job-${this.sessionId}-${this.nextJobId++}`;
10373
10429
  }
10374
10430
  /**
10375
- * Create a new job from a request body. `provenance` (internal-review) is what the
10431
+ * Create a new job from a request body. `provenance` is what the
10376
10432
  * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
10377
10433
  * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
10378
10434
  * to be given it.
@@ -10428,27 +10484,23 @@ var JobManager = class {
10428
10484
  this.watchDurableStatus(job);
10429
10485
  return job;
10430
10486
  }
10431
- /** Build an SDKJobContext and attach it to the job. */
10432
10487
  buildAndAttachContext(job, parsedInput) {
10433
10488
  const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
10434
10489
  const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
10435
10490
  job.sdkCtx = sdkCtx;
10436
10491
  return sdkCtx;
10437
10492
  }
10438
- /** Start the handler for a job. */
10439
10493
  runHandler(job, sdkCtx) {
10440
10494
  const cap = this.getCapability(job.capability);
10441
10495
  const onError = (err) => {
10442
10496
  if (isTerminal(job.status)) return;
10443
10497
  job.replayProviderSkip = 0;
10444
- if (!job.pendingPrompts.size && !job.pendingPayment) {
10445
- job.status = "failed";
10446
- providerMessage(job, "cancel", {
10447
- reason: err instanceof Error ? err.message : String(err)
10448
- });
10449
- job.terminalWork = this.finalizeTerminal(job);
10450
- void job.terminalWork;
10451
- }
10498
+ job.status = "failed";
10499
+ providerMessage(job, "cancel", {
10500
+ reason: err instanceof Error ? err.message : String(err)
10501
+ });
10502
+ job.terminalWork = this.finalizeTerminal(job);
10503
+ void job.terminalWork;
10452
10504
  };
10453
10505
  if (!cap) {
10454
10506
  if (!isTerminal(job.status)) {
@@ -10476,16 +10528,46 @@ var JobManager = class {
10476
10528
  onError(err);
10477
10529
  }
10478
10530
  }
10479
- /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
10480
10531
  async dispatchMessage(job, type, content) {
10532
+ if (type !== "response" && type !== "payment") {
10533
+ return this.dispatchMessageNow(job, type, content);
10534
+ }
10535
+ const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10536
+ await this.queueInputResume(job, async () => {
10537
+ const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10538
+ if (pending && current !== pending) return;
10539
+ return this.dispatchMessageNow(job, type, content);
10540
+ });
10541
+ }
10542
+ async queueInputResume(job, resume) {
10543
+ const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
10544
+ const next = previous.catch(() => void 0).then(resume);
10545
+ this.inputResumeTails.set(job, next);
10546
+ try {
10547
+ return await next;
10548
+ } finally {
10549
+ if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
10550
+ }
10551
+ }
10552
+ async claimLocalInput(job) {
10553
+ await job.messageAppenderTail;
10554
+ return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
10555
+ }
10556
+ async dispatchMessageNow(job, type, content) {
10481
10557
  const ctx = job.sdkCtx;
10482
10558
  const cap = this.getCapability(job.capability);
10483
10559
  if (type === "response") {
10484
10560
  const rc = content;
10485
10561
  const pending = job.pendingPrompts.get(rc.prompt_id);
10486
10562
  if (pending) {
10563
+ if (!await this.claimLocalInput(job)) {
10564
+ await this.adoptDurableTerminal(job);
10565
+ return;
10566
+ }
10567
+ if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
10487
10568
  job.pendingPrompts.delete(rc.prompt_id);
10488
- job.status = "processing";
10569
+ job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
10570
+ if (job.status === "awaiting-input") await this.persistJob(job);
10489
10571
  pending.resolve(rc);
10490
10572
  } else if (cap?.onResponse && ctx) {
10491
10573
  await cap.onResponse(ctx, rc);
@@ -10494,9 +10576,15 @@ var JobManager = class {
10494
10576
  const pc = content;
10495
10577
  if (job.pendingPayment) {
10496
10578
  const pp = job.pendingPayment;
10579
+ if (!await this.claimLocalInput(job)) {
10580
+ await this.adoptDurableTerminal(job);
10581
+ return;
10582
+ }
10583
+ if (job.pendingPayment !== pp || isTerminal(job.status)) return;
10497
10584
  job.pendingPayment = null;
10498
- job.status = "processing";
10499
- pp.resolve(pc);
10585
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
10586
+ if (job.status === "awaiting-input") await this.persistJob(job);
10587
+ await pp.resolve(pc);
10500
10588
  } else if (cap?.onPayment && ctx) {
10501
10589
  await cap.onPayment(ctx, pc);
10502
10590
  }
@@ -10505,6 +10593,7 @@ var JobManager = class {
10505
10593
  } else if (type === "cancel") {
10506
10594
  const reason = content?.reason ?? "Job cancelled";
10507
10595
  abortJob(job, reason);
10596
+ job.endedBy = "caller";
10508
10597
  job.status = "cancelled";
10509
10598
  if (cap?.onCancel && ctx) {
10510
10599
  await cap.onCancel(ctx, content);
@@ -10523,17 +10612,16 @@ var JobManager = class {
10523
10612
  });
10524
10613
  }
10525
10614
  }
10526
- // ── Persistence ──────────────────────────────────────────────────────
10527
10615
  /**
10528
10616
  * Persist a job to the backing store.
10529
10617
  *
10530
- * Awaits the per-job appender tail (internal-review) so that:
10531
- * 1. The snapshot's `messages` is consistent with the `job_messages` table
10532
- * — no row is still in flight at snapshot time.
10533
- * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
10534
- * can't race a still-pending `appendOutgoing` for the final yield
10535
- * message (which would otherwise wipe the row before an in-flight SSE
10536
- * subscriber's NOTIFY-driven fetch can see it).
10618
+ * Awaits the per-job appender tail so that:
10619
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
10620
+ * no row is still in flight at snapshot time.
10621
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
10622
+ * can't race a still-pending `appendOutgoing` for the final yield
10623
+ * message (which would otherwise wipe the row before an in-flight SSE
10624
+ * subscriber's NOTIFY-driven fetch can see it).
10537
10625
  */
10538
10626
  async persistJob(job) {
10539
10627
  if (job.messageAppenderTail) {
@@ -10554,9 +10642,17 @@ var JobManager = class {
10554
10642
  } else {
10555
10643
  await this.jobStore.save(record);
10556
10644
  }
10645
+ const saved = await this.jobStore.get(job.id);
10646
+ if (saved) {
10647
+ job.timingKnown = saved.timingKnown;
10648
+ job.endedBy = saved.endedBy;
10649
+ job.terminalAtMs = saved.terminalAtMs;
10650
+ job.callerWaitMs = saved.callerWaitMs;
10651
+ job.awaitingInputAtMs = saved.awaitingInputAtMs;
10652
+ }
10557
10653
  }
10558
10654
  /**
10559
- * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
10655
+ * Cross-machine terminal guard. `buildContext`'s guard reads the
10560
10656
  * in-memory `job.status`, so on its own it only protects the machine running
10561
10657
  * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
10562
10658
  * on a machine that isn't running the job — `app.ts` cancels it in the store
@@ -10568,7 +10664,7 @@ var JobManager = class {
10568
10664
  * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
10569
10665
  * the durable state won and the caller must skip its save.
10570
10666
  *
10571
- * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
10667
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
10572
10668
  * this within a round-trip of the remote cancel committing, which is what
10573
10669
  * actually stops the provider spend; `persistJob` calls it again on every save
10574
10670
  * as the backstop for the window where a cancel commits between a handler's
@@ -10576,8 +10672,10 @@ var JobManager = class {
10576
10672
  * stores' terminal-sticky `save`, which keeps the row itself correct).
10577
10673
  */
10578
10674
  async adoptDurableTerminal(job) {
10579
- const durable = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
10580
- if (!durable || !isTerminal(durable.status) || durable.status === job.status) return false;
10675
+ const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
10676
+ if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
10677
+ const durable = await this.jobStore.get(job.id);
10678
+ if (!durable) return false;
10581
10679
  console.warn(
10582
10680
  JSON.stringify({
10583
10681
  level: "job_terminal_elsewhere",
@@ -10588,6 +10686,11 @@ var JobManager = class {
10588
10686
  );
10589
10687
  job.status = durable.status;
10590
10688
  job.summary = durable.summary;
10689
+ job.timingKnown = durable.timingKnown;
10690
+ job.endedBy = durable.endedBy;
10691
+ job.terminalAtMs = durable.terminalAtMs;
10692
+ job.callerWaitMs = durable.callerWaitMs;
10693
+ job.awaitingInputAtMs = durable.awaitingInputAtMs;
10591
10694
  const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
10592
10695
  for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
10593
10696
  job.pendingPayment?.reject(new JobCancelledError(reason));
@@ -10607,7 +10710,7 @@ var JobManager = class {
10607
10710
  });
10608
10711
  }
10609
10712
  this.clearIdleTimer(job.id);
10610
- void this.issueReceiptForStoredJob(job.id);
10713
+ void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
10611
10714
  this.scheduleCleanup(job);
10612
10715
  if (durable.status !== "completed" && terminalCostPersisted) {
10613
10716
  await this.reportJobCost(job);
@@ -10653,6 +10756,7 @@ var JobManager = class {
10653
10756
  async retryTerminalCostHandoff(job, attempt) {
10654
10757
  if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
10655
10758
  await this.reportJobCost(job);
10759
+ await this.reportTerminalJob(job.id);
10656
10760
  }
10657
10761
  /** Durably merge a terminal handler owner's latest cost before reporting it. */
10658
10762
  async persistAdoptedTerminalCost(job) {
@@ -10682,7 +10786,7 @@ var JobManager = class {
10682
10786
  );
10683
10787
  }
10684
10788
  /**
10685
- * Watch the durable status of a locally-active job (internal-review).
10789
+ * Watch the durable status of a locally-active job.
10686
10790
  *
10687
10791
  * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
10688
10792
  * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
@@ -10723,7 +10827,7 @@ var JobManager = class {
10723
10827
  });
10724
10828
  }
10725
10829
  /**
10726
- * Tear down a job's durable-status subscription (internal-review). Must be called
10830
+ * Tear down a job's durable-status subscription. Must be called
10727
10831
  * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
10728
10832
  * the store's shared LISTEN connection. Idempotent.
10729
10833
  */
@@ -10735,12 +10839,12 @@ var JobManager = class {
10735
10839
  unsubscribe();
10736
10840
  }
10737
10841
  /**
10738
- * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
10842
+ * NOTIFY-driven durable-status re-read, coalesced per job.
10739
10843
  *
10740
10844
  * A notify that arrives while a re-read is in flight is queued rather than
10741
10845
  * dropped: the in-flight read may have observed the row a moment *before* the
10742
- * cancel committed, and dropping its notify would put the abort back where
10743
- * this issue found it — waiting for the handler's next store write.
10846
+ * cancel committed, and dropping its notify would leave the local handler
10847
+ * running until its next store write instead of aborting promptly.
10744
10848
  */
10745
10849
  async checkDurableTerminal(job) {
10746
10850
  if (this.statusChecksInFlight.has(job.id)) {
@@ -10770,13 +10874,13 @@ var JobManager = class {
10770
10874
  /**
10771
10875
  * Wire the Tx A appender when the store supports streaming.
10772
10876
  *
10773
- * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
10877
+ * Tx A: outgoing message + `pending_payment_msats` bump land in
10774
10878
  * the same transaction. The DB allocates the row's seq via
10775
10879
  * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
10776
10880
  * traffic on different machines can never collide on the `(job_id, seq)` PK.
10777
10881
  * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
10778
10882
  *
10779
- * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
10883
+ * Per-job serialisation: chained through `job.messageAppenderTail`
10780
10884
  * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
10781
10885
  * `complete`) commit their `appendOutgoing` transactions in call order.
10782
10886
  * Without this, the two transactions race for the `jobs` row lock and the
@@ -10810,7 +10914,7 @@ var JobManager = class {
10810
10914
  // In-memory counter, NOT the authoritative DB-allocated seq:
10811
10915
  // `appendOutgoing` allocates the real seq via `UPDATE ...
10812
10916
  // RETURNING` inside the transaction that just failed, so no DB
10813
- // seq was assigned for this message (internal-review).
10917
+ // seq was assigned for this message.
10814
10918
  localSeq: msg.seq,
10815
10919
  note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
10816
10920
  error: err instanceof Error ? err.message : String(err)
@@ -10821,7 +10925,6 @@ var JobManager = class {
10821
10925
  job.messageAppenderTail = next;
10822
10926
  };
10823
10927
  }
10824
- // ── Idle timeout ─────────────────────────────────────────────────────
10825
10928
  /** Start (or restart) the idle timer for a job. */
10826
10929
  startIdleTimer(job) {
10827
10930
  this.clearIdleTimer(job.id);
@@ -10839,7 +10942,7 @@ var JobManager = class {
10839
10942
  this.idleTimers.delete(id);
10840
10943
  }
10841
10944
  }
10842
- /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
10945
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
10843
10946
  async cancelJobIdle(job) {
10844
10947
  if (isTerminal(job.status)) return;
10845
10948
  const current = await this.jobStore.get(job.id).catch(() => void 0);
@@ -10858,6 +10961,7 @@ var JobManager = class {
10858
10961
  if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
10859
10962
  job.pendingPrompts.clear();
10860
10963
  job.pendingPayment = null;
10964
+ job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
10861
10965
  job.status = "cancelled";
10862
10966
  abortJob(job, "Job timed out (idle)");
10863
10967
  providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
@@ -10865,14 +10969,13 @@ var JobManager = class {
10865
10969
  void job.terminalWork;
10866
10970
  this.scheduleCleanup(job);
10867
10971
  }
10868
- // ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
10869
10972
  /**
10870
10973
  * Force-terminate stale non-terminal jobs. Two status-aware arms:
10871
- * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
10872
- * (`stale_no_terminal_status`) — the caller never paid / responded.
10873
- * • `processing`/`working` past `processingWatchdogMs` → `failed`
10874
- * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
10875
- * live workers fresh, so a stale row here means a dead process.
10974
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
10975
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
10976
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
10977
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
10978
+ * live workers fresh, so a stale row here means a dead process.
10876
10979
  *
10877
10980
  * Fires on a timer (wired in the constructor) and also callable on demand
10878
10981
  * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
@@ -10881,17 +10984,17 @@ var JobManager = class {
10881
10984
  * descriptors — the local activeJobs teardown only makes sense on the
10882
10985
  * JobManager that actually hosted the zombie handler.
10883
10986
  *
10884
- * **Both cutoffs float off the wall clock (internal-review).** Each is one
10987
+ * **Both cutoffs float off the wall clock.** Each is one
10885
10988
  * `Date.now()` sample compared against `last_activity_at`, stamped from a
10886
10989
  * different sample at a different moment, so a backwards step between the
10887
10990
  * two makes every row look more recently active than it is: the query
10888
10991
  * matches nothing and both arms go silent for the length of the skew. The
10889
10992
  * dead worker's job keeps its `processing` status, its credit hold stays
10890
- * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
10891
- * never fires. Same defect, same host class, as internal-review's orphan sweep.
10993
+ * `pending`, and the paid-job-death alert — wired to this reaper —
10994
+ * never fires. Same defect, same host class, as orphan sweep.
10892
10995
  *
10893
10996
  * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
10894
- * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
10997
+ * for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
10895
10998
  * of an unclaimed hold, refused outright for any live job row; ours
10896
10999
  * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
10897
11000
  * `expectedActivityBefore` is the same compensated threshold the query used,
@@ -11027,7 +11130,7 @@ var JobManager = class {
11027
11130
  }
11028
11131
  /**
11029
11132
  * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
11030
- * clock to cover a backwards step (internal-review). The compensation is capped,
11133
+ * clock to cover a backwards step. The compensation is capped,
11031
11134
  * not free: a claimed row must still have been silent for
11032
11135
  * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
11033
11136
  * force-fails a job that may well be alive.
@@ -11047,7 +11150,7 @@ var JobManager = class {
11047
11150
  * clock stepped. That pair is marginal already; the guard must not make it
11048
11151
  * deterministic. The cap collapses to zero there, which is today's
11049
11152
  * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
11050
- * — nothing heartbeats it by design, and its refresh is an inbound caller
11153
+ * nothing heartbeats it by design, and its refresh is an inbound caller
11051
11154
  * message on no cadence at all.
11052
11155
  *
11053
11156
  * **Why a magnitude and not a predicate.** The tempting sharper rule is to
@@ -11066,7 +11169,6 @@ var JobManager = class {
11066
11169
  if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
11067
11170
  return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
11068
11171
  }
11069
- // ── Terminal job content retention ───────────────────────────────────
11070
11172
  /**
11071
11173
  * Redact terminal job content beyond this DVM's configured window.
11072
11174
  *
@@ -11231,12 +11333,11 @@ var JobManager = class {
11231
11333
  );
11232
11334
  }
11233
11335
  }
11234
- // ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
11235
11336
  /**
11236
11337
  * Resolve `pending` credit draws the terminal funnel can no longer reach.
11237
11338
  * Two arms over one scan, distinguished by whether the draw's job row exists.
11238
11339
  *
11239
- * **No job row (internal-review) — release.** internal-review allocates the job id and
11340
+ * **No job row — release.** allocates the job id and
11240
11341
  * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
11241
11342
  * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
11242
11343
  * fail-closed `replay_detected` 401 in that window leaves a committed hold
@@ -11246,12 +11347,12 @@ var JobManager = class {
11246
11347
  * available balance stays reduced forever. Cosmetic for an implicit N=1
11247
11348
  * credit, a silent balance shrink for an explicit N>1 one.
11248
11349
  *
11249
- * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
11350
+ * **Terminal job row — reconcile to its outcome.** The funnel
11250
11351
  * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
11251
11352
  * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
11252
11353
  * nothing, and until now nothing retried. Same stranded hold, reached through
11253
11354
  * a different door — and for a `completed` job it strands the platform's
11254
- * books too, since draws *are* the revenue events (internal-review): the caller is
11355
+ * books too, since draws *are* the revenue events: the caller is
11255
11356
  * charged nothing, the balance never moves, and outstanding liability
11256
11357
  * (`deposits − draw revenue`) overstates forever. So a late settle books, off
11257
11358
  * the committed draw row, through the ordinary reporter path.
@@ -11379,8 +11480,8 @@ var JobManager = class {
11379
11480
  }
11380
11481
  }
11381
11482
  /**
11382
- * The end-of-scan line for a tick that resolved nothing (internal-review, extended
11383
- * by internal-review). Two shapes are worth a warning, and neither is the ordinary
11483
+ * The end-of-scan line for a tick that resolved nothing. Two shapes are
11484
+ * worth a warning, and neither is the ordinary
11384
11485
  * one — the timer runs every 5 minutes per ledger in production.
11385
11486
  *
11386
11487
  * `examined > 0`: aged candidates were looked at and none moved. Usually
@@ -11389,7 +11490,7 @@ var JobManager = class {
11389
11490
  *
11390
11491
  * `examined === 0`: the query returned nothing. Ordinarily that means there
11391
11492
  * is nothing to do — but it is equally what a blinded scan looks like, which
11392
- * is the case the internal-review gate could not reach. Probe for the oldest
11493
+ * is the case the gate could not reach. Probe for the oldest
11393
11494
  * `pending` hold at any age and speak up when one exists the scan should
11394
11495
  * have seen and didn't: already past the age gate outright, stamped ahead of
11395
11496
  * our clock (some other process's clock is fast), or hidden while our own
@@ -11441,7 +11542,7 @@ var JobManager = class {
11441
11542
  );
11442
11543
  }
11443
11544
  /**
11444
- * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
11545
+ * Resolve one aged `pending` draw whose job row reached terminal,
11445
11546
  * mirroring what `resolveCreditDraw` would have done in line. Returns true
11446
11547
  * when this call is the one that moved the draw — the caller counts it, and
11447
11548
  * only it books.
@@ -11514,7 +11615,7 @@ var JobManager = class {
11514
11615
  /**
11515
11616
  * Refresh `lastActivityAt` for jobs this process is actively running
11516
11617
  * (`processing`/`working`) so the processing watchdog only fires once the
11517
- * worker is genuinely dead (internal-review). `awaiting-input` jobs are
11618
+ * worker is genuinely dead. `awaiting-input` jobs are
11518
11619
  * deliberately excluded — their idle timeout must still elapse. Fires on the
11519
11620
  * heartbeat timer; also callable on demand for tests.
11520
11621
  */
@@ -11534,17 +11635,16 @@ var JobManager = class {
11534
11635
  this.heartbeatInFlight = false;
11535
11636
  }
11536
11637
  }
11537
- // ── Reactivation (replay) ────────────────────────────────────────────
11538
11638
  /**
11539
- * Single-execution claim for a reactivating machine (internal-review). Before
11639
+ * Single-execution claim for a reactivating machine. Before
11540
11640
  * replaying an `awaiting-input` job, wait a bounded grace window for a live
11541
11641
  * original handler to win the `awaiting-input → processing` CAS via its
11542
11642
  * NOTIFY wake. Resolves:
11543
- * • `false` — the row left `awaiting-input` during the window (the original
11544
- * handler claimed it / drove it terminal). Stand down; do not replay.
11545
- * • `true` — the window elapsed still `awaiting-input` (the original worker
11546
- * is gone or has no live resolver) AND this machine won the claim CAS.
11547
- * Replay for dead-worker recovery.
11643
+ * • `false` — the row left `awaiting-input` during the window (the original
11644
+ * handler claimed it / drove it terminal). Stand down; do not replay.
11645
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
11646
+ * is gone or has no live resolver) AND this machine won the claim CAS.
11647
+ * Replay for dead-worker recovery.
11548
11648
  *
11549
11649
  * Biasing the original handler to win kills the double execution (double
11550
11650
  * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
@@ -11567,7 +11667,7 @@ var JobManager = class {
11567
11667
  }
11568
11668
  }
11569
11669
  /**
11570
- * Record an inbound client message durably via Tx B (internal-review) and append
11670
+ * Record an inbound client message durably via Tx B and append
11571
11671
  * it to the in-memory `job.messages` array. Returns the DB-allocated seq
11572
11672
  * (`null` when the store isn't streamable — non-Postgres test fallback).
11573
11673
  *
@@ -11611,13 +11711,13 @@ var JobManager = class {
11611
11711
  * in-memory `ServerJob`, wires the appender, builds the context, starts
11612
11712
  * the idle timer. The caller has already recorded the inbound via Tx B
11613
11713
  * and run Tx C (verify + credit); reactivation is now decoupled from
11614
- * credit (internal-review, internal-review). The caller decides whether to run the
11714
+ * credit. The caller decides whether to run the
11615
11715
  * handler (Pattern A: status was `awaiting-input`) or to dispatch the
11616
11716
  * message to a custom handler (Pattern B).
11617
11717
  *
11618
11718
  * For DVMs that declare descriptor-level auth, the persisted
11619
11719
  * `record.input` carries the original signed envelope. We re-verify the
11620
- * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
11720
+ * signature here so a DB-layer tamper — compromised admin, SQL
11621
11721
  * injection, malicious operator with DB access — can't silently feed an
11622
11722
  * attacker-supplied pubkey/envelope into the handler. The drift window
11623
11723
  * and replay store are deliberately skipped: the persisted timestamp is
@@ -11631,7 +11731,7 @@ var JobManager = class {
11631
11731
  * What gets re-verified is the persisted row read back through
11632
11732
  * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
11633
11733
  * those are the caller's own signed bytes, the same ones `/v1/quote` and the
11634
- * submit checked (internal-review). That makes the tamper check strictly stronger
11734
+ * submit checked. That makes the tamper check strictly stronger
11635
11735
  * than the parsed form it replaced: an injected key the capability schema
11636
11736
  * doesn't declare used to be stripped before the signature was checked, so
11637
11737
  * it re-verified clean.
@@ -11676,7 +11776,7 @@ var JobManager = class {
11676
11776
  return { job, sdkCtx };
11677
11777
  }
11678
11778
  /**
11679
- * Verify an inbound payment and apply the credit via Tx C (internal-review).
11779
+ * Verify an inbound payment and apply the credit via Tx C.
11680
11780
  * The inbound message must have been recorded via Tx B already
11681
11781
  * (`recordInboundPending` returned `inboundSeq`). For non-streamable
11682
11782
  * stores (`inboundSeq === null`), falls back to in-memory mutation via
@@ -11686,12 +11786,12 @@ var JobManager = class {
11686
11786
  * `pendingPaymentMsats` without external verification. Gated on
11687
11787
  * `devModeSkipsPaymentVerification` — the same predicate the upfront path
11688
11788
  * reads, so a dev server wired to a mint verifies mid-job payments for real
11689
- * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
11789
+ * plus the explicit `dev_auto` opt-out the dev console uses,
11690
11790
  * which is the one caller that has no wallet to pay from. `dev_auto` loses to
11691
11791
  * any proof riding the same message: money on the wire always takes the rail,
11692
11792
  * so the flag can never leave a real token unspent against a credited job.
11693
11793
  *
11694
- * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
11794
+ * Tx C is not atomic with the rail commit. `verifyIncomingPayment`
11695
11795
  * commits the proofs *and* the ledger leg in one transaction and returns;
11696
11796
  * only then does `verifyAndCredit` write the job row, on its own connection.
11697
11797
  * Everything after that call therefore runs with the caller's money already
@@ -11741,7 +11841,7 @@ var JobManager = class {
11741
11841
  lockPubkeys: extra?.lockPubkeys,
11742
11842
  mintHealthTracker: extra?.mintHealthTracker,
11743
11843
  fxFetcher: this.opts.fxFetcher,
11744
- // internal-review: the ledger leg — a mid-job payment is a top-up that funds
11844
+ // the ledger leg — a mid-job payment is a top-up that funds
11745
11845
  // the job's credit and grows its draw, atomically with the rail commit.
11746
11846
  creditLedger: this.opts.creditLedger,
11747
11847
  processedPayments: this.opts.processedPayments,
@@ -11876,7 +11976,7 @@ var JobManager = class {
11876
11976
  return { ...unappliedCredit && { unappliedCredit } };
11877
11977
  }
11878
11978
  /**
11879
- * Tx C with a bounded retry (internal-review).
11979
+ * Tx C with a bounded retry.
11880
11980
  *
11881
11981
  * By the time this runs on the verified path, the rail commit and the ledger
11882
11982
  * leg have already committed in a transaction this call is not part of — so a
@@ -11963,7 +12063,7 @@ var JobManager = class {
11963
12063
  }
11964
12064
  /**
11965
12065
  * Answer a payment whose rail and ledger legs committed but whose job row
11966
- * could not be written (internal-review) — every retry spent, the money real.
12066
+ * could not be written — every retry spent, the money real.
11967
12067
  *
11968
12068
  * The refusal names the credit the money landed on so the caller can act on
11969
12069
  * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
@@ -11972,7 +12072,7 @@ var JobManager = class {
11972
12072
  * fault an operator should see in their 5xx rate. `retryable` is true because
11973
12073
  * the ask is genuinely still outstanding.
11974
12074
  *
11975
- * Nothing is released here. The hold is left to the internal-review reconciler,
12075
+ * Nothing is released here. The hold is left to the reconciler,
11976
12076
  * which makes the same decision — unbound draw ⇒ release, never settle — but
11977
12077
  * under the credit row's `FOR UPDATE`, after the job is terminal, where it
11978
12078
  * cannot race an in-flight COMMIT. Releasing from here would also be
@@ -12039,7 +12139,7 @@ var JobManager = class {
12039
12139
  }
12040
12140
  /**
12041
12141
  * Describe a deposit the job row never took, in the shape both the 201 and
12042
- * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
12142
+ * the `payment_unapplied` 500 carry. `undefined` when the payment
12043
12143
  * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
12044
12144
  * there is no credit to name, so the 201 carries no `credit` block and the
12045
12145
  * 500 takes its words from `unappliedCopy` directly.
@@ -12053,7 +12153,7 @@ var JobManager = class {
12053
12153
  ...unappliedCopy(reason, creditId)
12054
12154
  };
12055
12155
  }
12056
- /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
12156
+ /** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
12057
12157
  reportCreditDeposit(funding) {
12058
12158
  const onCreditFunded = this.opts.onCreditFunded;
12059
12159
  if (!funding || !onCreditFunded || !this.opts.dvmId) return;
@@ -12066,7 +12166,7 @@ var JobManager = class {
12066
12166
  /**
12067
12167
  * Internal: build the BuildContextOpts shared between `createJob` and
12068
12168
  * `reactivateJob`. Centralised so persistence/terminal callbacks and the
12069
- * internal-review NOTIFY-driven cross-machine wake stay in one place.
12169
+ * NOTIFY-driven cross-machine wake stay in one place.
12070
12170
  */
12071
12171
  buildContextOpts(job) {
12072
12172
  const jobStore = this.jobStore;
@@ -12085,7 +12185,16 @@ var JobManager = class {
12085
12185
  getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
12086
12186
  drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
12087
12187
  pricingCurrency: this.pricingCurrency,
12088
- claimForProcessing: isStreamableJobStore(jobStore) ? () => jobStore.claimForProcessing(job.id) : void 0,
12188
+ claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
12189
+ const pending = job.pendingPayment;
12190
+ return this.queueInputResume(job, async () => {
12191
+ if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
12192
+ if (!await this.claimLocalInput(job)) return false;
12193
+ if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
12194
+ await resume?.();
12195
+ return true;
12196
+ });
12197
+ } : void 0,
12089
12198
  onSuperseded: () => {
12090
12199
  this.clearIdleTimer(job.id);
12091
12200
  this.unwatchDurableStatus(job.id);
@@ -12096,7 +12205,7 @@ var JobManager = class {
12096
12205
  }
12097
12206
  /**
12098
12207
  * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
12099
- * job's remaining pool in (internal-review).
12208
+ * job's remaining pool in.
12100
12209
  *
12101
12210
  * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
12102
12211
  * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
@@ -12114,8 +12223,7 @@ var JobManager = class {
12114
12223
  * drew and whose pool this can't read or denominate answers `unpriced`, which
12115
12224
  * the gate declines to auto-credit from: the msat figure it would otherwise
12116
12225
  * fall back on is a slice of the credit's rail value at a ratio pinned
12117
- * whenever that credit was funded, which is the exact figure this issue exists
12118
- * to stop spending against.
12226
+ * whenever that credit was funded, so it cannot safely fund a new ask.
12119
12227
  */
12120
12228
  async resolveDrawPool(job) {
12121
12229
  const ledger = this.opts.creditLedger;
@@ -12164,7 +12272,7 @@ var JobManager = class {
12164
12272
  }
12165
12273
  /**
12166
12274
  * Issue the signed receipt for a job that has reached a terminal status
12167
- * (internal-review). Idempotent and safe to call from every terminal path — the
12275
+ * Idempotent and safe to call from every terminal path — the
12168
12276
  * store owns both the sequence allocation and the write-once persist, so
12169
12277
  * two machines racing on the same job converge on identical bytes and burn
12170
12278
  * exactly one sequence number.
@@ -12249,10 +12357,10 @@ var JobManager = class {
12249
12357
  };
12250
12358
  }
12251
12359
  /**
12252
- * Settle or release a terminal job's credit draw (internal-review): `completed`
12360
+ * Settle or release a terminal job's credit draw: `completed`
12253
12361
  * settles (the hold becomes a real debit); `failed`/`cancelled` releases
12254
12362
  * (the hold evaporates — "no debit on job failure", mechanically
12255
- * superseding the internal-review/974 no-op refund for credit-paid jobs, including
12363
+ * superseding the 974 no-op refund for credit-paid jobs, including
12256
12364
  * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
12257
12365
  * resolution returns the recorded state. Returns the `ReceiptCredit` block
12258
12366
  * for the receipt: `balance_after` is the recorded draw trajectory for a
@@ -12298,7 +12406,7 @@ var JobManager = class {
12298
12406
  }
12299
12407
  }
12300
12408
  /**
12301
- * The one place a terminal job's status becomes a ledger verb (internal-review):
12409
+ * The one place a terminal job's status becomes a ledger verb:
12302
12410
  * `completed` settles the hold into a real debit, `failed`/`cancelled`
12303
12411
  * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
12304
12412
  * late reconciler (`reconcileTerminalDraw`) — two paths that must never
@@ -12313,45 +12421,47 @@ var JobManager = class {
12313
12421
  * cleanup window hand back the same bytes as a cross-machine re-read.
12314
12422
  */
12315
12423
  async attachReceipt(job) {
12316
- const receipt = await this.issueReceipt(toJobRecord(job));
12424
+ const record = await this.jobStore.get(job.id);
12425
+ if (!record || !isTerminal(record.status)) return;
12426
+ const receipt = await this.issueReceipt(record);
12317
12427
  if (receipt) job.receipt = receipt;
12318
12428
  }
12319
12429
  /**
12320
- * Read-path repair for a terminal job carrying no receipt (internal-review), for
12430
+ * Read-path repair for a terminal job carrying no receipt, for
12321
12431
  * either an in-memory job or a store record. Free in the steady state — it
12322
12432
  * returns on the `receipt` check without touching the store — so it costs
12323
12433
  * only on the cases it exists for:
12324
12434
  *
12325
12435
  * - **A crash between the two store calls.** `claimReceiptSeq` commits the
12326
- * sequence number to the job row before `saveReceipt` writes the bytes; a
12327
- * process death in that window would otherwise strand that number
12328
- * forever, and a permanent gap is indistinguishable from the deliberate
12329
- * suppression `seq` exists to expose. Re-issuing here reuses the already
12330
- * committed number (the claim is idempotent per job) rather than
12331
- * allocating a second one.
12436
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
12437
+ * process death in that window would otherwise strand that number
12438
+ * forever, and a permanent gap is indistinguishable from the deliberate
12439
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
12440
+ * committed number (the claim is idempotent per job) rather than
12441
+ * allocating a second one.
12332
12442
  * - **A transient store failure** at the terminal: `issueReceipt` logs
12333
- * `receipt_issue_failed` and returns nothing rather than failing the job,
12334
- * so the next read retries.
12443
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
12444
+ * so the next read retries.
12335
12445
  * - **Jobs that terminated before the DVM had a receipt key.** They pick one
12336
- * up on first read, with `issued_at` reflecting when it was signed.
12446
+ * up on first read, with `issued_at` reflecting when it was signed.
12337
12447
  */
12338
12448
  async ensureReceipt(target) {
12339
- if (target.receipt) return;
12340
- const record = "listeners" in target ? toJobRecord(target) : target;
12449
+ if (target.receipt) {
12450
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12451
+ return;
12452
+ }
12453
+ const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
12454
+ if (!record || !isTerminal(record.status)) return;
12341
12455
  const receipt = await this.issueReceipt(record);
12342
12456
  if (receipt) target.receipt = receipt;
12457
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12343
12458
  }
12344
- /**
12345
- * Close out a terminal job whose caller hasn't already persisted it: sign
12346
- * the receipt, then save the snapshot. The snapshot save never writes the
12347
- * receipt column, so ordering only affects how soon a reader sees the
12348
- * receipt — this way a caller polling immediately after the terminal
12349
- * already finds it.
12350
- */
12459
+ /** Commit the terminal snapshot before resolving money and signing its durable evidence. */
12351
12460
  async finalizeTerminal(job) {
12461
+ await this.persistJob(job);
12352
12462
  await this.attachReceipt(job);
12353
- await this.persistJob(job).catch(() => void 0);
12354
12463
  await this.reportJobCost(job);
12464
+ await this.reportTerminalJob(job.id);
12355
12465
  }
12356
12466
  /**
12357
12467
  * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
@@ -12370,20 +12480,69 @@ var JobManager = class {
12370
12480
  }
12371
12481
  /**
12372
12482
  * Finish accounting for a terminal written directly to the store — the
12373
- * stale reaper and a cancel handled on a different machine (internal-review).
12483
+ * stale reaper and a cancel handled on a different machine.
12374
12484
  */
12375
12485
  async finalizeStoredTerminal(jobId) {
12376
12486
  const receipt = await this.issueReceiptForStoredJob(jobId);
12377
12487
  const record = await this.jobStore.get(jobId).catch(() => void 0);
12378
- if (record && isTerminal(record.status)) await this.reportJobCost(record);
12488
+ if (record && isTerminal(record.status)) {
12489
+ await this.reportJobCost(record);
12490
+ await this.reportTerminalJob(jobId);
12491
+ }
12379
12492
  return receipt;
12380
12493
  }
12494
+ async reportTerminalJob(jobId) {
12495
+ try {
12496
+ const callback = this.opts.onJobTerminal;
12497
+ const dvmId = this.opts.dvmId;
12498
+ if (!callback || !dvmId) return;
12499
+ const record = await this.jobStore.get(jobId);
12500
+ if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
12501
+ const receipt = await this.issueReceipt(record);
12502
+ if (this.opts.receiptIssuer && !receipt) return;
12503
+ if (record.creditId && record.drawId && this.opts.creditLedger) {
12504
+ const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
12505
+ if (!draw || draw.status === "pending") return;
12506
+ }
12507
+ await callback({
12508
+ dvmId,
12509
+ jobId: record.id,
12510
+ capability: record.capability,
12511
+ outcome: record.status,
12512
+ endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
12513
+ paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
12514
+ acceptedAtMs: record.createdAt,
12515
+ terminalAtMs: record.terminalAtMs,
12516
+ callerWaitMs: record.callerWaitMs ?? 0,
12517
+ receiptSeq: receipt?.seq ?? null
12518
+ });
12519
+ if ("markTerminalReported" in this.jobStore) {
12520
+ await this.jobStore.markTerminalReported(jobId);
12521
+ }
12522
+ } catch (error) {
12523
+ console.error(`[${jobId}] Terminal reporting failed:`, error);
12524
+ }
12525
+ }
12526
+ terminalReportRecoveryCursor;
12527
+ async recoverTerminalReports() {
12528
+ const store = this.jobStore;
12529
+ const dvmId = this.opts.dvmId;
12530
+ if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
12531
+ const recovery = store;
12532
+ const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
12533
+ for (const record of records) await this.reportTerminalJob(record.id);
12534
+ this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
12535
+ }
12381
12536
  /** Handle job reaching terminal state (completed, failed, cancelled). */
12382
12537
  async handleJobTerminal(job) {
12383
12538
  this.clearIdleTimer(job.id);
12539
+ if (await this.adoptDurableTerminal(job)) return;
12540
+ const durable = await this.jobStore.get(job.id);
12541
+ if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
12384
12542
  await this.attachReceipt(job);
12385
12543
  const booked = await this.bookRevenue(job);
12386
12544
  if (!booked) await this.reportJobCost(job);
12545
+ await this.reportTerminalJob(job.id);
12387
12546
  this.scheduleCleanup(job);
12388
12547
  }
12389
12548
  /**
@@ -12396,6 +12555,7 @@ var JobManager = class {
12396
12555
  async recoverTerminalCosts() {
12397
12556
  const store = this.jobStore;
12398
12557
  const dvmId = this.opts.dvmId;
12558
+ await this.recoverTerminalReports();
12399
12559
  if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
12400
12560
  return { examined: 0, queued: 0, complete: true };
12401
12561
  }
@@ -12480,11 +12640,11 @@ var JobManager = class {
12480
12640
  return true;
12481
12641
  }
12482
12642
  /**
12483
- * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
12643
+ * Report a completed paid job's revenue under its durable job identity.
12484
12644
  * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
12485
12645
  *
12486
12646
  * For a **credit-backed** job the revenue event is the *settled draw* (spec
12487
- * §10), and since internal-review the sats figure corrects the job's mirror of that
12647
+ * §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
12488
12648
  * draw against the draw itself: `withDrawBasis` stamps the job when the
12489
12649
  * payment lands, but a Bitcoin credit's settle re-prices the draw against
12490
12650
  * the funding lots it consumed. The correction is a delta, because a job's
@@ -12499,7 +12659,7 @@ var JobManager = class {
12499
12659
  * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
12500
12660
  * payload is reported verbatim.
12501
12661
  *
12502
- * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
12662
+ * Takes the fields rather than a `ServerJob` so the reconciler — a
12503
12663
  * background sweep that holds no in-process job — books through this exact
12504
12664
  * path off the row it read. `paymentTxHash` is passed **verbatim**, never
12505
12665
  * recomputed as a `drawSettlementRef`: it is the same column the in-line
@@ -12516,7 +12676,7 @@ var JobManager = class {
12516
12676
  * DVM with no reporter). Every drop that costs a booking logs first.
12517
12677
  *
12518
12678
  * The rail check sits **below** the draw re-read, not in the entry guard
12519
- * (internal-review): a settled draw the terminal can't report under is a real debit
12679
+ * a settled draw the terminal can't report under is a real debit
12520
12680
  * with no revenue event, permanently overstating outstanding liability
12521
12681
  * (`deposits − draw revenue`), and it used to return here in silence. Placed
12522
12682
  * after the re-read, `revenue_skipped_no_rail` can name the draw and its
@@ -12579,9 +12739,9 @@ var JobManager = class {
12579
12739
  paidMsats: job.paidMsats,
12580
12740
  creditRail: creditRail ?? null,
12581
12741
  // Four distinct causes needing four distinct fixes, so none of them
12582
- // is the else-branch: the credit predates internal-review and has no rail
12742
+ // is the else-branch: the credit predates and has no rail
12583
12743
  // (`credit_rail_null`); it carries one this build doesn't list, the
12584
- // internal-review sibling class (`credit_rail_unrecognized`); the credit's
12744
+ // sibling class (`credit_rail_unrecognized`); the credit's
12585
12745
  // rail is perfectly good and it is the *job row* that lost its own
12586
12746
  // (`job_rail_missing`), which no change to the ledger would fix; or
12587
12747
  // this manager was built with no `creditLedger` to read the draw
@@ -12629,7 +12789,7 @@ var JobManager = class {
12629
12789
  cashuFlow: job.cashuFlow,
12630
12790
  // Absent unless the handler declared one, and absent is a fact: the
12631
12791
  // platform shows "no costs reported" rather than a zero cost and a
12632
- // 100% margin (internal-review). Both halves or neither — a bare amount
12792
+ // 100% margin. Both halves or neither — a bare amount
12633
12793
  // in an unnamed currency is not a cost.
12634
12794
  ...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
12635
12795
  ...credit
@@ -12812,8 +12972,8 @@ var MintHealthTracker = class {
12812
12972
  firstProbeComplete = false;
12813
12973
  constructor(opts) {
12814
12974
  this.mints = [...opts.mints];
12815
- this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
12816
- this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
12975
+ this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
12976
+ this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
12817
12977
  this.fetchImpl = opts.fetchImpl;
12818
12978
  this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
12819
12979
  this.nowFn = opts.now ?? Date.now;
@@ -12864,7 +13024,7 @@ var MintHealthTracker = class {
12864
13024
  /**
12865
13025
  * True once the first full probe tick has landed. Before this, paid calls
12866
13026
  * are held at `503 mint_health_pending` rather than accepted against
12867
- * unvalidated mints (internal-review).
13027
+ * unvalidated mints.
12868
13028
  */
12869
13029
  initialProbeComplete() {
12870
13030
  return this.firstProbeComplete;
@@ -12889,7 +13049,7 @@ var MintHealthTracker = class {
12889
13049
  * gate — a deterministic, permanent config error, unlike a transient outage.
12890
13050
  * Unknown mints return false. `advertisedMints()` uses this to keep a
12891
13051
  * permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
12892
- * where a merely-flapping mint is still published (internal-review).
13052
+ * where a merely-flapping mint is still published.
12893
13053
  */
12894
13054
  isNutIncompatible(mintUrl) {
12895
13055
  return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
@@ -12956,7 +13116,7 @@ var MintHealthTracker = class {
12956
13116
  * One-shot structured summary emitted after the first probe tick. Replaces
12957
13117
  * the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
12958
13118
  * all-mints-unhealthy warning — the operator alert that the Fly restart-loop
12959
- * used to be before boot stopped refusing to start (internal-review).
13119
+ * used to be before boot stopped refusing to start.
12960
13120
  */
12961
13121
  logBootHealth() {
12962
13122
  const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
@@ -12981,15 +13141,15 @@ var MintHealthTracker = class {
12981
13141
  );
12982
13142
  }
12983
13143
  };
12984
- function resolveEnvTickMs() {
12985
- const raw = process.env.DVMKIT_MINT_HEALTH_TICK_MS;
13144
+ function resolveEnvTickMs(env = process.env) {
13145
+ const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
12986
13146
  if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
12987
13147
  const parsed = Number(raw);
12988
13148
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
12989
13149
  return Math.floor(parsed);
12990
13150
  }
12991
- function resolveEnvFailuresToSick() {
12992
- const raw = process.env.DVMKIT_MINT_FAILURES_TO_SICK;
13151
+ function resolveEnvFailuresToSick(env = process.env) {
13152
+ const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
12993
13153
  if (!raw) return DEFAULT_FAILURES_TO_SICK;
12994
13154
  const parsed = Number(raw);
12995
13155
  if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
@@ -14040,9 +14200,9 @@ async function handlePendingDrains(c, opts) {
14040
14200
  sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
14041
14201
  // The figure a `manual` payout is actually sized in: `amount_micro` is
14042
14202
  // fiat, and the operator sends the rail's own token. It is the same debit
14043
- // `completeX402ChannelDrain` treats as its settlement floor (internal-review),
14203
+ // `completeX402ChannelDrain` treats as its settlement floor,
14044
14204
  // so publishing it is what lets an operator settle from the row instead
14045
- // of assuming their stablecoin happens to be six-decimal (internal-review).
14205
+ // of assuming their stablecoin happens to be six-decimal.
14046
14206
  amount_native: d.drainedNative,
14047
14207
  settlement,
14048
14208
  // Why this row is classified as it is, and the channel an operator would
@@ -14565,7 +14725,7 @@ async function handleReconcileInvoice(c, opts) {
14565
14725
  fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
14566
14726
  expiryMs,
14567
14727
  // The settlements table types this deposit `repair` off the
14568
- // flag (internal-review): the sats landed at payment time and the
14728
+ // flag: the sats landed at payment time and the
14569
14729
  // deposit is the one row they get.
14570
14730
  repaired: true
14571
14731
  }
@@ -16560,7 +16720,7 @@ function autoApprovePayment(content) {
16560
16720
  headers: jobHeaders(true),
16561
16721
  // amount_msats rides along so an onPayment handler sees the same shape a
16562
16722
  // real rail would hand it. dev_auto is admitted on dev-mode servers only
16563
- // (internal-review) \u2014 hence the status check below rather than a silent catch.
16723
+ // hence the status check below rather than a silent catch.
16564
16724
  body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
16565
16725
  }).then(function(r) {
16566
16726
  if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
@@ -16707,7 +16867,7 @@ async function createDVMServer(descriptor, opts) {
16707
16867
  }
16708
16868
  })();
16709
16869
  try {
16710
- if (process.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16870
+ if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16711
16871
  console.warn(
16712
16872
  JSON.stringify({
16713
16873
  level: "cashu_opts_env_ignored",
@@ -16720,12 +16880,12 @@ async function createDVMServer(descriptor, opts) {
16720
16880
  const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
16721
16881
  const dvmId = opts.dvmId;
16722
16882
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16723
- const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
16883
+ const { RevenueReporter } = await import("./revenue-reporter-NNCNRY4C.js");
16724
16884
  const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16725
16885
  revenueReporter = reporter;
16726
16886
  await reporter.init();
16727
16887
  reporter.startRetryLoop();
16728
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-RG6XNGPI.js");
16888
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5PIRYFVQ.js");
16729
16889
  payoutReporter = new PayoutReporter({
16730
16890
  dvmId,
16731
16891
  db: opts.db,
@@ -16868,6 +17028,7 @@ async function createDVMServer(descriptor, opts) {
16868
17028
  processedPayments,
16869
17029
  onJobCompleted: effectiveOnJobCompleted,
16870
17030
  onJobCost: effectiveOnJobCost,
17031
+ onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
16871
17032
  onPaidJobDeath: effectiveOnPaidJobDeath,
16872
17033
  onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16873
17034
  onCreditFunded: opts.onCreditFunded,
@@ -16911,7 +17072,7 @@ var DVMServer = class {
16911
17072
  fxFetcher;
16912
17073
  /**
16913
17074
  * The currency this DVM prices in and denominates its credit ledger in,
16914
- * resolved once at construction (internal-review).
17075
+ * resolved once at construction.
16915
17076
  *
16916
17077
  * `configureDVM` and the CLI loader both resolve this field. The server is
16917
17078
  * also exported for direct JavaScript callers, so defaulting at the one
@@ -16920,29 +17081,24 @@ var DVMServer = class {
16920
17081
  */
16921
17082
  pricingCurrency;
16922
17083
  /**
16923
- * Last-good lock-pubkey state observed from Postgres (internal-review). Used as the
17084
+ * Last-good lock-pubkey state observed from Postgres. Used as the
16924
17085
  * fallback when `loadLockPubkeyState` throws — degrades more gracefully than
16925
17086
  * falling back to the env-derived seed, which post-rotation is the *retired*
16926
17087
  * pubkey. Null until the first successful read.
16927
17088
  */
16928
17089
  cachedLockPubkeyState = null;
16929
- /** Guards the opportunistic Tempo settlement cadence (internal-review). */
16930
17090
  tempoSweepInFlight = false;
16931
17091
  tempoSweepLastAttemptMs = 0;
16932
- /** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
16933
17092
  receiptIssuer;
16934
- /** Credit ledger the payment gate funds/draws through (internal-review). */
16935
17093
  creditLedger;
16936
- /** Durable x402/mpp funding markers (internal-review). */
16937
17094
  processedPayments;
16938
17095
  authAudience;
16939
17096
  /** Lazily constructed when a refreshed facilitator probe gains batch support. */
16940
17097
  x402BatchSettlement;
16941
17098
  /** Coalesces concurrent requests that first observe batch support. */
16942
17099
  x402BatchSettlementInit;
16943
- /** Durable exact-settlement coordinator shared by every exact payment surface. */
16944
17100
  x402ExactSettlement;
16945
- /** Withdraws this mount's Tempo session capability from the poll (internal-review). */
17101
+ /** Withdraws this mount's Tempo session capability from the poll. */
16946
17102
  undeclareSessionCapability;
16947
17103
  constructor(descriptor, opts) {
16948
17104
  this.descriptor = descriptor;
@@ -16966,7 +17122,7 @@ var DVMServer = class {
16966
17122
  this.mintHealthTracker = opts.mintHealthTracker;
16967
17123
  this.ownsMintHealthTracker = false;
16968
17124
  } else if (opts.mints && opts.mints.length > 0) {
16969
- this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
17125
+ this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
16970
17126
  this.ownsMintHealthTracker = true;
16971
17127
  } else {
16972
17128
  this.ownsMintHealthTracker = false;
@@ -16987,7 +17143,7 @@ var DVMServer = class {
16987
17143
  mints: opts.mints,
16988
17144
  jobStore: opts.jobStore,
16989
17145
  devMode: opts.devMode,
16990
- // Derived, not raw (internal-review). `ctx.requestPayment` gates its x402 and
17146
+ // Derived, not raw. `ctx.requestPayment` gates its x402 and
16991
17147
  // Tempo challenge issuance on this list, while `/v1/info`, `/v1/quote`
16992
17148
  // and the upfront 402 all read `resolvePaymentMethods()` — so passing the
16993
17149
  // raw option meant a DVM that never set `paymentMethods` (no first-party
@@ -17012,21 +17168,20 @@ var DVMServer = class {
17012
17168
  dvmId: opts.dvmId,
17013
17169
  authAudience: this.authAudience,
17014
17170
  fxFetcher: this.fxFetcher,
17015
- // internal-review: the mid-job top-up leg needs the same rail wiring the upfront
17171
+ // the mid-job top-up leg needs the same rail wiring the upfront
17016
17172
  // path gets — a divergence here would be a DVM whose upfront payment funds
17017
17173
  // a credit and whose mid-job payment silently doesn't.
17018
17174
  processedPayments: this.processedPayments,
17019
17175
  enqueueCreditDeposit: opts.enqueueCreditDeposit,
17020
17176
  enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
17021
17177
  creditTtlMs: descriptor.credit ? descriptor.credit.ttlSeconds * 1e3 : void 0,
17022
- // internal-review: the same read `creditCurrency()` serves everywhere else, so a
17023
- // mid-job ask pins in the denomination this DVM's credits actually use.
17024
17178
  pricingCurrency: this.pricingCurrency,
17025
17179
  onCreditFunded: opts.onCreditFunded,
17026
17180
  onJobCompleted: opts.onJobCompleted,
17027
17181
  onPaidJobCompleted: opts.onPaidJobCompleted,
17028
17182
  paymentMode: opts.paymentMode,
17029
17183
  onJobCost: opts.onJobCost,
17184
+ onJobTerminal: opts.onJobTerminal,
17030
17185
  onPaidJobDeath: opts.onPaidJobDeath,
17031
17186
  onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
17032
17187
  onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
@@ -17064,7 +17219,7 @@ var DVMServer = class {
17064
17219
  * Tear down this mount. Every leg is synchronous except the batch-settlement
17065
17220
  * server's own `stop()`; the returned promise settles once that has finished,
17066
17221
  * so a caller holding a resource behind it — the settlement pool the spend
17067
- * gate's store owns (internal-review) — can sequence its close instead of pulling it
17222
+ * gate's store owns — can sequence its close instead of pulling it
17068
17223
  * out from under an in-flight stop. A rejection remains observable so the app
17069
17224
  * and host teardown layers can finish later cleanup before surfacing it.
17070
17225
  */
@@ -17165,12 +17320,12 @@ var DVMServer = class {
17165
17320
  (c) => c.json({
17166
17321
  ok: true,
17167
17322
  // shortSha, not .slice(0, 7): a dirty-tree deploy bakes a `-dirty`
17168
- // marker (internal-review) that a raw truncation would cut off the end,
17323
+ // marker that a raw truncation would cut off the end,
17169
17324
  // leaving this endpoint claiming a clean commit while /v1/info's
17170
17325
  // untruncated sdk_sha says otherwise — same image, two answers.
17171
- gitSha: shortSha(process.env.GIT_SHA ?? "unknown"),
17172
- deployedAt: process.env.BUILD_TIME ?? "unknown",
17173
- deployId: process.env.DVMKIT_DEPLOY_ID ?? "unknown"
17326
+ gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
17327
+ deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
17328
+ deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17174
17329
  })
17175
17330
  );
17176
17331
  if (this.opts.devMode) {
@@ -17203,8 +17358,6 @@ var DVMServer = class {
17203
17358
  getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
17204
17359
  graceSeconds: this.graceSeconds(),
17205
17360
  nonceStore: this.adminCashuNonceStore,
17206
- // internal-review: a completed melt is a landed payout, queued in the
17207
- // transaction that marks it.
17208
17361
  ...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
17209
17362
  });
17210
17363
  }
@@ -17222,13 +17375,13 @@ var DVMServer = class {
17222
17375
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
17223
17376
  enqueueCreditDrain: this.opts.enqueueCreditDrain,
17224
17377
  lightningReceive: this.opts.lightningReceive,
17225
- // internal-review's x402 repair queue rides the same surface, and resolves
17378
+ // x402 repair queue rides the same surface, and resolves
17226
17379
  // the rail the same lazy way the verify path does — constructing the
17227
17380
  // settlement server probes the facilitator, which must not happen just
17228
17381
  // because the admin routes were mounted.
17229
17382
  x402BatchSettlement: () => this.resolveX402BatchSettlement(),
17230
17383
  x402ExactSettlement: this.x402ExactSettlement,
17231
- // internal-review's Tempo repair queue, same surface again. The mppx handle is
17384
+ // Tempo repair queue, same surface again. The mppx handle is
17232
17385
  // already built by the time routes mount, so the thunk is uniformity
17233
17386
  // rather than laziness — and it is the *handle*, not the store, so a
17234
17387
  // DVM wired with an in-memory session store resolves to a rail with no
@@ -17236,10 +17389,8 @@ var DVMServer = class {
17236
17389
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
17237
17390
  // Health-filtered, and read per request rather than captured: the
17238
17391
  // servicer picking a mint to buy a refund at wants the list that
17239
- // excludes a mint currently failing its probe (internal-review).
17392
+ // excludes a mint currently failing its probe.
17240
17393
  mints: () => this.advertisedMints(),
17241
- // internal-review: a repaired Tempo close is a landed payout the agent
17242
- // finished, queued in the transaction that books the drain.
17243
17394
  ...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
17244
17395
  });
17245
17396
  }
@@ -17250,7 +17401,7 @@ var DVMServer = class {
17250
17401
  /**
17251
17402
  * Load the live lock-pubkey state for this DVM. Reads from
17252
17403
  * `dvm_lock_pubkeys` on every call so rotations propagate without needing
17253
- * a process restart (internal-review). On success the result is cached so a later
17404
+ * a process restart. On success the result is cached so a later
17254
17405
  * DB outage can degrade to the last-observed state instead of the
17255
17406
  * env-derived seed (which post-rotation is the *retired* pubkey).
17256
17407
  */
@@ -17261,7 +17412,7 @@ var DVMServer = class {
17261
17412
  }
17262
17413
  /**
17263
17414
  * Resolve the active lock-pubkey set for the mid-job cashu accept path
17264
- * (internal-review). Returns the same `[current, ...retired_in_grace]` shape the
17415
+ * Returns the same `[current, ...retired_in_grace]` shape the
17265
17416
  * upfront flow uses at lines 709-718 so accept-time rotation tolerance is
17266
17417
  * consistent across upfront and mid-job. Falls back to `undefined` when
17267
17418
  * accumulator mode isn't wired (the receive path then rejects with
@@ -17298,13 +17449,13 @@ var DVMServer = class {
17298
17449
  }
17299
17450
  /**
17300
17451
  * Run the routine Tempo settlement cadence opportunistically, on ordinary
17301
- * traffic (internal-review).
17452
+ * traffic.
17302
17453
  *
17303
17454
  * Routine settlement used to ride the per-DVM watcher machine's four-minute
17304
17455
  * poll, which is exactly what kept an idle DVM awake and billing. It cannot
17305
17456
  * move to a timer for the same reason — a timer on a machine that ought to
17306
17457
  * be suspended either does not fire or defeats the suspension. So it rides
17307
- * requests instead, the way Lightning funding is credited (internal-review): a DVM
17458
+ * requests instead, the way Lightning funding is credited: a DVM
17308
17459
  * with no traffic has nothing to settle on a 24-hour cadence anyway, and the
17309
17460
  * forced-close guarantee does not depend on this path at all — that is the
17310
17461
  * chain-event wake, which reaches a suspended DVM through Fly Proxy.
@@ -17344,7 +17495,7 @@ var DVMServer = class {
17344
17495
  });
17345
17496
  }
17346
17497
  /**
17347
- * Whether one advertised MPP method may still be offered (internal-review).
17498
+ * Whether one advertised MPP method may still be offered.
17348
17499
  *
17349
17500
  * Only `tempo/session` is gated, and only when this DVM is platform-hosted:
17350
17501
  * a session channel is safe to accept exactly while something is watching
@@ -17360,7 +17511,6 @@ var DVMServer = class {
17360
17511
  tempoSessionOffered(method) {
17361
17512
  return this.tempoSessionWithheldReason(method) === void 0;
17362
17513
  }
17363
- /** Stable caller-facing reason for withholding one newly opened session. */
17364
17514
  tempoSessionWithheldReason(method) {
17365
17515
  if (method.method !== "tempo" || method.intent !== "session") return void 0;
17366
17516
  if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
@@ -17371,7 +17521,6 @@ var DVMServer = class {
17371
17521
  }
17372
17522
  return void 0;
17373
17523
  }
17374
- /** Await only the cold-start reads each fail-closed session gate needs. */
17375
17524
  async ensureTempoSessionHealth() {
17376
17525
  await Promise.all([
17377
17526
  this.opts.tempoObserverHealth?.ensureFresh?.(),
@@ -17382,7 +17531,6 @@ var DVMServer = class {
17382
17531
  * Whether this DVM is **configured** to take TIP-1034 session channels — the
17383
17532
  * value the observer-health poll carries back to the platform, where it keeps
17384
17533
  * `dvms.tempo_session_advertised_at` current between container deploys
17385
- * (internal-review).
17386
17534
  *
17387
17535
  * Deliberately the same question the platform's post-deploy `/v1/info` probe
17388
17536
  * answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
@@ -17394,13 +17542,12 @@ var DVMServer = class {
17394
17542
  * It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
17395
17543
  * and never {@link tempoSessionOffered}: what the column records is
17396
17544
  * capability, not availability, so an observer blip must leave it alone. That
17397
- * distinction is the whole of internal-review, and answering it here from the gate
17545
+ * distinction is the whole of and answering it here from the gate
17398
17546
  * would reintroduce it on a one-minute cadence.
17399
17547
  */
17400
17548
  tempoSessionCapability() {
17401
17549
  if (!this.opts.mpp) return false;
17402
17550
  if (!creditMenuConfigured({
17403
- // The SDK server only ever runs the container runtime — see `creditMenu`.
17404
17551
  runtime: "container",
17405
17552
  config: this.descriptor.credit,
17406
17553
  hasAuth: !!this.descriptor.auth,
@@ -17415,7 +17562,7 @@ var DVMServer = class {
17415
17562
  }
17416
17563
  /**
17417
17564
  * `POST /_internal/tempo-close-event` — the forced-close wake path
17418
- * (internal-review), replacing the per-DVM `tempo-session-watch` Fly Machine.
17565
+ * replacing the per-DVM `tempo-session-watch` Fly Machine.
17419
17566
  *
17420
17567
  * Deliberately **unauthenticated**, and that is the design rather than an
17421
17568
  * omission: the body is an event locator, so the DVM re-reads the event from
@@ -17494,7 +17641,7 @@ var DVMServer = class {
17494
17641
  }
17495
17642
  /**
17496
17643
  * Compose the `/v1/info#builder` block from `opts.builder` (inline SDK
17497
- * config) with a fallback to deploy-injected env vars (internal-review). Each
17644
+ * config) with a fallback to deploy-injected env vars. Each
17498
17645
  * field is independently sourced (opts wins per-field, env fills gaps);
17499
17646
  * all three of pubkey / attestation / signature must resolve to emit the
17500
17647
  * block — partial state is treated as absent rather than half-emitted.
@@ -17507,8 +17654,8 @@ var DVMServer = class {
17507
17654
  builderAttestationBlock() {
17508
17655
  const builder = this.opts.builder;
17509
17656
  const env = this.opts.env;
17510
- const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY ?? process.env.DVMKIT_BUILDER_PUBKEY;
17511
- const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE ?? process.env.DVMKIT_BUILDER_SIGNATURE;
17657
+ const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
17658
+ const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
17512
17659
  const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
17513
17660
  if (!pubkey || !signature || !attestation) return null;
17514
17661
  return { pubkey, attestation, signature };
@@ -17540,7 +17687,7 @@ var DVMServer = class {
17540
17687
  throw new AuthAudienceError();
17541
17688
  }
17542
17689
  /**
17543
- * The slug every receipt is stamped with (internal-review). Read off the signed
17690
+ * The slug every receipt is stamped with. Read off the signed
17544
17691
  * attestation payload, which is the only place the *attested* slug lives —
17545
17692
  * a receipt claiming a different name than the attestation binds would be
17546
17693
  * unverifiable. Falls back to the descriptor name for dev/self-host boots
@@ -17551,14 +17698,13 @@ var DVMServer = class {
17551
17698
  const slug = attestation?.slug;
17552
17699
  return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
17553
17700
  }
17554
- /** Immutable identity written into every signed receipt. */
17555
17701
  receiptDvmId() {
17556
17702
  const attestation = this.builderAttestationBlock()?.attestation;
17557
17703
  const dvmId = attestation?.dvm_id;
17558
17704
  return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
17559
17705
  }
17560
17706
  parseAttestationFromEnv(env) {
17561
- const raw = env.DVMKIT_BUILDER_ATTESTATION ?? process.env.DVMKIT_BUILDER_ATTESTATION;
17707
+ const raw = env.DVMKIT_BUILDER_ATTESTATION;
17562
17708
  if (!raw) return null;
17563
17709
  try {
17564
17710
  const parsed = JSON.parse(raw);
@@ -17569,7 +17715,7 @@ var DVMServer = class {
17569
17715
  }
17570
17716
  }
17571
17717
  /**
17572
- * Owner display block for `/v1/info#owner` (internal-review). Resolved from
17718
+ * Owner display block for `/v1/info#owner`. Resolved from
17573
17719
  * `opts.owner` or env vars injected by the platform deploy pipeline.
17574
17720
  * Returns null when no owner identity is configured — pre-unification
17575
17721
  * DVMs that haven't been redeployed omit the block.
@@ -17577,11 +17723,11 @@ var DVMServer = class {
17577
17723
  ownerDisplayBlock() {
17578
17724
  const owner = this.opts.owner;
17579
17725
  const env = this.opts.env;
17580
- const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE ?? process.env.DVMKIT_OWNER_HANDLE;
17726
+ const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
17581
17727
  if (!handle) return null;
17582
- const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? process.env.DVMKIT_OWNER_TYPE ?? "builder";
17583
- const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? process.env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
17584
- const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? process.env.DVMKIT_OWNER_AVATAR_URL ?? null;
17728
+ const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
17729
+ const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
17730
+ const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
17585
17731
  return { handle, display_name: displayName, avatar_url: avatarUrl, type };
17586
17732
  }
17587
17733
  defaultPaymentMethods() {
@@ -17600,14 +17746,13 @@ var DVMServer = class {
17600
17746
  if (!this.opts.x402.batchSettlement?.selfRelay) return support;
17601
17747
  return { ...support, batchSettlement: true };
17602
17748
  }
17603
- /** Whether this DVM can state its price exactly in x402's USDC unit. */
17604
17749
  x402CurrencySupported() {
17605
17750
  return this.pricingCurrency === "usd";
17606
17751
  }
17607
17752
  /**
17608
17753
  * Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
17609
17754
  * override and otherwise deriving from wired rails. Single source of truth so
17610
- * the `/v1/info` and `/v1/quote` rail enumerations can't drift (internal-review).
17755
+ * the `/v1/info` and `/v1/quote` rail enumerations can't drift.
17611
17756
  */
17612
17757
  resolvePaymentMethods() {
17613
17758
  const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
@@ -17620,13 +17765,13 @@ var DVMServer = class {
17620
17765
  }
17621
17766
  /**
17622
17767
  * Rails a **credit top-up** may arrive on — the funding menu's list
17623
- * (internal-review). A superset of {@link resolvePaymentMethods}, and deliberately
17768
+ * A superset of {@link resolvePaymentMethods}, and deliberately
17624
17769
  * a separate read: `lightning` funds a credit but is not an attached-proof
17625
17770
  * wire method, so it must never reach `/v1/info` or `/v1/quote`'s
17626
- * `payment.methods` (internal-review stands).
17771
+ * `payment.methods`.
17627
17772
  *
17628
- * The rail drops out when the receive wallet is unreachable (spec §4 menu
17629
- * degradation) — advertising it then would hand the caller an option that
17773
+ * The rail drops out of the funding menu when the receive wallet is unreachable;
17774
+ * advertising it then would hand the caller an option that
17630
17775
  * fails on use, while the other rails would have closed the sale.
17631
17776
  */
17632
17777
  resolveFundingMethods() {
@@ -17645,7 +17790,7 @@ var DVMServer = class {
17645
17790
  }
17646
17791
  /**
17647
17792
  * The credit menu's MPP instruments, split into what is on offer right now
17648
- * and what this DVM is configured for but withholding (internal-review).
17793
+ * and what this DVM is configured for but withholding.
17649
17794
  *
17650
17795
  * The split has to survive into the response rather than being filtered
17651
17796
  * away, because "not offered right now" and "not configured" are different
@@ -17673,7 +17818,7 @@ var DVMServer = class {
17673
17818
  }
17674
17819
  /**
17675
17820
  * Which x402 flavours a credit top-up may arrive on — the funding menu's
17676
- * `x402` sub-block (internal-review). Hosted health buys `exact`; a genuinely wired
17821
+ * `x402` sub-block. Hosted health buys `exact`; a genuinely wired
17677
17822
  * batch server buys `batch-settlement` independently, including while the
17678
17823
  * hosted facilitator is unavailable or stale.
17679
17824
  *
@@ -17690,13 +17835,12 @@ var DVMServer = class {
17690
17835
  ...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
17691
17836
  ];
17692
17837
  }
17693
- /** Whether a channel server is both supported and constructible for credit funding. */
17694
17838
  x402BatchAvailable() {
17695
17839
  return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
17696
17840
  }
17697
17841
  /**
17698
17842
  * Apply any Lightning funding invoice this caller has paid since we last
17699
- * looked — the pull half of pull-based crediting (internal-review, spec §4).
17843
+ * looked — the pull half of pull-based crediting.
17700
17844
  *
17701
17845
  * Called from every authenticated surface where the balance is about to
17702
17846
  * matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
@@ -17709,7 +17853,7 @@ var DVMServer = class {
17709
17853
  * outstanding, and never blocks the request when it fails.
17710
17854
  *
17711
17855
  * The SDK-owned reporter queues each Lightning deposit inside
17712
- * `settleInvoice`'s transaction (internal-review). The loop below only preserves
17856
+ * `settleInvoice`'s transaction. The loop below only preserves
17713
17857
  * the explicit `onCreditFunded` override, whose caller owns its durability.
17714
17858
  */
17715
17859
  async settleLightningFunding(callerPubkey, creditId) {
@@ -17740,7 +17884,6 @@ var DVMServer = class {
17740
17884
  });
17741
17885
  }
17742
17886
  }
17743
- /** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
17744
17887
  creditTtlMs() {
17745
17888
  return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
17746
17889
  }
@@ -17761,7 +17904,7 @@ var DVMServer = class {
17761
17904
  * The per-DVM + per-request rail wiring every payment verification needs —
17762
17905
  * everything except what the *operation* contributes (its price, its job or
17763
17906
  * fund id, the caller's credit fields). Shared by `/v1/job`'s upfront gate
17764
- * and `/v1/credit`'s top-up (internal-review) so a rail wired into one is wired
17907
+ * and `/v1/credit`'s top-up so a rail wired into one is wired
17765
17908
  * into both by construction; a divergence here would be a rail that funds
17766
17909
  * jobs but silently can't fund credits.
17767
17910
  */
@@ -17794,7 +17937,7 @@ var DVMServer = class {
17794
17937
  mppCredential,
17795
17938
  mpp: this.opts.mpp,
17796
17939
  resourcePath: c.req.path,
17797
- // internal-review/internal-review: the gate covers issuance and new-channel acceptance,
17940
+ // the gate covers issuance and new-channel acceptance,
17798
17941
  // not just the two advertisement surfaces. `ensureFresh` above makes this
17799
17942
  // an observation rather than a cold-start default on the one surface that
17800
17943
  // can act on it.
@@ -17812,8 +17955,6 @@ var DVMServer = class {
17812
17955
  requestId: c.req.header("X-Cashu-Request-Id"),
17813
17956
  mintHealthTracker: this.mintHealthTracker,
17814
17957
  fxFetcher: this.fxFetcher,
17815
- // internal-review: the ledger leg — every rail payment becomes fund_credit +
17816
- // draw, atomic with the rail commit.
17817
17958
  creditLedger: this.creditLedger,
17818
17959
  processedPayments: this.processedPayments,
17819
17960
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
@@ -17823,7 +17964,7 @@ var DVMServer = class {
17823
17964
  }
17824
17965
  /**
17825
17966
  * Assemble this DVM's funding menu for a response denominated in
17826
- * `currency` (internal-review). All the refusal logic — including the credits-spec
17967
+ * `currency`. All the refusal logic — including the credits-spec
17827
17968
  * §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
17828
17969
  * server's state. `callerPubkey` must be a *verified* envelope pubkey: the
17829
17970
  * balance echo is per-caller money and there is no safe fallback identity.
@@ -17832,8 +17973,6 @@ var DVMServer = class {
17832
17973
  await this.ensureTempoSessionHealth();
17833
17974
  const mppSplit = this.creditMppSplit();
17834
17975
  return buildCreditMenu({
17835
- // The SDK server only ever runs the container runtime; the isolate route
17836
- // calls the same builder with `"isolate"` and is refused there.
17837
17976
  runtime: "container",
17838
17977
  config: this.descriptor.credit,
17839
17978
  currency,
@@ -17842,7 +17981,7 @@ var DVMServer = class {
17842
17981
  tempoWithheld: mppSplit.withheld,
17843
17982
  x402Schemes: this.creditX402Schemes(),
17844
17983
  lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
17845
- // Configured, not advertised (internal-review): `advertisedMints()` is health
17984
+ // Configured, not advertised: `advertisedMints()` is health
17846
17985
  // filtered, and a rail that comes and goes with a mint's uptime is worse
17847
17986
  // product than one that is honestly absent.
17848
17987
  hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
@@ -17879,7 +18018,7 @@ var DVMServer = class {
17879
18018
  }
17880
18019
  /**
17881
18020
  * Currency this DVM's credit ledger is denominated in — the builder's
17882
- * declared pricing currency, `"usd"` unless they said otherwise (internal-review).
18021
+ * declared pricing currency, `"usd"` unless they said otherwise.
17883
18022
  *
17884
18023
  * Every denomination surface reads this one answer: the funding menu on
17885
18024
  * `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
@@ -17888,20 +18027,16 @@ var DVMServer = class {
17888
18027
  * static (USD-literal) price on a non-USD DVM, and a quote denominated
17889
18028
  * elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
17890
18029
  * is what a top-up funded in, on every path.
17891
- *
17892
- * It used to return the literal `"usd"`, which is how a EUR-pricing DVM
17893
- * came to fund `/v1/credit` top-ups in a denomination none of its jobs
17894
- * would ever draw from.
17895
18030
  */
17896
18031
  creditCurrency() {
17897
18032
  return this.pricingCurrency;
17898
18033
  }
17899
18034
  /**
17900
- * Quote-response `payment` block (internal-review). Carries the same `methods` array
18035
+ * Quote-response `payment` block. Carries the same `methods` array
17901
18036
  * `/v1/info` advertises plus a per-rail `refundable` map keyed by method, so a
17902
18037
  * caller can see which rails return funds on failure before committing. The
17903
18038
  * `methods` shape stays identical to `/v1/info` and the platform isolate route
17904
- * (post-internal-review nested `payment.methods`); refundability rides alongside.
18039
+ * (current nested `payment.methods`); refundability rides alongside.
17905
18040
  */
17906
18041
  quotePaymentBlock() {
17907
18042
  const methods = this.resolvePaymentMethods();
@@ -17910,8 +18045,6 @@ var DVMServer = class {
17910
18045
  refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
17911
18046
  };
17912
18047
  }
17913
- // ── Route handlers ──────────────────────────────────────────────────
17914
- /** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
17915
18048
  async handleProxy(c) {
17916
18049
  const raw = getCookie(c, "dvmkit_target");
17917
18050
  const target = raw ? decodeURIComponent(raw) : null;
@@ -17947,8 +18080,8 @@ var DVMServer = class {
17947
18080
  auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
17948
18081
  ...this.authAudience ? { auth_audience: this.authAudience } : {},
17949
18082
  protocol_version: 1,
17950
- sdk_sha: process.env.GIT_SHA ?? "unknown",
17951
- deploy_id: process.env.DVMKIT_DEPLOY_ID ?? "unknown"
18083
+ sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
18084
+ deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17952
18085
  };
17953
18086
  const ownerBlock = this.ownerDisplayBlock();
17954
18087
  if (ownerBlock) info.owner = ownerBlock;
@@ -17969,8 +18102,7 @@ var DVMServer = class {
17969
18102
  network: this.opts.x402.network,
17970
18103
  asset: this.opts.x402.asset ?? "USDC",
17971
18104
  ...this.opts.x402.facilitator && { facilitator: this.opts.x402.facilitator },
17972
- // Surface dual-serve state so x402 clients can confirm the 402 will
17973
- // carry their PaymentRequirements before submitting (internal-review).
18105
+ // Advertises whether 402 challenges include x402 PaymentRequirements.
17974
18106
  dual_serve: this.opts.x402.dualServe ?? true
17975
18107
  };
17976
18108
  }
@@ -17981,11 +18113,11 @@ var DVMServer = class {
17981
18113
  return reason ? [{ ...method, reason }] : [];
17982
18114
  });
17983
18115
  info.tempo = {
17984
- // internal-review: withheld sessions are withheld here too. `/v1/info` is
18116
+ // withheld sessions are withheld here too. `/v1/info` is
17985
18117
  // where a caller decides what to attempt, so advertising a session the
17986
18118
  // credit menu will refuse just sends them down a dead end.
17987
18119
  methods: configured.filter((method) => this.tempoSessionOffered(method)),
17988
- // …but say so rather than letting it vanish (internal-review): an agent that
18120
+ // …but say so rather than letting it vanish: an agent that
17989
18121
  // sees only the shortened list cannot tell a DVM that never did
17990
18122
  // sessions from one whose observer is down for a minute.
17991
18123
  ...withheld.length && { withheld },
@@ -18017,7 +18149,7 @@ var DVMServer = class {
18017
18149
  return c.json(info);
18018
18150
  }
18019
18151
  /**
18020
- * Render the `/v1/info.capabilities` block (internal-review). Each capability
18152
+ * Render the `/v1/info.capabilities` block. Each capability
18021
18153
  * advertises its `description`, optional `input_schema`, and optional
18022
18154
  * `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
18023
18155
  * `pricing.max` for static-priced ones. Capabilities without an `input`
@@ -18227,7 +18359,7 @@ var DVMServer = class {
18227
18359
  * the two callers — `/v1/quote` and `computeDynamicPrice` on the submit
18228
18360
  * path — cannot drift: a field added for one is present in the other by
18229
18361
  * construction, and both paths price against the same view of the caller's
18230
- * credit (internal-review, spec §7).
18362
+ * credit.
18231
18363
  */
18232
18364
  async buildQuoteContext(args) {
18233
18365
  const ctx = {
@@ -18250,15 +18382,15 @@ var DVMServer = class {
18250
18382
  }
18251
18383
  /**
18252
18384
  * Validate the `capability` field on a `/v1/job` or `/v1/quote` body
18253
- * (internal-review). Returns the canonical name when set + known; otherwise a
18385
+ * Returns the canonical name when set + known; otherwise a
18254
18386
  * structured 400 envelope agents can relay verbatim to a human caller.
18255
18387
  *
18256
18388
  * - **Missing** — `body.capability` is absent or empty. Surface as
18257
- * `capability_required` with a hint pointing at `/v1/info` so the agent
18258
- * can re-discover the capability set.
18389
+ * `capability_required` with a hint pointing at `/v1/info` so the agent
18390
+ * can re-discover the capability set.
18259
18391
  * - **Unknown** — `body.capability` is set but doesn't match any
18260
- * capability on this descriptor. Surface as `unknown_capability` listing
18261
- * the known names so the agent can correct without a round-trip.
18392
+ * capability on this descriptor. Surface as `unknown_capability` listing
18393
+ * the known names so the agent can correct without a round-trip.
18262
18394
  */
18263
18395
  requireCapabilityFromBody(c, capability) {
18264
18396
  const known = this.jobManager.capabilityNames();
@@ -18317,25 +18449,9 @@ var DVMServer = class {
18317
18449
  }
18318
18450
  }
18319
18451
  /**
18320
- * The one `invalid_input` body `/v1/quote` and `/v1/job` emit for a
18321
- * capability-input violation (internal-review).
18322
- *
18323
- * The two endpoints used to answer the same caller mistake with two unrelated
18324
- * shapes — quote gated on auth first, so a wrong field name came back 401
18325
- * `auth_error` + `sub_reason: "schema_invalid"`, while submit parsed first and
18326
- * came back 400 `invalid_input`. Both named the offending field (internal-review), so
18327
- * neither was wrong; an agent just had to recognise two codes and two statuses
18328
- * for one class of mistake, and the CLI's `parseStructuredError` mapped them to
18329
- * two different `DvmError`s — which is why its structured-input remediation
18330
- * (`invalid_input` only) never fired for the live fleet failure internal-review was
18331
- * written about.
18332
- *
18333
- * The 400 wins because it is the only shape both endpoints can produce: a DVM
18334
- * that declares no `auth` has no 401 to give, and quote didn't validate
18335
- * `cap.input` at all on that path, so it answered 200 to a body submit would
18336
- * reject. `display` is carried here as well as `message` because this is now
18337
- * the shape agents branch on, and it reuses the 401's sentence so the words
18338
- * relayed to a human don't change with the status.
18452
+ * Shared schema-violation response for quote and submit, including auth-less
18453
+ * DVMs. A 400 classifies caller input without requiring an auth gate; `display`
18454
+ * and `message` carry the same relayable explanation.
18339
18455
  */
18340
18456
  invalidInputResponse(c, err) {
18341
18457
  const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
@@ -18379,13 +18495,13 @@ var DVMServer = class {
18379
18495
  }
18380
18496
  /**
18381
18497
  * True when a failed capability-input parse is the auth gate's answer rather
18382
- * than {@link invalidInputResponse}'s (internal-review).
18498
+ * than {@link invalidInputResponse}'s.
18383
18499
  *
18384
18500
  * A capability schema may declare the signing-envelope fields required
18385
18501
  * (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
18386
18502
  * the required shape an *unsigned* request fails the input parse naming
18387
18503
  * `pubkey` / `signature` — a signing failure, and answering it with "your
18388
- * input is invalid: pubkey required" is the internal-review mistake in reverse. So a
18504
+ * input is invalid: pubkey required" is the mistake in reverse. So a
18389
18505
  * parse whose issues are **only** envelope-side falls through to the auth
18390
18506
  * gate, which answers 401 `signature_invalid` with the remedy that works.
18391
18507
  *
@@ -18402,13 +18518,13 @@ var DVMServer = class {
18402
18518
  }
18403
18519
  /**
18404
18520
  * Mints to advertise to clients on `/v1/info` and `/v1/quote`. Filters the
18405
- * configured list through the runtime health tracker (internal-review). When every
18521
+ * configured list through the runtime health tracker. When every
18406
18522
  * mint is currently sick it falls back to the still-configured list rather
18407
18523
  * than publish an empty rail — a *transient* outage shouldn't read as "no
18408
18524
  * Cashu" and push well-behaved agents to bypass the rail. But NUT-incompatible
18409
- * mints are excluded even from that fallback (internal-review): NUT-incompatibility
18525
+ * mints are excluded even from that fallback: NUT-incompatibility
18410
18526
  * is a permanent, deterministic config error the DVM can never settle
18411
- * against, so per AC bullet 3 such a mint is never advertised. If *every*
18527
+ * against, so such a mint is never advertised. If *every*
18412
18528
  * configured mint is NUT-incompatible the rail correctly goes empty.
18413
18529
  */
18414
18530
  advertisedMints() {
@@ -18837,9 +18953,9 @@ var DVMServer = class {
18837
18953
  priceMsats,
18838
18954
  capabilityName,
18839
18955
  requesterTokenHash,
18840
- // internal-review: everything the idempotent-replay path needs to recognise a
18956
+ // everything the idempotent-replay path needs to recognise a
18841
18957
  // retry of THIS call and re-issue the token its lost response carried.
18842
- // internal-review: `jobId` is the pre-allocated id the payment's ledger draw
18958
+ // `jobId` is the pre-allocated id the payment's ledger draw
18843
18959
  // already committed under.
18844
18960
  {
18845
18961
  requesterToken: jobToken,
@@ -18878,7 +18994,7 @@ var DVMServer = class {
18878
18994
  }
18879
18995
  }
18880
18996
  /**
18881
- * Book a committed funding as a deposit (internal-review, spec §10). Fire-and-forget
18997
+ * Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
18882
18998
  * onto the reporter's durable queue: a funding is a liability the platform
18883
18999
  * must show, but a reporting hiccup must never fail the paid request that
18884
19000
  * just succeeded.
@@ -18893,7 +19009,7 @@ var DVMServer = class {
18893
19009
  );
18894
19010
  }
18895
19011
  /**
18896
- * Book a kept short payment as tagged revenue (internal-review, operator ruling).
19012
+ * Book a kept short payment as tagged revenue.
18897
19013
  * The proofs are committed and the draw was settled on the spot, so the money
18898
19014
  * is earned — but no job exists to carry it, hence the synthetic job id. It
18899
19015
  * is deliberately **not** a deposit: nothing is owed back, so a liability
@@ -18923,7 +19039,7 @@ var DVMServer = class {
18923
19039
  });
18924
19040
  }
18925
19041
  /**
18926
- * The idempotent half of `POST /v1/job` (internal-review). A paid submit whose
19042
+ * The idempotent half of `POST /v1/job`. A paid submit whose
18927
19043
  * response the caller lost is safe to retry — the accumulator's
18928
19044
  * `UNIQUE(dvm_id, request_id, proof_secret)` makes the duplicate a no-op —
18929
19045
  * but until now it was also useless: the caller got a 409 and the `job_id` /
@@ -18933,7 +19049,7 @@ var DVMServer = class {
18933
19049
  * original caller. Proof is reproducing the request, **not** possessing the
18934
19050
  * spent Cashu token: the token is a bearer secret, so gating on it alone
18935
19051
  * would hand the job (and its result) to anyone who lifted it. The legitimate
18936
- * retry always has the body — the internal-review resume sweep re-POSTs it — while a
19052
+ * retry always has the body — the resume sweep re-POSTs it — while a
18937
19053
  * token thief does not. On a descriptor-auth DVM the caller pubkey must match
18938
19054
  * too. Anything short of that falls through to the 409 the caller has always
18939
19055
  * gotten.
@@ -18960,8 +19076,8 @@ var DVMServer = class {
18960
19076
  return buildSubmitResponse(c, prior, prior.requesterToken);
18961
19077
  }
18962
19078
  /**
18963
- * The draw-keyed half of idempotent recovery (internal-review, spec §2 condition
18964
- * 1). The signed body's `(credit_id, draw_id)` matched a recorded draw;
19079
+ * The draw-keyed half of idempotent recovery. The signed body's
19080
+ * `(credit_id, draw_id)` matched a recorded draw;
18965
19081
  * resolve the job it paid for (the draw carries its pre-allocated job id)
18966
19082
  * and re-issue the original response under the SAME proof-of-origin gates
18967
19083
  * as the header regime: reproduce the fingerprint, sign as the same caller
@@ -19050,7 +19166,6 @@ var DVMServer = class {
19050
19166
  }
19051
19167
  return void 0;
19052
19168
  }
19053
- /** Recover one still-pending draw recorded for the stable public job id. */
19054
19169
  async recoverDrawAcceptance(opts) {
19055
19170
  const dvmId = this.opts.dvmId;
19056
19171
  if (!dvmId) return void 0;
@@ -19082,7 +19197,7 @@ var DVMServer = class {
19082
19197
  /**
19083
19198
  * Recompute the upfront price for a dynamic-priced capability by
19084
19199
  * re-invoking its `onQuote.handler` with the parsed job input as `data`
19085
- * (internal-review). Returns a 400-style error envelope when the input doesn't
19200
+ * Returns a 400-style error envelope when the input doesn't
19086
19201
  * satisfy `onQuote.schema` or the handler throws — failing closed is the
19087
19202
  * right default, since the alternative (running the job with
19088
19203
  * `requiredMsats=undefined`) lets unpaid jobs through.
@@ -19291,13 +19406,6 @@ var DVMServer = class {
19291
19406
  }
19292
19407
  return c.json({ messages: job.messages.filter((m) => m.seq > after) });
19293
19408
  }
19294
- /**
19295
- * Structured 409 for a write (message POST / cancel) against a job that has
19296
- * already reached a terminal status (internal-review). The two-audiences convention
19297
- * wants a `code` an intermediary agent can branch on plus a `hint` it can
19298
- * relay verbatim — a bare `{ error }` string gave the openclaw nothing to act
19299
- * on and it re-paid a completed job into this 409 (internal-review step 7).
19300
- */
19301
19409
  jobNotActiveResponse(c) {
19302
19410
  return c.json(
19303
19411
  {
@@ -19470,8 +19578,8 @@ var DVMServer = class {
19470
19578
  }
19471
19579
  /**
19472
19580
  * Tear down this machine's in-memory reactivation scaffold when it stands
19473
- * down (lost the reactivation advisory lock, or deferred to a live original
19474
- * handler that won the single-execution claim — internal-review / internal-review). Leaving
19581
+ * down after losing the reactivation advisory lock or deferring to a live original
19582
+ * handler that won the single-execution claim. Leaving
19475
19583
  * the job in `activeJobs` with a live idle timer would, one idle window
19476
19584
  * later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
19477
19585
  * must not persist our stale snapshot; clearing the appender prevents that.
@@ -19507,6 +19615,7 @@ var DVMServer = class {
19507
19615
  await jm.finalizeStoredTerminal(id);
19508
19616
  return c.json({ status: "cancelled" });
19509
19617
  }
19618
+ record.endedBy = "caller";
19510
19619
  record.status = "cancelled";
19511
19620
  record.lastActivityAt = Date.now();
19512
19621
  await jm.store.save(record);
@@ -19852,7 +19961,7 @@ var PlatformTempoObserverHealth = class {
19852
19961
  }
19853
19962
  }
19854
19963
  /**
19855
- * Register one mount's session capability (internal-review).
19964
+ * Register one mount's session capability.
19856
19965
  *
19857
19966
  * A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
19858
19967
  * platform row that token identifies is the deployment as a whole — so the
@@ -19875,7 +19984,7 @@ var PlatformTempoObserverHealth = class {
19875
19984
  * mount exists, so at boot there is routinely an open probe carrying **no**
19876
19985
  * `tempo_session` at all — handing it back to the mount that just declared
19877
19986
  * one would leave a scale-to-zero DVM unstamped until traffic arrived, which
19878
- * is exactly the container-restart case internal-review is about. Same shape with
19987
+ * is exactly the container-restart case is about. Same shape with
19879
19988
  * two mounts: the second would join a probe whose URL already read the first
19880
19989
  * one's `false`.
19881
19990
  */
@@ -19923,7 +20032,7 @@ var PlatformTempoObserverHealth = class {
19923
20032
  }
19924
20033
  /**
19925
20034
  * The probe URL, carrying this deployment's session capability when a mount
19926
- * has declared one (internal-review).
20035
+ * has declared one.
19927
20036
  *
19928
20037
  * Absent means "no signal", not "no sessions": a container running an older
19929
20038
  * SDK build sends nothing, and the platform must leave its stamp exactly