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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +12 -0
  2. package/dist/{chunk-BIP6G74V.js → chunk-2ATUAUAO.js} +8 -8
  3. package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
  4. package/dist/{chunk-EVBK675R.js → chunk-6GRIKOFB.js} +28 -23
  5. package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
  6. package/dist/{chunk-L4OYF4DQ.js → chunk-FT6HTUM4.js} +1 -1
  7. package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
  8. package/dist/{chunk-U6M3ATSG.js → chunk-JDT5LCJC.js} +40 -6
  9. package/dist/{chunk-FROTD5XQ.js → chunk-JLXYOV4Y.js} +1 -2
  10. package/dist/{chunk-M7LHFJ5K.js → chunk-KMZXTBLA.js} +2 -2
  11. package/dist/{chunk-6BQM7TOW.js → chunk-L67WTZX2.js} +3 -7
  12. package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
  13. package/dist/{chunk-JZWELPFH.js → chunk-MRAGS5VP.js} +1 -1
  14. package/dist/{chunk-2UUXIIOC.js → chunk-O2X2CCKH.js} +3 -3
  15. package/dist/{chunk-TQWGQCNV.js → chunk-OMIQMMME.js} +3 -3
  16. package/dist/{chunk-UB5FZ43T.js → chunk-PCUQZDZA.js} +475 -366
  17. package/dist/{chunk-KVEHHC7W.js → chunk-PHHAYRQV.js} +7 -9
  18. package/dist/{chunk-SSSZUVWM.js → chunk-QP53RWAD.js} +88 -38
  19. package/dist/{chunk-DMNLFNTW.js → chunk-QT4ONTST.js} +1 -1
  20. package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
  21. package/dist/{chunk-MLRCSJYX.js → chunk-V7EVFLAK.js} +87 -90
  22. package/dist/{chunk-E4EVGPDX.js → chunk-XQXJKJ3P.js} +0 -2
  23. package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-5ZEJRI46.js} +1 -1
  24. package/dist/{credit-menu-s5HmGCqx.d.ts → credit-menu-D4Gcdgc4.d.ts} +488 -644
  25. package/dist/{fx-D860pZvP.d.ts → fx-B0SLBe5x.d.ts} +38 -82
  26. package/dist/index.d.ts +11 -14
  27. package/dist/index.js +2 -2
  28. package/dist/internal/caller.d.ts +618 -1525
  29. package/dist/internal/caller.js +28 -60
  30. package/dist/internal/server.d.ts +36 -61
  31. package/dist/internal/server.js +11 -11
  32. package/dist/{job-store-BUGqvCfL.d.ts → job-store-B2uZvga4.d.ts} +70 -59
  33. package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-CQBnQgsT.d.ts} +19 -27
  34. package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-ZOH6C3N4.js} +2 -2
  35. package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-IOJBF7DB.js} +1 -1
  36. package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-5PIRYFVQ.js} +1 -1
  37. package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-ISRHBMOU.js} +1 -1
  38. package/dist/{postgres-job-store-3RAXMNSY.js → postgres-job-store-OGQ6IT4U.js} +1 -1
  39. package/dist/{postgres-kv-store-JFBDP5IP.js → postgres-kv-store-D5E2EZ24.js} +1 -1
  40. package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-IZFLTTAC.js} +1 -1
  41. package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
  42. package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-FIDI3RNH.js} +1 -1
  43. package/dist/{revenue-reporter-ASZ7SHHH.js → revenue-reporter-NNCNRY4C.js} +1 -1
  44. package/dist/server/index.d.ts +53 -59
  45. package/dist/server/index.js +38 -37
  46. package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
  47. package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-CXg7ziML.d.ts} +389 -551
  48. package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-76TDAF34.js} +1 -1
  49. package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-DXM7QXJQ.js} +3 -3
  50. package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-O67H5M4N.js} +2 -2
  51. package/dist/testing/index.d.ts +5 -15
  52. package/dist/testing/index.js +4 -11
  53. package/dist/{usd-DoRuAckA.d.ts → usd-BNDg1715.d.ts} +14 -16
  54. package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
  55. package/dist/{x402-5H27DCBE.js → x402-7S2EFINY.js} +2 -2
  56. package/package.json +2 -1
@@ -15,14 +15,14 @@ import {
15
15
  lotOwedSats,
16
16
  netOwedSats,
17
17
  x402SettlementPending
18
- } from "./chunk-MLRCSJYX.js";
18
+ } from "./chunk-V7EVFLAK.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,7 @@ 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). */
45
+ /** See {@link CreditLedger.useX402SettlementGate} — same contract. */
46
46
  useX402SettlementGate(gate) {
47
47
  this.x402Settlements = gate;
48
48
  }
@@ -62,12 +62,12 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
62
62
  );
63
63
  return run(() => this.drawSync(args, blocking));
64
64
  }
65
- /** See {@link CreditLedger.blockingX402Refund} — same contract (internal-review). */
65
+ /** See {@link CreditLedger.blockingX402Refund} — same contract. */
66
66
  async blockingX402Refund(channelId, statuses) {
67
67
  if (!channelId || !this.x402Settlements) return void 0;
68
68
  return this.x402Settlements.pendingRefundSettlement(channelId, statuses);
69
69
  }
70
- /** The unresolved x402 refund that must stop an effect on `creditId` (internal-review). */
70
+ /** The unresolved x402 refund that must stop an effect on `creditId`. */
71
71
  blockingRefundForCredit(creditId, statuses) {
72
72
  return this.blockingX402Refund(this.credits.get(creditId)?.x402ChannelId, statuses);
73
73
  }
@@ -540,7 +540,7 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
540
540
  credit.draws.set(args.drawId, draw);
541
541
  return { ...drawResult(draw, credit), replayed: false };
542
542
  }
543
- /** Mid-job top-up growth (internal-review) — see `CreditLedger.growDraw` for the semantics. */
543
+ /** Mid-job top-up growth — see `CreditLedger.growDraw` for the semantics. */
544
544
  growDrawSync(args) {
545
545
  assertAmount(args.addAmountMicro, { min: 1 });
546
546
  const credit = this.requireCredit(args.creditId);
@@ -667,7 +667,7 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
667
667
  const page = after ? out.filter((d) => compareStaleDrawKey(d, after) > 0) : out;
668
668
  return Promise.resolve(page.slice(0, args.limit));
669
669
  }
670
- /** Settled draws' native value for the credit an x402 channel funded (internal-review). */
670
+ /** Settled draws' native value for the credit an x402 channel funded. */
671
671
  earnedNativeForX402Channel(channelId) {
672
672
  for (const credit of this.credits.values()) {
673
673
  if (credit.x402ChannelId !== channelId) continue;
@@ -757,7 +757,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
757
757
  [...this.x402Losses.values()].sort((a, b) => b.observedAt - a.observedAt || a.creditId.localeCompare(b.creditId)).slice(0, args?.limit ?? 50)
758
758
  );
759
759
  }
760
- // ── Drains (internal-review) — semantics mirror the Postgres ledger ─────────
761
760
  async requestDrain(args) {
762
761
  const wedged = await this.blockingRefundForCredit(
763
762
  args.creditId,
@@ -1046,7 +1045,6 @@ var MemoryCreditLedger = class _MemoryCreditLedger {
1046
1045
  if (drain) drain.receipts.push(args.receipt);
1047
1046
  });
1048
1047
  }
1049
- // ── Internals ─────────────────────────────────────────────────────────
1050
1048
  requireDrain(creditId, drainId) {
1051
1049
  const credit = this.credits.get(creditId);
1052
1050
  const drain = credit?.drains.get(drainId);
@@ -139,7 +139,6 @@ var PostgresJobStore = class {
139
139
  async init() {
140
140
  await withSdkInitLock(this.pool, () => this.createTables());
141
141
  }
142
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
143
142
  async createTables() {
144
143
  await this.pool.query(`
145
144
  CREATE TABLE IF NOT EXISTS jobs (
@@ -156,10 +155,10 @@ var PostgresJobStore = class {
156
155
  payment_mint TEXT,
157
156
  received_proofs JSONB NOT NULL DEFAULT '[]',
158
157
  pending_payment_msats BIGINT,
159
- -- Keeps its mpp spelling past internal-review's rail rename (internal-review): it
158
+ -- Keeps its mpp spelling past rail rename: it
160
159
  -- holds mppx challenge ids \u2014 objects of the MPP envelope the Tempo rail
161
160
  -- rides, not the rail key \u2014 and it is server-internal anti-replay state
162
- -- (internal-review) that never reaches a caller.
161
+ -- that never reaches a caller.
163
162
  pending_mpp_challenge_ids JSONB,
164
163
  pending_x402_nonce TEXT,
165
164
  pending_x402_amount_usdc_micro TEXT,
@@ -168,6 +167,12 @@ var PostgresJobStore = class {
168
167
  created_at BIGINT NOT NULL,
169
168
  last_activity_at BIGINT NOT NULL
170
169
  );
170
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS timing_known BOOLEAN NOT NULL DEFAULT false;
171
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS terminal_at_ms BIGINT;
172
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS caller_wait_ms BIGINT NOT NULL DEFAULT 0;
173
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS awaiting_input_at_ms BIGINT;
174
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS ended_by TEXT;
175
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS terminal_report_enqueued BOOLEAN NOT NULL DEFAULT false;
171
176
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS pending_mpp_challenge_ids JSONB;
172
177
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS pending_x402_nonce TEXT;
173
178
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS pending_x402_amount_usdc_micro TEXT;
@@ -178,7 +183,7 @@ var PostgresJobStore = class {
178
183
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS native_amount DOUBLE PRECISION;
179
184
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS native_asset TEXT;
180
185
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cashu_flow TEXT;
181
- -- internal-review: what the job cost the BUILDER to serve, declared by the
186
+ -- what the job cost the BUILDER to serve, declared by the
182
187
  -- handler through ctx.cost(). NULL is "unreported", deliberately
183
188
  -- distinguishable from a declared zero.
184
189
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS cost_amount_micro BIGINT;
@@ -195,7 +200,7 @@ var PostgresJobStore = class {
195
200
  -- It powers StreamableJobStore cross-machine seq allocation; isolate
196
201
  -- DVMs are single-machine and use basic JobStore only.
197
202
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS next_seq INTEGER NOT NULL DEFAULT 1;
198
- -- internal-review: the rail formerly written as 'mpp' is 'tempo'. A live job's
203
+ -- the rail formerly written as 'mpp' is 'tempo'. A live job's
199
204
  -- mid-job payment compares this against the incoming rail, and the
200
205
  -- receipt reports it.
201
206
  UPDATE jobs SET payment_rail = 'tempo' WHERE payment_rail = 'mpp';
@@ -205,41 +210,41 @@ var PostgresJobStore = class {
205
210
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS request_fingerprint TEXT;
206
211
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS requester_pubkey TEXT;
207
212
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS request_id TEXT;
208
- -- internal-review: the route accepted by the live auth gate. Kept outside the
213
+ -- the route accepted by the live auth gate. Kept outside the
209
214
  -- caller-controlled envelope so reactivation cannot derive its expected
210
215
  -- route from the proof it is meant to check.
211
216
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS auth_request_path TEXT;
212
- -- internal-review: the signed receipt, written once at terminal by saveReceipt,
217
+ -- the signed receipt, written once at terminal by saveReceipt,
213
218
  -- plus the per-DVM terminal counter value this job holds. Neither is in
214
219
  -- save's column list \u2014 a stale snapshot must not be able to clear a
215
220
  -- receipt or re-allocate a sequence number.
216
221
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt JSONB;
217
222
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS receipt_seq BIGINT;
218
223
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS redacted_at BIGINT;
219
- -- internal-review: credit-ledger linkage. Written at submission alongside the
224
+ -- credit-ledger linkage. Written at submission alongside the
220
225
  -- payment fields; the terminal funnel settles/releases the draw and the
221
226
  -- receipt countersigns the ReceiptCredit block from it.
222
227
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS credit_id TEXT;
223
228
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS draw_id TEXT;
224
- -- internal-review: the original signed funding artifact and the snapshot served
229
+ -- the original signed funding artifact and the snapshot served
225
230
  -- with a fund-and-draw response. INSERT-only for replay stability.
226
231
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS funding_receipt JSONB;
227
232
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS funding_credit JSONB;
228
- -- internal-review: the outstanding mid-job ask, denominated in fiat micro at the
233
+ -- the outstanding mid-job ask, denominated in fiat micro at the
229
234
  -- instant it was emitted. The top-up's ledger leg funds and grows the
230
235
  -- draw in this figure, so it must not move with the BTC/USD rate between
231
236
  -- the ask and the payment (and the paying machine is often not the one
232
237
  -- that asked, so it has to be durable rather than handler-local).
233
238
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS pending_payment_fiat_micro BIGINT;
234
239
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS pending_payment_fiat_currency TEXT;
235
- -- internal-review: the same figure accumulated over the job's whole life, which
240
+ -- the same figure accumulated over the job's whole life, which
236
241
  -- is what caps mid-job draw growth. It has to be its own column rather
237
242
  -- than a read of the one above: Tx C decrements the outstanding figure as
238
243
  -- payments land, and a cap that shrank with it would let a duplicate
239
244
  -- payment buy the same ask twice.
240
245
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS asked_topup_micro BIGINT;
241
246
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS asked_topup_currency TEXT;
242
- -- internal-review: why the ceiling above is absent, on the jobs where it is. The
247
+ -- why the ceiling above is absent, on the jobs where it is. The
243
248
  -- durable half of the -1 sentinel \u2014 an uncapped job has to be findable by
244
249
  -- query, not by grepping a log line off a machine that may be long gone.
245
250
  ALTER TABLE jobs ADD COLUMN IF NOT EXISTS topup_cap_unenforced_reason TEXT;
@@ -262,7 +267,7 @@ var PostgresJobStore = class {
262
267
  ON jobs (dvm_id, id)
263
268
  WHERE cost_revision IS NOT NULL
264
269
  AND status IN ('completed', 'failed', 'cancelled');
265
- -- internal-review: the idempotent-replay lookup. payment_tx_hash carries the
270
+ -- the idempotent-replay lookup. payment_tx_hash carries the
266
271
  -- caller's X-Cashu-Request-Id on the accumulator path, so a retried paid
267
272
  -- submit finds the job its payment already created.
268
273
  CREATE INDEX IF NOT EXISTS idx_jobs_payment_tx_hash ON jobs (payment_tx_hash);
@@ -294,7 +299,7 @@ var PostgresJobStore = class {
294
299
  );
295
300
  ALTER TABLE job_messages ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'verified';
296
301
 
297
- -- internal-review: per-DVM monotonic receipt counter. Single row by
302
+ -- per-DVM monotonic receipt counter. Single row by
298
303
  -- construction (the CHECK pins the key), and per-DVM by construction
299
304
  -- too \u2014 the SDK runs one database per DVM (see postgres-kv-store.ts).
300
305
  -- Allocated with the same UPDATE \u2026 RETURNING trick as jobs.next_seq so
@@ -313,7 +318,7 @@ var PostgresJobStore = class {
313
318
  return rowToRecord(rows[0]);
314
319
  }
315
320
  /**
316
- * internal-review. Indexed by `idx_jobs_payment_tx_hash`. `paymentTxHash` is unique
321
+ * Indexed by `idx_jobs_payment_tx_hash`. `paymentTxHash` is unique
317
322
  * per settlement in practice — the accumulator's
318
323
  * `UNIQUE(dvm_id, request_id, proof_secret)` is what makes a second job under
319
324
  * the same request id impossible — but order by `created_at` so a
@@ -443,6 +448,7 @@ var PostgresJobStore = class {
443
448
  }
444
449
  }
445
450
  async saveRecord(record, dvmId) {
451
+ const now = Date.now();
446
452
  const { rowCount } = await this.pool.query(
447
453
  `INSERT INTO jobs (
448
454
  id, tags, capability, input, params, requester_id, status, summary,
@@ -454,15 +460,22 @@ var PostgresJobStore = class {
454
460
  last_activity_at, next_seq, requester_token_hash, requester_token,
455
461
  request_fingerprint, requester_pubkey, request_id, credit_id, draw_id,
456
462
  pending_payment_fiat_micro, pending_payment_fiat_currency, auth_request_path,
457
- funding_receipt, funding_credit, dvm_id
458
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45)
463
+ funding_receipt, funding_credit, dvm_id, terminal_at_ms, caller_wait_ms, awaiting_input_at_ms, ended_by, timing_known
464
+ ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,$45,$46,$47,$48,$49,$51)
459
465
  ON CONFLICT (id) DO UPDATE SET
466
+ terminal_at_ms = COALESCE(jobs.terminal_at_ms, EXCLUDED.terminal_at_ms),
467
+ ended_by = CASE WHEN jobs.status IN ('completed', 'failed', 'cancelled') THEN jobs.ended_by ELSE EXCLUDED.ended_by END,
468
+ caller_wait_ms = CASE WHEN jobs.status IN ('completed', 'failed', 'cancelled') THEN jobs.caller_wait_ms
469
+ ELSE jobs.caller_wait_ms + CASE WHEN jobs.status = 'awaiting-input' AND EXCLUDED.status <> 'awaiting-input'
470
+ THEN GREATEST(0, $50 - COALESCE(jobs.awaiting_input_at_ms, jobs.last_activity_at)) ELSE 0 END END,
471
+ awaiting_input_at_ms = CASE WHEN EXCLUDED.status = 'awaiting-input'
472
+ THEN COALESCE(jobs.awaiting_input_at_ms, $50) ELSE NULL END,
460
473
  status = EXCLUDED.status,
461
474
  summary = EXCLUDED.summary,
462
475
  messages = EXCLUDED.messages,
463
476
  -- GREATEST keeps seq monotonic: a stale snapshot from a machine
464
477
  -- whose in-memory job.seq is behind the DB high-water mark must
465
- -- not regress jobs.seq. See internal-review review feedback.
478
+ -- not regress jobs.seq.
466
479
  seq = GREATEST(jobs.seq, EXCLUDED.seq),
467
480
  payment_mint = EXCLUDED.payment_mint,
468
481
  payment_rail = EXCLUDED.payment_rail,
@@ -502,7 +515,7 @@ var PostgresJobStore = class {
502
515
  step_cache = EXCLUDED.step_cache,
503
516
  state = EXCLUDED.state,
504
517
  last_activity_at = EXCLUDED.last_activity_at
505
- -- internal-review: terminal is terminal. On a multi-machine DVM a DELETE can
518
+ -- terminal is terminal. On a multi-machine DVM a DELETE can
506
519
  -- land on a machine that isn't running the handler, which cancels the
507
520
  -- job in the store only; the handler's machine never learns of it and
508
521
  -- would otherwise persist completed over the cancelled row. Once a row is
@@ -547,7 +560,7 @@ var PostgresJobStore = class {
547
560
  record.lastActivityAt,
548
561
  Math.max(record.seq + 1, 1),
549
562
  record.requesterTokenHash ?? null,
550
- // internal-review: submission-time provenance. Like requester_token_hash,
563
+ // submission-time provenance. Like requester_token_hash,
551
564
  // these are INSERT-only — the ON CONFLICT branch above never touches
552
565
  // them, so a later snapshot save can't rewrite what the caller must
553
566
  // reproduce to recover the job.
@@ -555,23 +568,29 @@ var PostgresJobStore = class {
555
568
  record.requestFingerprint ?? null,
556
569
  record.requesterPubkey ?? null,
557
570
  record.requestId ?? null,
558
- // internal-review: ledger linkage — INSERT-only for the same reason.
571
+ // ledger linkage — INSERT-only for the same reason.
559
572
  record.creditId ?? null,
560
573
  record.drawId ?? null,
561
- // internal-review: the pinned mid-job ask. INSERT-only, exactly like its
574
+ // the pinned mid-job ask. INSERT-only, exactly like its
562
575
  // companion `pending_payment_msats` — both are owned by the
563
576
  // transactional paths (`appendOutgoing` sets them, `verifyAndCredit`
564
577
  // clears them), so a stale snapshot must not be able to reset one
565
578
  // while the other stands.
566
579
  record.pendingPaymentFiatMicro ?? null,
567
580
  record.pendingPaymentFiatCurrency ?? null,
568
- // internal-review: submission provenance, INSERT-only with the fingerprint
581
+ // submission provenance, INSERT-only with the fingerprint
569
582
  // and caller identity above. Snapshot saves cannot rewrite the route
570
583
  // whose live gate accepted this proof.
571
584
  record.authRequestPath ?? null,
572
585
  record.fundingReceipt ? JSON.stringify(record.fundingReceipt) : null,
573
586
  record.fundingCredit ? JSON.stringify(record.fundingCredit) : null,
574
- dvmId ?? null
587
+ dvmId ?? null,
588
+ isTerminalStatus(record.status) ? now : null,
589
+ record.callerWaitMs ?? 0,
590
+ record.status === "awaiting-input" ? record.awaitingInputAtMs ?? now : null,
591
+ isTerminalStatus(record.status) && record.status !== "completed" ? record.endedBy ?? "provider" : null,
592
+ now,
593
+ record.timingKnown ?? true
575
594
  ]
576
595
  );
577
596
  if (rowCount === 0) {
@@ -588,6 +607,21 @@ var PostgresJobStore = class {
588
607
  await this.pool.query("DELETE FROM job_messages WHERE job_id = $1", [record.id]).catch(() => void 0);
589
608
  }
590
609
  }
610
+ /** Advance only after the reporter durably enqueued or deduplicated the event. */
611
+ async markTerminalReported(jobId) {
612
+ await this.pool.query("UPDATE jobs SET terminal_report_enqueued = true WHERE id = $1", [jobId]);
613
+ }
614
+ /** Bounded crash recovery for terminal events not yet queued. */
615
+ async listUnreportedTerminalReports(dvmId, capabilities, afterJobId) {
616
+ const { rows } = await this.pool.query(
617
+ `SELECT * FROM jobs WHERE dvm_id = $1 AND terminal_report_enqueued = false AND terminal_at_ms IS NOT NULL AND timing_known = true
618
+ AND status IN ('completed', 'failed', 'cancelled')
619
+ AND ($2::text[] IS NULL OR capability = ANY($2))
620
+ AND ($3::text IS NULL OR id > $3) ORDER BY id LIMIT 100`,
621
+ [dvmId, capabilities, afterJobId ?? null]
622
+ );
623
+ return rows.map(rowToRecord);
624
+ }
591
625
  /**
592
626
  * Read terminal zero-revenue jobs whose latest declared-cost revision has
593
627
  * not yet reached the durable reporter outbox.
@@ -733,16 +767,19 @@ var PostgresJobStore = class {
733
767
  if (rows.length === 0) return void 0;
734
768
  return rows[0].receipt;
735
769
  }
736
- // ── Tx A / Tx B / Tx C (internal-review) ────────────────────────────────────
737
770
  async appendOutgoing(jobId, message, opts) {
738
771
  const client = await this.pool.connect();
739
772
  try {
740
773
  await client.query("BEGIN");
741
774
  const { rows } = await client.query(
742
- `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2
775
+ `UPDATE jobs SET next_seq = next_seq + 1, last_activity_at = $2,
776
+ awaiting_input_at_ms = CASE WHEN $3::boolean AND status NOT IN ('completed', 'failed', 'cancelled')
777
+ THEN COALESCE(awaiting_input_at_ms, $2) ELSE awaiting_input_at_ms END,
778
+ status = CASE WHEN $3::boolean AND status NOT IN ('completed', 'failed', 'cancelled')
779
+ THEN 'awaiting-input' ELSE status END
743
780
  WHERE id = $1 AND redacted_at IS NULL
744
781
  RETURNING (next_seq - 1) AS seq`,
745
- [jobId, Date.now()]
782
+ [jobId, Date.now(), opts?.awaitingInput ?? false]
746
783
  );
747
784
  if (rows.length === 0) {
748
785
  await client.query("ROLLBACK");
@@ -761,7 +798,7 @@ var PostgresJobStore = class {
761
798
  pending_mpp_challenge_ids = $3,
762
799
  pending_x402_nonce = $4,
763
800
  pending_x402_amount_usdc_micro = $5,
764
- -- internal-review: accumulate the fiat ask alongside the msat one when the
801
+ -- accumulate the fiat ask alongside the msat one when the
765
802
  -- currency matches what's already outstanding, replace when it
766
803
  -- doesn't. A NULL micro clears both \u2014 an ask that couldn't be
767
804
  -- priced in fiat must not leave a stale figure the top-up would
@@ -773,7 +810,7 @@ var PostgresJobStore = class {
773
810
  ELSE $6
774
811
  END,
775
812
  pending_payment_fiat_currency = CASE WHEN $6::bigint IS NULL THEN NULL ELSE $7 END,
776
- -- internal-review: the cumulative ask total, the ceiling on this job's
813
+ -- the cumulative ask total, the ceiling on this job's
777
814
  -- draw growth. Same accumulate rule as above, minus the reset \u2014
778
815
  -- it is monotonic by definition. Mirrors accumulateAskedTopUp
779
816
  -- in job-store.ts; edit the two together. -1 is the sticky "can't
@@ -786,12 +823,12 @@ var PostgresJobStore = class {
786
823
  WHEN asked_topup_currency IS NOT DISTINCT FROM $7 THEN asked_topup_micro + $6
787
824
  ELSE -1
788
825
  END,
789
- -- First-write-wins, and internal-review made that load-bearing: this is
826
+ -- First-write-wins because this is
790
827
  -- the job's own ask denomination, read back by ctx.requestPayment
791
828
  -- so every later ask pins in it. The ELSE -1 arm above is the
792
829
  -- backstop for a row an older build wrote, not a live path.
793
830
  asked_topup_currency = COALESCE(asked_topup_currency, $7),
794
- -- internal-review: the sticky reason the ceiling is gone, mirroring the
831
+ -- the sticky reason the ceiling is gone, mirroring the
795
832
  -- sentinel's own stickiness through COALESCE. NULL arms mean "no
796
833
  -- new poison" \u2014 an already-poisoned row keeps its first reason.
797
834
  topup_cap_unenforced_reason = COALESCE(topup_cap_unenforced_reason, CASE
@@ -901,7 +938,7 @@ var PostgresJobStore = class {
901
938
  WHEN $3::boolean THEN NULL
902
939
  ELSE GREATEST(0, COALESCE(pending_payment_msats, 0) - $2)
903
940
  END,
904
- -- internal-review: the pinned fiat ask dies with the msat ask it denominates.
941
+ -- the pinned fiat ask dies with the msat ask it denominates.
905
942
  -- Cleared outright rather than decremented: a partially-paid ask keeps
906
943
  -- its original fiat envelope so the remainder still funds at the rate
907
944
  -- the caller was quoted.
@@ -1031,7 +1068,9 @@ var PostgresJobStore = class {
1031
1068
  }
1032
1069
  async claimForProcessing(jobId) {
1033
1070
  const { rowCount } = await this.pool.query(
1034
- "UPDATE jobs SET status='processing', last_activity_at=$2 WHERE id=$1 AND status='awaiting-input'",
1071
+ `UPDATE jobs SET status='processing', last_activity_at=$2,
1072
+ caller_wait_ms = caller_wait_ms + GREATEST(0, $2 - COALESCE(awaiting_input_at_ms, last_activity_at)),
1073
+ awaiting_input_at_ms = NULL WHERE id=$1 AND status='awaiting-input'`,
1035
1074
  [jobId, Date.now()]
1036
1075
  );
1037
1076
  return rowCount === 1;
@@ -1060,7 +1099,6 @@ var PostgresJobStore = class {
1060
1099
  client.release();
1061
1100
  }
1062
1101
  }
1063
- // ── Streaming ─────────────────────────────────────────────────────────
1064
1102
  async subscribeMessages(jobId, afterSeq, onMessage) {
1065
1103
  const sub = { afterSeq, cb: onMessage };
1066
1104
  let subs = this.messageSubscribers.get(jobId);
@@ -1157,7 +1195,6 @@ var PostgresJobStore = class {
1157
1195
  }
1158
1196
  }).catch(() => void 0);
1159
1197
  }
1160
- // ── Internal ────────────────────────────────────────────────────────
1161
1198
  /**
1162
1199
  * Drive a non-terminal job to `terminalStatus` and append its final `cancel`
1163
1200
  * message in one transaction. Backs both `cancelStaleJob` (which passes an
@@ -1204,6 +1241,11 @@ var PostgresJobStore = class {
1204
1241
  );
1205
1242
  await client.query(
1206
1243
  `UPDATE jobs SET
1244
+ ended_by = CASE WHEN $7::boolean OR status = 'awaiting-input' THEN 'caller' ELSE 'provider' END,
1245
+ terminal_at_ms = $5,
1246
+ caller_wait_ms = caller_wait_ms + CASE WHEN status = 'awaiting-input'
1247
+ THEN GREATEST(0, $5 - COALESCE(awaiting_input_at_ms, last_activity_at)) ELSE 0 END,
1248
+ awaiting_input_at_ms = NULL,
1207
1249
  status = $6,
1208
1250
  summary = COALESCE(summary, $2),
1209
1251
  messages = COALESCE(messages, '[]'::jsonb) || $3::jsonb,
@@ -1211,7 +1253,7 @@ var PostgresJobStore = class {
1211
1253
  next_seq = $4 + 1,
1212
1254
  last_activity_at = $5
1213
1255
  WHERE id = $1`,
1214
- [jobId, reason, JSON.stringify([cancelMsg]), nextSeq, now, terminalStatus]
1256
+ [jobId, reason, JSON.stringify([cancelMsg]), nextSeq, now, terminalStatus, !staleOnly]
1215
1257
  );
1216
1258
  await client.query("SELECT pg_notify('job_messages', $1)", [jobId]);
1217
1259
  await client.query("COMMIT");
@@ -1236,7 +1278,7 @@ var PostgresJobStore = class {
1236
1278
  return rowToCounters(rows[0]);
1237
1279
  }
1238
1280
  /**
1239
- * The credit binding on the job row, read inside Tx C (internal-review). Used on the
1281
+ * The credit binding on the job row, read inside Tx C. Used on the
1240
1282
  * paths that don't get it from a `RETURNING` — the `alreadyVerified` skip and
1241
1283
  * a non-payment inbound.
1242
1284
  */
@@ -1268,7 +1310,7 @@ function rowToRecord(row) {
1268
1310
  return {
1269
1311
  id: row.id,
1270
1312
  tags: row.tags,
1271
- // Pre-internal-review rows persisted before this migration shipped may carry NULL
1313
+ // Earlier rows persisted before this migration shipped may carry NULL
1272
1314
  // here. Fall back to "" so reactivation lookups surface a clear
1273
1315
  // "unknown capability" error rather than crashing on a typed read.
1274
1316
  capability: row.capability ?? "",
@@ -1282,6 +1324,11 @@ function rowToRecord(row) {
1282
1324
  requestId: row.request_id ?? void 0,
1283
1325
  authRequestPath: row.auth_request_path ?? void 0,
1284
1326
  status: row.status,
1327
+ timingKnown: row.timing_known === true,
1328
+ endedBy: row.ended_by ?? void 0,
1329
+ terminalAtMs: row.terminal_at_ms == null ? void 0 : Number(row.terminal_at_ms),
1330
+ callerWaitMs: Number(row.caller_wait_ms ?? 0),
1331
+ awaitingInputAtMs: row.awaiting_input_at_ms == null ? void 0 : Number(row.awaiting_input_at_ms),
1285
1332
  summary: row.summary,
1286
1333
  messages: row.messages,
1287
1334
  seq: row.seq,
@@ -1319,6 +1366,9 @@ function rowToRecord(row) {
1319
1366
  redactedAt: row.redacted_at == null ? void 0 : Number(row.redacted_at)
1320
1367
  };
1321
1368
  }
1369
+ function isTerminalStatus(status) {
1370
+ return status === "completed" || status === "failed" || status === "cancelled";
1371
+ }
1322
1372
 
1323
1373
  export {
1324
1374
  PostgresJobStore
@@ -39,7 +39,7 @@ var PostgresProcessedPaymentStore = class {
39
39
  async init() {
40
40
  await withSdkInitLock(this.pool, () => this.createTables());
41
41
  }
42
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
42
+ /** The boot DDL itself — always runs under {@link withSdkInitLock}. */
43
43
  async createTables() {
44
44
  await this.pool.query(`
45
45
  CREATE TABLE IF NOT EXISTS processed_payments (
@@ -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();