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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-BIP6G74V.js → chunk-2ATUAUAO.js} +8 -8
  3. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  4. package/dist/{chunk-EVBK675R.js → chunk-6GRIKOFB.js} +28 -23
  5. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-FT6HTUM4.js} +1 -1
  7. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  8. package/dist/{chunk-U6M3ATSG.js → chunk-JDT5LCJC.js} +40 -6
  9. package/dist/{chunk-FROTD5XQ.js → chunk-JLXYOV4Y.js} +1 -2
  10. package/dist/{chunk-M7LHFJ5K.js → chunk-KMZXTBLA.js} +2 -2
  11. package/dist/{chunk-6BQM7TOW.js → chunk-L67WTZX2.js} +3 -7
  12. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  13. package/dist/{chunk-JZWELPFH.js → chunk-MRAGS5VP.js} +1 -1
  14. package/dist/{chunk-2UUXIIOC.js → chunk-O2X2CCKH.js} +3 -3
  15. package/dist/{chunk-TQWGQCNV.js → chunk-OMIQMMME.js} +3 -3
  16. package/dist/{chunk-5PBOA25N.js → chunk-PCUQZDZA.js} +436 -355
  17. package/dist/{chunk-KVEHHC7W.js → chunk-PHHAYRQV.js} +7 -9
  18. package/dist/{chunk-SSSZUVWM.js → chunk-QP53RWAD.js} +88 -38
  19. package/dist/{chunk-DMNLFNTW.js → chunk-QT4ONTST.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-MLRCSJYX.js → chunk-V7EVFLAK.js} +87 -90
  22. package/dist/{chunk-E4EVGPDX.js → chunk-XQXJKJ3P.js} +0 -2
  23. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-5ZEJRI46.js} +1 -1
  24. package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-D4Gcdgc4.d.ts} +487 -643
  25. package/dist/{fx-D860pZvP.d.ts → fx-B0SLBe5x.d.ts} +38 -82
  26. package/dist/index.d.ts +11 -14
  27. package/dist/index.js +2 -2
  28. package/dist/internal/caller.d.ts +618 -1525
  29. package/dist/internal/caller.js +28 -60
  30. package/dist/internal/server.d.ts +36 -61
  31. package/dist/internal/server.js +11 -11
  32. package/dist/{job-store-BUGqvCfL.d.ts → job-store-B2uZvga4.d.ts} +70 -59
  33. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-CQBnQgsT.d.ts} +19 -27
  34. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-ZOH6C3N4.js} +2 -2
  35. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-IOJBF7DB.js} +1 -1
  36. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-5PIRYFVQ.js} +1 -1
  37. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-ISRHBMOU.js} +1 -1
  38. package/dist/{postgres-job-store-3RAXMNSY.js → postgres-job-store-OGQ6IT4U.js} +1 -1
  39. package/dist/{postgres-kv-store-JFBDP5IP.js → postgres-kv-store-D5E2EZ24.js} +1 -1
  40. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-IZFLTTAC.js} +1 -1
  41. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  42. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-FIDI3RNH.js} +1 -1
  43. package/dist/{revenue-reporter-ASZ7SHHH.js → revenue-reporter-NNCNRY4C.js} +1 -1
  44. package/dist/server/index.d.ts +53 -59
  45. package/dist/server/index.js +38 -37
  46. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  47. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-CXg7ziML.d.ts} +389 -551
  48. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-76TDAF34.js} +1 -1
  49. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-DXM7QXJQ.js} +3 -3
  50. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-O67H5M4N.js} +2 -2
  51. package/dist/testing/index.d.ts +5 -15
  52. package/dist/testing/index.js +4 -11
  53. package/dist/{usd-DoRuAckA.d.ts → usd-BNDg1715.d.ts} +14 -16
  54. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  55. package/dist/{x402-5H27DCBE.js → x402-7S2EFINY.js} +2 -2
  56. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MemoryProcessedPaymentStore,
3
3
  ProcessedPaymentReplayError
4
- } from "./chunk-DMNLFNTW.js";
4
+ } from "./chunk-QT4ONTST.js";
5
5
  import {
6
6
  createX402FacilitatorAuthHeaders,
7
7
  createX402FacilitatorClient,
@@ -10,7 +10,7 @@ import {
10
10
  } from "./chunk-BQ2NMWKE.js";
11
11
  import {
12
12
  MemoryCreditLedger
13
- } from "./chunk-KVEHHC7W.js";
13
+ } from "./chunk-PHHAYRQV.js";
14
14
  import {
15
15
  SIGNED_ENVELOPE_FIELDS,
16
16
  SignedRequestError,
@@ -35,7 +35,7 @@ import {
35
35
  localMessage,
36
36
  providerMessage,
37
37
  toJobRecord
38
- } from "./chunk-U6M3ATSG.js";
38
+ } from "./chunk-JDT5LCJC.js";
39
39
  import {
40
40
  ADMIN_AUTH_CLOCK_SKEW_SECONDS,
41
41
  ADMIN_AUTH_NONCE_TTL_SECONDS,
@@ -100,7 +100,7 @@ import {
100
100
  pinDimension,
101
101
  withSpan,
102
102
  withTraceContext
103
- } from "./chunk-E4EVGPDX.js";
103
+ } from "./chunk-XQXJKJ3P.js";
104
104
  import {
105
105
  FxRateUnavailableError,
106
106
  InvalidCurrencyError,
@@ -109,7 +109,7 @@ import {
109
109
  fxRateFor,
110
110
  resolveFxSourceFromEnv,
111
111
  validateCurrency
112
- } from "./chunk-27V2ILSR.js";
112
+ } from "./chunk-4A2RAKCW.js";
113
113
  import {
114
114
  fiatToSatsCeil,
115
115
  parseUsdPrice,
@@ -130,7 +130,7 @@ import {
130
130
  isNonChannelBitcoinRail,
131
131
  netOwedSats,
132
132
  x402SettlementPending
133
- } from "./chunk-MLRCSJYX.js";
133
+ } from "./chunk-V7EVFLAK.js";
134
134
  import {
135
135
  withSdkInitLock
136
136
  } from "./chunk-S3XAHZQY.js";
@@ -141,7 +141,7 @@ import {
141
141
  advertisedMethods,
142
142
  challengeMeta,
143
143
  isTempoInsufficientFundsError
144
- } from "./chunk-M7LHFJ5K.js";
144
+ } from "./chunk-KMZXTBLA.js";
145
145
 
146
146
  // src/sdk/server/lock-pubkey-store.ts
147
147
  async function initLockPubkeyTable(db) {
@@ -883,7 +883,7 @@ async function buildCreditMenu(args) {
883
883
  funding,
884
884
  // Rendered when either half has something to say: a DVM configured for
885
885
  // sessions only would otherwise lose the whole block the moment the gate
886
- // closed, taking its capability signal with it (internal-review).
886
+ // closed, taking its capability signal with it.
887
887
  ...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
888
888
  tempo: {
889
889
  methods: tempoMethods,
@@ -896,7 +896,7 @@ async function buildCreditMenu(args) {
896
896
  // are the same misadvertisement in the other direction.
897
897
  ...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
898
898
  };
899
- if (args.funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
899
+ if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
900
900
  try {
901
901
  const fx = await args.fxFetcher.fetch();
902
902
  menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
@@ -905,7 +905,7 @@ async function buildCreditMenu(args) {
905
905
  "credit_menu_lightning_floor_unavailable",
906
906
  `credit menu degraded: could not express the Lightning funding floor in ${currency}, so the lightning rail is omitted from the funding menu until a rate is available.`
907
907
  );
908
- menu.funding = args.funding.filter((rail) => rail !== "lightning");
908
+ menu.funding = menu.funding.filter((rail) => rail !== "lightning");
909
909
  }
910
910
  }
911
911
  const echo = await selectPrimaryCredit({
@@ -1950,7 +1950,7 @@ async function createX402BatchSettlementServer(opts) {
1950
1950
  ...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
1951
1951
  // Every claim this fleet builds is capped at earned draws — the claim
1952
1952
  // timer, `claimAndSettle()`, and the withdraw-race rush claim all route
1953
- // through this selector (internal-review). A builder-supplied
1953
+ // through this selector. A builder-supplied
1954
1954
  // `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
1955
1955
  // a money invariant, not a policy knob. Upstream's refund job builds its
1956
1956
  // bundled claim from storage instead and never consults a selector, which
@@ -2071,14 +2071,13 @@ async function createX402BatchSettlementServer(opts) {
2071
2071
  // move money. The voucher base does not: a cooperatively drained and
2072
2072
  // reused channel can present the same cumulative maximum again under a
2073
2073
  // fresh authorization, which made the second legitimate deposit
2074
- // collide with the first row (internal-review). Conversely, keying by the
2074
+ // collide with the first row. Conversely, keying by the
2075
2075
  // credit effect would let one signed authorization open several rows.
2076
2076
  //
2077
2077
  // A cooperative refund belongs to one caller drain: after an operator
2078
2078
  // writes a failed drain off, the unchanged channel can legitimately
2079
2079
  // present the same voucher base under a fresh drain id. Keep that new
2080
2080
  // effect out of the old row's `(operation, payment_id)` replay slot
2081
- // (internal-review).
2082
2081
  paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
2083
2082
  amount: matched.amount
2084
2083
  };
@@ -3631,7 +3630,7 @@ async function verifyUpfrontPayment(opts) {
3631
3630
  // Nothing settled, so the EIP-3009 nonce is unspent and the same
3632
3631
  // header is still payable — unlike the post-settlement branch
3633
3632
  // below, where saying this would be telling the caller to pay
3634
- // twice (internal-review).
3633
+ // twice.
3635
3634
  hint: `Nothing was settled \u2014 re-sign this request with fund.amount_micro set to expected_micro (${predicted}), re-attach this same X-PAYMENT header, and resubmit.`
3636
3635
  }
3637
3636
  );
@@ -3652,7 +3651,7 @@ async function verifyUpfrontPayment(opts) {
3652
3651
  x402Requirements
3653
3652
  });
3654
3653
  }
3655
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
3654
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
3656
3655
  const receipt = await verifyX402Payment(
3657
3656
  x402Payment,
3658
3657
  x402Config,
@@ -3689,7 +3688,7 @@ async function verifyUpfrontPayment(opts) {
3689
3688
  `fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
3690
3689
  // `expected_micro` states the valuation, but carries no corrective
3691
3690
  // hint here: the facilitator already settled, so re-presenting
3692
- // this credential would be a second payment (internal-review).
3691
+ // this credential would be a second payment.
3693
3692
  { extra: { expected_micro: fundedMicro } }
3694
3693
  );
3695
3694
  }
@@ -4195,16 +4194,16 @@ var PAYMENT_ERROR_COPY = {
4195
4194
  // Only the Cashu accumulator emits this code, and it evaluates each call's
4196
4195
  // own proofs — nothing sums a partial with a later one, on any path. So the
4197
4196
  // 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
4197
+ // mid-job it sits on a credit the per-site hint names
4198
+ // Explicit upfront short deposits use `insufficient_credit`
4199
+ // instead. Telling a caller to "send the difference" produced a
4201
4200
  // second short pay either way.
4202
4201
  payment_insufficient: {
4203
4202
  display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
4204
4203
  hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
4205
4204
  retryable: true
4206
4205
  },
4207
- // internal-review: the rail and the ledger leg commit together, but the job row is
4206
+ // the rail and the ledger leg commit together, but the job row is
4208
4207
  // written by a *second* transaction (`verifyAndCredit`). When that one can't
4209
4208
  // be made to land, the money is real and the caller's — it just isn't on the
4210
4209
  // job. Say so rather than answering a bare 500: the per-site body names the
@@ -4251,7 +4250,7 @@ var PAYMENT_ERROR_COPY = {
4251
4250
  hint: "Use a different prepaid balance or pay per call. To reclaim this balance after running jobs finish, inspect it with 'dvm wallet tempo-exit <dvm>'.",
4252
4251
  retryable: false
4253
4252
  },
4254
- // internal-review: a refund on this credit's payment channel reached the chain and
4253
+ // a refund on this credit's payment channel reached the chain and
4255
4254
  // never finished booking, so the escrow behind the balance may already have
4256
4255
  // been paid back. Spending it would buy work the service can never collect
4257
4256
  // for. The copy must not promise the money is gone *or* that it is safe to
@@ -4283,7 +4282,7 @@ var PAYMENT_ERROR_COPY = {
4283
4282
  hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
4284
4283
  retryable: true
4285
4284
  },
4286
- // internal-review: a per-rail floor, not the menu-wide minimum — today only the
4285
+ // a per-rail floor, not the menu-wide minimum — today only the
4287
4286
  // lightning rail carries one (the receive channel's routing floor). Refused
4288
4287
  // before any invoice is minted, so nothing is ever paid against this code;
4289
4288
  // `retryable: false` because re-sending the identical request can never
@@ -4313,7 +4312,7 @@ var PAYMENT_ERROR_COPY = {
4313
4312
  hint: "The balance is zero or fully held by in-flight jobs. Wait for running jobs to finish, then request the drain again with a fresh drain_id.",
4314
4313
  retryable: true
4315
4314
  },
4316
- // internal-review. Distinct from `nothing_to_drain` because the balance is real and
4315
+ // Distinct from `nothing_to_drain` because the balance is real and
4317
4316
  // waiting changes nothing: a reusable Bitcoin balance is a deposit the
4318
4317
  // service owes back in the asset it received, and what is left here converts
4319
4318
  // to less than one satoshi at the rate its own funding landed at. There is
@@ -4345,7 +4344,7 @@ var PAYMENT_ERROR_COPY = {
4345
4344
  hint: "Run 'dvm wallet tempo-exit <dvm>' to inspect the channel, request its on-chain exit if needed, and withdraw after the 15-minute grace period.",
4346
4345
  retryable: false
4347
4346
  },
4348
- // internal-review. The voucher verified and the ledger leg rolled back with the
4347
+ // The voucher verified and the ledger leg rolled back with the
4349
4348
  // failed submission, so nothing was charged and nothing about the payment is
4350
4349
  // wrong — which is exactly what the `payment_invalid` this replaced got
4351
4350
  // backwards, telling an agent to discard a credential that is still good. The
@@ -4422,7 +4421,7 @@ var PAYMENT_ERROR_COPY = {
4422
4421
  hint: "The token's proofs are pending at the mint; retry in a few seconds.",
4423
4422
  retryable: true
4424
4423
  },
4425
- // internal-review. Not failures — the two `lightning_*` states below are the normal
4424
+ // Not failures — the two `lightning_*` states below are the normal
4426
4425
  // shape of an interactive funding leg, which is why both are retryable and
4427
4426
  // neither `display` implies anything went wrong.
4428
4427
  lightning_settlement_pending: {
@@ -4442,7 +4441,7 @@ var PAYMENT_ERROR_COPY = {
4442
4441
  },
4443
4442
  replay_rejected: {
4444
4443
  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
4444
+ // this hint reaches a caller whose money already landed — the
4446
4445
  // accumulator holds these proofs under this request id. It used to read
4447
4446
  // "use a fresh token and a new request id for the next payment", which an
4448
4447
  // agent could follow straight into paying twice for the same charge.
@@ -4586,7 +4585,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
4586
4585
  snapshot
4587
4586
  });
4588
4587
  }
4589
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
4588
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
4590
4589
  const receipt = await verifyX402Payment(
4591
4590
  body.content.x402_payment,
4592
4591
  opts.x402Config,
@@ -5003,7 +5002,7 @@ async function processIncomingPayment(job, body, opts) {
5003
5002
  try {
5004
5003
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5005
5004
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5006
- const { verifyX402Payment } = await import("./x402-5H27DCBE.js");
5005
+ const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
5007
5006
  const receipt = await verifyX402Payment(
5008
5007
  body.content.x402_payment,
5009
5008
  opts.x402Config,
@@ -5307,7 +5306,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
5307
5306
  // requestId is non-null here because acceptAccumulatorPayment returns
5308
5307
  // kind:"rejected" with code "missing_request_id" when unset (handled by
5309
5308
  // the rejected branch above). Thread it as the per-call settlement
5310
- // reference for revenue_events.tx_hash (internal-review). Unique per
5309
+ // reference for revenue_events.tx_hash. Unique per
5311
5310
  // (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
5312
5311
  // it serves as a stable id even though it isn't an on-chain hash.
5313
5312
  paymentTxHash: opts.requestId,
@@ -5456,7 +5455,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
5456
5455
  amount_micro: priceFiat.amountMicro,
5457
5456
  // Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
5458
5457
  // 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
5458
+ // from an earlier one. The first is the rate source down, the
5460
5459
  // second is the rate source having quietly stopped carrying a currency
5461
5460
  // this DVM advertises — a config problem, and a different repair.
5462
5461
  ...denomination.carriedForward === true && { rate_carried_forward: true },
@@ -5687,7 +5686,7 @@ async function fundAndDraw(ctx, funding, tx) {
5687
5686
  creditId: creditId2,
5688
5687
  drawId: drawId2,
5689
5688
  addAmountMicro: fundedMicro,
5690
- // internal-review: never grow the draw past what the job cumulatively asked.
5689
+ // never grow the draw past what the job cumulatively asked.
5691
5690
  // The available-balance guard inside `growDraw` cannot do this — the
5692
5691
  // `fund` above raised the balance by exactly this amount — so a second
5693
5692
  // payment for one ask would otherwise be absorbed and settle as revenue.
@@ -5800,7 +5799,7 @@ function creditDepositPayload(dvmId, funding) {
5800
5799
  creditCurrency: funding.currency,
5801
5800
  fundedAt: funding.fundedAt,
5802
5801
  expiryMs: funding.expiryMs,
5803
- // internal-review: label a session-funded deposit as one, whether or not the chain
5802
+ // label a session-funded deposit as one, whether or not the chain
5804
5803
  // binding below could be read. `rail: "tempo"` alone does not say — a
5805
5804
  // `tempo/charge` one-shot funds a credit with no channel at all — so this is
5806
5805
  // what lets the platform tell a routine charge from a session whose binding
@@ -5808,7 +5807,7 @@ function creditDepositPayload(dvmId, funding) {
5808
5807
  ...funding.tempoSession && {
5809
5808
  tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
5810
5809
  },
5811
- // internal-review: the channel registration rides the deposit report, so it
5810
+ // the channel registration rides the deposit report, so it
5812
5811
  // commits with the voucher acceptance that created the channel's liability.
5813
5812
  ...funding.tempoChannel && funding.tempoSession && {
5814
5813
  tempoChannel: {
@@ -6075,7 +6074,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
6075
6074
  receivedProofs: [],
6076
6075
  error: {
6077
6076
  // 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
6077
+ // is upstream's and may include the relay lock holder's
6079
6078
  // backend pid and `client_addr`. The detail is logged where the throw is.
6080
6079
  body: paymentErrorBody("x402_settlement_submission_failed", {
6081
6080
  message: "x402 batch settlement submission failed",
@@ -6674,7 +6673,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6674
6673
  /** Client-generated idempotency key — a retry with the same id credits once. */
6675
6674
  fund_id: z.string().min(1),
6676
6675
  /**
6677
- * SHA-256 hex over the raw funding artifact (spec §2 condition 3).
6676
+ * SHA-256 hex over the raw funding artifact (the funding-commitment rule).
6678
6677
  * Required for every attach rail; absent on `method: "lightning"`, which
6679
6678
  * carries no caller-supplied artifact to hash — see {@link CreditRequest}.
6680
6679
  */
@@ -6698,7 +6697,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
6698
6697
  * rail this build does not carry is answered by `validateDrainTarget`
6699
6698
  * with a typed `drain_method_unsupported` naming the rails that do
6700
6699
  * 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
6700
+ * caller of a removed rail (`lightning`) and the caller of a
6702
6701
  * typo deserve the same actionable refusal.
6703
6702
  */
6704
6703
  method: z.string().min(1).optional(),
@@ -6826,12 +6825,12 @@ async function handleBalance(c, deps, args) {
6826
6825
  return c.json({
6827
6826
  op: "balance",
6828
6827
  // Expired credits are included: expiry ends spending, never ownership
6829
- // (spec §5), so stranded value must stay visible to whoever owns it.
6828
+ // (the caller-ownership contract), so stranded value must stay visible to whoever owns it.
6830
6829
  credits: all.map(
6831
6830
  (credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
6832
6831
  ),
6833
6832
  ...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
6834
- // internal-review: the funding menu the quote and 402 surfaces already advertise,
6833
+ // the funding menu the quote and 402 surfaces already advertise,
6835
6834
  // echoed here so one read answers "what do I hold, and what may I add".
6836
6835
  // Under the key `menu`, not `credit` — that one is the credit projection on
6837
6836
  // this route. Carried without the caller echo fields: `credits[]` above says
@@ -7116,7 +7115,7 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7116
7115
  expectedChannelId,
7117
7116
  // The close intent is re-asserted from inside the verifier, once the
7118
7117
  // credential is bound to this credit's own channel and before anything
7119
- // broadcasts (internal-review). Run above the call instead and a credential
7118
+ // broadcasts. Run above the call instead and a credential
7120
7119
  // signed for another channel touches the reservation on its way to a
7121
7120
  // refusal that was never going to close anything.
7122
7121
  onBound: async () => {
@@ -7422,7 +7421,7 @@ async function handleX402ChannelDrain(c, deps, args) {
7422
7421
  effectId: `${args.credit.creditId}:${args.drainId}`,
7423
7422
  // The voucher the caller presents is what actually gets revoked and paid,
7424
7423
  // so the settlement layer refuses one signed for any other channel before
7425
- // it touches anything (internal-review).
7424
+ // it touches anything.
7426
7425
  expectedChannelId: channelId,
7427
7426
  refundBoundNative,
7428
7427
  commit: async (_funding, tx) => {
@@ -7641,7 +7640,7 @@ function drainBody(record) {
7641
7640
  status: record.status,
7642
7641
  amount_micro: record.amountMicro,
7643
7642
  currency: record.currency,
7644
- // The in-kind obligation, where there is one (internal-review): a Bitcoin
7643
+ // The in-kind obligation, where there is one: a Bitcoin
7645
7644
  // reclaim returns the deposited sats pro rata, at the rate each funding
7646
7645
  // landed at, so this figure was fixed when the reclaim was requested and
7647
7646
  // does not move with the market. Published beside the fiat one rather than
@@ -7664,7 +7663,7 @@ function drainCopy(record) {
7664
7663
  // Named because it is the obligation, and because it is
7665
7664
  // fixed: a Bitcoin refund returns the share of the Bitcoin you
7666
7665
  // 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
7666
+ // price does before you collect it. The mint's own
7668
7667
  // redemption charge is named rather than glossed — it is a sat
7669
7668
  // or two at a fee-charging mint and nothing at a free one, and
7670
7669
  // a figure an agent relays to a human should not be a promise
@@ -8326,7 +8325,7 @@ var DRAIN_CONFLICT_COPY = {
8326
8325
  },
8327
8326
  // Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
8328
8327
  // 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
8328
+ // drift consolidating it was meant to prevent. Tempo answers its
8330
8329
  // own through `tempoFloorRefusal`, which reaches the same conclusion a step
8331
8330
  // earlier and adds the one rail-specific fact — its channel is already closed.
8332
8331
  refund_floor: {
@@ -8337,13 +8336,13 @@ var DRAIN_CONFLICT_COPY = {
8337
8336
  retryable: false,
8338
8337
  // True even though the ledger transaction that would have written the drain
8339
8338
  // 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
8339
+ // outside that transaction and holds this drain id as its
8341
8340
  // `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
8342
8341
  // drain under — so the caller has to keep it. Dropping it mints a fresh one
8343
8342
  // that the wedged-channel gate refuses as `settlement_pending` anyway, and
8344
8343
  // re-presenting *this* id is that gate's own documented exemption. Tempo
8345
8344
  // reaches the same `true` one artifact earlier, off a `credit_drains` row
8346
- // its close-intent reservation already committed (internal-review).
8345
+ // its close-intent reservation already committed.
8347
8346
  drainRegistered: true
8348
8347
  }
8349
8348
  };
@@ -8441,7 +8440,7 @@ function creditBody(credit, reconciliationPending = false) {
8441
8440
  remaining_micro: credit.availableMicro,
8442
8441
  expiry_ms: credit.expiryMs,
8443
8442
  expired: credit.expired,
8444
- // Reclaimable via op "drain" (internal-review) — true whenever spendable-or-
8443
+ // Reclaimable via op "drain" — true whenever spendable-or-
8445
8444
  // expired value remains. Expiry ends spending, never ownership.
8446
8445
  drainable: credit.status === "active" && credit.availableMicro > 0,
8447
8446
  ...reconciliationPending && { reconciliation_pending: true },
@@ -9315,13 +9314,13 @@ function buildContext(job, opts) {
9315
9314
  if (terminated("prompt")) {
9316
9315
  return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
9317
9316
  }
9317
+ job.status = "awaiting-input";
9318
9318
  providerMessage(job, "prompt", {
9319
9319
  id,
9320
9320
  text,
9321
9321
  ...promptOpts?.options && { options: promptOpts.options },
9322
9322
  ...promptOpts?.schema && { schema: promptOpts.schema }
9323
9323
  });
9324
- job.status = "awaiting-input";
9325
9324
  if (job.replayHighSeq !== null) {
9326
9325
  const response = job.messages.find(
9327
9326
  (m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
@@ -9435,7 +9434,7 @@ function buildContext(job, opts) {
9435
9434
  poolBasis: pool.basis,
9436
9435
  askCurrency,
9437
9436
  // `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
9437
+ // an fx outage on a bare-msats ask, or a sub-micro amount — or it
9439
9438
  // pinned in a currency this pool isn't denominated in.
9440
9439
  ...pool.basis === "draw" && { poolCurrency: pool.currency }
9441
9440
  });
@@ -9570,8 +9569,8 @@ function buildContext(job, opts) {
9570
9569
  ask_msats: amountMsats
9571
9570
  });
9572
9571
  }
9573
- providerMessage(job, "payment-request", paymentRequestContent);
9574
9572
  job.status = "awaiting-input";
9573
+ providerMessage(job, "payment-request", paymentRequestContent);
9575
9574
  const paidMsatsAtYield = job.paidMsats;
9576
9575
  const p = new Promise((resolve, reject) => {
9577
9576
  let resolved = false;
@@ -9580,12 +9579,24 @@ function buildContext(job, opts) {
9580
9579
  unsubscribeNotify?.();
9581
9580
  unsubscribeNotify = void 0;
9582
9581
  };
9583
- const doResolve = (pc) => {
9582
+ const doResolve = async (pc) => {
9584
9583
  if (resolved) return;
9585
9584
  resolved = true;
9586
9585
  cleanup();
9587
9586
  creditedToHandlerMsats += amountMsats;
9588
9587
  creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
9588
+ if (job.pendingPayment?.resolve === doResolve) {
9589
+ job.pendingPayment = null;
9590
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
9591
+ if (job.status === "awaiting-input" && opts.onYield) {
9592
+ try {
9593
+ await opts.onYield();
9594
+ } catch (err) {
9595
+ reject(err);
9596
+ return;
9597
+ }
9598
+ }
9599
+ }
9589
9600
  resolve(pc);
9590
9601
  };
9591
9602
  const doReject = (err) => {
@@ -9602,11 +9613,17 @@ function buildContext(job, opts) {
9602
9613
  if (resolved) return;
9603
9614
  try {
9604
9615
  const counters = await getCounters();
9605
- if (!counters) return;
9616
+ if (!counters || resolved) return;
9606
9617
  const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
9607
9618
  if (satisfied) {
9619
+ const resume = async () => {
9620
+ job.paidMsats = counters.paidMsats;
9621
+ job.pendingPaymentMsats = counters.pendingPaymentMsats;
9622
+ await doResolve({ amount_msats: amountMsats });
9623
+ };
9608
9624
  if (opts.claimForProcessing) {
9609
- const won = await opts.claimForProcessing();
9625
+ const won = await opts.claimForProcessing(resume);
9626
+ if (resolved) return;
9610
9627
  if (!won) {
9611
9628
  resolved = true;
9612
9629
  cleanup();
@@ -9614,10 +9631,7 @@ function buildContext(job, opts) {
9614
9631
  return;
9615
9632
  }
9616
9633
  }
9617
- job.paidMsats = counters.paidMsats;
9618
- job.pendingPaymentMsats = counters.pendingPaymentMsats;
9619
- if (job.status === "awaiting-input") job.status = "processing";
9620
- doResolve({ amount_msats: amountMsats });
9634
+ await resume();
9621
9635
  }
9622
9636
  } catch (err) {
9623
9637
  log.error("requestPayment NOTIFY check failed", { error: String(err) });
@@ -9668,12 +9682,12 @@ function buildContext(job, opts) {
9668
9682
  runTerminal();
9669
9683
  },
9670
9684
  // `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
9685
+ // No rail hands value backwards: a failed job's debit is the
9672
9686
  // 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
9687
+ // balance through the `drain` op. The Cashu branch that used to
9674
9688
  // send proofs back here was dead — `job.receivedProofs` has been empty on
9675
9689
  // every path since the accumulator became the commit boundary for inbound
9676
- // proofs (internal-review) — and internal-review, the accumulator-debit rewrite that would
9690
+ // proofs — and the accumulator-debit rewrite that would
9677
9691
  // have revived it, was cancelled as superseded by the ledger.
9678
9692
  fail: (error) => {
9679
9693
  if (terminated("fail")) return;
@@ -9743,13 +9757,17 @@ var ReceiptIssuer = class {
9743
9757
  }
9744
9758
  /**
9745
9759
  * Build and sign the receipt for a terminal job. `seq` comes from
9746
- * `JobStore.claimReceiptSeq`, `issuedAt` defaults to now (tests pin it).
9760
+ * `JobStore.claimReceiptSeq`; `issuedAt` is epoch seconds and defaults to now.
9747
9761
  *
9748
9762
  * Free jobs get receipts too, with `paid.msats: 0` — they still consume a
9749
9763
  * sequence number and still attest an outcome. Whether feedback is
9750
9764
  * paid-only is the feedback layer's call, not this one's.
9751
9765
  *
9752
- * `credit` is the resolved draw block (internal-review) — present on every job
9766
+ * Timing and attribution come from the committed terminal row, not signing
9767
+ * time or handler input. Legacy rows without those durable inputs omit the
9768
+ * optional blocks rather than inventing historical evidence.
9769
+ *
9770
+ * `credit` is the resolved draw block — present on every job
9753
9771
  * whose payment funded/drew the ledger, absent otherwise. Signature-
9754
9772
  * compatible either way: `canonicalize` omits absent keys.
9755
9773
  */
@@ -9762,6 +9780,14 @@ var ReceiptIssuer = class {
9762
9780
  capability: record.capability,
9763
9781
  outcome: outcomeFor(record.status),
9764
9782
  reason: terminalReason(record),
9783
+ ...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
9784
+ timing: {
9785
+ accepted_at_ms: record.createdAt,
9786
+ terminal_at_ms: record.terminalAtMs,
9787
+ caller_wait_ms: record.callerWaitMs ?? 0
9788
+ }
9789
+ },
9790
+ ...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
9765
9791
  seq,
9766
9792
  issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
9767
9793
  requester_pubkey: record.requesterPubkey ?? null,
@@ -9780,7 +9806,7 @@ var ReceiptIssuer = class {
9780
9806
  return signReceipt(unsigned, this.secret);
9781
9807
  }
9782
9808
  /**
9783
- * Countersign one reclaim event (internal-review). Unlike job receipts there is
9809
+ * Countersign one reclaim event. Unlike job receipts there is
9784
9810
  * no store-allocated sequence — the drain's own `ledger_seq` (taken under
9785
9811
  * the credit row lock, shared with draws) already orders it in the
9786
9812
  * per-credit evidence chain.
@@ -10097,12 +10123,16 @@ var JobManager = class {
10097
10123
  cleanupTimers = /* @__PURE__ */ new Map();
10098
10124
  /** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
10099
10125
  terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
10100
- /** Per-job NOTIFY unsubscribe handles for the durable-status watch (internal-review). */
10126
+ /** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
10101
10127
  statusWatchers = /* @__PURE__ */ new Map();
10102
- /** Job ids whose durable-status re-read is in flight — coalesces notify storms (internal-review). */
10128
+ /** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
10103
10129
  statusChecksInFlight = /* @__PURE__ */ new Set();
10104
- /** Job ids that were notified mid-re-read and must be re-checked (internal-review). */
10130
+ /** Job ids that were notified mid-re-read and must be re-checked. */
10105
10131
  statusChecksQueued = /* @__PURE__ */ new Set();
10132
+ // Every local input resume holds this queue through its durable wait update.
10133
+ // A prompt response and a payment NOTIFY must not mistake each other for a
10134
+ // different machine's successful claim.
10135
+ inputResumeTails = /* @__PURE__ */ new WeakMap();
10106
10136
  sessionId = randomBytes4(3).toString("hex");
10107
10137
  nextJobId = 1;
10108
10138
  accumulatorMonitor;
@@ -10110,7 +10140,7 @@ var JobManager = class {
10110
10140
  processingWatchdogMs;
10111
10141
  reactivationClaimGraceMs;
10112
10142
  /**
10113
- * Worker-heartbeat cadence (internal-review). Also floors the internal-review clock
10143
+ * Worker-heartbeat cadence. Also floors the clock
10114
10144
  * guard's compensation on the processing arm — see
10115
10145
  * {@link staleSweepCompensationCapMs}.
10116
10146
  */
@@ -10130,7 +10160,7 @@ var JobManager = class {
10130
10160
  jobRetentionResumeFrom;
10131
10161
  /**
10132
10162
  * A `Date.now()` reading that cannot regress within this manager's lifetime
10133
- * (internal-review). Anchored at construction, so a manager already running when
10163
+ * Anchored at construction, so a manager already running when
10134
10164
  * the clock stepped is covered. The stale-job sweep's two cutoffs, the
10135
10165
  * orphan-draw age gate and the reactivation claim grace all read it; see
10136
10166
  * {@link createMonotonicClock} for what it costs and where it is clamped.
@@ -10143,7 +10173,7 @@ var JobManager = class {
10143
10173
  jobRetentionSweepInFlight = false;
10144
10174
  heartbeatInFlight = false;
10145
10175
  /**
10146
- * The denomination of every credit this DVM opens (internal-review). Resolved once,
10176
+ * The denomination of every credit this DVM opens. Resolved once,
10147
10177
  * with the same boundary fallback `DVMServer` applies for direct JavaScript
10148
10178
  * callers that force an incomplete value past the descriptor contract.
10149
10179
  */
@@ -10158,7 +10188,7 @@ var JobManager = class {
10158
10188
  this.jobStore = new MemoryJobStore();
10159
10189
  } else {
10160
10190
  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."
10191
+ "JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL)."
10162
10192
  );
10163
10193
  }
10164
10194
  if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
@@ -10166,7 +10196,7 @@ var JobManager = class {
10166
10196
  JSON.stringify({
10167
10197
  level: "warn",
10168
10198
  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."
10199
+ message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods."
10170
10200
  })
10171
10201
  );
10172
10202
  }
@@ -10265,7 +10295,7 @@ var JobManager = class {
10265
10295
  }
10266
10296
  }
10267
10297
  }
10268
- if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
10298
+ if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
10269
10299
  this.terminalCostRecoveryTimer = setInterval(() => {
10270
10300
  void this.recoverTerminalCosts().catch((err) => {
10271
10301
  console.error(
@@ -10279,16 +10309,14 @@ var JobManager = class {
10279
10309
  this.terminalCostRecoveryTimer.unref();
10280
10310
  }
10281
10311
  }
10282
- /** Active in-memory jobs. */
10283
10312
  get activeJobs() {
10284
10313
  return this._activeJobs;
10285
10314
  }
10286
- /** The backing job store. */
10287
10315
  get store() {
10288
10316
  return this.jobStore;
10289
10317
  }
10290
10318
  /**
10291
- * True when this DVM will actually issue receipts (internal-review) — a key *and*
10319
+ * True when this DVM will actually issue receipts — a key *and*
10292
10320
  * a store that can allocate sequence numbers and persist bytes write-once.
10293
10321
  * `/v1/info#receipts` reads this rather than the key alone, so the flag can
10294
10322
  * never promise something `issueReceipt` silently declines to do.
@@ -10305,7 +10333,7 @@ var JobManager = class {
10305
10333
  return Object.keys(this.descriptor.capabilities);
10306
10334
  }
10307
10335
  /**
10308
- * Resolve a capability's static `price` to msats (internal-review).
10336
+ * Resolve a capability's static `price` to msats.
10309
10337
  *
10310
10338
  * `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
10311
10339
  * Returns `undefined` for dynamic-priced capabilities (those that declare
@@ -10318,7 +10346,7 @@ var JobManager = class {
10318
10346
  }
10319
10347
  /**
10320
10348
  * Translate a capability's static `price` into the fiat envelope used by
10321
- * the per-capability `pricing.max` advertised on `/v1/info` (internal-review).
10349
+ * the per-capability `pricing.max` advertised on `/v1/info`.
10322
10350
  * Returns `undefined` for dynamic-priced capabilities and for unknown
10323
10351
  * names (the route handler validates the name before invoking this).
10324
10352
  */
@@ -10328,24 +10356,24 @@ var JobManager = class {
10328
10356
  return resolvePricingMax(cap.price);
10329
10357
  }
10330
10358
  /**
10331
- * Parse a request body through a capability's input schema (internal-review).
10359
+ * Parse a request body through a capability's input schema.
10332
10360
  *
10333
10361
  * Resolution order when a capability declares an `input` schema:
10334
10362
  * 1. `body.data` is preferred — agents pass structured fields directly
10335
- * (CLI builds this from `--param k=v`).
10363
+ * (CLI builds this from `--param k=v`).
10336
10364
  * 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
10337
- * JSON-string contract.
10365
+ * JSON-string contract.
10338
10366
  * 3. Neither usable → throw `MissingStructuredInputError` so the route can
10339
- * return `invalid_input` with a hint pointing at `/v1/info`.
10367
+ * return `invalid_input` with a hint pointing at `/v1/info`.
10340
10368
  *
10341
10369
  * When the capability has no `input` schema, returns `body.input` raw
10342
10370
  * (primitive path).
10343
10371
  *
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
10372
+ * The credit envelope is removed before the schema runs, on both
10373
+ * branches — it rides the signed body but is protocol-level, so a
10346
10374
  * top-level `.strict()` capability schema would otherwise reject every
10347
10375
  * 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
10376
+ * The persisted `record.input` the reactivation path re-reads is the
10349
10377
  * pre-Zod wire form, so it comes through the second branch carrying them too.
10350
10378
  */
10351
10379
  parseInput(body, capability) {
@@ -10363,7 +10391,7 @@ var JobManager = class {
10363
10391
  throw new MissingStructuredInputError();
10364
10392
  }
10365
10393
  /**
10366
- * Look up the per-capability descriptor by name (internal-review). Throws when the
10394
+ * Look up the per-capability descriptor by name. Throws when the
10367
10395
  * name doesn't exist — the route layer validates body.capability first, so
10368
10396
  * reaching this with an unknown name is a programmer error.
10369
10397
  */
@@ -10389,7 +10417,7 @@ var JobManager = class {
10389
10417
  return this.descriptor.capabilities[name];
10390
10418
  }
10391
10419
  /**
10392
- * Allocate the id the next job will be created under (internal-review). The
10420
+ * Allocate the id the next job will be created under. The
10393
10421
  * submit path calls this BEFORE payment verification so the ledger draw
10394
10422
  * commits with its job linkage, then passes the id back through
10395
10423
  * `createJob`'s provenance. Ids allocated for requests whose payment is
@@ -10400,7 +10428,7 @@ var JobManager = class {
10400
10428
  return `job-${this.sessionId}-${this.nextJobId++}`;
10401
10429
  }
10402
10430
  /**
10403
- * Create a new job from a request body. `provenance` (internal-review) is what the
10431
+ * Create a new job from a request body. `provenance` is what the
10404
10432
  * idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
10405
10433
  * to re-issue, and the fingerprint + caller pubkey a retry must reproduce
10406
10434
  * to be given it.
@@ -10456,27 +10484,23 @@ var JobManager = class {
10456
10484
  this.watchDurableStatus(job);
10457
10485
  return job;
10458
10486
  }
10459
- /** Build an SDKJobContext and attach it to the job. */
10460
10487
  buildAndAttachContext(job, parsedInput) {
10461
10488
  const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
10462
10489
  const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
10463
10490
  job.sdkCtx = sdkCtx;
10464
10491
  return sdkCtx;
10465
10492
  }
10466
- /** Start the handler for a job. */
10467
10493
  runHandler(job, sdkCtx) {
10468
10494
  const cap = this.getCapability(job.capability);
10469
10495
  const onError = (err) => {
10470
10496
  if (isTerminal(job.status)) return;
10471
10497
  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
- }
10498
+ job.status = "failed";
10499
+ providerMessage(job, "cancel", {
10500
+ reason: err instanceof Error ? err.message : String(err)
10501
+ });
10502
+ job.terminalWork = this.finalizeTerminal(job);
10503
+ void job.terminalWork;
10480
10504
  };
10481
10505
  if (!cap) {
10482
10506
  if (!isTerminal(job.status)) {
@@ -10504,16 +10528,46 @@ var JobManager = class {
10504
10528
  onError(err);
10505
10529
  }
10506
10530
  }
10507
- /** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
10508
10531
  async dispatchMessage(job, type, content) {
10532
+ if (type !== "response" && type !== "payment") {
10533
+ return this.dispatchMessageNow(job, type, content);
10534
+ }
10535
+ const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10536
+ await this.queueInputResume(job, async () => {
10537
+ const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
10538
+ if (pending && current !== pending) return;
10539
+ return this.dispatchMessageNow(job, type, content);
10540
+ });
10541
+ }
10542
+ async queueInputResume(job, resume) {
10543
+ const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
10544
+ const next = previous.catch(() => void 0).then(resume);
10545
+ this.inputResumeTails.set(job, next);
10546
+ try {
10547
+ return await next;
10548
+ } finally {
10549
+ if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
10550
+ }
10551
+ }
10552
+ async claimLocalInput(job) {
10553
+ await job.messageAppenderTail;
10554
+ return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
10555
+ }
10556
+ async dispatchMessageNow(job, type, content) {
10509
10557
  const ctx = job.sdkCtx;
10510
10558
  const cap = this.getCapability(job.capability);
10511
10559
  if (type === "response") {
10512
10560
  const rc = content;
10513
10561
  const pending = job.pendingPrompts.get(rc.prompt_id);
10514
10562
  if (pending) {
10563
+ if (!await this.claimLocalInput(job)) {
10564
+ await this.adoptDurableTerminal(job);
10565
+ return;
10566
+ }
10567
+ if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
10515
10568
  job.pendingPrompts.delete(rc.prompt_id);
10516
- job.status = "processing";
10569
+ job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
10570
+ if (job.status === "awaiting-input") await this.persistJob(job);
10517
10571
  pending.resolve(rc);
10518
10572
  } else if (cap?.onResponse && ctx) {
10519
10573
  await cap.onResponse(ctx, rc);
@@ -10522,9 +10576,15 @@ var JobManager = class {
10522
10576
  const pc = content;
10523
10577
  if (job.pendingPayment) {
10524
10578
  const pp = job.pendingPayment;
10579
+ if (!await this.claimLocalInput(job)) {
10580
+ await this.adoptDurableTerminal(job);
10581
+ return;
10582
+ }
10583
+ if (job.pendingPayment !== pp || isTerminal(job.status)) return;
10525
10584
  job.pendingPayment = null;
10526
- job.status = "processing";
10527
- pp.resolve(pc);
10585
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
10586
+ if (job.status === "awaiting-input") await this.persistJob(job);
10587
+ await pp.resolve(pc);
10528
10588
  } else if (cap?.onPayment && ctx) {
10529
10589
  await cap.onPayment(ctx, pc);
10530
10590
  }
@@ -10533,6 +10593,7 @@ var JobManager = class {
10533
10593
  } else if (type === "cancel") {
10534
10594
  const reason = content?.reason ?? "Job cancelled";
10535
10595
  abortJob(job, reason);
10596
+ job.endedBy = "caller";
10536
10597
  job.status = "cancelled";
10537
10598
  if (cap?.onCancel && ctx) {
10538
10599
  await cap.onCancel(ctx, content);
@@ -10551,17 +10612,16 @@ var JobManager = class {
10551
10612
  });
10552
10613
  }
10553
10614
  }
10554
- // ── Persistence ──────────────────────────────────────────────────────
10555
10615
  /**
10556
10616
  * Persist a job to the backing store.
10557
10617
  *
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).
10618
+ * Awaits the per-job appender tail so that:
10619
+ * 1. The snapshot's `messages` is consistent with the `job_messages` table
10620
+ * no row is still in flight at snapshot time.
10621
+ * 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
10622
+ * can't race a still-pending `appendOutgoing` for the final yield
10623
+ * message (which would otherwise wipe the row before an in-flight SSE
10624
+ * subscriber's NOTIFY-driven fetch can see it).
10565
10625
  */
10566
10626
  async persistJob(job) {
10567
10627
  if (job.messageAppenderTail) {
@@ -10582,9 +10642,17 @@ var JobManager = class {
10582
10642
  } else {
10583
10643
  await this.jobStore.save(record);
10584
10644
  }
10645
+ const saved = await this.jobStore.get(job.id);
10646
+ if (saved) {
10647
+ job.timingKnown = saved.timingKnown;
10648
+ job.endedBy = saved.endedBy;
10649
+ job.terminalAtMs = saved.terminalAtMs;
10650
+ job.callerWaitMs = saved.callerWaitMs;
10651
+ job.awaitingInputAtMs = saved.awaitingInputAtMs;
10652
+ }
10585
10653
  }
10586
10654
  /**
10587
- * Cross-machine terminal guard (internal-review). `buildContext`'s guard reads the
10655
+ * Cross-machine terminal guard. `buildContext`'s guard reads the
10588
10656
  * in-memory `job.status`, so on its own it only protects the machine running
10589
10657
  * the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
10590
10658
  * on a machine that isn't running the job — `app.ts` cancels it in the store
@@ -10596,7 +10664,7 @@ var JobManager = class {
10596
10664
  * fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
10597
10665
  * the durable state won and the caller must skip its save.
10598
10666
  *
10599
- * Two triggers: `watchDurableStatus`'s NOTIFY subscription (internal-review) fires
10667
+ * Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
10600
10668
  * this within a round-trip of the remote cancel committing, which is what
10601
10669
  * actually stops the provider spend; `persistJob` calls it again on every save
10602
10670
  * as the backstop for the window where a cancel commits between a handler's
@@ -10604,8 +10672,10 @@ var JobManager = class {
10604
10672
  * stores' terminal-sticky `save`, which keeps the row itself correct).
10605
10673
  */
10606
10674
  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;
10675
+ const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
10676
+ if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
10677
+ const durable = await this.jobStore.get(job.id);
10678
+ if (!durable) return false;
10609
10679
  console.warn(
10610
10680
  JSON.stringify({
10611
10681
  level: "job_terminal_elsewhere",
@@ -10616,6 +10686,11 @@ var JobManager = class {
10616
10686
  );
10617
10687
  job.status = durable.status;
10618
10688
  job.summary = durable.summary;
10689
+ job.timingKnown = durable.timingKnown;
10690
+ job.endedBy = durable.endedBy;
10691
+ job.terminalAtMs = durable.terminalAtMs;
10692
+ job.callerWaitMs = durable.callerWaitMs;
10693
+ job.awaitingInputAtMs = durable.awaitingInputAtMs;
10619
10694
  const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
10620
10695
  for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
10621
10696
  job.pendingPayment?.reject(new JobCancelledError(reason));
@@ -10635,7 +10710,7 @@ var JobManager = class {
10635
10710
  });
10636
10711
  }
10637
10712
  this.clearIdleTimer(job.id);
10638
- void this.issueReceiptForStoredJob(job.id);
10713
+ void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
10639
10714
  this.scheduleCleanup(job);
10640
10715
  if (durable.status !== "completed" && terminalCostPersisted) {
10641
10716
  await this.reportJobCost(job);
@@ -10681,6 +10756,7 @@ var JobManager = class {
10681
10756
  async retryTerminalCostHandoff(job, attempt) {
10682
10757
  if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
10683
10758
  await this.reportJobCost(job);
10759
+ await this.reportTerminalJob(job.id);
10684
10760
  }
10685
10761
  /** Durably merge a terminal handler owner's latest cost before reporting it. */
10686
10762
  async persistAdoptedTerminalCost(job) {
@@ -10710,7 +10786,7 @@ var JobManager = class {
10710
10786
  );
10711
10787
  }
10712
10788
  /**
10713
- * Watch the durable status of a locally-active job (internal-review).
10789
+ * Watch the durable status of a locally-active job.
10714
10790
  *
10715
10791
  * `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
10716
10792
  * holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
@@ -10751,7 +10827,7 @@ var JobManager = class {
10751
10827
  });
10752
10828
  }
10753
10829
  /**
10754
- * Tear down a job's durable-status subscription (internal-review). Must be called
10830
+ * Tear down a job's durable-status subscription. Must be called
10755
10831
  * wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
10756
10832
  * the store's shared LISTEN connection. Idempotent.
10757
10833
  */
@@ -10763,12 +10839,12 @@ var JobManager = class {
10763
10839
  unsubscribe();
10764
10840
  }
10765
10841
  /**
10766
- * NOTIFY-driven durable-status re-read, coalesced per job (internal-review).
10842
+ * NOTIFY-driven durable-status re-read, coalesced per job.
10767
10843
  *
10768
10844
  * A notify that arrives while a re-read is in flight is queued rather than
10769
10845
  * 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.
10846
+ * cancel committed, and dropping its notify would leave the local handler
10847
+ * running until its next store write instead of aborting promptly.
10772
10848
  */
10773
10849
  async checkDurableTerminal(job) {
10774
10850
  if (this.statusChecksInFlight.has(job.id)) {
@@ -10798,13 +10874,13 @@ var JobManager = class {
10798
10874
  /**
10799
10875
  * Wire the Tx A appender when the store supports streaming.
10800
10876
  *
10801
- * Tx A (internal-review): outgoing message + `pending_payment_msats` bump land in
10877
+ * Tx A: outgoing message + `pending_payment_msats` bump land in
10802
10878
  * the same transaction. The DB allocates the row's seq via
10803
10879
  * `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
10804
10880
  * traffic on different machines can never collide on the `(job_id, seq)` PK.
10805
10881
  * In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
10806
10882
  *
10807
- * Per-job serialisation (internal-review): chained through `job.messageAppenderTail`
10883
+ * Per-job serialisation: chained through `job.messageAppenderTail`
10808
10884
  * so two synchronous `providerMessage` calls (e.g. `artifact` followed by
10809
10885
  * `complete`) commit their `appendOutgoing` transactions in call order.
10810
10886
  * Without this, the two transactions race for the `jobs` row lock and the
@@ -10838,7 +10914,7 @@ var JobManager = class {
10838
10914
  // In-memory counter, NOT the authoritative DB-allocated seq:
10839
10915
  // `appendOutgoing` allocates the real seq via `UPDATE ...
10840
10916
  // RETURNING` inside the transaction that just failed, so no DB
10841
- // seq was assigned for this message (internal-review).
10917
+ // seq was assigned for this message.
10842
10918
  localSeq: msg.seq,
10843
10919
  note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
10844
10920
  error: err instanceof Error ? err.message : String(err)
@@ -10849,7 +10925,6 @@ var JobManager = class {
10849
10925
  job.messageAppenderTail = next;
10850
10926
  };
10851
10927
  }
10852
- // ── Idle timeout ─────────────────────────────────────────────────────
10853
10928
  /** Start (or restart) the idle timer for a job. */
10854
10929
  startIdleTimer(job) {
10855
10930
  this.clearIdleTimer(job.id);
@@ -10867,7 +10942,7 @@ var JobManager = class {
10867
10942
  this.idleTimers.delete(id);
10868
10943
  }
10869
10944
  }
10870
- /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard (internal-review). */
10945
+ /** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
10871
10946
  async cancelJobIdle(job) {
10872
10947
  if (isTerminal(job.status)) return;
10873
10948
  const current = await this.jobStore.get(job.id).catch(() => void 0);
@@ -10886,6 +10961,7 @@ var JobManager = class {
10886
10961
  if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
10887
10962
  job.pendingPrompts.clear();
10888
10963
  job.pendingPayment = null;
10964
+ job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
10889
10965
  job.status = "cancelled";
10890
10966
  abortJob(job, "Job timed out (idle)");
10891
10967
  providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
@@ -10893,14 +10969,13 @@ var JobManager = class {
10893
10969
  void job.terminalWork;
10894
10970
  this.scheduleCleanup(job);
10895
10971
  }
10896
- // ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
10897
10972
  /**
10898
10973
  * 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.
10974
+ * • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
10975
+ * (`stale_no_terminal_status`) — the caller never paid / responded.
10976
+ * • `processing`/`working` past `processingWatchdogMs` → `failed`
10977
+ * (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
10978
+ * live workers fresh, so a stale row here means a dead process.
10904
10979
  *
10905
10980
  * Fires on a timer (wired in the constructor) and also callable on demand
10906
10981
  * (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
@@ -10909,17 +10984,17 @@ var JobManager = class {
10909
10984
  * descriptors — the local activeJobs teardown only makes sense on the
10910
10985
  * JobManager that actually hosted the zombie handler.
10911
10986
  *
10912
- * **Both cutoffs float off the wall clock (internal-review).** Each is one
10987
+ * **Both cutoffs float off the wall clock.** Each is one
10913
10988
  * `Date.now()` sample compared against `last_activity_at`, stamped from a
10914
10989
  * different sample at a different moment, so a backwards step between the
10915
10990
  * two makes every row look more recently active than it is: the query
10916
10991
  * matches nothing and both arms go silent for the length of the skew. The
10917
10992
  * 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.
10993
+ * `pending`, and the paid-job-death alert — wired to this reaper —
10994
+ * never fires. Same defect, same host class, as orphan sweep.
10920
10995
  *
10921
10996
  * **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
10997
+ * for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
10923
10998
  * of an unclaimed hold, refused outright for any live job row; ours
10924
10999
  * force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
10925
11000
  * `expectedActivityBefore` is the same compensated threshold the query used,
@@ -11055,7 +11130,7 @@ var JobManager = class {
11055
11130
  }
11056
11131
  /**
11057
11132
  * 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,
11133
+ * clock to cover a backwards step. The compensation is capped,
11059
11134
  * not free: a claimed row must still have been silent for
11060
11135
  * `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
11061
11136
  * force-fails a job that may well be alive.
@@ -11075,7 +11150,7 @@ var JobManager = class {
11075
11150
  * clock stepped. That pair is marginal already; the guard must not make it
11076
11151
  * deterministic. The cap collapses to zero there, which is today's
11077
11152
  * 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
11153
+ * nothing heartbeats it by design, and its refresh is an inbound caller
11079
11154
  * message on no cadence at all.
11080
11155
  *
11081
11156
  * **Why a magnitude and not a predicate.** The tempting sharper rule is to
@@ -11094,7 +11169,6 @@ var JobManager = class {
11094
11169
  if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
11095
11170
  return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
11096
11171
  }
11097
- // ── Terminal job content retention ───────────────────────────────────
11098
11172
  /**
11099
11173
  * Redact terminal job content beyond this DVM's configured window.
11100
11174
  *
@@ -11259,12 +11333,11 @@ var JobManager = class {
11259
11333
  );
11260
11334
  }
11261
11335
  }
11262
- // ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
11263
11336
  /**
11264
11337
  * Resolve `pending` credit draws the terminal funnel can no longer reach.
11265
11338
  * Two arms over one scan, distinguished by whether the draw's job row exists.
11266
11339
  *
11267
- * **No job row (internal-review) — release.** internal-review allocates the job id and
11340
+ * **No job row — release.** allocates the job id and
11268
11341
  * commits the ledger draw, with its `job_id` linkage, before `recordReplay`
11269
11342
  * and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
11270
11343
  * fail-closed `replay_detected` 401 in that window leaves a committed hold
@@ -11274,12 +11347,12 @@ var JobManager = class {
11274
11347
  * available balance stays reduced forever. Cosmetic for an implicit N=1
11275
11348
  * credit, a silent balance shrink for an explicit N>1 one.
11276
11349
  *
11277
- * **Terminal job row (internal-review) — reconcile to its outcome.** The funnel
11350
+ * **Terminal job row — reconcile to its outcome.** The funnel
11278
11351
  * demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
11279
11352
  * `issueReceipt` catches it, logs `credit_resolve_failed` and returns
11280
11353
  * nothing, and until now nothing retried. Same stranded hold, reached through
11281
11354
  * 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
11355
+ * books too, since draws *are* the revenue events: the caller is
11283
11356
  * charged nothing, the balance never moves, and outstanding liability
11284
11357
  * (`deposits − draw revenue`) overstates forever. So a late settle books, off
11285
11358
  * the committed draw row, through the ordinary reporter path.
@@ -11407,8 +11480,8 @@ var JobManager = class {
11407
11480
  }
11408
11481
  }
11409
11482
  /**
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
11483
+ * The end-of-scan line for a tick that resolved nothing. Two shapes are
11484
+ * worth a warning, and neither is the ordinary
11412
11485
  * one — the timer runs every 5 minutes per ledger in production.
11413
11486
  *
11414
11487
  * `examined > 0`: aged candidates were looked at and none moved. Usually
@@ -11417,7 +11490,7 @@ var JobManager = class {
11417
11490
  *
11418
11491
  * `examined === 0`: the query returned nothing. Ordinarily that means there
11419
11492
  * 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
11493
+ * is the case the gate could not reach. Probe for the oldest
11421
11494
  * `pending` hold at any age and speak up when one exists the scan should
11422
11495
  * have seen and didn't: already past the age gate outright, stamped ahead of
11423
11496
  * our clock (some other process's clock is fast), or hidden while our own
@@ -11469,7 +11542,7 @@ var JobManager = class {
11469
11542
  );
11470
11543
  }
11471
11544
  /**
11472
- * Resolve one aged `pending` draw whose job row reached terminal (internal-review),
11545
+ * Resolve one aged `pending` draw whose job row reached terminal,
11473
11546
  * mirroring what `resolveCreditDraw` would have done in line. Returns true
11474
11547
  * when this call is the one that moved the draw — the caller counts it, and
11475
11548
  * only it books.
@@ -11542,7 +11615,7 @@ var JobManager = class {
11542
11615
  /**
11543
11616
  * Refresh `lastActivityAt` for jobs this process is actively running
11544
11617
  * (`processing`/`working`) so the processing watchdog only fires once the
11545
- * worker is genuinely dead (internal-review). `awaiting-input` jobs are
11618
+ * worker is genuinely dead. `awaiting-input` jobs are
11546
11619
  * deliberately excluded — their idle timeout must still elapse. Fires on the
11547
11620
  * heartbeat timer; also callable on demand for tests.
11548
11621
  */
@@ -11562,17 +11635,16 @@ var JobManager = class {
11562
11635
  this.heartbeatInFlight = false;
11563
11636
  }
11564
11637
  }
11565
- // ── Reactivation (replay) ────────────────────────────────────────────
11566
11638
  /**
11567
- * Single-execution claim for a reactivating machine (internal-review). Before
11639
+ * Single-execution claim for a reactivating machine. Before
11568
11640
  * replaying an `awaiting-input` job, wait a bounded grace window for a live
11569
11641
  * original handler to win the `awaiting-input → processing` CAS via its
11570
11642
  * 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.
11643
+ * • `false` — the row left `awaiting-input` during the window (the original
11644
+ * handler claimed it / drove it terminal). Stand down; do not replay.
11645
+ * • `true` — the window elapsed still `awaiting-input` (the original worker
11646
+ * is gone or has no live resolver) AND this machine won the claim CAS.
11647
+ * Replay for dead-worker recovery.
11576
11648
  *
11577
11649
  * Biasing the original handler to win kills the double execution (double
11578
11650
  * substrate spend, clobbered artifact) without heartbeating `awaiting-input`
@@ -11595,7 +11667,7 @@ var JobManager = class {
11595
11667
  }
11596
11668
  }
11597
11669
  /**
11598
- * Record an inbound client message durably via Tx B (internal-review) and append
11670
+ * Record an inbound client message durably via Tx B and append
11599
11671
  * it to the in-memory `job.messages` array. Returns the DB-allocated seq
11600
11672
  * (`null` when the store isn't streamable — non-Postgres test fallback).
11601
11673
  *
@@ -11639,13 +11711,13 @@ var JobManager = class {
11639
11711
  * in-memory `ServerJob`, wires the appender, builds the context, starts
11640
11712
  * the idle timer. The caller has already recorded the inbound via Tx B
11641
11713
  * and run Tx C (verify + credit); reactivation is now decoupled from
11642
- * credit (internal-review, internal-review). The caller decides whether to run the
11714
+ * credit. The caller decides whether to run the
11643
11715
  * handler (Pattern A: status was `awaiting-input`) or to dispatch the
11644
11716
  * message to a custom handler (Pattern B).
11645
11717
  *
11646
11718
  * For DVMs that declare descriptor-level auth, the persisted
11647
11719
  * `record.input` carries the original signed envelope. We re-verify the
11648
- * signature here (internal-review) so a DB-layer tamper — compromised admin, SQL
11720
+ * signature here so a DB-layer tamper — compromised admin, SQL
11649
11721
  * injection, malicious operator with DB access — can't silently feed an
11650
11722
  * attacker-supplied pubkey/envelope into the handler. The drift window
11651
11723
  * and replay store are deliberately skipped: the persisted timestamp is
@@ -11659,7 +11731,7 @@ var JobManager = class {
11659
11731
  * What gets re-verified is the persisted row read back through
11660
11732
  * {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
11661
11733
  * 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
11734
+ * submit checked. That makes the tamper check strictly stronger
11663
11735
  * than the parsed form it replaced: an injected key the capability schema
11664
11736
  * doesn't declare used to be stripped before the signature was checked, so
11665
11737
  * it re-verified clean.
@@ -11704,7 +11776,7 @@ var JobManager = class {
11704
11776
  return { job, sdkCtx };
11705
11777
  }
11706
11778
  /**
11707
- * Verify an inbound payment and apply the credit via Tx C (internal-review).
11779
+ * Verify an inbound payment and apply the credit via Tx C.
11708
11780
  * The inbound message must have been recorded via Tx B already
11709
11781
  * (`recordInboundPending` returned `inboundSeq`). For non-streamable
11710
11782
  * stores (`inboundSeq === null`), falls back to in-memory mutation via
@@ -11714,12 +11786,12 @@ var JobManager = class {
11714
11786
  * `pendingPaymentMsats` without external verification. Gated on
11715
11787
  * `devModeSkipsPaymentVerification` — the same predicate the upfront path
11716
11788
  * 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,
11789
+ * plus the explicit `dev_auto` opt-out the dev console uses,
11718
11790
  * which is the one caller that has no wallet to pay from. `dev_auto` loses to
11719
11791
  * any proof riding the same message: money on the wire always takes the rail,
11720
11792
  * so the flag can never leave a real token unspent against a credited job.
11721
11793
  *
11722
- * Tx C is not atomic with the rail commit (internal-review). `verifyIncomingPayment`
11794
+ * Tx C is not atomic with the rail commit. `verifyIncomingPayment`
11723
11795
  * commits the proofs *and* the ledger leg in one transaction and returns;
11724
11796
  * only then does `verifyAndCredit` write the job row, on its own connection.
11725
11797
  * Everything after that call therefore runs with the caller's money already
@@ -11769,7 +11841,7 @@ var JobManager = class {
11769
11841
  lockPubkeys: extra?.lockPubkeys,
11770
11842
  mintHealthTracker: extra?.mintHealthTracker,
11771
11843
  fxFetcher: this.opts.fxFetcher,
11772
- // internal-review: the ledger leg — a mid-job payment is a top-up that funds
11844
+ // the ledger leg — a mid-job payment is a top-up that funds
11773
11845
  // the job's credit and grows its draw, atomically with the rail commit.
11774
11846
  creditLedger: this.opts.creditLedger,
11775
11847
  processedPayments: this.opts.processedPayments,
@@ -11904,7 +11976,7 @@ var JobManager = class {
11904
11976
  return { ...unappliedCredit && { unappliedCredit } };
11905
11977
  }
11906
11978
  /**
11907
- * Tx C with a bounded retry (internal-review).
11979
+ * Tx C with a bounded retry.
11908
11980
  *
11909
11981
  * By the time this runs on the verified path, the rail commit and the ledger
11910
11982
  * leg have already committed in a transaction this call is not part of — so a
@@ -11991,7 +12063,7 @@ var JobManager = class {
11991
12063
  }
11992
12064
  /**
11993
12065
  * 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.
12066
+ * could not be written — every retry spent, the money real.
11995
12067
  *
11996
12068
  * The refusal names the credit the money landed on so the caller can act on
11997
12069
  * it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
@@ -12000,7 +12072,7 @@ var JobManager = class {
12000
12072
  * fault an operator should see in their 5xx rate. `retryable` is true because
12001
12073
  * the ask is genuinely still outstanding.
12002
12074
  *
12003
- * Nothing is released here. The hold is left to the internal-review reconciler,
12075
+ * Nothing is released here. The hold is left to the reconciler,
12004
12076
  * which makes the same decision — unbound draw ⇒ release, never settle — but
12005
12077
  * under the credit row's `FOR UPDATE`, after the job is terminal, where it
12006
12078
  * cannot race an in-flight COMMIT. Releasing from here would also be
@@ -12067,7 +12139,7 @@ var JobManager = class {
12067
12139
  }
12068
12140
  /**
12069
12141
  * 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
12142
+ * the `payment_unapplied` 500 carry. `undefined` when the payment
12071
12143
  * ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
12072
12144
  * there is no credit to name, so the 201 carries no `credit` block and the
12073
12145
  * 500 takes its words from `unappliedCopy` directly.
@@ -12081,7 +12153,7 @@ var JobManager = class {
12081
12153
  ...unappliedCopy(reason, creditId)
12082
12154
  };
12083
12155
  }
12084
- /** Book a mid-job top-up's funding as a deposit (internal-review, spec §10). */
12156
+ /** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
12085
12157
  reportCreditDeposit(funding) {
12086
12158
  const onCreditFunded = this.opts.onCreditFunded;
12087
12159
  if (!funding || !onCreditFunded || !this.opts.dvmId) return;
@@ -12094,7 +12166,7 @@ var JobManager = class {
12094
12166
  /**
12095
12167
  * Internal: build the BuildContextOpts shared between `createJob` and
12096
12168
  * `reactivateJob`. Centralised so persistence/terminal callbacks and the
12097
- * internal-review NOTIFY-driven cross-machine wake stay in one place.
12169
+ * NOTIFY-driven cross-machine wake stay in one place.
12098
12170
  */
12099
12171
  buildContextOpts(job) {
12100
12172
  const jobStore = this.jobStore;
@@ -12113,7 +12185,16 @@ var JobManager = class {
12113
12185
  getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
12114
12186
  drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
12115
12187
  pricingCurrency: this.pricingCurrency,
12116
- claimForProcessing: isStreamableJobStore(jobStore) ? () => jobStore.claimForProcessing(job.id) : void 0,
12188
+ claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
12189
+ const pending = job.pendingPayment;
12190
+ return this.queueInputResume(job, async () => {
12191
+ if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
12192
+ if (!await this.claimLocalInput(job)) return false;
12193
+ if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
12194
+ await resume?.();
12195
+ return true;
12196
+ });
12197
+ } : void 0,
12117
12198
  onSuperseded: () => {
12118
12199
  this.clearIdleTimer(job.id);
12119
12200
  this.unwatchDurableStatus(job.id);
@@ -12124,7 +12205,7 @@ var JobManager = class {
12124
12205
  }
12125
12206
  /**
12126
12207
  * Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
12127
- * job's remaining pool in (internal-review).
12208
+ * job's remaining pool in.
12128
12209
  *
12129
12210
  * The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
12130
12211
  * `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
@@ -12142,8 +12223,7 @@ var JobManager = class {
12142
12223
  * drew and whose pool this can't read or denominate answers `unpriced`, which
12143
12224
  * the gate declines to auto-credit from: the msat figure it would otherwise
12144
12225
  * 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.
12226
+ * whenever that credit was funded, so it cannot safely fund a new ask.
12147
12227
  */
12148
12228
  async resolveDrawPool(job) {
12149
12229
  const ledger = this.opts.creditLedger;
@@ -12192,7 +12272,7 @@ var JobManager = class {
12192
12272
  }
12193
12273
  /**
12194
12274
  * 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
12275
+ * Idempotent and safe to call from every terminal path — the
12196
12276
  * store owns both the sequence allocation and the write-once persist, so
12197
12277
  * two machines racing on the same job converge on identical bytes and burn
12198
12278
  * exactly one sequence number.
@@ -12277,10 +12357,10 @@ var JobManager = class {
12277
12357
  };
12278
12358
  }
12279
12359
  /**
12280
- * Settle or release a terminal job's credit draw (internal-review): `completed`
12360
+ * Settle or release a terminal job's credit draw: `completed`
12281
12361
  * settles (the hold becomes a real debit); `failed`/`cancelled` releases
12282
12362
  * (the hold evaporates — "no debit on job failure", mechanically
12283
- * superseding the internal-review/974 no-op refund for credit-paid jobs, including
12363
+ * superseding the 974 no-op refund for credit-paid jobs, including
12284
12364
  * the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
12285
12365
  * resolution returns the recorded state. Returns the `ReceiptCredit` block
12286
12366
  * for the receipt: `balance_after` is the recorded draw trajectory for a
@@ -12326,7 +12406,7 @@ var JobManager = class {
12326
12406
  }
12327
12407
  }
12328
12408
  /**
12329
- * The one place a terminal job's status becomes a ledger verb (internal-review):
12409
+ * The one place a terminal job's status becomes a ledger verb:
12330
12410
  * `completed` settles the hold into a real debit, `failed`/`cancelled`
12331
12411
  * release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
12332
12412
  * late reconciler (`reconcileTerminalDraw`) — two paths that must never
@@ -12341,45 +12421,47 @@ var JobManager = class {
12341
12421
  * cleanup window hand back the same bytes as a cross-machine re-read.
12342
12422
  */
12343
12423
  async attachReceipt(job) {
12344
- const receipt = await this.issueReceipt(toJobRecord(job));
12424
+ const record = await this.jobStore.get(job.id);
12425
+ if (!record || !isTerminal(record.status)) return;
12426
+ const receipt = await this.issueReceipt(record);
12345
12427
  if (receipt) job.receipt = receipt;
12346
12428
  }
12347
12429
  /**
12348
- * Read-path repair for a terminal job carrying no receipt (internal-review), for
12430
+ * Read-path repair for a terminal job carrying no receipt, for
12349
12431
  * either an in-memory job or a store record. Free in the steady state — it
12350
12432
  * returns on the `receipt` check without touching the store — so it costs
12351
12433
  * only on the cases it exists for:
12352
12434
  *
12353
12435
  * - **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.
12436
+ * sequence number to the job row before `saveReceipt` writes the bytes; a
12437
+ * process death in that window would otherwise strand that number
12438
+ * forever, and a permanent gap is indistinguishable from the deliberate
12439
+ * suppression `seq` exists to expose. Re-issuing here reuses the already
12440
+ * committed number (the claim is idempotent per job) rather than
12441
+ * allocating a second one.
12360
12442
  * - **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.
12443
+ * `receipt_issue_failed` and returns nothing rather than failing the job,
12444
+ * so the next read retries.
12363
12445
  * - **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.
12446
+ * up on first read, with `issued_at` reflecting when it was signed.
12365
12447
  */
12366
12448
  async ensureReceipt(target) {
12367
- if (target.receipt) return;
12368
- const record = "listeners" in target ? toJobRecord(target) : target;
12449
+ if (target.receipt) {
12450
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12451
+ return;
12452
+ }
12453
+ const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
12454
+ if (!record || !isTerminal(record.status)) return;
12369
12455
  const receipt = await this.issueReceipt(record);
12370
12456
  if (receipt) target.receipt = receipt;
12457
+ if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
12371
12458
  }
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
- */
12459
+ /** Commit the terminal snapshot before resolving money and signing its durable evidence. */
12379
12460
  async finalizeTerminal(job) {
12461
+ await this.persistJob(job);
12380
12462
  await this.attachReceipt(job);
12381
- await this.persistJob(job).catch(() => void 0);
12382
12463
  await this.reportJobCost(job);
12464
+ await this.reportTerminalJob(job.id);
12383
12465
  }
12384
12466
  /**
12385
12467
  * Issue a receipt for a job this process doesn't hold in `activeJobs` — a
@@ -12398,20 +12480,69 @@ var JobManager = class {
12398
12480
  }
12399
12481
  /**
12400
12482
  * Finish accounting for a terminal written directly to the store — the
12401
- * stale reaper and a cancel handled on a different machine (internal-review).
12483
+ * stale reaper and a cancel handled on a different machine.
12402
12484
  */
12403
12485
  async finalizeStoredTerminal(jobId) {
12404
12486
  const receipt = await this.issueReceiptForStoredJob(jobId);
12405
12487
  const record = await this.jobStore.get(jobId).catch(() => void 0);
12406
- if (record && isTerminal(record.status)) await this.reportJobCost(record);
12488
+ if (record && isTerminal(record.status)) {
12489
+ await this.reportJobCost(record);
12490
+ await this.reportTerminalJob(jobId);
12491
+ }
12407
12492
  return receipt;
12408
12493
  }
12494
+ async reportTerminalJob(jobId) {
12495
+ try {
12496
+ const callback = this.opts.onJobTerminal;
12497
+ const dvmId = this.opts.dvmId;
12498
+ if (!callback || !dvmId) return;
12499
+ const record = await this.jobStore.get(jobId);
12500
+ if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
12501
+ const receipt = await this.issueReceipt(record);
12502
+ if (this.opts.receiptIssuer && !receipt) return;
12503
+ if (record.creditId && record.drawId && this.opts.creditLedger) {
12504
+ const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
12505
+ if (!draw || draw.status === "pending") return;
12506
+ }
12507
+ await callback({
12508
+ dvmId,
12509
+ jobId: record.id,
12510
+ capability: record.capability,
12511
+ outcome: record.status,
12512
+ endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
12513
+ paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
12514
+ acceptedAtMs: record.createdAt,
12515
+ terminalAtMs: record.terminalAtMs,
12516
+ callerWaitMs: record.callerWaitMs ?? 0,
12517
+ receiptSeq: receipt?.seq ?? null
12518
+ });
12519
+ if ("markTerminalReported" in this.jobStore) {
12520
+ await this.jobStore.markTerminalReported(jobId);
12521
+ }
12522
+ } catch (error) {
12523
+ console.error(`[${jobId}] Terminal reporting failed:`, error);
12524
+ }
12525
+ }
12526
+ terminalReportRecoveryCursor;
12527
+ async recoverTerminalReports() {
12528
+ const store = this.jobStore;
12529
+ const dvmId = this.opts.dvmId;
12530
+ if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
12531
+ const recovery = store;
12532
+ const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
12533
+ for (const record of records) await this.reportTerminalJob(record.id);
12534
+ this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
12535
+ }
12409
12536
  /** Handle job reaching terminal state (completed, failed, cancelled). */
12410
12537
  async handleJobTerminal(job) {
12411
12538
  this.clearIdleTimer(job.id);
12539
+ if (await this.adoptDurableTerminal(job)) return;
12540
+ const durable = await this.jobStore.get(job.id);
12541
+ if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
12412
12542
  await this.attachReceipt(job);
12413
12543
  const booked = await this.bookRevenue(job);
12414
12544
  if (!booked) await this.reportJobCost(job);
12545
+ await this.reportTerminalJob(job.id);
12415
12546
  this.scheduleCleanup(job);
12416
12547
  }
12417
12548
  /**
@@ -12424,6 +12555,7 @@ var JobManager = class {
12424
12555
  async recoverTerminalCosts() {
12425
12556
  const store = this.jobStore;
12426
12557
  const dvmId = this.opts.dvmId;
12558
+ await this.recoverTerminalReports();
12427
12559
  if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
12428
12560
  return { examined: 0, queued: 0, complete: true };
12429
12561
  }
@@ -12508,11 +12640,11 @@ var JobManager = class {
12508
12640
  return true;
12509
12641
  }
12510
12642
  /**
12511
- * Report a completed paid job's revenue (internal-review, re-keyed by internal-review).
12643
+ * Report a completed paid job's revenue under its durable job identity.
12512
12644
  * Fire-and-forget — the `RevenueReporter` owns persistence and retry.
12513
12645
  *
12514
12646
  * 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
12647
+ * §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
12516
12648
  * draw against the draw itself: `withDrawBasis` stamps the job when the
12517
12649
  * payment lands, but a Bitcoin credit's settle re-prices the draw against
12518
12650
  * the funding lots it consumed. The correction is a delta, because a job's
@@ -12527,7 +12659,7 @@ var JobManager = class {
12527
12659
  * whose price couldn't be fiat-denominated) nothing changes: the pre-credits
12528
12660
  * payload is reported verbatim.
12529
12661
  *
12530
- * Takes the fields rather than a `ServerJob` so the internal-review reconciler — a
12662
+ * Takes the fields rather than a `ServerJob` so the reconciler — a
12531
12663
  * background sweep that holds no in-process job — books through this exact
12532
12664
  * path off the row it read. `paymentTxHash` is passed **verbatim**, never
12533
12665
  * recomputed as a `drawSettlementRef`: it is the same column the in-line
@@ -12544,7 +12676,7 @@ var JobManager = class {
12544
12676
  * DVM with no reporter). Every drop that costs a booking logs first.
12545
12677
  *
12546
12678
  * 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
12679
+ * a settled draw the terminal can't report under is a real debit
12548
12680
  * with no revenue event, permanently overstating outstanding liability
12549
12681
  * (`deposits − draw revenue`), and it used to return here in silence. Placed
12550
12682
  * after the re-read, `revenue_skipped_no_rail` can name the draw and its
@@ -12607,9 +12739,9 @@ var JobManager = class {
12607
12739
  paidMsats: job.paidMsats,
12608
12740
  creditRail: creditRail ?? null,
12609
12741
  // 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
12742
+ // is the else-branch: the credit predates and has no rail
12611
12743
  // (`credit_rail_null`); it carries one this build doesn't list, the
12612
- // internal-review sibling class (`credit_rail_unrecognized`); the credit's
12744
+ // sibling class (`credit_rail_unrecognized`); the credit's
12613
12745
  // rail is perfectly good and it is the *job row* that lost its own
12614
12746
  // (`job_rail_missing`), which no change to the ledger would fix; or
12615
12747
  // this manager was built with no `creditLedger` to read the draw
@@ -12657,7 +12789,7 @@ var JobManager = class {
12657
12789
  cashuFlow: job.cashuFlow,
12658
12790
  // Absent unless the handler declared one, and absent is a fact: the
12659
12791
  // platform shows "no costs reported" rather than a zero cost and a
12660
- // 100% margin (internal-review). Both halves or neither — a bare amount
12792
+ // 100% margin. Both halves or neither — a bare amount
12661
12793
  // in an unnamed currency is not a cost.
12662
12794
  ...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
12663
12795
  ...credit
@@ -12840,8 +12972,8 @@ var MintHealthTracker = class {
12840
12972
  firstProbeComplete = false;
12841
12973
  constructor(opts) {
12842
12974
  this.mints = [...opts.mints];
12843
- this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
12844
- this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
12975
+ this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
12976
+ this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
12845
12977
  this.fetchImpl = opts.fetchImpl;
12846
12978
  this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
12847
12979
  this.nowFn = opts.now ?? Date.now;
@@ -12892,7 +13024,7 @@ var MintHealthTracker = class {
12892
13024
  /**
12893
13025
  * True once the first full probe tick has landed. Before this, paid calls
12894
13026
  * are held at `503 mint_health_pending` rather than accepted against
12895
- * unvalidated mints (internal-review).
13027
+ * unvalidated mints.
12896
13028
  */
12897
13029
  initialProbeComplete() {
12898
13030
  return this.firstProbeComplete;
@@ -12917,7 +13049,7 @@ var MintHealthTracker = class {
12917
13049
  * gate — a deterministic, permanent config error, unlike a transient outage.
12918
13050
  * Unknown mints return false. `advertisedMints()` uses this to keep a
12919
13051
  * permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
12920
- * where a merely-flapping mint is still published (internal-review).
13052
+ * where a merely-flapping mint is still published.
12921
13053
  */
12922
13054
  isNutIncompatible(mintUrl) {
12923
13055
  return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
@@ -12984,7 +13116,7 @@ var MintHealthTracker = class {
12984
13116
  * One-shot structured summary emitted after the first probe tick. Replaces
12985
13117
  * the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
12986
13118
  * all-mints-unhealthy warning — the operator alert that the Fly restart-loop
12987
- * used to be before boot stopped refusing to start (internal-review).
13119
+ * used to be before boot stopped refusing to start.
12988
13120
  */
12989
13121
  logBootHealth() {
12990
13122
  const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
@@ -13009,15 +13141,15 @@ var MintHealthTracker = class {
13009
13141
  );
13010
13142
  }
13011
13143
  };
13012
- function resolveEnvTickMs() {
13013
- const raw = process.env.DVMKIT_MINT_HEALTH_TICK_MS;
13144
+ function resolveEnvTickMs(env = process.env) {
13145
+ const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
13014
13146
  if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
13015
13147
  const parsed = Number(raw);
13016
13148
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
13017
13149
  return Math.floor(parsed);
13018
13150
  }
13019
- function resolveEnvFailuresToSick() {
13020
- const raw = process.env.DVMKIT_MINT_FAILURES_TO_SICK;
13151
+ function resolveEnvFailuresToSick(env = process.env) {
13152
+ const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
13021
13153
  if (!raw) return DEFAULT_FAILURES_TO_SICK;
13022
13154
  const parsed = Number(raw);
13023
13155
  if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
@@ -14068,9 +14200,9 @@ async function handlePendingDrains(c, opts) {
14068
14200
  sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
14069
14201
  // The figure a `manual` payout is actually sized in: `amount_micro` is
14070
14202
  // fiat, and the operator sends the rail's own token. It is the same debit
14071
- // `completeX402ChannelDrain` treats as its settlement floor (internal-review),
14203
+ // `completeX402ChannelDrain` treats as its settlement floor,
14072
14204
  // 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).
14205
+ // of assuming their stablecoin happens to be six-decimal.
14074
14206
  amount_native: d.drainedNative,
14075
14207
  settlement,
14076
14208
  // Why this row is classified as it is, and the channel an operator would
@@ -14593,7 +14725,7 @@ async function handleReconcileInvoice(c, opts) {
14593
14725
  fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
14594
14726
  expiryMs,
14595
14727
  // The settlements table types this deposit `repair` off the
14596
- // flag (internal-review): the sats landed at payment time and the
14728
+ // flag: the sats landed at payment time and the
14597
14729
  // deposit is the one row they get.
14598
14730
  repaired: true
14599
14731
  }
@@ -16588,7 +16720,7 @@ function autoApprovePayment(content) {
16588
16720
  headers: jobHeaders(true),
16589
16721
  // amount_msats rides along so an onPayment handler sees the same shape a
16590
16722
  // 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.
16723
+ // hence the status check below rather than a silent catch.
16592
16724
  body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
16593
16725
  }).then(function(r) {
16594
16726
  if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
@@ -16735,7 +16867,7 @@ async function createDVMServer(descriptor, opts) {
16735
16867
  }
16736
16868
  })();
16737
16869
  try {
16738
- if (process.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16870
+ if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
16739
16871
  console.warn(
16740
16872
  JSON.stringify({
16741
16873
  level: "cashu_opts_env_ignored",
@@ -16748,12 +16880,12 @@ async function createDVMServer(descriptor, opts) {
16748
16880
  const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
16749
16881
  const dvmId = opts.dvmId;
16750
16882
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
16751
- const { RevenueReporter } = await import("./revenue-reporter-ASZ7SHHH.js");
16883
+ const { RevenueReporter } = await import("./revenue-reporter-NNCNRY4C.js");
16752
16884
  const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
16753
16885
  revenueReporter = reporter;
16754
16886
  await reporter.init();
16755
16887
  reporter.startRetryLoop();
16756
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-RG6XNGPI.js");
16888
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5PIRYFVQ.js");
16757
16889
  payoutReporter = new PayoutReporter({
16758
16890
  dvmId,
16759
16891
  db: opts.db,
@@ -16896,6 +17028,7 @@ async function createDVMServer(descriptor, opts) {
16896
17028
  processedPayments,
16897
17029
  onJobCompleted: effectiveOnJobCompleted,
16898
17030
  onJobCost: effectiveOnJobCost,
17031
+ onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
16899
17032
  onPaidJobDeath: effectiveOnPaidJobDeath,
16900
17033
  onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
16901
17034
  onCreditFunded: opts.onCreditFunded,
@@ -16939,7 +17072,7 @@ var DVMServer = class {
16939
17072
  fxFetcher;
16940
17073
  /**
16941
17074
  * The currency this DVM prices in and denominates its credit ledger in,
16942
- * resolved once at construction (internal-review).
17075
+ * resolved once at construction.
16943
17076
  *
16944
17077
  * `configureDVM` and the CLI loader both resolve this field. The server is
16945
17078
  * also exported for direct JavaScript callers, so defaulting at the one
@@ -16948,29 +17081,24 @@ var DVMServer = class {
16948
17081
  */
16949
17082
  pricingCurrency;
16950
17083
  /**
16951
- * Last-good lock-pubkey state observed from Postgres (internal-review). Used as the
17084
+ * Last-good lock-pubkey state observed from Postgres. Used as the
16952
17085
  * fallback when `loadLockPubkeyState` throws — degrades more gracefully than
16953
17086
  * falling back to the env-derived seed, which post-rotation is the *retired*
16954
17087
  * pubkey. Null until the first successful read.
16955
17088
  */
16956
17089
  cachedLockPubkeyState = null;
16957
- /** Guards the opportunistic Tempo settlement cadence (internal-review). */
16958
17090
  tempoSweepInFlight = false;
16959
17091
  tempoSweepLastAttemptMs = 0;
16960
- /** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
16961
17092
  receiptIssuer;
16962
- /** Credit ledger the payment gate funds/draws through (internal-review). */
16963
17093
  creditLedger;
16964
- /** Durable x402/mpp funding markers (internal-review). */
16965
17094
  processedPayments;
16966
17095
  authAudience;
16967
17096
  /** Lazily constructed when a refreshed facilitator probe gains batch support. */
16968
17097
  x402BatchSettlement;
16969
17098
  /** Coalesces concurrent requests that first observe batch support. */
16970
17099
  x402BatchSettlementInit;
16971
- /** Durable exact-settlement coordinator shared by every exact payment surface. */
16972
17100
  x402ExactSettlement;
16973
- /** Withdraws this mount's Tempo session capability from the poll (internal-review). */
17101
+ /** Withdraws this mount's Tempo session capability from the poll. */
16974
17102
  undeclareSessionCapability;
16975
17103
  constructor(descriptor, opts) {
16976
17104
  this.descriptor = descriptor;
@@ -16994,7 +17122,7 @@ var DVMServer = class {
16994
17122
  this.mintHealthTracker = opts.mintHealthTracker;
16995
17123
  this.ownsMintHealthTracker = false;
16996
17124
  } else if (opts.mints && opts.mints.length > 0) {
16997
- this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
17125
+ this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
16998
17126
  this.ownsMintHealthTracker = true;
16999
17127
  } else {
17000
17128
  this.ownsMintHealthTracker = false;
@@ -17015,7 +17143,7 @@ var DVMServer = class {
17015
17143
  mints: opts.mints,
17016
17144
  jobStore: opts.jobStore,
17017
17145
  devMode: opts.devMode,
17018
- // Derived, not raw (internal-review). `ctx.requestPayment` gates its x402 and
17146
+ // Derived, not raw. `ctx.requestPayment` gates its x402 and
17019
17147
  // Tempo challenge issuance on this list, while `/v1/info`, `/v1/quote`
17020
17148
  // and the upfront 402 all read `resolvePaymentMethods()` — so passing the
17021
17149
  // raw option meant a DVM that never set `paymentMethods` (no first-party
@@ -17040,21 +17168,20 @@ var DVMServer = class {
17040
17168
  dvmId: opts.dvmId,
17041
17169
  authAudience: this.authAudience,
17042
17170
  fxFetcher: this.fxFetcher,
17043
- // internal-review: the mid-job top-up leg needs the same rail wiring the upfront
17171
+ // the mid-job top-up leg needs the same rail wiring the upfront
17044
17172
  // path gets — a divergence here would be a DVM whose upfront payment funds
17045
17173
  // a credit and whose mid-job payment silently doesn't.
17046
17174
  processedPayments: this.processedPayments,
17047
17175
  enqueueCreditDeposit: opts.enqueueCreditDeposit,
17048
17176
  enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
17049
17177
  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
17178
  pricingCurrency: this.pricingCurrency,
17053
17179
  onCreditFunded: opts.onCreditFunded,
17054
17180
  onJobCompleted: opts.onJobCompleted,
17055
17181
  onPaidJobCompleted: opts.onPaidJobCompleted,
17056
17182
  paymentMode: opts.paymentMode,
17057
17183
  onJobCost: opts.onJobCost,
17184
+ onJobTerminal: opts.onJobTerminal,
17058
17185
  onPaidJobDeath: opts.onPaidJobDeath,
17059
17186
  onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
17060
17187
  onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
@@ -17092,7 +17219,7 @@ var DVMServer = class {
17092
17219
  * Tear down this mount. Every leg is synchronous except the batch-settlement
17093
17220
  * server's own `stop()`; the returned promise settles once that has finished,
17094
17221
  * 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
17222
+ * gate's store owns — can sequence its close instead of pulling it
17096
17223
  * out from under an in-flight stop. A rejection remains observable so the app
17097
17224
  * and host teardown layers can finish later cleanup before surfacing it.
17098
17225
  */
@@ -17193,12 +17320,12 @@ var DVMServer = class {
17193
17320
  (c) => c.json({
17194
17321
  ok: true,
17195
17322
  // 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,
17323
+ // marker that a raw truncation would cut off the end,
17197
17324
  // leaving this endpoint claiming a clean commit while /v1/info's
17198
17325
  // 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"
17326
+ gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
17327
+ deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
17328
+ deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17202
17329
  })
17203
17330
  );
17204
17331
  if (this.opts.devMode) {
@@ -17231,8 +17358,6 @@ var DVMServer = class {
17231
17358
  getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
17232
17359
  graceSeconds: this.graceSeconds(),
17233
17360
  nonceStore: this.adminCashuNonceStore,
17234
- // internal-review: a completed melt is a landed payout, queued in the
17235
- // transaction that marks it.
17236
17361
  ...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
17237
17362
  });
17238
17363
  }
@@ -17250,13 +17375,13 @@ var DVMServer = class {
17250
17375
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
17251
17376
  enqueueCreditDrain: this.opts.enqueueCreditDrain,
17252
17377
  lightningReceive: this.opts.lightningReceive,
17253
- // internal-review's x402 repair queue rides the same surface, and resolves
17378
+ // x402 repair queue rides the same surface, and resolves
17254
17379
  // the rail the same lazy way the verify path does — constructing the
17255
17380
  // settlement server probes the facilitator, which must not happen just
17256
17381
  // because the admin routes were mounted.
17257
17382
  x402BatchSettlement: () => this.resolveX402BatchSettlement(),
17258
17383
  x402ExactSettlement: this.x402ExactSettlement,
17259
- // internal-review's Tempo repair queue, same surface again. The mppx handle is
17384
+ // Tempo repair queue, same surface again. The mppx handle is
17260
17385
  // already built by the time routes mount, so the thunk is uniformity
17261
17386
  // rather than laziness — and it is the *handle*, not the store, so a
17262
17387
  // DVM wired with an in-memory session store resolves to a rail with no
@@ -17264,10 +17389,8 @@ var DVMServer = class {
17264
17389
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
17265
17390
  // Health-filtered, and read per request rather than captured: the
17266
17391
  // servicer picking a mint to buy a refund at wants the list that
17267
- // excludes a mint currently failing its probe (internal-review).
17392
+ // excludes a mint currently failing its probe.
17268
17393
  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
17394
  ...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
17272
17395
  });
17273
17396
  }
@@ -17278,7 +17401,7 @@ var DVMServer = class {
17278
17401
  /**
17279
17402
  * Load the live lock-pubkey state for this DVM. Reads from
17280
17403
  * `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
17404
+ * a process restart. On success the result is cached so a later
17282
17405
  * DB outage can degrade to the last-observed state instead of the
17283
17406
  * env-derived seed (which post-rotation is the *retired* pubkey).
17284
17407
  */
@@ -17289,7 +17412,7 @@ var DVMServer = class {
17289
17412
  }
17290
17413
  /**
17291
17414
  * 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
17415
+ * Returns the same `[current, ...retired_in_grace]` shape the
17293
17416
  * upfront flow uses at lines 709-718 so accept-time rotation tolerance is
17294
17417
  * consistent across upfront and mid-job. Falls back to `undefined` when
17295
17418
  * accumulator mode isn't wired (the receive path then rejects with
@@ -17326,13 +17449,13 @@ var DVMServer = class {
17326
17449
  }
17327
17450
  /**
17328
17451
  * Run the routine Tempo settlement cadence opportunistically, on ordinary
17329
- * traffic (internal-review).
17452
+ * traffic.
17330
17453
  *
17331
17454
  * Routine settlement used to ride the per-DVM watcher machine's four-minute
17332
17455
  * poll, which is exactly what kept an idle DVM awake and billing. It cannot
17333
17456
  * move to a timer for the same reason — a timer on a machine that ought to
17334
17457
  * 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
17458
+ * requests instead, the way Lightning funding is credited: a DVM
17336
17459
  * with no traffic has nothing to settle on a 24-hour cadence anyway, and the
17337
17460
  * forced-close guarantee does not depend on this path at all — that is the
17338
17461
  * chain-event wake, which reaches a suspended DVM through Fly Proxy.
@@ -17372,7 +17495,7 @@ var DVMServer = class {
17372
17495
  });
17373
17496
  }
17374
17497
  /**
17375
- * Whether one advertised MPP method may still be offered (internal-review).
17498
+ * Whether one advertised MPP method may still be offered.
17376
17499
  *
17377
17500
  * Only `tempo/session` is gated, and only when this DVM is platform-hosted:
17378
17501
  * a session channel is safe to accept exactly while something is watching
@@ -17388,7 +17511,6 @@ var DVMServer = class {
17388
17511
  tempoSessionOffered(method) {
17389
17512
  return this.tempoSessionWithheldReason(method) === void 0;
17390
17513
  }
17391
- /** Stable caller-facing reason for withholding one newly opened session. */
17392
17514
  tempoSessionWithheldReason(method) {
17393
17515
  if (method.method !== "tempo" || method.intent !== "session") return void 0;
17394
17516
  if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
@@ -17399,7 +17521,6 @@ var DVMServer = class {
17399
17521
  }
17400
17522
  return void 0;
17401
17523
  }
17402
- /** Await only the cold-start reads each fail-closed session gate needs. */
17403
17524
  async ensureTempoSessionHealth() {
17404
17525
  await Promise.all([
17405
17526
  this.opts.tempoObserverHealth?.ensureFresh?.(),
@@ -17410,7 +17531,6 @@ var DVMServer = class {
17410
17531
  * Whether this DVM is **configured** to take TIP-1034 session channels — the
17411
17532
  * value the observer-health poll carries back to the platform, where it keeps
17412
17533
  * `dvms.tempo_session_advertised_at` current between container deploys
17413
- * (internal-review).
17414
17534
  *
17415
17535
  * Deliberately the same question the platform's post-deploy `/v1/info` probe
17416
17536
  * answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
@@ -17422,13 +17542,12 @@ var DVMServer = class {
17422
17542
  * It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
17423
17543
  * and never {@link tempoSessionOffered}: what the column records is
17424
17544
  * 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
17545
+ * distinction is the whole of and answering it here from the gate
17426
17546
  * would reintroduce it on a one-minute cadence.
17427
17547
  */
17428
17548
  tempoSessionCapability() {
17429
17549
  if (!this.opts.mpp) return false;
17430
17550
  if (!creditMenuConfigured({
17431
- // The SDK server only ever runs the container runtime — see `creditMenu`.
17432
17551
  runtime: "container",
17433
17552
  config: this.descriptor.credit,
17434
17553
  hasAuth: !!this.descriptor.auth,
@@ -17443,7 +17562,7 @@ var DVMServer = class {
17443
17562
  }
17444
17563
  /**
17445
17564
  * `POST /_internal/tempo-close-event` — the forced-close wake path
17446
- * (internal-review), replacing the per-DVM `tempo-session-watch` Fly Machine.
17565
+ * replacing the per-DVM `tempo-session-watch` Fly Machine.
17447
17566
  *
17448
17567
  * Deliberately **unauthenticated**, and that is the design rather than an
17449
17568
  * omission: the body is an event locator, so the DVM re-reads the event from
@@ -17522,7 +17641,7 @@ var DVMServer = class {
17522
17641
  }
17523
17642
  /**
17524
17643
  * Compose the `/v1/info#builder` block from `opts.builder` (inline SDK
17525
- * config) with a fallback to deploy-injected env vars (internal-review). Each
17644
+ * config) with a fallback to deploy-injected env vars. Each
17526
17645
  * field is independently sourced (opts wins per-field, env fills gaps);
17527
17646
  * all three of pubkey / attestation / signature must resolve to emit the
17528
17647
  * block — partial state is treated as absent rather than half-emitted.
@@ -17535,8 +17654,8 @@ var DVMServer = class {
17535
17654
  builderAttestationBlock() {
17536
17655
  const builder = this.opts.builder;
17537
17656
  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;
17657
+ const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
17658
+ const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
17540
17659
  const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
17541
17660
  if (!pubkey || !signature || !attestation) return null;
17542
17661
  return { pubkey, attestation, signature };
@@ -17568,7 +17687,7 @@ var DVMServer = class {
17568
17687
  throw new AuthAudienceError();
17569
17688
  }
17570
17689
  /**
17571
- * The slug every receipt is stamped with (internal-review). Read off the signed
17690
+ * The slug every receipt is stamped with. Read off the signed
17572
17691
  * attestation payload, which is the only place the *attested* slug lives —
17573
17692
  * a receipt claiming a different name than the attestation binds would be
17574
17693
  * unverifiable. Falls back to the descriptor name for dev/self-host boots
@@ -17579,14 +17698,13 @@ var DVMServer = class {
17579
17698
  const slug = attestation?.slug;
17580
17699
  return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
17581
17700
  }
17582
- /** Immutable identity written into every signed receipt. */
17583
17701
  receiptDvmId() {
17584
17702
  const attestation = this.builderAttestationBlock()?.attestation;
17585
17703
  const dvmId = attestation?.dvm_id;
17586
17704
  return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
17587
17705
  }
17588
17706
  parseAttestationFromEnv(env) {
17589
- const raw = env.DVMKIT_BUILDER_ATTESTATION ?? process.env.DVMKIT_BUILDER_ATTESTATION;
17707
+ const raw = env.DVMKIT_BUILDER_ATTESTATION;
17590
17708
  if (!raw) return null;
17591
17709
  try {
17592
17710
  const parsed = JSON.parse(raw);
@@ -17597,7 +17715,7 @@ var DVMServer = class {
17597
17715
  }
17598
17716
  }
17599
17717
  /**
17600
- * Owner display block for `/v1/info#owner` (internal-review). Resolved from
17718
+ * Owner display block for `/v1/info#owner`. Resolved from
17601
17719
  * `opts.owner` or env vars injected by the platform deploy pipeline.
17602
17720
  * Returns null when no owner identity is configured — pre-unification
17603
17721
  * DVMs that haven't been redeployed omit the block.
@@ -17605,11 +17723,11 @@ var DVMServer = class {
17605
17723
  ownerDisplayBlock() {
17606
17724
  const owner = this.opts.owner;
17607
17725
  const env = this.opts.env;
17608
- const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE ?? process.env.DVMKIT_OWNER_HANDLE;
17726
+ const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
17609
17727
  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;
17728
+ const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
17729
+ const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
17730
+ const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
17613
17731
  return { handle, display_name: displayName, avatar_url: avatarUrl, type };
17614
17732
  }
17615
17733
  defaultPaymentMethods() {
@@ -17628,14 +17746,13 @@ var DVMServer = class {
17628
17746
  if (!this.opts.x402.batchSettlement?.selfRelay) return support;
17629
17747
  return { ...support, batchSettlement: true };
17630
17748
  }
17631
- /** Whether this DVM can state its price exactly in x402's USDC unit. */
17632
17749
  x402CurrencySupported() {
17633
17750
  return this.pricingCurrency === "usd";
17634
17751
  }
17635
17752
  /**
17636
17753
  * Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
17637
17754
  * 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).
17755
+ * the `/v1/info` and `/v1/quote` rail enumerations can't drift.
17639
17756
  */
17640
17757
  resolvePaymentMethods() {
17641
17758
  const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
@@ -17648,13 +17765,13 @@ var DVMServer = class {
17648
17765
  }
17649
17766
  /**
17650
17767
  * Rails a **credit top-up** may arrive on — the funding menu's list
17651
- * (internal-review). A superset of {@link resolvePaymentMethods}, and deliberately
17768
+ * A superset of {@link resolvePaymentMethods}, and deliberately
17652
17769
  * a separate read: `lightning` funds a credit but is not an attached-proof
17653
17770
  * wire method, so it must never reach `/v1/info` or `/v1/quote`'s
17654
- * `payment.methods` (internal-review stands).
17771
+ * `payment.methods`.
17655
17772
  *
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
17773
+ * The rail drops out of the funding menu when the receive wallet is unreachable;
17774
+ * advertising it then would hand the caller an option that
17658
17775
  * fails on use, while the other rails would have closed the sale.
17659
17776
  */
17660
17777
  resolveFundingMethods() {
@@ -17673,7 +17790,7 @@ var DVMServer = class {
17673
17790
  }
17674
17791
  /**
17675
17792
  * 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).
17793
+ * and what this DVM is configured for but withholding.
17677
17794
  *
17678
17795
  * The split has to survive into the response rather than being filtered
17679
17796
  * away, because "not offered right now" and "not configured" are different
@@ -17701,7 +17818,7 @@ var DVMServer = class {
17701
17818
  }
17702
17819
  /**
17703
17820
  * 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
17821
+ * `x402` sub-block. Hosted health buys `exact`; a genuinely wired
17705
17822
  * batch server buys `batch-settlement` independently, including while the
17706
17823
  * hosted facilitator is unavailable or stale.
17707
17824
  *
@@ -17718,13 +17835,12 @@ var DVMServer = class {
17718
17835
  ...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
17719
17836
  ];
17720
17837
  }
17721
- /** Whether a channel server is both supported and constructible for credit funding. */
17722
17838
  x402BatchAvailable() {
17723
17839
  return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
17724
17840
  }
17725
17841
  /**
17726
17842
  * 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).
17843
+ * looked — the pull half of pull-based crediting.
17728
17844
  *
17729
17845
  * Called from every authenticated surface where the balance is about to
17730
17846
  * matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
@@ -17737,7 +17853,7 @@ var DVMServer = class {
17737
17853
  * outstanding, and never blocks the request when it fails.
17738
17854
  *
17739
17855
  * The SDK-owned reporter queues each Lightning deposit inside
17740
- * `settleInvoice`'s transaction (internal-review). The loop below only preserves
17856
+ * `settleInvoice`'s transaction. The loop below only preserves
17741
17857
  * the explicit `onCreditFunded` override, whose caller owns its durability.
17742
17858
  */
17743
17859
  async settleLightningFunding(callerPubkey, creditId) {
@@ -17768,7 +17884,6 @@ var DVMServer = class {
17768
17884
  });
17769
17885
  }
17770
17886
  }
17771
- /** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
17772
17887
  creditTtlMs() {
17773
17888
  return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
17774
17889
  }
@@ -17789,7 +17904,7 @@ var DVMServer = class {
17789
17904
  * The per-DVM + per-request rail wiring every payment verification needs —
17790
17905
  * everything except what the *operation* contributes (its price, its job or
17791
17906
  * 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
17907
+ * and `/v1/credit`'s top-up so a rail wired into one is wired
17793
17908
  * into both by construction; a divergence here would be a rail that funds
17794
17909
  * jobs but silently can't fund credits.
17795
17910
  */
@@ -17822,7 +17937,7 @@ var DVMServer = class {
17822
17937
  mppCredential,
17823
17938
  mpp: this.opts.mpp,
17824
17939
  resourcePath: c.req.path,
17825
- // internal-review/internal-review: the gate covers issuance and new-channel acceptance,
17940
+ // the gate covers issuance and new-channel acceptance,
17826
17941
  // not just the two advertisement surfaces. `ensureFresh` above makes this
17827
17942
  // an observation rather than a cold-start default on the one surface that
17828
17943
  // can act on it.
@@ -17840,8 +17955,6 @@ var DVMServer = class {
17840
17955
  requestId: c.req.header("X-Cashu-Request-Id"),
17841
17956
  mintHealthTracker: this.mintHealthTracker,
17842
17957
  fxFetcher: this.fxFetcher,
17843
- // internal-review: the ledger leg — every rail payment becomes fund_credit +
17844
- // draw, atomic with the rail commit.
17845
17958
  creditLedger: this.creditLedger,
17846
17959
  processedPayments: this.processedPayments,
17847
17960
  enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
@@ -17851,7 +17964,7 @@ var DVMServer = class {
17851
17964
  }
17852
17965
  /**
17853
17966
  * Assemble this DVM's funding menu for a response denominated in
17854
- * `currency` (internal-review). All the refusal logic — including the credits-spec
17967
+ * `currency`. All the refusal logic — including the credits-spec
17855
17968
  * §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
17856
17969
  * server's state. `callerPubkey` must be a *verified* envelope pubkey: the
17857
17970
  * balance echo is per-caller money and there is no safe fallback identity.
@@ -17860,8 +17973,6 @@ var DVMServer = class {
17860
17973
  await this.ensureTempoSessionHealth();
17861
17974
  const mppSplit = this.creditMppSplit();
17862
17975
  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
17976
  runtime: "container",
17866
17977
  config: this.descriptor.credit,
17867
17978
  currency,
@@ -17870,7 +17981,7 @@ var DVMServer = class {
17870
17981
  tempoWithheld: mppSplit.withheld,
17871
17982
  x402Schemes: this.creditX402Schemes(),
17872
17983
  lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
17873
- // Configured, not advertised (internal-review): `advertisedMints()` is health
17984
+ // Configured, not advertised: `advertisedMints()` is health
17874
17985
  // filtered, and a rail that comes and goes with a mint's uptime is worse
17875
17986
  // product than one that is honestly absent.
17876
17987
  hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
@@ -17907,7 +18018,7 @@ var DVMServer = class {
17907
18018
  }
17908
18019
  /**
17909
18020
  * Currency this DVM's credit ledger is denominated in — the builder's
17910
- * declared pricing currency, `"usd"` unless they said otherwise (internal-review).
18021
+ * declared pricing currency, `"usd"` unless they said otherwise.
17911
18022
  *
17912
18023
  * Every denomination surface reads this one answer: the funding menu on
17913
18024
  * `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
@@ -17916,20 +18027,16 @@ var DVMServer = class {
17916
18027
  * static (USD-literal) price on a non-USD DVM, and a quote denominated
17917
18028
  * elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
17918
18029
  * 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
18030
  */
17924
18031
  creditCurrency() {
17925
18032
  return this.pricingCurrency;
17926
18033
  }
17927
18034
  /**
17928
- * Quote-response `payment` block (internal-review). Carries the same `methods` array
18035
+ * Quote-response `payment` block. Carries the same `methods` array
17929
18036
  * `/v1/info` advertises plus a per-rail `refundable` map keyed by method, so a
17930
18037
  * caller can see which rails return funds on failure before committing. The
17931
18038
  * `methods` shape stays identical to `/v1/info` and the platform isolate route
17932
- * (post-internal-review nested `payment.methods`); refundability rides alongside.
18039
+ * (current nested `payment.methods`); refundability rides alongside.
17933
18040
  */
17934
18041
  quotePaymentBlock() {
17935
18042
  const methods = this.resolvePaymentMethods();
@@ -17938,8 +18045,6 @@ var DVMServer = class {
17938
18045
  refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
17939
18046
  };
17940
18047
  }
17941
- // ── Route handlers ──────────────────────────────────────────────────
17942
- /** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
17943
18048
  async handleProxy(c) {
17944
18049
  const raw = getCookie(c, "dvmkit_target");
17945
18050
  const target = raw ? decodeURIComponent(raw) : null;
@@ -17975,8 +18080,8 @@ var DVMServer = class {
17975
18080
  auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
17976
18081
  ...this.authAudience ? { auth_audience: this.authAudience } : {},
17977
18082
  protocol_version: 1,
17978
- sdk_sha: process.env.GIT_SHA ?? "unknown",
17979
- deploy_id: process.env.DVMKIT_DEPLOY_ID ?? "unknown"
18083
+ sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
18084
+ deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
17980
18085
  };
17981
18086
  const ownerBlock = this.ownerDisplayBlock();
17982
18087
  if (ownerBlock) info.owner = ownerBlock;
@@ -17997,8 +18102,7 @@ var DVMServer = class {
17997
18102
  network: this.opts.x402.network,
17998
18103
  asset: this.opts.x402.asset ?? "USDC",
17999
18104
  ...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).
18105
+ // Advertises whether 402 challenges include x402 PaymentRequirements.
18002
18106
  dual_serve: this.opts.x402.dualServe ?? true
18003
18107
  };
18004
18108
  }
@@ -18009,11 +18113,11 @@ var DVMServer = class {
18009
18113
  return reason ? [{ ...method, reason }] : [];
18010
18114
  });
18011
18115
  info.tempo = {
18012
- // internal-review: withheld sessions are withheld here too. `/v1/info` is
18116
+ // withheld sessions are withheld here too. `/v1/info` is
18013
18117
  // where a caller decides what to attempt, so advertising a session the
18014
18118
  // credit menu will refuse just sends them down a dead end.
18015
18119
  methods: configured.filter((method) => this.tempoSessionOffered(method)),
18016
- // …but say so rather than letting it vanish (internal-review): an agent that
18120
+ // …but say so rather than letting it vanish: an agent that
18017
18121
  // sees only the shortened list cannot tell a DVM that never did
18018
18122
  // sessions from one whose observer is down for a minute.
18019
18123
  ...withheld.length && { withheld },
@@ -18045,7 +18149,7 @@ var DVMServer = class {
18045
18149
  return c.json(info);
18046
18150
  }
18047
18151
  /**
18048
- * Render the `/v1/info.capabilities` block (internal-review). Each capability
18152
+ * Render the `/v1/info.capabilities` block. Each capability
18049
18153
  * advertises its `description`, optional `input_schema`, and optional
18050
18154
  * `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
18051
18155
  * `pricing.max` for static-priced ones. Capabilities without an `input`
@@ -18255,7 +18359,7 @@ var DVMServer = class {
18255
18359
  * the two callers — `/v1/quote` and `computeDynamicPrice` on the submit
18256
18360
  * path — cannot drift: a field added for one is present in the other by
18257
18361
  * construction, and both paths price against the same view of the caller's
18258
- * credit (internal-review, spec §7).
18362
+ * credit.
18259
18363
  */
18260
18364
  async buildQuoteContext(args) {
18261
18365
  const ctx = {
@@ -18278,15 +18382,15 @@ var DVMServer = class {
18278
18382
  }
18279
18383
  /**
18280
18384
  * Validate the `capability` field on a `/v1/job` or `/v1/quote` body
18281
- * (internal-review). Returns the canonical name when set + known; otherwise a
18385
+ * Returns the canonical name when set + known; otherwise a
18282
18386
  * structured 400 envelope agents can relay verbatim to a human caller.
18283
18387
  *
18284
18388
  * - **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.
18389
+ * `capability_required` with a hint pointing at `/v1/info` so the agent
18390
+ * can re-discover the capability set.
18287
18391
  * - **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.
18392
+ * capability on this descriptor. Surface as `unknown_capability` listing
18393
+ * the known names so the agent can correct without a round-trip.
18290
18394
  */
18291
18395
  requireCapabilityFromBody(c, capability) {
18292
18396
  const known = this.jobManager.capabilityNames();
@@ -18345,25 +18449,9 @@ var DVMServer = class {
18345
18449
  }
18346
18450
  }
18347
18451
  /**
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.
18452
+ * Shared schema-violation response for quote and submit, including auth-less
18453
+ * DVMs. A 400 classifies caller input without requiring an auth gate; `display`
18454
+ * and `message` carry the same relayable explanation.
18367
18455
  */
18368
18456
  invalidInputResponse(c, err) {
18369
18457
  const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
@@ -18407,13 +18495,13 @@ var DVMServer = class {
18407
18495
  }
18408
18496
  /**
18409
18497
  * True when a failed capability-input parse is the auth gate's answer rather
18410
- * than {@link invalidInputResponse}'s (internal-review).
18498
+ * than {@link invalidInputResponse}'s.
18411
18499
  *
18412
18500
  * A capability schema may declare the signing-envelope fields required
18413
18501
  * (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
18414
18502
  * the required shape an *unsigned* request fails the input parse naming
18415
18503
  * `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
18504
+ * input is invalid: pubkey required" is the mistake in reverse. So a
18417
18505
  * parse whose issues are **only** envelope-side falls through to the auth
18418
18506
  * gate, which answers 401 `signature_invalid` with the remedy that works.
18419
18507
  *
@@ -18430,13 +18518,13 @@ var DVMServer = class {
18430
18518
  }
18431
18519
  /**
18432
18520
  * 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
18521
+ * configured list through the runtime health tracker. When every
18434
18522
  * mint is currently sick it falls back to the still-configured list rather
18435
18523
  * than publish an empty rail — a *transient* outage shouldn't read as "no
18436
18524
  * 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
18525
+ * mints are excluded even from that fallback: NUT-incompatibility
18438
18526
  * 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*
18527
+ * against, so such a mint is never advertised. If *every*
18440
18528
  * configured mint is NUT-incompatible the rail correctly goes empty.
18441
18529
  */
18442
18530
  advertisedMints() {
@@ -18865,9 +18953,9 @@ var DVMServer = class {
18865
18953
  priceMsats,
18866
18954
  capabilityName,
18867
18955
  requesterTokenHash,
18868
- // internal-review: everything the idempotent-replay path needs to recognise a
18956
+ // everything the idempotent-replay path needs to recognise a
18869
18957
  // 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
18958
+ // `jobId` is the pre-allocated id the payment's ledger draw
18871
18959
  // already committed under.
18872
18960
  {
18873
18961
  requesterToken: jobToken,
@@ -18906,7 +18994,7 @@ var DVMServer = class {
18906
18994
  }
18907
18995
  }
18908
18996
  /**
18909
- * Book a committed funding as a deposit (internal-review, spec §10). Fire-and-forget
18997
+ * Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
18910
18998
  * onto the reporter's durable queue: a funding is a liability the platform
18911
18999
  * must show, but a reporting hiccup must never fail the paid request that
18912
19000
  * just succeeded.
@@ -18921,7 +19009,7 @@ var DVMServer = class {
18921
19009
  );
18922
19010
  }
18923
19011
  /**
18924
- * Book a kept short payment as tagged revenue (internal-review, operator ruling).
19012
+ * Book a kept short payment as tagged revenue.
18925
19013
  * The proofs are committed and the draw was settled on the spot, so the money
18926
19014
  * is earned — but no job exists to carry it, hence the synthetic job id. It
18927
19015
  * is deliberately **not** a deposit: nothing is owed back, so a liability
@@ -18951,7 +19039,7 @@ var DVMServer = class {
18951
19039
  });
18952
19040
  }
18953
19041
  /**
18954
- * The idempotent half of `POST /v1/job` (internal-review). A paid submit whose
19042
+ * The idempotent half of `POST /v1/job`. A paid submit whose
18955
19043
  * response the caller lost is safe to retry — the accumulator's
18956
19044
  * `UNIQUE(dvm_id, request_id, proof_secret)` makes the duplicate a no-op —
18957
19045
  * but until now it was also useless: the caller got a 409 and the `job_id` /
@@ -18961,7 +19049,7 @@ var DVMServer = class {
18961
19049
  * original caller. Proof is reproducing the request, **not** possessing the
18962
19050
  * spent Cashu token: the token is a bearer secret, so gating on it alone
18963
19051
  * 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
19052
+ * retry always has the body — the resume sweep re-POSTs it — while a
18965
19053
  * token thief does not. On a descriptor-auth DVM the caller pubkey must match
18966
19054
  * too. Anything short of that falls through to the 409 the caller has always
18967
19055
  * gotten.
@@ -18988,8 +19076,8 @@ var DVMServer = class {
18988
19076
  return buildSubmitResponse(c, prior, prior.requesterToken);
18989
19077
  }
18990
19078
  /**
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;
19079
+ * The draw-keyed half of idempotent recovery. The signed body's
19080
+ * `(credit_id, draw_id)` matched a recorded draw;
18993
19081
  * resolve the job it paid for (the draw carries its pre-allocated job id)
18994
19082
  * and re-issue the original response under the SAME proof-of-origin gates
18995
19083
  * as the header regime: reproduce the fingerprint, sign as the same caller
@@ -19078,7 +19166,6 @@ var DVMServer = class {
19078
19166
  }
19079
19167
  return void 0;
19080
19168
  }
19081
- /** Recover one still-pending draw recorded for the stable public job id. */
19082
19169
  async recoverDrawAcceptance(opts) {
19083
19170
  const dvmId = this.opts.dvmId;
19084
19171
  if (!dvmId) return void 0;
@@ -19110,7 +19197,7 @@ var DVMServer = class {
19110
19197
  /**
19111
19198
  * Recompute the upfront price for a dynamic-priced capability by
19112
19199
  * 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
19200
+ * Returns a 400-style error envelope when the input doesn't
19114
19201
  * satisfy `onQuote.schema` or the handler throws — failing closed is the
19115
19202
  * right default, since the alternative (running the job with
19116
19203
  * `requiredMsats=undefined`) lets unpaid jobs through.
@@ -19319,13 +19406,6 @@ var DVMServer = class {
19319
19406
  }
19320
19407
  return c.json({ messages: job.messages.filter((m) => m.seq > after) });
19321
19408
  }
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
19409
  jobNotActiveResponse(c) {
19330
19410
  return c.json(
19331
19411
  {
@@ -19498,8 +19578,8 @@ var DVMServer = class {
19498
19578
  }
19499
19579
  /**
19500
19580
  * 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
19581
+ * down after losing the reactivation advisory lock or deferring to a live original
19582
+ * handler that won the single-execution claim. Leaving
19503
19583
  * the job in `activeJobs` with a live idle timer would, one idle window
19504
19584
  * later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
19505
19585
  * must not persist our stale snapshot; clearing the appender prevents that.
@@ -19535,6 +19615,7 @@ var DVMServer = class {
19535
19615
  await jm.finalizeStoredTerminal(id);
19536
19616
  return c.json({ status: "cancelled" });
19537
19617
  }
19618
+ record.endedBy = "caller";
19538
19619
  record.status = "cancelled";
19539
19620
  record.lastActivityAt = Date.now();
19540
19621
  await jm.store.save(record);
@@ -19880,7 +19961,7 @@ var PlatformTempoObserverHealth = class {
19880
19961
  }
19881
19962
  }
19882
19963
  /**
19883
- * Register one mount's session capability (internal-review).
19964
+ * Register one mount's session capability.
19884
19965
  *
19885
19966
  * A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
19886
19967
  * platform row that token identifies is the deployment as a whole — so the
@@ -19903,7 +19984,7 @@ var PlatformTempoObserverHealth = class {
19903
19984
  * mount exists, so at boot there is routinely an open probe carrying **no**
19904
19985
  * `tempo_session` at all — handing it back to the mount that just declared
19905
19986
  * 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
19987
+ * is exactly the container-restart case is about. Same shape with
19907
19988
  * two mounts: the second would join a probe whose URL already read the first
19908
19989
  * one's `false`.
19909
19990
  */
@@ -19951,7 +20032,7 @@ var PlatformTempoObserverHealth = class {
19951
20032
  }
19952
20033
  /**
19953
20034
  * The probe URL, carrying this deployment's session capability when a mount
19954
- * has declared one (internal-review).
20035
+ * has declared one.
19955
20036
  *
19956
20037
  * Absent means "no signal", not "no sessions": a container running an older
19957
20038
  * SDK build sends nothing, and the platform must leave its stamp exactly