@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.
- package/README.md +12 -0
- package/dist/{chunk-BIP6G74V.js → chunk-2ATUAUAO.js} +8 -8
- package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
- package/dist/{chunk-EVBK675R.js → chunk-6GRIKOFB.js} +28 -23
- package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
- package/dist/{chunk-L4OYF4DQ.js → chunk-FT6HTUM4.js} +1 -1
- package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
- package/dist/{chunk-U6M3ATSG.js → chunk-JDT5LCJC.js} +40 -6
- package/dist/{chunk-FROTD5XQ.js → chunk-JLXYOV4Y.js} +1 -2
- package/dist/{chunk-M7LHFJ5K.js → chunk-KMZXTBLA.js} +2 -2
- package/dist/{chunk-6BQM7TOW.js → chunk-L67WTZX2.js} +3 -7
- package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
- package/dist/{chunk-JZWELPFH.js → chunk-MRAGS5VP.js} +1 -1
- package/dist/{chunk-2UUXIIOC.js → chunk-O2X2CCKH.js} +3 -3
- package/dist/{chunk-TQWGQCNV.js → chunk-OMIQMMME.js} +3 -3
- package/dist/{chunk-UB5FZ43T.js → chunk-PCUQZDZA.js} +475 -366
- package/dist/{chunk-KVEHHC7W.js → chunk-PHHAYRQV.js} +7 -9
- package/dist/{chunk-SSSZUVWM.js → chunk-QP53RWAD.js} +88 -38
- package/dist/{chunk-DMNLFNTW.js → chunk-QT4ONTST.js} +1 -1
- package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
- package/dist/{chunk-MLRCSJYX.js → chunk-V7EVFLAK.js} +87 -90
- package/dist/{chunk-E4EVGPDX.js → chunk-XQXJKJ3P.js} +0 -2
- package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-5ZEJRI46.js} +1 -1
- package/dist/{credit-menu-s5HmGCqx.d.ts → credit-menu-D4Gcdgc4.d.ts} +488 -644
- package/dist/{fx-D860pZvP.d.ts → fx-B0SLBe5x.d.ts} +38 -82
- package/dist/index.d.ts +11 -14
- package/dist/index.js +2 -2
- package/dist/internal/caller.d.ts +618 -1525
- package/dist/internal/caller.js +28 -60
- package/dist/internal/server.d.ts +36 -61
- package/dist/internal/server.js +11 -11
- package/dist/{job-store-BUGqvCfL.d.ts → job-store-B2uZvga4.d.ts} +70 -59
- package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-CQBnQgsT.d.ts} +19 -27
- package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-ZOH6C3N4.js} +2 -2
- package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-IOJBF7DB.js} +1 -1
- package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-5PIRYFVQ.js} +1 -1
- package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-ISRHBMOU.js} +1 -1
- package/dist/{postgres-job-store-3RAXMNSY.js → postgres-job-store-OGQ6IT4U.js} +1 -1
- package/dist/{postgres-kv-store-JFBDP5IP.js → postgres-kv-store-D5E2EZ24.js} +1 -1
- package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-IZFLTTAC.js} +1 -1
- package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
- package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-FIDI3RNH.js} +1 -1
- package/dist/{revenue-reporter-ASZ7SHHH.js → revenue-reporter-NNCNRY4C.js} +1 -1
- package/dist/server/index.d.ts +53 -59
- package/dist/server/index.js +38 -37
- package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
- package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-CXg7ziML.d.ts} +389 -551
- package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-76TDAF34.js} +1 -1
- package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-DXM7QXJQ.js} +3 -3
- package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-O67H5M4N.js} +2 -2
- package/dist/testing/index.d.ts +5 -15
- package/dist/testing/index.js +4 -11
- package/dist/{usd-DoRuAckA.d.ts → usd-BNDg1715.d.ts} +14 -16
- package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
- package/dist/{x402-5H27DCBE.js → x402-7S2EFINY.js} +2 -2
- package/package.json +2 -1
|
@@ -15,14 +15,14 @@ import {
|
|
|
15
15
|
lotOwedSats,
|
|
16
16
|
netOwedSats,
|
|
17
17
|
x402SettlementPending
|
|
18
|
-
} from "./chunk-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
--
|
|
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
|
-
//
|
|
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
|
-
//
|
|
571
|
+
// ledger linkage — INSERT-only for the same reason.
|
|
559
572
|
record.creditId ?? null,
|
|
560
573
|
record.drawId ?? null,
|
|
561
|
-
//
|
|
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
|
-
//
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
|
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
|
-
--
|
|
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
|
-
--
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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}
|
|
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 (
|