@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
package/README.md CHANGED
@@ -102,6 +102,18 @@ Use the Node.js release in `.nvmrc` (Node 22). Install with `npm ci`, then run t
102
102
 
103
103
  The public surface is exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`.
104
104
 
105
+ When you pass `env` to `serve()` or `createDVMHost()`, that object is the complete host
106
+ environment. SDK configuration, including provenance, builder and owner identity, and
107
+ mint-health defaults, reads only from it. Omit `env` to use `process.env`.
108
+
109
+ ## Terminal evidence
110
+
111
+ New job receipts keep schema version `1` and add optional signed `timing` with `accepted_at_ms`, `terminal_at_ms`, and cumulative `caller_wait_ms`. Acceptance and termination use Unix milliseconds; `issued_at` remains signing time in Unix seconds. Non-completed receipts also include SDK-derived `ended_by`: explicit caller cancellation and unanswered prompt/payment expiry are `caller`; handler failures, worker death, and idle expiry during processing or working are `provider`. Handlers cannot choose this attribution. Legacy receipts omit these fields and still verify. Jobs accepted before this change, including jobs still active during an upgrade, omit the additions and do not produce terminal reports because earlier caller waits are unknown; the SDK does not invent historical fault evidence.
112
+
113
+ Hosted servers with platform URL, token, DVM identity and durable database configured enqueue every terminal job, including free jobs, through the durable report outbox to `/_internal/job-terminal`. The payload contains the DVM/job identity, capability, outcome, attribution, payment-presence flag, timing and optional receipt sequence. It contains no caller identity, payment references or amounts, input, or output. The permanent deduplication claim and outbox insert commit together; startup and periodic recovery cover a crash after terminal persistence. Deliveries can repeat, so the platform deduplicates by DVM/job identity.
114
+
115
+ Deploy the platform endpoint before deploying report-emitting servers. These reports use the existing revenue transport policy: transport/5xx failures retry and abandon after 24 hours; 4xx responses, including 404, become permanently stale. The portable receipt and report contract is in `protocol-fixtures/job-terminal-evidence.json`; compatibility is tested with the pinned published `@dvmkit/sdk@0.1.5-rc.9` verifier.
116
+
105
117
  ## Compatibility and versioning
106
118
 
107
119
  This is pre-1.0 alpha software and the promises match that. Breaking changes can land in any release, they are announced in the release notes for the release that carries them, and there is no long-term support release. Security fixes are made on the latest published release rather than backported.
@@ -10,7 +10,7 @@ import {
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,
@@ -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
@@ -42,8 +42,8 @@ var FxRateUnavailableError = class extends Error {
42
42
  display;
43
43
  hint;
44
44
  /**
45
- * Set only when the platform explicitly refused a *known-stale* snapshot
46
- * (internal-review). Absent means transient/unreachable — a distinction the payment
45
+ * Set only when the platform explicitly refused a *known-stale* snapshot.
46
+ * Absent means transient/unreachable — a distinction the payment
47
47
  * rail's amount computation branches on: refuse on `knownStale`, degrade to
48
48
  * the last-known rate without it. See {@link FxKnownStale}.
49
49
  */
@@ -5,6 +5,7 @@ import {
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,20 @@ 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).
169
+ * Join a credit drain to the transaction that made it terminal.
153
170
  *
154
171
  * Same discipline as {@link enqueueDeposit} and the same reason: no network
155
172
  * I/O here, only the durable queue row, written through `tx` so the report
156
173
  * and the status CAS commit or roll back together. A drain whose row was
157
174
  * lost between the two would overstate outstanding liability forever —
158
175
  * 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}).
162
176
  */
163
177
  async enqueueCreditDrain(tx, payload) {
164
178
  await this.insertPending(tx, "drain", payload);
165
179
  }
166
180
  /**
167
181
  * Join a payout to the transaction that commits the movement it reports
168
- * (internal-review) — the batch close, the accumulator mark, the drain booking.
182
+ * the batch close, the accumulator mark, the drain booking.
169
183
  * No network I/O, only the durable queue row through `tx`, the discipline
170
184
  * {@link enqueueCreditDrain} set and for the same reason: the money has
171
185
  * already moved on a rail this ledger does not own, so the platform row is
@@ -187,7 +201,7 @@ var RevenueReporter = class _RevenueReporter {
187
201
  return this.enqueue("payout", payload, payload.payoutId);
188
202
  }
189
203
  /**
190
- * Report one rail's pending-pool snapshot (internal-review). Best-effort and
204
+ * Report one rail's pending-pool snapshot. Best-effort and
191
205
  * fire-and-forget, deliberately unlike {@link reportPayout}: a snapshot is
192
206
  * replaced by the next tick, so queueing a stale one behind an outage would
193
207
  * only deliver figures the platform already has newer ones for.
@@ -225,7 +239,7 @@ var RevenueReporter = class _RevenueReporter {
225
239
  }
226
240
  /**
227
241
  * Join a credit-expiry release — or the revival that reverses one — to the
228
- * transaction that made it true (internal-review).
242
+ * transaction that made it true.
229
243
  *
230
244
  * Same discipline as {@link enqueueCreditDrain}: no network I/O, only the
231
245
  * durable queue row written through `tx`. A release whose row was lost after
@@ -237,7 +251,7 @@ var RevenueReporter = class _RevenueReporter {
237
251
  await this.insertPending(tx, "expiry_release", payload);
238
252
  }
239
253
  /**
240
- * Report a reaper-force-failed paid job to the platform (internal-review) so the
254
+ * Report a reaper-force-failed paid job to the platform so the
241
255
  * operator receives an ambient notice that a DVM died or wedged mid-job.
242
256
  * Best-effort and fire-and-forget: unlike {@link report} there is no local
243
257
  * durable queue — a non-2xx or transport failure is structured-logged
@@ -277,7 +291,7 @@ var RevenueReporter = class _RevenueReporter {
277
291
  }
278
292
  }
279
293
  /**
280
- * Report a settled draw whose revenue event was skipped (internal-review).
294
+ * Report a settled draw whose revenue event was skipped.
281
295
  * Best-effort and fire-and-forget: the SDK's structured
282
296
  * `revenue_skipped_no_rail` log is the durable backstop, so a failed notice
283
297
  * must never wedge terminal handling or the orphan-draw reconciler.
@@ -317,7 +331,6 @@ var RevenueReporter = class _RevenueReporter {
317
331
  );
318
332
  }
319
333
  }
320
- /** Report the self-relay's Base gas gauge for platform-side paging and recovery. */
321
334
  async reportX402SelfRelayGasBalance(payload) {
322
335
  const url = `${this.platformUrl}/_internal/x402-self-relay-gas-balance`;
323
336
  try {
@@ -349,7 +362,6 @@ var RevenueReporter = class _RevenueReporter {
349
362
  );
350
363
  }
351
364
  }
352
- /** Report redacted self-relay RPC health for platform-side paging and recovery. */
353
365
  async reportX402SelfRelayRpcHealth(payload) {
354
366
  const url = `${this.platformUrl}/_internal/x402-self-relay-rpc-health`;
355
367
  try {
@@ -381,11 +393,9 @@ var RevenueReporter = class _RevenueReporter {
381
393
  );
382
394
  }
383
395
  }
384
- /** Report the Tempo operator's fee-token gauge for paging and recovery. */
385
396
  async reportTempoSettlementBalance(payload) {
386
397
  await this.postTempoSettlementReadiness("balance", payload);
387
398
  }
388
- /** Report a redacted operator fee rejection immediately. */
389
399
  async reportTempoSettlementFailure(payload) {
390
400
  await this.postTempoSettlementReadiness("failure", payload);
391
401
  }
@@ -422,14 +432,12 @@ var RevenueReporter = class _RevenueReporter {
422
432
  );
423
433
  }
424
434
  }
425
- /** Start the background retry loop. */
426
435
  startRetryLoop() {
427
436
  if (this.retryTimer) return;
428
437
  this.retryTimer = setInterval(() => {
429
438
  void this.drainPending();
430
439
  }, _RevenueReporter.RETRY_INTERVAL_MS);
431
440
  }
432
- /** Stop the background retry loop. */
433
441
  stop() {
434
442
  if (this.retryTimer) {
435
443
  clearInterval(this.retryTimer);
@@ -466,8 +474,8 @@ var RevenueReporter = class _RevenueReporter {
466
474
  /**
467
475
  * Deliver one bounded batch from the durable queue.
468
476
  *
469
- * Public so startup/recovery tests and operator tooling can drive the same
470
- * drain the background timer uses without reaching into private state.
477
+ * Processes due, non-stale rows; failures back off up to five minutes and
478
+ * become terminal after 24 hours. Successful deliveries remove their rows.
471
479
  */
472
480
  async drainPending() {
473
481
  try {
@@ -517,13 +525,11 @@ var RevenueReporter = class _RevenueReporter {
517
525
  console.error("[revenue-reporter] Retry loop error:", err);
518
526
  }
519
527
  }
520
- /** Durably queue one report of either kind, then try it once immediately. */
521
528
  async enqueue(kind, payload, label) {
522
529
  const id = await this.insertPending(this.db, kind, payload);
523
530
  void this.attemptDelivery(id, payload, kind, label).catch(() => {
524
531
  });
525
532
  }
526
- /** Insert one pending report through either the pool or an open transaction. */
527
533
  async insertPending(q, kind, payload) {
528
534
  const id = randomUUID();
529
535
  await q.query(
@@ -575,7 +581,6 @@ var RevenueReporter = class _RevenueReporter {
575
581
  console.warn(`[revenue-reporter] delivery failed for ${id}: HTTP ${res.status} \u2014 will retry`);
576
582
  throw new Error(`Platform returned ${res.status}`);
577
583
  }
578
- /** Mark a pending row terminal (stale) with a classification reason. */
579
584
  async markStale(id, reason) {
580
585
  await this.db.query(
581
586
  `UPDATE ${PENDING_DVM_REPORTS_TABLE} SET stale = true, stale_reason = $1 WHERE id = $2`,
@@ -162,13 +162,11 @@ var SpanWriter = class {
162
162
  void this.flush().catch(noop);
163
163
  }
164
164
  }
165
- /** Start periodic flush to the transport. */
166
165
  startFlush() {
167
166
  this.flushTimer = setInterval(() => {
168
167
  void this.flush().catch(noop);
169
168
  }, this.flushIntervalMs);
170
169
  }
171
- /** Stop periodic flush. */
172
170
  stopFlush() {
173
171
  if (this.flushTimer) {
174
172
  clearInterval(this.flushTimer);
@@ -216,12 +214,10 @@ var SpanWriter = class {
216
214
  this.flushing = false;
217
215
  }
218
216
  }
219
- /** Flush remaining spans on shutdown. */
220
217
  async shutdown() {
221
218
  this.stopFlush();
222
219
  await this.flush();
223
220
  }
224
- /** Number of spans dropped due to buffer overflow. */
225
221
  getDropCount() {
226
222
  return this.dropCount;
227
223
  }
@@ -229,7 +225,6 @@ var SpanWriter = class {
229
225
  getTruncatedSpanCount() {
230
226
  return this.truncatedSpanCount;
231
227
  }
232
- /** Current buffer size. */
233
228
  getBufferSize() {
234
229
  return this.buffer.length;
235
230
  }
@@ -28,7 +28,7 @@ var PostgresReplayStore = class {
28
28
  async init() {
29
29
  await withSdkInitLock(this.pool, () => this.createTables());
30
30
  }
31
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
31
+ /** The boot DDL itself — always runs under {@link withSdkInitLock}. */
32
32
  async createTables() {
33
33
  await this.pool.query(`
34
34
  CREATE TABLE IF NOT EXISTS ${this.tableName} (
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  InvalidCurrencyError,
10
10
  validateCurrency
11
- } from "./chunk-27V2ILSR.js";
11
+ } from "./chunk-4A2RAKCW.js";
12
12
  import {
13
13
  parseUsdPrice
14
14
  } from "./chunk-AT6V3SY7.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  StepCache,
3
3
  loggers
4
- } from "./chunk-E4EVGPDX.js";
4
+ } from "./chunk-XQXJKJ3P.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) {
@@ -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,
@@ -454,7 +454,7 @@ function buildMppMethods(opts, env) {
454
454
  // Without this mppx installs `Store.memory()`, so the consumed-hash
455
455
  // guard is per-process and a credential replayed onto a sibling machine
456
456
  // (or after a restart) reaches a broadcast the chain then rejects
457
- // instead of a cheap refusal (internal-review).
457
+ // instead of a cheap refusal.
458
458
  ...opts.tempoCharge && { store: opts.tempoCharge.store }
459
459
  })
460
460
  );
@@ -468,7 +468,7 @@ function buildMppMethods(opts, env) {
468
468
  // A server-driven settle/close must spend fees from the same TIP-20
469
469
  // token the channel carries. Leaving this implicit lets an account's
470
470
  // stale fee preference win even when that token has too little left
471
- // to broadcast, while the channel token is funded (internal-review).
471
+ // to broadcast, while the channel token is funded.
472
472
  feeToken: tempoCurrency,
473
473
  decimals: 6,
474
474
  unitType: "request",
@@ -200,7 +200,6 @@ var PayoutReporter = class {
200
200
  warn("payout_flush_failed", { error: message(err) });
201
201
  }
202
202
  }
203
- // ── cashu ─────────────────────────────────────────────────────────────
204
203
  /** The hook `mark-melted` runs inside its transaction: the mark and its payout commit together. */
205
204
  meltHook() {
206
205
  return {
@@ -262,7 +261,6 @@ var PayoutReporter = class {
262
261
  void this.snapshotCashu();
263
262
  }
264
263
  }
265
- // ── tempo ─────────────────────────────────────────────────────────────
266
264
  /** The host attaches the session store and ledger readers once they exist. */
267
265
  attachTempo(reader) {
268
266
  this.tempo = reader;
@@ -307,7 +305,7 @@ var PayoutReporter = class {
307
305
  };
308
306
  }
309
307
  /**
310
- * The operator repaired a wedged cooperative close (internal-review). The close's
308
+ * The operator repaired a wedged cooperative close. The close's
311
309
  * payee side landed on chain when the close did; if the live hook reported
312
310
  * it, this upgrades that row to `repair`, otherwise it is the row. The
313
311
  * figure is the receipt's payee total for the channel.
@@ -345,7 +343,6 @@ var PayoutReporter = class {
345
343
  void this.snapshotTempo();
346
344
  }
347
345
  }
348
- // ── x402 ──────────────────────────────────────────────────────────────
349
346
  /** The observer the batch-settlement server attaches to and routes its manager through. */
350
347
  x402Observer() {
351
348
  return {
@@ -370,7 +367,7 @@ var PayoutReporter = class {
370
367
  * every settle closes it. The claim delta is read off storage rather than
371
368
  * off upstream's result, which carries only a voucher count; under the
372
369
  * fleet lock the before/after read is consistent. A channel the claim
373
- * removed on its way through — a refund's claim-then-delete (internal-review) —
370
+ * removed on its way through — a refund's claim-then-delete —
374
371
  * moved whatever it still owed before it went, and that value reaches
375
372
  * `pay_to` in this batch too, so it counts at its pre-claim figure.
376
373
  */
@@ -502,7 +499,6 @@ var PayoutReporter = class {
502
499
  warn("payout_batch_track_failed", { scope: ctx.scope, error: message(trackErr) });
503
500
  }
504
501
  }
505
- // ── snapshots ─────────────────────────────────────────────────────────
506
502
  /** Every rail this reporter can read, each on its own failure boundary. */
507
503
  async snapshot() {
508
504
  await this.snapshotCashu();
@@ -639,7 +635,7 @@ var PayoutReporter = class {
639
635
  /**
640
636
  * Tempo: each active channel's spent-but-unsettled balance — earned, not
641
637
  * yet paid to the recipient — and the cooperative closes wedged between
642
- * chain and ledger (internal-review), aged past the same floor the repair queue
638
+ * chain and ledger, aged past the same floor the repair queue
643
639
  * uses so an in-flight close is not reported as stuck.
644
640
  */
645
641
  async snapshotTempo() {
@@ -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
  };
@@ -106,7 +106,7 @@ var PostgresTempoChargeStore = class {
106
106
  client.release();
107
107
  }
108
108
  }
109
- /** The boot DDL itself — always runs under {@link withSdkInitLock} (internal-review). */
109
+ /** The boot DDL itself — always runs under {@link withSdkInitLock}. */
110
110
  async createTables() {
111
111
  await this.pool.query(`
112
112
  CREATE TABLE IF NOT EXISTS ${this.tableName} (
@@ -2,7 +2,7 @@ import {
2
2
  isValidEvmPrivateKey,
3
3
  loadConfig,
4
4
  updateConfig
5
- } from "./chunk-CGKZDODG.js";
5
+ } from "./chunk-MG67KXU7.js";
6
6
  import {
7
7
  DvmError,
8
8
  resolveRpcHttpTransportOptions,
@@ -101,7 +101,7 @@ async function generateTempoKey(opts = {}) {
101
101
  );
102
102
  }
103
103
  if (existing && opts.force) {
104
- const { assertTempoWalletIdle } = await import("./tempo-lifecycle-DFIXQ54Q.js");
104
+ const { assertTempoWalletIdle } = await import("./tempo-lifecycle-DXM7QXJQ.js");
105
105
  await assertTempoWalletIdle("replace");
106
106
  }
107
107
  const { privateKeyToAccount, generatePrivateKey } = await import("viem/accounts");
@@ -155,7 +155,7 @@ async function getTempoUsdcBalance(account) {
155
155
  usdc: Number(raw) / 1e6,
156
156
  tokenAddress,
157
157
  chainId: network.chainId,
158
- // internal-review: redacted here rather than at a display surface, so the clear
158
+ // redacted here rather than at a display surface, so the clear
159
159
  // endpoint never leaves this function on the success path either.
160
160
  rpcUrlRedacted: redactUrl(rpcUrl),
161
161
  rpcSource,