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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +14 -0
  2. package/dist/{chunk-MLRCSJYX.js → chunk-2D75MNNG.js} +178 -550
  3. package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
  4. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  5. package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-AJNFXR7J.js} +2 -2
  7. package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
  8. package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
  9. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  10. package/dist/{chunk-TQWGQCNV.js → chunk-FPOLJRBF.js} +5 -5
  11. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  12. package/dist/{chunk-SSSZUVWM.js → chunk-JAHRF5X3.js} +89 -39
  13. package/dist/{chunk-5PBOA25N.js → chunk-JNKQZVZX.js} +472 -617
  14. package/dist/{chunk-U6M3ATSG.js → chunk-MD3H3BF7.js} +40 -6
  15. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  16. package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
  17. package/dist/{chunk-JZWELPFH.js → chunk-N5B66UOS.js} +4 -16
  18. package/dist/{chunk-BIP6G74V.js → chunk-O6D72KYA.js} +10 -10
  19. package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-KVEHHC7W.js → chunk-U5VNEALI.js} +24 -13
  22. package/dist/{chunk-FROTD5XQ.js → chunk-V5ZRATJH.js} +2 -3
  23. package/dist/{chunk-EVBK675R.js → chunk-VA7GT6C6.js} +31 -37
  24. package/dist/{chunk-2UUXIIOC.js → chunk-VIVXB4CD.js} +3 -3
  25. package/dist/{chunk-6BQM7TOW.js → chunk-XMDQOXHO.js} +11 -38
  26. package/dist/{chunk-DMNLFNTW.js → chunk-XRLKGCAW.js} +2 -2
  27. package/dist/{chunk-M7LHFJ5K.js → chunk-XZ7LW4S5.js} +2 -3
  28. package/dist/{chunk-E4EVGPDX.js → chunk-YO56VCTQ.js} +0 -6
  29. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-MHA652TV.js} +2 -2
  30. package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-kDA9RVzD.d.ts} +524 -1077
  31. package/dist/{fx-D860pZvP.d.ts → fx-DhyT1Cjh.d.ts} +38 -82
  32. package/dist/index.d.ts +11 -26
  33. package/dist/index.js +2 -2
  34. package/dist/internal/caller.d.ts +644 -2140
  35. package/dist/internal/caller.js +32 -66
  36. package/dist/internal/server.d.ts +40 -90
  37. package/dist/internal/server.js +16 -16
  38. package/dist/{job-store-BUGqvCfL.d.ts → job-store-D3CcZp_g.d.ts} +73 -66
  39. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-BhsyrBLa.d.ts} +19 -41
  40. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-BYBATRN3.js} +3 -3
  41. package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
  42. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-MPP3ST5X.js} +1 -1
  43. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-KEMUXEUO.js} +2 -2
  44. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-6N2V5MJN.js} +2 -2
  45. package/dist/postgres-job-store-MZZZMS2B.js +7 -0
  46. package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
  47. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
  48. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  49. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-GKMAWOM3.js} +2 -2
  50. package/dist/revenue-reporter-O75BGYFK.js +7 -0
  51. package/dist/server/index.d.ts +47 -159
  52. package/dist/server/index.js +67 -138
  53. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  54. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-v2yhyhSZ.d.ts} +617 -1532
  55. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-SDTUNTKA.js} +2 -2
  56. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-G5SB4HFC.js} +5 -5
  57. package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
  58. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-ZFNUDFVO.js} +2 -2
  59. package/dist/testing/index.d.ts +5 -15
  60. package/dist/testing/index.js +4 -11
  61. package/dist/{usd-DoRuAckA.d.ts → usd-Cu9G6xv2.d.ts} +14 -16
  62. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  63. package/dist/{x402-5H27DCBE.js → x402-CI6PSHNO.js} +5 -5
  64. package/package.json +3 -2
  65. package/dist/postgres-job-store-3RAXMNSY.js +0 -7
  66. package/dist/postgres-kv-store-JFBDP5IP.js +0 -7
  67. package/dist/revenue-reporter-ASZ7SHHH.js +0 -7
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StepCache,
3
3
  loggers
4
- } from "./chunk-E4EVGPDX.js";
4
+ } from "./chunk-YO56VCTQ.js";
5
5
  import {
6
6
  redactUrl
7
7
  } from "./chunk-FUJ36YDV.js";
@@ -23,6 +23,11 @@ function toJobRecord(job) {
23
23
  authRequestPath: job.authRequestPath,
24
24
  status: job.status,
25
25
  summary: job.summary,
26
+ timingKnown: job.timingKnown,
27
+ endedBy: job.endedBy,
28
+ terminalAtMs: job.terminalAtMs,
29
+ callerWaitMs: job.callerWaitMs,
30
+ awaitingInputAtMs: job.awaitingInputAtMs,
26
31
  messages: job.messages,
27
32
  seq: job.seq,
28
33
  paidMsats: job.paidMsats,
@@ -69,6 +74,11 @@ function fromJobRecord(record) {
69
74
  requesterTokenHash: record.requesterTokenHash,
70
75
  status: record.status,
71
76
  summary: record.summary,
77
+ timingKnown: record.timingKnown,
78
+ endedBy: record.endedBy,
79
+ terminalAtMs: record.terminalAtMs,
80
+ callerWaitMs: record.callerWaitMs,
81
+ awaitingInputAtMs: record.awaitingInputAtMs,
72
82
  messages: [...record.messages],
73
83
  seq: record.seq,
74
84
  paidMsats: record.paidMsats,
@@ -138,11 +148,12 @@ function providerMessage(job, type, content) {
138
148
  const pendingPaymentDelta = typeof paymentRequest.amount_msats === "number" ? paymentRequest.amount_msats : void 0;
139
149
  const pendingMppChallengeIds = paymentRequest.tempo?.challenges?.map((challenge) => challenge.id).filter((id) => typeof id === "string");
140
150
  job.messageAppender?.(msg, {
151
+ awaitingInput: job.status === "awaiting-input",
141
152
  pendingPaymentDelta,
142
153
  pendingMppChallengeIds,
143
154
  pendingX402Nonce: typeof paymentRequest.x402?.nonce === "string" ? paymentRequest.x402.nonce : void 0,
144
155
  pendingX402AmountUsdcMicro: typeof paymentRequest.x402?.required_usdc_micro === "string" ? paymentRequest.x402.required_usdc_micro : void 0,
145
- // internal-review: read off the job rather than the content — the fiat envelope
156
+ // read off the job rather than the content — the fiat envelope
146
157
  // is ledger state, not part of the wire shape, and `requestPayment` has
147
158
  // already pinned it on the job by the time this fires (same ordering the
148
159
  // `pendingPaymentMsats` comment above describes).
@@ -151,7 +162,7 @@ function providerMessage(job, type, content) {
151
162
  });
152
163
  return;
153
164
  }
154
- job.messageAppender?.(msg);
165
+ job.messageAppender?.(msg, type === "prompt" ? { awaitingInput: job.status === "awaiting-input" } : void 0);
155
166
  }
156
167
  function localMessage(job, from, type, content) {
157
168
  job.seq++;
@@ -215,9 +226,7 @@ var MemoryJobStore = class {
215
226
  notifySubscribers = /* @__PURE__ */ new Map();
216
227
  reactivationLocks = /* @__PURE__ */ new Set();
217
228
  requestIdClaims = /* @__PURE__ */ new Map();
218
- /** Per-DVM monotonic receipt counter (internal-review) — the `receipt_counter` row. */
219
229
  receiptCounter = 0;
220
- /** Receipt sequence already allocated to a job, keyed by job id. */
221
230
  receiptSeqs = /* @__PURE__ */ new Map();
222
231
  get(id) {
223
232
  const record = this.data.get(id);
@@ -280,7 +289,10 @@ var MemoryJobStore = class {
280
289
  return Promise.resolve();
281
290
  }
282
291
  const snapshot = structuredClone(record);
292
+ if (existing) snapshot.createdAt = existing.createdAt;
283
293
  if (existing?.receipt) snapshot.receipt = existing.receipt;
294
+ snapshot.timingKnown = existing ? existing.timingKnown ?? false : record.timingKnown ?? true;
295
+ applyTiming(snapshot, existing, Date.now());
284
296
  this.data.set(record.id, snapshot);
285
297
  const cur = this.nextSeq.get(record.id) ?? 1;
286
298
  this.nextSeq.set(record.id, Math.max(cur, record.seq + 1));
@@ -351,6 +363,10 @@ var MemoryJobStore = class {
351
363
  }
352
364
  const seq = this.allocateSeq(jobId);
353
365
  const fullMessage = { ...message, seq };
366
+ if (opts?.awaitingInput && !isTerminal(record.status)) {
367
+ applyTiming(record, structuredClone(record), Date.now(), "awaiting-input");
368
+ record.status = "awaiting-input";
369
+ }
354
370
  this.messagesFor(jobId).push({ message: fullMessage, status: "verified" });
355
371
  if (opts?.pendingPaymentDelta !== void 0) {
356
372
  if (opts.pendingPaymentDelta !== 0) {
@@ -503,6 +519,7 @@ var MemoryJobStore = class {
503
519
  claimForProcessing(jobId) {
504
520
  const record = this.data.get(jobId);
505
521
  if (record?.status !== "awaiting-input") return Promise.resolve(false);
522
+ applyTiming(record, structuredClone(record), Date.now(), "processing");
506
523
  record.status = "processing";
507
524
  record.lastActivityAt = Date.now();
508
525
  return Promise.resolve(true);
@@ -565,7 +582,6 @@ var MemoryJobStore = class {
565
582
  this.notifySubscribers.clear();
566
583
  return Promise.resolve();
567
584
  }
568
- // ── Internal helpers ──────────────────────────────────────────────────
569
585
  /**
570
586
  * Shared body of `cancelStaleJob` (activity cutoff supplied) and `cancelJob`
571
587
  * (`null` cutoff — unconditional). Mirrors `PostgresJobStore.terminateJob`:
@@ -586,6 +602,8 @@ var MemoryJobStore = class {
586
602
  content: { reason }
587
603
  };
588
604
  this.messagesFor(jobId).push({ message: cancelMsg, status: "verified" });
605
+ record.endedBy = expectedActivityBefore === null || record.status === "awaiting-input" ? "caller" : "provider";
606
+ applyTiming(record, structuredClone(record), Date.now(), terminalStatus);
589
607
  record.status = terminalStatus;
590
608
  record.summary = record.summary ?? reason;
591
609
  record.messages = [...record.messages, cancelMsg];
@@ -645,6 +663,22 @@ function snapshotCounters(record) {
645
663
  summary: record.summary
646
664
  };
647
665
  }
666
+ function applyTiming(next, prior, now, status = next.status) {
667
+ if (prior && isTerminal(prior.status)) {
668
+ next.endedBy = prior.endedBy;
669
+ next.terminalAtMs = prior.terminalAtMs;
670
+ next.callerWaitMs = prior.callerWaitMs;
671
+ next.awaitingInputAtMs = void 0;
672
+ return;
673
+ }
674
+ const start = prior?.awaitingInputAtMs ?? (prior?.status === "awaiting-input" ? prior.lastActivityAt : void 0);
675
+ next.callerWaitMs = (prior?.callerWaitMs ?? next.callerWaitMs ?? 0) + (start !== void 0 && status !== "awaiting-input" ? Math.max(0, now - start) : 0);
676
+ next.awaitingInputAtMs = status === "awaiting-input" ? start ?? now : void 0;
677
+ if (isTerminal(status)) {
678
+ next.terminalAtMs = now;
679
+ next.endedBy = status === "completed" ? void 0 : next.endedBy ?? "provider";
680
+ }
681
+ }
648
682
 
649
683
  // src/sdk/server/instrumented-fetch.ts
650
684
  function createInstrumentedFetch(signal) {
@@ -30,7 +30,6 @@ var PROTECTED_CONFIG_FIELDS = ["nwcUri", "defaultIdentity", "tempo", "x402"];
30
30
  var ProtectedFieldAuthorization = class {
31
31
  /** Targets this authorization covers. A field also covers its own leaves. */
32
32
  targets;
33
- /** Prefer {@link authorizeProtectedRemoval}; this exists for it to call. */
34
33
  constructor(targets) {
35
34
  this.targets = [...targets];
36
35
  }
@@ -304,11 +303,7 @@ function protectedRemovalError(targets, reason) {
304
303
  }
305
304
  var PROTECTED_FIELDS = {
306
305
  nwcUri: { aliases: [], credentialLeaves: [], secret: true, hexCredential: false },
307
- // A pointer at a key, not a key: worth protecting from removal, and never
308
- // worth stripping out of a stored copy — that is recovery lost for nothing.
309
306
  defaultIdentity: { aliases: [], credentialLeaves: [], secret: false, hexCredential: false },
310
- // `mpp` is the pre-internal-review name; a caller who has not run a mutating command
311
- // since the rename still has their private key under it.
312
307
  tempo: { aliases: ["mpp"], credentialLeaves: ["apiKey"], secret: true, hexCredential: true },
313
308
  x402: { aliases: [], credentialLeaves: ["privateKey"], secret: true, hexCredential: true }
314
309
  };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IMPLICIT_CREDIT_TTL_MS
3
- } from "./chunk-5URG56JJ.js";
3
+ } from "./chunk-A4CM2BD3.js";
4
4
  import {
5
5
  CREDITS_FILE,
6
6
  DvmError,
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-YG7G4DPZ.js";
5
5
  import {
6
6
  withSdkInitLock
7
- } from "./chunk-S3XAHZQY.js";
7
+ } from "./chunk-COYKJRMM.js";
8
8
 
9
9
  // src/sdk/server/tempo-charge-store.ts
10
10
  var DEFAULT_TABLE_NAME = "mpp_tempo_charge_replays";
@@ -46,7 +46,6 @@ var PostgresTempoChargeStore = class {
46
46
  );
47
47
  return rows[0] ? parseStoreValue(rows[0].value_json) : null;
48
48
  }
49
- /** Replace one stored value. */
50
49
  async put(key, value) {
51
50
  await this.update(key, () => ({ op: "set", value, result: void 0 }));
52
51
  }
@@ -55,19 +54,9 @@ var PostgresTempoChargeStore = class {
55
54
  await this.update(key, () => ({ op: "delete", result: void 0 }));
56
55
  }
57
56
  /**
58
- * Atomic read-modify-write, linearized across every machine sharing the pool.
59
- *
60
- * The advisory lock is taken before the row lock because a key's *first*
61
- * write is the one that matters here: `FOR UPDATE` locks nothing when no row
62
- * exists, so two machines marking one hash for the first time would both read
63
- * `null` and both believe they won.
64
- *
65
- * Replay keys are unique per credential, so they never contend. The contended
66
- * key is the fee-sponsor budget — one per `(chain, sponsor)`, read-modified
67
- * several times per sponsored charge — and serializing it across machines is
68
- * the point of sharing the store at all. The critical section is a SELECT and
69
- * an upsert, so a queue of waiters clears in tens of milliseconds; a pooled
70
- * connection is held for exactly that long and never across a chain call.
57
+ * Advisory locking covers the absent-row first write before the row lock.
58
+ * Both competing presentations of one credential and the shared sponsor
59
+ * budget need this serialization. The transaction contains no chain call.
71
60
  */
72
61
  async update(key, fn) {
73
62
  const client = await this.pool.connect();
@@ -106,7 +95,6 @@ var PostgresTempoChargeStore = class {
106
95
  client.release();
107
96
  }
108
97
  }
109
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
110
98
  async createTables() {
111
99
  await this.pool.query(`
112
100
  CREATE TABLE IF NOT EXISTS ${this.tableName} (
@@ -4,13 +4,13 @@ import {
4
4
  listCredits,
5
5
  maxAdvertisedMicro,
6
6
  withinAdvertisedTolerance
7
- } from "./chunk-KSQEFVFJ.js";
7
+ } from "./chunk-MMCE7XSR.js";
8
8
  import {
9
9
  authorizeProtectedRemoval,
10
10
  isValidEvmPrivateKey,
11
11
  loadConfig,
12
12
  updateConfig
13
- } from "./chunk-CGKZDODG.js";
13
+ } from "./chunk-MG67KXU7.js";
14
14
  import {
15
15
  X402_DEFAULT_NETWORK,
16
16
  X402_V1_VERSION,
@@ -34,7 +34,7 @@ import {
34
34
  getLastKnownBtcRate,
35
35
  msatsToUsdc,
36
36
  usdcToMsats
37
- } from "./chunk-5URG56JJ.js";
37
+ } from "./chunk-A4CM2BD3.js";
38
38
  import {
39
39
  DvmError,
40
40
  X402_CHANNELS_FILE,
@@ -190,7 +190,7 @@ function x402PendingFundingJson(entry) {
190
190
  amount_micro: entry.amountMicro,
191
191
  // What the ERC-3009 authorization can still pull, which is the figure a
192
192
  // `--force` retirement is really deciding about: a deposit's authorized
193
- // value can exceed the amount funded (internal-review), so `amount_micro` alone
193
+ // value can exceed the amount funded, so `amount_micro` alone
194
194
  // understates it. Null on a voucher-only payload, which authorizes no
195
195
  // deposit at all.
196
196
  deposit_micro: entry.depositMicro ?? null,
@@ -243,7 +243,7 @@ var DvmX402ChannelStorage = class {
243
243
  /**
244
244
  * Upstream's ordinary write. Its chain-recovery paths are authoritative and
245
245
  * clear the unreconciled stamp. DVM-response paths scope this same write to
246
- * persist their weaker provenance atomically with the balance (internal-review).
246
+ * persist their weaker provenance atomically with the balance.
247
247
  */
248
248
  set(key, context) {
249
249
  return fromSynchronousStore(() => {
@@ -266,7 +266,7 @@ var DvmX402ChannelStorage = class {
266
266
  }
267
267
  /**
268
268
  * Write a context whose `balance` carries a deposit this machine added on its
269
- * own authority, and stamp the record as unreconciled (internal-review).
269
+ * own authority, and stamp the record as unreconciled.
270
270
  *
271
271
  * Separate from {@link set} because the two differ in exactly one thing that
272
272
  * matters later: where the figure came from. Nothing else about the write
@@ -279,7 +279,7 @@ var DvmX402ChannelStorage = class {
279
279
  }
280
280
  /**
281
281
  * Write a context whose `balance` is byte-for-byte the one already stored,
282
- * leaving the unreconciled stamp exactly as it found it (internal-review).
282
+ * leaving the unreconciled stamp exactly as it found it.
283
283
  *
284
284
  * The replay acknowledgement's idempotent second pass is what needs this: it
285
285
  * restates a cumulative base without adding to the balance, so it observes
@@ -294,7 +294,7 @@ var DvmX402ChannelStorage = class {
294
294
  }
295
295
  /**
296
296
  * Seed upstream's cooperative-refund probe without changing the acknowledged
297
- * base. Records written before internal-review have only the upstream context, so
297
+ * base. Records written before have only the upstream context, so
298
298
  * `hold` adopts its acknowledged value rather than losing that base to the
299
299
  * chain floor this seeds. The seed itself is a `channels()` read, so it is
300
300
  * `observed` and settles the unreconciled question like any other.
@@ -605,7 +605,7 @@ function loadFile() {
605
605
  channels: raw.channels,
606
606
  pending: raw.pending,
607
607
  // Tolerated rather than required, unlike its two siblings: every file
608
- // written before internal-review lacks the key, and failing closed on it would
608
+ // written before lacks the key, and failing closed on it would
609
609
  // read as a corrupt journal on a store that is perfectly intact.
610
610
  exact: Array.isArray(raw.exact) ? raw.exact : []
611
611
  };
@@ -753,7 +753,7 @@ async function getX402Balance(wallet) {
753
753
  usdc: Number(balance) / 1e6,
754
754
  network: w.network,
755
755
  tokenAddress: spec.usdc,
756
- // internal-review: redacted here rather than at a display surface, so the clear
756
+ // redacted here rather than at a display surface, so the clear
757
757
  // endpoint never leaves this function on the success path either.
758
758
  rpcUrlRedacted: rpcUrl ? redactUrl(rpcUrl) : void 0,
759
759
  rpcSource
@@ -2,7 +2,7 @@ import {
2
2
  costAnchor,
3
3
  formatUsd,
4
4
  msatsToUsd
5
- } from "./chunk-5URG56JJ.js";
5
+ } from "./chunk-A4CM2BD3.js";
6
6
  import {
7
7
  AGENT_MNEMONIC_FILE,
8
8
  BUILDER_CONFIG_DIR,
@@ -17,7 +17,6 @@ var MemoryKVStore = class {
17
17
  this.data.set(key, { value, expiry });
18
18
  return Promise.resolve();
19
19
  }
20
- /** Delete a key. */
21
20
  delete(key) {
22
21
  this.data.delete(key);
23
22
  return Promise.resolve();
@@ -15,14 +15,14 @@ import {
15
15
  lotOwedSats,
16
16
  netOwedSats,
17
17
  x402SettlementPending
18
- } from "./chunk-MLRCSJYX.js";
18
+ } from "./chunk-2D75MNNG.js";
19
19
 
20
20
  // src/sdk/server/memory-credit-ledger.ts
21
21
  import { randomUUID } from "crypto";
22
22
  var MemoryCreditLedger = class _MemoryCreditLedger {
23
23
  /**
24
24
  * Per-process and lost on restart, so a DVM backed by this ledger never
25
- * advertises the funding menu outside devMode (internal-review) — funding on one
25
+ * advertises the funding menu outside devMode — funding on one
26
26
  * machine and drawing on another would `credit_not_found`.
27
27
  */
28
28
  durable = false;
@@ -42,7 +42,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
42
42
  fork.x402Settlements = this.x402Settlements;
43
43
  return fork;
44
44
  }
45
- /** See {@link CreditLedger.useX402SettlementGate} — same contract (internal-review). */
46
45
  useX402SettlementGate(gate) {
47
46
  this.x402Settlements = gate;
48
47
  }
@@ -62,12 +61,10 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
62
61
  );
63
62
  return run(() => this.drawSync(args, blocking));
64
63
  }
65
- /** See {@link CreditLedger.blockingX402Refund} — same contract (internal-review). */
66
64
  async blockingX402Refund(channelId, statuses) {
67
65
  if (!channelId || !this.x402Settlements) return void 0;
68
66
  return this.x402Settlements.pendingRefundSettlement(channelId, statuses);
69
67
  }
70
- /** The unresolved x402 refund that must stop an effect on `creditId` (internal-review). */
71
68
  blockingRefundForCredit(creditId, statuses) {
72
69
  return this.blockingX402Refund(this.credits.get(creditId)?.x402ChannelId, statuses);
73
70
  }
@@ -540,7 +537,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
540
537
  credit.draws.set(args.drawId, draw);
541
538
  return { ...drawResult(draw, credit), replayed: false };
542
539
  }
543
- /** Mid-job top-up growth (internal-review) — see `CreditLedger.growDraw` for the semantics. */
544
540
  growDrawSync(args) {
545
541
  assertAmount(args.addAmountMicro, { min: 1 });
546
542
  const credit = this.requireCredit(args.creditId);
@@ -667,7 +663,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
667
663
  const page = after ? out.filter((d) => compareStaleDrawKey(d, after) > 0) : out;
668
664
  return Promise.resolve(page.slice(0, args.limit));
669
665
  }
670
- /** Settled draws' native value for the credit an x402 channel funded (internal-review). */
671
666
  earnedNativeForX402Channel(channelId) {
672
667
  for (const credit of this.credits.values()) {
673
668
  if (credit.x402ChannelId !== channelId) continue;
@@ -757,7 +752,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
757
752
  [...this.x402Losses.values()].sort((a, b) => b.observedAt - a.observedAt || a.creditId.localeCompare(b.creditId)).slice(0, args?.limit ?? 50)
758
753
  );
759
754
  }
760
- // ── Drains (internal-review) — semantics mirror the Postgres ledger ─────────
761
755
  async requestDrain(args) {
762
756
  const wedged = await this.blockingRefundForCredit(
763
757
  args.creditId,
@@ -861,15 +855,17 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
861
855
  bitcoinDepositLiability() {
862
856
  const byRail = /* @__PURE__ */ new Map();
863
857
  const byCurrency = /* @__PURE__ */ new Map();
858
+ const uncoveredByCurrency = /* @__PURE__ */ new Map();
859
+ const unbackedByCurrency = /* @__PURE__ */ new Map();
864
860
  const liability = {
865
861
  depositSats: 0,
866
862
  credits: 0,
867
863
  byRail: [],
868
864
  byCurrency: [],
869
865
  uncoveredCredits: 0,
870
- uncoveredMicro: 0,
866
+ uncoveredByCurrency: [],
871
867
  unbackedCredits: 0,
872
- unbackedMicro: 0
868
+ unbackedByCurrency: []
873
869
  };
874
870
  for (const credit of this.credits.values()) {
875
871
  if (!isNonChannelBitcoinRail(credit.rail)) continue;
@@ -887,10 +883,24 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
887
883
  liability.credits++;
888
884
  if (covered < credit.balanceMicro) {
889
885
  liability.uncoveredCredits++;
890
- liability.uncoveredMicro += credit.balanceMicro;
886
+ const currency2 = uncoveredByCurrency.get(credit.currency) ?? {
887
+ currency: credit.currency,
888
+ micro: 0,
889
+ credits: 0
890
+ };
891
+ currency2.micro += credit.balanceMicro;
892
+ currency2.credits++;
893
+ uncoveredByCurrency.set(credit.currency, currency2);
891
894
  } else if (basisless > 0) {
892
895
  liability.unbackedCredits++;
893
- liability.unbackedMicro += credit.balanceMicro;
896
+ const currency2 = unbackedByCurrency.get(credit.currency) ?? {
897
+ currency: credit.currency,
898
+ micro: 0,
899
+ credits: 0
900
+ };
901
+ currency2.micro += credit.balanceMicro;
902
+ currency2.credits++;
903
+ unbackedByCurrency.set(credit.currency, currency2);
894
904
  }
895
905
  const rail = byRail.get(credit.rail) ?? { rail: credit.rail, sats: 0, credits: 0 };
896
906
  rail.sats += owed;
@@ -907,6 +917,8 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
907
917
  }
908
918
  liability.byRail = [...byRail.values()];
909
919
  liability.byCurrency = [...byCurrency.values()];
920
+ liability.uncoveredByCurrency = [...uncoveredByCurrency.values()];
921
+ liability.unbackedByCurrency = [...unbackedByCurrency.values()];
910
922
  return Promise.resolve(liability);
911
923
  }
912
924
  listFundingLots(creditId) {
@@ -1046,7 +1058,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
1046
1058
  if (drain) drain.receipts.push(args.receipt);
1047
1059
  });
1048
1060
  }
1049
- // ── Internals ─────────────────────────────────────────────────────────
1050
1061
  requireDrain(creditId, drainId) {
1051
1062
  const credit = this.credits.get(creditId);
1052
1063
  const drain = credit?.drains.get(drainId);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  withSdkInitLock
3
- } from "./chunk-S3XAHZQY.js";
3
+ } from "./chunk-COYKJRMM.js";
4
4
 
5
5
  // src/sdk/server/postgres-kv-store.ts
6
6
  var PostgresKVStore = class {
@@ -12,12 +12,11 @@ var PostgresKVStore = class {
12
12
  async init() {
13
13
  await withSdkInitLock(this.pool, () => this.createTables());
14
14
  }
15
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
16
15
  async createTables() {
17
16
  await this.pool.query(`
18
17
  -- Single-tenant KV table aligned with the platform's isolate storage schema.
19
18
  -- The only structural difference is the absence of dvm_id (SDK is single-DVM-per-DB).
20
- -- Any column addition should land on both schemas; see M06 / internal-review for the convergence precedent.
19
+ -- Keep column additions aligned with the platform isolate schema.
21
20
  CREATE TABLE IF NOT EXISTS kv_store (
22
21
  key TEXT PRIMARY KEY,
23
22
  value JSONB NOT NULL,
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  withSdkInitLock
3
- } from "./chunk-S3XAHZQY.js";
3
+ } from "./chunk-COYKJRMM.js";
4
4
 
5
5
  // src/sdk/server/revenue-reporter.ts
6
6
  import { randomUUID } from "crypto";
7
7
  var ENDPOINTS = {
8
+ job_terminal: "/_internal/job-terminal",
8
9
  revenue: "/_internal/job-revenue",
9
10
  cost: "/_internal/job-cost",
10
11
  deposit: "/_internal/credit-deposit",
@@ -51,7 +52,6 @@ var RevenueReporter = class _RevenueReporter {
51
52
  async init() {
52
53
  await withSdkInitLock(this.db, () => this.createTables());
53
54
  }
54
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
55
55
  async createTables() {
56
56
  await this.db.query(`
57
57
  CREATE TABLE IF NOT EXISTS ${PENDING_DVM_REPORTS_TABLE} (
@@ -65,6 +65,10 @@ var RevenueReporter = class _RevenueReporter {
65
65
  kind TEXT NOT NULL DEFAULT 'revenue'
66
66
  )
67
67
  `);
68
+ await this.db.query(`CREATE TABLE IF NOT EXISTS reported_job_terminals (
69
+ dvm_id TEXT NOT NULL, job_id TEXT NOT NULL, created_at BIGINT NOT NULL,
70
+ PRIMARY KEY (dvm_id, job_id)
71
+ )`);
68
72
  await this.assertSdkTableShape(PENDING_DVM_REPORTS_TABLE);
69
73
  await this.db.query(
70
74
  `ALTER TABLE ${PENDING_DVM_REPORTS_TABLE} ADD COLUMN IF NOT EXISTS stale_reason TEXT`
@@ -74,7 +78,6 @@ var RevenueReporter = class _RevenueReporter {
74
78
  );
75
79
  await this.migrateLegacySdkOutbox();
76
80
  }
77
- /** Copy rows from the old SDK-owned table without touching the platform's table of that name. */
78
81
  async migrateLegacySdkOutbox() {
79
82
  const columns = await this.tableColumns(LEGACY_PENDING_REPORTS_TABLE);
80
83
  if (columns.size === 0) return;
@@ -106,7 +109,6 @@ var RevenueReporter = class _RevenueReporter {
106
109
  )
107
110
  `);
108
111
  }
109
- /** Refuse a pre-existing relation whose columns cannot safely serve as the SDK outbox. */
110
112
  async assertSdkTableShape(tableName) {
111
113
  this.assertSdkColumns(tableName, await this.tableColumns(tableName));
112
114
  }
@@ -130,6 +132,21 @@ var RevenueReporter = class _RevenueReporter {
130
132
  async report(payload) {
131
133
  return this.enqueue("revenue", payload, payload.jobId);
132
134
  }
135
+ /** The permanent deduplication claim and outbox insert commit together.
136
+ * The claim survives successful delivery, including for custom job stores.
137
+ */
138
+ async reportTerminal(payload) {
139
+ const id = randomUUID();
140
+ const { rowCount } = await this.db.query(
141
+ `WITH claimed AS (
142
+ INSERT INTO reported_job_terminals (dvm_id, job_id, created_at)
143
+ VALUES ($5, $1, $4) ON CONFLICT DO NOTHING RETURNING job_id
144
+ ) INSERT INTO ${PENDING_DVM_REPORTS_TABLE} (id, payload, created_at, kind)
145
+ SELECT $2, $3::jsonb, $4, 'job_terminal' FROM claimed`,
146
+ [payload.jobId, id, JSON.stringify(payload), Date.now(), payload.dvmId]
147
+ );
148
+ if (rowCount) void this.attemptDelivery(id, payload, "job_terminal", payload.jobId).catch(() => void 0);
149
+ }
133
150
  /** Persist a zero-revenue job cost and attempt immediate delivery. */
134
151
  async reportCost(payload) {
135
152
  return this.enqueue("cost", payload, payload.jobId);
@@ -149,23 +166,15 @@ var RevenueReporter = class _RevenueReporter {
149
166
  await this.insertPending(tx, "release", payload);
150
167
  }
151
168
  /**
152
- * Join a credit drain to the transaction that made it terminal (internal-review).
153
- *
154
- * Same discipline as {@link enqueueDeposit} and the same reason: no network
155
- * I/O here, only the durable queue row, written through `tx` so the report
156
- * and the status CAS commit or roll back together. A drain whose row was
157
- * lost between the two would overstate outstanding liability forever —
158
- * the mirror image of a lost deposit.
159
- *
160
- * Named for the payload rather than the verb because `drain` already means
161
- * "flush the queue" in this class (see {@link drainPending}).
169
+ * Await durable queue insertion through tx so the terminal drain and its
170
+ * report commit or roll back together; no network I/O here.
162
171
  */
163
172
  async enqueueCreditDrain(tx, payload) {
164
173
  await this.insertPending(tx, "drain", payload);
165
174
  }
166
175
  /**
167
176
  * Join a payout to the transaction that commits the movement it reports
168
- * (internal-review) — the batch close, the accumulator mark, the drain booking.
177
+ * the batch close, the accumulator mark, the drain booking.
169
178
  * No network I/O, only the durable queue row through `tx`, the discipline
170
179
  * {@link enqueueCreditDrain} set and for the same reason: the money has
171
180
  * already moved on a rail this ledger does not own, so the platform row is
@@ -187,7 +196,7 @@ var RevenueReporter = class _RevenueReporter {
187
196
  return this.enqueue("payout", payload, payload.payoutId);
188
197
  }
189
198
  /**
190
- * Report one rail's pending-pool snapshot (internal-review). Best-effort and
199
+ * Report one rail's pending-pool snapshot. Best-effort and
191
200
  * fire-and-forget, deliberately unlike {@link reportPayout}: a snapshot is
192
201
  * replaced by the next tick, so queueing a stale one behind an outage would
193
202
  * only deliver figures the platform already has newer ones for.
@@ -224,20 +233,14 @@ var RevenueReporter = class _RevenueReporter {
224
233
  }
225
234
  }
226
235
  /**
227
- * Join a credit-expiry release — or the revival that reverses one — to the
228
- * transaction that made it true (internal-review).
229
- *
230
- * Same discipline as {@link enqueueCreditDrain}: no network I/O, only the
231
- * durable queue row written through `tx`. A release whose row was lost after
232
- * the sweep committed would understate committed value forever; a lost
233
- * reversal would overstate it, which is the worse direction — the builder
234
- * would see money as theirs that a revived credit can still buy work with.
236
+ * Await durable queue insertion through tx so expiry release or reversal and
237
+ * its report commit together. A missing reversal would overstate builder value.
235
238
  */
236
239
  async enqueueCreditExpiryRelease(tx, payload) {
237
240
  await this.insertPending(tx, "expiry_release", payload);
238
241
  }
239
242
  /**
240
- * Report a reaper-force-failed paid job to the platform (internal-review) so the
243
+ * Report a reaper-force-failed paid job to the platform so the
241
244
  * operator receives an ambient notice that a DVM died or wedged mid-job.
242
245
  * Best-effort and fire-and-forget: unlike {@link report} there is no local
243
246
  * durable queue — a non-2xx or transport failure is structured-logged
@@ -277,7 +280,7 @@ var RevenueReporter = class _RevenueReporter {
277
280
  }
278
281
  }
279
282
  /**
280
- * Report a settled draw whose revenue event was skipped (internal-review).
283
+ * Report a settled draw whose revenue event was skipped.
281
284
  * Best-effort and fire-and-forget: the SDK's structured
282
285
  * `revenue_skipped_no_rail` log is the durable backstop, so a failed notice
283
286
  * must never wedge terminal handling or the orphan-draw reconciler.
@@ -317,7 +320,6 @@ var RevenueReporter = class _RevenueReporter {
317
320
  );
318
321
  }
319
322
  }
320
- /** Report the self-relay's Base gas gauge for platform-side paging and recovery. */
321
323
  async reportX402SelfRelayGasBalance(payload) {
322
324
  const url = `${this.platformUrl}/_internal/x402-self-relay-gas-balance`;
323
325
  try {
@@ -349,7 +351,6 @@ var RevenueReporter = class _RevenueReporter {
349
351
  );
350
352
  }
351
353
  }
352
- /** Report redacted self-relay RPC health for platform-side paging and recovery. */
353
354
  async reportX402SelfRelayRpcHealth(payload) {
354
355
  const url = `${this.platformUrl}/_internal/x402-self-relay-rpc-health`;
355
356
  try {
@@ -381,11 +382,9 @@ var RevenueReporter = class _RevenueReporter {
381
382
  );
382
383
  }
383
384
  }
384
- /** Report the Tempo operator's fee-token gauge for paging and recovery. */
385
385
  async reportTempoSettlementBalance(payload) {
386
386
  await this.postTempoSettlementReadiness("balance", payload);
387
387
  }
388
- /** Report a redacted operator fee rejection immediately. */
389
388
  async reportTempoSettlementFailure(payload) {
390
389
  await this.postTempoSettlementReadiness("failure", payload);
391
390
  }
@@ -422,14 +421,12 @@ var RevenueReporter = class _RevenueReporter {
422
421
  );
423
422
  }
424
423
  }
425
- /** Start the background retry loop. */
426
424
  startRetryLoop() {
427
425
  if (this.retryTimer) return;
428
426
  this.retryTimer = setInterval(() => {
429
427
  void this.drainPending();
430
428
  }, _RevenueReporter.RETRY_INTERVAL_MS);
431
429
  }
432
- /** Stop the background retry loop. */
433
430
  stop() {
434
431
  if (this.retryTimer) {
435
432
  clearInterval(this.retryTimer);
@@ -466,8 +463,8 @@ var RevenueReporter = class _RevenueReporter {
466
463
  /**
467
464
  * Deliver one bounded batch from the durable queue.
468
465
  *
469
- * Public so startup/recovery tests and operator tooling can drive the same
470
- * drain the background timer uses without reaching into private state.
466
+ * Processes due, non-stale rows; failures back off up to five minutes and
467
+ * become terminal after 24 hours. Successful deliveries remove their rows.
471
468
  */
472
469
  async drainPending() {
473
470
  try {
@@ -517,13 +514,11 @@ var RevenueReporter = class _RevenueReporter {
517
514
  console.error("[revenue-reporter] Retry loop error:", err);
518
515
  }
519
516
  }
520
- /** Durably queue one report of either kind, then try it once immediately. */
521
517
  async enqueue(kind, payload, label) {
522
518
  const id = await this.insertPending(this.db, kind, payload);
523
519
  void this.attemptDelivery(id, payload, kind, label).catch(() => {
524
520
  });
525
521
  }
526
- /** Insert one pending report through either the pool or an open transaction. */
527
522
  async insertPending(q, kind, payload) {
528
523
  const id = randomUUID();
529
524
  await q.query(
@@ -575,7 +570,6 @@ var RevenueReporter = class _RevenueReporter {
575
570
  console.warn(`[revenue-reporter] delivery failed for ${id}: HTTP ${res.status} \u2014 will retry`);
576
571
  throw new Error(`Platform returned ${res.status}`);
577
572
  }
578
- /** Mark a pending row terminal (stale) with a classification reason. */
579
573
  async markStale(id, reason) {
580
574
  await this.db.query(
581
575
  `UPDATE ${PENDING_DVM_REPORTS_TABLE} SET stale = true, stale_reason = $1 WHERE id = $2`,