@dvmkit/sdk 0.9.0 → 0.10.1

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 (52) hide show
  1. package/dist/{chunk-GODOH2AF.js → chunk-2OGBZPPJ.js} +43 -21
  2. package/dist/{chunk-4VV7WACY.js → chunk-2XQLL6N6.js} +1 -1
  3. package/dist/{chunk-LCCG5HQM.js → chunk-3Q6FTZDB.js} +2 -2
  4. package/dist/{chunk-TRY7BJA4.js → chunk-3R7NQA3Z.js} +1 -1
  5. package/dist/{chunk-KGL3YMZA.js → chunk-AJ6UHXUA.js} +1 -1
  6. package/dist/{chunk-BAQRUYRL.js → chunk-EWZFANHJ.js} +1 -1
  7. package/dist/{chunk-K2MSIKMM.js → chunk-FFXXGCVG.js} +1 -1
  8. package/dist/{chunk-WLPLHXNP.js → chunk-JNECYURT.js} +1 -1
  9. package/dist/{chunk-TFDR2YP2.js → chunk-KFCEXCZM.js} +1 -1
  10. package/dist/{chunk-IYQQXEHV.js → chunk-LDIPVPKI.js} +1 -1
  11. package/dist/{chunk-QSZW2I5Y.js → chunk-M3T5JYDJ.js} +15 -8
  12. package/dist/{chunk-Q3VYRU5R.js → chunk-MB6R4XPZ.js} +1 -1
  13. package/dist/{chunk-IBX2ORBV.js → chunk-MMGSYMIC.js} +1 -1
  14. package/dist/{chunk-LFVIZH2G.js → chunk-MU3IE434.js} +278 -152
  15. package/dist/{chunk-OIP44KBB.js → chunk-QGNXYVBO.js} +47 -12
  16. package/dist/{chunk-YYRY32NR.js → chunk-STJBBVNL.js} +2 -2
  17. package/dist/{chunk-73C4JN2E.js → chunk-SYBFMHXQ.js} +381 -87
  18. package/dist/{chunk-WB3E7KE7.js → chunk-VMHYGKWT.js} +1 -1
  19. package/dist/{chunk-GAZBBMAA.js → chunk-VTHYQCDB.js} +1 -1
  20. package/dist/{chunk-LW7KZQHG.js → chunk-VUGEEMLT.js} +1 -1
  21. package/dist/{chunk-HC4I7LAV.js → chunk-W3VGCNG6.js} +1 -1
  22. package/dist/{chunk-ZBQKXAYK.js → chunk-YOSZIWC3.js} +1 -1
  23. package/dist/{content-retention-CVBYehRS.d.ts → content-retention-CrILCNxk.d.ts} +1 -1
  24. package/dist/{credit-ledger-CZQ56DYI.js → credit-ledger-4PZNAMDT.js} +3 -3
  25. package/dist/{credit-menu-QntEhZJS.d.ts → credit-menu-BWsVqMh-.d.ts} +6 -4
  26. package/dist/{fx-CMyNFenZ.d.ts → fx-BM5h4o5S.d.ts} +1 -1
  27. package/dist/index.d.ts +5 -5
  28. package/dist/index.js +4 -4
  29. package/dist/internal/caller.d.ts +7 -7
  30. package/dist/internal/caller.js +7 -7
  31. package/dist/internal/server.d.ts +7 -7
  32. package/dist/internal/server.js +20 -20
  33. package/dist/{job-store-qOD-FrQF.d.ts → job-store-92v8e1LQ.d.ts} +1 -1
  34. package/dist/{lightning-backend-DtdG6UT5.d.ts → lightning-backend-9oky_wJi.d.ts} +1 -1
  35. package/dist/{memory-credit-ledger-EMN7G3MO.js → memory-credit-ledger-RP7DT5IY.js} +4 -4
  36. package/dist/{mpp-setup-VW53KFPO.js → mpp-setup-FURD45LI.js} +3 -3
  37. package/dist/{payout-reporter-TZX2S7KG.js → payout-reporter-HVJ6WUF3.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-V464OAJB.js → postgres-consumed-credential-store-WO6OCFXZ.js} +1 -1
  39. package/dist/{postgres-job-store-27VOCSPO.js → postgres-job-store-H45PRBLA.js} +2 -2
  40. package/dist/{postgres-replay-store-7GJFAQXY.js → postgres-replay-store-XXJSA457.js} +2 -2
  41. package/dist/{pricing-NNMBNP7N.js → pricing-X4XU5HXY.js} +3 -3
  42. package/dist/{revenue-reporter-4EAVPWXS.js → revenue-reporter-EV3K5L6B.js} +2 -2
  43. package/dist/server/index.d.ts +75 -11
  44. package/dist/server/index.js +65 -27
  45. package/dist/{step-cache-B7svmPz3.d.ts → step-cache--7yA5Ihr.d.ts} +48 -6
  46. package/dist/{tempo-charge-store-2RPOJBM2.js → tempo-charge-store-5JOIYUIH.js} +2 -2
  47. package/dist/{tempo-session-store-LI3Q3A3U.js → tempo-session-store-S6QMLQUA.js} +3 -3
  48. package/dist/testing/index.d.ts +3 -3
  49. package/dist/testing/index.js +6 -6
  50. package/dist/{usd-D3izFz71.d.ts → usd-C7lvLvLk.d.ts} +1 -1
  51. package/dist/{x402-SL36LSOD.js → x402-EN5LSOVM.js} +3 -3
  52. package/package.json +1 -1
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-XRLKGCAW.js";
6
6
  import {
7
7
  x402EarnedCeiling
8
- } from "./chunk-K2MSIKMM.js";
8
+ } from "./chunk-FFXXGCVG.js";
9
9
  import {
10
10
  createX402FacilitatorAuthHeaders,
11
11
  createX402FacilitatorClient,
@@ -14,13 +14,13 @@ import {
14
14
  verifyWithFacilitator,
15
15
  withPaymentDiagnostics,
16
16
  withPaymentStage
17
- } from "./chunk-GAZBBMAA.js";
17
+ } from "./chunk-VTHYQCDB.js";
18
18
  import {
19
19
  MemoryCreditLedger
20
- } from "./chunk-ZBQKXAYK.js";
20
+ } from "./chunk-YOSZIWC3.js";
21
21
  import {
22
22
  PostgresJobStore
23
- } from "./chunk-GODOH2AF.js";
23
+ } from "./chunk-2OGBZPPJ.js";
24
24
  import {
25
25
  parseStoreValue
26
26
  } from "./chunk-YG7G4DPZ.js";
@@ -31,7 +31,7 @@ import {
31
31
  humanizeZodIssues,
32
32
  signedRequestInput,
33
33
  zodIssuesOf
34
- } from "./chunk-4VV7WACY.js";
34
+ } from "./chunk-2XQLL6N6.js";
35
35
  import {
36
36
  MemoryJobStore,
37
37
  UNCAPPABLE_ASK_TOTAL,
@@ -40,7 +40,7 @@ import {
40
40
  isJobRetentionStore,
41
41
  isReceiptIssuingStore,
42
42
  isStreamableJobStore
43
- } from "./chunk-TFDR2YP2.js";
43
+ } from "./chunk-KFCEXCZM.js";
44
44
  import {
45
45
  ContentHoldError,
46
46
  JobCancelledError,
@@ -80,7 +80,7 @@ import {
80
80
  signReceipt,
81
81
  toLockPubkey,
82
82
  verifyChallenge
83
- } from "./chunk-LW7KZQHG.js";
83
+ } from "./chunk-VUGEEMLT.js";
84
84
  import {
85
85
  X402_BATCH_SETTLEMENT_SCHEME,
86
86
  X402_DEFAULT_FACILITATOR,
@@ -108,7 +108,7 @@ import {
108
108
  } from "./chunk-UKD3R2KU.js";
109
109
  import {
110
110
  createConsoleLogger
111
- } from "./chunk-TRY7BJA4.js";
111
+ } from "./chunk-3R7NQA3Z.js";
112
112
  import {
113
113
  StepCache
114
114
  } from "./chunk-BQJIFRAZ.js";
@@ -125,7 +125,7 @@ import {
125
125
  roundUsd,
126
126
  satsToFiat,
127
127
  validateCurrency
128
- } from "./chunk-HC4I7LAV.js";
128
+ } from "./chunk-W3VGCNG6.js";
129
129
  import {
130
130
  CreditLedger,
131
131
  CreditLedgerError,
@@ -141,13 +141,13 @@ import {
141
141
  isNonChannelBitcoinRail,
142
142
  netOwedSats,
143
143
  x402SettlementPending
144
- } from "./chunk-YYRY32NR.js";
144
+ } from "./chunk-STJBBVNL.js";
145
145
  import {
146
146
  withSdkInitLock
147
147
  } from "./chunk-COYKJRMM.js";
148
148
  import {
149
149
  withPostgresRecoveryContext
150
- } from "./chunk-IBX2ORBV.js";
150
+ } from "./chunk-MMGSYMIC.js";
151
151
  import {
152
152
  DvmError,
153
153
  x402NetworkByCaip2
@@ -162,17 +162,17 @@ import {
162
162
  installHostedTempoCloseGuard,
163
163
  isTempoInsufficientFundsError,
164
164
  resolveTempoNetwork
165
- } from "./chunk-LFVIZH2G.js";
165
+ } from "./chunk-MU3IE434.js";
166
166
  import {
167
167
  loggers,
168
168
  pinDimension,
169
169
  withSpan,
170
170
  withTraceContext
171
- } from "./chunk-QSZW2I5Y.js";
171
+ } from "./chunk-M3T5JYDJ.js";
172
172
  import {
173
173
  createDiagnosticCapabilityAuthority,
174
174
  diagnosticConsole
175
- } from "./chunk-OIP44KBB.js";
175
+ } from "./chunk-QGNXYVBO.js";
176
176
 
177
177
  // src/sdk/server/lock-pubkey-store.ts
178
178
  async function initLockPubkeyTable(db) {
@@ -3884,7 +3884,7 @@ async function verifyUpfrontPaymentInScope(opts) {
3884
3884
  x402Requirements
3885
3885
  });
3886
3886
  }
3887
- const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
3887
+ const { verifyX402Payment } = await import("./x402-EN5LSOVM.js");
3888
3888
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
3889
3889
  x402Payment,
3890
3890
  x402Config,
@@ -4193,7 +4193,18 @@ async function verifyUpfrontPaymentInScope(opts) {
4193
4193
  replayRealm,
4194
4194
  cred.challenge.id,
4195
4195
  CONSUMED_CREDENTIAL_TTL_MS / 1e3
4196
- ));
4196
+ )).catch((error) => {
4197
+ const transactionHash = tempoReceiptTransactionHash(tempoReceipt);
4198
+ if (!transactionHash) throw error;
4199
+ diagnosticConsole.error({
4200
+ level: "error",
4201
+ event: "tempo_replay_mark_failed",
4202
+ phase: "upfront",
4203
+ rail: "tempo",
4204
+ transaction_hash: transactionHash,
4205
+ ...paymentFailureFields(error)
4206
+ });
4207
+ });
4197
4208
  }
4198
4209
  if (ledgerCtx) {
4199
4210
  try {
@@ -4897,7 +4908,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
4897
4908
  snapshot
4898
4909
  });
4899
4910
  }
4900
- const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
4911
+ const { verifyX402Payment } = await import("./x402-EN5LSOVM.js");
4901
4912
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
4902
4913
  body.content.x402_payment,
4903
4914
  opts.x402Config,
@@ -5328,7 +5339,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
5328
5339
  try {
5329
5340
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5330
5341
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5331
- const { verifyX402Payment } = await import("./x402-SL36LSOD.js");
5342
+ const { verifyX402Payment } = await import("./x402-EN5LSOVM.js");
5332
5343
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5333
5344
  body.content.x402_payment,
5334
5345
  opts.x402Config,
@@ -7572,13 +7583,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7572
7583
  try {
7573
7584
  const state = await rail.mpp.getTempoSessionDrainState?.(expectedChannelId);
7574
7585
  if (state?.finalized || state?.deposit === 0n) {
7575
- const recovered2 = await rail.mpp.recoverTempoSessionClose?.(
7576
- expectedChannelId,
7577
- record.drainId
7578
- );
7579
- if (recovered2) {
7580
- return await markTempoDrainSent(deps, rail.mpp, record, expectedChannelId, recovered2);
7581
- }
7586
+ const recovered2 = await recoverTempoDrain(deps, rail.mpp, record, expectedChannelId);
7587
+ if (recovered2) return recovered2;
7582
7588
  }
7583
7589
  if (state && (state.closeRequestedAt !== 0n || isDefinitiveTempoCloseFailure(error))) {
7584
7590
  return await releaseTempoDrain(c, deps, record, expectedChannelId);
@@ -7666,10 +7672,38 @@ function tempoExitRequiredResponse(record) {
7666
7672
  function tempoCloseCredentialNeedsRefresh(error) {
7667
7673
  return error instanceof MppxErrors.InvalidChallengeError || error instanceof MppxErrors.PaymentExpiredError;
7668
7674
  }
7675
+ function tempoRefundUnprovenResponse(record) {
7676
+ return jsonPaymentError("settlement_pending", 409, {
7677
+ message: "The full Tempo refund could not be proven yet.",
7678
+ display: "Your funds remain held and have not been lost. The DVM operator has been alerted.",
7679
+ hint: `Keep drain ${record.drainId}; no replacement drain is needed.`,
7680
+ extra: { drain_registered: true, drain_id: record.drainId }
7681
+ });
7682
+ }
7669
7683
  async function recoverTempoDrain(deps, mpp, record, channelId) {
7670
7684
  try {
7671
- const reference = await mpp.recoverTempoSessionClose?.(channelId, record.drainId);
7672
- return reference ? await markTempoDrainSent(deps, mpp, record, channelId, reference) : null;
7685
+ const result = await mpp.recoverTempoSessionClose?.(channelId, record.drainId, record.creditId);
7686
+ const durable = await deps.ledger.getDrain({ creditId: record.creditId, drainId: record.drainId });
7687
+ if (durable?.status === "sent") return durable;
7688
+ if (result && typeof result === "object") {
7689
+ return tempoRefundUnprovenResponse(durable ?? record);
7690
+ }
7691
+ if (!result) return null;
7692
+ if (!/^0x[0-9a-fA-F]{64}$/.test(result)) {
7693
+ loggers.tempo.warn("tempo_session_drain_refund_unproven", {
7694
+ channel_id: channelId,
7695
+ credit_id: record.creditId,
7696
+ drain_id: record.drainId,
7697
+ amount_at_stake_native: record.drainedNative?.toString() ?? null,
7698
+ amount_at_stake_micro: record.amountMicro,
7699
+ currency: record.currency,
7700
+ reason: "close_evidence_missing",
7701
+ error: "Recovery returned no proven refund"
7702
+ });
7703
+ return tempoRefundUnprovenResponse(durable ?? record);
7704
+ }
7705
+ if (durable?.status === "exit_required") return null;
7706
+ return await markTempoDrainSent(deps, mpp, durable ?? record, channelId, result);
7673
7707
  } catch (error) {
7674
7708
  const refused = tempoFloorRefusal(error, record);
7675
7709
  if (refused) return refused;
@@ -7679,7 +7713,9 @@ async function recoverTempoDrain(deps, mpp, record, channelId) {
7679
7713
  error: error instanceof Error ? error.message : String(error)
7680
7714
  });
7681
7715
  return jsonPaymentError("payment_invalid", 402, {
7682
- message: "The Tempo session close is still being reconciled; retry this drain."
7716
+ message: "The Tempo session close is still being reconciled; retry this drain.",
7717
+ hint: `Retry this same drain ${record.drainId} after the service can read and persist the close evidence.`,
7718
+ retryable: true
7683
7719
  });
7684
7720
  }
7685
7721
  }
@@ -9646,6 +9682,16 @@ import { createPublicClient as createPublicClient4, decodeEventLog, decodeFuncti
9646
9682
  var HOSTED_SETTLEMENT_UNRESOLVED_SQL = `state NOT IN ('delivered','no_report')
9647
9683
  OR (rail='x402' AND operation IN ('claim','refund')
9648
9684
  AND COALESCE(original->>'authorizationCallData','') !~* '^0x([0-9a-f]{2})+$')`;
9685
+ var TEMPO_CLOSE_RETRYABLE_SQL = `target.rail='tempo' AND target.operation='close'
9686
+ AND target.state IN ('unmatched','no_report') AND target.tx_hash IS NULL
9687
+ AND NOT EXISTS (
9688
+ SELECT 1 FROM sdk_hosted_settlement_report_obligations other
9689
+ WHERE other.dvm_id=target.dvm_id AND other.scope=target.scope
9690
+ AND other.rail='tempo' AND other.operation='close' AND other.channel_id=target.channel_id
9691
+ AND other.id<>target.id AND (other.state<>'no_report' OR other.tx_hash IS NOT NULL)
9692
+ )`;
9693
+ var TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL = `target.rail='tempo' AND target.operation='close'
9694
+ AND target.state='unmatched' AND target.tx_hash IS NULL`;
9649
9695
  var X402_SETTLED_EVENT = parseAbiItem2(
9650
9696
  "event Settled(address indexed receiver,address indexed token,address indexed sender,uint128 amount)"
9651
9697
  );
@@ -9860,7 +9906,7 @@ var HostedSettlementReportObligations = class {
9860
9906
  network TEXT NOT NULL, contract TEXT NOT NULL, channel_id TEXT NOT NULL,
9861
9907
  original JSONB NOT NULL,
9862
9908
  state TEXT NOT NULL CHECK (state IN ('reserved','observed','report_queued','delivered','no_report','exhausted','unmatched')),
9863
- close_search_before BIGINT, close_write_off_note TEXT,
9909
+ close_search_before BIGINT, close_write_off_note TEXT, close_written_off_at_ms BIGINT,
9864
9910
  tx_hash TEXT, block_number BIGINT, block_hash TEXT, delta_native NUMERIC(78,0),
9865
9911
  payout_id TEXT, report_id TEXT, report_payload JSONB, delivered_at_ms BIGINT,
9866
9912
  created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
@@ -9877,7 +9923,7 @@ var HostedSettlementReportObligations = class {
9877
9923
  `SELECT attname FROM pg_attribute
9878
9924
  WHERE attrelid='sdk_hosted_settlement_report_obligations'::regclass AND NOT attisdropped`
9879
9925
  );
9880
- for (const [name, type] of [["close_search_before", "BIGINT"], ["close_write_off_note", "TEXT"]]) {
9926
+ for (const [name, type] of [["close_search_before", "BIGINT"], ["close_write_off_note", "TEXT"], ["close_written_off_at_ms", "BIGINT"]]) {
9881
9927
  if (!columns.rows.some((column) => column.attname === name)) {
9882
9928
  await this.pool.query(`ALTER TABLE sdk_hosted_settlement_report_obligations ADD COLUMN ${name} ${type}`);
9883
9929
  }
@@ -10076,41 +10122,104 @@ var HostedSettlementReportObligations = class {
10076
10122
  );
10077
10123
  return result.rowCount === 1;
10078
10124
  }
10125
+ async listTempoCloseReports(args) {
10126
+ const result = await this.pool.query(
10127
+ `WITH candidates AS (
10128
+ SELECT target.*, (${TEMPO_CLOSE_RETRYABLE_SQL}) AS retryable,
10129
+ (${TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL}) AS write_off_allowed
10130
+ FROM sdk_hosted_settlement_report_obligations target
10131
+ WHERE target.dvm_id=$1 AND target.scope=$2 AND target.rail='tempo' AND target.operation='close'
10132
+ AND target.state IN ('unmatched','no_report')
10133
+ )
10134
+ SELECT * FROM candidates
10135
+ WHERE ($6::boolean OR (state='unmatched' AND (retryable OR write_off_allowed)))
10136
+ AND ($4::bigint IS NULL OR (created_at_ms,id)>($4,$5))
10137
+ ORDER BY created_at_ms,id LIMIT $3`,
10138
+ [
10139
+ this.dvmId,
10140
+ this.scope,
10141
+ args.limit,
10142
+ args.after?.createdAtMs ?? null,
10143
+ args.after?.obligationId ?? null,
10144
+ args.all ?? false
10145
+ ]
10146
+ );
10147
+ return result.rows.map((row) => ({
10148
+ id: row.id,
10149
+ channelId: row.channel_id,
10150
+ amountNative: tempoCloseAmount(row.original),
10151
+ status: row.state,
10152
+ createdAtMs: Number(row.created_at_ms),
10153
+ updatedAtMs: Number(row.updated_at_ms),
10154
+ deliveredAtMs: row.delivered_at_ms === null ? null : Number(row.delivered_at_ms),
10155
+ retryable: row.retryable,
10156
+ writeOffAllowed: row.write_off_allowed,
10157
+ writtenOff: row.close_write_off_note !== null,
10158
+ writeOffNote: row.close_write_off_note,
10159
+ writtenOffAtMs: row.close_written_off_at_ms === null ? null : Number(row.close_written_off_at_ms)
10160
+ }));
10161
+ }
10162
+ async hasTempoCloseReport(id) {
10163
+ const result = await this.pool.query(
10164
+ `SELECT id FROM sdk_hosted_settlement_report_obligations
10165
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'`,
10166
+ [id, this.dvmId, this.scope]
10167
+ );
10168
+ return result.rowCount === 1;
10169
+ }
10079
10170
  async writeOffTempoClose(id, note) {
10080
10171
  const result = await this.pool.query(
10081
- `UPDATE sdk_hosted_settlement_report_obligations SET state='no_report', close_write_off_note=$4,
10172
+ `UPDATE sdk_hosted_settlement_report_obligations target
10173
+ SET state='no_report', close_write_off_note=$4,
10174
+ close_written_off_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
10082
10175
  updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10083
- WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
10084
- AND state='unmatched' AND tx_hash IS NULL RETURNING channel_id,original`,
10176
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND ${TEMPO_CLOSE_WRITE_OFF_ALLOWED_SQL}
10177
+ RETURNING channel_id,original`,
10085
10178
  [id, this.dvmId, this.scope, note]
10086
10179
  );
10087
10180
  const row = result.rows[0];
10088
- return row && { channelId: row.channel_id, amount: (BigInt(String(row.original.highestVoucherAmount)) - BigInt(String(row.original.settledOnChain))).toString() };
10181
+ if (!row) return void 0;
10182
+ const written = { channelId: row.channel_id, amount: tempoCloseAmount(row.original) };
10183
+ loggers.tempo.warn("tempo_session_close_report_written_off", {
10184
+ channel_id: written.channelId,
10185
+ obligation_id: id,
10186
+ amount_forgone_native: written.amount
10187
+ });
10188
+ return written;
10089
10189
  }
10090
10190
  async reopenTempoClose(id) {
10091
- return transaction(this.pool, async (tx) => {
10191
+ const reopened = await transaction(this.pool, async (tx) => {
10092
10192
  const owned = await tx.query(
10093
10193
  "SELECT id FROM sdk_hosted_settlement_report_obligations WHERE id=$1 AND dvm_id=$2 AND scope=$3",
10094
10194
  [id, this.dvmId, this.scope]
10095
10195
  );
10096
- if (!owned.rowCount) return false;
10196
+ if (!owned.rowCount) return void 0;
10097
10197
  await this.lockIdentity(tx);
10198
+ const prior = await tx.query(
10199
+ `SELECT channel_id,state,close_write_off_note,original FROM sdk_hosted_settlement_report_obligations
10200
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 FOR UPDATE`,
10201
+ [id, this.dvmId, this.scope]
10202
+ );
10203
+ const row = prior.rows[0];
10204
+ if (!row) return void 0;
10098
10205
  const result = await tx.query(
10099
10206
  `UPDATE sdk_hosted_settlement_report_obligations target
10100
10207
  SET state='reserved', close_search_before=NULL,
10101
10208
  updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10102
- WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
10103
- AND state IN ('unmatched','no_report') AND tx_hash IS NULL
10104
- AND NOT EXISTS (
10105
- SELECT 1 FROM sdk_hosted_settlement_report_obligations other
10106
- WHERE other.dvm_id=target.dvm_id AND other.scope=target.scope
10107
- AND other.rail='tempo' AND other.operation='close' AND other.channel_id=target.channel_id
10108
- AND other.id<>target.id AND (other.state<>'no_report' OR other.tx_hash IS NOT NULL)
10109
- )`,
10209
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND ${TEMPO_CLOSE_RETRYABLE_SQL}`,
10110
10210
  [id, this.dvmId, this.scope]
10111
10211
  );
10112
- return result.rowCount === 1;
10212
+ return result.rowCount === 1 ? row : void 0;
10113
10213
  });
10214
+ if (!reopened) return false;
10215
+ loggers.tempo.info("tempo_session_close_report_reopened", {
10216
+ obligation_id: id,
10217
+ channel_id: reopened.channel_id,
10218
+ prior_status: reopened.state,
10219
+ written_off: reopened.close_write_off_note !== null,
10220
+ amount_native: tempoCloseAmount(reopened.original)
10221
+ });
10222
+ return true;
10114
10223
  }
10115
10224
  async reserveTempoIn(tx, channelId, operation, drainId) {
10116
10225
  const config = this.tempo;
@@ -10185,6 +10294,17 @@ var HostedSettlementReportObligations = class {
10185
10294
  searchBefore: searchCursor(row.close_search_before)
10186
10295
  };
10187
10296
  }
10297
+ /** A recorded close transaction remains refund evidence after its report is delivered. */
10298
+ async tempoCloseTransaction(channelId, drainId) {
10299
+ const result = await this.pool.query(
10300
+ `SELECT tx_hash FROM sdk_hosted_settlement_report_obligations
10301
+ WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
10302
+ AND original->>'drainId'=$4 AND tx_hash IS NOT NULL
10303
+ ORDER BY created_at_ms DESC LIMIT 1`,
10304
+ [this.dvmId, this.scope, channelId.toLowerCase(), drainId]
10305
+ );
10306
+ return result.rows[0]?.tx_hash;
10307
+ }
10188
10308
  /**
10189
10309
  * Where the next search for this close obligation's transaction resumes. It
10190
10310
  * lives on the obligation, not the drain's close intent, because releasing
@@ -10836,12 +10956,10 @@ async function writeOffHostedTempoCloseReport(args) {
10836
10956
  if (!note || note.length > 2e3) throw new Error("A write-off requires a note of 1 to 2000 characters");
10837
10957
  const row = await new HostedSettlementReportObligations(args.db, args.dvmId, args.scope).writeOffTempoClose(args.obligationId, note);
10838
10958
  if (!row) return false;
10839
- loggers.tempo.warn("tempo_session_close_report_written_off", {
10840
- channel_id: row.channelId,
10841
- obligation_id: args.obligationId,
10842
- amount_forgone_native: row.amount
10843
- });
10844
- return true;
10959
+ return args.includeDetails ? { channelId: row.channelId, amountForgoneNative: row.amount } : true;
10960
+ }
10961
+ function tempoCloseAmount(original) {
10962
+ return (BigInt(String(original.highestVoucherAmount)) - BigInt(String(original.settledOnChain))).toString();
10845
10963
  }
10846
10964
  function searchCursor(value) {
10847
10965
  return value === null ? null : BigInt(value);
@@ -12174,7 +12292,7 @@ function jobCredentialGate(opts) {
12174
12292
  // to a human verbatim, so the conditional advice lives in `hint`, which
12175
12293
  // is what the agent acts on.
12176
12294
  display: "Couldn't retrieve your result \u2014 the request was missing its job credential.",
12177
- hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@0.2.0, then retry the same command with --endpoint <url> --job-token <hex> (or set DVM_JOB_ENDPOINT and DVM_JOB_TOKEN).`
12295
+ hint: `Send the job_token from the POST /v1/job response as the X-Job-Token header${alternative}. The token is minted once, at submit, and the DVM cannot reissue it \u2014 a job submitted from another machine, or by a caller that never stored it, can't be read here. If your caller never sends X-Job-Token at all, upgrade: npm i -g @dvmkit/dvm-cli@latest, then retry the same command with --endpoint <url> --job-token <hex> (or set DVM_JOB_ENDPOINT and DVM_JOB_TOKEN).`
12178
12296
  };
12179
12297
  }
12180
12298
  };
@@ -12607,22 +12725,17 @@ function buildContext(job, opts) {
12607
12725
  };
12608
12726
  const doResolve = async (pc) => {
12609
12727
  if (resolved) return;
12728
+ if (job.pendingPayment?.resolve === doResolve) {
12729
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
12730
+ const persist = opts.onPaymentResume ?? opts.onYield;
12731
+ if (job.status === "awaiting-input" && persist) await persist();
12732
+ if (resolved) return;
12733
+ job.pendingPayment = null;
12734
+ }
12610
12735
  resolved = true;
12611
12736
  cleanup();
12612
12737
  creditedToHandlerMsats += amountMsats;
12613
12738
  creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
12614
- if (job.pendingPayment?.resolve === doResolve) {
12615
- job.pendingPayment = null;
12616
- job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
12617
- if (job.status === "awaiting-input" && opts.onYield) {
12618
- try {
12619
- await opts.onYield();
12620
- } catch (err) {
12621
- reject(err);
12622
- return;
12623
- }
12624
- }
12625
- }
12626
12739
  resolve(pc);
12627
12740
  };
12628
12741
  const doReject = (err) => {
@@ -13148,6 +13261,9 @@ var JobManager = class {
13148
13261
  // A prompt response and a payment NOTIFY must not mistake each other for a
13149
13262
  // different machine's successful claim.
13150
13263
  inputResumeTails = /* @__PURE__ */ new WeakMap();
13264
+ // A failed partial-wait snapshot leaves our durable claim in processing.
13265
+ // Retain its continuation so a later input can finish it without a new claim.
13266
+ claimedPaymentResumes = /* @__PURE__ */ new WeakMap();
13151
13267
  sessionId = randomBytes4(3).toString("hex");
13152
13268
  nextJobId = 1;
13153
13269
  accumulatorMonitor;
@@ -13554,7 +13670,10 @@ var JobManager = class {
13554
13670
  }
13555
13671
  async queueInputResume(job, resume) {
13556
13672
  const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
13557
- const next = previous.catch(() => void 0).then(resume);
13673
+ const next = previous.catch(() => void 0).then(async () => {
13674
+ await this.claimedPaymentResumes.get(job)?.();
13675
+ return resume();
13676
+ });
13558
13677
  this.inputResumeTails.set(job, next);
13559
13678
  try {
13560
13679
  return await next;
@@ -13566,6 +13685,19 @@ var JobManager = class {
13566
13685
  await job.messageAppenderTail;
13567
13686
  return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
13568
13687
  }
13688
+ async resumeClaimedPayment(job, resume) {
13689
+ const retry = async () => {
13690
+ try {
13691
+ if (!isTerminal(job.status) && !await this.adoptDurableTerminal(job)) await resume();
13692
+ this.claimedPaymentResumes.delete(job);
13693
+ } catch (error) {
13694
+ if (!(error instanceof JobSnapshotPersistenceError)) this.claimedPaymentResumes.delete(job);
13695
+ throw error;
13696
+ }
13697
+ };
13698
+ this.claimedPaymentResumes.set(job, retry);
13699
+ await retry();
13700
+ }
13569
13701
  async dispatchMessageNow(job, type, content) {
13570
13702
  const ctx = job.sdkCtx;
13571
13703
  const cap = this.getCapability(job.capability);
@@ -13594,10 +13726,13 @@ var JobManager = class {
13594
13726
  return;
13595
13727
  }
13596
13728
  if (job.pendingPayment !== pp || isTerminal(job.status)) return;
13597
- job.pendingPayment = null;
13598
- job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
13599
- if (job.status === "awaiting-input") await this.persistJob(job);
13600
- await pp.resolve(pc);
13729
+ await this.resumeClaimedPayment(job, async () => {
13730
+ job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
13731
+ if (job.status === "awaiting-input") await retryJobSnapshot(() => this.persistJob(job));
13732
+ if (job.pendingPayment !== pp || isTerminal(job.status)) return;
13733
+ job.pendingPayment = null;
13734
+ await pp.resolve(pc);
13735
+ });
13601
13736
  } else if (cap?.onPayment && ctx) {
13602
13737
  await cap.onPayment(ctx, pc);
13603
13738
  }
@@ -13650,10 +13785,14 @@ var JobManager = class {
13650
13785
  record.seq = Math.max(record.seq, highSeq);
13651
13786
  }
13652
13787
  }
13653
- if (this.opts.dvmId && isTerminalCostRecoveryStore(this.jobStore)) {
13654
- await this.jobStore.saveForDvm(record, this.opts.dvmId);
13655
- } else {
13656
- await this.jobStore.save(record);
13788
+ try {
13789
+ if (this.opts.dvmId && isTerminalCostRecoveryStore(this.jobStore)) {
13790
+ await this.jobStore.saveForDvm(record, this.opts.dvmId);
13791
+ } else {
13792
+ await this.jobStore.save(record);
13793
+ }
13794
+ } catch (error) {
13795
+ throw new JobSnapshotPersistenceError(error);
13657
13796
  }
13658
13797
  const saved = await this.jobStore.get(job.id);
13659
13798
  if (saved) {
@@ -15193,6 +15332,7 @@ var JobManager = class {
15193
15332
  fxFetcher: this.opts.fxFetcher,
15194
15333
  onTerminal: () => this.handleJobTerminal(job),
15195
15334
  onYield: () => this.persistJob(job),
15335
+ onPaymentResume: () => retryJobSnapshot(() => this.persistJob(job)),
15196
15336
  subscribeNotifications: isStreamableJobStore(jobStore) ? (cb) => jobStore.subscribeNotifications(job.id, cb) : void 0,
15197
15337
  getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
15198
15338
  drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
@@ -15203,7 +15343,9 @@ var JobManager = class {
15203
15343
  if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
15204
15344
  if (!await this.claimLocalInput(job)) return false;
15205
15345
  if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
15206
- await resume?.();
15346
+ await this.resumeClaimedPayment(job, async () => {
15347
+ await resume?.();
15348
+ });
15207
15349
  return true;
15208
15350
  });
15209
15351
  } : void 0,
@@ -15964,6 +16106,23 @@ function resolvePricingMax(raw) {
15964
16106
  if (raw === void 0) return void 0;
15965
16107
  return { amount: roundUsd(parseUsdPrice(raw, "price")), currency: "usd" };
15966
16108
  }
16109
+ var JobSnapshotPersistenceError = class extends Error {
16110
+ constructor(cause) {
16111
+ super("Job snapshot could not be persisted", { cause });
16112
+ this.name = "JobSnapshotPersistenceError";
16113
+ }
16114
+ };
16115
+ async function retryJobSnapshot(persist) {
16116
+ for (let attempt = 0; ; attempt++) {
16117
+ try {
16118
+ await persist();
16119
+ return;
16120
+ } catch (error) {
16121
+ if (!(error instanceof JobSnapshotPersistenceError) || attempt >= 2) throw error;
16122
+ await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
16123
+ }
16124
+ }
16125
+ }
15967
16126
 
15968
16127
  // src/sdk/server/mint-health-tracker.ts
15969
16128
  var DEFAULT_TICK_INTERVAL_MS2 = 5 * 60 * 1e3;
@@ -17033,6 +17192,9 @@ function installAdminCreditRoutes(app, opts) {
17033
17192
  "/admin/credit/reconcile-x402-exact-settlement",
17034
17193
  (c) => handleReconcileX402Exact(c, opts)
17035
17194
  );
17195
+ app.get("/admin/credit/tempo-close-reports", (c) => handleTempoCloseReports(c, opts));
17196
+ app.post("/admin/credit/retry-tempo-close-report", (c) => handleTempoCloseReportAction(c, opts, "retry"));
17197
+ app.post("/admin/credit/write-off-tempo-close-report", (c) => handleTempoCloseReportAction(c, opts, "write-off"));
17036
17198
  app.get("/admin/credit/tempo-drains", (c) => handleTempoDrains(c, opts));
17037
17199
  app.get("/admin/credit/tempo-losses", (c) => handleTempoLosses(c, opts));
17038
17200
  app.post("/admin/credit/reconcile-tempo-drain", (c) => handleReconcileTempo(c, opts));
@@ -18008,6 +18170,101 @@ async function handleWriteOffX402(c, opts) {
18008
18170
  replayed: !outcome.written
18009
18171
  });
18010
18172
  }
18173
+ function tempoCloseReportsUnavailable(c) {
18174
+ return c.json({
18175
+ error: "tempo_close_report_unavailable",
18176
+ message: "Hosted settlement reports are not configured on this DVM."
18177
+ }, 503);
18178
+ }
18179
+ async function handleTempoCloseReports(c, opts) {
18180
+ const auth = await requireAuth(c, opts);
18181
+ if ("response" in auth) return auth.response;
18182
+ if (!opts.hostedSettlementReports) return tempoCloseReportsUnavailable(c);
18183
+ const limit = parseTempoLimit(c.req.query("limit"));
18184
+ if (limit === void 0) {
18185
+ return c.json({ error: "invalid_query", message: "limit must be an integer in 1..50." }, 400);
18186
+ }
18187
+ let after;
18188
+ const rawAfter = c.req.query("after");
18189
+ if (rawAfter !== void 0) {
18190
+ try {
18191
+ const value = JSON.parse(Buffer.from(rawAfter, "base64url").toString("utf8"));
18192
+ if (!Array.isArray(value) || value.length !== 2 || !Number.isSafeInteger(value[0]) || value[0] < 0 || typeof value[1] !== "string" || !value[1]) throw new Error("invalid cursor");
18193
+ after = { createdAtMs: value[0], obligationId: value[1] };
18194
+ } catch {
18195
+ return c.json({ error: "invalid_query", message: "after must be a next_after value from a previous response." }, 400);
18196
+ }
18197
+ }
18198
+ const all = c.req.query("all");
18199
+ if (all !== void 0 && all !== "0" && all !== "1") {
18200
+ return c.json({ error: "invalid_query", message: "all must be 0 or 1." }, 400);
18201
+ }
18202
+ const rows2 = await opts.hostedSettlementReports.store.listTempoCloseReports({ limit, after, all: all === "1" });
18203
+ const last = rows2.length === limit ? rows2.at(-1) : void 0;
18204
+ const response = {
18205
+ reports: rows2.map((row) => ({
18206
+ obligation_id: row.id,
18207
+ channel_id: row.channelId,
18208
+ amount_native: row.amountNative,
18209
+ status: row.status,
18210
+ created_at_ms: row.createdAtMs,
18211
+ updated_at_ms: row.updatedAtMs,
18212
+ delivered_at_ms: row.deliveredAtMs,
18213
+ retryable: row.retryable,
18214
+ write_off_allowed: row.writeOffAllowed,
18215
+ written_off: row.writtenOff,
18216
+ write_off_note: row.writeOffNote,
18217
+ written_off_at_ms: row.writtenOffAtMs
18218
+ })),
18219
+ ...last ? { next_after: Buffer.from(JSON.stringify([last.createdAtMs, last.id])).toString("base64url") } : {}
18220
+ };
18221
+ return c.json(response);
18222
+ }
18223
+ async function handleTempoCloseReportAction(c, opts, action) {
18224
+ const auth = await requireAuth(c, opts);
18225
+ if ("response" in auth) return auth.response;
18226
+ const reports = opts.hostedSettlementReports;
18227
+ if (!reports) return tempoCloseReportsUnavailable(c);
18228
+ const body = await c.req.json().catch(() => null);
18229
+ if (!body || typeof body.obligation_id !== "string" || !body.obligation_id.trim()) {
18230
+ return c.json({ error: "invalid_request", message: "obligation_id must be a non-empty string." }, 400);
18231
+ }
18232
+ const obligationId = body.obligation_id.trim();
18233
+ if (action === "retry") {
18234
+ if (await reports.store.reopenTempoClose(obligationId)) {
18235
+ const response = { status: "reserved", obligation_id: obligationId };
18236
+ return c.json(response);
18237
+ }
18238
+ } else {
18239
+ if (typeof body.note !== "string" || !body.note.trim() || body.note.trim().length > 2e3) {
18240
+ return c.json({ error: "invalid_request", message: "note must contain 1 to 2000 characters after trimming." }, 400);
18241
+ }
18242
+ const note = body.note.trim();
18243
+ const result = await reports.store.writeOffTempoClose(obligationId, note);
18244
+ if (result) {
18245
+ const response = {
18246
+ status: "written_off",
18247
+ obligation_id: obligationId,
18248
+ channel_id: result.channelId,
18249
+ amount_forgone_native: result.amount,
18250
+ note
18251
+ };
18252
+ return c.json(response);
18253
+ }
18254
+ }
18255
+ if (!await reports.store.hasTempoCloseReport(obligationId)) {
18256
+ return c.json({
18257
+ error: "tempo_close_report_not_found",
18258
+ message: "Close-report obligation not found.",
18259
+ obligation_id: obligationId
18260
+ }, 404);
18261
+ }
18262
+ return c.json({
18263
+ error: "tempo_close_report_conflict",
18264
+ message: action === "retry" ? "Retry requires an unsubmitted unmatched or no_report close obligation without a competing report." : "Write-off requires an unsubmitted unmatched close obligation.",
18265
+ obligation_id: obligationId
18266
+ }, 409);
18267
+ }
18011
18268
  async function handleTempoDrains(c, opts) {
18012
18269
  const auth = await requireAuth(c, opts);
18013
18270
  if ("response" in auth) return auth.response;
@@ -19913,7 +20170,7 @@ async function createDVMServer(descriptor, opts) {
19913
20170
  }
19914
20171
  }
19915
20172
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
19916
- const { RevenueReporter } = await import("./revenue-reporter-4EAVPWXS.js");
20173
+ const { RevenueReporter } = await import("./revenue-reporter-EV3K5L6B.js");
19917
20174
  const reporter = new RevenueReporter(
19918
20175
  opts.db,
19919
20176
  platformUrl,
@@ -19923,7 +20180,7 @@ async function createDVMServer(descriptor, opts) {
19923
20180
  revenueReporter = reporter;
19924
20181
  await reporter.init();
19925
20182
  reporter.startRetryLoop();
19926
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-TZX2S7KG.js");
20183
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-HVJ6WUF3.js");
19927
20184
  payoutReporter = new PayoutReporter({
19928
20185
  dvmId,
19929
20186
  db: opts.db,
@@ -20590,6 +20847,9 @@ var DVMServer = class {
20590
20847
  // DVM wired with an in-memory session store resolves to a rail with no
20591
20848
  // close-intent read and the verbs answer 503.
20592
20849
  tempoDrainRail: () => Promise.resolve(this.opts.mpp),
20850
+ ...this.opts.hostedSettlementReports ? {
20851
+ hostedSettlementReports: { store: this.opts.hostedSettlementReports }
20852
+ } : {},
20593
20853
  // Health-filtered, and read per request rather than captured: the
20594
20854
  // servicer picking a mint to buy a refund at wants the list that
20595
20855
  // excludes a mint currently failing its probe.
@@ -22708,8 +22968,14 @@ var DVMServer = class {
22708
22968
  await jm.markInboundVerified(activeJob.id, inboundSeq2);
22709
22969
  }
22710
22970
  jm.startIdleTimer(activeJob);
22711
- await jm.dispatchMessage(activeJob, body2.type, body2.content);
22712
- await jm.persistJob(activeJob);
22971
+ try {
22972
+ await jm.dispatchMessage(activeJob, body2.type, body2.content);
22973
+ await this.persistMessageSnapshot(jm, activeJob, body2.type);
22974
+ } catch (error) {
22975
+ const response = this.postPaymentSnapshotFailure(c, error, activeJob, body2.type, inboundSeq2, unappliedCredit2);
22976
+ if (response) return response;
22977
+ throw error;
22978
+ }
22713
22979
  const lastMsg2 = activeJob.messages[activeJob.messages.length - 1];
22714
22980
  return c.json({ seq: lastMsg2.seq, ...unappliedCredit2 && { credit: unappliedCredit2 } }, 201);
22715
22981
  }
@@ -22792,20 +23058,48 @@ var DVMServer = class {
22792
23058
  return c.json({ seq: lastMsg2.seq, ...creditBlock }, 201);
22793
23059
  }
22794
23060
  jm.startIdleTimer(job);
22795
- if (isStreamableJobStore(jm.store)) {
22796
- const store = jm.store;
22797
- const lockResult = await store.tryReactivationLock(job.id, async () => {
23061
+ try {
23062
+ if (isStreamableJobStore(jm.store)) {
23063
+ const store = jm.store;
23064
+ const lockResult = await store.tryReactivationLock(job.id, async () => {
23065
+ await this.runReactivation(jm, job, body, record.status);
23066
+ });
23067
+ if (!lockResult.acquired) {
23068
+ this.teardownReactivationScaffold(jm, job);
23069
+ }
23070
+ } else {
22798
23071
  await this.runReactivation(jm, job, body, record.status);
22799
- });
22800
- if (!lockResult.acquired) {
22801
- this.teardownReactivationScaffold(jm, job);
22802
23072
  }
22803
- } else {
22804
- await this.runReactivation(jm, job, body, record.status);
23073
+ } catch (error) {
23074
+ const response = this.postPaymentSnapshotFailure(c, error, job, body.type, inboundSeq, unappliedCredit);
23075
+ if (response) return response;
23076
+ throw error;
22805
23077
  }
22806
23078
  const lastMsg = job.messages[job.messages.length - 1];
22807
23079
  return c.json({ seq: lastMsg.seq, ...creditBlock }, 201);
22808
23080
  }
23081
+ async persistMessageSnapshot(jm, job, type) {
23082
+ if (type === "payment") {
23083
+ await retryJobSnapshot(() => jm.persistJob(job));
23084
+ } else {
23085
+ await jm.persistJob(job);
23086
+ }
23087
+ }
23088
+ postPaymentSnapshotFailure(c, error, job, type, inboundSeq, unappliedCredit) {
23089
+ if (!(error instanceof JobSnapshotPersistenceError) || type !== "payment" || inboundSeq === null || !isStreamableJobStore(this.jobManager.store)) return void 0;
23090
+ diagnosticConsole.error({ level: "job_snapshot_unavailable", job_id: job.id, seq: inboundSeq });
23091
+ return c.json({
23092
+ error: "job_snapshot_unavailable",
23093
+ message: "Payment was recorded but the job snapshot could not be persisted.",
23094
+ display: "Your payment was recorded. The service could not save the latest job progress.",
23095
+ hint: "Do not send another payment. Read the job status and messages; if the worker stopped, resume with a non-payment message.",
23096
+ retryable: false,
23097
+ payment_status: "recorded",
23098
+ job_id: job.id,
23099
+ seq: inboundSeq,
23100
+ ...unappliedCredit && { credit: unappliedCredit }
23101
+ }, 503);
23102
+ }
22809
23103
  /**
22810
23104
  * Run the handler or dispatch the inbound after Tx B + Tx C have already
22811
23105
  * committed the credit. Status decides the behaviour: `awaiting-input`
@@ -22831,7 +23125,7 @@ var DVMServer = class {
22831
23125
  });
22832
23126
  } else {
22833
23127
  await jm.dispatchMessage(job, body.type, body.content);
22834
- await jm.persistJob(job);
23128
+ await this.persistMessageSnapshot(jm, job, body.type);
22835
23129
  }
22836
23130
  }
22837
23131
  /**