@dvmkit/sdk 0.1.5-rc.9 → 0.2.1-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 (67) hide show
  1. package/README.md +14 -0
  2. package/dist/{chunk-MLRCSJYX.js → chunk-2D75MNNG.js} +178 -550
  3. package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
  4. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  5. package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-AJNFXR7J.js} +2 -2
  7. package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
  8. package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
  9. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  10. package/dist/{chunk-TQWGQCNV.js → chunk-FPOLJRBF.js} +5 -5
  11. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  12. package/dist/{chunk-SSSZUVWM.js → chunk-JAHRF5X3.js} +89 -39
  13. package/dist/{chunk-5PBOA25N.js → chunk-JNKQZVZX.js} +472 -617
  14. package/dist/{chunk-U6M3ATSG.js → chunk-MD3H3BF7.js} +40 -6
  15. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  16. package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
  17. package/dist/{chunk-JZWELPFH.js → chunk-N5B66UOS.js} +4 -16
  18. package/dist/{chunk-BIP6G74V.js → chunk-O6D72KYA.js} +10 -10
  19. package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-KVEHHC7W.js → chunk-U5VNEALI.js} +24 -13
  22. package/dist/{chunk-FROTD5XQ.js → chunk-V5ZRATJH.js} +2 -3
  23. package/dist/{chunk-EVBK675R.js → chunk-VA7GT6C6.js} +31 -37
  24. package/dist/{chunk-2UUXIIOC.js → chunk-VIVXB4CD.js} +3 -3
  25. package/dist/{chunk-6BQM7TOW.js → chunk-XMDQOXHO.js} +11 -38
  26. package/dist/{chunk-DMNLFNTW.js → chunk-XRLKGCAW.js} +2 -2
  27. package/dist/{chunk-M7LHFJ5K.js → chunk-XZ7LW4S5.js} +2 -3
  28. package/dist/{chunk-E4EVGPDX.js → chunk-YO56VCTQ.js} +0 -6
  29. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-MHA652TV.js} +2 -2
  30. package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-kDA9RVzD.d.ts} +524 -1077
  31. package/dist/{fx-D860pZvP.d.ts → fx-DhyT1Cjh.d.ts} +38 -82
  32. package/dist/index.d.ts +11 -26
  33. package/dist/index.js +2 -2
  34. package/dist/internal/caller.d.ts +644 -2140
  35. package/dist/internal/caller.js +32 -66
  36. package/dist/internal/server.d.ts +40 -90
  37. package/dist/internal/server.js +16 -16
  38. package/dist/{job-store-BUGqvCfL.d.ts → job-store-D3CcZp_g.d.ts} +73 -66
  39. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-BhsyrBLa.d.ts} +19 -41
  40. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-BYBATRN3.js} +3 -3
  41. package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
  42. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-MPP3ST5X.js} +1 -1
  43. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-KEMUXEUO.js} +2 -2
  44. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-6N2V5MJN.js} +2 -2
  45. package/dist/postgres-job-store-MZZZMS2B.js +7 -0
  46. package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
  47. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
  48. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  49. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-GKMAWOM3.js} +2 -2
  50. package/dist/revenue-reporter-O75BGYFK.js +7 -0
  51. package/dist/server/index.d.ts +47 -159
  52. package/dist/server/index.js +67 -138
  53. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  54. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-v2yhyhSZ.d.ts} +617 -1532
  55. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-SDTUNTKA.js} +2 -2
  56. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-G5SB4HFC.js} +5 -5
  57. package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
  58. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-ZFNUDFVO.js} +2 -2
  59. package/dist/testing/index.d.ts +5 -15
  60. package/dist/testing/index.js +4 -11
  61. package/dist/{usd-DoRuAckA.d.ts → usd-Cu9G6xv2.d.ts} +14 -16
  62. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  63. package/dist/{x402-5H27DCBE.js → x402-CI6PSHNO.js} +5 -5
  64. package/package.json +3 -2
  65. package/dist/postgres-job-store-3RAXMNSY.js +0 -7
  66. package/dist/postgres-kv-store-JFBDP5IP.js +0 -7
  67. package/dist/revenue-reporter-ASZ7SHHH.js +0 -7
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryProcessedPaymentStore,
3
3
  ProcessedPaymentReplayError
4
- } from "./chunk-DMNLFNTW.js";
4
+ } from "./chunk-XRLKGCAW.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-U5VNEALI.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-MD3H3BF7.js";
39
39
  import {
40
40
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
41
41
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -60,7 +60,7 @@ import {
60
60
  toLockPubkey,
61
61
  verifyCashuToken,
62
62
  verifyChallenge
63
- } from "./chunk-KXCKFIJI.js";
63
+ } from "./chunk-QX52F6IB.js";
64
64
  import {
65
65
  X402_BATCH_SETTLEMENT_SCHEME,
66
66
  X402_DEFAULT_FACILITATOR,
@@ -83,7 +83,7 @@ import {
83
83
  msatsToUsd,
84
84
  msatsToUsdc,
85
85
  usdcToMsats
86
- } from "./chunk-5URG56JJ.js";
86
+ } from "./chunk-A4CM2BD3.js";
87
87
  import {
88
88
  resolveRpcOverride
89
89
  } from "./chunk-MKI6OVW4.js";
@@ -100,7 +100,7 @@ import {
100
100
  pinDimension,
101
101
  withSpan,
102
102
  withTraceContext
103
- } from "./chunk-E4EVGPDX.js";
103
+ } from "./chunk-YO56VCTQ.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,10 +130,10 @@ import {
130
130
  isNonChannelBitcoinRail,
131
131
  netOwedSats,
132
132
  x402SettlementPending
133
- } from "./chunk-MLRCSJYX.js";
133
+ } from "./chunk-2D75MNNG.js";
134
134
  import {
135
135
  withSdkInitLock
136
- } from "./chunk-S3XAHZQY.js";
136
+ } from "./chunk-COYKJRMM.js";
137
137
  import {
138
138
  withPostgresRecoveryContext
139
139
  } from "./chunk-C3MTFLC6.js";
@@ -141,7 +141,7 @@ import {
141
141
  advertisedMethods,
142
142
  challengeMeta,
143
143
  isTempoInsufficientFundsError
144
- } from "./chunk-M7LHFJ5K.js";
144
+ } from "./chunk-XZ7LW4S5.js";
145
145
 
146
146
  // src/sdk/server/lock-pubkey-store.ts
147
147
  async function initLockPubkeyTable(db) {
@@ -881,22 +881,19 @@ async function buildCreditMenu(args) {
881
881
  ttl_ms: args.config.ttlSeconds * 1e3,
882
882
  currency,
883
883
  funding,
884
- // Rendered when either half has something to say: a DVM configured for
885
- // sessions only would otherwise lose the whole block the moment the gate
886
- // closed, taking its capability signal with it (internal-review).
884
+ // Keep withheld Tempo capability discoverable even when no method is currently offered.
887
885
  ...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
888
886
  tempo: {
889
887
  methods: tempoMethods,
890
888
  ...tempoWithheld.length > 0 && { withheld: tempoWithheld }
891
889
  }
892
890
  },
893
- // Tied to the rail's own presence, the way `lightning_min_micro` is below:
894
891
  // a builder who overrides `paymentMethods` can drop x402 from `funding`
895
892
  // while the rail stays wired, and terms for a rail the menu doesn't offer
896
893
  // are the same misadvertisement in the other direction.
897
894
  ...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
898
895
  };
899
- if (args.funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
896
+ if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
900
897
  try {
901
898
  const fx = await args.fxFetcher.fetch();
902
899
  menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
@@ -905,7 +902,7 @@ async function buildCreditMenu(args) {
905
902
  "credit_menu_lightning_floor_unavailable",
906
903
  `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
904
  );
908
- menu.funding = args.funding.filter((rail) => rail !== "lightning");
905
+ menu.funding = menu.funding.filter((rail) => rail !== "lightning");
909
906
  }
910
907
  }
911
908
  const echo = await selectPrimaryCredit({
@@ -1950,7 +1947,7 @@ async function createX402BatchSettlementServer(opts) {
1950
1947
  ...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
1951
1948
  // Every claim this fleet builds is capped at earned draws — the claim
1952
1949
  // timer, `claimAndSettle()`, and the withdraw-race rush claim all route
1953
- // through this selector (internal-review). A builder-supplied
1950
+ // through this selector. A builder-supplied
1954
1951
  // `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
1955
1952
  // a money invariant, not a policy knob. Upstream's refund job builds its
1956
1953
  // bundled claim from storage instead and never consults a selector, which
@@ -2071,14 +2068,13 @@ async function createX402BatchSettlementServer(opts) {
2071
2068
  // move money. The voucher base does not: a cooperatively drained and
2072
2069
  // reused channel can present the same cumulative maximum again under a
2073
2070
  // fresh authorization, which made the second legitimate deposit
2074
- // collide with the first row (internal-review). Conversely, keying by the
2071
+ // collide with the first row. Conversely, keying by the
2075
2072
  // credit effect would let one signed authorization open several rows.
2076
2073
  //
2077
2074
  // A cooperative refund belongs to one caller drain: after an operator
2078
2075
  // writes a failed drain off, the unchanged channel can legitimately
2079
2076
  // present the same voucher base under a fresh drain id. Keep that new
2080
2077
  // effect out of the old row's `(operation, payment_id)` replay slot
2081
- // (internal-review).
2082
2078
  paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
2083
2079
  amount: matched.amount
2084
2080
  };
@@ -3631,7 +3627,7 @@ async function verifyUpfrontPayment(opts) {
3631
3627
  // Nothing settled, so the EIP-3009 nonce is unspent and the same
3632
3628
  // header is still payable — unlike the post-settlement branch
3633
3629
  // below, where saying this would be telling the caller to pay
3634
- // twice (internal-review).
3630
+ // twice.
3635
3631
  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.`
3636
3632
  }
3637
3633
  );
@@ -3652,7 +3648,7 @@ async function verifyUpfrontPayment(opts) {
3652
3648
  x402Requirements
3653
3649
  });
3654
3650
  }
3655
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
3651
+ const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
3656
3652
  const receipt = await verifyX402Payment(
3657
3653
  x402Payment,
3658
3654
  x402Config,
@@ -3689,7 +3685,7 @@ async function verifyUpfrontPayment(opts) {
3689
3685
  `fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
3690
3686
  // `expected_micro` states the valuation, but carries no corrective
3691
3687
  // hint here: the facilitator already settled, so re-presenting
3692
- // this credential would be a second payment (internal-review).
3688
+ // this credential would be a second payment.
3693
3689
  { extra: { expected_micro: fundedMicro } }
3694
3690
  );
3695
3691
  }
@@ -4195,16 +4191,16 @@ var PAYMENT_ERROR_COPY = {
4195
4191
  // Only the Cashu accumulator emits this code, and it evaluates each call's
4196
4192
  // own proofs — nothing sums a partial with a later one, on any path. So the
4197
4193
  // retry owes the full amount: implicit upfront the shortfall was forfeited
4198
- // (internal-review), mid-job it sits on a credit the per-site hint names
4199
- // (internal-review). Explicit upfront short deposits use `insufficient_credit`
4200
- // instead (internal-review). Telling a caller to "send the difference" produced a
4194
+ // mid-job it sits on a credit the per-site hint names
4195
+ // Explicit upfront short deposits use `insufficient_credit`
4196
+ // instead. Telling a caller to "send the difference" produced a
4201
4197
  // second short pay either way.
4202
4198
  payment_insufficient: {
4203
4199
  display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
4204
4200
  hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
4205
4201
  retryable: true
4206
4202
  },
4207
- // internal-review: the rail and the ledger leg commit together, but the job row is
4203
+ // the rail and the ledger leg commit together, but the job row is
4208
4204
  // written by a *second* transaction (`verifyAndCredit`). When that one can't
4209
4205
  // be made to land, the money is real and the caller's — it just isn't on the
4210
4206
  // job. Say so rather than answering a bare 500: the per-site body names the
@@ -4251,7 +4247,7 @@ var PAYMENT_ERROR_COPY = {
4251
4247
  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>'.",
4252
4248
  retryable: false
4253
4249
  },
4254
- // internal-review: a refund on this credit's payment channel reached the chain and
4250
+ // a refund on this credit's payment channel reached the chain and
4255
4251
  // never finished booking, so the escrow behind the balance may already have
4256
4252
  // been paid back. Spending it would buy work the service can never collect
4257
4253
  // for. The copy must not promise the money is gone *or* that it is safe to
@@ -4283,7 +4279,7 @@ var PAYMENT_ERROR_COPY = {
4283
4279
  hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
4284
4280
  retryable: true
4285
4281
  },
4286
- // internal-review: a per-rail floor, not the menu-wide minimum — today only the
4282
+ // a per-rail floor, not the menu-wide minimum — today only the
4287
4283
  // lightning rail carries one (the receive channel's routing floor). Refused
4288
4284
  // before any invoice is minted, so nothing is ever paid against this code;
4289
4285
  // `retryable: false` because re-sending the identical request can never
@@ -4313,7 +4309,7 @@ var PAYMENT_ERROR_COPY = {
4313
4309
  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.",
4314
4310
  retryable: true
4315
4311
  },
4316
- // internal-review. Distinct from `nothing_to_drain` because the balance is real and
4312
+ // Distinct from `nothing_to_drain` because the balance is real and
4317
4313
  // waiting changes nothing: a reusable Bitcoin balance is a deposit the
4318
4314
  // service owes back in the asset it received, and what is left here converts
4319
4315
  // to less than one satoshi at the rate its own funding landed at. There is
@@ -4345,7 +4341,7 @@ var PAYMENT_ERROR_COPY = {
4345
4341
  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.",
4346
4342
  retryable: false
4347
4343
  },
4348
- // internal-review. The voucher verified and the ledger leg rolled back with the
4344
+ // The voucher verified and the ledger leg rolled back with the
4349
4345
  // failed submission, so nothing was charged and nothing about the payment is
4350
4346
  // wrong — which is exactly what the `payment_invalid` this replaced got
4351
4347
  // backwards, telling an agent to discard a credential that is still good. The
@@ -4422,7 +4418,7 @@ var PAYMENT_ERROR_COPY = {
4422
4418
  hint: "The token's proofs are pending at the mint; retry in a few seconds.",
4423
4419
  retryable: true
4424
4420
  },
4425
- // internal-review. Not failures — the two `lightning_*` states below are the normal
4421
+ // Not failures — the two `lightning_*` states below are the normal
4426
4422
  // shape of an interactive funding leg, which is why both are retryable and
4427
4423
  // neither `display` implies anything went wrong.
4428
4424
  lightning_settlement_pending: {
@@ -4442,7 +4438,7 @@ var PAYMENT_ERROR_COPY = {
4442
4438
  },
4443
4439
  replay_rejected: {
4444
4440
  display: "This payment was already submitted, so it wasn't charged again.",
4445
- // internal-review: this hint reaches a caller whose money already landed — the
4441
+ // this hint reaches a caller whose money already landed — the
4446
4442
  // accumulator holds these proofs under this request id. It used to read
4447
4443
  // "use a fresh token and a new request id for the next payment", which an
4448
4444
  // agent could follow straight into paying twice for the same charge.
@@ -4586,7 +4582,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4586
4582
  snapshot
4587
4583
  });
4588
4584
  }
4589
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
4585
+ const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
4590
4586
  const receipt = await verifyX402Payment(
4591
4587
  body.content.x402_payment,
4592
4588
  opts.x402Config,
@@ -5003,7 +4999,7 @@ async function processIncomingPayment(job, body, opts) {
5003
4999
  try {
5004
5000
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5005
5001
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5006
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
5002
+ const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
5007
5003
  const receipt = await verifyX402Payment(
5008
5004
  body.content.x402_payment,
5009
5005
  opts.x402Config,
@@ -5307,7 +5303,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5307
5303
  // requestId is non-null here because acceptAccumulatorPayment returns
5308
5304
  // kind:"rejected" with code "missing_request_id" when unset (handled by
5309
5305
  // the rejected branch above). Thread it as the per-call settlement
5310
- // reference for revenue_events.tx_hash (internal-review). Unique per
5306
+ // reference for revenue_events.tx_hash. Unique per
5311
5307
  // (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
5312
5308
  // it serves as a stable id even though it isn't an on-chain hash.
5313
5309
  paymentTxHash: opts.requestId,
@@ -5456,7 +5452,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5456
5452
  amount_micro: priceFiat.amountMicro,
5457
5453
  // Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
5458
5454
  // the live fetch landed without this currency and it was carried forward
5459
- // from an earlier one (internal-review). The first is the rate source down, the
5455
+ // from an earlier one. The first is the rate source down, the
5460
5456
  // second is the rate source having quietly stopped carrying a currency
5461
5457
  // this DVM advertises — a config problem, and a different repair.
5462
5458
  ...denomination.carriedForward === true && { rate_carried_forward: true },
@@ -5687,7 +5683,7 @@ async function fundAndDraw(ctx, funding, tx) {
5687
5683
  creditId: creditId2,
5688
5684
  drawId: drawId2,
5689
5685
  addAmountMicro: fundedMicro,
5690
- // internal-review: never grow the draw past what the job cumulatively asked.
5686
+ // never grow the draw past what the job cumulatively asked.
5691
5687
  // The available-balance guard inside `growDraw` cannot do this — the
5692
5688
  // `fund` above raised the balance by exactly this amount — so a second
5693
5689
  // payment for one ask would otherwise be absorbed and settle as revenue.
@@ -5800,7 +5796,7 @@ function creditDepositPayload(dvmId, funding) {
5800
5796
  creditCurrency: funding.currency,
5801
5797
  fundedAt: funding.fundedAt,
5802
5798
  expiryMs: funding.expiryMs,
5803
- // internal-review: label a session-funded deposit as one, whether or not the chain
5799
+ // label a session-funded deposit as one, whether or not the chain
5804
5800
  // binding below could be read. `rail: "tempo"` alone does not say — a
5805
5801
  // `tempo/charge` one-shot funds a credit with no channel at all — so this is
5806
5802
  // what lets the platform tell a routine charge from a session whose binding
@@ -5808,7 +5804,7 @@ function creditDepositPayload(dvmId, funding) {
5808
5804
  ...funding.tempoSession && {
5809
5805
  tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
5810
5806
  },
5811
- // internal-review: the channel registration rides the deposit report, so it
5807
+ // the channel registration rides the deposit report, so it
5812
5808
  // commits with the voucher acceptance that created the channel's liability.
5813
5809
  ...funding.tempoChannel && funding.tempoSession && {
5814
5810
  tempoChannel: {
@@ -6075,7 +6071,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
6075
6071
  receivedProofs: [],
6076
6072
  error: {
6077
6073
  // A fixed message plus the reason enum, never `err.message`: that string
6078
- // is upstream's, and on this PR's own case it is the relay lock holder's
6074
+ // is upstream's and may include the relay lock holder's
6079
6075
  // backend pid and `client_addr`. The detail is logged where the throw is.
6080
6076
  body: paymentErrorBody("x402_settlement_submission_failed", {
6081
6077
  message: "x402 batch settlement submission failed",
@@ -6674,7 +6670,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6674
6670
  /** Client-generated idempotency key — a retry with the same id credits once. */
6675
6671
  fund_id: z.string().min(1),
6676
6672
  /**
6677
- * SHA-256 hex over the raw funding artifact (spec §2 condition 3).
6673
+ * SHA-256 hex over the raw funding artifact (the funding-commitment rule).
6678
6674
  * Required for every attach rail; absent on `method: "lightning"`, which
6679
6675
  * carries no caller-supplied artifact to hash — see {@link CreditRequest}.
6680
6676
  */
@@ -6692,14 +6688,8 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6692
6688
  /** Client-generated idempotency key — the same id polls/collects one drain. */
6693
6689
  drain_id: z.string().min(1),
6694
6690
  /**
6695
- * Payout rail. Required on the first request; optional on a poll.
6696
- *
6697
- * Deliberately a bare string rather than the `DrainMethod` enum, so a
6698
- * rail this build does not carry is answered by `validateDrainTarget`
6699
- * with a typed `drain_method_unsupported` naming the rails that do
6700
- * work — not by a Zod parse failure that says only "invalid enum". The
6701
- * caller of a removed rail (`lightning`, internal-review) and the caller of a
6702
- * typo deserve the same actionable refusal.
6691
+ * Required on a fresh drain, optional on poll. Keep this a string so an unsupported
6692
+ * rail receives its specific typed refusal before payout-shape validation.
6703
6693
  */
6704
6694
  method: z.string().min(1).optional(),
6705
6695
  /**
@@ -6825,17 +6815,12 @@ async function handleBalance(c, deps, args) {
6825
6815
  const primary = pickPrimary(all, args.menu.currency);
6826
6816
  return c.json({
6827
6817
  op: "balance",
6828
- // Expired credits are included: expiry ends spending, never ownership
6829
- // (spec §5), so stranded value must stay visible to whoever owns it.
6818
+ // Keep expired credit records visible. Nominal expiry alone does not release ownership;
6819
+ // an expiry sweep can later zero the undrawn balance.
6830
6820
  credits: all.map(
6831
6821
  (credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
6832
6822
  ),
6833
6823
  ...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
6834
- // internal-review: the funding menu the quote and 402 surfaces already advertise,
6835
- // echoed here so one read answers "what do I hold, and what may I add".
6836
- // Under the key `menu`, not `credit` — that one is the credit projection on
6837
- // this route. Carried without the caller echo fields: `credits[]` above says
6838
- // the same thing in more detail, so `deps.menu()` is called without a pubkey.
6839
6824
  menu: args.menu
6840
6825
  });
6841
6826
  }
@@ -7114,11 +7099,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7114
7099
  resourcePath: c.req.path,
7115
7100
  action: "close",
7116
7101
  expectedChannelId,
7117
- // The close intent is re-asserted from inside the verifier, once the
7118
- // credential is bound to this credit's own channel and before anything
7119
- // broadcasts (internal-review). Run above the call instead and a credential
7120
- // signed for another channel touches the reservation on its way to a
7121
- // refusal that was never going to close anything.
7102
+ // Reassert the intent only after the verifier binds the credential to this channel, and
7103
+ // before broadcast. A wrong-channel signature cannot create or cancel its close intent.
7122
7104
  onBound: async () => {
7123
7105
  await beginClose(expectedChannelId, record.drainId);
7124
7106
  recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
@@ -7420,9 +7402,6 @@ async function handleX402ChannelDrain(c, deps, args) {
7420
7402
  amount: "0",
7421
7403
  operation: "refund",
7422
7404
  effectId: `${args.credit.creditId}:${args.drainId}`,
7423
- // The voucher the caller presents is what actually gets revoked and paid,
7424
- // so the settlement layer refuses one signed for any other channel before
7425
- // it touches anything (internal-review).
7426
7405
  expectedChannelId: channelId,
7427
7406
  refundBoundNative,
7428
7407
  commit: async (_funding, tx) => {
@@ -7641,12 +7620,9 @@ function drainBody(record) {
7641
7620
  status: record.status,
7642
7621
  amount_micro: record.amountMicro,
7643
7622
  currency: record.currency,
7644
- // The in-kind obligation, where there is one (internal-review): a Bitcoin
7645
- // reclaim returns the deposited sats pro rata, at the rate each funding
7646
- // landed at, so this figure was fixed when the reclaim was requested and
7647
- // does not move with the market. Published beside the fiat one rather than
7648
- // instead of it — the fiat figure is what the caller's balance was
7649
- // denominated in, and both are things an agent relays.
7623
+ // Fixed in-kind Bitcoin refund obligation after the delivery reserve. It reflects the
7624
+ // funding lots remaining at request time; null means no whole-balance in-kind price was
7625
+ // available.
7650
7626
  ...record.owedSats !== null ? { owed_sats: record.owedSats } : {},
7651
7627
  ledger_seq: record.ledgerSeq,
7652
7628
  created_at: record.createdAt,
@@ -7660,17 +7636,7 @@ function drainCopy(record) {
7660
7636
  case "pending":
7661
7637
  if (record.method === "cashu") {
7662
7638
  return {
7663
- display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." : (
7664
- // Named because it is the obligation, and because it is
7665
- // fixed: a Bitcoin refund returns the share of the Bitcoin you
7666
- // deposited that you never spent, so it does not move if the
7667
- // price does before you collect it (internal-review). The mint's own
7668
- // redemption charge is named rather than glossed — it is a sat
7669
- // or two at a fee-charging mint and nothing at a free one, and
7670
- // a figure an agent relays to a human should not be a promise
7671
- // that lands short.
7672
- `The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`
7673
- ),
7639
+ display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." : `The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`,
7674
7640
  hint: "Poll this op with the same drain_id until a token appears. Preparation can be delayed (e.g. the mint is temporarily down) \u2014 the refund stays owed to you either way."
7675
7641
  };
7676
7642
  }
@@ -7796,11 +7762,6 @@ async function handleFund(c, deps, args) {
7796
7762
  fundId: fund.fund_id,
7797
7763
  amountMicro: fund.amount_micro,
7798
7764
  menu,
7799
- // Lightning and MPP name their rail in the signed body. Header-attached
7800
- // Cashu and x402 are discriminated by `verifyUpfrontPayment`, which still
7801
- // refuses their mismatches before money moves. Lightning must be checked
7802
- // here because past this point the bolt11 is minted and payable; MPP is
7803
- // checked here so discovery never advertises the wrong Tempo instrument.
7804
7765
  rail: fund.method,
7805
7766
  tempoIntent: fund.method === "tempo" ? fund.intent : void 0
7806
7767
  });
@@ -8091,9 +8052,8 @@ function lightningInvoiceBlock(invoice) {
8091
8052
  invoice: invoice.bolt11,
8092
8053
  payment_hash: invoice.paymentHash,
8093
8054
  amount_msats: invoice.amountMsats,
8094
- // Named for its unit, like every other timestamp an agent reads off this
8095
- // API (`ttl_ms`, `expiry_ms`): bolt11 expiries are conventionally quoted
8096
- // in seconds, and a bare `expires_at` invites a reader to assume that.
8055
+ // Wire timestamp is milliseconds; bolt11 lifetimes are conventionally expressed in
8056
+ // seconds.
8097
8057
  expires_at_ms: invoice.expiresAt
8098
8058
  }
8099
8059
  };
@@ -8324,26 +8284,13 @@ var DRAIN_CONFLICT_COPY = {
8324
8284
  retryable: true,
8325
8285
  drainRegistered: false
8326
8286
  },
8327
- // Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
8328
- // channel rails, and two doors with two answers about one shortfall is the
8329
- // drift consolidating it was meant to prevent (internal-review). Tempo answers its
8330
- // own through `tempoFloorRefusal`, which reaches the same conclusion a step
8331
- // earlier and adds the one rail-specific fact — its channel is already closed.
8332
8287
  refund_floor: {
8333
8288
  display: "The refund reached the chain, but it paid less than the balance this service had recorded, so nothing was booked here. Your on-chain refund arrived; the difference has to be reconciled by the service operator.",
8334
8289
  hint: ({ creditId, drainId }) => `Report drain ${drainId} on credit ${creditId} to the service operator \u2014 only they can reconcile the difference. Retrying this drain, or requesting a new drain_id, lands here again.`,
8335
- // Terminal for the caller, unlike the two above: no amount of waiting or
8336
- // re-signing changes what the chain already paid.
8337
8290
  retryable: false,
8338
- // True even though the ledger transaction that would have written the drain
8339
- // row rolled back around this refusal. The wedged settlement row is recorded
8340
- // outside that transaction (internal-review) and holds this drain id as its
8341
- // `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
8342
- // drain under — so the caller has to keep it. Dropping it mints a fresh one
8343
- // that the wedged-channel gate refuses as `settlement_pending` anyway, and
8344
- // re-presenting *this* id is that gate's own documented exemption. Tempo
8345
- // reaches the same `true` one artifact earlier, off a `credit_drains` row
8346
- // its close-intent reservation already committed (internal-review).
8291
+ // The settlement evidence survives outside the rolled-back ledger transaction. Keep
8292
+ // drain_registered:true so the caller retains this effect ID for repair even when no
8293
+ // drain row committed.
8347
8294
  drainRegistered: true
8348
8295
  }
8349
8296
  };
@@ -8389,8 +8336,6 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
8389
8336
  message: error.message,
8390
8337
  display: copy?.display,
8391
8338
  hint: copy?.hint({ creditId, drainId }),
8392
- // An unclassified conflict keeps the duplicate-payout copy's own
8393
- // `retryable`, and the id it was never told anything about.
8394
8339
  retryable: copy?.retryable,
8395
8340
  extra: { drain_registered: copy?.drainRegistered ?? false }
8396
8341
  });
@@ -8441,8 +8386,8 @@ function creditBody(credit, reconciliationPending = false) {
8441
8386
  remaining_micro: credit.availableMicro,
8442
8387
  expiry_ms: credit.expiryMs,
8443
8388
  expired: credit.expired,
8444
- // Reclaimable via op "drain" (internal-review) — true whenever spendable-or-
8445
- // expired value remains. Expiry ends spending, never ownership.
8389
+ // Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
8390
+ // value.
8446
8391
  drainable: credit.status === "active" && credit.availableMicro > 0,
8447
8392
  ...reconciliationPending && { reconciliation_pending: true },
8448
8393
  ledger_seq: credit.lastLedgerSeq
@@ -8976,11 +8921,9 @@ var X402ExactSettlementServer = class {
8976
8921
  }
8977
8922
  return this.finalize(intent, args.btcUsdRate, args.commit);
8978
8923
  }
8979
- /** Read the operator queue. */
8980
8924
  list(args) {
8981
8925
  return this.opts.store.list(args);
8982
8926
  }
8983
- /** Read durable attempt evidence for one intent. */
8984
8927
  attempts(dvmId, intentId) {
8985
8928
  return this.opts.store.attempts(dvmId, intentId);
8986
8929
  }
@@ -9315,13 +9258,13 @@ function buildContext(job, opts) {
9315
9258
  if (terminated("prompt")) {
9316
9259
  return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
9317
9260
  }
9261
+ job.status = "awaiting-input";
9318
9262
  providerMessage(job, "prompt", {
9319
9263
  id,
9320
9264
  text,
9321
9265
  ...promptOpts?.options && { options: promptOpts.options },
9322
9266
  ...promptOpts?.schema && { schema: promptOpts.schema }
9323
9267
  });
9324
- job.status = "awaiting-input";
9325
9268
  if (job.replayHighSeq !== null) {
9326
9269
  const response = job.messages.find(
9327
9270
  (m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
@@ -9435,7 +9378,7 @@ function buildContext(job, opts) {
9435
9378
  poolBasis: pool.basis,
9436
9379
  askCurrency,
9437
9380
  // `draw` here means the pool read fine and either the ASK wouldn't pin
9438
- // — an fx outage on a bare-msats ask, or a sub-micro amount — or it
9381
+ // an fx outage on a bare-msats ask, or a sub-micro amount — or it
9439
9382
  // pinned in a currency this pool isn't denominated in.
9440
9383
  ...pool.basis === "draw" && { poolCurrency: pool.currency }
9441
9384
  });
@@ -9570,8 +9513,8 @@ function buildContext(job, opts) {
9570
9513
  ask_msats: amountMsats
9571
9514
  });
9572
9515
  }
9573
- providerMessage(job, "payment-request", paymentRequestContent);
9574
9516
  job.status = "awaiting-input";
9517
+ providerMessage(job, "payment-request", paymentRequestContent);
9575
9518
  const paidMsatsAtYield = job.paidMsats;
9576
9519
  const p = new Promise((resolve, reject) => {
9577
9520
  let resolved = false;
@@ -9580,12 +9523,24 @@ function buildContext(job, opts) {
9580
9523
  unsubscribeNotify?.();
9581
9524
  unsubscribeNotify = void 0;
9582
9525
  };
9583
- const doResolve = (pc) => {
9526
+ const doResolve = async (pc) => {
9584
9527
  if (resolved) return;
9585
9528
  resolved = true;
9586
9529
  cleanup();
9587
9530
  creditedToHandlerMsats += amountMsats;
9588
9531
  creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
9532
+ if (job.pendingPayment?.resolve === doResolve) {
9533
+ job.pendingPayment = null;
9534
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
9535
+ if (job.status === "awaiting-input" && opts.onYield) {
9536
+ try {
9537
+ await opts.onYield();
9538
+ } catch (err) {
9539
+ reject(err);
9540
+ return;
9541
+ }
9542
+ }
9543
+ }
9589
9544
  resolve(pc);
9590
9545
  };
9591
9546
  const doReject = (err) => {
@@ -9602,11 +9557,17 @@ function buildContext(job, opts) {
9602
9557
  if (resolved) return;
9603
9558
  try {
9604
9559
  const counters = await getCounters();
9605
- if (!counters) return;
9560
+ if (!counters || resolved) return;
9606
9561
  const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
9607
9562
  if (satisfied) {
9563
+ const resume = async () => {
9564
+ job.paidMsats = counters.paidMsats;
9565
+ job.pendingPaymentMsats = counters.pendingPaymentMsats;
9566
+ await doResolve({ amount_msats: amountMsats });
9567
+ };
9608
9568
  if (opts.claimForProcessing) {
9609
- const won = await opts.claimForProcessing();
9569
+ const won = await opts.claimForProcessing(resume);
9570
+ if (resolved) return;
9610
9571
  if (!won) {
9611
9572
  resolved = true;
9612
9573
  cleanup();
@@ -9614,10 +9575,7 @@ function buildContext(job, opts) {
9614
9575
  return;
9615
9576
  }
9616
9577
  }
9617
- job.paidMsats = counters.paidMsats;
9618
- job.pendingPaymentMsats = counters.pendingPaymentMsats;
9619
- if (job.status === "awaiting-input") job.status = "processing";
9620
- doResolve({ amount_msats: amountMsats });
9578
+ await resume();
9621
9579
  }
9622
9580
  } catch (err) {
9623
9581
  log.error("requestPayment NOTIFY check failed", { error: String(err) });
@@ -9668,12 +9626,12 @@ function buildContext(job, opts) {
9668
9626
  runTerminal();
9669
9627
  },
9670
9628
  // `failOpts.refund` is a caller-fault annotation and nothing more
9671
- // (internal-review). No rail hands value backwards: a failed job's debit is the
9629
+ // No rail hands value backwards: a failed job's debit is the
9672
9630
  // credit draw the terminal funnel releases, and the caller reclaims that
9673
- // balance through the internal-review `drain` op. The Cashu branch that used to
9631
+ // balance through the `drain` op. The Cashu branch that used to
9674
9632
  // send proofs back here was dead — `job.receivedProofs` has been empty on
9675
9633
  // every path since the accumulator became the commit boundary for inbound
9676
- // proofs (internal-review) — and internal-review, the accumulator-debit rewrite that would
9634
+ // proofs — and the accumulator-debit rewrite that would
9677
9635
  // have revived it, was cancelled as superseded by the ledger.
9678
9636
  fail: (error) => {
9679
9637
  if (terminated("fail")) return;
@@ -9742,16 +9700,10 @@ var ReceiptIssuer = class {
9742
9700
  this.pubkey = receiptPubkeyFromSecret(opts.secret);
9743
9701
  }
9744
9702
  /**
9745
- * Build and sign the receipt for a terminal job. `seq` comes from
9746
- * `JobStore.claimReceiptSeq`, `issuedAt` defaults to now (tests pin it).
9747
- *
9748
- * Free jobs get receipts too, with `paid.msats: 0` — they still consume a
9749
- * sequence number and still attest an outcome. Whether feedback is
9750
- * paid-only is the feedback layer's call, not this one's.
9751
- *
9752
- * `credit` is the resolved draw block (internal-review) — present on every job
9753
- * whose payment funded/drew the ledger, absent otherwise. Signature-
9754
- * compatible either way: `canonicalize` omits absent keys.
9703
+ * seq is store-allocated; issuedAt is epoch seconds. Free jobs also consume a
9704
+ * sequence and attest their outcome. Timing/attribution comes from the committed
9705
+ * terminal row; legacy rows omit unknown historical fields. credit is present
9706
+ * only for a resolved ledger draw.
9755
9707
  */
9756
9708
  issue(record, seq, issuedAt, credit) {
9757
9709
  const unsigned = {
@@ -9762,6 +9714,14 @@ var ReceiptIssuer = class {
9762
9714
  capability: record.capability,
9763
9715
  outcome: outcomeFor(record.status),
9764
9716
  reason: terminalReason(record),
9717
+ ...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
9718
+ timing: {
9719
+ accepted_at_ms: record.createdAt,
9720
+ terminal_at_ms: record.terminalAtMs,
9721
+ caller_wait_ms: record.callerWaitMs ?? 0
9722
+ }
9723
+ },
9724
+ ...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
9765
9725
  seq,
9766
9726
  issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
9767
9727
  requester_pubkey: record.requesterPubkey ?? null,
@@ -9780,7 +9740,7 @@ var ReceiptIssuer = class {
9780
9740
  return signReceipt(unsigned, this.secret);
9781
9741
  }
9782
9742
  /**
9783
- * Countersign one reclaim event (internal-review). Unlike job receipts there is
9743
+ * Countersign one reclaim event. Unlike job receipts there is
9784
9744
  * no store-allocated sequence — the drain's own `ledger_seq` (taken under
9785
9745
  * the credit row lock, shared with draws) already orders it in the
9786
9746
  * per-credit evidence chain.
@@ -9854,7 +9814,6 @@ function resultHashFor(record) {
9854
9814
  }
9855
9815
 
9856
9816
  // src/lib/cashu/mint-info.ts
9857
- var MINT_INFO_TTL_MS = 5 * 6e4;
9858
9817
  async function getKeysetFinalExpiry(mintUrl, keysetId) {
9859
9818
  try {
9860
9819
  const wallet = await getWallet(mintUrl);
@@ -10097,12 +10056,16 @@ var JobManager = class {
10097
10056
  cleanupTimers = /* @__PURE__ */ new Map();
10098
10057
  /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
10099
10058
  terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
10100
- /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
10059
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
10101
10060
  statusWatchers = /* @__PURE__ */ new Map();
10102
- /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
10061
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
10103
10062
  statusChecksInFlight = /* @__PURE__ */ new Set();
10104
- /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
10063
+ /** Job ids that were notified mid-re-read and must be re-checked. */
10105
10064
  statusChecksQueued = /* @__PURE__ */ new Set();
10065
+ // Every local input resume holds this queue through its durable wait update.
10066
+ // A prompt response and a payment NOTIFY must not mistake each other for a
10067
+ // different machine's successful claim.
10068
+ inputResumeTails = /* @__PURE__ */ new WeakMap();
10106
10069
  sessionId = randomBytes4(3).toString("hex");
10107
10070
  nextJobId = 1;
10108
10071
  accumulatorMonitor;
@@ -10110,7 +10073,7 @@ var JobManager = class {
10110
10073
  processingWatchdogMs;
10111
10074
  reactivationClaimGraceMs;
10112
10075
  /**
10113
- * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
10076
+ * Worker-heartbeat cadence. Also floors the clock
10114
10077
  * guard's compensation on the processing arm — see
10115
10078
  * {@link staleSweepCompensationCapMs}.
10116
10079
  */
@@ -10130,7 +10093,7 @@ var JobManager = class {
10130
10093
  jobRetentionResumeFrom;
10131
10094
  /**
10132
10095
  * A `Date.now()` reading that cannot regress within this manager's lifetime
10133
- * (internal-review). Anchored at construction, so a manager already running when
10096
+ * Anchored at construction, so a manager already running when
10134
10097
  * the clock stepped is covered. The stale-job sweep's two cutoffs, the
10135
10098
  * orphan-draw age gate and the reactivation claim grace all read it; see
10136
10099
  * {@link createMonotonicClock} for what it costs and where it is clamped.
@@ -10143,7 +10106,7 @@ var JobManager = class {
10143
10106
  jobRetentionSweepInFlight = false;
10144
10107
  heartbeatInFlight = false;
10145
10108
  /**
10146
- * The denomination of every credit this DVM opens (internal-review). Resolved once,
10109
+ * The denomination of every credit this DVM opens. Resolved once,
10147
10110
  * with the same boundary fallback `DVMServer` applies for direct JavaScript
10148
10111
  * callers that force an incomplete value past the descriptor contract.
10149
10112
  */
@@ -10158,7 +10121,7 @@ var JobManager = class {
10158
10121
  this.jobStore = new MemoryJobStore();
10159
10122
  } else {
10160
10123
  throw new Error(
10161
- "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."
10124
+ "JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL)."
10162
10125
  );
10163
10126
  }
10164
10127
  if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
@@ -10166,7 +10129,7 @@ var JobManager = class {
10166
10129
  JSON.stringify({
10167
10130
  level: "warn",
10168
10131
  event: "receipts_disabled_store_unsupported",
10169
- 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."
10132
+ 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."
10170
10133
  })
10171
10134
  );
10172
10135
  }
@@ -10265,7 +10228,7 @@ var JobManager = class {
10265
10228
  }
10266
10229
  }
10267
10230
  }
10268
- if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
10231
+ if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
10269
10232
  this.terminalCostRecoveryTimer = setInterval(() => {
10270
10233
  void this.recoverTerminalCosts().catch((err) => {
10271
10234
  console.error(
@@ -10279,16 +10242,14 @@ var JobManager = class {
10279
10242
  this.terminalCostRecoveryTimer.unref();
10280
10243
  }
10281
10244
  }
10282
- /** Active in-memory jobs. */
10283
10245
  get activeJobs() {
10284
10246
  return this._activeJobs;
10285
10247
  }
10286
- /** The backing job store. */
10287
10248
  get store() {
10288
10249
  return this.jobStore;
10289
10250
  }
10290
10251
  /**
10291
- * True when this DVM will actually issue receipts (internal-review) — a key *and*
10252
+ * True when this DVM will actually issue receipts — a key *and*
10292
10253
  * a store that can allocate sequence numbers and persist bytes write-once.
10293
10254
  * `/v1/info#receipts` reads this rather than the key alone, so the flag can
10294
10255
  * never promise something `issueReceipt` silently declines to do.
@@ -10296,16 +10257,14 @@ var JobManager = class {
10296
10257
  get receiptsEnabled() {
10297
10258
  return this.opts.receiptIssuer !== void 0 && isReceiptIssuingStore(this.jobStore);
10298
10259
  }
10299
- /** True when the named capability exists on this descriptor. */
10300
10260
  hasCapability(name) {
10301
10261
  return Object.prototype.hasOwnProperty.call(this.descriptor.capabilities, name);
10302
10262
  }
10303
- /** Names of every capability this descriptor exposes — used for error envelopes. */
10304
10263
  capabilityNames() {
10305
10264
  return Object.keys(this.descriptor.capabilities);
10306
10265
  }
10307
10266
  /**
10308
- * Resolve a capability's static `price` to msats (internal-review).
10267
+ * Resolve a capability's static `price` to msats.
10309
10268
  *
10310
10269
  * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
10311
10270
  * Returns `undefined` for dynamic-priced capabilities (those that declare
@@ -10318,7 +10277,7 @@ var JobManager = class {
10318
10277
  }
10319
10278
  /**
10320
10279
  * Translate a capability's static `price` into the fiat envelope used by
10321
- * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
10280
+ * the per-capability `pricing.max` advertised on `/v1/info`.
10322
10281
  * Returns `undefined` for dynamic-priced capabilities and for unknown
10323
10282
  * names (the route handler validates the name before invoking this).
10324
10283
  */
@@ -10328,24 +10287,24 @@ var JobManager = class {
10328
10287
  return resolvePricingMax(cap.price);
10329
10288
  }
10330
10289
  /**
10331
- * Parse a request body through a capability's input schema (internal-review).
10290
+ * Parse a request body through a capability's input schema.
10332
10291
  *
10333
10292
  * Resolution order when a capability declares an `input` schema:
10334
10293
  * 1. `body.data` is preferred — agents pass structured fields directly
10335
- * (CLI builds this from `--param k=v`).
10294
+ * (CLI builds this from `--param k=v`).
10336
10295
  * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
10337
- * JSON-string contract.
10296
+ * JSON-string contract.
10338
10297
  * 3. Neither usable → throw `MissingStructuredInputError` so the route can
10339
- * return `invalid_input` with a hint pointing at `/v1/info`.
10298
+ * return `invalid_input` with a hint pointing at `/v1/info`.
10340
10299
  *
10341
10300
  * When the capability has no `input` schema, returns `body.input` raw
10342
10301
  * (primitive path).
10343
10302
  *
10344
- * The internal-review credit envelope is removed before the schema runs, on both
10345
- * branches (internal-review) — it rides the signed body but is protocol-level, so a
10303
+ * The credit envelope is removed before the schema runs, on both
10304
+ * branches — it rides the signed body but is protocol-level, so a
10346
10305
  * top-level `.strict()` capability schema would otherwise reject every
10347
10306
  * explicit draw as an unknown key, before the envelope was even extracted.
10348
- * The persisted `record.input` the internal-review reactivation path re-reads is the
10307
+ * The persisted `record.input` the reactivation path re-reads is the
10349
10308
  * pre-Zod wire form, so it comes through the second branch carrying them too.
10350
10309
  */
10351
10310
  parseInput(body, capability) {
@@ -10363,7 +10322,7 @@ var JobManager = class {
10363
10322
  throw new MissingStructuredInputError();
10364
10323
  }
10365
10324
  /**
10366
- * Look up the per-capability descriptor by name (internal-review). Throws when the
10325
+ * Look up the per-capability descriptor by name. Throws when the
10367
10326
  * name doesn't exist — the route layer validates body.capability first, so
10368
10327
  * reaching this with an unknown name is a programmer error.
10369
10328
  */
@@ -10389,7 +10348,7 @@ var JobManager = class {
10389
10348
  return this.descriptor.capabilities[name];
10390
10349
  }
10391
10350
  /**
10392
- * Allocate the id the next job will be created under (internal-review). The
10351
+ * Allocate the id the next job will be created under. The
10393
10352
  * submit path calls this BEFORE payment verification so the ledger draw
10394
10353
  * commits with its job linkage, then passes the id back through
10395
10354
  * `createJob`'s provenance. Ids allocated for requests whose payment is
@@ -10400,7 +10359,7 @@ var JobManager = class {
10400
10359
  return `job-${this.sessionId}-${this.nextJobId++}`;
10401
10360
  }
10402
10361
  /**
10403
- * Create a new job from a request body. `provenance` (internal-review) is what the
10362
+ * Create a new job from a request body. `provenance` is what the
10404
10363
  * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
10405
10364
  * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
10406
10365
  * to be given it.
@@ -10456,27 +10415,23 @@ var JobManager = class {
10456
10415
  this.watchDurableStatus(job);
10457
10416
  return job;
10458
10417
  }
10459
- /** Build an SDKJobContext and attach it to the job. */
10460
10418
  buildAndAttachContext(job, parsedInput) {
10461
10419
  const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
10462
10420
  const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
10463
10421
  job.sdkCtx = sdkCtx;
10464
10422
  return sdkCtx;
10465
10423
  }
10466
- /** Start the handler for a job. */
10467
10424
  runHandler(job, sdkCtx) {
10468
10425
  const cap = this.getCapability(job.capability);
10469
10426
  const onError = (err) => {
10470
10427
  if (isTerminal(job.status)) return;
10471
10428
  job.replayProviderSkip = 0;
10472
- if (!job.pendingPrompts.size && !job.pendingPayment) {
10473
- job.status = "failed";
10474
- providerMessage(job, "cancel", {
10475
- reason: err instanceof Error ? err.message : String(err)
10476
- });
10477
- job.terminalWork = this.finalizeTerminal(job);
10478
- void job.terminalWork;
10479
- }
10429
+ job.status = "failed";
10430
+ providerMessage(job, "cancel", {
10431
+ reason: err instanceof Error ? err.message : String(err)
10432
+ });
10433
+ job.terminalWork = this.finalizeTerminal(job);
10434
+ void job.terminalWork;
10480
10435
  };
10481
10436
  if (!cap) {
10482
10437
  if (!isTerminal(job.status)) {
@@ -10504,16 +10459,46 @@ var JobManager = class {
10504
10459
  onError(err);
10505
10460
  }
10506
10461
  }
10507
- /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
10508
10462
  async dispatchMessage(job, type, content) {
10463
+ if (type !== "response" && type !== "payment") {
10464
+ return this.dispatchMessageNow(job, type, content);
10465
+ }
10466
+ const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10467
+ await this.queueInputResume(job, async () => {
10468
+ const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10469
+ if (pending && current !== pending) return;
10470
+ return this.dispatchMessageNow(job, type, content);
10471
+ });
10472
+ }
10473
+ async queueInputResume(job, resume) {
10474
+ const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
10475
+ const next = previous.catch(() => void 0).then(resume);
10476
+ this.inputResumeTails.set(job, next);
10477
+ try {
10478
+ return await next;
10479
+ } finally {
10480
+ if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
10481
+ }
10482
+ }
10483
+ async claimLocalInput(job) {
10484
+ await job.messageAppenderTail;
10485
+ return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
10486
+ }
10487
+ async dispatchMessageNow(job, type, content) {
10509
10488
  const ctx = job.sdkCtx;
10510
10489
  const cap = this.getCapability(job.capability);
10511
10490
  if (type === "response") {
10512
10491
  const rc = content;
10513
10492
  const pending = job.pendingPrompts.get(rc.prompt_id);
10514
10493
  if (pending) {
10494
+ if (!await this.claimLocalInput(job)) {
10495
+ await this.adoptDurableTerminal(job);
10496
+ return;
10497
+ }
10498
+ if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
10515
10499
  job.pendingPrompts.delete(rc.prompt_id);
10516
- job.status = "processing";
10500
+ job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
10501
+ if (job.status === "awaiting-input") await this.persistJob(job);
10517
10502
  pending.resolve(rc);
10518
10503
  } else if (cap?.onResponse && ctx) {
10519
10504
  await cap.onResponse(ctx, rc);
@@ -10522,9 +10507,15 @@ var JobManager = class {
10522
10507
  const pc = content;
10523
10508
  if (job.pendingPayment) {
10524
10509
  const pp = job.pendingPayment;
10510
+ if (!await this.claimLocalInput(job)) {
10511
+ await this.adoptDurableTerminal(job);
10512
+ return;
10513
+ }
10514
+ if (job.pendingPayment !== pp || isTerminal(job.status)) return;
10525
10515
  job.pendingPayment = null;
10526
- job.status = "processing";
10527
- pp.resolve(pc);
10516
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
10517
+ if (job.status === "awaiting-input") await this.persistJob(job);
10518
+ await pp.resolve(pc);
10528
10519
  } else if (cap?.onPayment && ctx) {
10529
10520
  await cap.onPayment(ctx, pc);
10530
10521
  }
@@ -10533,6 +10524,7 @@ var JobManager = class {
10533
10524
  } else if (type === "cancel") {
10534
10525
  const reason = content?.reason ?? "Job cancelled";
10535
10526
  abortJob(job, reason);
10527
+ job.endedBy = "caller";
10536
10528
  job.status = "cancelled";
10537
10529
  if (cap?.onCancel && ctx) {
10538
10530
  await cap.onCancel(ctx, content);
@@ -10551,17 +10543,16 @@ var JobManager = class {
10551
10543
  });
10552
10544
  }
10553
10545
  }
10554
- // ── Persistence ──────────────────────────────────────────────────────
10555
10546
  /**
10556
10547
  * Persist a job to the backing store.
10557
10548
  *
10558
- * Awaits the per-job appender tail (internal-review) so that:
10559
- * 1. The snapshot's `messages` is consistent with the `job_messages` table
10560
- * — no row is still in flight at snapshot time.
10561
- * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
10562
- * can't race a still-pending `appendOutgoing` for the final yield
10563
- * message (which would otherwise wipe the row before an in-flight SSE
10564
- * subscriber's NOTIFY-driven fetch can see it).
10549
+ * Awaits the per-job appender tail so that:
10550
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
10551
+ * no row is still in flight at snapshot time.
10552
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
10553
+ * can't race a still-pending `appendOutgoing` for the final yield
10554
+ * message (which would otherwise wipe the row before an in-flight SSE
10555
+ * subscriber's NOTIFY-driven fetch can see it).
10565
10556
  */
10566
10557
  async persistJob(job) {
10567
10558
  if (job.messageAppenderTail) {
@@ -10582,9 +10573,17 @@ var JobManager = class {
10582
10573
  } else {
10583
10574
  await this.jobStore.save(record);
10584
10575
  }
10576
+ const saved = await this.jobStore.get(job.id);
10577
+ if (saved) {
10578
+ job.timingKnown = saved.timingKnown;
10579
+ job.endedBy = saved.endedBy;
10580
+ job.terminalAtMs = saved.terminalAtMs;
10581
+ job.callerWaitMs = saved.callerWaitMs;
10582
+ job.awaitingInputAtMs = saved.awaitingInputAtMs;
10583
+ }
10585
10584
  }
10586
10585
  /**
10587
- * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
10586
+ * Cross-machine terminal guard. `buildContext`'s guard reads the
10588
10587
  * in-memory `job.status`, so on its own it only protects the machine running
10589
10588
  * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
10590
10589
  * on a machine that isn't running the job — `app.ts` cancels it in the store
@@ -10596,7 +10595,7 @@ var JobManager = class {
10596
10595
  * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
10597
10596
  * the durable state won and the caller must skip its save.
10598
10597
  *
10599
- * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
10598
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
10600
10599
  * this within a round-trip of the remote cancel committing, which is what
10601
10600
  * actually stops the provider spend; `persistJob` calls it again on every save
10602
10601
  * as the backstop for the window where a cancel commits between a handler's
@@ -10604,8 +10603,10 @@ var JobManager = class {
10604
10603
  * stores' terminal-sticky `save`, which keeps the row itself correct).
10605
10604
  */
10606
10605
  async adoptDurableTerminal(job) {
10607
- const durable = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
10608
- if (!durable || !isTerminal(durable.status) || durable.status === job.status) return false;
10606
+ const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
10607
+ if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
10608
+ const durable = await this.jobStore.get(job.id);
10609
+ if (!durable) return false;
10609
10610
  console.warn(
10610
10611
  JSON.stringify({
10611
10612
  level: "job_terminal_elsewhere",
@@ -10616,6 +10617,11 @@ var JobManager = class {
10616
10617
  );
10617
10618
  job.status = durable.status;
10618
10619
  job.summary = durable.summary;
10620
+ job.timingKnown = durable.timingKnown;
10621
+ job.endedBy = durable.endedBy;
10622
+ job.terminalAtMs = durable.terminalAtMs;
10623
+ job.callerWaitMs = durable.callerWaitMs;
10624
+ job.awaitingInputAtMs = durable.awaitingInputAtMs;
10619
10625
  const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
10620
10626
  for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
10621
10627
  job.pendingPayment?.reject(new JobCancelledError(reason));
@@ -10635,7 +10641,7 @@ var JobManager = class {
10635
10641
  });
10636
10642
  }
10637
10643
  this.clearIdleTimer(job.id);
10638
- void this.issueReceiptForStoredJob(job.id);
10644
+ void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
10639
10645
  this.scheduleCleanup(job);
10640
10646
  if (durable.status !== "completed" && terminalCostPersisted) {
10641
10647
  await this.reportJobCost(job);
@@ -10681,6 +10687,7 @@ var JobManager = class {
10681
10687
  async retryTerminalCostHandoff(job, attempt) {
10682
10688
  if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
10683
10689
  await this.reportJobCost(job);
10690
+ await this.reportTerminalJob(job.id);
10684
10691
  }
10685
10692
  /** Durably merge a terminal handler owner's latest cost before reporting it. */
10686
10693
  async persistAdoptedTerminalCost(job) {
@@ -10710,7 +10717,7 @@ var JobManager = class {
10710
10717
  );
10711
10718
  }
10712
10719
  /**
10713
- * Watch the durable status of a locally-active job (internal-review).
10720
+ * Watch the durable status of a locally-active job.
10714
10721
  *
10715
10722
  * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
10716
10723
  * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
@@ -10751,7 +10758,7 @@ var JobManager = class {
10751
10758
  });
10752
10759
  }
10753
10760
  /**
10754
- * Tear down a job's durable-status subscription (internal-review). Must be called
10761
+ * Tear down a job's durable-status subscription. Must be called
10755
10762
  * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
10756
10763
  * the store's shared LISTEN connection. Idempotent.
10757
10764
  */
@@ -10763,12 +10770,12 @@ var JobManager = class {
10763
10770
  unsubscribe();
10764
10771
  }
10765
10772
  /**
10766
- * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
10773
+ * NOTIFY-driven durable-status re-read, coalesced per job.
10767
10774
  *
10768
10775
  * A notify that arrives while a re-read is in flight is queued rather than
10769
10776
  * dropped: the in-flight read may have observed the row a moment *before* the
10770
- * cancel committed, and dropping its notify would put the abort back where
10771
- * this issue found it — waiting for the handler's next store write.
10777
+ * cancel committed, and dropping its notify would leave the local handler
10778
+ * running until its next store write instead of aborting promptly.
10772
10779
  */
10773
10780
  async checkDurableTerminal(job) {
10774
10781
  if (this.statusChecksInFlight.has(job.id)) {
@@ -10798,13 +10805,13 @@ var JobManager = class {
10798
10805
  /**
10799
10806
  * Wire the Tx A appender when the store supports streaming.
10800
10807
  *
10801
- * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
10808
+ * Tx A: outgoing message + `pending_payment_msats` bump land in
10802
10809
  * the same transaction. The DB allocates the row's seq via
10803
10810
  * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
10804
10811
  * traffic on different machines can never collide on the `(job_id, seq)` PK.
10805
10812
  * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
10806
10813
  *
10807
- * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
10814
+ * Per-job serialisation: chained through `job.messageAppenderTail`
10808
10815
  * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
10809
10816
  * `complete`) commit their `appendOutgoing` transactions in call order.
10810
10817
  * Without this, the two transactions race for the `jobs` row lock and the
@@ -10838,7 +10845,7 @@ var JobManager = class {
10838
10845
  // In-memory counter, NOT the authoritative DB-allocated seq:
10839
10846
  // `appendOutgoing` allocates the real seq via `UPDATE ...
10840
10847
  // RETURNING` inside the transaction that just failed, so no DB
10841
- // seq was assigned for this message (internal-review).
10848
+ // seq was assigned for this message.
10842
10849
  localSeq: msg.seq,
10843
10850
  note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
10844
10851
  error: err instanceof Error ? err.message : String(err)
@@ -10849,8 +10856,6 @@ var JobManager = class {
10849
10856
  job.messageAppenderTail = next;
10850
10857
  };
10851
10858
  }
10852
- // ── Idle timeout ─────────────────────────────────────────────────────
10853
- /** Start (or restart) the idle timer for a job. */
10854
10859
  startIdleTimer(job) {
10855
10860
  this.clearIdleTimer(job.id);
10856
10861
  const timeoutMs = this.descriptor.idleTimeout * 1e3;
@@ -10859,7 +10864,6 @@ var JobManager = class {
10859
10864
  }, timeoutMs);
10860
10865
  this.idleTimers.set(job.id, timer);
10861
10866
  }
10862
- /** Clear the idle timer for a job. */
10863
10867
  clearIdleTimer(id) {
10864
10868
  const timer = this.idleTimers.get(id);
10865
10869
  if (timer) {
@@ -10867,7 +10871,7 @@ var JobManager = class {
10867
10871
  this.idleTimers.delete(id);
10868
10872
  }
10869
10873
  }
10870
- /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
10874
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
10871
10875
  async cancelJobIdle(job) {
10872
10876
  if (isTerminal(job.status)) return;
10873
10877
  const current = await this.jobStore.get(job.id).catch(() => void 0);
@@ -10886,6 +10890,7 @@ var JobManager = class {
10886
10890
  if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
10887
10891
  job.pendingPrompts.clear();
10888
10892
  job.pendingPayment = null;
10893
+ job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
10889
10894
  job.status = "cancelled";
10890
10895
  abortJob(job, "Job timed out (idle)");
10891
10896
  providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
@@ -10893,14 +10898,13 @@ var JobManager = class {
10893
10898
  void job.terminalWork;
10894
10899
  this.scheduleCleanup(job);
10895
10900
  }
10896
- // ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
10897
10901
  /**
10898
10902
  * Force-terminate stale non-terminal jobs. Two status-aware arms:
10899
- * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
10900
- * (`stale_no_terminal_status`) — the caller never paid / responded.
10901
- * • `processing`/`working` past `processingWatchdogMs` → `failed`
10902
- * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
10903
- * live workers fresh, so a stale row here means a dead process.
10903
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
10904
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
10905
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
10906
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
10907
+ * live workers fresh, so a stale row here means a dead process.
10904
10908
  *
10905
10909
  * Fires on a timer (wired in the constructor) and also callable on demand
10906
10910
  * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
@@ -10909,17 +10913,17 @@ var JobManager = class {
10909
10913
  * descriptors — the local activeJobs teardown only makes sense on the
10910
10914
  * JobManager that actually hosted the zombie handler.
10911
10915
  *
10912
- * **Both cutoffs float off the wall clock (internal-review).** Each is one
10916
+ * **Both cutoffs float off the wall clock.** Each is one
10913
10917
  * `Date.now()` sample compared against `last_activity_at`, stamped from a
10914
10918
  * different sample at a different moment, so a backwards step between the
10915
10919
  * two makes every row look more recently active than it is: the query
10916
10920
  * matches nothing and both arms go silent for the length of the skew. The
10917
10921
  * dead worker's job keeps its `processing` status, its credit hold stays
10918
- * `pending`, and the internal-review paid-job-death alert — wired to this reaper —
10919
- * never fires. Same defect, same host class, as internal-review's orphan sweep.
10922
+ * `pending`, and the paid-job-death alert — wired to this reaper —
10923
+ * never fires. Same defect, same host class, as orphan sweep.
10920
10924
  *
10921
10925
  * **The compensation is capped** — see {@link staleSweepCompensationCapMs}
10922
- * for where each arm's cap comes from — which internal-review did not need to do. Its eagerness costs at worst an early release
10926
+ * for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
10923
10927
  * of an unclaimed hold, refused outright for any live job row; ours
10924
10928
  * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
10925
10929
  * `expectedActivityBefore` is the same compensated threshold the query used,
@@ -11055,7 +11059,7 @@ var JobManager = class {
11055
11059
  }
11056
11060
  /**
11057
11061
  * How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
11058
- * clock to cover a backwards step (internal-review). The compensation is capped,
11062
+ * clock to cover a backwards step. The compensation is capped,
11059
11063
  * not free: a claimed row must still have been silent for
11060
11064
  * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
11061
11065
  * force-fails a job that may well be alive.
@@ -11075,7 +11079,7 @@ var JobManager = class {
11075
11079
  * clock stepped. That pair is marginal already; the guard must not make it
11076
11080
  * deterministic. The cap collapses to zero there, which is today's
11077
11081
  * behaviour: late, never wrong. The `awaiting-input` arm takes no such term
11078
- * — nothing heartbeats it by design, and its refresh is an inbound caller
11082
+ * nothing heartbeats it by design, and its refresh is an inbound caller
11079
11083
  * message on no cadence at all.
11080
11084
  *
11081
11085
  * **Why a magnitude and not a predicate.** The tempting sharper rule is to
@@ -11094,7 +11098,6 @@ var JobManager = class {
11094
11098
  if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
11095
11099
  return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
11096
11100
  }
11097
- // ── Terminal job content retention ───────────────────────────────────
11098
11101
  /**
11099
11102
  * Redact terminal job content beyond this DVM's configured window.
11100
11103
  *
@@ -11259,12 +11262,11 @@ var JobManager = class {
11259
11262
  );
11260
11263
  }
11261
11264
  }
11262
- // ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
11263
11265
  /**
11264
11266
  * Resolve `pending` credit draws the terminal funnel can no longer reach.
11265
11267
  * Two arms over one scan, distinguished by whether the draw's job row exists.
11266
11268
  *
11267
- * **No job row (internal-review) — release.** internal-review allocates the job id and
11269
+ * **No job row — release.** allocates the job id and
11268
11270
  * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
11269
11271
  * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
11270
11272
  * fail-closed `replay_detected` 401 in that window leaves a committed hold
@@ -11274,12 +11276,12 @@ var JobManager = class {
11274
11276
  * available balance stays reduced forever. Cosmetic for an implicit N=1
11275
11277
  * credit, a silent balance shrink for an explicit N>1 one.
11276
11278
  *
11277
- * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
11279
+ * **Terminal job row — reconcile to its outcome.** The funnel
11278
11280
  * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
11279
11281
  * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
11280
11282
  * nothing, and until now nothing retried. Same stranded hold, reached through
11281
11283
  * a different door — and for a `completed` job it strands the platform's
11282
- * books too, since draws *are* the revenue events (internal-review): the caller is
11284
+ * books too, since draws *are* the revenue events: the caller is
11283
11285
  * charged nothing, the balance never moves, and outstanding liability
11284
11286
  * (`deposits − draw revenue`) overstates forever. So a late settle books, off
11285
11287
  * the committed draw row, through the ordinary reporter path.
@@ -11407,8 +11409,8 @@ var JobManager = class {
11407
11409
  }
11408
11410
  }
11409
11411
  /**
11410
- * The end-of-scan line for a tick that resolved nothing (internal-review, extended
11411
- * by internal-review). Two shapes are worth a warning, and neither is the ordinary
11412
+ * The end-of-scan line for a tick that resolved nothing. Two shapes are
11413
+ * worth a warning, and neither is the ordinary
11412
11414
  * one — the timer runs every 5 minutes per ledger in production.
11413
11415
  *
11414
11416
  * `examined > 0`: aged candidates were looked at and none moved. Usually
@@ -11417,7 +11419,7 @@ var JobManager = class {
11417
11419
  *
11418
11420
  * `examined === 0`: the query returned nothing. Ordinarily that means there
11419
11421
  * is nothing to do — but it is equally what a blinded scan looks like, which
11420
- * is the case the internal-review gate could not reach. Probe for the oldest
11422
+ * is the case the gate could not reach. Probe for the oldest
11421
11423
  * `pending` hold at any age and speak up when one exists the scan should
11422
11424
  * have seen and didn't: already past the age gate outright, stamped ahead of
11423
11425
  * our clock (some other process's clock is fast), or hidden while our own
@@ -11469,7 +11471,7 @@ var JobManager = class {
11469
11471
  );
11470
11472
  }
11471
11473
  /**
11472
- * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
11474
+ * Resolve one aged `pending` draw whose job row reached terminal,
11473
11475
  * mirroring what `resolveCreditDraw` would have done in line. Returns true
11474
11476
  * when this call is the one that moved the draw — the caller counts it, and
11475
11477
  * only it books.
@@ -11542,7 +11544,7 @@ var JobManager = class {
11542
11544
  /**
11543
11545
  * Refresh `lastActivityAt` for jobs this process is actively running
11544
11546
  * (`processing`/`working`) so the processing watchdog only fires once the
11545
- * worker is genuinely dead (internal-review). `awaiting-input` jobs are
11547
+ * worker is genuinely dead. `awaiting-input` jobs are
11546
11548
  * deliberately excluded — their idle timeout must still elapse. Fires on the
11547
11549
  * heartbeat timer; also callable on demand for tests.
11548
11550
  */
@@ -11562,17 +11564,16 @@ var JobManager = class {
11562
11564
  this.heartbeatInFlight = false;
11563
11565
  }
11564
11566
  }
11565
- // ── Reactivation (replay) ────────────────────────────────────────────
11566
11567
  /**
11567
- * Single-execution claim for a reactivating machine (internal-review). Before
11568
+ * Single-execution claim for a reactivating machine. Before
11568
11569
  * replaying an `awaiting-input` job, wait a bounded grace window for a live
11569
11570
  * original handler to win the `awaiting-input → processing` CAS via its
11570
11571
  * NOTIFY wake. Resolves:
11571
- * • `false` — the row left `awaiting-input` during the window (the original
11572
- * handler claimed it / drove it terminal). Stand down; do not replay.
11573
- * • `true` — the window elapsed still `awaiting-input` (the original worker
11574
- * is gone or has no live resolver) AND this machine won the claim CAS.
11575
- * Replay for dead-worker recovery.
11572
+ * • `false` — the row left `awaiting-input` during the window (the original
11573
+ * handler claimed it / drove it terminal). Stand down; do not replay.
11574
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
11575
+ * is gone or has no live resolver) AND this machine won the claim CAS.
11576
+ * Replay for dead-worker recovery.
11576
11577
  *
11577
11578
  * Biasing the original handler to win kills the double execution (double
11578
11579
  * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
@@ -11595,7 +11596,7 @@ var JobManager = class {
11595
11596
  }
11596
11597
  }
11597
11598
  /**
11598
- * Record an inbound client message durably via Tx B (internal-review) and append
11599
+ * Record an inbound client message durably via Tx B and append
11599
11600
  * it to the in-memory `job.messages` array. Returns the DB-allocated seq
11600
11601
  * (`null` when the store isn't streamable — non-Postgres test fallback).
11601
11602
  *
@@ -11639,13 +11640,13 @@ var JobManager = class {
11639
11640
  * in-memory `ServerJob`, wires the appender, builds the context, starts
11640
11641
  * the idle timer. The caller has already recorded the inbound via Tx B
11641
11642
  * and run Tx C (verify + credit); reactivation is now decoupled from
11642
- * credit (internal-review, internal-review). The caller decides whether to run the
11643
+ * credit. The caller decides whether to run the
11643
11644
  * handler (Pattern A: status was `awaiting-input`) or to dispatch the
11644
11645
  * message to a custom handler (Pattern B).
11645
11646
  *
11646
11647
  * For DVMs that declare descriptor-level auth, the persisted
11647
11648
  * `record.input` carries the original signed envelope. We re-verify the
11648
- * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
11649
+ * signature here so a DB-layer tamper — compromised admin, SQL
11649
11650
  * injection, malicious operator with DB access — can't silently feed an
11650
11651
  * attacker-supplied pubkey/envelope into the handler. The drift window
11651
11652
  * and replay store are deliberately skipped: the persisted timestamp is
@@ -11659,7 +11660,7 @@ var JobManager = class {
11659
11660
  * What gets re-verified is the persisted row read back through
11660
11661
  * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
11661
11662
  * those are the caller's own signed bytes, the same ones `/v1/quote` and the
11662
- * submit checked (internal-review). That makes the tamper check strictly stronger
11663
+ * submit checked. That makes the tamper check strictly stronger
11663
11664
  * than the parsed form it replaced: an injected key the capability schema
11664
11665
  * doesn't declare used to be stripped before the signature was checked, so
11665
11666
  * it re-verified clean.
@@ -11704,7 +11705,7 @@ var JobManager = class {
11704
11705
  return { job, sdkCtx };
11705
11706
  }
11706
11707
  /**
11707
- * Verify an inbound payment and apply the credit via Tx C (internal-review).
11708
+ * Verify an inbound payment and apply the credit via Tx C.
11708
11709
  * The inbound message must have been recorded via Tx B already
11709
11710
  * (`recordInboundPending` returned `inboundSeq`). For non-streamable
11710
11711
  * stores (`inboundSeq === null`), falls back to in-memory mutation via
@@ -11714,12 +11715,12 @@ var JobManager = class {
11714
11715
  * `pendingPaymentMsats` without external verification. Gated on
11715
11716
  * `devModeSkipsPaymentVerification` — the same predicate the upfront path
11716
11717
  * reads, so a dev server wired to a mint verifies mid-job payments for real
11717
- * (internal-review) — plus the explicit `dev_auto` opt-out the dev console uses,
11718
+ * plus the explicit `dev_auto` opt-out the dev console uses,
11718
11719
  * which is the one caller that has no wallet to pay from. `dev_auto` loses to
11719
11720
  * any proof riding the same message: money on the wire always takes the rail,
11720
11721
  * so the flag can never leave a real token unspent against a credited job.
11721
11722
  *
11722
- * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
11723
+ * Tx C is not atomic with the rail commit. `verifyIncomingPayment`
11723
11724
  * commits the proofs *and* the ledger leg in one transaction and returns;
11724
11725
  * only then does `verifyAndCredit` write the job row, on its own connection.
11725
11726
  * Everything after that call therefore runs with the caller's money already
@@ -11769,7 +11770,7 @@ var JobManager = class {
11769
11770
  lockPubkeys: extra?.lockPubkeys,
11770
11771
  mintHealthTracker: extra?.mintHealthTracker,
11771
11772
  fxFetcher: this.opts.fxFetcher,
11772
- // internal-review: the ledger leg — a mid-job payment is a top-up that funds
11773
+ // the ledger leg — a mid-job payment is a top-up that funds
11773
11774
  // the job's credit and grows its draw, atomically with the rail commit.
11774
11775
  creditLedger: this.opts.creditLedger,
11775
11776
  processedPayments: this.opts.processedPayments,
@@ -11904,7 +11905,7 @@ var JobManager = class {
11904
11905
  return { ...unappliedCredit && { unappliedCredit } };
11905
11906
  }
11906
11907
  /**
11907
- * Tx C with a bounded retry (internal-review).
11908
+ * Tx C with a bounded retry.
11908
11909
  *
11909
11910
  * By the time this runs on the verified path, the rail commit and the ledger
11910
11911
  * leg have already committed in a transaction this call is not part of — so a
@@ -11991,7 +11992,7 @@ var JobManager = class {
11991
11992
  }
11992
11993
  /**
11993
11994
  * Answer a payment whose rail and ledger legs committed but whose job row
11994
- * could not be written (internal-review) — every retry spent, the money real.
11995
+ * could not be written — every retry spent, the money real.
11995
11996
  *
11996
11997
  * The refusal names the credit the money landed on so the caller can act on
11997
11998
  * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
@@ -12000,7 +12001,7 @@ var JobManager = class {
12000
12001
  * fault an operator should see in their 5xx rate. `retryable` is true because
12001
12002
  * the ask is genuinely still outstanding.
12002
12003
  *
12003
- * Nothing is released here. The hold is left to the internal-review reconciler,
12004
+ * Nothing is released here. The hold is left to the reconciler,
12004
12005
  * which makes the same decision — unbound draw ⇒ release, never settle — but
12005
12006
  * under the credit row's `FOR UPDATE`, after the job is terminal, where it
12006
12007
  * cannot race an in-flight COMMIT. Releasing from here would also be
@@ -12067,7 +12068,7 @@ var JobManager = class {
12067
12068
  }
12068
12069
  /**
12069
12070
  * Describe a deposit the job row never took, in the shape both the 201 and
12070
- * the `payment_unapplied` 500 carry (internal-review). `undefined` when the payment
12071
+ * the `payment_unapplied` 500 carry. `undefined` when the payment
12071
12072
  * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
12072
12073
  * there is no credit to name, so the 201 carries no `credit` block and the
12073
12074
  * 500 takes its words from `unappliedCopy` directly.
@@ -12081,7 +12082,7 @@ var JobManager = class {
12081
12082
  ...unappliedCopy(reason, creditId)
12082
12083
  };
12083
12084
  }
12084
- /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
12085
+ /** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
12085
12086
  reportCreditDeposit(funding) {
12086
12087
  const onCreditFunded = this.opts.onCreditFunded;
12087
12088
  if (!funding || !onCreditFunded || !this.opts.dvmId) return;
@@ -12094,7 +12095,7 @@ var JobManager = class {
12094
12095
  /**
12095
12096
  * Internal: build the BuildContextOpts shared between `createJob` and
12096
12097
  * `reactivateJob`. Centralised so persistence/terminal callbacks and the
12097
- * internal-review NOTIFY-driven cross-machine wake stay in one place.
12098
+ * NOTIFY-driven cross-machine wake stay in one place.
12098
12099
  */
12099
12100
  buildContextOpts(job) {
12100
12101
  const jobStore = this.jobStore;
@@ -12113,7 +12114,16 @@ var JobManager = class {
12113
12114
  getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
12114
12115
  drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
12115
12116
  pricingCurrency: this.pricingCurrency,
12116
- claimForProcessing: isStreamableJobStore(jobStore) ? () => jobStore.claimForProcessing(job.id) : void 0,
12117
+ claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
12118
+ const pending = job.pendingPayment;
12119
+ return this.queueInputResume(job, async () => {
12120
+ if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
12121
+ if (!await this.claimLocalInput(job)) return false;
12122
+ if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
12123
+ await resume?.();
12124
+ return true;
12125
+ });
12126
+ } : void 0,
12117
12127
  onSuperseded: () => {
12118
12128
  this.clearIdleTimer(job.id);
12119
12129
  this.unwatchDurableStatus(job.id);
@@ -12124,7 +12134,7 @@ var JobManager = class {
12124
12134
  }
12125
12135
  /**
12126
12136
  * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
12127
- * job's remaining pool in (internal-review).
12137
+ * job's remaining pool in.
12128
12138
  *
12129
12139
  * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
12130
12140
  * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
@@ -12142,8 +12152,7 @@ var JobManager = class {
12142
12152
  * drew and whose pool this can't read or denominate answers `unpriced`, which
12143
12153
  * the gate declines to auto-credit from: the msat figure it would otherwise
12144
12154
  * fall back on is a slice of the credit's rail value at a ratio pinned
12145
- * whenever that credit was funded, which is the exact figure this issue exists
12146
- * to stop spending against.
12155
+ * whenever that credit was funded, so it cannot safely fund a new ask.
12147
12156
  */
12148
12157
  async resolveDrawPool(job) {
12149
12158
  const ledger = this.opts.creditLedger;
@@ -12192,7 +12201,7 @@ var JobManager = class {
12192
12201
  }
12193
12202
  /**
12194
12203
  * Issue the signed receipt for a job that has reached a terminal status
12195
- * (internal-review). Idempotent and safe to call from every terminal path — the
12204
+ * Idempotent and safe to call from every terminal path — the
12196
12205
  * store owns both the sequence allocation and the write-once persist, so
12197
12206
  * two machines racing on the same job converge on identical bytes and burn
12198
12207
  * exactly one sequence number.
@@ -12277,10 +12286,10 @@ var JobManager = class {
12277
12286
  };
12278
12287
  }
12279
12288
  /**
12280
- * Settle or release a terminal job's credit draw (internal-review): `completed`
12289
+ * Settle or release a terminal job's credit draw: `completed`
12281
12290
  * settles (the hold becomes a real debit); `failed`/`cancelled` releases
12282
12291
  * (the hold evaporates — "no debit on job failure", mechanically
12283
- * superseding the internal-review/974 no-op refund for credit-paid jobs, including
12292
+ * superseding the 974 no-op refund for credit-paid jobs, including
12284
12293
  * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
12285
12294
  * resolution returns the recorded state. Returns the `ReceiptCredit` block
12286
12295
  * for the receipt: `balance_after` is the recorded draw trajectory for a
@@ -12326,7 +12335,7 @@ var JobManager = class {
12326
12335
  }
12327
12336
  }
12328
12337
  /**
12329
- * The one place a terminal job's status becomes a ledger verb (internal-review):
12338
+ * The one place a terminal job's status becomes a ledger verb:
12330
12339
  * `completed` settles the hold into a real debit, `failed`/`cancelled`
12331
12340
  * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
12332
12341
  * late reconciler (`reconcileTerminalDraw`) — two paths that must never
@@ -12341,45 +12350,47 @@ var JobManager = class {
12341
12350
  * cleanup window hand back the same bytes as a cross-machine re-read.
12342
12351
  */
12343
12352
  async attachReceipt(job) {
12344
- const receipt = await this.issueReceipt(toJobRecord(job));
12353
+ const record = await this.jobStore.get(job.id);
12354
+ if (!record || !isTerminal(record.status)) return;
12355
+ const receipt = await this.issueReceipt(record);
12345
12356
  if (receipt) job.receipt = receipt;
12346
12357
  }
12347
12358
  /**
12348
- * Read-path repair for a terminal job carrying no receipt (internal-review), for
12359
+ * Read-path repair for a terminal job carrying no receipt, for
12349
12360
  * either an in-memory job or a store record. Free in the steady state — it
12350
12361
  * returns on the `receipt` check without touching the store — so it costs
12351
12362
  * only on the cases it exists for:
12352
12363
  *
12353
12364
  * - **A crash between the two store calls.** `claimReceiptSeq` commits the
12354
- * sequence number to the job row before `saveReceipt` writes the bytes; a
12355
- * process death in that window would otherwise strand that number
12356
- * forever, and a permanent gap is indistinguishable from the deliberate
12357
- * suppression `seq` exists to expose. Re-issuing here reuses the already
12358
- * committed number (the claim is idempotent per job) rather than
12359
- * allocating a second one.
12365
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
12366
+ * process death in that window would otherwise strand that number
12367
+ * forever, and a permanent gap is indistinguishable from the deliberate
12368
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
12369
+ * committed number (the claim is idempotent per job) rather than
12370
+ * allocating a second one.
12360
12371
  * - **A transient store failure** at the terminal: `issueReceipt` logs
12361
- * `receipt_issue_failed` and returns nothing rather than failing the job,
12362
- * so the next read retries.
12372
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
12373
+ * so the next read retries.
12363
12374
  * - **Jobs that terminated before the DVM had a receipt key.** They pick one
12364
- * up on first read, with `issued_at` reflecting when it was signed.
12375
+ * up on first read, with `issued_at` reflecting when it was signed.
12365
12376
  */
12366
12377
  async ensureReceipt(target) {
12367
- if (target.receipt) return;
12368
- const record = "listeners" in target ? toJobRecord(target) : target;
12378
+ if (target.receipt) {
12379
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12380
+ return;
12381
+ }
12382
+ const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
12383
+ if (!record || !isTerminal(record.status)) return;
12369
12384
  const receipt = await this.issueReceipt(record);
12370
12385
  if (receipt) target.receipt = receipt;
12386
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12371
12387
  }
12372
- /**
12373
- * Close out a terminal job whose caller hasn't already persisted it: sign
12374
- * the receipt, then save the snapshot. The snapshot save never writes the
12375
- * receipt column, so ordering only affects how soon a reader sees the
12376
- * receipt — this way a caller polling immediately after the terminal
12377
- * already finds it.
12378
- */
12388
+ /** Commit the terminal snapshot before resolving money and signing its durable evidence. */
12379
12389
  async finalizeTerminal(job) {
12390
+ await this.persistJob(job);
12380
12391
  await this.attachReceipt(job);
12381
- await this.persistJob(job).catch(() => void 0);
12382
12392
  await this.reportJobCost(job);
12393
+ await this.reportTerminalJob(job.id);
12383
12394
  }
12384
12395
  /**
12385
12396
  * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
@@ -12398,20 +12409,68 @@ var JobManager = class {
12398
12409
  }
12399
12410
  /**
12400
12411
  * Finish accounting for a terminal written directly to the store — the
12401
- * stale reaper and a cancel handled on a different machine (internal-review).
12412
+ * stale reaper and a cancel handled on a different machine.
12402
12413
  */
12403
12414
  async finalizeStoredTerminal(jobId) {
12404
12415
  const receipt = await this.issueReceiptForStoredJob(jobId);
12405
12416
  const record = await this.jobStore.get(jobId).catch(() => void 0);
12406
- if (record && isTerminal(record.status)) await this.reportJobCost(record);
12417
+ if (record && isTerminal(record.status)) {
12418
+ await this.reportJobCost(record);
12419
+ await this.reportTerminalJob(jobId);
12420
+ }
12407
12421
  return receipt;
12408
12422
  }
12409
- /** Handle job reaching terminal state (completed, failed, cancelled). */
12423
+ async reportTerminalJob(jobId) {
12424
+ try {
12425
+ const callback = this.opts.onJobTerminal;
12426
+ const dvmId = this.opts.dvmId;
12427
+ if (!callback || !dvmId) return;
12428
+ const record = await this.jobStore.get(jobId);
12429
+ if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
12430
+ const receipt = await this.issueReceipt(record);
12431
+ if (this.opts.receiptIssuer && !receipt) return;
12432
+ if (record.creditId && record.drawId && this.opts.creditLedger) {
12433
+ const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
12434
+ if (!draw || draw.status === "pending") return;
12435
+ }
12436
+ await callback({
12437
+ dvmId,
12438
+ jobId: record.id,
12439
+ capability: record.capability,
12440
+ outcome: record.status,
12441
+ endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
12442
+ paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
12443
+ acceptedAtMs: record.createdAt,
12444
+ terminalAtMs: record.terminalAtMs,
12445
+ callerWaitMs: record.callerWaitMs ?? 0,
12446
+ receiptSeq: receipt?.seq ?? null
12447
+ });
12448
+ if ("markTerminalReported" in this.jobStore) {
12449
+ await this.jobStore.markTerminalReported(jobId);
12450
+ }
12451
+ } catch (error) {
12452
+ console.error(`[${jobId}] Terminal reporting failed:`, error);
12453
+ }
12454
+ }
12455
+ terminalReportRecoveryCursor;
12456
+ async recoverTerminalReports() {
12457
+ const store = this.jobStore;
12458
+ const dvmId = this.opts.dvmId;
12459
+ if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
12460
+ const recovery = store;
12461
+ const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
12462
+ for (const record of records) await this.reportTerminalJob(record.id);
12463
+ this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
12464
+ }
12410
12465
  async handleJobTerminal(job) {
12411
12466
  this.clearIdleTimer(job.id);
12467
+ if (await this.adoptDurableTerminal(job)) return;
12468
+ const durable = await this.jobStore.get(job.id);
12469
+ if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
12412
12470
  await this.attachReceipt(job);
12413
12471
  const booked = await this.bookRevenue(job);
12414
12472
  if (!booked) await this.reportJobCost(job);
12473
+ await this.reportTerminalJob(job.id);
12415
12474
  this.scheduleCleanup(job);
12416
12475
  }
12417
12476
  /**
@@ -12424,6 +12483,7 @@ var JobManager = class {
12424
12483
  async recoverTerminalCosts() {
12425
12484
  const store = this.jobStore;
12426
12485
  const dvmId = this.opts.dvmId;
12486
+ await this.recoverTerminalReports();
12427
12487
  if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
12428
12488
  return { examined: 0, queued: 0, complete: true };
12429
12489
  }
@@ -12508,11 +12568,11 @@ var JobManager = class {
12508
12568
  return true;
12509
12569
  }
12510
12570
  /**
12511
- * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
12571
+ * Report a completed paid job's revenue under its durable job identity.
12512
12572
  * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
12513
12573
  *
12514
12574
  * For a **credit-backed** job the revenue event is the *settled draw* (spec
12515
- * §10), and since internal-review the sats figure corrects the job's mirror of that
12575
+ * §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
12516
12576
  * draw against the draw itself: `withDrawBasis` stamps the job when the
12517
12577
  * payment lands, but a Bitcoin credit's settle re-prices the draw against
12518
12578
  * the funding lots it consumed. The correction is a delta, because a job's
@@ -12527,7 +12587,7 @@ var JobManager = class {
12527
12587
  * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
12528
12588
  * payload is reported verbatim.
12529
12589
  *
12530
- * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
12590
+ * Takes the fields rather than a `ServerJob` so the reconciler — a
12531
12591
  * background sweep that holds no in-process job — books through this exact
12532
12592
  * path off the row it read. `paymentTxHash` is passed **verbatim**, never
12533
12593
  * recomputed as a `drawSettlementRef`: it is the same column the in-line
@@ -12544,7 +12604,7 @@ var JobManager = class {
12544
12604
  * DVM with no reporter). Every drop that costs a booking logs first.
12545
12605
  *
12546
12606
  * The rail check sits **below** the draw re-read, not in the entry guard
12547
- * (internal-review): a settled draw the terminal can't report under is a real debit
12607
+ * a settled draw the terminal can't report under is a real debit
12548
12608
  * with no revenue event, permanently overstating outstanding liability
12549
12609
  * (`deposits − draw revenue`), and it used to return here in silence. Placed
12550
12610
  * after the re-read, `revenue_skipped_no_rail` can name the draw and its
@@ -12607,9 +12667,9 @@ var JobManager = class {
12607
12667
  paidMsats: job.paidMsats,
12608
12668
  creditRail: creditRail ?? null,
12609
12669
  // Four distinct causes needing four distinct fixes, so none of them
12610
- // is the else-branch: the credit predates internal-review and has no rail
12670
+ // is the else-branch: the credit predates and has no rail
12611
12671
  // (`credit_rail_null`); it carries one this build doesn't list, the
12612
- // internal-review sibling class (`credit_rail_unrecognized`); the credit's
12672
+ // sibling class (`credit_rail_unrecognized`); the credit's
12613
12673
  // rail is perfectly good and it is the *job row* that lost its own
12614
12674
  // (`job_rail_missing`), which no change to the ledger would fix; or
12615
12675
  // this manager was built with no `creditLedger` to read the draw
@@ -12657,7 +12717,7 @@ var JobManager = class {
12657
12717
  cashuFlow: job.cashuFlow,
12658
12718
  // Absent unless the handler declared one, and absent is a fact: the
12659
12719
  // platform shows "no costs reported" rather than a zero cost and a
12660
- // 100% margin (internal-review). Both halves or neither — a bare amount
12720
+ // 100% margin. Both halves or neither — a bare amount
12661
12721
  // in an unnamed currency is not a cost.
12662
12722
  ...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
12663
12723
  ...credit
@@ -12677,7 +12737,6 @@ var JobManager = class {
12677
12737
  }, 6e4);
12678
12738
  this.cleanupTimers.set(job.id, timer);
12679
12739
  }
12680
- /** Clear all timers. Called on server shutdown to allow clean exit. */
12681
12740
  shutdown() {
12682
12741
  for (const [id] of this.idleTimers) {
12683
12742
  this.clearIdleTimer(id);
@@ -12840,8 +12899,8 @@ var MintHealthTracker = class {
12840
12899
  firstProbeComplete = false;
12841
12900
  constructor(opts) {
12842
12901
  this.mints = [...opts.mints];
12843
- this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
12844
- this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
12902
+ this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
12903
+ this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
12845
12904
  this.fetchImpl = opts.fetchImpl;
12846
12905
  this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
12847
12906
  this.nowFn = opts.now ?? Date.now;
@@ -12892,7 +12951,7 @@ var MintHealthTracker = class {
12892
12951
  /**
12893
12952
  * True once the first full probe tick has landed. Before this, paid calls
12894
12953
  * are held at `503 mint_health_pending` rather than accepted against
12895
- * unvalidated mints (internal-review).
12954
+ * unvalidated mints.
12896
12955
  */
12897
12956
  initialProbeComplete() {
12898
12957
  return this.firstProbeComplete;
@@ -12917,12 +12976,11 @@ var MintHealthTracker = class {
12917
12976
  * gate — a deterministic, permanent config error, unlike a transient outage.
12918
12977
  * Unknown mints return false. `advertisedMints()` uses this to keep a
12919
12978
  * permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
12920
- * where a merely-flapping mint is still published (internal-review).
12979
+ * where a merely-flapping mint is still published.
12921
12980
  */
12922
12981
  isNutIncompatible(mintUrl) {
12923
12982
  return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
12924
12983
  }
12925
- /** Plain-object snapshot keyed by mint URL — for tests + observability. */
12926
12984
  snapshot() {
12927
12985
  return Object.fromEntries(Array.from(this.state.entries()).map(([k, v]) => [k, { ...v }]));
12928
12986
  }
@@ -12980,12 +13038,7 @@ var MintHealthTracker = class {
12980
13038
  );
12981
13039
  }
12982
13040
  }
12983
- /**
12984
- * One-shot structured summary emitted after the first probe tick. Replaces
12985
- * the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
12986
- * all-mints-unhealthy warning — the operator alert that the Fly restart-loop
12987
- * used to be before boot stopped refusing to start (internal-review).
12988
- */
13041
+ /** One boot summary, including an all-unhealthy warning. */
12989
13042
  logBootHealth() {
12990
13043
  const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
12991
13044
  mintUrl,
@@ -13009,15 +13062,15 @@ var MintHealthTracker = class {
13009
13062
  );
13010
13063
  }
13011
13064
  };
13012
- function resolveEnvTickMs() {
13013
- const raw = process.env.DVMKIT_MINT_HEALTH_TICK_MS;
13065
+ function resolveEnvTickMs(env = process.env) {
13066
+ const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
13014
13067
  if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
13015
13068
  const parsed = Number(raw);
13016
13069
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
13017
13070
  return Math.floor(parsed);
13018
13071
  }
13019
- function resolveEnvFailuresToSick() {
13020
- const raw = process.env.DVMKIT_MINT_FAILURES_TO_SICK;
13072
+ function resolveEnvFailuresToSick(env = process.env) {
13073
+ const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
13021
13074
  if (!raw) return DEFAULT_FAILURES_TO_SICK;
13022
13075
  const parsed = Number(raw);
13023
13076
  if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
@@ -13116,7 +13169,6 @@ var PostgresX402ExactSettlementStore = class {
13116
13169
  this.pool = pool;
13117
13170
  }
13118
13171
  pool;
13119
- /** Create the exact settlement tables. */
13120
13172
  async init() {
13121
13173
  await withSdkInitLock(this.pool, async () => {
13122
13174
  await this.pool.query(`
@@ -14062,20 +14114,11 @@ async function handlePendingDrains(c, opts) {
14062
14114
  amount_micro: d.amountMicro,
14063
14115
  amount_sats: amountSats,
14064
14116
  amount_msats: amountSats === null ? null : amountSats * 1e3,
14065
- // Whether this row's sats figure is the in-kind obligation or a live-rate
14066
- // conversion. An operator settling a refund should be able to see which,
14067
- // and the servicer's own logs carry it for the same reason.
14068
14117
  sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
14069
- // The figure a `manual` payout is actually sized in: `amount_micro` is
14070
- // fiat, and the operator sends the rail's own token. It is the same debit
14071
- // `completeX402ChannelDrain` treats as its settlement floor (internal-review),
14072
- // so publishing it is what lets an operator settle from the row instead
14073
- // of assuming their stablecoin happens to be six-decimal (internal-review).
14118
+ // Rail-native debit, distinct from fiat amount_micro. This is the channel refund floor
14119
+ // and the size a manual payout must cover.
14074
14120
  amount_native: d.drainedNative,
14075
14121
  settlement,
14076
- // Why this row is classified as it is, and the channel an operator would
14077
- // look up to see the close land. `payout` cannot answer it: a
14078
- // channel-backed mpp drain carries `{}`.
14079
14122
  channel_id: d.channelId,
14080
14123
  created_at: d.createdAt
14081
14124
  };
@@ -14086,13 +14129,7 @@ async function handlePendingDrains(c, opts) {
14086
14129
  acc[method] = pending.filter((d) => d.method === method).reduce((sum, d) => sum + d.amountMicro, 0);
14087
14130
  return acc;
14088
14131
  }, {}),
14089
- // What no job will ever pay down: the operator's own worklist, in the one
14090
- // denomination these rows have.
14091
14132
  manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum, d) => sum + d.amount_micro, 0),
14092
- // The rest of the stablecoin liability — owed, but by a channel close in
14093
- // flight rather than by a person. Published beside `manual_micro` so the
14094
- // difference between the two is visible rather than inferred from a
14095
- // `by_method_micro` figure neither of them matches.
14096
14133
  channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum, d) => sum + d.amount_micro, 0)
14097
14134
  };
14098
14135
  return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
@@ -14121,17 +14158,21 @@ async function handleBitcoinLiability(c, opts) {
14121
14158
  drain_liability_sats: drainSats,
14122
14159
  drain_count: promised.length,
14123
14160
  total_owed_sats: deposit.depositSats + drainSats,
14124
- // Money this figure does not speak for, published rather than folded in:
14125
- // `uncovered` is a credit whose lots fall short of its balance, `unbacked`
14126
- // is one holding any open micro with no sats basis. A reclaim on either
14127
- // prices the whole balance off a live rate, so both are reported whole and
14128
- // neither contributes to `deposit_liability_sats` — a sweep computed
14129
- // against `total_owed_sats` alone is short by whatever they turn out to be
14130
- // worth.
14161
+ // Uncovered or basisless balances require whole-balance live-rate pricing and do not
14162
+ // contribute to deposit_liability_sats. total_owed_sats alone cannot reserve their
14163
+ // eventual refunds.
14131
14164
  uncovered_credits: deposit.uncoveredCredits,
14132
- uncovered_micro: deposit.uncoveredMicro,
14165
+ uncovered_by_currency: deposit.uncoveredByCurrency.map((currency) => ({
14166
+ currency: currency.currency,
14167
+ micro: currency.micro,
14168
+ credits: currency.credits
14169
+ })),
14133
14170
  unbacked_credits: deposit.unbackedCredits,
14134
- unbacked_micro: deposit.unbackedMicro,
14171
+ unbacked_by_currency: deposit.unbackedByCurrency.map((currency) => ({
14172
+ currency: currency.currency,
14173
+ micro: currency.micro,
14174
+ credits: currency.credits
14175
+ })),
14135
14176
  unpriced_drains: drainsUnpriced
14136
14177
  });
14137
14178
  }
@@ -14569,9 +14610,6 @@ async function handleReconcileInvoice(c, opts) {
14569
14610
  const result = await opts.ledger.reconcileBlockedInvoice({
14570
14611
  paymentHash,
14571
14612
  targetCreditId,
14572
- // `fund`'s upsert OVERWRITES `expiry_ms`, so a bare `now + ttl` would
14573
- // shorten a target credit with more life left on it — destroying
14574
- // spendable value while purporting to add some.
14575
14613
  expiryMs,
14576
14614
  basis,
14577
14615
  ...settledAtMs === void 0 ? {} : { settledAtMs },
@@ -14592,9 +14630,6 @@ async function handleReconcileInvoice(c, opts) {
14592
14630
  creditCurrency: invoice.currency,
14593
14631
  fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
14594
14632
  expiryMs,
14595
- // The settlements table types this deposit `repair` off the
14596
- // flag (internal-review): the sats landed at payment time and the
14597
- // deposit is the one row they get.
14598
14633
  repaired: true
14599
14634
  }
14600
14635
  }
@@ -14677,8 +14712,6 @@ async function handleWriteOffInvoice(c, opts) {
14677
14712
  amount_micro: written.amountMicro,
14678
14713
  currency: written.currency,
14679
14714
  note: written.writeOffNote,
14680
- // Mirrors the reconcile's `replayed`: the operator asked for a decision and
14681
- // finds one already recorded, possibly with someone else's note on it.
14682
14715
  replayed: !outcome.written
14683
14716
  });
14684
14717
  }
@@ -16588,7 +16621,7 @@ function autoApprovePayment(content) {
16588
16621
  headers: jobHeaders(true),
16589
16622
  // amount_msats rides along so an onPayment handler sees the same shape a
16590
16623
  // real rail would hand it. dev_auto is admitted on dev-mode servers only
16591
- // (internal-review) \u2014 hence the status check below rather than a silent catch.
16624
+ // hence the status check below rather than a silent catch.
16592
16625
  body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
16593
16626
  }).then(function(r) {
16594
16627
  if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
@@ -16735,7 +16768,7 @@ async function createDVMServer(descriptor, opts) {
16735
16768
  }
16736
16769
  })();
16737
16770
  try {
16738
- if (process.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16771
+ if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16739
16772
  console.warn(
16740
16773
  JSON.stringify({
16741
16774
  level: "cashu_opts_env_ignored",
@@ -16748,12 +16781,12 @@ async function createDVMServer(descriptor, opts) {
16748
16781
  const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
16749
16782
  const dvmId = opts.dvmId;
16750
16783
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16751
- const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
16784
+ const { RevenueReporter } = await import("./revenue-reporter-O75BGYFK.js");
16752
16785
  const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16753
16786
  revenueReporter = reporter;
16754
16787
  await reporter.init();
16755
16788
  reporter.startRetryLoop();
16756
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-RG6XNGPI.js");
16789
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KEMUXEUO.js");
16757
16790
  payoutReporter = new PayoutReporter({
16758
16791
  dvmId,
16759
16792
  db: opts.db,
@@ -16896,6 +16929,7 @@ async function createDVMServer(descriptor, opts) {
16896
16929
  processedPayments,
16897
16930
  onJobCompleted: effectiveOnJobCompleted,
16898
16931
  onJobCost: effectiveOnJobCost,
16932
+ onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
16899
16933
  onPaidJobDeath: effectiveOnPaidJobDeath,
16900
16934
  onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16901
16935
  onCreditFunded: opts.onCreditFunded,
@@ -16939,7 +16973,7 @@ var DVMServer = class {
16939
16973
  fxFetcher;
16940
16974
  /**
16941
16975
  * The currency this DVM prices in and denominates its credit ledger in,
16942
- * resolved once at construction (internal-review).
16976
+ * resolved once at construction.
16943
16977
  *
16944
16978
  * `configureDVM` and the CLI loader both resolve this field. The server is
16945
16979
  * also exported for direct JavaScript callers, so defaulting at the one
@@ -16948,29 +16982,24 @@ var DVMServer = class {
16948
16982
  */
16949
16983
  pricingCurrency;
16950
16984
  /**
16951
- * Last-good lock-pubkey state observed from Postgres (internal-review). Used as the
16985
+ * Last-good lock-pubkey state observed from Postgres. Used as the
16952
16986
  * fallback when `loadLockPubkeyState` throws — degrades more gracefully than
16953
16987
  * falling back to the env-derived seed, which post-rotation is the *retired*
16954
16988
  * pubkey. Null until the first successful read.
16955
16989
  */
16956
16990
  cachedLockPubkeyState = null;
16957
- /** Guards the opportunistic Tempo settlement cadence (internal-review). */
16958
16991
  tempoSweepInFlight = false;
16959
16992
  tempoSweepLastAttemptMs = 0;
16960
- /** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
16961
16993
  receiptIssuer;
16962
- /** Credit ledger the payment gate funds/draws through (internal-review). */
16963
16994
  creditLedger;
16964
- /** Durable x402/mpp funding markers (internal-review). */
16965
16995
  processedPayments;
16966
16996
  authAudience;
16967
16997
  /** Lazily constructed when a refreshed facilitator probe gains batch support. */
16968
16998
  x402BatchSettlement;
16969
16999
  /** Coalesces concurrent requests that first observe batch support. */
16970
17000
  x402BatchSettlementInit;
16971
- /** Durable exact-settlement coordinator shared by every exact payment surface. */
16972
17001
  x402ExactSettlement;
16973
- /** Withdraws this mount's Tempo session capability from the poll (internal-review). */
17002
+ /** Withdraws this mount's Tempo session capability from the poll. */
16974
17003
  undeclareSessionCapability;
16975
17004
  constructor(descriptor, opts) {
16976
17005
  this.descriptor = descriptor;
@@ -16994,7 +17023,7 @@ var DVMServer = class {
16994
17023
  this.mintHealthTracker = opts.mintHealthTracker;
16995
17024
  this.ownsMintHealthTracker = false;
16996
17025
  } else if (opts.mints && opts.mints.length > 0) {
16997
- this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
17026
+ this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
16998
17027
  this.ownsMintHealthTracker = true;
16999
17028
  } else {
17000
17029
  this.ownsMintHealthTracker = false;
@@ -17015,17 +17044,9 @@ var DVMServer = class {
17015
17044
  mints: opts.mints,
17016
17045
  jobStore: opts.jobStore,
17017
17046
  devMode: opts.devMode,
17018
- // Derived, not raw (internal-review). `ctx.requestPayment` gates its x402 and
17019
- // Tempo challenge issuance on this list, while `/v1/info`, `/v1/quote`
17020
- // and the upfront 402 all read `resolvePaymentMethods()` — so passing the
17021
- // raw option meant a DVM that never set `paymentMethods` (no first-party
17022
- // one does; the descriptor documents it as "derived from configured
17023
- // rails") advertised both rails, took the upfront payment on either, and
17024
- // then emitted a mid-job payment-request carrying no rail at all. The
17025
- // caller's responder had nothing to sign and the job stalled at
17026
- // needs-approval — reachable only from a rung that raises a real mid-job
17027
- // ask, which nothing above L1 did. Facilitator availability is still
17028
- // re-checked per ask in `context.ts`, so a static list is safe here.
17047
+ // Use the same derived rail list as discovery/upfront payment; otherwise
17048
+ // default-configured rails vanish from mid-job asks. Availability is rechecked
17049
+ // per ask.
17029
17050
  paymentMethods: (opts.paymentMethods ?? this.defaultPaymentMethods()).filter(
17030
17051
  (method) => method !== "x402" || this.x402CurrencySupported()
17031
17052
  ),
@@ -17040,21 +17061,20 @@ var DVMServer = class {
17040
17061
  dvmId: opts.dvmId,
17041
17062
  authAudience: this.authAudience,
17042
17063
  fxFetcher: this.fxFetcher,
17043
- // internal-review: the mid-job top-up leg needs the same rail wiring the upfront
17064
+ // the mid-job top-up leg needs the same rail wiring the upfront
17044
17065
  // path gets — a divergence here would be a DVM whose upfront payment funds
17045
17066
  // a credit and whose mid-job payment silently doesn't.
17046
17067
  processedPayments: this.processedPayments,
17047
17068
  enqueueCreditDeposit: opts.enqueueCreditDeposit,
17048
17069
  enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
17049
17070
  creditTtlMs: descriptor.credit ? descriptor.credit.ttlSeconds * 1e3 : void 0,
17050
- // internal-review: the same read `creditCurrency()` serves everywhere else, so a
17051
- // mid-job ask pins in the denomination this DVM's credits actually use.
17052
17071
  pricingCurrency: this.pricingCurrency,
17053
17072
  onCreditFunded: opts.onCreditFunded,
17054
17073
  onJobCompleted: opts.onJobCompleted,
17055
17074
  onPaidJobCompleted: opts.onPaidJobCompleted,
17056
17075
  paymentMode: opts.paymentMode,
17057
17076
  onJobCost: opts.onJobCost,
17077
+ onJobTerminal: opts.onJobTerminal,
17058
17078
  onPaidJobDeath: opts.onPaidJobDeath,
17059
17079
  onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
17060
17080
  onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
@@ -17092,7 +17112,7 @@ var DVMServer = class {
17092
17112
  * Tear down this mount. Every leg is synchronous except the batch-settlement
17093
17113
  * server's own `stop()`; the returned promise settles once that has finished,
17094
17114
  * so a caller holding a resource behind it — the settlement pool the spend
17095
- * gate's store owns (internal-review) — can sequence its close instead of pulling it
17115
+ * gate's store owns — can sequence its close instead of pulling it
17096
17116
  * out from under an in-flight stop. A rejection remains observable so the app
17097
17117
  * and host teardown layers can finish later cleanup before surfacing it.
17098
17118
  */
@@ -17193,12 +17213,12 @@ var DVMServer = class {
17193
17213
  (c) => c.json({
17194
17214
  ok: true,
17195
17215
  // shortSha, not .slice(0, 7): a dirty-tree deploy bakes a `-dirty`
17196
- // marker (internal-review) that a raw truncation would cut off the end,
17216
+ // marker that a raw truncation would cut off the end,
17197
17217
  // leaving this endpoint claiming a clean commit while /v1/info's
17198
17218
  // untruncated sdk_sha says otherwise — same image, two answers.
17199
- gitSha: shortSha(process.env.GIT_SHA ?? "unknown"),
17200
- deployedAt: process.env.BUILD_TIME ?? "unknown",
17201
- deployId: process.env.DVMKIT_DEPLOY_ID ?? "unknown"
17219
+ gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
17220
+ deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
17221
+ deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17202
17222
  })
17203
17223
  );
17204
17224
  if (this.opts.devMode) {
@@ -17231,8 +17251,6 @@ var DVMServer = class {
17231
17251
  getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
17232
17252
  graceSeconds: this.graceSeconds(),
17233
17253
  nonceStore: this.adminCashuNonceStore,
17234
- // internal-review: a completed melt is a landed payout, queued in the
17235
- // transaction that marks it.
17236
17254
  ...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
17237
17255
  });
17238
17256
  }
@@ -17250,13 +17268,13 @@ var DVMServer = class {
17250
17268
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
17251
17269
  enqueueCreditDrain: this.opts.enqueueCreditDrain,
17252
17270
  lightningReceive: this.opts.lightningReceive,
17253
- // internal-review's x402 repair queue rides the same surface, and resolves
17271
+ // x402 repair queue rides the same surface, and resolves
17254
17272
  // the rail the same lazy way the verify path does — constructing the
17255
17273
  // settlement server probes the facilitator, which must not happen just
17256
17274
  // because the admin routes were mounted.
17257
17275
  x402BatchSettlement: () => this.resolveX402BatchSettlement(),
17258
17276
  x402ExactSettlement: this.x402ExactSettlement,
17259
- // internal-review's Tempo repair queue, same surface again. The mppx handle is
17277
+ // Tempo repair queue, same surface again. The mppx handle is
17260
17278
  // already built by the time routes mount, so the thunk is uniformity
17261
17279
  // rather than laziness — and it is the *handle*, not the store, so a
17262
17280
  // DVM wired with an in-memory session store resolves to a rail with no
@@ -17264,10 +17282,8 @@ var DVMServer = class {
17264
17282
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
17265
17283
  // Health-filtered, and read per request rather than captured: the
17266
17284
  // servicer picking a mint to buy a refund at wants the list that
17267
- // excludes a mint currently failing its probe (internal-review).
17285
+ // excludes a mint currently failing its probe.
17268
17286
  mints: () => this.advertisedMints(),
17269
- // internal-review: a repaired Tempo close is a landed payout the agent
17270
- // finished, queued in the transaction that books the drain.
17271
17287
  ...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
17272
17288
  });
17273
17289
  }
@@ -17278,7 +17294,7 @@ var DVMServer = class {
17278
17294
  /**
17279
17295
  * Load the live lock-pubkey state for this DVM. Reads from
17280
17296
  * `dvm_lock_pubkeys` on every call so rotations propagate without needing
17281
- * a process restart (internal-review). On success the result is cached so a later
17297
+ * a process restart. On success the result is cached so a later
17282
17298
  * DB outage can degrade to the last-observed state instead of the
17283
17299
  * env-derived seed (which post-rotation is the *retired* pubkey).
17284
17300
  */
@@ -17288,12 +17304,8 @@ var DVMServer = class {
17288
17304
  return state;
17289
17305
  }
17290
17306
  /**
17291
- * Resolve the active lock-pubkey set for the mid-job cashu accept path
17292
- * (internal-review). Returns the same `[current, ...retired_in_grace]` shape the
17293
- * upfront flow uses at lines 709-718 so accept-time rotation tolerance is
17294
- * consistent across upfront and mid-job. Falls back to `undefined` when
17295
- * accumulator mode isn't wired (the receive path then rejects with
17296
- * `cashu_not_p2pk_locked`).
17307
+ * Mid-job and upfront receive paths use current plus grace-period retired keys.
17308
+ * Without accumulator mode this is undefined and the receive path refuses it.
17297
17309
  */
17298
17310
  async resolveLockPubkeysForReceive() {
17299
17311
  if (this.opts.disposableCashu && this.opts.lockPubkey) return [toLockPubkey(this.opts.lockPubkey)];
@@ -17308,12 +17320,8 @@ var DVMServer = class {
17308
17320
  }
17309
17321
  }
17310
17322
  /**
17311
- * Best-effort lock-pubkey state for read-only consumers (`/v1/info` and the
17312
- * accept path). Tries Postgres; on failure falls back to the last-good
17313
- * cached state, and only as a final resort to the env-derived seed in a
17314
- * single-entry array. The cache makes the degradation safe across a
17315
- * rotation — without it the env seed advertises and accepts the retired
17316
- * pubkey while the DB is unreachable.
17323
+ * Read Postgres first, then fall back to last-observed state, then the env seed.
17324
+ * Caching avoids reverting an observed rotation to the old seed during an outage.
17317
17325
  */
17318
17326
  async lockPubkeyStateForRead(db, dvmId) {
17319
17327
  try {
@@ -17325,22 +17333,9 @@ var DVMServer = class {
17325
17333
  }
17326
17334
  }
17327
17335
  /**
17328
- * Run the routine Tempo settlement cadence opportunistically, on ordinary
17329
- * traffic (internal-review).
17330
- *
17331
- * Routine settlement used to ride the per-DVM watcher machine's four-minute
17332
- * poll, which is exactly what kept an idle DVM awake and billing. It cannot
17333
- * move to a timer for the same reason — a timer on a machine that ought to
17334
- * be suspended either does not fire or defeats the suspension. So it rides
17335
- * requests instead, the way Lightning funding is credited (internal-review): a DVM
17336
- * with no traffic has nothing to settle on a 24-hour cadence anyway, and the
17337
- * forced-close guarantee does not depend on this path at all — that is the
17338
- * chain-event wake, which reaches a suspended DVM through Fly Proxy.
17339
- *
17340
- * Fire-and-forget and self-throttled: the request never waits on it, only
17341
- * one runs at a time, and a failure is logged rather than surfaced. The
17342
- * sweep re-checks each channel's own schedule, so the throttle here only
17343
- * bounds how often we pay for the store scan.
17336
+ * Opportunistic traffic avoids a timer that keeps an idle machine awake. Forced
17337
+ * close servicing is independent. Fire-and-forget, one sweep at a time; failures
17338
+ * are logged. The throttle bounds scans, not each channel's settlement schedule.
17344
17339
  */
17345
17340
  maybeSweepTempoSessions() {
17346
17341
  const sweep = this.opts.mpp?.sweepTempoSessions;
@@ -17372,7 +17367,7 @@ var DVMServer = class {
17372
17367
  });
17373
17368
  }
17374
17369
  /**
17375
- * Whether one advertised MPP method may still be offered (internal-review).
17370
+ * Whether one advertised MPP method may still be offered.
17376
17371
  *
17377
17372
  * Only `tempo/session` is gated, and only when this DVM is platform-hosted:
17378
17373
  * a session channel is safe to accept exactly while something is watching
@@ -17388,7 +17383,6 @@ var DVMServer = class {
17388
17383
  tempoSessionOffered(method) {
17389
17384
  return this.tempoSessionWithheldReason(method) === void 0;
17390
17385
  }
17391
- /** Stable caller-facing reason for withholding one newly opened session. */
17392
17386
  tempoSessionWithheldReason(method) {
17393
17387
  if (method.method !== "tempo" || method.intent !== "session") return void 0;
17394
17388
  if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
@@ -17399,7 +17393,6 @@ var DVMServer = class {
17399
17393
  }
17400
17394
  return void 0;
17401
17395
  }
17402
- /** Await only the cold-start reads each fail-closed session gate needs. */
17403
17396
  async ensureTempoSessionHealth() {
17404
17397
  await Promise.all([
17405
17398
  this.opts.tempoObserverHealth?.ensureFresh?.(),
@@ -17407,28 +17400,13 @@ var DVMServer = class {
17407
17400
  ]);
17408
17401
  }
17409
17402
  /**
17410
- * Whether this DVM is **configured** to take TIP-1034 session channels — the
17411
- * value the observer-health poll carries back to the platform, where it keeps
17412
- * `dvms.tempo_session_advertised_at` current between container deploys
17413
- * (internal-review).
17414
- *
17415
- * Deliberately the same question the platform's post-deploy `/v1/info` probe
17416
- * answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
17417
- * menu exists at all, and the configured MPP methods include `tempo/session`.
17418
- * Credit-scoped because sessions are confined to `POST /v1/credit` — a
17419
- * top-level session method with no credit menu is not a channel anyone can
17420
- * open, so it is not a candidate the payee fallback should reach for.
17421
- *
17422
- * It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
17423
- * and never {@link tempoSessionOffered}: what the column records is
17424
- * capability, not availability, so an observer blip must leave it alone. That
17425
- * distinction is the whole of internal-review, and answering it here from the gate
17426
- * would reintroduce it on a one-minute cadence.
17403
+ * Report configured session capability independently of current availability.
17404
+ * A temporary observer outage must not remove the platform's watch signal.
17405
+ * Credit menu, auth and session method configuration still gate that capability.
17427
17406
  */
17428
17407
  tempoSessionCapability() {
17429
17408
  if (!this.opts.mpp) return false;
17430
17409
  if (!creditMenuConfigured({
17431
- // The SDK server only ever runs the container runtime — see `creditMenu`.
17432
17410
  runtime: "container",
17433
17411
  config: this.descriptor.credit,
17434
17412
  hasAuth: !!this.descriptor.auth,
@@ -17442,25 +17420,11 @@ var DVMServer = class {
17442
17420
  );
17443
17421
  }
17444
17422
  /**
17445
- * `POST /_internal/tempo-close-event` — the forced-close wake path
17446
- * (internal-review), replacing the per-DVM `tempo-session-watch` Fly Machine.
17447
- *
17448
- * Deliberately **unauthenticated**, and that is the design rather than an
17449
- * omission: the body is an event locator, so the DVM re-reads the event from
17450
- * its own Tempo RPC and verifies chain, escrow, topic and local channel
17451
- * ownership before touching money (see `handleTempoCloseEvent`). An
17452
- * unverifiable locator can therefore do nothing, which is what lets the
17453
- * platform wake a DVM without holding a signing key or a recoverable copy of
17454
- * the DVM's bearer token.
17455
- *
17456
- * What an unauthenticated caller *can* do is spend the DVM's resources: each
17457
- * POST is an unthrottled outbound `eth_getTransactionReceipt` against the
17458
- * builder's own Tempo provider, billed to them, before anything about the
17459
- * locator has been checked — the verification needs the receipt to happen at
17460
- * all. So the cost of an open endpoint is metered RPC calls, not money: no
17461
- * locator can move value except one naming a real close of a channel this
17462
- * DVM owns, and settling that is what the routine cadence would do anyway.
17463
- * Watch the provider's quota rather than the ledger.
17423
+ * Unauthenticated wake carrying only an event locator. After parsing, verify
17424
+ * the receipt from the DVM's own RPC, including chain, escrow, topic and local
17425
+ * channel ownership before settlement. The observer needs no spending secret.
17426
+ * Syntactically valid requests can still consume unthrottled provider RPC quota;
17427
+ * malformed locators return 400 before that read.
17464
17428
  */
17465
17429
  registerTempoCloseEventRoute() {
17466
17430
  const mpp = this.opts.mpp;
@@ -17521,22 +17485,14 @@ var DVMServer = class {
17521
17485
  });
17522
17486
  }
17523
17487
  /**
17524
- * Compose the `/v1/info#builder` block from `opts.builder` (inline SDK
17525
- * config) with a fallback to deploy-injected env vars (internal-review). Each
17526
- * field is independently sourced (opts wins per-field, env fills gaps);
17527
- * all three of pubkey / attestation / signature must resolve to emit the
17528
- * block — partial state is treated as absent rather than half-emitted.
17529
- *
17530
- * Env-var form: `DVMKIT_BUILDER_ATTESTATION` is a JSON string (Fly secrets
17531
- * can't carry structured data), so it's parsed here. A malformed JSON
17532
- * string drops the block rather than throwing, so a corrupted env var
17533
- * can't take the DVM down.
17488
+ * Options override environment per field. Emit only when pubkey, attestation
17489
+ * and signature all exist; malformed attestation JSON omits the block.
17534
17490
  */
17535
17491
  builderAttestationBlock() {
17536
17492
  const builder = this.opts.builder;
17537
17493
  const env = this.opts.env;
17538
- const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY ?? process.env.DVMKIT_BUILDER_PUBKEY;
17539
- const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE ?? process.env.DVMKIT_BUILDER_SIGNATURE;
17494
+ const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
17495
+ const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
17540
17496
  const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
17541
17497
  if (!pubkey || !signature || !attestation) return null;
17542
17498
  return { pubkey, attestation, signature };
@@ -17568,7 +17524,7 @@ var DVMServer = class {
17568
17524
  throw new AuthAudienceError();
17569
17525
  }
17570
17526
  /**
17571
- * The slug every receipt is stamped with (internal-review). Read off the signed
17527
+ * The slug every receipt is stamped with. Read off the signed
17572
17528
  * attestation payload, which is the only place the *attested* slug lives —
17573
17529
  * a receipt claiming a different name than the attestation binds would be
17574
17530
  * unverifiable. Falls back to the descriptor name for dev/self-host boots
@@ -17579,14 +17535,13 @@ var DVMServer = class {
17579
17535
  const slug = attestation?.slug;
17580
17536
  return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
17581
17537
  }
17582
- /** Immutable identity written into every signed receipt. */
17583
17538
  receiptDvmId() {
17584
17539
  const attestation = this.builderAttestationBlock()?.attestation;
17585
17540
  const dvmId = attestation?.dvm_id;
17586
17541
  return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
17587
17542
  }
17588
17543
  parseAttestationFromEnv(env) {
17589
- const raw = env.DVMKIT_BUILDER_ATTESTATION ?? process.env.DVMKIT_BUILDER_ATTESTATION;
17544
+ const raw = env.DVMKIT_BUILDER_ATTESTATION;
17590
17545
  if (!raw) return null;
17591
17546
  try {
17592
17547
  const parsed = JSON.parse(raw);
@@ -17597,7 +17552,7 @@ var DVMServer = class {
17597
17552
  }
17598
17553
  }
17599
17554
  /**
17600
- * Owner display block for `/v1/info#owner` (internal-review). Resolved from
17555
+ * Owner display block for `/v1/info#owner`. Resolved from
17601
17556
  * `opts.owner` or env vars injected by the platform deploy pipeline.
17602
17557
  * Returns null when no owner identity is configured — pre-unification
17603
17558
  * DVMs that haven't been redeployed omit the block.
@@ -17605,11 +17560,11 @@ var DVMServer = class {
17605
17560
  ownerDisplayBlock() {
17606
17561
  const owner = this.opts.owner;
17607
17562
  const env = this.opts.env;
17608
- const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE ?? process.env.DVMKIT_OWNER_HANDLE;
17563
+ const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
17609
17564
  if (!handle) return null;
17610
- const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? process.env.DVMKIT_OWNER_TYPE ?? "builder";
17611
- const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? process.env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
17612
- const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? process.env.DVMKIT_OWNER_AVATAR_URL ?? null;
17565
+ const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
17566
+ const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
17567
+ const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
17613
17568
  return { handle, display_name: displayName, avatar_url: avatarUrl, type };
17614
17569
  }
17615
17570
  defaultPaymentMethods() {
@@ -17628,14 +17583,13 @@ var DVMServer = class {
17628
17583
  if (!this.opts.x402.batchSettlement?.selfRelay) return support;
17629
17584
  return { ...support, batchSettlement: true };
17630
17585
  }
17631
- /** Whether this DVM can state its price exactly in x402's USDC unit. */
17632
17586
  x402CurrencySupported() {
17633
17587
  return this.pricingCurrency === "usd";
17634
17588
  }
17635
17589
  /**
17636
17590
  * Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
17637
17591
  * override and otherwise deriving from wired rails. Single source of truth so
17638
- * the `/v1/info` and `/v1/quote` rail enumerations can't drift (internal-review).
17592
+ * the `/v1/info` and `/v1/quote` rail enumerations can't drift.
17639
17593
  */
17640
17594
  resolvePaymentMethods() {
17641
17595
  const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
@@ -17648,13 +17602,13 @@ var DVMServer = class {
17648
17602
  }
17649
17603
  /**
17650
17604
  * Rails a **credit top-up** may arrive on — the funding menu's list
17651
- * (internal-review). A superset of {@link resolvePaymentMethods}, and deliberately
17605
+ * A superset of {@link resolvePaymentMethods}, and deliberately
17652
17606
  * a separate read: `lightning` funds a credit but is not an attached-proof
17653
17607
  * wire method, so it must never reach `/v1/info` or `/v1/quote`'s
17654
- * `payment.methods` (internal-review stands).
17608
+ * `payment.methods`.
17655
17609
  *
17656
- * The rail drops out when the receive wallet is unreachable (spec §4 menu
17657
- * degradation) — advertising it then would hand the caller an option that
17610
+ * The rail drops out of the funding menu when the receive wallet is unreachable;
17611
+ * advertising it then would hand the caller an option that
17658
17612
  * fails on use, while the other rails would have closed the sale.
17659
17613
  */
17660
17614
  resolveFundingMethods() {
@@ -17673,7 +17627,7 @@ var DVMServer = class {
17673
17627
  }
17674
17628
  /**
17675
17629
  * The credit menu's MPP instruments, split into what is on offer right now
17676
- * and what this DVM is configured for but withholding (internal-review).
17630
+ * and what this DVM is configured for but withholding.
17677
17631
  *
17678
17632
  * The split has to survive into the response rather than being filtered
17679
17633
  * away, because "not offered right now" and "not configured" are different
@@ -17701,7 +17655,7 @@ var DVMServer = class {
17701
17655
  }
17702
17656
  /**
17703
17657
  * Which x402 flavours a credit top-up may arrive on — the funding menu's
17704
- * `x402` sub-block (internal-review). Hosted health buys `exact`; a genuinely wired
17658
+ * `x402` sub-block. Hosted health buys `exact`; a genuinely wired
17705
17659
  * batch server buys `batch-settlement` independently, including while the
17706
17660
  * hosted facilitator is unavailable or stale.
17707
17661
  *
@@ -17718,13 +17672,12 @@ var DVMServer = class {
17718
17672
  ...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
17719
17673
  ];
17720
17674
  }
17721
- /** Whether a channel server is both supported and constructible for credit funding. */
17722
17675
  x402BatchAvailable() {
17723
17676
  return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
17724
17677
  }
17725
17678
  /**
17726
17679
  * Apply any Lightning funding invoice this caller has paid since we last
17727
- * looked — the pull half of pull-based crediting (internal-review, spec §4).
17680
+ * looked — the pull half of pull-based crediting.
17728
17681
  *
17729
17682
  * Called from every authenticated surface where the balance is about to
17730
17683
  * matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
@@ -17737,7 +17690,7 @@ var DVMServer = class {
17737
17690
  * outstanding, and never blocks the request when it fails.
17738
17691
  *
17739
17692
  * The SDK-owned reporter queues each Lightning deposit inside
17740
- * `settleInvoice`'s transaction (internal-review). The loop below only preserves
17693
+ * `settleInvoice`'s transaction. The loop below only preserves
17741
17694
  * the explicit `onCreditFunded` override, whose caller owns its durability.
17742
17695
  */
17743
17696
  async settleLightningFunding(callerPubkey, creditId) {
@@ -17768,7 +17721,6 @@ var DVMServer = class {
17768
17721
  });
17769
17722
  }
17770
17723
  }
17771
- /** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
17772
17724
  creditTtlMs() {
17773
17725
  return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
17774
17726
  }
@@ -17789,7 +17741,7 @@ var DVMServer = class {
17789
17741
  * The per-DVM + per-request rail wiring every payment verification needs —
17790
17742
  * everything except what the *operation* contributes (its price, its job or
17791
17743
  * fund id, the caller's credit fields). Shared by `/v1/job`'s upfront gate
17792
- * and `/v1/credit`'s top-up (internal-review) so a rail wired into one is wired
17744
+ * and `/v1/credit`'s top-up so a rail wired into one is wired
17793
17745
  * into both by construction; a divergence here would be a rail that funds
17794
17746
  * jobs but silently can't fund credits.
17795
17747
  */
@@ -17822,7 +17774,7 @@ var DVMServer = class {
17822
17774
  mppCredential,
17823
17775
  mpp: this.opts.mpp,
17824
17776
  resourcePath: c.req.path,
17825
- // internal-review/internal-review: the gate covers issuance and new-channel acceptance,
17777
+ // the gate covers issuance and new-channel acceptance,
17826
17778
  // not just the two advertisement surfaces. `ensureFresh` above makes this
17827
17779
  // an observation rather than a cold-start default on the one surface that
17828
17780
  // can act on it.
@@ -17840,8 +17792,6 @@ var DVMServer = class {
17840
17792
  requestId: c.req.header("X-Cashu-Request-Id"),
17841
17793
  mintHealthTracker: this.mintHealthTracker,
17842
17794
  fxFetcher: this.fxFetcher,
17843
- // internal-review: the ledger leg — every rail payment becomes fund_credit +
17844
- // draw, atomic with the rail commit.
17845
17795
  creditLedger: this.creditLedger,
17846
17796
  processedPayments: this.processedPayments,
17847
17797
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
@@ -17851,7 +17801,7 @@ var DVMServer = class {
17851
17801
  }
17852
17802
  /**
17853
17803
  * Assemble this DVM's funding menu for a response denominated in
17854
- * `currency` (internal-review). All the refusal logic — including the credits-spec
17804
+ * `currency`. All the refusal logic — including the credits-spec
17855
17805
  * §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
17856
17806
  * server's state. `callerPubkey` must be a *verified* envelope pubkey: the
17857
17807
  * balance echo is per-caller money and there is no safe fallback identity.
@@ -17860,8 +17810,6 @@ var DVMServer = class {
17860
17810
  await this.ensureTempoSessionHealth();
17861
17811
  const mppSplit = this.creditMppSplit();
17862
17812
  return buildCreditMenu({
17863
- // The SDK server only ever runs the container runtime; the isolate route
17864
- // calls the same builder with `"isolate"` and is refused there.
17865
17813
  runtime: "container",
17866
17814
  config: this.descriptor.credit,
17867
17815
  currency,
@@ -17870,7 +17818,7 @@ var DVMServer = class {
17870
17818
  tempoWithheld: mppSplit.withheld,
17871
17819
  x402Schemes: this.creditX402Schemes(),
17872
17820
  lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
17873
- // Configured, not advertised (internal-review): `advertisedMints()` is health
17821
+ // Configured, not advertised: `advertisedMints()` is health
17874
17822
  // filtered, and a rail that comes and goes with a mint's uptime is worse
17875
17823
  // product than one that is honestly absent.
17876
17824
  hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
@@ -17907,7 +17855,7 @@ var DVMServer = class {
17907
17855
  }
17908
17856
  /**
17909
17857
  * Currency this DVM's credit ledger is denominated in — the builder's
17910
- * declared pricing currency, `"usd"` unless they said otherwise (internal-review).
17858
+ * declared pricing currency, `"usd"` unless they said otherwise.
17911
17859
  *
17912
17860
  * Every denomination surface reads this one answer: the funding menu on
17913
17861
  * `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
@@ -17916,20 +17864,13 @@ var DVMServer = class {
17916
17864
  * static (USD-literal) price on a non-USD DVM, and a quote denominated
17917
17865
  * elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
17918
17866
  * is what a top-up funded in, on every path.
17919
- *
17920
- * It used to return the literal `"usd"`, which is how a EUR-pricing DVM
17921
- * came to fund `/v1/credit` top-ups in a denomination none of its jobs
17922
- * would ever draw from.
17923
17867
  */
17924
17868
  creditCurrency() {
17925
17869
  return this.pricingCurrency;
17926
17870
  }
17927
17871
  /**
17928
- * Quote-response `payment` block (internal-review). Carries the same `methods` array
17929
- * `/v1/info` advertises plus a per-rail `refundable` map keyed by method, so a
17930
- * caller can see which rails return funds on failure before committing. The
17931
- * `methods` shape stays identical to `/v1/info` and the platform isolate route
17932
- * (post-internal-review nested `payment.methods`); refundability rides alongside.
17872
+ * Advertise the same method list as info. Refundability describes the rail
17873
+ * itself; failed jobs release their pending credit draw separately.
17933
17874
  */
17934
17875
  quotePaymentBlock() {
17935
17876
  const methods = this.resolvePaymentMethods();
@@ -17938,8 +17879,6 @@ var DVMServer = class {
17938
17879
  refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
17939
17880
  };
17940
17881
  }
17941
- // ── Route handlers ──────────────────────────────────────────────────
17942
- /** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
17943
17882
  async handleProxy(c) {
17944
17883
  const raw = getCookie(c, "dvmkit_target");
17945
17884
  const target = raw ? decodeURIComponent(raw) : null;
@@ -17975,8 +17914,8 @@ var DVMServer = class {
17975
17914
  auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
17976
17915
  ...this.authAudience ? { auth_audience: this.authAudience } : {},
17977
17916
  protocol_version: 1,
17978
- sdk_sha: process.env.GIT_SHA ?? "unknown",
17979
- deploy_id: process.env.DVMKIT_DEPLOY_ID ?? "unknown"
17917
+ sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
17918
+ deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17980
17919
  };
17981
17920
  const ownerBlock = this.ownerDisplayBlock();
17982
17921
  if (ownerBlock) info.owner = ownerBlock;
@@ -17997,8 +17936,7 @@ var DVMServer = class {
17997
17936
  network: this.opts.x402.network,
17998
17937
  asset: this.opts.x402.asset ?? "USDC",
17999
17938
  ...this.opts.x402.facilitator && { facilitator: this.opts.x402.facilitator },
18000
- // Surface dual-serve state so x402 clients can confirm the 402 will
18001
- // carry their PaymentRequirements before submitting (internal-review).
17939
+ // Advertises whether 402 challenges include x402 PaymentRequirements.
18002
17940
  dual_serve: this.opts.x402.dualServe ?? true
18003
17941
  };
18004
17942
  }
@@ -18009,11 +17947,11 @@ var DVMServer = class {
18009
17947
  return reason ? [{ ...method, reason }] : [];
18010
17948
  });
18011
17949
  info.tempo = {
18012
- // internal-review: withheld sessions are withheld here too. `/v1/info` is
17950
+ // withheld sessions are withheld here too. `/v1/info` is
18013
17951
  // where a caller decides what to attempt, so advertising a session the
18014
17952
  // credit menu will refuse just sends them down a dead end.
18015
17953
  methods: configured.filter((method) => this.tempoSessionOffered(method)),
18016
- // …but say so rather than letting it vanish (internal-review): an agent that
17954
+ // …but say so rather than letting it vanish: an agent that
18017
17955
  // sees only the shortened list cannot tell a DVM that never did
18018
17956
  // sessions from one whose observer is down for a minute.
18019
17957
  ...withheld.length && { withheld },
@@ -18045,12 +17983,8 @@ var DVMServer = class {
18045
17983
  return c.json(info);
18046
17984
  }
18047
17985
  /**
18048
- * Render the `/v1/info.capabilities` block (internal-review). Each capability
18049
- * advertises its `description`, optional `input_schema`, and optional
18050
- * `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
18051
- * `pricing.max` for static-priced ones. Capabilities without an `input`
18052
- * schema or any pricing surface omit those fields entirely; free
18053
- * capabilities (no `price`) omit `pricing` entirely.
17986
+ * Input-mode JSON Schema keeps defaulted fields optional on the advertised
17987
+ * wire contract. Capability names are the descriptor's keys.
18054
17988
  */
18055
17989
  renderCapabilities() {
18056
17990
  const out = {};
@@ -18251,11 +18185,8 @@ var DVMServer = class {
18251
18185
  return c.json(res);
18252
18186
  }
18253
18187
  /**
18254
- * Build the {@link QuoteContext} a pricing handler receives. Extracted so
18255
- * the two callers — `/v1/quote` and `computeDynamicPrice` on the submit
18256
- * path — cannot drift: a field added for one is present in the other by
18257
- * construction, and both paths price against the same view of the caller's
18258
- * credit (internal-review, spec §7).
18188
+ * Callers supply quote-schema-parsed data when a quote schema exists, alongside
18189
+ * raw input text and params. Quote and job pricing share these runtime helpers.
18259
18190
  */
18260
18191
  async buildQuoteContext(args) {
18261
18192
  const ctx = {
@@ -18277,16 +18208,8 @@ var DVMServer = class {
18277
18208
  return ctx;
18278
18209
  }
18279
18210
  /**
18280
- * Validate the `capability` field on a `/v1/job` or `/v1/quote` body
18281
- * (internal-review). Returns the canonical name when set + known; otherwise a
18282
- * structured 400 envelope agents can relay verbatim to a human caller.
18283
- *
18284
- * - **Missing** — `body.capability` is absent or empty. Surface as
18285
- * `capability_required` with a hint pointing at `/v1/info` so the agent
18286
- * can re-discover the capability set.
18287
- * - **Unknown** — `body.capability` is set but doesn't match any
18288
- * capability on this descriptor. Surface as `unknown_capability` listing
18289
- * the known names so the agent can correct without a round-trip.
18211
+ * Missing or unknown capabilities return structured errors with the current
18212
+ * info-schema URL.
18290
18213
  */
18291
18214
  requireCapabilityFromBody(c, capability) {
18292
18215
  const known = this.jobManager.capabilityNames();
@@ -18345,25 +18268,9 @@ var DVMServer = class {
18345
18268
  }
18346
18269
  }
18347
18270
  /**
18348
- * The one `invalid_input` body `/v1/quote` and `/v1/job` emit for a
18349
- * capability-input violation (internal-review).
18350
- *
18351
- * The two endpoints used to answer the same caller mistake with two unrelated
18352
- * shapes — quote gated on auth first, so a wrong field name came back 401
18353
- * `auth_error` + `sub_reason: "schema_invalid"`, while submit parsed first and
18354
- * came back 400 `invalid_input`. Both named the offending field (internal-review), so
18355
- * neither was wrong; an agent just had to recognise two codes and two statuses
18356
- * for one class of mistake, and the CLI's `parseStructuredError` mapped them to
18357
- * two different `DvmError`s — which is why its structured-input remediation
18358
- * (`invalid_input` only) never fired for the live fleet failure internal-review was
18359
- * written about.
18360
- *
18361
- * The 400 wins because it is the only shape both endpoints can produce: a DVM
18362
- * that declares no `auth` has no 401 to give, and quote didn't validate
18363
- * `cap.input` at all on that path, so it answered 200 to a body submit would
18364
- * reject. `display` is carried here as well as `message` because this is now
18365
- * the shape agents branch on, and it reuses the 401's sentence so the words
18366
- * relayed to a human don't change with the status.
18271
+ * Shared schema-violation response for quote and submit, including auth-less
18272
+ * DVMs. A 400 classifies caller input without requiring an auth gate; `display`
18273
+ * and `message` carry the same relayable explanation.
18367
18274
  */
18368
18275
  invalidInputResponse(c, err) {
18369
18276
  const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
@@ -18406,22 +18313,9 @@ var DVMServer = class {
18406
18313
  );
18407
18314
  }
18408
18315
  /**
18409
- * True when a failed capability-input parse is the auth gate's answer rather
18410
- * than {@link invalidInputResponse}'s (internal-review).
18411
- *
18412
- * A capability schema may declare the signing-envelope fields required
18413
- * (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
18414
- * the required shape an *unsigned* request fails the input parse naming
18415
- * `pubkey` / `signature` — a signing failure, and answering it with "your
18416
- * input is invalid: pubkey required" is the internal-review mistake in reverse. So a
18417
- * parse whose issues are **only** envelope-side falls through to the auth
18418
- * gate, which answers 401 `signature_invalid` with the remedy that works.
18419
- *
18420
- * Deferring is safe because the gate below re-parses the same schema over the
18421
- * same bytes, so a caller that reaches it with an envelope-side failure is
18422
- * rejected there for certain; both routes still carry a backstop for the
18423
- * impossible case. It applies only to a DVM that declares `auth` — with no
18424
- * gate below, the 400 is the only answer available.
18316
+ * On authenticated DVMs, envelope-only parse failures defer to auth
18317
+ * (401 signature_invalid). Builder input issues remain 400 invalid_input.
18318
+ * Auth reparses the same schema/raw input; a backstop refuses disagreement.
18425
18319
  */
18426
18320
  deferToAuthGate(err) {
18427
18321
  if (!this.descriptor.auth) return false;
@@ -18430,13 +18324,13 @@ var DVMServer = class {
18430
18324
  }
18431
18325
  /**
18432
18326
  * Mints to advertise to clients on `/v1/info` and `/v1/quote`. Filters the
18433
- * configured list through the runtime health tracker (internal-review). When every
18327
+ * configured list through the runtime health tracker. When every
18434
18328
  * mint is currently sick it falls back to the still-configured list rather
18435
18329
  * than publish an empty rail — a *transient* outage shouldn't read as "no
18436
18330
  * Cashu" and push well-behaved agents to bypass the rail. But NUT-incompatible
18437
- * mints are excluded even from that fallback (internal-review): NUT-incompatibility
18331
+ * mints are excluded even from that fallback: NUT-incompatibility
18438
18332
  * is a permanent, deterministic config error the DVM can never settle
18439
- * against, so per AC bullet 3 such a mint is never advertised. If *every*
18333
+ * against, so such a mint is never advertised. If *every*
18440
18334
  * configured mint is NUT-incompatible the rail correctly goes empty.
18441
18335
  */
18442
18336
  advertisedMints() {
@@ -18865,10 +18759,6 @@ var DVMServer = class {
18865
18759
  priceMsats,
18866
18760
  capabilityName,
18867
18761
  requesterTokenHash,
18868
- // internal-review: everything the idempotent-replay path needs to recognise a
18869
- // retry of THIS call and re-issue the token its lost response carried.
18870
- // internal-review: `jobId` is the pre-allocated id the payment's ledger draw
18871
- // already committed under.
18872
18762
  {
18873
18763
  requesterToken: jobToken,
18874
18764
  requestFingerprint: fingerprint,
@@ -18906,7 +18796,7 @@ var DVMServer = class {
18906
18796
  }
18907
18797
  }
18908
18798
  /**
18909
- * Book a committed funding as a deposit (internal-review, spec §10). Fire-and-forget
18799
+ * Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
18910
18800
  * onto the reporter's durable queue: a funding is a liability the platform
18911
18801
  * must show, but a reporting hiccup must never fail the paid request that
18912
18802
  * just succeeded.
@@ -18921,7 +18811,7 @@ var DVMServer = class {
18921
18811
  );
18922
18812
  }
18923
18813
  /**
18924
- * Book a kept short payment as tagged revenue (internal-review, operator ruling).
18814
+ * Book a kept short payment as tagged revenue.
18925
18815
  * The proofs are committed and the draw was settled on the spot, so the money
18926
18816
  * is earned — but no job exists to carry it, hence the synthetic job id. It
18927
18817
  * is deliberately **not** a deposit: nothing is owed back, so a liability
@@ -18951,20 +18841,9 @@ var DVMServer = class {
18951
18841
  });
18952
18842
  }
18953
18843
  /**
18954
- * The idempotent half of `POST /v1/job` (internal-review). A paid submit whose
18955
- * response the caller lost is safe to retry — the accumulator's
18956
- * `UNIQUE(dvm_id, request_id, proof_secret)` makes the duplicate a no-op —
18957
- * but until now it was also useless: the caller got a 409 and the `job_id` /
18958
- * `job_token` it paid for stayed unreachable forever.
18959
- *
18960
- * Returns the original job's submit response iff the retry proves it is the
18961
- * original caller. Proof is reproducing the request, **not** possessing the
18962
- * spent Cashu token: the token is a bearer secret, so gating on it alone
18963
- * would hand the job (and its result) to anyone who lifted it. The legitimate
18964
- * retry always has the body — the internal-review resume sweep re-POSTs it — while a
18965
- * token thief does not. On a descriptor-auth DVM the caller pubkey must match
18966
- * too. Anything short of that falls through to the 409 the caller has always
18967
- * gotten.
18844
+ * Recover a lost paid response only for matching request fingerprint,
18845
+ * authenticated pubkey and requester identity. A spent bearer token alone
18846
+ * is not proof of request ownership.
18968
18847
  */
18969
18848
  async recoverReplayedJob(c, opts) {
18970
18849
  if (!opts.requestId) return void 0;
@@ -18988,16 +18867,9 @@ var DVMServer = class {
18988
18867
  return buildSubmitResponse(c, prior, prior.requesterToken);
18989
18868
  }
18990
18869
  /**
18991
- * The draw-keyed half of idempotent recovery (internal-review, spec §2 condition
18992
- * 1). The signed body's `(credit_id, draw_id)` matched a recorded draw;
18993
- * resolve the job it paid for (the draw carries its pre-allocated job id)
18994
- * and re-issue the original response under the SAME proof-of-origin gates
18995
- * as the header regime: reproduce the fingerprint, sign as the same caller
18996
- * (explicit draws only exist on descriptor-auth DVMs), match requesterId.
18997
- *
18998
- * A recorded draw whose job row never landed (crash between the ledger
18999
- * commit and the job persist) has nothing to hand back — refuse with the
19000
- * 409 and log loudly; the pending hold is surfaced by `listPendingDraws`.
18870
+ * Resolve the recorded draw's job under the same fingerprint/pubkey/requester
18871
+ * checks as header recovery. If no job exists, the caller may still recover a
18872
+ * pending durable acceptance under its bound public request-id claim.
19001
18873
  */
19002
18874
  async recoverDrawnJob(c, opts) {
19003
18875
  let jobId = opts.jobId;
@@ -19078,7 +18950,6 @@ var DVMServer = class {
19078
18950
  }
19079
18951
  return void 0;
19080
18952
  }
19081
- /** Recover one still-pending draw recorded for the stable public job id. */
19082
18953
  async recoverDrawAcceptance(opts) {
19083
18954
  const dvmId = this.opts.dvmId;
19084
18955
  if (!dvmId) return void 0;
@@ -19108,12 +18979,9 @@ var DVMServer = class {
19108
18979
  return payment;
19109
18980
  }
19110
18981
  /**
19111
- * Recompute the upfront price for a dynamic-priced capability by
19112
- * re-invoking its `onQuote.handler` with the parsed job input as `data`
19113
- * (internal-review). Returns a 400-style error envelope when the input doesn't
19114
- * satisfy `onQuote.schema` or the handler throws — failing closed is the
19115
- * right default, since the alternative (running the job with
19116
- * `requiredMsats=undefined`) lets unpaid jobs through.
18982
+ * Recompute dynamic upfront pricing from parsed job input, applying the quote
18983
+ * schema separately. Refuse invalid input/results, currency mismatch or missing
18984
+ * FX rather than running without a price (400/500/503 as classified below).
19117
18985
  */
19118
18986
  async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey) {
19119
18987
  let data = parsedInput;
@@ -19319,13 +19187,6 @@ var DVMServer = class {
19319
19187
  }
19320
19188
  return c.json({ messages: job.messages.filter((m) => m.seq > after) });
19321
19189
  }
19322
- /**
19323
- * Structured 409 for a write (message POST / cancel) against a job that has
19324
- * already reached a terminal status (internal-review). The two-audiences convention
19325
- * wants a `code` an intermediary agent can branch on plus a `hint` it can
19326
- * relay verbatim — a bare `{ error }` string gave the openclaw nothing to act
19327
- * on and it re-paid a completed job into this 409 (internal-review step 7).
19328
- */
19329
19190
  jobNotActiveResponse(c) {
19330
19191
  return c.json(
19331
19192
  {
@@ -19498,8 +19359,8 @@ var DVMServer = class {
19498
19359
  }
19499
19360
  /**
19500
19361
  * Tear down this machine's in-memory reactivation scaffold when it stands
19501
- * down (lost the reactivation advisory lock, or deferred to a live original
19502
- * handler that won the single-execution claim — internal-review / internal-review). Leaving
19362
+ * down after losing the reactivation advisory lock or deferring to a live original
19363
+ * handler that won the single-execution claim. Leaving
19503
19364
  * the job in `activeJobs` with a live idle timer would, one idle window
19504
19365
  * later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
19505
19366
  * must not persist our stale snapshot; clearing the appender prevents that.
@@ -19535,6 +19396,7 @@ var DVMServer = class {
19535
19396
  await jm.finalizeStoredTerminal(id);
19536
19397
  return c.json({ status: "cancelled" });
19537
19398
  }
19399
+ record.endedBy = "caller";
19538
19400
  record.status = "cancelled";
19539
19401
  record.lastActivityAt = Date.now();
19540
19402
  await jm.store.save(record);
@@ -19880,7 +19742,7 @@ var PlatformTempoObserverHealth = class {
19880
19742
  }
19881
19743
  }
19882
19744
  /**
19883
- * Register one mount's session capability (internal-review).
19745
+ * Register one mount's session capability.
19884
19746
  *
19885
19747
  * A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
19886
19748
  * platform row that token identifies is the deployment as a whole — so the
@@ -19896,16 +19758,9 @@ var PlatformTempoObserverHealth = class {
19896
19758
  };
19897
19759
  }
19898
19760
  /**
19899
- * Prime the signal during boot without making a menu request wait.
19900
- *
19901
- * De-dupes on the probe in flight, but only while that probe still speaks for
19902
- * the current set of declarations. `host.ts` primes this client before any
19903
- * mount exists, so at boot there is routinely an open probe carrying **no**
19904
- * `tempo_session` at all — handing it back to the mount that just declared
19905
- * one would leave a scale-to-zero DVM unstamped until traffic arrived, which
19906
- * is exactly the container-restart case internal-review is about. Same shape with
19907
- * two mounts: the second would join a probe whose URL already read the first
19908
- * one's `false`.
19761
+ * De-duplicate probes only while their declaration epoch is current. A mount
19762
+ * declared during boot or another mount's probe must await a fresh probe so
19763
+ * the platform receives the updated capability, even before traffic arrives.
19909
19764
  */
19910
19765
  async refresh() {
19911
19766
  const open = this.inFlight;
@@ -19951,7 +19806,7 @@ var PlatformTempoObserverHealth = class {
19951
19806
  }
19952
19807
  /**
19953
19808
  * The probe URL, carrying this deployment's session capability when a mount
19954
- * has declared one (internal-review).
19809
+ * has declared one.
19955
19810
  *
19956
19811
  * Absent means "no signal", not "no sessions": a container running an older
19957
19812
  * SDK build sends nothing, and the platform must leave its stamp exactly