@dvmkit/sdk 0.8.0 → 0.8.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-5CQKSLAU.js → chunk-2U6LAASN.js} +1 -1
  2. package/dist/{chunk-6FEHRZO7.js → chunk-7PM4R4GD.js} +1 -1
  3. package/dist/{chunk-EBCLWOKF.js → chunk-AGE5XZKE.js} +1 -1
  4. package/dist/{chunk-2I3OHMTY.js → chunk-ALWVJ2RB.js} +1 -1
  5. package/dist/{chunk-ATXDKX6F.js → chunk-AVV5SYGO.js} +1 -1
  6. package/dist/{chunk-DZQYQN2X.js → chunk-BFGQG4WY.js} +7 -0
  7. package/dist/{chunk-VYP6ADCS.js → chunk-FM6QAGJC.js} +2 -2
  8. package/dist/{chunk-QPXI7YPH.js → chunk-G3MPHW5I.js} +1 -1
  9. package/dist/{chunk-I6PHA54S.js → chunk-IHQUBATH.js} +1 -1
  10. package/dist/{chunk-5K5E6ME6.js → chunk-ILBSZMJL.js} +1 -1
  11. package/dist/{chunk-BL6LRWXD.js → chunk-IOQIXRGM.js} +1 -1
  12. package/dist/{chunk-X3JEME6P.js → chunk-J5J63WDC.js} +1 -1
  13. package/dist/{chunk-GSPV3UIM.js → chunk-JIZ4ZIXX.js} +254 -72
  14. package/dist/{chunk-TFYY5APM.js → chunk-NA2KOR6G.js} +1 -1
  15. package/dist/{chunk-PRSKC7AD.js → chunk-NU5RWJ5B.js} +1 -1
  16. package/dist/{chunk-IFWUWHTW.js → chunk-OP7VCGQR.js} +1 -1
  17. package/dist/{chunk-GB5XPAA5.js → chunk-QKE5NDPF.js} +213 -38
  18. package/dist/{chunk-55NKZQOC.js → chunk-RSNC5T6C.js} +1 -1
  19. package/dist/{chunk-XXLUIXPO.js → chunk-UDJNZ3UG.js} +1 -1
  20. package/dist/{chunk-YPSJU3OH.js → chunk-UVS7MRJX.js} +2 -2
  21. package/dist/{chunk-P5WEZXJ2.js → chunk-UWGBI5WK.js} +58 -1
  22. package/dist/{chunk-RTFBVXCN.js → chunk-ZEPJF4JC.js} +1 -1
  23. package/dist/{content-retention-D2K4MKNO.d.ts → content-retention-B7ddtJ2y.d.ts} +1 -1
  24. package/dist/{credit-ledger-AOU6S34N.js → credit-ledger-KIQIE6I6.js} +3 -3
  25. package/dist/{credit-menu-C-8g2cHT.d.ts → credit-menu-BH3g9GQa.d.ts} +4 -4
  26. package/dist/{fx-Ql4nvdbh.d.ts → fx-DN26_J7o.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 +6 -6
  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-DCvJQUX0.d.ts → job-store-Cq21JT37.d.ts} +1 -1
  34. package/dist/{lightning-backend-CY5ML3km.d.ts → lightning-backend-S8F8nf8q.d.ts} +1 -1
  35. package/dist/{memory-credit-ledger-4HWYINKJ.js → memory-credit-ledger-ATYFAYBG.js} +4 -4
  36. package/dist/{mpp-setup-2BKTPEK6.js → mpp-setup-ZTORW3VF.js} +3 -3
  37. package/dist/{payout-reporter-HQSIIBFL.js → payout-reporter-NN6K2IVQ.js} +2 -2
  38. package/dist/{postgres-consumed-credential-store-R7DNU6CM.js → postgres-consumed-credential-store-XJTE5PCA.js} +1 -1
  39. package/dist/{postgres-job-store-6VGFL6XE.js → postgres-job-store-AR6ZJUQV.js} +2 -2
  40. package/dist/{postgres-replay-store-SY26NUCI.js → postgres-replay-store-33K33QV7.js} +2 -2
  41. package/dist/{pricing-H2RA4X2R.js → pricing-G5IN5YJE.js} +3 -3
  42. package/dist/{revenue-reporter-X52GPZ5U.js → revenue-reporter-KDUZLD5F.js} +2 -2
  43. package/dist/server/index.d.ts +10 -10
  44. package/dist/server/index.js +27 -27
  45. package/dist/{step-cache-BOWAn_BH.d.ts → step-cache-CR12EZ1Q.d.ts} +108 -4
  46. package/dist/{tempo-charge-store-NKXJ2AXE.js → tempo-charge-store-XEGJYM3G.js} +2 -2
  47. package/dist/{tempo-session-store-ZSDQIWSU.js → tempo-session-store-3WTDOO7H.js} +3 -3
  48. package/dist/testing/index.d.ts +3 -3
  49. package/dist/testing/index.js +6 -6
  50. package/dist/{usd-MAtMp3DB.d.ts → usd-s_VZ_eS7.d.ts} +1 -1
  51. package/dist/{x402-Q4S36NHQ.js → x402-HQ5GRQZU.js} +3 -3
  52. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-BFGQG4WY.js";
4
4
 
5
5
  // src/sdk/pricing/currency.ts
6
6
  var InvalidCurrencyError = class extends Error {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-BFGQG4WY.js";
4
4
 
5
5
  // src/lib/pg-retry.ts
6
6
  import { AsyncLocalStorage } from "async_hooks";
@@ -15,7 +15,7 @@ import {
15
15
  lotOwedSats,
16
16
  netOwedSats,
17
17
  x402SettlementPending
18
- } from "./chunk-VYP6ADCS.js";
18
+ } from "./chunk-FM6QAGJC.js";
19
19
 
20
20
  // src/sdk/server/memory-credit-ledger.ts
21
21
  import { randomUUID } from "crypto";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-BFGQG4WY.js";
4
4
  import {
5
5
  withSdkInitLock
6
6
  } from "./chunk-COYKJRMM.js";
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-66HGCPBU.js";
7
7
  import {
8
8
  diagnosticConsole
9
- } from "./chunk-DZQYQN2X.js";
9
+ } from "./chunk-BFGQG4WY.js";
10
10
 
11
11
  // src/observability/payment-diagnostics.ts
12
12
  import { AsyncLocalStorage } from "async_hooks";
@@ -416,11 +416,18 @@ var diagnostic_catalogue_v1_default = {
416
416
  "tempo_routine_sweep",
417
417
  "tempo_routine_sweep_failed",
418
418
  "tempo_session_advertised_refresh_failed",
419
+ "tempo_session_close_attempts_exhausted",
419
420
  "tempo_session_close_challenge_failed",
421
+ "tempo_session_close_exit_resolved",
420
422
  "tempo_session_close_failed",
421
423
  "tempo_session_close_failure_reconcile_failed",
424
+ "tempo_session_close_reconcile_failed",
422
425
  "tempo_session_close_recovery_failed",
426
+ "tempo_session_close_recovery_search_paused",
423
427
  "tempo_session_close_recovery_unmatched",
428
+ "tempo_session_close_reservation_released",
429
+ "tempo_session_close_settlement_callback_failed",
430
+ "tempo_session_close_settlement_unreported",
424
431
  "tempo_session_open_refused",
425
432
  "tempo_session_settled",
426
433
  "tempo_session_withheld",
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  applyRetryToPool
3
- } from "./chunk-6FEHRZO7.js";
3
+ } from "./chunk-7PM4R4GD.js";
4
4
  import {
5
5
  x402NetworkByCaip2
6
6
  } from "./chunk-BPYMEMQX.js";
7
7
  import {
8
8
  diagnosticConsole
9
- } from "./chunk-DZQYQN2X.js";
9
+ } from "./chunk-BFGQG4WY.js";
10
10
  import {
11
11
  withSdkInitLock
12
12
  } from "./chunk-COYKJRMM.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-AFV77S4C.js";
5
5
  import {
6
6
  diagnosticConsole
7
- } from "./chunk-DZQYQN2X.js";
7
+ } from "./chunk-BFGQG4WY.js";
8
8
  import {
9
9
  withSdkInitLock
10
10
  } from "./chunk-COYKJRMM.js";
@@ -5,7 +5,7 @@ import {
5
5
  } from "./chunk-AFV77S4C.js";
6
6
  import {
7
7
  loggers
8
- } from "./chunk-BL6LRWXD.js";
8
+ } from "./chunk-IOQIXRGM.js";
9
9
  import {
10
10
  redactUrl
11
11
  } from "./chunk-FUJ36YDV.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-UKD3R2KU.js";
8
8
  import {
9
9
  diagnosticConsole
10
- } from "./chunk-DZQYQN2X.js";
10
+ } from "./chunk-BFGQG4WY.js";
11
11
 
12
12
  // src/sdk/server/zod-error.ts
13
13
  function humanizeZodError(err, opts) {
@@ -7,7 +7,7 @@ import {
7
7
  extendDiagnosticIdentityAuthority,
8
8
  projectDiagnosticFields,
9
9
  projectSpanRow
10
- } from "./chunk-DZQYQN2X.js";
10
+ } from "./chunk-BFGQG4WY.js";
11
11
 
12
12
  // src/observability/context.ts
13
13
  import { AsyncLocalStorage } from "async_hooks";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  diagnosticConsole
3
- } from "./chunk-DZQYQN2X.js";
3
+ } from "./chunk-BFGQG4WY.js";
4
4
  import {
5
5
  withSdkInitLock
6
6
  } from "./chunk-COYKJRMM.js";
@@ -7,10 +7,10 @@ import {
7
7
  installHostedTempoCloseGuard,
8
8
  isTempoInsufficientFundsError,
9
9
  resolveTempoNetwork
10
- } from "./chunk-GB5XPAA5.js";
10
+ } from "./chunk-QKE5NDPF.js";
11
11
  import {
12
12
  x402EarnedCeiling
13
- } from "./chunk-X3JEME6P.js";
13
+ } from "./chunk-J5J63WDC.js";
14
14
  import {
15
15
  createX402FacilitatorAuthHeaders,
16
16
  createX402FacilitatorClient,
@@ -19,13 +19,13 @@ import {
19
19
  verifyWithFacilitator,
20
20
  withPaymentDiagnostics,
21
21
  withPaymentStage
22
- } from "./chunk-ATXDKX6F.js";
22
+ } from "./chunk-AVV5SYGO.js";
23
23
  import {
24
24
  MemoryCreditLedger
25
- } from "./chunk-EBCLWOKF.js";
25
+ } from "./chunk-AGE5XZKE.js";
26
26
  import {
27
27
  PostgresJobStore
28
- } from "./chunk-QPXI7YPH.js";
28
+ } from "./chunk-G3MPHW5I.js";
29
29
  import {
30
30
  parseStoreValue
31
31
  } from "./chunk-YG7G4DPZ.js";
@@ -36,7 +36,7 @@ import {
36
36
  humanizeZodIssues,
37
37
  signedRequestInput,
38
38
  zodIssuesOf
39
- } from "./chunk-5K5E6ME6.js";
39
+ } from "./chunk-ILBSZMJL.js";
40
40
  import {
41
41
  MemoryJobStore,
42
42
  UNCAPPABLE_ASK_TOTAL,
@@ -45,7 +45,7 @@ import {
45
45
  isJobRetentionStore,
46
46
  isReceiptIssuingStore,
47
47
  isStreamableJobStore
48
- } from "./chunk-I6PHA54S.js";
48
+ } from "./chunk-IHQUBATH.js";
49
49
  import {
50
50
  ContentHoldError,
51
51
  JobCancelledError,
@@ -85,7 +85,7 @@ import {
85
85
  signReceipt,
86
86
  toLockPubkey,
87
87
  verifyChallenge
88
- } from "./chunk-RTFBVXCN.js";
88
+ } from "./chunk-ZEPJF4JC.js";
89
89
  import {
90
90
  X402_BATCH_SETTLEMENT_SCHEME,
91
91
  X402_DEFAULT_FACILITATOR,
@@ -113,13 +113,13 @@ import {
113
113
  } from "./chunk-UKD3R2KU.js";
114
114
  import {
115
115
  createConsoleLogger
116
- } from "./chunk-IFWUWHTW.js";
116
+ } from "./chunk-OP7VCGQR.js";
117
117
  import {
118
118
  loggers,
119
119
  pinDimension,
120
120
  withSpan,
121
121
  withTraceContext
122
- } from "./chunk-BL6LRWXD.js";
122
+ } from "./chunk-IOQIXRGM.js";
123
123
  import {
124
124
  StepCache
125
125
  } from "./chunk-BQJIFRAZ.js";
@@ -136,7 +136,7 @@ import {
136
136
  roundUsd,
137
137
  satsToFiat,
138
138
  validateCurrency
139
- } from "./chunk-5CQKSLAU.js";
139
+ } from "./chunk-2U6LAASN.js";
140
140
  import {
141
141
  CreditLedger,
142
142
  CreditLedgerError,
@@ -152,10 +152,10 @@ import {
152
152
  isNonChannelBitcoinRail,
153
153
  netOwedSats,
154
154
  x402SettlementPending
155
- } from "./chunk-VYP6ADCS.js";
155
+ } from "./chunk-FM6QAGJC.js";
156
156
  import {
157
157
  withPostgresRecoveryContext
158
- } from "./chunk-6FEHRZO7.js";
158
+ } from "./chunk-7PM4R4GD.js";
159
159
  import {
160
160
  DvmError,
161
161
  x402NetworkByCaip2
@@ -163,7 +163,7 @@ import {
163
163
  import {
164
164
  createDiagnosticCapabilityAuthority,
165
165
  diagnosticConsole
166
- } from "./chunk-DZQYQN2X.js";
166
+ } from "./chunk-BFGQG4WY.js";
167
167
  import {
168
168
  MemoryProcessedPaymentStore,
169
169
  PostgresProcessedPaymentStore,
@@ -3883,7 +3883,7 @@ async function verifyUpfrontPaymentInScope(opts) {
3883
3883
  x402Requirements
3884
3884
  });
3885
3885
  }
3886
- const { verifyX402Payment } = await import("./x402-Q4S36NHQ.js");
3886
+ const { verifyX402Payment } = await import("./x402-HQ5GRQZU.js");
3887
3887
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
3888
3888
  x402Payment,
3889
3889
  x402Config,
@@ -4896,7 +4896,7 @@ async function verifyIncomingPaymentInScope(body, opts, snapshot) {
4896
4896
  snapshot
4897
4897
  });
4898
4898
  }
4899
- const { verifyX402Payment } = await import("./x402-Q4S36NHQ.js");
4899
+ const { verifyX402Payment } = await import("./x402-HQ5GRQZU.js");
4900
4900
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
4901
4901
  body.content.x402_payment,
4902
4902
  opts.x402Config,
@@ -5327,7 +5327,7 @@ async function processIncomingPaymentInScope(job, body, opts) {
5327
5327
  try {
5328
5328
  const requiredMsats = job.pendingPaymentMsats ?? 0;
5329
5329
  const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
5330
- const { verifyX402Payment } = await import("./x402-Q4S36NHQ.js");
5330
+ const { verifyX402Payment } = await import("./x402-HQ5GRQZU.js");
5331
5331
  const receipt = await withPaymentStage("verifier_broadcast", () => verifyX402Payment(
5332
5332
  body.content.x402_payment,
5333
5333
  opts.x402Config,
@@ -7487,17 +7487,16 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7487
7487
  await beginClose(expectedChannelId, record.drainId);
7488
7488
  recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
7489
7489
  if (deps.hostedSettlementReports) {
7490
- const reserved = await deps.hostedSettlementReports.reserveTempo(
7491
- expectedChannelId,
7492
- "close",
7493
- record.drainId
7490
+ hostedObligationId = await reserveHostedTempoClose(
7491
+ deps.hostedSettlementReports,
7492
+ rail.mpp,
7493
+ record,
7494
+ expectedChannelId
7494
7495
  );
7495
- if (reserved.replayed) throw new Error("Hosted Tempo close already has an original effect attempt");
7496
- hostedObligationId = reserved.id;
7497
7496
  if (!rail.mpp?.permitHostedTempoClose) {
7498
7497
  throw new Error("Hosted Tempo close credential guard is unavailable");
7499
7498
  }
7500
- rail.mpp.permitHostedTempoClose(rail.mppCredential, reserved.id);
7499
+ rail.mpp.permitHostedTempoClose(rail.mppCredential, hostedObligationId);
7501
7500
  }
7502
7501
  }
7503
7502
  });
@@ -7522,6 +7521,14 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7522
7521
  }
7523
7522
  const wrongChannel = tempoWrongChannelRefusal(error, record);
7524
7523
  if (wrongChannel) return wrongChannel;
7524
+ if (error instanceof HostedTempoCloseAttemptsExhaustedError) {
7525
+ return jsonPaymentError("tempo_channel_exit_required", 409, {
7526
+ message: `Tempo drain ${record.drainId} reached its limit of cooperative close attempts that never reached the chain.`,
7527
+ display: "The service could not complete a cooperative close of this stablecoin channel after several attempts. Your funds stay in the channel; use the timed on-chain exit to recover them.",
7528
+ hint: "Start the channel's timed on-chain exit. Retrying this drain with another close credential is refused.",
7529
+ extra: { drain_registered: true, drain_id: record.drainId, channel_id: expectedChannelId }
7530
+ });
7531
+ }
7525
7532
  const refused = tempoFloorRefusal(error, record);
7526
7533
  if (refused) return refused;
7527
7534
  if (tempoCloseCredentialNeedsRefresh(error)) {
@@ -7568,6 +7575,49 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
7568
7575
  });
7569
7576
  }
7570
7577
  }
7578
+ var HOSTED_TEMPO_CLOSE_STALE_MS = 3e5;
7579
+ var HOSTED_TEMPO_CLOSE_MAX_RELEASES = 3;
7580
+ var HOSTED_TEMPO_CLOSE_IN_FLIGHT = "Hosted Tempo close already has an original effect attempt";
7581
+ var HostedTempoCloseAttemptsExhaustedError = class extends Error {
7582
+ };
7583
+ async function reserveHostedTempoClose(reports, mpp, record, channelId) {
7584
+ const pending = await reports.pendingTempoClose(channelId, record.drainId);
7585
+ if (!pending) {
7586
+ const reserved = await reports.reserveTempo(channelId, "close", record.drainId);
7587
+ if (reserved.replayed) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
7588
+ return reserved.id;
7589
+ }
7590
+ if (pending.txHash || !mpp.getTempoSessionDrainState) throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
7591
+ const state = await mpp.getTempoSessionDrainState(channelId);
7592
+ if (state.finalized || state.deposit === 0n || state.closeRequestedAt !== 0n) {
7593
+ throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
7594
+ }
7595
+ const result = await reports.releaseStaleTempoClose(pending.id, channelId, record.drainId, {
7596
+ staleAfterMs: HOSTED_TEMPO_CLOSE_STALE_MS,
7597
+ maxReleases: HOSTED_TEMPO_CLOSE_MAX_RELEASES
7598
+ });
7599
+ if (result.outcome === "in_flight") throw new Error(HOSTED_TEMPO_CLOSE_IN_FLIGHT);
7600
+ if (result.outcome === "exhausted") {
7601
+ loggers.tempo.warn("tempo_session_close_attempts_exhausted", {
7602
+ credit_id: record.creditId,
7603
+ drain_id: record.drainId,
7604
+ channel_id: channelId,
7605
+ obligation_id: pending.id,
7606
+ attempt: result.releases + 1
7607
+ });
7608
+ throw new HostedTempoCloseAttemptsExhaustedError(
7609
+ `Tempo drain ${record.drainId} exhausted its cooperative close attempts`
7610
+ );
7611
+ }
7612
+ loggers.tempo.warn("tempo_session_close_reservation_released", {
7613
+ credit_id: record.creditId,
7614
+ drain_id: record.drainId,
7615
+ channel_id: channelId,
7616
+ obligation_id: pending.id,
7617
+ attempt: result.releases + 1
7618
+ });
7619
+ return result.id;
7620
+ }
7571
7621
  function tempoCloseCredentialNeedsRefresh(error) {
7572
7622
  return error instanceof MppxErrors.InvalidChallengeError || error instanceof MppxErrors.PaymentExpiredError;
7573
7623
  }
@@ -9931,6 +9981,8 @@ var HostedSettlementReportObligations = class {
9931
9981
  created_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint,
9932
9982
  updated_at_ms BIGINT NOT NULL DEFAULT FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
9933
9983
  );
9984
+ ALTER TABLE sdk_hosted_settlement_report_obligations
9985
+ ADD COLUMN IF NOT EXISTS close_search_before BIGINT;
9934
9986
  CREATE INDEX IF NOT EXISTS sdk_hosted_settlement_report_pending
9935
9987
  ON sdk_hosted_settlement_report_obligations (dvm_id,scope,state)
9936
9988
  WHERE state NOT IN ('delivered','no_report');
@@ -10037,53 +10089,109 @@ var HostedSettlementReportObligations = class {
10037
10089
  });
10038
10090
  }
10039
10091
  async reserveTempo(channelId, operation, drainId) {
10040
- const config = this.tempo;
10041
- if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
10092
+ return transaction(this.pool, (tx) => this.reserveTempoIn(tx, channelId, operation, drainId));
10093
+ }
10094
+ /**
10095
+ * Retire a close reservation that never recorded a transaction and reserve
10096
+ * the drain's next attempt, in one transaction, so the drain always keeps one
10097
+ * outstanding close obligation. A close from the retired attempt that lands
10098
+ * late is a close of the same channel: `recordSubmitted` on the retired row
10099
+ * writes it onto the successor, and recovery attaches it there if that
10100
+ * attempt never got to write, so it is reported once.
10101
+ *
10102
+ * The caller has just read the channel open on chain. The age bound only
10103
+ * spares a reservation young enough to be inside its first receipt wait; an
10104
+ * attempt slowed past it by a degraded RPC can still be running.
10105
+ */
10106
+ async releaseStaleTempoClose(id, channelId, drainId, limits) {
10042
10107
  return transaction(this.pool, async (tx) => {
10043
- const state = await tx.query(
10044
- "SELECT state_json FROM mpp_tempo_channels WHERE channel_key=$1",
10045
- [channelId.toLowerCase()]
10108
+ await this.lockIdentity(tx);
10109
+ const row = await this.lockRow(tx, id);
10110
+ if (row.rail !== "tempo" || row.operation !== "close" || row.channel_id !== channelId.toLowerCase() || row.original.drainId !== drainId || row.state !== "reserved" || row.tx_hash) {
10111
+ return { outcome: "in_flight" };
10112
+ }
10113
+ const age = await tx.query(
10114
+ `SELECT updated_at_ms <= FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint - $2 AS stale
10115
+ FROM sdk_hosted_settlement_report_obligations WHERE id=$1`,
10116
+ [id, limits.staleAfterMs]
10046
10117
  );
10047
- if (!state.rows[0]) throw new Error("Hosted Tempo channel is not durable");
10048
- const identity = await this.lockIdentity(tx);
10049
- const parsed = parseStoreValue(state.rows[0].state_json);
10050
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Hosted Tempo state is invalid");
10051
- const row = parsed;
10052
- if (row.chainId !== Number(config.network.split(":")[1]) || String(row.escrowContract).toLowerCase() !== config.escrow.toLowerCase() || String(row.payee).toLowerCase() !== config.payee.toLowerCase() || String(row.token).toLowerCase() !== config.token.toLowerCase()) {
10053
- throw new Error("Hosted Tempo channel has another chain or recipient");
10054
- }
10055
- if (operation === "close") {
10056
- if (!drainId) throw new Error("Hosted Tempo close requires the original drain");
10057
- const close = await tx.query(
10058
- "SELECT drain_id FROM mpp_tempo_close_intents WHERE channel_id=$1",
10059
- [channelId.toLowerCase()]
10060
- );
10061
- if (close.rows[0]?.drain_id !== drainId) throw new Error("Hosted Tempo close has no matching drain");
10062
- }
10063
- const highestVoucherAmount = nativeAmount(row.highestVoucherAmount);
10064
- const settledOnChain = nativeAmount(row.settledOnChain);
10065
- const spent = nativeAmount(row.spent);
10066
- if (BigInt(settledOnChain) > BigInt(highestVoucherAmount) || BigInt(spent) > BigInt(highestVoucherAmount)) {
10067
- throw new Error("Hosted Tempo settlement source has inconsistent amounts");
10068
- }
10069
- const original = operation === "settle" ? {
10070
- highestVoucherAmount,
10071
- settledOnChain,
10072
- spent
10073
- } : { drainId, highestVoucherAmount, settledOnChain, spent };
10074
- return this.insert(
10075
- tx,
10076
- identity,
10077
- "tempo",
10078
- operation,
10079
- config.network,
10080
- config.escrow,
10081
- channelId.toLowerCase(),
10082
- original,
10083
- operation === "close" ? [drainId] : [highestVoucherAmount]
10118
+ if (!age.rows[0]?.stale) return { outcome: "in_flight" };
10119
+ const released = Number((await tx.query(
10120
+ `SELECT COUNT(*)::text AS n FROM sdk_hosted_settlement_report_obligations
10121
+ WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
10122
+ AND original->>'drainId'=$4 AND state='no_report' AND tx_hash IS NULL`,
10123
+ [this.dvmId, this.scope, channelId.toLowerCase(), drainId]
10124
+ )).rows[0].n);
10125
+ if (released >= limits.maxReleases) return { outcome: "exhausted", releases: released };
10126
+ await tx.query(
10127
+ `UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
10128
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
10129
+ [id]
10084
10130
  );
10131
+ const next = await this.reserveTempoIn(tx, channelId, "close", drainId);
10132
+ return { outcome: "reserved", id: next.id, releases: released + 1 };
10085
10133
  });
10086
10134
  }
10135
+ /**
10136
+ * Resolve an unsubmitted close reservation whose channel the payer exited
10137
+ * on its own: no cooperative close of ours can land on a finalized channel.
10138
+ */
10139
+ async resolveTempoCloseExit(id) {
10140
+ const result = await this.pool.query(
10141
+ `UPDATE sdk_hosted_settlement_report_obligations SET state='no_report',
10142
+ updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint
10143
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'
10144
+ AND state='reserved' AND tx_hash IS NULL`,
10145
+ [id, this.dvmId, this.scope]
10146
+ );
10147
+ return result.rowCount === 1;
10148
+ }
10149
+ async reserveTempoIn(tx, channelId, operation, drainId) {
10150
+ const config = this.tempo;
10151
+ if (!config || !hex(channelId, 32)) throw new Error("Hosted Tempo settlement configuration is unavailable");
10152
+ const state = await tx.query(
10153
+ "SELECT state_json FROM mpp_tempo_channels WHERE channel_key=$1",
10154
+ [channelId.toLowerCase()]
10155
+ );
10156
+ if (!state.rows[0]) throw new Error("Hosted Tempo channel is not durable");
10157
+ const identity = await this.lockIdentity(tx);
10158
+ const parsed = parseStoreValue(state.rows[0].state_json);
10159
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Hosted Tempo state is invalid");
10160
+ const row = parsed;
10161
+ if (row.chainId !== Number(config.network.split(":")[1]) || String(row.escrowContract).toLowerCase() !== config.escrow.toLowerCase() || String(row.payee).toLowerCase() !== config.payee.toLowerCase() || String(row.token).toLowerCase() !== config.token.toLowerCase()) {
10162
+ throw new Error("Hosted Tempo channel has another chain or recipient");
10163
+ }
10164
+ if (operation === "close") {
10165
+ if (!drainId) throw new Error("Hosted Tempo close requires the original drain");
10166
+ const close = await tx.query(
10167
+ "SELECT drain_id FROM mpp_tempo_close_intents WHERE channel_id=$1",
10168
+ [channelId.toLowerCase()]
10169
+ );
10170
+ if (close.rows[0]?.drain_id !== drainId) throw new Error("Hosted Tempo close has no matching drain");
10171
+ }
10172
+ const highestVoucherAmount = nativeAmount(row.highestVoucherAmount);
10173
+ const settledOnChain = nativeAmount(row.settledOnChain);
10174
+ const spent = nativeAmount(row.spent);
10175
+ if (BigInt(settledOnChain) > BigInt(highestVoucherAmount) || BigInt(spent) > BigInt(highestVoucherAmount)) {
10176
+ throw new Error("Hosted Tempo settlement source has inconsistent amounts");
10177
+ }
10178
+ const original = operation === "settle" ? {
10179
+ highestVoucherAmount,
10180
+ settledOnChain,
10181
+ spent
10182
+ } : { drainId, highestVoucherAmount, settledOnChain, spent };
10183
+ return this.insert(
10184
+ tx,
10185
+ identity,
10186
+ "tempo",
10187
+ operation,
10188
+ config.network,
10189
+ config.escrow,
10190
+ channelId.toLowerCase(),
10191
+ original,
10192
+ operation === "close" ? [drainId] : [highestVoucherAmount]
10193
+ );
10194
+ }
10087
10195
  async findPendingTempo(channelId, operation) {
10088
10196
  const result = await this.pool.query(
10089
10197
  `SELECT * FROM sdk_hosted_settlement_report_obligations
@@ -10096,14 +10204,32 @@ var HostedSettlementReportObligations = class {
10096
10204
  /** The original close obligation reserved for this drain, while its report is outstanding. */
10097
10205
  async pendingTempoClose(channelId, drainId) {
10098
10206
  const result = await this.pool.query(
10099
- `SELECT id,tx_hash,created_at_ms FROM sdk_hosted_settlement_report_obligations
10207
+ `SELECT id,tx_hash,created_at_ms,close_search_before FROM sdk_hosted_settlement_report_obligations
10100
10208
  WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
10101
10209
  AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
10102
10210
  ORDER BY created_at_ms DESC LIMIT 1`,
10103
10211
  [this.dvmId, this.scope, channelId.toLowerCase(), drainId]
10104
10212
  );
10105
10213
  const row = result.rows[0];
10106
- return row && { id: row.id, txHash: row.tx_hash, reservedAtMs: Number(row.created_at_ms) };
10214
+ return row && {
10215
+ id: row.id,
10216
+ txHash: row.tx_hash,
10217
+ reservedAtMs: Number(row.created_at_ms),
10218
+ searchBefore: searchCursor(row.close_search_before)
10219
+ };
10220
+ }
10221
+ /**
10222
+ * Where the next search for this close obligation's transaction resumes. It
10223
+ * lives on the obligation, not the drain's close intent, because releasing
10224
+ * the drain deletes the intent while the obligation still needs the search.
10225
+ */
10226
+ async recordTempoCloseSearch(id, resumeAt) {
10227
+ await this.pool.query(
10228
+ `UPDATE sdk_hosted_settlement_report_obligations
10229
+ SET close_search_before = LEAST(COALESCE(close_search_before, $4), $4)
10230
+ WHERE id=$1 AND dvm_id=$2 AND scope=$3 AND rail='tempo' AND operation='close'`,
10231
+ [id, this.dvmId, this.scope, resumeAt.toString()]
10232
+ );
10107
10233
  }
10108
10234
  async submittedIds(rail, operation, txHash, batchId) {
10109
10235
  if (!hex(txHash, 32)) throw new Error("Hosted effect has no exact transaction hash");
@@ -10211,17 +10337,67 @@ var HostedSettlementReportObligations = class {
10211
10337
  if (!hex(txHash, 32)) throw new Error("Hosted settlement has no transaction hash");
10212
10338
  const write = this.x402SettlementStore ? this.x402Transaction.bind(this) : (operation) => transaction(this.pool, operation);
10213
10339
  await write(async (tx) => {
10214
- const row = await this.lockRow(tx, id);
10340
+ let row = await this.lockRow(tx, id);
10341
+ if (row.state === "no_report" && !row.tx_hash) {
10342
+ row = await this.successorOfRetiredClose(tx, row);
10343
+ }
10215
10344
  if (row.tx_hash && row.tx_hash.toLowerCase() !== txHash.toLowerCase()) {
10216
10345
  throw new Error("Hosted settlement transaction replay conflicts");
10217
10346
  }
10218
10347
  await tx.query(
10219
10348
  `UPDATE sdk_hosted_settlement_report_obligations SET tx_hash=COALESCE(tx_hash,$2),
10220
10349
  updated_at_ms=FLOOR(EXTRACT(EPOCH FROM clock_timestamp()) * 1000)::bigint WHERE id=$1`,
10221
- [id, txHash.toLowerCase()]
10350
+ [row.id, txHash.toLowerCase()]
10222
10351
  );
10223
10352
  });
10224
10353
  }
10354
+ /**
10355
+ * Close reservations that never recorded a transaction, oldest first. Each
10356
+ * one owes a payout report only if its close lands, so it stays unresolved
10357
+ * until a reconcile reads how its channel finalized.
10358
+ */
10359
+ async unsubmittedTempoCloses(limit = 100) {
10360
+ const result = await this.pool.query(
10361
+ `SELECT id,channel_id,original->>'drainId' AS drain_id,created_at_ms,close_search_before
10362
+ FROM sdk_hosted_settlement_report_obligations
10363
+ WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close'
10364
+ AND state='reserved' AND tx_hash IS NULL
10365
+ ORDER BY created_at_ms,id LIMIT $3`,
10366
+ [this.dvmId, this.scope, limit]
10367
+ );
10368
+ return result.rows.flatMap((row) => row.drain_id === null ? [] : [{
10369
+ id: row.id,
10370
+ channelId: row.channel_id,
10371
+ drainId: row.drain_id,
10372
+ reservedAtMs: Number(row.created_at_ms),
10373
+ searchBefore: searchCursor(row.close_search_before)
10374
+ }]);
10375
+ }
10376
+ /**
10377
+ * A retired close reservation's attempt can still be running when its
10378
+ * successor is reserved, and its close can land. The close is the same
10379
+ * channel's, so it belongs to the drain's outstanding obligation; writing it
10380
+ * onto the retired row would leave that obligation without a transaction and
10381
+ * the payout unreported.
10382
+ */
10383
+ async successorOfRetiredClose(tx, retired) {
10384
+ const drainId = retired.original.drainId;
10385
+ if (retired.rail !== "tempo" || retired.operation !== "close" || typeof drainId !== "string") {
10386
+ throw new Error("Hosted settlement obligation was resolved without a transaction");
10387
+ }
10388
+ const successor = await tx.query(
10389
+ `SELECT id FROM sdk_hosted_settlement_report_obligations
10390
+ WHERE dvm_id=$1 AND scope=$2 AND rail='tempo' AND operation='close' AND channel_id=$3
10391
+ AND original->>'drainId'=$4 AND state NOT IN ('delivered','no_report')
10392
+ ORDER BY created_at_ms DESC LIMIT 1`,
10393
+ [this.dvmId, this.scope, retired.channel_id, drainId]
10394
+ );
10395
+ if (!successor.rows[0]) {
10396
+ throw new Error("Hosted Tempo close was retired and the drain has no outstanding obligation");
10397
+ }
10398
+ const locked = await this.lockRow(tx, successor.rows[0].id);
10399
+ return locked.state === "no_report" && !locked.tx_hash ? this.successorOfRetiredClose(tx, locked) : locked;
10400
+ }
10225
10401
  async recoveryCandidates() {
10226
10402
  const result = await this.pool.query(
10227
10403
  `SELECT * FROM sdk_hosted_settlement_report_obligations
@@ -10683,6 +10859,9 @@ var HostedSettlementReportObligations = class {
10683
10859
  return { id: row.id, replayed: true };
10684
10860
  }
10685
10861
  };
10862
+ function searchCursor(value) {
10863
+ return value === null ? null : BigInt(value);
10864
+ }
10686
10865
 
10687
10866
  // src/sdk/server/hosting-channel-evidence.ts
10688
10867
  import { createHash as createHash7 } from "crypto";
@@ -19584,7 +19763,7 @@ async function createDVMServer(descriptor, opts) {
19584
19763
  }
19585
19764
  }
19586
19765
  if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
19587
- const { RevenueReporter } = await import("./revenue-reporter-X52GPZ5U.js");
19766
+ const { RevenueReporter } = await import("./revenue-reporter-KDUZLD5F.js");
19588
19767
  const reporter = new RevenueReporter(
19589
19768
  opts.db,
19590
19769
  platformUrl,
@@ -19594,7 +19773,7 @@ async function createDVMServer(descriptor, opts) {
19594
19773
  revenueReporter = reporter;
19595
19774
  await reporter.init();
19596
19775
  reporter.startRetryLoop();
19597
- const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-HQSIIBFL.js");
19776
+ const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-NN6K2IVQ.js");
19598
19777
  payoutReporter = new PayoutReporter({
19599
19778
  dvmId,
19600
19779
  db: opts.db,
@@ -20051,6 +20230,9 @@ var DVMServer = class {
20051
20230
  if (!certificateOnly && await control.close(command, support) === "conflict") {
20052
20231
  return c.json({ error: "hosting_closure_conflict" }, 409);
20053
20232
  }
20233
+ await this.opts.mpp?.reconcileTempoSessionCloses?.().catch(() => {
20234
+ diagnosticConsole.warn({ level: "tempo_routine_sweep_failed", dvm_id: this.opts.dvmId, error: void 0 });
20235
+ });
20054
20236
  const evidence = await control.certificate({ ...command, action: "certificate" }, support);
20055
20237
  if (!evidence) return c.json({ error: "hosting_closure_stale" }, 409);
20056
20238
  return c.json(evidence);
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-YG7G4DPZ.js";
5
5
  import {
6
6
  diagnosticConsole
7
- } from "./chunk-DZQYQN2X.js";
7
+ } from "./chunk-BFGQG4WY.js";
8
8
  import {
9
9
  withSdkInitLock
10
10
  } from "./chunk-COYKJRMM.js";