@dvmkit/sdk 0.1.5-rc.9 → 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-5PBOA25N.js → chunk-PCUQZDZA.js} +436 -355
- 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-enwMbn55.d.ts → credit-menu-D4Gcdgc4.d.ts} +487 -643
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryProcessedPaymentStore,
|
|
3
3
|
ProcessedPaymentReplayError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-QT4ONTST.js";
|
|
5
5
|
import {
|
|
6
6
|
createX402FacilitatorAuthHeaders,
|
|
7
7
|
createX402FacilitatorClient,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./chunk-BQ2NMWKE.js";
|
|
11
11
|
import {
|
|
12
12
|
MemoryCreditLedger
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-PHHAYRQV.js";
|
|
14
14
|
import {
|
|
15
15
|
SIGNED_ENVELOPE_FIELDS,
|
|
16
16
|
SignedRequestError,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
localMessage,
|
|
36
36
|
providerMessage,
|
|
37
37
|
toJobRecord
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-JDT5LCJC.js";
|
|
39
39
|
import {
|
|
40
40
|
ADMIN_AUTH_CLOCK_SKEW_SECONDS,
|
|
41
41
|
ADMIN_AUTH_NONCE_TTL_SECONDS,
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
pinDimension,
|
|
101
101
|
withSpan,
|
|
102
102
|
withTraceContext
|
|
103
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-XQXJKJ3P.js";
|
|
104
104
|
import {
|
|
105
105
|
FxRateUnavailableError,
|
|
106
106
|
InvalidCurrencyError,
|
|
@@ -109,7 +109,7 @@ import {
|
|
|
109
109
|
fxRateFor,
|
|
110
110
|
resolveFxSourceFromEnv,
|
|
111
111
|
validateCurrency
|
|
112
|
-
} from "./chunk-
|
|
112
|
+
} from "./chunk-4A2RAKCW.js";
|
|
113
113
|
import {
|
|
114
114
|
fiatToSatsCeil,
|
|
115
115
|
parseUsdPrice,
|
|
@@ -130,7 +130,7 @@ import {
|
|
|
130
130
|
isNonChannelBitcoinRail,
|
|
131
131
|
netOwedSats,
|
|
132
132
|
x402SettlementPending
|
|
133
|
-
} from "./chunk-
|
|
133
|
+
} from "./chunk-V7EVFLAK.js";
|
|
134
134
|
import {
|
|
135
135
|
withSdkInitLock
|
|
136
136
|
} from "./chunk-S3XAHZQY.js";
|
|
@@ -141,7 +141,7 @@ import {
|
|
|
141
141
|
advertisedMethods,
|
|
142
142
|
challengeMeta,
|
|
143
143
|
isTempoInsufficientFundsError
|
|
144
|
-
} from "./chunk-
|
|
144
|
+
} from "./chunk-KMZXTBLA.js";
|
|
145
145
|
|
|
146
146
|
// src/sdk/server/lock-pubkey-store.ts
|
|
147
147
|
async function initLockPubkeyTable(db) {
|
|
@@ -883,7 +883,7 @@ async function buildCreditMenu(args) {
|
|
|
883
883
|
funding,
|
|
884
884
|
// Rendered when either half has something to say: a DVM configured for
|
|
885
885
|
// sessions only would otherwise lose the whole block the moment the gate
|
|
886
|
-
// closed, taking its capability signal with it
|
|
886
|
+
// closed, taking its capability signal with it.
|
|
887
887
|
...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
|
|
888
888
|
tempo: {
|
|
889
889
|
methods: tempoMethods,
|
|
@@ -896,7 +896,7 @@ async function buildCreditMenu(args) {
|
|
|
896
896
|
// are the same misadvertisement in the other direction.
|
|
897
897
|
...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
|
|
898
898
|
};
|
|
899
|
-
if (
|
|
899
|
+
if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
|
|
900
900
|
try {
|
|
901
901
|
const fx = await args.fxFetcher.fetch();
|
|
902
902
|
menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
|
|
@@ -905,7 +905,7 @@ async function buildCreditMenu(args) {
|
|
|
905
905
|
"credit_menu_lightning_floor_unavailable",
|
|
906
906
|
`credit menu degraded: could not express the Lightning funding floor in ${currency}, so the lightning rail is omitted from the funding menu until a rate is available.`
|
|
907
907
|
);
|
|
908
|
-
menu.funding =
|
|
908
|
+
menu.funding = menu.funding.filter((rail) => rail !== "lightning");
|
|
909
909
|
}
|
|
910
910
|
}
|
|
911
911
|
const echo = await selectPrimaryCredit({
|
|
@@ -1950,7 +1950,7 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
1950
1950
|
...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
|
|
1951
1951
|
// Every claim this fleet builds is capped at earned draws — the claim
|
|
1952
1952
|
// timer, `claimAndSettle()`, and the withdraw-race rush claim all route
|
|
1953
|
-
// through this selector
|
|
1953
|
+
// through this selector. A builder-supplied
|
|
1954
1954
|
// `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
|
|
1955
1955
|
// a money invariant, not a policy knob. Upstream's refund job builds its
|
|
1956
1956
|
// bundled claim from storage instead and never consults a selector, which
|
|
@@ -2071,14 +2071,13 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
2071
2071
|
// move money. The voucher base does not: a cooperatively drained and
|
|
2072
2072
|
// reused channel can present the same cumulative maximum again under a
|
|
2073
2073
|
// fresh authorization, which made the second legitimate deposit
|
|
2074
|
-
// collide with the first row
|
|
2074
|
+
// collide with the first row. Conversely, keying by the
|
|
2075
2075
|
// credit effect would let one signed authorization open several rows.
|
|
2076
2076
|
//
|
|
2077
2077
|
// A cooperative refund belongs to one caller drain: after an operator
|
|
2078
2078
|
// writes a failed drain off, the unchanged channel can legitimately
|
|
2079
2079
|
// present the same voucher base under a fresh drain id. Keep that new
|
|
2080
2080
|
// effect out of the old row's `(operation, payment_id)` replay slot
|
|
2081
|
-
// (internal-review).
|
|
2082
2081
|
paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
|
|
2083
2082
|
amount: matched.amount
|
|
2084
2083
|
};
|
|
@@ -3631,7 +3630,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3631
3630
|
// Nothing settled, so the EIP-3009 nonce is unspent and the same
|
|
3632
3631
|
// header is still payable — unlike the post-settlement branch
|
|
3633
3632
|
// below, where saying this would be telling the caller to pay
|
|
3634
|
-
// twice
|
|
3633
|
+
// twice.
|
|
3635
3634
|
hint: `Nothing was settled \u2014 re-sign this request with fund.amount_micro set to expected_micro (${predicted}), re-attach this same X-PAYMENT header, and resubmit.`
|
|
3636
3635
|
}
|
|
3637
3636
|
);
|
|
@@ -3652,7 +3651,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3652
3651
|
x402Requirements
|
|
3653
3652
|
});
|
|
3654
3653
|
}
|
|
3655
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3654
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
3656
3655
|
const receipt = await verifyX402Payment(
|
|
3657
3656
|
x402Payment,
|
|
3658
3657
|
x402Config,
|
|
@@ -3689,7 +3688,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3689
3688
|
`fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
|
|
3690
3689
|
// `expected_micro` states the valuation, but carries no corrective
|
|
3691
3690
|
// hint here: the facilitator already settled, so re-presenting
|
|
3692
|
-
// this credential would be a second payment
|
|
3691
|
+
// this credential would be a second payment.
|
|
3693
3692
|
{ extra: { expected_micro: fundedMicro } }
|
|
3694
3693
|
);
|
|
3695
3694
|
}
|
|
@@ -4195,16 +4194,16 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4195
4194
|
// Only the Cashu accumulator emits this code, and it evaluates each call's
|
|
4196
4195
|
// own proofs — nothing sums a partial with a later one, on any path. So the
|
|
4197
4196
|
// retry owes the full amount: implicit upfront the shortfall was forfeited
|
|
4198
|
-
//
|
|
4199
|
-
//
|
|
4200
|
-
// instead
|
|
4197
|
+
// mid-job it sits on a credit the per-site hint names
|
|
4198
|
+
// Explicit upfront short deposits use `insufficient_credit`
|
|
4199
|
+
// instead. Telling a caller to "send the difference" produced a
|
|
4201
4200
|
// second short pay either way.
|
|
4202
4201
|
payment_insufficient: {
|
|
4203
4202
|
display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
|
|
4204
4203
|
hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
|
|
4205
4204
|
retryable: true
|
|
4206
4205
|
},
|
|
4207
|
-
//
|
|
4206
|
+
// the rail and the ledger leg commit together, but the job row is
|
|
4208
4207
|
// written by a *second* transaction (`verifyAndCredit`). When that one can't
|
|
4209
4208
|
// be made to land, the money is real and the caller's — it just isn't on the
|
|
4210
4209
|
// job. Say so rather than answering a bare 500: the per-site body names the
|
|
@@ -4251,7 +4250,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4251
4250
|
hint: "Use a different prepaid balance or pay per call. To reclaim this balance after running jobs finish, inspect it with 'dvm wallet tempo-exit <dvm>'.",
|
|
4252
4251
|
retryable: false
|
|
4253
4252
|
},
|
|
4254
|
-
//
|
|
4253
|
+
// a refund on this credit's payment channel reached the chain and
|
|
4255
4254
|
// never finished booking, so the escrow behind the balance may already have
|
|
4256
4255
|
// been paid back. Spending it would buy work the service can never collect
|
|
4257
4256
|
// for. The copy must not promise the money is gone *or* that it is safe to
|
|
@@ -4283,7 +4282,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4283
4282
|
hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
|
|
4284
4283
|
retryable: true
|
|
4285
4284
|
},
|
|
4286
|
-
//
|
|
4285
|
+
// a per-rail floor, not the menu-wide minimum — today only the
|
|
4287
4286
|
// lightning rail carries one (the receive channel's routing floor). Refused
|
|
4288
4287
|
// before any invoice is minted, so nothing is ever paid against this code;
|
|
4289
4288
|
// `retryable: false` because re-sending the identical request can never
|
|
@@ -4313,7 +4312,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4313
4312
|
hint: "The balance is zero or fully held by in-flight jobs. Wait for running jobs to finish, then request the drain again with a fresh drain_id.",
|
|
4314
4313
|
retryable: true
|
|
4315
4314
|
},
|
|
4316
|
-
//
|
|
4315
|
+
// Distinct from `nothing_to_drain` because the balance is real and
|
|
4317
4316
|
// waiting changes nothing: a reusable Bitcoin balance is a deposit the
|
|
4318
4317
|
// service owes back in the asset it received, and what is left here converts
|
|
4319
4318
|
// to less than one satoshi at the rate its own funding landed at. There is
|
|
@@ -4345,7 +4344,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4345
4344
|
hint: "Run 'dvm wallet tempo-exit <dvm>' to inspect the channel, request its on-chain exit if needed, and withdraw after the 15-minute grace period.",
|
|
4346
4345
|
retryable: false
|
|
4347
4346
|
},
|
|
4348
|
-
//
|
|
4347
|
+
// The voucher verified and the ledger leg rolled back with the
|
|
4349
4348
|
// failed submission, so nothing was charged and nothing about the payment is
|
|
4350
4349
|
// wrong — which is exactly what the `payment_invalid` this replaced got
|
|
4351
4350
|
// backwards, telling an agent to discard a credential that is still good. The
|
|
@@ -4422,7 +4421,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4422
4421
|
hint: "The token's proofs are pending at the mint; retry in a few seconds.",
|
|
4423
4422
|
retryable: true
|
|
4424
4423
|
},
|
|
4425
|
-
//
|
|
4424
|
+
// Not failures — the two `lightning_*` states below are the normal
|
|
4426
4425
|
// shape of an interactive funding leg, which is why both are retryable and
|
|
4427
4426
|
// neither `display` implies anything went wrong.
|
|
4428
4427
|
lightning_settlement_pending: {
|
|
@@ -4442,7 +4441,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4442
4441
|
},
|
|
4443
4442
|
replay_rejected: {
|
|
4444
4443
|
display: "This payment was already submitted, so it wasn't charged again.",
|
|
4445
|
-
//
|
|
4444
|
+
// this hint reaches a caller whose money already landed — the
|
|
4446
4445
|
// accumulator holds these proofs under this request id. It used to read
|
|
4447
4446
|
// "use a fresh token and a new request id for the next payment", which an
|
|
4448
4447
|
// agent could follow straight into paying twice for the same charge.
|
|
@@ -4586,7 +4585,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
4586
4585
|
snapshot
|
|
4587
4586
|
});
|
|
4588
4587
|
}
|
|
4589
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4588
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
4590
4589
|
const receipt = await verifyX402Payment(
|
|
4591
4590
|
body.content.x402_payment,
|
|
4592
4591
|
opts.x402Config,
|
|
@@ -5003,7 +5002,7 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
5003
5002
|
try {
|
|
5004
5003
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5005
5004
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5006
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5005
|
+
const { verifyX402Payment } = await import("./x402-7S2EFINY.js");
|
|
5007
5006
|
const receipt = await verifyX402Payment(
|
|
5008
5007
|
body.content.x402_payment,
|
|
5009
5008
|
opts.x402Config,
|
|
@@ -5307,7 +5306,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5307
5306
|
// requestId is non-null here because acceptAccumulatorPayment returns
|
|
5308
5307
|
// kind:"rejected" with code "missing_request_id" when unset (handled by
|
|
5309
5308
|
// the rejected branch above). Thread it as the per-call settlement
|
|
5310
|
-
// reference for revenue_events.tx_hash
|
|
5309
|
+
// reference for revenue_events.tx_hash. Unique per
|
|
5311
5310
|
// (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
|
|
5312
5311
|
// it serves as a stable id even though it isn't an on-chain hash.
|
|
5313
5312
|
paymentTxHash: opts.requestId,
|
|
@@ -5456,7 +5455,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
|
|
|
5456
5455
|
amount_micro: priceFiat.amountMicro,
|
|
5457
5456
|
// Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
|
|
5458
5457
|
// the live fetch landed without this currency and it was carried forward
|
|
5459
|
-
// from an earlier one
|
|
5458
|
+
// from an earlier one. The first is the rate source down, the
|
|
5460
5459
|
// second is the rate source having quietly stopped carrying a currency
|
|
5461
5460
|
// this DVM advertises — a config problem, and a different repair.
|
|
5462
5461
|
...denomination.carriedForward === true && { rate_carried_forward: true },
|
|
@@ -5687,7 +5686,7 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
5687
5686
|
creditId: creditId2,
|
|
5688
5687
|
drawId: drawId2,
|
|
5689
5688
|
addAmountMicro: fundedMicro,
|
|
5690
|
-
//
|
|
5689
|
+
// never grow the draw past what the job cumulatively asked.
|
|
5691
5690
|
// The available-balance guard inside `growDraw` cannot do this — the
|
|
5692
5691
|
// `fund` above raised the balance by exactly this amount — so a second
|
|
5693
5692
|
// payment for one ask would otherwise be absorbed and settle as revenue.
|
|
@@ -5800,7 +5799,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5800
5799
|
creditCurrency: funding.currency,
|
|
5801
5800
|
fundedAt: funding.fundedAt,
|
|
5802
5801
|
expiryMs: funding.expiryMs,
|
|
5803
|
-
//
|
|
5802
|
+
// label a session-funded deposit as one, whether or not the chain
|
|
5804
5803
|
// binding below could be read. `rail: "tempo"` alone does not say — a
|
|
5805
5804
|
// `tempo/charge` one-shot funds a credit with no channel at all — so this is
|
|
5806
5805
|
// what lets the platform tell a routine charge from a session whose binding
|
|
@@ -5808,7 +5807,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5808
5807
|
...funding.tempoSession && {
|
|
5809
5808
|
tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
|
|
5810
5809
|
},
|
|
5811
|
-
//
|
|
5810
|
+
// the channel registration rides the deposit report, so it
|
|
5812
5811
|
// commits with the voucher acceptance that created the channel's liability.
|
|
5813
5812
|
...funding.tempoChannel && funding.tempoSession && {
|
|
5814
5813
|
tempoChannel: {
|
|
@@ -6075,7 +6074,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
|
|
|
6075
6074
|
receivedProofs: [],
|
|
6076
6075
|
error: {
|
|
6077
6076
|
// A fixed message plus the reason enum, never `err.message`: that string
|
|
6078
|
-
// is upstream's
|
|
6077
|
+
// is upstream's and may include the relay lock holder's
|
|
6079
6078
|
// backend pid and `client_addr`. The detail is logged where the throw is.
|
|
6080
6079
|
body: paymentErrorBody("x402_settlement_submission_failed", {
|
|
6081
6080
|
message: "x402 batch settlement submission failed",
|
|
@@ -6674,7 +6673,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6674
6673
|
/** Client-generated idempotency key — a retry with the same id credits once. */
|
|
6675
6674
|
fund_id: z.string().min(1),
|
|
6676
6675
|
/**
|
|
6677
|
-
* SHA-256 hex over the raw funding artifact (
|
|
6676
|
+
* SHA-256 hex over the raw funding artifact (the funding-commitment rule).
|
|
6678
6677
|
* Required for every attach rail; absent on `method: "lightning"`, which
|
|
6679
6678
|
* carries no caller-supplied artifact to hash — see {@link CreditRequest}.
|
|
6680
6679
|
*/
|
|
@@ -6698,7 +6697,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6698
6697
|
* rail this build does not carry is answered by `validateDrainTarget`
|
|
6699
6698
|
* with a typed `drain_method_unsupported` naming the rails that do
|
|
6700
6699
|
* work — not by a Zod parse failure that says only "invalid enum". The
|
|
6701
|
-
* caller of a removed rail (`lightning
|
|
6700
|
+
* caller of a removed rail (`lightning`) and the caller of a
|
|
6702
6701
|
* typo deserve the same actionable refusal.
|
|
6703
6702
|
*/
|
|
6704
6703
|
method: z.string().min(1).optional(),
|
|
@@ -6826,12 +6825,12 @@ async function handleBalance(c, deps, args) {
|
|
|
6826
6825
|
return c.json({
|
|
6827
6826
|
op: "balance",
|
|
6828
6827
|
// Expired credits are included: expiry ends spending, never ownership
|
|
6829
|
-
// (
|
|
6828
|
+
// (the caller-ownership contract), so stranded value must stay visible to whoever owns it.
|
|
6830
6829
|
credits: all.map(
|
|
6831
6830
|
(credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
|
|
6832
6831
|
),
|
|
6833
6832
|
...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
|
|
6834
|
-
//
|
|
6833
|
+
// the funding menu the quote and 402 surfaces already advertise,
|
|
6835
6834
|
// echoed here so one read answers "what do I hold, and what may I add".
|
|
6836
6835
|
// Under the key `menu`, not `credit` — that one is the credit projection on
|
|
6837
6836
|
// this route. Carried without the caller echo fields: `credits[]` above says
|
|
@@ -7116,7 +7115,7 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7116
7115
|
expectedChannelId,
|
|
7117
7116
|
// The close intent is re-asserted from inside the verifier, once the
|
|
7118
7117
|
// credential is bound to this credit's own channel and before anything
|
|
7119
|
-
// broadcasts
|
|
7118
|
+
// broadcasts. Run above the call instead and a credential
|
|
7120
7119
|
// signed for another channel touches the reservation on its way to a
|
|
7121
7120
|
// refusal that was never going to close anything.
|
|
7122
7121
|
onBound: async () => {
|
|
@@ -7422,7 +7421,7 @@ async function handleX402ChannelDrain(c, deps, args) {
|
|
|
7422
7421
|
effectId: `${args.credit.creditId}:${args.drainId}`,
|
|
7423
7422
|
// The voucher the caller presents is what actually gets revoked and paid,
|
|
7424
7423
|
// so the settlement layer refuses one signed for any other channel before
|
|
7425
|
-
// it touches anything
|
|
7424
|
+
// it touches anything.
|
|
7426
7425
|
expectedChannelId: channelId,
|
|
7427
7426
|
refundBoundNative,
|
|
7428
7427
|
commit: async (_funding, tx) => {
|
|
@@ -7641,7 +7640,7 @@ function drainBody(record) {
|
|
|
7641
7640
|
status: record.status,
|
|
7642
7641
|
amount_micro: record.amountMicro,
|
|
7643
7642
|
currency: record.currency,
|
|
7644
|
-
// The in-kind obligation, where there is one
|
|
7643
|
+
// The in-kind obligation, where there is one: a Bitcoin
|
|
7645
7644
|
// reclaim returns the deposited sats pro rata, at the rate each funding
|
|
7646
7645
|
// landed at, so this figure was fixed when the reclaim was requested and
|
|
7647
7646
|
// does not move with the market. Published beside the fiat one rather than
|
|
@@ -7664,7 +7663,7 @@ function drainCopy(record) {
|
|
|
7664
7663
|
// Named because it is the obligation, and because it is
|
|
7665
7664
|
// fixed: a Bitcoin refund returns the share of the Bitcoin you
|
|
7666
7665
|
// deposited that you never spent, so it does not move if the
|
|
7667
|
-
// price does before you collect it
|
|
7666
|
+
// price does before you collect it. The mint's own
|
|
7668
7667
|
// redemption charge is named rather than glossed — it is a sat
|
|
7669
7668
|
// or two at a fee-charging mint and nothing at a free one, and
|
|
7670
7669
|
// a figure an agent relays to a human should not be a promise
|
|
@@ -8326,7 +8325,7 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8326
8325
|
},
|
|
8327
8326
|
// Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
|
|
8328
8327
|
// channel rails, and two doors with two answers about one shortfall is the
|
|
8329
|
-
// drift consolidating it was meant to prevent
|
|
8328
|
+
// drift consolidating it was meant to prevent. Tempo answers its
|
|
8330
8329
|
// own through `tempoFloorRefusal`, which reaches the same conclusion a step
|
|
8331
8330
|
// earlier and adds the one rail-specific fact — its channel is already closed.
|
|
8332
8331
|
refund_floor: {
|
|
@@ -8337,13 +8336,13 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8337
8336
|
retryable: false,
|
|
8338
8337
|
// True even though the ledger transaction that would have written the drain
|
|
8339
8338
|
// row rolled back around this refusal. The wedged settlement row is recorded
|
|
8340
|
-
// outside that transaction
|
|
8339
|
+
// outside that transaction and holds this drain id as its
|
|
8341
8340
|
// `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
|
|
8342
8341
|
// drain under — so the caller has to keep it. Dropping it mints a fresh one
|
|
8343
8342
|
// that the wedged-channel gate refuses as `settlement_pending` anyway, and
|
|
8344
8343
|
// re-presenting *this* id is that gate's own documented exemption. Tempo
|
|
8345
8344
|
// reaches the same `true` one artifact earlier, off a `credit_drains` row
|
|
8346
|
-
// its close-intent reservation already committed
|
|
8345
|
+
// its close-intent reservation already committed.
|
|
8347
8346
|
drainRegistered: true
|
|
8348
8347
|
}
|
|
8349
8348
|
};
|
|
@@ -8441,7 +8440,7 @@ function creditBody(credit, reconciliationPending = false) {
|
|
|
8441
8440
|
remaining_micro: credit.availableMicro,
|
|
8442
8441
|
expiry_ms: credit.expiryMs,
|
|
8443
8442
|
expired: credit.expired,
|
|
8444
|
-
// Reclaimable via op "drain"
|
|
8443
|
+
// Reclaimable via op "drain" — true whenever spendable-or-
|
|
8445
8444
|
// expired value remains. Expiry ends spending, never ownership.
|
|
8446
8445
|
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
8447
8446
|
...reconciliationPending && { reconciliation_pending: true },
|
|
@@ -9315,13 +9314,13 @@ function buildContext(job, opts) {
|
|
|
9315
9314
|
if (terminated("prompt")) {
|
|
9316
9315
|
return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
|
|
9317
9316
|
}
|
|
9317
|
+
job.status = "awaiting-input";
|
|
9318
9318
|
providerMessage(job, "prompt", {
|
|
9319
9319
|
id,
|
|
9320
9320
|
text,
|
|
9321
9321
|
...promptOpts?.options && { options: promptOpts.options },
|
|
9322
9322
|
...promptOpts?.schema && { schema: promptOpts.schema }
|
|
9323
9323
|
});
|
|
9324
|
-
job.status = "awaiting-input";
|
|
9325
9324
|
if (job.replayHighSeq !== null) {
|
|
9326
9325
|
const response = job.messages.find(
|
|
9327
9326
|
(m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
|
|
@@ -9435,7 +9434,7 @@ function buildContext(job, opts) {
|
|
|
9435
9434
|
poolBasis: pool.basis,
|
|
9436
9435
|
askCurrency,
|
|
9437
9436
|
// `draw` here means the pool read fine and either the ASK wouldn't pin
|
|
9438
|
-
//
|
|
9437
|
+
// an fx outage on a bare-msats ask, or a sub-micro amount — or it
|
|
9439
9438
|
// pinned in a currency this pool isn't denominated in.
|
|
9440
9439
|
...pool.basis === "draw" && { poolCurrency: pool.currency }
|
|
9441
9440
|
});
|
|
@@ -9570,8 +9569,8 @@ function buildContext(job, opts) {
|
|
|
9570
9569
|
ask_msats: amountMsats
|
|
9571
9570
|
});
|
|
9572
9571
|
}
|
|
9573
|
-
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9574
9572
|
job.status = "awaiting-input";
|
|
9573
|
+
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9575
9574
|
const paidMsatsAtYield = job.paidMsats;
|
|
9576
9575
|
const p = new Promise((resolve, reject) => {
|
|
9577
9576
|
let resolved = false;
|
|
@@ -9580,12 +9579,24 @@ function buildContext(job, opts) {
|
|
|
9580
9579
|
unsubscribeNotify?.();
|
|
9581
9580
|
unsubscribeNotify = void 0;
|
|
9582
9581
|
};
|
|
9583
|
-
const doResolve = (pc) => {
|
|
9582
|
+
const doResolve = async (pc) => {
|
|
9584
9583
|
if (resolved) return;
|
|
9585
9584
|
resolved = true;
|
|
9586
9585
|
cleanup();
|
|
9587
9586
|
creditedToHandlerMsats += amountMsats;
|
|
9588
9587
|
creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
|
|
9588
|
+
if (job.pendingPayment?.resolve === doResolve) {
|
|
9589
|
+
job.pendingPayment = null;
|
|
9590
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
9591
|
+
if (job.status === "awaiting-input" && opts.onYield) {
|
|
9592
|
+
try {
|
|
9593
|
+
await opts.onYield();
|
|
9594
|
+
} catch (err) {
|
|
9595
|
+
reject(err);
|
|
9596
|
+
return;
|
|
9597
|
+
}
|
|
9598
|
+
}
|
|
9599
|
+
}
|
|
9589
9600
|
resolve(pc);
|
|
9590
9601
|
};
|
|
9591
9602
|
const doReject = (err) => {
|
|
@@ -9602,11 +9613,17 @@ function buildContext(job, opts) {
|
|
|
9602
9613
|
if (resolved) return;
|
|
9603
9614
|
try {
|
|
9604
9615
|
const counters = await getCounters();
|
|
9605
|
-
if (!counters) return;
|
|
9616
|
+
if (!counters || resolved) return;
|
|
9606
9617
|
const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
|
|
9607
9618
|
if (satisfied) {
|
|
9619
|
+
const resume = async () => {
|
|
9620
|
+
job.paidMsats = counters.paidMsats;
|
|
9621
|
+
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9622
|
+
await doResolve({ amount_msats: amountMsats });
|
|
9623
|
+
};
|
|
9608
9624
|
if (opts.claimForProcessing) {
|
|
9609
|
-
const won = await opts.claimForProcessing();
|
|
9625
|
+
const won = await opts.claimForProcessing(resume);
|
|
9626
|
+
if (resolved) return;
|
|
9610
9627
|
if (!won) {
|
|
9611
9628
|
resolved = true;
|
|
9612
9629
|
cleanup();
|
|
@@ -9614,10 +9631,7 @@ function buildContext(job, opts) {
|
|
|
9614
9631
|
return;
|
|
9615
9632
|
}
|
|
9616
9633
|
}
|
|
9617
|
-
|
|
9618
|
-
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9619
|
-
if (job.status === "awaiting-input") job.status = "processing";
|
|
9620
|
-
doResolve({ amount_msats: amountMsats });
|
|
9634
|
+
await resume();
|
|
9621
9635
|
}
|
|
9622
9636
|
} catch (err) {
|
|
9623
9637
|
log.error("requestPayment NOTIFY check failed", { error: String(err) });
|
|
@@ -9668,12 +9682,12 @@ function buildContext(job, opts) {
|
|
|
9668
9682
|
runTerminal();
|
|
9669
9683
|
},
|
|
9670
9684
|
// `failOpts.refund` is a caller-fault annotation and nothing more
|
|
9671
|
-
//
|
|
9685
|
+
// No rail hands value backwards: a failed job's debit is the
|
|
9672
9686
|
// credit draw the terminal funnel releases, and the caller reclaims that
|
|
9673
|
-
// balance through the
|
|
9687
|
+
// balance through the `drain` op. The Cashu branch that used to
|
|
9674
9688
|
// send proofs back here was dead — `job.receivedProofs` has been empty on
|
|
9675
9689
|
// every path since the accumulator became the commit boundary for inbound
|
|
9676
|
-
// proofs
|
|
9690
|
+
// proofs — and the accumulator-debit rewrite that would
|
|
9677
9691
|
// have revived it, was cancelled as superseded by the ledger.
|
|
9678
9692
|
fail: (error) => {
|
|
9679
9693
|
if (terminated("fail")) return;
|
|
@@ -9743,13 +9757,17 @@ var ReceiptIssuer = class {
|
|
|
9743
9757
|
}
|
|
9744
9758
|
/**
|
|
9745
9759
|
* Build and sign the receipt for a terminal job. `seq` comes from
|
|
9746
|
-
* `JobStore.claimReceiptSeq
|
|
9760
|
+
* `JobStore.claimReceiptSeq`; `issuedAt` is epoch seconds and defaults to now.
|
|
9747
9761
|
*
|
|
9748
9762
|
* Free jobs get receipts too, with `paid.msats: 0` — they still consume a
|
|
9749
9763
|
* sequence number and still attest an outcome. Whether feedback is
|
|
9750
9764
|
* paid-only is the feedback layer's call, not this one's.
|
|
9751
9765
|
*
|
|
9752
|
-
*
|
|
9766
|
+
* Timing and attribution come from the committed terminal row, not signing
|
|
9767
|
+
* time or handler input. Legacy rows without those durable inputs omit the
|
|
9768
|
+
* optional blocks rather than inventing historical evidence.
|
|
9769
|
+
*
|
|
9770
|
+
* `credit` is the resolved draw block — present on every job
|
|
9753
9771
|
* whose payment funded/drew the ledger, absent otherwise. Signature-
|
|
9754
9772
|
* compatible either way: `canonicalize` omits absent keys.
|
|
9755
9773
|
*/
|
|
@@ -9762,6 +9780,14 @@ var ReceiptIssuer = class {
|
|
|
9762
9780
|
capability: record.capability,
|
|
9763
9781
|
outcome: outcomeFor(record.status),
|
|
9764
9782
|
reason: terminalReason(record),
|
|
9783
|
+
...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
|
|
9784
|
+
timing: {
|
|
9785
|
+
accepted_at_ms: record.createdAt,
|
|
9786
|
+
terminal_at_ms: record.terminalAtMs,
|
|
9787
|
+
caller_wait_ms: record.callerWaitMs ?? 0
|
|
9788
|
+
}
|
|
9789
|
+
},
|
|
9790
|
+
...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
|
|
9765
9791
|
seq,
|
|
9766
9792
|
issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
|
|
9767
9793
|
requester_pubkey: record.requesterPubkey ?? null,
|
|
@@ -9780,7 +9806,7 @@ var ReceiptIssuer = class {
|
|
|
9780
9806
|
return signReceipt(unsigned, this.secret);
|
|
9781
9807
|
}
|
|
9782
9808
|
/**
|
|
9783
|
-
* Countersign one reclaim event
|
|
9809
|
+
* Countersign one reclaim event. Unlike job receipts there is
|
|
9784
9810
|
* no store-allocated sequence — the drain's own `ledger_seq` (taken under
|
|
9785
9811
|
* the credit row lock, shared with draws) already orders it in the
|
|
9786
9812
|
* per-credit evidence chain.
|
|
@@ -10097,12 +10123,16 @@ var JobManager = class {
|
|
|
10097
10123
|
cleanupTimers = /* @__PURE__ */ new Map();
|
|
10098
10124
|
/** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
|
|
10099
10125
|
terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
|
|
10100
|
-
/** Per-job NOTIFY unsubscribe handles for the durable-status watch
|
|
10126
|
+
/** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
|
|
10101
10127
|
statusWatchers = /* @__PURE__ */ new Map();
|
|
10102
|
-
/** Job ids whose durable-status re-read is in flight — coalesces notify storms
|
|
10128
|
+
/** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
|
|
10103
10129
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
10104
|
-
/** Job ids that were notified mid-re-read and must be re-checked
|
|
10130
|
+
/** Job ids that were notified mid-re-read and must be re-checked. */
|
|
10105
10131
|
statusChecksQueued = /* @__PURE__ */ new Set();
|
|
10132
|
+
// Every local input resume holds this queue through its durable wait update.
|
|
10133
|
+
// A prompt response and a payment NOTIFY must not mistake each other for a
|
|
10134
|
+
// different machine's successful claim.
|
|
10135
|
+
inputResumeTails = /* @__PURE__ */ new WeakMap();
|
|
10106
10136
|
sessionId = randomBytes4(3).toString("hex");
|
|
10107
10137
|
nextJobId = 1;
|
|
10108
10138
|
accumulatorMonitor;
|
|
@@ -10110,7 +10140,7 @@ var JobManager = class {
|
|
|
10110
10140
|
processingWatchdogMs;
|
|
10111
10141
|
reactivationClaimGraceMs;
|
|
10112
10142
|
/**
|
|
10113
|
-
* Worker-heartbeat cadence
|
|
10143
|
+
* Worker-heartbeat cadence. Also floors the clock
|
|
10114
10144
|
* guard's compensation on the processing arm — see
|
|
10115
10145
|
* {@link staleSweepCompensationCapMs}.
|
|
10116
10146
|
*/
|
|
@@ -10130,7 +10160,7 @@ var JobManager = class {
|
|
|
10130
10160
|
jobRetentionResumeFrom;
|
|
10131
10161
|
/**
|
|
10132
10162
|
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
10133
|
-
*
|
|
10163
|
+
* Anchored at construction, so a manager already running when
|
|
10134
10164
|
* the clock stepped is covered. The stale-job sweep's two cutoffs, the
|
|
10135
10165
|
* orphan-draw age gate and the reactivation claim grace all read it; see
|
|
10136
10166
|
* {@link createMonotonicClock} for what it costs and where it is clamped.
|
|
@@ -10143,7 +10173,7 @@ var JobManager = class {
|
|
|
10143
10173
|
jobRetentionSweepInFlight = false;
|
|
10144
10174
|
heartbeatInFlight = false;
|
|
10145
10175
|
/**
|
|
10146
|
-
* The denomination of every credit this DVM opens
|
|
10176
|
+
* The denomination of every credit this DVM opens. Resolved once,
|
|
10147
10177
|
* with the same boundary fallback `DVMServer` applies for direct JavaScript
|
|
10148
10178
|
* callers that force an incomplete value past the descriptor contract.
|
|
10149
10179
|
*/
|
|
@@ -10158,7 +10188,7 @@ var JobManager = class {
|
|
|
10158
10188
|
this.jobStore = new MemoryJobStore();
|
|
10159
10189
|
} else {
|
|
10160
10190
|
throw new Error(
|
|
10161
|
-
"JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL).
|
|
10191
|
+
"JobManager requires a persistent jobStore in production. Pass `jobStore` explicitly, set `devMode: true`, or construct via createDVMHost (which boots a PostgresJobStore from DATABASE_URL)."
|
|
10162
10192
|
);
|
|
10163
10193
|
}
|
|
10164
10194
|
if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
|
|
@@ -10166,7 +10196,7 @@ var JobManager = class {
|
|
|
10166
10196
|
JSON.stringify({
|
|
10167
10197
|
level: "warn",
|
|
10168
10198
|
event: "receipts_disabled_store_unsupported",
|
|
10169
|
-
message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods.
|
|
10199
|
+
message: "DVMKIT_RECEIPT_KEY is set but the configured jobStore does not implement ReceiptIssuingStore (claimReceiptSeq / saveReceipt), so no receipts will be issued and /v1/info will not advertise them. Use PostgresJobStore, MemoryJobStore, or implement both methods."
|
|
10170
10200
|
})
|
|
10171
10201
|
);
|
|
10172
10202
|
}
|
|
@@ -10265,7 +10295,7 @@ var JobManager = class {
|
|
|
10265
10295
|
}
|
|
10266
10296
|
}
|
|
10267
10297
|
}
|
|
10268
|
-
if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10298
|
+
if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10269
10299
|
this.terminalCostRecoveryTimer = setInterval(() => {
|
|
10270
10300
|
void this.recoverTerminalCosts().catch((err) => {
|
|
10271
10301
|
console.error(
|
|
@@ -10279,16 +10309,14 @@ var JobManager = class {
|
|
|
10279
10309
|
this.terminalCostRecoveryTimer.unref();
|
|
10280
10310
|
}
|
|
10281
10311
|
}
|
|
10282
|
-
/** Active in-memory jobs. */
|
|
10283
10312
|
get activeJobs() {
|
|
10284
10313
|
return this._activeJobs;
|
|
10285
10314
|
}
|
|
10286
|
-
/** The backing job store. */
|
|
10287
10315
|
get store() {
|
|
10288
10316
|
return this.jobStore;
|
|
10289
10317
|
}
|
|
10290
10318
|
/**
|
|
10291
|
-
* True when this DVM will actually issue receipts
|
|
10319
|
+
* True when this DVM will actually issue receipts — a key *and*
|
|
10292
10320
|
* a store that can allocate sequence numbers and persist bytes write-once.
|
|
10293
10321
|
* `/v1/info#receipts` reads this rather than the key alone, so the flag can
|
|
10294
10322
|
* never promise something `issueReceipt` silently declines to do.
|
|
@@ -10305,7 +10333,7 @@ var JobManager = class {
|
|
|
10305
10333
|
return Object.keys(this.descriptor.capabilities);
|
|
10306
10334
|
}
|
|
10307
10335
|
/**
|
|
10308
|
-
* Resolve a capability's static `price` to msats
|
|
10336
|
+
* Resolve a capability's static `price` to msats.
|
|
10309
10337
|
*
|
|
10310
10338
|
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
10311
10339
|
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
@@ -10318,7 +10346,7 @@ var JobManager = class {
|
|
|
10318
10346
|
}
|
|
10319
10347
|
/**
|
|
10320
10348
|
* Translate a capability's static `price` into the fiat envelope used by
|
|
10321
|
-
* the per-capability `pricing.max` advertised on `/v1/info
|
|
10349
|
+
* the per-capability `pricing.max` advertised on `/v1/info`.
|
|
10322
10350
|
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
10323
10351
|
* names (the route handler validates the name before invoking this).
|
|
10324
10352
|
*/
|
|
@@ -10328,24 +10356,24 @@ var JobManager = class {
|
|
|
10328
10356
|
return resolvePricingMax(cap.price);
|
|
10329
10357
|
}
|
|
10330
10358
|
/**
|
|
10331
|
-
* Parse a request body through a capability's input schema
|
|
10359
|
+
* Parse a request body through a capability's input schema.
|
|
10332
10360
|
*
|
|
10333
10361
|
* Resolution order when a capability declares an `input` schema:
|
|
10334
10362
|
* 1. `body.data` is preferred — agents pass structured fields directly
|
|
10335
|
-
*
|
|
10363
|
+
* (CLI builds this from `--param k=v`).
|
|
10336
10364
|
* 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
|
|
10337
|
-
*
|
|
10365
|
+
* JSON-string contract.
|
|
10338
10366
|
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
10339
|
-
*
|
|
10367
|
+
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
10340
10368
|
*
|
|
10341
10369
|
* When the capability has no `input` schema, returns `body.input` raw
|
|
10342
10370
|
* (primitive path).
|
|
10343
10371
|
*
|
|
10344
|
-
* The
|
|
10345
|
-
* branches
|
|
10372
|
+
* The credit envelope is removed before the schema runs, on both
|
|
10373
|
+
* branches — it rides the signed body but is protocol-level, so a
|
|
10346
10374
|
* top-level `.strict()` capability schema would otherwise reject every
|
|
10347
10375
|
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
10348
|
-
* The persisted `record.input` the
|
|
10376
|
+
* The persisted `record.input` the reactivation path re-reads is the
|
|
10349
10377
|
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
10350
10378
|
*/
|
|
10351
10379
|
parseInput(body, capability) {
|
|
@@ -10363,7 +10391,7 @@ var JobManager = class {
|
|
|
10363
10391
|
throw new MissingStructuredInputError();
|
|
10364
10392
|
}
|
|
10365
10393
|
/**
|
|
10366
|
-
* Look up the per-capability descriptor by name
|
|
10394
|
+
* Look up the per-capability descriptor by name. Throws when the
|
|
10367
10395
|
* name doesn't exist — the route layer validates body.capability first, so
|
|
10368
10396
|
* reaching this with an unknown name is a programmer error.
|
|
10369
10397
|
*/
|
|
@@ -10389,7 +10417,7 @@ var JobManager = class {
|
|
|
10389
10417
|
return this.descriptor.capabilities[name];
|
|
10390
10418
|
}
|
|
10391
10419
|
/**
|
|
10392
|
-
* Allocate the id the next job will be created under
|
|
10420
|
+
* Allocate the id the next job will be created under. The
|
|
10393
10421
|
* submit path calls this BEFORE payment verification so the ledger draw
|
|
10394
10422
|
* commits with its job linkage, then passes the id back through
|
|
10395
10423
|
* `createJob`'s provenance. Ids allocated for requests whose payment is
|
|
@@ -10400,7 +10428,7 @@ var JobManager = class {
|
|
|
10400
10428
|
return `job-${this.sessionId}-${this.nextJobId++}`;
|
|
10401
10429
|
}
|
|
10402
10430
|
/**
|
|
10403
|
-
* Create a new job from a request body. `provenance`
|
|
10431
|
+
* Create a new job from a request body. `provenance` is what the
|
|
10404
10432
|
* idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
|
|
10405
10433
|
* to re-issue, and the fingerprint + caller pubkey a retry must reproduce
|
|
10406
10434
|
* to be given it.
|
|
@@ -10456,27 +10484,23 @@ var JobManager = class {
|
|
|
10456
10484
|
this.watchDurableStatus(job);
|
|
10457
10485
|
return job;
|
|
10458
10486
|
}
|
|
10459
|
-
/** Build an SDKJobContext and attach it to the job. */
|
|
10460
10487
|
buildAndAttachContext(job, parsedInput) {
|
|
10461
10488
|
const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
|
|
10462
10489
|
const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
|
|
10463
10490
|
job.sdkCtx = sdkCtx;
|
|
10464
10491
|
return sdkCtx;
|
|
10465
10492
|
}
|
|
10466
|
-
/** Start the handler for a job. */
|
|
10467
10493
|
runHandler(job, sdkCtx) {
|
|
10468
10494
|
const cap = this.getCapability(job.capability);
|
|
10469
10495
|
const onError = (err) => {
|
|
10470
10496
|
if (isTerminal(job.status)) return;
|
|
10471
10497
|
job.replayProviderSkip = 0;
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
void job.terminalWork;
|
|
10479
|
-
}
|
|
10498
|
+
job.status = "failed";
|
|
10499
|
+
providerMessage(job, "cancel", {
|
|
10500
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
10501
|
+
});
|
|
10502
|
+
job.terminalWork = this.finalizeTerminal(job);
|
|
10503
|
+
void job.terminalWork;
|
|
10480
10504
|
};
|
|
10481
10505
|
if (!cap) {
|
|
10482
10506
|
if (!isTerminal(job.status)) {
|
|
@@ -10504,16 +10528,46 @@ var JobManager = class {
|
|
|
10504
10528
|
onError(err);
|
|
10505
10529
|
}
|
|
10506
10530
|
}
|
|
10507
|
-
/** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
|
|
10508
10531
|
async dispatchMessage(job, type, content) {
|
|
10532
|
+
if (type !== "response" && type !== "payment") {
|
|
10533
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10534
|
+
}
|
|
10535
|
+
const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10536
|
+
await this.queueInputResume(job, async () => {
|
|
10537
|
+
const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10538
|
+
if (pending && current !== pending) return;
|
|
10539
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10540
|
+
});
|
|
10541
|
+
}
|
|
10542
|
+
async queueInputResume(job, resume) {
|
|
10543
|
+
const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
|
|
10544
|
+
const next = previous.catch(() => void 0).then(resume);
|
|
10545
|
+
this.inputResumeTails.set(job, next);
|
|
10546
|
+
try {
|
|
10547
|
+
return await next;
|
|
10548
|
+
} finally {
|
|
10549
|
+
if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
|
|
10550
|
+
}
|
|
10551
|
+
}
|
|
10552
|
+
async claimLocalInput(job) {
|
|
10553
|
+
await job.messageAppenderTail;
|
|
10554
|
+
return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
|
|
10555
|
+
}
|
|
10556
|
+
async dispatchMessageNow(job, type, content) {
|
|
10509
10557
|
const ctx = job.sdkCtx;
|
|
10510
10558
|
const cap = this.getCapability(job.capability);
|
|
10511
10559
|
if (type === "response") {
|
|
10512
10560
|
const rc = content;
|
|
10513
10561
|
const pending = job.pendingPrompts.get(rc.prompt_id);
|
|
10514
10562
|
if (pending) {
|
|
10563
|
+
if (!await this.claimLocalInput(job)) {
|
|
10564
|
+
await this.adoptDurableTerminal(job);
|
|
10565
|
+
return;
|
|
10566
|
+
}
|
|
10567
|
+
if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
|
|
10515
10568
|
job.pendingPrompts.delete(rc.prompt_id);
|
|
10516
|
-
job.status = "processing";
|
|
10569
|
+
job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
|
|
10570
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10517
10571
|
pending.resolve(rc);
|
|
10518
10572
|
} else if (cap?.onResponse && ctx) {
|
|
10519
10573
|
await cap.onResponse(ctx, rc);
|
|
@@ -10522,9 +10576,15 @@ var JobManager = class {
|
|
|
10522
10576
|
const pc = content;
|
|
10523
10577
|
if (job.pendingPayment) {
|
|
10524
10578
|
const pp = job.pendingPayment;
|
|
10579
|
+
if (!await this.claimLocalInput(job)) {
|
|
10580
|
+
await this.adoptDurableTerminal(job);
|
|
10581
|
+
return;
|
|
10582
|
+
}
|
|
10583
|
+
if (job.pendingPayment !== pp || isTerminal(job.status)) return;
|
|
10525
10584
|
job.pendingPayment = null;
|
|
10526
|
-
job.status = "processing";
|
|
10527
|
-
|
|
10585
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
10586
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10587
|
+
await pp.resolve(pc);
|
|
10528
10588
|
} else if (cap?.onPayment && ctx) {
|
|
10529
10589
|
await cap.onPayment(ctx, pc);
|
|
10530
10590
|
}
|
|
@@ -10533,6 +10593,7 @@ var JobManager = class {
|
|
|
10533
10593
|
} else if (type === "cancel") {
|
|
10534
10594
|
const reason = content?.reason ?? "Job cancelled";
|
|
10535
10595
|
abortJob(job, reason);
|
|
10596
|
+
job.endedBy = "caller";
|
|
10536
10597
|
job.status = "cancelled";
|
|
10537
10598
|
if (cap?.onCancel && ctx) {
|
|
10538
10599
|
await cap.onCancel(ctx, content);
|
|
@@ -10551,17 +10612,16 @@ var JobManager = class {
|
|
|
10551
10612
|
});
|
|
10552
10613
|
}
|
|
10553
10614
|
}
|
|
10554
|
-
// ── Persistence ──────────────────────────────────────────────────────
|
|
10555
10615
|
/**
|
|
10556
10616
|
* Persist a job to the backing store.
|
|
10557
10617
|
*
|
|
10558
|
-
* Awaits the per-job appender tail
|
|
10559
|
-
*
|
|
10560
|
-
*
|
|
10561
|
-
*
|
|
10562
|
-
*
|
|
10563
|
-
*
|
|
10564
|
-
*
|
|
10618
|
+
* Awaits the per-job appender tail so that:
|
|
10619
|
+
* 1. The snapshot's `messages` is consistent with the `job_messages` table
|
|
10620
|
+
* no row is still in flight at snapshot time.
|
|
10621
|
+
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
10622
|
+
* can't race a still-pending `appendOutgoing` for the final yield
|
|
10623
|
+
* message (which would otherwise wipe the row before an in-flight SSE
|
|
10624
|
+
* subscriber's NOTIFY-driven fetch can see it).
|
|
10565
10625
|
*/
|
|
10566
10626
|
async persistJob(job) {
|
|
10567
10627
|
if (job.messageAppenderTail) {
|
|
@@ -10582,9 +10642,17 @@ var JobManager = class {
|
|
|
10582
10642
|
} else {
|
|
10583
10643
|
await this.jobStore.save(record);
|
|
10584
10644
|
}
|
|
10645
|
+
const saved = await this.jobStore.get(job.id);
|
|
10646
|
+
if (saved) {
|
|
10647
|
+
job.timingKnown = saved.timingKnown;
|
|
10648
|
+
job.endedBy = saved.endedBy;
|
|
10649
|
+
job.terminalAtMs = saved.terminalAtMs;
|
|
10650
|
+
job.callerWaitMs = saved.callerWaitMs;
|
|
10651
|
+
job.awaitingInputAtMs = saved.awaitingInputAtMs;
|
|
10652
|
+
}
|
|
10585
10653
|
}
|
|
10586
10654
|
/**
|
|
10587
|
-
* Cross-machine terminal guard
|
|
10655
|
+
* Cross-machine terminal guard. `buildContext`'s guard reads the
|
|
10588
10656
|
* in-memory `job.status`, so on its own it only protects the machine running
|
|
10589
10657
|
* the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
|
|
10590
10658
|
* on a machine that isn't running the job — `app.ts` cancels it in the store
|
|
@@ -10596,7 +10664,7 @@ var JobManager = class {
|
|
|
10596
10664
|
* fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
|
|
10597
10665
|
* the durable state won and the caller must skip its save.
|
|
10598
10666
|
*
|
|
10599
|
-
* Two triggers: `watchDurableStatus`'s NOTIFY subscription
|
|
10667
|
+
* Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
|
|
10600
10668
|
* this within a round-trip of the remote cancel committing, which is what
|
|
10601
10669
|
* actually stops the provider spend; `persistJob` calls it again on every save
|
|
10602
10670
|
* as the backstop for the window where a cancel commits between a handler's
|
|
@@ -10604,8 +10672,10 @@ var JobManager = class {
|
|
|
10604
10672
|
* stores' terminal-sticky `save`, which keeps the row itself correct).
|
|
10605
10673
|
*/
|
|
10606
10674
|
async adoptDurableTerminal(job) {
|
|
10607
|
-
const
|
|
10608
|
-
if (!
|
|
10675
|
+
const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
|
|
10676
|
+
if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
|
|
10677
|
+
const durable = await this.jobStore.get(job.id);
|
|
10678
|
+
if (!durable) return false;
|
|
10609
10679
|
console.warn(
|
|
10610
10680
|
JSON.stringify({
|
|
10611
10681
|
level: "job_terminal_elsewhere",
|
|
@@ -10616,6 +10686,11 @@ var JobManager = class {
|
|
|
10616
10686
|
);
|
|
10617
10687
|
job.status = durable.status;
|
|
10618
10688
|
job.summary = durable.summary;
|
|
10689
|
+
job.timingKnown = durable.timingKnown;
|
|
10690
|
+
job.endedBy = durable.endedBy;
|
|
10691
|
+
job.terminalAtMs = durable.terminalAtMs;
|
|
10692
|
+
job.callerWaitMs = durable.callerWaitMs;
|
|
10693
|
+
job.awaitingInputAtMs = durable.awaitingInputAtMs;
|
|
10619
10694
|
const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
|
|
10620
10695
|
for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
|
|
10621
10696
|
job.pendingPayment?.reject(new JobCancelledError(reason));
|
|
@@ -10635,7 +10710,7 @@ var JobManager = class {
|
|
|
10635
10710
|
});
|
|
10636
10711
|
}
|
|
10637
10712
|
this.clearIdleTimer(job.id);
|
|
10638
|
-
void this.issueReceiptForStoredJob(job.id);
|
|
10713
|
+
void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
|
|
10639
10714
|
this.scheduleCleanup(job);
|
|
10640
10715
|
if (durable.status !== "completed" && terminalCostPersisted) {
|
|
10641
10716
|
await this.reportJobCost(job);
|
|
@@ -10681,6 +10756,7 @@ var JobManager = class {
|
|
|
10681
10756
|
async retryTerminalCostHandoff(job, attempt) {
|
|
10682
10757
|
if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
|
|
10683
10758
|
await this.reportJobCost(job);
|
|
10759
|
+
await this.reportTerminalJob(job.id);
|
|
10684
10760
|
}
|
|
10685
10761
|
/** Durably merge a terminal handler owner's latest cost before reporting it. */
|
|
10686
10762
|
async persistAdoptedTerminalCost(job) {
|
|
@@ -10710,7 +10786,7 @@ var JobManager = class {
|
|
|
10710
10786
|
);
|
|
10711
10787
|
}
|
|
10712
10788
|
/**
|
|
10713
|
-
* Watch the durable status of a locally-active job
|
|
10789
|
+
* Watch the durable status of a locally-active job.
|
|
10714
10790
|
*
|
|
10715
10791
|
* `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
|
|
10716
10792
|
* holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
|
|
@@ -10751,7 +10827,7 @@ var JobManager = class {
|
|
|
10751
10827
|
});
|
|
10752
10828
|
}
|
|
10753
10829
|
/**
|
|
10754
|
-
* Tear down a job's durable-status subscription
|
|
10830
|
+
* Tear down a job's durable-status subscription. Must be called
|
|
10755
10831
|
* wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
|
|
10756
10832
|
* the store's shared LISTEN connection. Idempotent.
|
|
10757
10833
|
*/
|
|
@@ -10763,12 +10839,12 @@ var JobManager = class {
|
|
|
10763
10839
|
unsubscribe();
|
|
10764
10840
|
}
|
|
10765
10841
|
/**
|
|
10766
|
-
* NOTIFY-driven durable-status re-read, coalesced per job
|
|
10842
|
+
* NOTIFY-driven durable-status re-read, coalesced per job.
|
|
10767
10843
|
*
|
|
10768
10844
|
* A notify that arrives while a re-read is in flight is queued rather than
|
|
10769
10845
|
* dropped: the in-flight read may have observed the row a moment *before* the
|
|
10770
|
-
* cancel committed, and dropping its notify would
|
|
10771
|
-
*
|
|
10846
|
+
* cancel committed, and dropping its notify would leave the local handler
|
|
10847
|
+
* running until its next store write instead of aborting promptly.
|
|
10772
10848
|
*/
|
|
10773
10849
|
async checkDurableTerminal(job) {
|
|
10774
10850
|
if (this.statusChecksInFlight.has(job.id)) {
|
|
@@ -10798,13 +10874,13 @@ var JobManager = class {
|
|
|
10798
10874
|
/**
|
|
10799
10875
|
* Wire the Tx A appender when the store supports streaming.
|
|
10800
10876
|
*
|
|
10801
|
-
* Tx A
|
|
10877
|
+
* Tx A: outgoing message + `pending_payment_msats` bump land in
|
|
10802
10878
|
* the same transaction. The DB allocates the row's seq via
|
|
10803
10879
|
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
10804
10880
|
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
10805
10881
|
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
10806
10882
|
*
|
|
10807
|
-
* Per-job serialisation
|
|
10883
|
+
* Per-job serialisation: chained through `job.messageAppenderTail`
|
|
10808
10884
|
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
10809
10885
|
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
10810
10886
|
* Without this, the two transactions race for the `jobs` row lock and the
|
|
@@ -10838,7 +10914,7 @@ var JobManager = class {
|
|
|
10838
10914
|
// In-memory counter, NOT the authoritative DB-allocated seq:
|
|
10839
10915
|
// `appendOutgoing` allocates the real seq via `UPDATE ...
|
|
10840
10916
|
// RETURNING` inside the transaction that just failed, so no DB
|
|
10841
|
-
// seq was assigned for this message
|
|
10917
|
+
// seq was assigned for this message.
|
|
10842
10918
|
localSeq: msg.seq,
|
|
10843
10919
|
note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
|
|
10844
10920
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -10849,7 +10925,6 @@ var JobManager = class {
|
|
|
10849
10925
|
job.messageAppenderTail = next;
|
|
10850
10926
|
};
|
|
10851
10927
|
}
|
|
10852
|
-
// ── Idle timeout ─────────────────────────────────────────────────────
|
|
10853
10928
|
/** Start (or restart) the idle timer for a job. */
|
|
10854
10929
|
startIdleTimer(job) {
|
|
10855
10930
|
this.clearIdleTimer(job.id);
|
|
@@ -10867,7 +10942,7 @@ var JobManager = class {
|
|
|
10867
10942
|
this.idleTimers.delete(id);
|
|
10868
10943
|
}
|
|
10869
10944
|
}
|
|
10870
|
-
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard
|
|
10945
|
+
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
|
|
10871
10946
|
async cancelJobIdle(job) {
|
|
10872
10947
|
if (isTerminal(job.status)) return;
|
|
10873
10948
|
const current = await this.jobStore.get(job.id).catch(() => void 0);
|
|
@@ -10886,6 +10961,7 @@ var JobManager = class {
|
|
|
10886
10961
|
if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
|
|
10887
10962
|
job.pendingPrompts.clear();
|
|
10888
10963
|
job.pendingPayment = null;
|
|
10964
|
+
job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
|
|
10889
10965
|
job.status = "cancelled";
|
|
10890
10966
|
abortJob(job, "Job timed out (idle)");
|
|
10891
10967
|
providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
|
|
@@ -10893,14 +10969,13 @@ var JobManager = class {
|
|
|
10893
10969
|
void job.terminalWork;
|
|
10894
10970
|
this.scheduleCleanup(job);
|
|
10895
10971
|
}
|
|
10896
|
-
// ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
|
|
10897
10972
|
/**
|
|
10898
10973
|
* Force-terminate stale non-terminal jobs. Two status-aware arms:
|
|
10899
|
-
*
|
|
10900
|
-
*
|
|
10901
|
-
*
|
|
10902
|
-
*
|
|
10903
|
-
*
|
|
10974
|
+
* • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
|
|
10975
|
+
* (`stale_no_terminal_status`) — the caller never paid / responded.
|
|
10976
|
+
* • `processing`/`working` past `processingWatchdogMs` → `failed`
|
|
10977
|
+
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
10978
|
+
* live workers fresh, so a stale row here means a dead process.
|
|
10904
10979
|
*
|
|
10905
10980
|
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
10906
10981
|
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
@@ -10909,17 +10984,17 @@ var JobManager = class {
|
|
|
10909
10984
|
* descriptors — the local activeJobs teardown only makes sense on the
|
|
10910
10985
|
* JobManager that actually hosted the zombie handler.
|
|
10911
10986
|
*
|
|
10912
|
-
* **Both cutoffs float off the wall clock
|
|
10987
|
+
* **Both cutoffs float off the wall clock.** Each is one
|
|
10913
10988
|
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
10914
10989
|
* different sample at a different moment, so a backwards step between the
|
|
10915
10990
|
* two makes every row look more recently active than it is: the query
|
|
10916
10991
|
* matches nothing and both arms go silent for the length of the skew. The
|
|
10917
10992
|
* dead worker's job keeps its `processing` status, its credit hold stays
|
|
10918
|
-
* `pending`, and the
|
|
10919
|
-
* never fires. Same defect, same host class, as
|
|
10993
|
+
* `pending`, and the paid-job-death alert — wired to this reaper —
|
|
10994
|
+
* never fires. Same defect, same host class, as orphan sweep.
|
|
10920
10995
|
*
|
|
10921
10996
|
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
10922
|
-
* for where each arm's cap comes from — which
|
|
10997
|
+
* for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
|
|
10923
10998
|
* of an unclaimed hold, refused outright for any live job row; ours
|
|
10924
10999
|
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
10925
11000
|
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
@@ -11055,7 +11130,7 @@ var JobManager = class {
|
|
|
11055
11130
|
}
|
|
11056
11131
|
/**
|
|
11057
11132
|
* How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
|
|
11058
|
-
* clock to cover a backwards step
|
|
11133
|
+
* clock to cover a backwards step. The compensation is capped,
|
|
11059
11134
|
* not free: a claimed row must still have been silent for
|
|
11060
11135
|
* `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
|
|
11061
11136
|
* force-fails a job that may well be alive.
|
|
@@ -11075,7 +11150,7 @@ var JobManager = class {
|
|
|
11075
11150
|
* clock stepped. That pair is marginal already; the guard must not make it
|
|
11076
11151
|
* deterministic. The cap collapses to zero there, which is today's
|
|
11077
11152
|
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
11078
|
-
*
|
|
11153
|
+
* nothing heartbeats it by design, and its refresh is an inbound caller
|
|
11079
11154
|
* message on no cadence at all.
|
|
11080
11155
|
*
|
|
11081
11156
|
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
@@ -11094,7 +11169,6 @@ var JobManager = class {
|
|
|
11094
11169
|
if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
|
|
11095
11170
|
return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
|
|
11096
11171
|
}
|
|
11097
|
-
// ── Terminal job content retention ───────────────────────────────────
|
|
11098
11172
|
/**
|
|
11099
11173
|
* Redact terminal job content beyond this DVM's configured window.
|
|
11100
11174
|
*
|
|
@@ -11259,12 +11333,11 @@ var JobManager = class {
|
|
|
11259
11333
|
);
|
|
11260
11334
|
}
|
|
11261
11335
|
}
|
|
11262
|
-
// ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
|
|
11263
11336
|
/**
|
|
11264
11337
|
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
11265
11338
|
* Two arms over one scan, distinguished by whether the draw's job row exists.
|
|
11266
11339
|
*
|
|
11267
|
-
* **No job row
|
|
11340
|
+
* **No job row — release.** allocates the job id and
|
|
11268
11341
|
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
11269
11342
|
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
11270
11343
|
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
@@ -11274,12 +11347,12 @@ var JobManager = class {
|
|
|
11274
11347
|
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
11275
11348
|
* credit, a silent balance shrink for an explicit N>1 one.
|
|
11276
11349
|
*
|
|
11277
|
-
* **Terminal job row
|
|
11350
|
+
* **Terminal job row — reconcile to its outcome.** The funnel
|
|
11278
11351
|
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
11279
11352
|
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
11280
11353
|
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
11281
11354
|
* a different door — and for a `completed` job it strands the platform's
|
|
11282
|
-
* books too, since draws *are* the revenue events
|
|
11355
|
+
* books too, since draws *are* the revenue events: the caller is
|
|
11283
11356
|
* charged nothing, the balance never moves, and outstanding liability
|
|
11284
11357
|
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
11285
11358
|
* the committed draw row, through the ordinary reporter path.
|
|
@@ -11407,8 +11480,8 @@ var JobManager = class {
|
|
|
11407
11480
|
}
|
|
11408
11481
|
}
|
|
11409
11482
|
/**
|
|
11410
|
-
* The end-of-scan line for a tick that resolved nothing
|
|
11411
|
-
*
|
|
11483
|
+
* The end-of-scan line for a tick that resolved nothing. Two shapes are
|
|
11484
|
+
* worth a warning, and neither is the ordinary
|
|
11412
11485
|
* one — the timer runs every 5 minutes per ledger in production.
|
|
11413
11486
|
*
|
|
11414
11487
|
* `examined > 0`: aged candidates were looked at and none moved. Usually
|
|
@@ -11417,7 +11490,7 @@ var JobManager = class {
|
|
|
11417
11490
|
*
|
|
11418
11491
|
* `examined === 0`: the query returned nothing. Ordinarily that means there
|
|
11419
11492
|
* is nothing to do — but it is equally what a blinded scan looks like, which
|
|
11420
|
-
* is the case the
|
|
11493
|
+
* is the case the gate could not reach. Probe for the oldest
|
|
11421
11494
|
* `pending` hold at any age and speak up when one exists the scan should
|
|
11422
11495
|
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
11423
11496
|
* our clock (some other process's clock is fast), or hidden while our own
|
|
@@ -11469,7 +11542,7 @@ var JobManager = class {
|
|
|
11469
11542
|
);
|
|
11470
11543
|
}
|
|
11471
11544
|
/**
|
|
11472
|
-
* Resolve one aged `pending` draw whose job row reached terminal
|
|
11545
|
+
* Resolve one aged `pending` draw whose job row reached terminal,
|
|
11473
11546
|
* mirroring what `resolveCreditDraw` would have done in line. Returns true
|
|
11474
11547
|
* when this call is the one that moved the draw — the caller counts it, and
|
|
11475
11548
|
* only it books.
|
|
@@ -11542,7 +11615,7 @@ var JobManager = class {
|
|
|
11542
11615
|
/**
|
|
11543
11616
|
* Refresh `lastActivityAt` for jobs this process is actively running
|
|
11544
11617
|
* (`processing`/`working`) so the processing watchdog only fires once the
|
|
11545
|
-
* worker is genuinely dead
|
|
11618
|
+
* worker is genuinely dead. `awaiting-input` jobs are
|
|
11546
11619
|
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
11547
11620
|
* heartbeat timer; also callable on demand for tests.
|
|
11548
11621
|
*/
|
|
@@ -11562,17 +11635,16 @@ var JobManager = class {
|
|
|
11562
11635
|
this.heartbeatInFlight = false;
|
|
11563
11636
|
}
|
|
11564
11637
|
}
|
|
11565
|
-
// ── Reactivation (replay) ────────────────────────────────────────────
|
|
11566
11638
|
/**
|
|
11567
|
-
* Single-execution claim for a reactivating machine
|
|
11639
|
+
* Single-execution claim for a reactivating machine. Before
|
|
11568
11640
|
* replaying an `awaiting-input` job, wait a bounded grace window for a live
|
|
11569
11641
|
* original handler to win the `awaiting-input → processing` CAS via its
|
|
11570
11642
|
* NOTIFY wake. Resolves:
|
|
11571
|
-
*
|
|
11572
|
-
*
|
|
11573
|
-
*
|
|
11574
|
-
*
|
|
11575
|
-
*
|
|
11643
|
+
* • `false` — the row left `awaiting-input` during the window (the original
|
|
11644
|
+
* handler claimed it / drove it terminal). Stand down; do not replay.
|
|
11645
|
+
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
11646
|
+
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
11647
|
+
* Replay for dead-worker recovery.
|
|
11576
11648
|
*
|
|
11577
11649
|
* Biasing the original handler to win kills the double execution (double
|
|
11578
11650
|
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
@@ -11595,7 +11667,7 @@ var JobManager = class {
|
|
|
11595
11667
|
}
|
|
11596
11668
|
}
|
|
11597
11669
|
/**
|
|
11598
|
-
* Record an inbound client message durably via Tx B
|
|
11670
|
+
* Record an inbound client message durably via Tx B and append
|
|
11599
11671
|
* it to the in-memory `job.messages` array. Returns the DB-allocated seq
|
|
11600
11672
|
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
11601
11673
|
*
|
|
@@ -11639,13 +11711,13 @@ var JobManager = class {
|
|
|
11639
11711
|
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
11640
11712
|
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
11641
11713
|
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
11642
|
-
* credit
|
|
11714
|
+
* credit. The caller decides whether to run the
|
|
11643
11715
|
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
11644
11716
|
* message to a custom handler (Pattern B).
|
|
11645
11717
|
*
|
|
11646
11718
|
* For DVMs that declare descriptor-level auth, the persisted
|
|
11647
11719
|
* `record.input` carries the original signed envelope. We re-verify the
|
|
11648
|
-
* signature here
|
|
11720
|
+
* signature here so a DB-layer tamper — compromised admin, SQL
|
|
11649
11721
|
* injection, malicious operator with DB access — can't silently feed an
|
|
11650
11722
|
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
11651
11723
|
* and replay store are deliberately skipped: the persisted timestamp is
|
|
@@ -11659,7 +11731,7 @@ var JobManager = class {
|
|
|
11659
11731
|
* What gets re-verified is the persisted row read back through
|
|
11660
11732
|
* {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
|
|
11661
11733
|
* those are the caller's own signed bytes, the same ones `/v1/quote` and the
|
|
11662
|
-
* submit checked
|
|
11734
|
+
* submit checked. That makes the tamper check strictly stronger
|
|
11663
11735
|
* than the parsed form it replaced: an injected key the capability schema
|
|
11664
11736
|
* doesn't declare used to be stripped before the signature was checked, so
|
|
11665
11737
|
* it re-verified clean.
|
|
@@ -11704,7 +11776,7 @@ var JobManager = class {
|
|
|
11704
11776
|
return { job, sdkCtx };
|
|
11705
11777
|
}
|
|
11706
11778
|
/**
|
|
11707
|
-
* Verify an inbound payment and apply the credit via Tx C
|
|
11779
|
+
* Verify an inbound payment and apply the credit via Tx C.
|
|
11708
11780
|
* The inbound message must have been recorded via Tx B already
|
|
11709
11781
|
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
11710
11782
|
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
@@ -11714,12 +11786,12 @@ var JobManager = class {
|
|
|
11714
11786
|
* `pendingPaymentMsats` without external verification. Gated on
|
|
11715
11787
|
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
11716
11788
|
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
11717
|
-
*
|
|
11789
|
+
* plus the explicit `dev_auto` opt-out the dev console uses,
|
|
11718
11790
|
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
11719
11791
|
* any proof riding the same message: money on the wire always takes the rail,
|
|
11720
11792
|
* so the flag can never leave a real token unspent against a credited job.
|
|
11721
11793
|
*
|
|
11722
|
-
* Tx C is not atomic with the rail commit
|
|
11794
|
+
* Tx C is not atomic with the rail commit. `verifyIncomingPayment`
|
|
11723
11795
|
* commits the proofs *and* the ledger leg in one transaction and returns;
|
|
11724
11796
|
* only then does `verifyAndCredit` write the job row, on its own connection.
|
|
11725
11797
|
* Everything after that call therefore runs with the caller's money already
|
|
@@ -11769,7 +11841,7 @@ var JobManager = class {
|
|
|
11769
11841
|
lockPubkeys: extra?.lockPubkeys,
|
|
11770
11842
|
mintHealthTracker: extra?.mintHealthTracker,
|
|
11771
11843
|
fxFetcher: this.opts.fxFetcher,
|
|
11772
|
-
//
|
|
11844
|
+
// the ledger leg — a mid-job payment is a top-up that funds
|
|
11773
11845
|
// the job's credit and grows its draw, atomically with the rail commit.
|
|
11774
11846
|
creditLedger: this.opts.creditLedger,
|
|
11775
11847
|
processedPayments: this.opts.processedPayments,
|
|
@@ -11904,7 +11976,7 @@ var JobManager = class {
|
|
|
11904
11976
|
return { ...unappliedCredit && { unappliedCredit } };
|
|
11905
11977
|
}
|
|
11906
11978
|
/**
|
|
11907
|
-
* Tx C with a bounded retry
|
|
11979
|
+
* Tx C with a bounded retry.
|
|
11908
11980
|
*
|
|
11909
11981
|
* By the time this runs on the verified path, the rail commit and the ledger
|
|
11910
11982
|
* leg have already committed in a transaction this call is not part of — so a
|
|
@@ -11991,7 +12063,7 @@ var JobManager = class {
|
|
|
11991
12063
|
}
|
|
11992
12064
|
/**
|
|
11993
12065
|
* Answer a payment whose rail and ledger legs committed but whose job row
|
|
11994
|
-
* could not be written
|
|
12066
|
+
* could not be written — every retry spent, the money real.
|
|
11995
12067
|
*
|
|
11996
12068
|
* The refusal names the credit the money landed on so the caller can act on
|
|
11997
12069
|
* it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
|
|
@@ -12000,7 +12072,7 @@ var JobManager = class {
|
|
|
12000
12072
|
* fault an operator should see in their 5xx rate. `retryable` is true because
|
|
12001
12073
|
* the ask is genuinely still outstanding.
|
|
12002
12074
|
*
|
|
12003
|
-
* Nothing is released here. The hold is left to the
|
|
12075
|
+
* Nothing is released here. The hold is left to the reconciler,
|
|
12004
12076
|
* which makes the same decision — unbound draw ⇒ release, never settle — but
|
|
12005
12077
|
* under the credit row's `FOR UPDATE`, after the job is terminal, where it
|
|
12006
12078
|
* cannot race an in-flight COMMIT. Releasing from here would also be
|
|
@@ -12067,7 +12139,7 @@ var JobManager = class {
|
|
|
12067
12139
|
}
|
|
12068
12140
|
/**
|
|
12069
12141
|
* Describe a deposit the job row never took, in the shape both the 201 and
|
|
12070
|
-
* the `payment_unapplied` 500 carry
|
|
12142
|
+
* the `payment_unapplied` 500 carry. `undefined` when the payment
|
|
12071
12143
|
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
12072
12144
|
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
12073
12145
|
* 500 takes its words from `unappliedCopy` directly.
|
|
@@ -12081,7 +12153,7 @@ var JobManager = class {
|
|
|
12081
12153
|
...unappliedCopy(reason, creditId)
|
|
12082
12154
|
};
|
|
12083
12155
|
}
|
|
12084
|
-
/** Book a mid-job top-up's funding as a deposit (
|
|
12156
|
+
/** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
|
|
12085
12157
|
reportCreditDeposit(funding) {
|
|
12086
12158
|
const onCreditFunded = this.opts.onCreditFunded;
|
|
12087
12159
|
if (!funding || !onCreditFunded || !this.opts.dvmId) return;
|
|
@@ -12094,7 +12166,7 @@ var JobManager = class {
|
|
|
12094
12166
|
/**
|
|
12095
12167
|
* Internal: build the BuildContextOpts shared between `createJob` and
|
|
12096
12168
|
* `reactivateJob`. Centralised so persistence/terminal callbacks and the
|
|
12097
|
-
*
|
|
12169
|
+
* NOTIFY-driven cross-machine wake stay in one place.
|
|
12098
12170
|
*/
|
|
12099
12171
|
buildContextOpts(job) {
|
|
12100
12172
|
const jobStore = this.jobStore;
|
|
@@ -12113,7 +12185,16 @@ var JobManager = class {
|
|
|
12113
12185
|
getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
|
|
12114
12186
|
drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
|
|
12115
12187
|
pricingCurrency: this.pricingCurrency,
|
|
12116
|
-
claimForProcessing: isStreamableJobStore(jobStore) ? () =>
|
|
12188
|
+
claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
|
|
12189
|
+
const pending = job.pendingPayment;
|
|
12190
|
+
return this.queueInputResume(job, async () => {
|
|
12191
|
+
if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12192
|
+
if (!await this.claimLocalInput(job)) return false;
|
|
12193
|
+
if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12194
|
+
await resume?.();
|
|
12195
|
+
return true;
|
|
12196
|
+
});
|
|
12197
|
+
} : void 0,
|
|
12117
12198
|
onSuperseded: () => {
|
|
12118
12199
|
this.clearIdleTimer(job.id);
|
|
12119
12200
|
this.unwatchDurableStatus(job.id);
|
|
@@ -12124,7 +12205,7 @@ var JobManager = class {
|
|
|
12124
12205
|
}
|
|
12125
12206
|
/**
|
|
12126
12207
|
* Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
|
|
12127
|
-
* job's remaining pool in
|
|
12208
|
+
* job's remaining pool in.
|
|
12128
12209
|
*
|
|
12129
12210
|
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
12130
12211
|
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
@@ -12142,8 +12223,7 @@ var JobManager = class {
|
|
|
12142
12223
|
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
12143
12224
|
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
12144
12225
|
* fall back on is a slice of the credit's rail value at a ratio pinned
|
|
12145
|
-
* whenever that credit was funded,
|
|
12146
|
-
* to stop spending against.
|
|
12226
|
+
* whenever that credit was funded, so it cannot safely fund a new ask.
|
|
12147
12227
|
*/
|
|
12148
12228
|
async resolveDrawPool(job) {
|
|
12149
12229
|
const ledger = this.opts.creditLedger;
|
|
@@ -12192,7 +12272,7 @@ var JobManager = class {
|
|
|
12192
12272
|
}
|
|
12193
12273
|
/**
|
|
12194
12274
|
* Issue the signed receipt for a job that has reached a terminal status
|
|
12195
|
-
*
|
|
12275
|
+
* Idempotent and safe to call from every terminal path — the
|
|
12196
12276
|
* store owns both the sequence allocation and the write-once persist, so
|
|
12197
12277
|
* two machines racing on the same job converge on identical bytes and burn
|
|
12198
12278
|
* exactly one sequence number.
|
|
@@ -12277,10 +12357,10 @@ var JobManager = class {
|
|
|
12277
12357
|
};
|
|
12278
12358
|
}
|
|
12279
12359
|
/**
|
|
12280
|
-
* Settle or release a terminal job's credit draw
|
|
12360
|
+
* Settle or release a terminal job's credit draw: `completed`
|
|
12281
12361
|
* settles (the hold becomes a real debit); `failed`/`cancelled` releases
|
|
12282
12362
|
* (the hold evaporates — "no debit on job failure", mechanically
|
|
12283
|
-
* superseding the
|
|
12363
|
+
* superseding the 974 no-op refund for credit-paid jobs, including
|
|
12284
12364
|
* the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
|
|
12285
12365
|
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
12286
12366
|
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
@@ -12326,7 +12406,7 @@ var JobManager = class {
|
|
|
12326
12406
|
}
|
|
12327
12407
|
}
|
|
12328
12408
|
/**
|
|
12329
|
-
* The one place a terminal job's status becomes a ledger verb
|
|
12409
|
+
* The one place a terminal job's status becomes a ledger verb:
|
|
12330
12410
|
* `completed` settles the hold into a real debit, `failed`/`cancelled`
|
|
12331
12411
|
* release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
|
|
12332
12412
|
* late reconciler (`reconcileTerminalDraw`) — two paths that must never
|
|
@@ -12341,45 +12421,47 @@ var JobManager = class {
|
|
|
12341
12421
|
* cleanup window hand back the same bytes as a cross-machine re-read.
|
|
12342
12422
|
*/
|
|
12343
12423
|
async attachReceipt(job) {
|
|
12344
|
-
const
|
|
12424
|
+
const record = await this.jobStore.get(job.id);
|
|
12425
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12426
|
+
const receipt = await this.issueReceipt(record);
|
|
12345
12427
|
if (receipt) job.receipt = receipt;
|
|
12346
12428
|
}
|
|
12347
12429
|
/**
|
|
12348
|
-
* Read-path repair for a terminal job carrying no receipt
|
|
12430
|
+
* Read-path repair for a terminal job carrying no receipt, for
|
|
12349
12431
|
* either an in-memory job or a store record. Free in the steady state — it
|
|
12350
12432
|
* returns on the `receipt` check without touching the store — so it costs
|
|
12351
12433
|
* only on the cases it exists for:
|
|
12352
12434
|
*
|
|
12353
12435
|
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
12354
|
-
*
|
|
12355
|
-
*
|
|
12356
|
-
*
|
|
12357
|
-
*
|
|
12358
|
-
*
|
|
12359
|
-
*
|
|
12436
|
+
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
12437
|
+
* process death in that window would otherwise strand that number
|
|
12438
|
+
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
12439
|
+
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
12440
|
+
* committed number (the claim is idempotent per job) rather than
|
|
12441
|
+
* allocating a second one.
|
|
12360
12442
|
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
12361
|
-
*
|
|
12362
|
-
*
|
|
12443
|
+
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
12444
|
+
* so the next read retries.
|
|
12363
12445
|
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
12364
|
-
*
|
|
12446
|
+
* up on first read, with `issued_at` reflecting when it was signed.
|
|
12365
12447
|
*/
|
|
12366
12448
|
async ensureReceipt(target) {
|
|
12367
|
-
if (target.receipt)
|
|
12368
|
-
|
|
12449
|
+
if (target.receipt) {
|
|
12450
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12451
|
+
return;
|
|
12452
|
+
}
|
|
12453
|
+
const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
|
|
12454
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12369
12455
|
const receipt = await this.issueReceipt(record);
|
|
12370
12456
|
if (receipt) target.receipt = receipt;
|
|
12457
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12371
12458
|
}
|
|
12372
|
-
/**
|
|
12373
|
-
* Close out a terminal job whose caller hasn't already persisted it: sign
|
|
12374
|
-
* the receipt, then save the snapshot. The snapshot save never writes the
|
|
12375
|
-
* receipt column, so ordering only affects how soon a reader sees the
|
|
12376
|
-
* receipt — this way a caller polling immediately after the terminal
|
|
12377
|
-
* already finds it.
|
|
12378
|
-
*/
|
|
12459
|
+
/** Commit the terminal snapshot before resolving money and signing its durable evidence. */
|
|
12379
12460
|
async finalizeTerminal(job) {
|
|
12461
|
+
await this.persistJob(job);
|
|
12380
12462
|
await this.attachReceipt(job);
|
|
12381
|
-
await this.persistJob(job).catch(() => void 0);
|
|
12382
12463
|
await this.reportJobCost(job);
|
|
12464
|
+
await this.reportTerminalJob(job.id);
|
|
12383
12465
|
}
|
|
12384
12466
|
/**
|
|
12385
12467
|
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
@@ -12398,20 +12480,69 @@ var JobManager = class {
|
|
|
12398
12480
|
}
|
|
12399
12481
|
/**
|
|
12400
12482
|
* Finish accounting for a terminal written directly to the store — the
|
|
12401
|
-
* stale reaper and a cancel handled on a different machine
|
|
12483
|
+
* stale reaper and a cancel handled on a different machine.
|
|
12402
12484
|
*/
|
|
12403
12485
|
async finalizeStoredTerminal(jobId) {
|
|
12404
12486
|
const receipt = await this.issueReceiptForStoredJob(jobId);
|
|
12405
12487
|
const record = await this.jobStore.get(jobId).catch(() => void 0);
|
|
12406
|
-
if (record && isTerminal(record.status))
|
|
12488
|
+
if (record && isTerminal(record.status)) {
|
|
12489
|
+
await this.reportJobCost(record);
|
|
12490
|
+
await this.reportTerminalJob(jobId);
|
|
12491
|
+
}
|
|
12407
12492
|
return receipt;
|
|
12408
12493
|
}
|
|
12494
|
+
async reportTerminalJob(jobId) {
|
|
12495
|
+
try {
|
|
12496
|
+
const callback = this.opts.onJobTerminal;
|
|
12497
|
+
const dvmId = this.opts.dvmId;
|
|
12498
|
+
if (!callback || !dvmId) return;
|
|
12499
|
+
const record = await this.jobStore.get(jobId);
|
|
12500
|
+
if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
|
|
12501
|
+
const receipt = await this.issueReceipt(record);
|
|
12502
|
+
if (this.opts.receiptIssuer && !receipt) return;
|
|
12503
|
+
if (record.creditId && record.drawId && this.opts.creditLedger) {
|
|
12504
|
+
const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
|
|
12505
|
+
if (!draw || draw.status === "pending") return;
|
|
12506
|
+
}
|
|
12507
|
+
await callback({
|
|
12508
|
+
dvmId,
|
|
12509
|
+
jobId: record.id,
|
|
12510
|
+
capability: record.capability,
|
|
12511
|
+
outcome: record.status,
|
|
12512
|
+
endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
|
|
12513
|
+
paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
|
|
12514
|
+
acceptedAtMs: record.createdAt,
|
|
12515
|
+
terminalAtMs: record.terminalAtMs,
|
|
12516
|
+
callerWaitMs: record.callerWaitMs ?? 0,
|
|
12517
|
+
receiptSeq: receipt?.seq ?? null
|
|
12518
|
+
});
|
|
12519
|
+
if ("markTerminalReported" in this.jobStore) {
|
|
12520
|
+
await this.jobStore.markTerminalReported(jobId);
|
|
12521
|
+
}
|
|
12522
|
+
} catch (error) {
|
|
12523
|
+
console.error(`[${jobId}] Terminal reporting failed:`, error);
|
|
12524
|
+
}
|
|
12525
|
+
}
|
|
12526
|
+
terminalReportRecoveryCursor;
|
|
12527
|
+
async recoverTerminalReports() {
|
|
12528
|
+
const store = this.jobStore;
|
|
12529
|
+
const dvmId = this.opts.dvmId;
|
|
12530
|
+
if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
|
|
12531
|
+
const recovery = store;
|
|
12532
|
+
const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
|
|
12533
|
+
for (const record of records) await this.reportTerminalJob(record.id);
|
|
12534
|
+
this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
|
|
12535
|
+
}
|
|
12409
12536
|
/** Handle job reaching terminal state (completed, failed, cancelled). */
|
|
12410
12537
|
async handleJobTerminal(job) {
|
|
12411
12538
|
this.clearIdleTimer(job.id);
|
|
12539
|
+
if (await this.adoptDurableTerminal(job)) return;
|
|
12540
|
+
const durable = await this.jobStore.get(job.id);
|
|
12541
|
+
if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
|
|
12412
12542
|
await this.attachReceipt(job);
|
|
12413
12543
|
const booked = await this.bookRevenue(job);
|
|
12414
12544
|
if (!booked) await this.reportJobCost(job);
|
|
12545
|
+
await this.reportTerminalJob(job.id);
|
|
12415
12546
|
this.scheduleCleanup(job);
|
|
12416
12547
|
}
|
|
12417
12548
|
/**
|
|
@@ -12424,6 +12555,7 @@ var JobManager = class {
|
|
|
12424
12555
|
async recoverTerminalCosts() {
|
|
12425
12556
|
const store = this.jobStore;
|
|
12426
12557
|
const dvmId = this.opts.dvmId;
|
|
12558
|
+
await this.recoverTerminalReports();
|
|
12427
12559
|
if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
|
|
12428
12560
|
return { examined: 0, queued: 0, complete: true };
|
|
12429
12561
|
}
|
|
@@ -12508,11 +12640,11 @@ var JobManager = class {
|
|
|
12508
12640
|
return true;
|
|
12509
12641
|
}
|
|
12510
12642
|
/**
|
|
12511
|
-
* Report a completed paid job's revenue
|
|
12643
|
+
* Report a completed paid job's revenue under its durable job identity.
|
|
12512
12644
|
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
12513
12645
|
*
|
|
12514
12646
|
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
12515
|
-
* §10), and since
|
|
12647
|
+
* §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
|
|
12516
12648
|
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
12517
12649
|
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
12518
12650
|
* the funding lots it consumed. The correction is a delta, because a job's
|
|
@@ -12527,7 +12659,7 @@ var JobManager = class {
|
|
|
12527
12659
|
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
12528
12660
|
* payload is reported verbatim.
|
|
12529
12661
|
*
|
|
12530
|
-
* Takes the fields rather than a `ServerJob` so the
|
|
12662
|
+
* Takes the fields rather than a `ServerJob` so the reconciler — a
|
|
12531
12663
|
* background sweep that holds no in-process job — books through this exact
|
|
12532
12664
|
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
12533
12665
|
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
@@ -12544,7 +12676,7 @@ var JobManager = class {
|
|
|
12544
12676
|
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
12545
12677
|
*
|
|
12546
12678
|
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
12547
|
-
*
|
|
12679
|
+
* a settled draw the terminal can't report under is a real debit
|
|
12548
12680
|
* with no revenue event, permanently overstating outstanding liability
|
|
12549
12681
|
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
12550
12682
|
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
@@ -12607,9 +12739,9 @@ var JobManager = class {
|
|
|
12607
12739
|
paidMsats: job.paidMsats,
|
|
12608
12740
|
creditRail: creditRail ?? null,
|
|
12609
12741
|
// Four distinct causes needing four distinct fixes, so none of them
|
|
12610
|
-
// is the else-branch: the credit predates
|
|
12742
|
+
// is the else-branch: the credit predates and has no rail
|
|
12611
12743
|
// (`credit_rail_null`); it carries one this build doesn't list, the
|
|
12612
|
-
//
|
|
12744
|
+
// sibling class (`credit_rail_unrecognized`); the credit's
|
|
12613
12745
|
// rail is perfectly good and it is the *job row* that lost its own
|
|
12614
12746
|
// (`job_rail_missing`), which no change to the ledger would fix; or
|
|
12615
12747
|
// this manager was built with no `creditLedger` to read the draw
|
|
@@ -12657,7 +12789,7 @@ var JobManager = class {
|
|
|
12657
12789
|
cashuFlow: job.cashuFlow,
|
|
12658
12790
|
// Absent unless the handler declared one, and absent is a fact: the
|
|
12659
12791
|
// platform shows "no costs reported" rather than a zero cost and a
|
|
12660
|
-
// 100% margin
|
|
12792
|
+
// 100% margin. Both halves or neither — a bare amount
|
|
12661
12793
|
// in an unnamed currency is not a cost.
|
|
12662
12794
|
...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
|
|
12663
12795
|
...credit
|
|
@@ -12840,8 +12972,8 @@ var MintHealthTracker = class {
|
|
|
12840
12972
|
firstProbeComplete = false;
|
|
12841
12973
|
constructor(opts) {
|
|
12842
12974
|
this.mints = [...opts.mints];
|
|
12843
|
-
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
|
|
12844
|
-
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
|
|
12975
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
|
|
12976
|
+
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
|
|
12845
12977
|
this.fetchImpl = opts.fetchImpl;
|
|
12846
12978
|
this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
|
|
12847
12979
|
this.nowFn = opts.now ?? Date.now;
|
|
@@ -12892,7 +13024,7 @@ var MintHealthTracker = class {
|
|
|
12892
13024
|
/**
|
|
12893
13025
|
* True once the first full probe tick has landed. Before this, paid calls
|
|
12894
13026
|
* are held at `503 mint_health_pending` rather than accepted against
|
|
12895
|
-
* unvalidated mints
|
|
13027
|
+
* unvalidated mints.
|
|
12896
13028
|
*/
|
|
12897
13029
|
initialProbeComplete() {
|
|
12898
13030
|
return this.firstProbeComplete;
|
|
@@ -12917,7 +13049,7 @@ var MintHealthTracker = class {
|
|
|
12917
13049
|
* gate — a deterministic, permanent config error, unlike a transient outage.
|
|
12918
13050
|
* Unknown mints return false. `advertisedMints()` uses this to keep a
|
|
12919
13051
|
* permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
|
|
12920
|
-
* where a merely-flapping mint is still published
|
|
13052
|
+
* where a merely-flapping mint is still published.
|
|
12921
13053
|
*/
|
|
12922
13054
|
isNutIncompatible(mintUrl) {
|
|
12923
13055
|
return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
|
|
@@ -12984,7 +13116,7 @@ var MintHealthTracker = class {
|
|
|
12984
13116
|
* One-shot structured summary emitted after the first probe tick. Replaces
|
|
12985
13117
|
* the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
|
|
12986
13118
|
* all-mints-unhealthy warning — the operator alert that the Fly restart-loop
|
|
12987
|
-
* used to be before boot stopped refusing to start
|
|
13119
|
+
* used to be before boot stopped refusing to start.
|
|
12988
13120
|
*/
|
|
12989
13121
|
logBootHealth() {
|
|
12990
13122
|
const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
|
|
@@ -13009,15 +13141,15 @@ var MintHealthTracker = class {
|
|
|
13009
13141
|
);
|
|
13010
13142
|
}
|
|
13011
13143
|
};
|
|
13012
|
-
function resolveEnvTickMs() {
|
|
13013
|
-
const raw =
|
|
13144
|
+
function resolveEnvTickMs(env = process.env) {
|
|
13145
|
+
const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
|
|
13014
13146
|
if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
|
|
13015
13147
|
const parsed = Number(raw);
|
|
13016
13148
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
|
|
13017
13149
|
return Math.floor(parsed);
|
|
13018
13150
|
}
|
|
13019
|
-
function resolveEnvFailuresToSick() {
|
|
13020
|
-
const raw =
|
|
13151
|
+
function resolveEnvFailuresToSick(env = process.env) {
|
|
13152
|
+
const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
|
|
13021
13153
|
if (!raw) return DEFAULT_FAILURES_TO_SICK;
|
|
13022
13154
|
const parsed = Number(raw);
|
|
13023
13155
|
if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
|
|
@@ -14068,9 +14200,9 @@ async function handlePendingDrains(c, opts) {
|
|
|
14068
14200
|
sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
|
|
14069
14201
|
// The figure a `manual` payout is actually sized in: `amount_micro` is
|
|
14070
14202
|
// fiat, and the operator sends the rail's own token. It is the same debit
|
|
14071
|
-
// `completeX402ChannelDrain` treats as its settlement floor
|
|
14203
|
+
// `completeX402ChannelDrain` treats as its settlement floor,
|
|
14072
14204
|
// so publishing it is what lets an operator settle from the row instead
|
|
14073
|
-
// of assuming their stablecoin happens to be six-decimal
|
|
14205
|
+
// of assuming their stablecoin happens to be six-decimal.
|
|
14074
14206
|
amount_native: d.drainedNative,
|
|
14075
14207
|
settlement,
|
|
14076
14208
|
// Why this row is classified as it is, and the channel an operator would
|
|
@@ -14593,7 +14725,7 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14593
14725
|
fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
|
|
14594
14726
|
expiryMs,
|
|
14595
14727
|
// The settlements table types this deposit `repair` off the
|
|
14596
|
-
// flag
|
|
14728
|
+
// flag: the sats landed at payment time and the
|
|
14597
14729
|
// deposit is the one row they get.
|
|
14598
14730
|
repaired: true
|
|
14599
14731
|
}
|
|
@@ -16588,7 +16720,7 @@ function autoApprovePayment(content) {
|
|
|
16588
16720
|
headers: jobHeaders(true),
|
|
16589
16721
|
// amount_msats rides along so an onPayment handler sees the same shape a
|
|
16590
16722
|
// real rail would hand it. dev_auto is admitted on dev-mode servers only
|
|
16591
|
-
//
|
|
16723
|
+
// hence the status check below rather than a silent catch.
|
|
16592
16724
|
body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
|
|
16593
16725
|
}).then(function(r) {
|
|
16594
16726
|
if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
|
|
@@ -16735,7 +16867,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16735
16867
|
}
|
|
16736
16868
|
})();
|
|
16737
16869
|
try {
|
|
16738
|
-
if (
|
|
16870
|
+
if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
|
|
16739
16871
|
console.warn(
|
|
16740
16872
|
JSON.stringify({
|
|
16741
16873
|
level: "cashu_opts_env_ignored",
|
|
@@ -16748,12 +16880,12 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16748
16880
|
const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
|
|
16749
16881
|
const dvmId = opts.dvmId;
|
|
16750
16882
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
16751
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
16883
|
+
const { RevenueReporter } = await import("./revenue-reporter-NNCNRY4C.js");
|
|
16752
16884
|
const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
|
|
16753
16885
|
revenueReporter = reporter;
|
|
16754
16886
|
await reporter.init();
|
|
16755
16887
|
reporter.startRetryLoop();
|
|
16756
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
16888
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-5PIRYFVQ.js");
|
|
16757
16889
|
payoutReporter = new PayoutReporter({
|
|
16758
16890
|
dvmId,
|
|
16759
16891
|
db: opts.db,
|
|
@@ -16896,6 +17028,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16896
17028
|
processedPayments,
|
|
16897
17029
|
onJobCompleted: effectiveOnJobCompleted,
|
|
16898
17030
|
onJobCost: effectiveOnJobCost,
|
|
17031
|
+
onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
|
|
16899
17032
|
onPaidJobDeath: effectiveOnPaidJobDeath,
|
|
16900
17033
|
onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
|
|
16901
17034
|
onCreditFunded: opts.onCreditFunded,
|
|
@@ -16939,7 +17072,7 @@ var DVMServer = class {
|
|
|
16939
17072
|
fxFetcher;
|
|
16940
17073
|
/**
|
|
16941
17074
|
* The currency this DVM prices in and denominates its credit ledger in,
|
|
16942
|
-
* resolved once at construction
|
|
17075
|
+
* resolved once at construction.
|
|
16943
17076
|
*
|
|
16944
17077
|
* `configureDVM` and the CLI loader both resolve this field. The server is
|
|
16945
17078
|
* also exported for direct JavaScript callers, so defaulting at the one
|
|
@@ -16948,29 +17081,24 @@ var DVMServer = class {
|
|
|
16948
17081
|
*/
|
|
16949
17082
|
pricingCurrency;
|
|
16950
17083
|
/**
|
|
16951
|
-
* Last-good lock-pubkey state observed from Postgres
|
|
17084
|
+
* Last-good lock-pubkey state observed from Postgres. Used as the
|
|
16952
17085
|
* fallback when `loadLockPubkeyState` throws — degrades more gracefully than
|
|
16953
17086
|
* falling back to the env-derived seed, which post-rotation is the *retired*
|
|
16954
17087
|
* pubkey. Null until the first successful read.
|
|
16955
17088
|
*/
|
|
16956
17089
|
cachedLockPubkeyState = null;
|
|
16957
|
-
/** Guards the opportunistic Tempo settlement cadence (internal-review). */
|
|
16958
17090
|
tempoSweepInFlight = false;
|
|
16959
17091
|
tempoSweepLastAttemptMs = 0;
|
|
16960
|
-
/** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
|
|
16961
17092
|
receiptIssuer;
|
|
16962
|
-
/** Credit ledger the payment gate funds/draws through (internal-review). */
|
|
16963
17093
|
creditLedger;
|
|
16964
|
-
/** Durable x402/mpp funding markers (internal-review). */
|
|
16965
17094
|
processedPayments;
|
|
16966
17095
|
authAudience;
|
|
16967
17096
|
/** Lazily constructed when a refreshed facilitator probe gains batch support. */
|
|
16968
17097
|
x402BatchSettlement;
|
|
16969
17098
|
/** Coalesces concurrent requests that first observe batch support. */
|
|
16970
17099
|
x402BatchSettlementInit;
|
|
16971
|
-
/** Durable exact-settlement coordinator shared by every exact payment surface. */
|
|
16972
17100
|
x402ExactSettlement;
|
|
16973
|
-
/** Withdraws this mount's Tempo session capability from the poll
|
|
17101
|
+
/** Withdraws this mount's Tempo session capability from the poll. */
|
|
16974
17102
|
undeclareSessionCapability;
|
|
16975
17103
|
constructor(descriptor, opts) {
|
|
16976
17104
|
this.descriptor = descriptor;
|
|
@@ -16994,7 +17122,7 @@ var DVMServer = class {
|
|
|
16994
17122
|
this.mintHealthTracker = opts.mintHealthTracker;
|
|
16995
17123
|
this.ownsMintHealthTracker = false;
|
|
16996
17124
|
} else if (opts.mints && opts.mints.length > 0) {
|
|
16997
|
-
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
|
|
17125
|
+
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
|
|
16998
17126
|
this.ownsMintHealthTracker = true;
|
|
16999
17127
|
} else {
|
|
17000
17128
|
this.ownsMintHealthTracker = false;
|
|
@@ -17015,7 +17143,7 @@ var DVMServer = class {
|
|
|
17015
17143
|
mints: opts.mints,
|
|
17016
17144
|
jobStore: opts.jobStore,
|
|
17017
17145
|
devMode: opts.devMode,
|
|
17018
|
-
// Derived, not raw
|
|
17146
|
+
// Derived, not raw. `ctx.requestPayment` gates its x402 and
|
|
17019
17147
|
// Tempo challenge issuance on this list, while `/v1/info`, `/v1/quote`
|
|
17020
17148
|
// and the upfront 402 all read `resolvePaymentMethods()` — so passing the
|
|
17021
17149
|
// raw option meant a DVM that never set `paymentMethods` (no first-party
|
|
@@ -17040,21 +17168,20 @@ var DVMServer = class {
|
|
|
17040
17168
|
dvmId: opts.dvmId,
|
|
17041
17169
|
authAudience: this.authAudience,
|
|
17042
17170
|
fxFetcher: this.fxFetcher,
|
|
17043
|
-
//
|
|
17171
|
+
// the mid-job top-up leg needs the same rail wiring the upfront
|
|
17044
17172
|
// path gets — a divergence here would be a DVM whose upfront payment funds
|
|
17045
17173
|
// a credit and whose mid-job payment silently doesn't.
|
|
17046
17174
|
processedPayments: this.processedPayments,
|
|
17047
17175
|
enqueueCreditDeposit: opts.enqueueCreditDeposit,
|
|
17048
17176
|
enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
|
|
17049
17177
|
creditTtlMs: descriptor.credit ? descriptor.credit.ttlSeconds * 1e3 : void 0,
|
|
17050
|
-
// internal-review: the same read `creditCurrency()` serves everywhere else, so a
|
|
17051
|
-
// mid-job ask pins in the denomination this DVM's credits actually use.
|
|
17052
17178
|
pricingCurrency: this.pricingCurrency,
|
|
17053
17179
|
onCreditFunded: opts.onCreditFunded,
|
|
17054
17180
|
onJobCompleted: opts.onJobCompleted,
|
|
17055
17181
|
onPaidJobCompleted: opts.onPaidJobCompleted,
|
|
17056
17182
|
paymentMode: opts.paymentMode,
|
|
17057
17183
|
onJobCost: opts.onJobCost,
|
|
17184
|
+
onJobTerminal: opts.onJobTerminal,
|
|
17058
17185
|
onPaidJobDeath: opts.onPaidJobDeath,
|
|
17059
17186
|
onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
|
|
17060
17187
|
onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
|
|
@@ -17092,7 +17219,7 @@ var DVMServer = class {
|
|
|
17092
17219
|
* Tear down this mount. Every leg is synchronous except the batch-settlement
|
|
17093
17220
|
* server's own `stop()`; the returned promise settles once that has finished,
|
|
17094
17221
|
* so a caller holding a resource behind it — the settlement pool the spend
|
|
17095
|
-
* gate's store owns
|
|
17222
|
+
* gate's store owns — can sequence its close instead of pulling it
|
|
17096
17223
|
* out from under an in-flight stop. A rejection remains observable so the app
|
|
17097
17224
|
* and host teardown layers can finish later cleanup before surfacing it.
|
|
17098
17225
|
*/
|
|
@@ -17193,12 +17320,12 @@ var DVMServer = class {
|
|
|
17193
17320
|
(c) => c.json({
|
|
17194
17321
|
ok: true,
|
|
17195
17322
|
// shortSha, not .slice(0, 7): a dirty-tree deploy bakes a `-dirty`
|
|
17196
|
-
// marker
|
|
17323
|
+
// marker that a raw truncation would cut off the end,
|
|
17197
17324
|
// leaving this endpoint claiming a clean commit while /v1/info's
|
|
17198
17325
|
// untruncated sdk_sha says otherwise — same image, two answers.
|
|
17199
|
-
gitSha: shortSha(
|
|
17200
|
-
deployedAt:
|
|
17201
|
-
deployId:
|
|
17326
|
+
gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
|
|
17327
|
+
deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
|
|
17328
|
+
deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17202
17329
|
})
|
|
17203
17330
|
);
|
|
17204
17331
|
if (this.opts.devMode) {
|
|
@@ -17231,8 +17358,6 @@ var DVMServer = class {
|
|
|
17231
17358
|
getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
|
|
17232
17359
|
graceSeconds: this.graceSeconds(),
|
|
17233
17360
|
nonceStore: this.adminCashuNonceStore,
|
|
17234
|
-
// internal-review: a completed melt is a landed payout, queued in the
|
|
17235
|
-
// transaction that marks it.
|
|
17236
17361
|
...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
|
|
17237
17362
|
});
|
|
17238
17363
|
}
|
|
@@ -17250,13 +17375,13 @@ var DVMServer = class {
|
|
|
17250
17375
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
17251
17376
|
enqueueCreditDrain: this.opts.enqueueCreditDrain,
|
|
17252
17377
|
lightningReceive: this.opts.lightningReceive,
|
|
17253
|
-
//
|
|
17378
|
+
// x402 repair queue rides the same surface, and resolves
|
|
17254
17379
|
// the rail the same lazy way the verify path does — constructing the
|
|
17255
17380
|
// settlement server probes the facilitator, which must not happen just
|
|
17256
17381
|
// because the admin routes were mounted.
|
|
17257
17382
|
x402BatchSettlement: () => this.resolveX402BatchSettlement(),
|
|
17258
17383
|
x402ExactSettlement: this.x402ExactSettlement,
|
|
17259
|
-
//
|
|
17384
|
+
// Tempo repair queue, same surface again. The mppx handle is
|
|
17260
17385
|
// already built by the time routes mount, so the thunk is uniformity
|
|
17261
17386
|
// rather than laziness — and it is the *handle*, not the store, so a
|
|
17262
17387
|
// DVM wired with an in-memory session store resolves to a rail with no
|
|
@@ -17264,10 +17389,8 @@ var DVMServer = class {
|
|
|
17264
17389
|
tempoDrainRail: () => Promise.resolve(this.opts.mpp),
|
|
17265
17390
|
// Health-filtered, and read per request rather than captured: the
|
|
17266
17391
|
// servicer picking a mint to buy a refund at wants the list that
|
|
17267
|
-
// excludes a mint currently failing its probe
|
|
17392
|
+
// excludes a mint currently failing its probe.
|
|
17268
17393
|
mints: () => this.advertisedMints(),
|
|
17269
|
-
// internal-review: a repaired Tempo close is a landed payout the agent
|
|
17270
|
-
// finished, queued in the transaction that books the drain.
|
|
17271
17394
|
...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
|
|
17272
17395
|
});
|
|
17273
17396
|
}
|
|
@@ -17278,7 +17401,7 @@ var DVMServer = class {
|
|
|
17278
17401
|
/**
|
|
17279
17402
|
* Load the live lock-pubkey state for this DVM. Reads from
|
|
17280
17403
|
* `dvm_lock_pubkeys` on every call so rotations propagate without needing
|
|
17281
|
-
* a process restart
|
|
17404
|
+
* a process restart. On success the result is cached so a later
|
|
17282
17405
|
* DB outage can degrade to the last-observed state instead of the
|
|
17283
17406
|
* env-derived seed (which post-rotation is the *retired* pubkey).
|
|
17284
17407
|
*/
|
|
@@ -17289,7 +17412,7 @@ var DVMServer = class {
|
|
|
17289
17412
|
}
|
|
17290
17413
|
/**
|
|
17291
17414
|
* Resolve the active lock-pubkey set for the mid-job cashu accept path
|
|
17292
|
-
*
|
|
17415
|
+
* Returns the same `[current, ...retired_in_grace]` shape the
|
|
17293
17416
|
* upfront flow uses at lines 709-718 so accept-time rotation tolerance is
|
|
17294
17417
|
* consistent across upfront and mid-job. Falls back to `undefined` when
|
|
17295
17418
|
* accumulator mode isn't wired (the receive path then rejects with
|
|
@@ -17326,13 +17449,13 @@ var DVMServer = class {
|
|
|
17326
17449
|
}
|
|
17327
17450
|
/**
|
|
17328
17451
|
* Run the routine Tempo settlement cadence opportunistically, on ordinary
|
|
17329
|
-
* traffic
|
|
17452
|
+
* traffic.
|
|
17330
17453
|
*
|
|
17331
17454
|
* Routine settlement used to ride the per-DVM watcher machine's four-minute
|
|
17332
17455
|
* poll, which is exactly what kept an idle DVM awake and billing. It cannot
|
|
17333
17456
|
* move to a timer for the same reason — a timer on a machine that ought to
|
|
17334
17457
|
* be suspended either does not fire or defeats the suspension. So it rides
|
|
17335
|
-
* requests instead, the way Lightning funding is credited
|
|
17458
|
+
* requests instead, the way Lightning funding is credited: a DVM
|
|
17336
17459
|
* with no traffic has nothing to settle on a 24-hour cadence anyway, and the
|
|
17337
17460
|
* forced-close guarantee does not depend on this path at all — that is the
|
|
17338
17461
|
* chain-event wake, which reaches a suspended DVM through Fly Proxy.
|
|
@@ -17372,7 +17495,7 @@ var DVMServer = class {
|
|
|
17372
17495
|
});
|
|
17373
17496
|
}
|
|
17374
17497
|
/**
|
|
17375
|
-
* Whether one advertised MPP method may still be offered
|
|
17498
|
+
* Whether one advertised MPP method may still be offered.
|
|
17376
17499
|
*
|
|
17377
17500
|
* Only `tempo/session` is gated, and only when this DVM is platform-hosted:
|
|
17378
17501
|
* a session channel is safe to accept exactly while something is watching
|
|
@@ -17388,7 +17511,6 @@ var DVMServer = class {
|
|
|
17388
17511
|
tempoSessionOffered(method) {
|
|
17389
17512
|
return this.tempoSessionWithheldReason(method) === void 0;
|
|
17390
17513
|
}
|
|
17391
|
-
/** Stable caller-facing reason for withholding one newly opened session. */
|
|
17392
17514
|
tempoSessionWithheldReason(method) {
|
|
17393
17515
|
if (method.method !== "tempo" || method.intent !== "session") return void 0;
|
|
17394
17516
|
if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
|
|
@@ -17399,7 +17521,6 @@ var DVMServer = class {
|
|
|
17399
17521
|
}
|
|
17400
17522
|
return void 0;
|
|
17401
17523
|
}
|
|
17402
|
-
/** Await only the cold-start reads each fail-closed session gate needs. */
|
|
17403
17524
|
async ensureTempoSessionHealth() {
|
|
17404
17525
|
await Promise.all([
|
|
17405
17526
|
this.opts.tempoObserverHealth?.ensureFresh?.(),
|
|
@@ -17410,7 +17531,6 @@ var DVMServer = class {
|
|
|
17410
17531
|
* Whether this DVM is **configured** to take TIP-1034 session channels — the
|
|
17411
17532
|
* value the observer-health poll carries back to the platform, where it keeps
|
|
17412
17533
|
* `dvms.tempo_session_advertised_at` current between container deploys
|
|
17413
|
-
* (internal-review).
|
|
17414
17534
|
*
|
|
17415
17535
|
* Deliberately the same question the platform's post-deploy `/v1/info` probe
|
|
17416
17536
|
* answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
|
|
@@ -17422,13 +17542,12 @@ var DVMServer = class {
|
|
|
17422
17542
|
* It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
|
|
17423
17543
|
* and never {@link tempoSessionOffered}: what the column records is
|
|
17424
17544
|
* capability, not availability, so an observer blip must leave it alone. That
|
|
17425
|
-
* distinction is the whole of
|
|
17545
|
+
* distinction is the whole of and answering it here from the gate
|
|
17426
17546
|
* would reintroduce it on a one-minute cadence.
|
|
17427
17547
|
*/
|
|
17428
17548
|
tempoSessionCapability() {
|
|
17429
17549
|
if (!this.opts.mpp) return false;
|
|
17430
17550
|
if (!creditMenuConfigured({
|
|
17431
|
-
// The SDK server only ever runs the container runtime — see `creditMenu`.
|
|
17432
17551
|
runtime: "container",
|
|
17433
17552
|
config: this.descriptor.credit,
|
|
17434
17553
|
hasAuth: !!this.descriptor.auth,
|
|
@@ -17443,7 +17562,7 @@ var DVMServer = class {
|
|
|
17443
17562
|
}
|
|
17444
17563
|
/**
|
|
17445
17564
|
* `POST /_internal/tempo-close-event` — the forced-close wake path
|
|
17446
|
-
*
|
|
17565
|
+
* replacing the per-DVM `tempo-session-watch` Fly Machine.
|
|
17447
17566
|
*
|
|
17448
17567
|
* Deliberately **unauthenticated**, and that is the design rather than an
|
|
17449
17568
|
* omission: the body is an event locator, so the DVM re-reads the event from
|
|
@@ -17522,7 +17641,7 @@ var DVMServer = class {
|
|
|
17522
17641
|
}
|
|
17523
17642
|
/**
|
|
17524
17643
|
* Compose the `/v1/info#builder` block from `opts.builder` (inline SDK
|
|
17525
|
-
* config) with a fallback to deploy-injected env vars
|
|
17644
|
+
* config) with a fallback to deploy-injected env vars. Each
|
|
17526
17645
|
* field is independently sourced (opts wins per-field, env fills gaps);
|
|
17527
17646
|
* all three of pubkey / attestation / signature must resolve to emit the
|
|
17528
17647
|
* block — partial state is treated as absent rather than half-emitted.
|
|
@@ -17535,8 +17654,8 @@ var DVMServer = class {
|
|
|
17535
17654
|
builderAttestationBlock() {
|
|
17536
17655
|
const builder = this.opts.builder;
|
|
17537
17656
|
const env = this.opts.env;
|
|
17538
|
-
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY
|
|
17539
|
-
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE
|
|
17657
|
+
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
|
|
17658
|
+
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
|
|
17540
17659
|
const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
|
|
17541
17660
|
if (!pubkey || !signature || !attestation) return null;
|
|
17542
17661
|
return { pubkey, attestation, signature };
|
|
@@ -17568,7 +17687,7 @@ var DVMServer = class {
|
|
|
17568
17687
|
throw new AuthAudienceError();
|
|
17569
17688
|
}
|
|
17570
17689
|
/**
|
|
17571
|
-
* The slug every receipt is stamped with
|
|
17690
|
+
* The slug every receipt is stamped with. Read off the signed
|
|
17572
17691
|
* attestation payload, which is the only place the *attested* slug lives —
|
|
17573
17692
|
* a receipt claiming a different name than the attestation binds would be
|
|
17574
17693
|
* unverifiable. Falls back to the descriptor name for dev/self-host boots
|
|
@@ -17579,14 +17698,13 @@ var DVMServer = class {
|
|
|
17579
17698
|
const slug = attestation?.slug;
|
|
17580
17699
|
return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
|
|
17581
17700
|
}
|
|
17582
|
-
/** Immutable identity written into every signed receipt. */
|
|
17583
17701
|
receiptDvmId() {
|
|
17584
17702
|
const attestation = this.builderAttestationBlock()?.attestation;
|
|
17585
17703
|
const dvmId = attestation?.dvm_id;
|
|
17586
17704
|
return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
|
|
17587
17705
|
}
|
|
17588
17706
|
parseAttestationFromEnv(env) {
|
|
17589
|
-
const raw = env.DVMKIT_BUILDER_ATTESTATION
|
|
17707
|
+
const raw = env.DVMKIT_BUILDER_ATTESTATION;
|
|
17590
17708
|
if (!raw) return null;
|
|
17591
17709
|
try {
|
|
17592
17710
|
const parsed = JSON.parse(raw);
|
|
@@ -17597,7 +17715,7 @@ var DVMServer = class {
|
|
|
17597
17715
|
}
|
|
17598
17716
|
}
|
|
17599
17717
|
/**
|
|
17600
|
-
* Owner display block for `/v1/info#owner
|
|
17718
|
+
* Owner display block for `/v1/info#owner`. Resolved from
|
|
17601
17719
|
* `opts.owner` or env vars injected by the platform deploy pipeline.
|
|
17602
17720
|
* Returns null when no owner identity is configured — pre-unification
|
|
17603
17721
|
* DVMs that haven't been redeployed omit the block.
|
|
@@ -17605,11 +17723,11 @@ var DVMServer = class {
|
|
|
17605
17723
|
ownerDisplayBlock() {
|
|
17606
17724
|
const owner = this.opts.owner;
|
|
17607
17725
|
const env = this.opts.env;
|
|
17608
|
-
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE
|
|
17726
|
+
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
|
|
17609
17727
|
if (!handle) return null;
|
|
17610
|
-
const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ??
|
|
17611
|
-
const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ??
|
|
17612
|
-
const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ??
|
|
17728
|
+
const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
|
|
17729
|
+
const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
|
|
17730
|
+
const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
|
|
17613
17731
|
return { handle, display_name: displayName, avatar_url: avatarUrl, type };
|
|
17614
17732
|
}
|
|
17615
17733
|
defaultPaymentMethods() {
|
|
@@ -17628,14 +17746,13 @@ var DVMServer = class {
|
|
|
17628
17746
|
if (!this.opts.x402.batchSettlement?.selfRelay) return support;
|
|
17629
17747
|
return { ...support, batchSettlement: true };
|
|
17630
17748
|
}
|
|
17631
|
-
/** Whether this DVM can state its price exactly in x402's USDC unit. */
|
|
17632
17749
|
x402CurrencySupported() {
|
|
17633
17750
|
return this.pricingCurrency === "usd";
|
|
17634
17751
|
}
|
|
17635
17752
|
/**
|
|
17636
17753
|
* Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
|
|
17637
17754
|
* override and otherwise deriving from wired rails. Single source of truth so
|
|
17638
|
-
* the `/v1/info` and `/v1/quote` rail enumerations can't drift
|
|
17755
|
+
* the `/v1/info` and `/v1/quote` rail enumerations can't drift.
|
|
17639
17756
|
*/
|
|
17640
17757
|
resolvePaymentMethods() {
|
|
17641
17758
|
const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
|
|
@@ -17648,13 +17765,13 @@ var DVMServer = class {
|
|
|
17648
17765
|
}
|
|
17649
17766
|
/**
|
|
17650
17767
|
* Rails a **credit top-up** may arrive on — the funding menu's list
|
|
17651
|
-
*
|
|
17768
|
+
* A superset of {@link resolvePaymentMethods}, and deliberately
|
|
17652
17769
|
* a separate read: `lightning` funds a credit but is not an attached-proof
|
|
17653
17770
|
* wire method, so it must never reach `/v1/info` or `/v1/quote`'s
|
|
17654
|
-
* `payment.methods
|
|
17771
|
+
* `payment.methods`.
|
|
17655
17772
|
*
|
|
17656
|
-
* The rail drops out when the receive wallet is unreachable
|
|
17657
|
-
*
|
|
17773
|
+
* The rail drops out of the funding menu when the receive wallet is unreachable;
|
|
17774
|
+
* advertising it then would hand the caller an option that
|
|
17658
17775
|
* fails on use, while the other rails would have closed the sale.
|
|
17659
17776
|
*/
|
|
17660
17777
|
resolveFundingMethods() {
|
|
@@ -17673,7 +17790,7 @@ var DVMServer = class {
|
|
|
17673
17790
|
}
|
|
17674
17791
|
/**
|
|
17675
17792
|
* The credit menu's MPP instruments, split into what is on offer right now
|
|
17676
|
-
* and what this DVM is configured for but withholding
|
|
17793
|
+
* and what this DVM is configured for but withholding.
|
|
17677
17794
|
*
|
|
17678
17795
|
* The split has to survive into the response rather than being filtered
|
|
17679
17796
|
* away, because "not offered right now" and "not configured" are different
|
|
@@ -17701,7 +17818,7 @@ var DVMServer = class {
|
|
|
17701
17818
|
}
|
|
17702
17819
|
/**
|
|
17703
17820
|
* Which x402 flavours a credit top-up may arrive on — the funding menu's
|
|
17704
|
-
* `x402` sub-block
|
|
17821
|
+
* `x402` sub-block. Hosted health buys `exact`; a genuinely wired
|
|
17705
17822
|
* batch server buys `batch-settlement` independently, including while the
|
|
17706
17823
|
* hosted facilitator is unavailable or stale.
|
|
17707
17824
|
*
|
|
@@ -17718,13 +17835,12 @@ var DVMServer = class {
|
|
|
17718
17835
|
...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
|
|
17719
17836
|
];
|
|
17720
17837
|
}
|
|
17721
|
-
/** Whether a channel server is both supported and constructible for credit funding. */
|
|
17722
17838
|
x402BatchAvailable() {
|
|
17723
17839
|
return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
|
|
17724
17840
|
}
|
|
17725
17841
|
/**
|
|
17726
17842
|
* Apply any Lightning funding invoice this caller has paid since we last
|
|
17727
|
-
* looked — the pull half of pull-based crediting
|
|
17843
|
+
* looked — the pull half of pull-based crediting.
|
|
17728
17844
|
*
|
|
17729
17845
|
* Called from every authenticated surface where the balance is about to
|
|
17730
17846
|
* matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
|
|
@@ -17737,7 +17853,7 @@ var DVMServer = class {
|
|
|
17737
17853
|
* outstanding, and never blocks the request when it fails.
|
|
17738
17854
|
*
|
|
17739
17855
|
* The SDK-owned reporter queues each Lightning deposit inside
|
|
17740
|
-
* `settleInvoice`'s transaction
|
|
17856
|
+
* `settleInvoice`'s transaction. The loop below only preserves
|
|
17741
17857
|
* the explicit `onCreditFunded` override, whose caller owns its durability.
|
|
17742
17858
|
*/
|
|
17743
17859
|
async settleLightningFunding(callerPubkey, creditId) {
|
|
@@ -17768,7 +17884,6 @@ var DVMServer = class {
|
|
|
17768
17884
|
});
|
|
17769
17885
|
}
|
|
17770
17886
|
}
|
|
17771
|
-
/** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
|
|
17772
17887
|
creditTtlMs() {
|
|
17773
17888
|
return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
|
|
17774
17889
|
}
|
|
@@ -17789,7 +17904,7 @@ var DVMServer = class {
|
|
|
17789
17904
|
* The per-DVM + per-request rail wiring every payment verification needs —
|
|
17790
17905
|
* everything except what the *operation* contributes (its price, its job or
|
|
17791
17906
|
* fund id, the caller's credit fields). Shared by `/v1/job`'s upfront gate
|
|
17792
|
-
* and `/v1/credit`'s top-up
|
|
17907
|
+
* and `/v1/credit`'s top-up so a rail wired into one is wired
|
|
17793
17908
|
* into both by construction; a divergence here would be a rail that funds
|
|
17794
17909
|
* jobs but silently can't fund credits.
|
|
17795
17910
|
*/
|
|
@@ -17822,7 +17937,7 @@ var DVMServer = class {
|
|
|
17822
17937
|
mppCredential,
|
|
17823
17938
|
mpp: this.opts.mpp,
|
|
17824
17939
|
resourcePath: c.req.path,
|
|
17825
|
-
//
|
|
17940
|
+
// the gate covers issuance and new-channel acceptance,
|
|
17826
17941
|
// not just the two advertisement surfaces. `ensureFresh` above makes this
|
|
17827
17942
|
// an observation rather than a cold-start default on the one surface that
|
|
17828
17943
|
// can act on it.
|
|
@@ -17840,8 +17955,6 @@ var DVMServer = class {
|
|
|
17840
17955
|
requestId: c.req.header("X-Cashu-Request-Id"),
|
|
17841
17956
|
mintHealthTracker: this.mintHealthTracker,
|
|
17842
17957
|
fxFetcher: this.fxFetcher,
|
|
17843
|
-
// internal-review: the ledger leg — every rail payment becomes fund_credit +
|
|
17844
|
-
// draw, atomic with the rail commit.
|
|
17845
17958
|
creditLedger: this.creditLedger,
|
|
17846
17959
|
processedPayments: this.processedPayments,
|
|
17847
17960
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
@@ -17851,7 +17964,7 @@ var DVMServer = class {
|
|
|
17851
17964
|
}
|
|
17852
17965
|
/**
|
|
17853
17966
|
* Assemble this DVM's funding menu for a response denominated in
|
|
17854
|
-
* `currency
|
|
17967
|
+
* `currency`. All the refusal logic — including the credits-spec
|
|
17855
17968
|
* §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
|
|
17856
17969
|
* server's state. `callerPubkey` must be a *verified* envelope pubkey: the
|
|
17857
17970
|
* balance echo is per-caller money and there is no safe fallback identity.
|
|
@@ -17860,8 +17973,6 @@ var DVMServer = class {
|
|
|
17860
17973
|
await this.ensureTempoSessionHealth();
|
|
17861
17974
|
const mppSplit = this.creditMppSplit();
|
|
17862
17975
|
return buildCreditMenu({
|
|
17863
|
-
// The SDK server only ever runs the container runtime; the isolate route
|
|
17864
|
-
// calls the same builder with `"isolate"` and is refused there.
|
|
17865
17976
|
runtime: "container",
|
|
17866
17977
|
config: this.descriptor.credit,
|
|
17867
17978
|
currency,
|
|
@@ -17870,7 +17981,7 @@ var DVMServer = class {
|
|
|
17870
17981
|
tempoWithheld: mppSplit.withheld,
|
|
17871
17982
|
x402Schemes: this.creditX402Schemes(),
|
|
17872
17983
|
lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
|
|
17873
|
-
// Configured, not advertised
|
|
17984
|
+
// Configured, not advertised: `advertisedMints()` is health
|
|
17874
17985
|
// filtered, and a rail that comes and goes with a mint's uptime is worse
|
|
17875
17986
|
// product than one that is honestly absent.
|
|
17876
17987
|
hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
|
|
@@ -17907,7 +18018,7 @@ var DVMServer = class {
|
|
|
17907
18018
|
}
|
|
17908
18019
|
/**
|
|
17909
18020
|
* Currency this DVM's credit ledger is denominated in — the builder's
|
|
17910
|
-
* declared pricing currency, `"usd"` unless they said otherwise
|
|
18021
|
+
* declared pricing currency, `"usd"` unless they said otherwise.
|
|
17911
18022
|
*
|
|
17912
18023
|
* Every denomination surface reads this one answer: the funding menu on
|
|
17913
18024
|
* `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
|
|
@@ -17916,20 +18027,16 @@ var DVMServer = class {
|
|
|
17916
18027
|
* static (USD-literal) price on a non-USD DVM, and a quote denominated
|
|
17917
18028
|
* elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
|
|
17918
18029
|
* is what a top-up funded in, on every path.
|
|
17919
|
-
*
|
|
17920
|
-
* It used to return the literal `"usd"`, which is how a EUR-pricing DVM
|
|
17921
|
-
* came to fund `/v1/credit` top-ups in a denomination none of its jobs
|
|
17922
|
-
* would ever draw from.
|
|
17923
18030
|
*/
|
|
17924
18031
|
creditCurrency() {
|
|
17925
18032
|
return this.pricingCurrency;
|
|
17926
18033
|
}
|
|
17927
18034
|
/**
|
|
17928
|
-
* Quote-response `payment` block
|
|
18035
|
+
* Quote-response `payment` block. Carries the same `methods` array
|
|
17929
18036
|
* `/v1/info` advertises plus a per-rail `refundable` map keyed by method, so a
|
|
17930
18037
|
* caller can see which rails return funds on failure before committing. The
|
|
17931
18038
|
* `methods` shape stays identical to `/v1/info` and the platform isolate route
|
|
17932
|
-
* (
|
|
18039
|
+
* (current nested `payment.methods`); refundability rides alongside.
|
|
17933
18040
|
*/
|
|
17934
18041
|
quotePaymentBlock() {
|
|
17935
18042
|
const methods = this.resolvePaymentMethods();
|
|
@@ -17938,8 +18045,6 @@ var DVMServer = class {
|
|
|
17938
18045
|
refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
|
|
17939
18046
|
};
|
|
17940
18047
|
}
|
|
17941
|
-
// ── Route handlers ──────────────────────────────────────────────────
|
|
17942
|
-
/** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
|
|
17943
18048
|
async handleProxy(c) {
|
|
17944
18049
|
const raw = getCookie(c, "dvmkit_target");
|
|
17945
18050
|
const target = raw ? decodeURIComponent(raw) : null;
|
|
@@ -17975,8 +18080,8 @@ var DVMServer = class {
|
|
|
17975
18080
|
auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
|
|
17976
18081
|
...this.authAudience ? { auth_audience: this.authAudience } : {},
|
|
17977
18082
|
protocol_version: 1,
|
|
17978
|
-
sdk_sha:
|
|
17979
|
-
deploy_id:
|
|
18083
|
+
sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
|
|
18084
|
+
deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17980
18085
|
};
|
|
17981
18086
|
const ownerBlock = this.ownerDisplayBlock();
|
|
17982
18087
|
if (ownerBlock) info.owner = ownerBlock;
|
|
@@ -17997,8 +18102,7 @@ var DVMServer = class {
|
|
|
17997
18102
|
network: this.opts.x402.network,
|
|
17998
18103
|
asset: this.opts.x402.asset ?? "USDC",
|
|
17999
18104
|
...this.opts.x402.facilitator && { facilitator: this.opts.x402.facilitator },
|
|
18000
|
-
//
|
|
18001
|
-
// carry their PaymentRequirements before submitting (internal-review).
|
|
18105
|
+
// Advertises whether 402 challenges include x402 PaymentRequirements.
|
|
18002
18106
|
dual_serve: this.opts.x402.dualServe ?? true
|
|
18003
18107
|
};
|
|
18004
18108
|
}
|
|
@@ -18009,11 +18113,11 @@ var DVMServer = class {
|
|
|
18009
18113
|
return reason ? [{ ...method, reason }] : [];
|
|
18010
18114
|
});
|
|
18011
18115
|
info.tempo = {
|
|
18012
|
-
//
|
|
18116
|
+
// withheld sessions are withheld here too. `/v1/info` is
|
|
18013
18117
|
// where a caller decides what to attempt, so advertising a session the
|
|
18014
18118
|
// credit menu will refuse just sends them down a dead end.
|
|
18015
18119
|
methods: configured.filter((method) => this.tempoSessionOffered(method)),
|
|
18016
|
-
// …but say so rather than letting it vanish
|
|
18120
|
+
// …but say so rather than letting it vanish: an agent that
|
|
18017
18121
|
// sees only the shortened list cannot tell a DVM that never did
|
|
18018
18122
|
// sessions from one whose observer is down for a minute.
|
|
18019
18123
|
...withheld.length && { withheld },
|
|
@@ -18045,7 +18149,7 @@ var DVMServer = class {
|
|
|
18045
18149
|
return c.json(info);
|
|
18046
18150
|
}
|
|
18047
18151
|
/**
|
|
18048
|
-
* Render the `/v1/info.capabilities` block
|
|
18152
|
+
* Render the `/v1/info.capabilities` block. Each capability
|
|
18049
18153
|
* advertises its `description`, optional `input_schema`, and optional
|
|
18050
18154
|
* `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
|
|
18051
18155
|
* `pricing.max` for static-priced ones. Capabilities without an `input`
|
|
@@ -18255,7 +18359,7 @@ var DVMServer = class {
|
|
|
18255
18359
|
* the two callers — `/v1/quote` and `computeDynamicPrice` on the submit
|
|
18256
18360
|
* path — cannot drift: a field added for one is present in the other by
|
|
18257
18361
|
* construction, and both paths price against the same view of the caller's
|
|
18258
|
-
* credit
|
|
18362
|
+
* credit.
|
|
18259
18363
|
*/
|
|
18260
18364
|
async buildQuoteContext(args) {
|
|
18261
18365
|
const ctx = {
|
|
@@ -18278,15 +18382,15 @@ var DVMServer = class {
|
|
|
18278
18382
|
}
|
|
18279
18383
|
/**
|
|
18280
18384
|
* Validate the `capability` field on a `/v1/job` or `/v1/quote` body
|
|
18281
|
-
*
|
|
18385
|
+
* Returns the canonical name when set + known; otherwise a
|
|
18282
18386
|
* structured 400 envelope agents can relay verbatim to a human caller.
|
|
18283
18387
|
*
|
|
18284
18388
|
* - **Missing** — `body.capability` is absent or empty. Surface as
|
|
18285
|
-
*
|
|
18286
|
-
*
|
|
18389
|
+
* `capability_required` with a hint pointing at `/v1/info` so the agent
|
|
18390
|
+
* can re-discover the capability set.
|
|
18287
18391
|
* - **Unknown** — `body.capability` is set but doesn't match any
|
|
18288
|
-
*
|
|
18289
|
-
*
|
|
18392
|
+
* capability on this descriptor. Surface as `unknown_capability` listing
|
|
18393
|
+
* the known names so the agent can correct without a round-trip.
|
|
18290
18394
|
*/
|
|
18291
18395
|
requireCapabilityFromBody(c, capability) {
|
|
18292
18396
|
const known = this.jobManager.capabilityNames();
|
|
@@ -18345,25 +18449,9 @@ var DVMServer = class {
|
|
|
18345
18449
|
}
|
|
18346
18450
|
}
|
|
18347
18451
|
/**
|
|
18348
|
-
*
|
|
18349
|
-
*
|
|
18350
|
-
*
|
|
18351
|
-
* The two endpoints used to answer the same caller mistake with two unrelated
|
|
18352
|
-
* shapes — quote gated on auth first, so a wrong field name came back 401
|
|
18353
|
-
* `auth_error` + `sub_reason: "schema_invalid"`, while submit parsed first and
|
|
18354
|
-
* came back 400 `invalid_input`. Both named the offending field (internal-review), so
|
|
18355
|
-
* neither was wrong; an agent just had to recognise two codes and two statuses
|
|
18356
|
-
* for one class of mistake, and the CLI's `parseStructuredError` mapped them to
|
|
18357
|
-
* two different `DvmError`s — which is why its structured-input remediation
|
|
18358
|
-
* (`invalid_input` only) never fired for the live fleet failure internal-review was
|
|
18359
|
-
* written about.
|
|
18360
|
-
*
|
|
18361
|
-
* The 400 wins because it is the only shape both endpoints can produce: a DVM
|
|
18362
|
-
* that declares no `auth` has no 401 to give, and quote didn't validate
|
|
18363
|
-
* `cap.input` at all on that path, so it answered 200 to a body submit would
|
|
18364
|
-
* reject. `display` is carried here as well as `message` because this is now
|
|
18365
|
-
* the shape agents branch on, and it reuses the 401's sentence so the words
|
|
18366
|
-
* relayed to a human don't change with the status.
|
|
18452
|
+
* Shared schema-violation response for quote and submit, including auth-less
|
|
18453
|
+
* DVMs. A 400 classifies caller input without requiring an auth gate; `display`
|
|
18454
|
+
* and `message` carry the same relayable explanation.
|
|
18367
18455
|
*/
|
|
18368
18456
|
invalidInputResponse(c, err) {
|
|
18369
18457
|
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
@@ -18407,13 +18495,13 @@ var DVMServer = class {
|
|
|
18407
18495
|
}
|
|
18408
18496
|
/**
|
|
18409
18497
|
* True when a failed capability-input parse is the auth gate's answer rather
|
|
18410
|
-
* than {@link invalidInputResponse}'s
|
|
18498
|
+
* than {@link invalidInputResponse}'s.
|
|
18411
18499
|
*
|
|
18412
18500
|
* A capability schema may declare the signing-envelope fields required
|
|
18413
18501
|
* (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
|
|
18414
18502
|
* the required shape an *unsigned* request fails the input parse naming
|
|
18415
18503
|
* `pubkey` / `signature` — a signing failure, and answering it with "your
|
|
18416
|
-
* input is invalid: pubkey required" is the
|
|
18504
|
+
* input is invalid: pubkey required" is the mistake in reverse. So a
|
|
18417
18505
|
* parse whose issues are **only** envelope-side falls through to the auth
|
|
18418
18506
|
* gate, which answers 401 `signature_invalid` with the remedy that works.
|
|
18419
18507
|
*
|
|
@@ -18430,13 +18518,13 @@ var DVMServer = class {
|
|
|
18430
18518
|
}
|
|
18431
18519
|
/**
|
|
18432
18520
|
* Mints to advertise to clients on `/v1/info` and `/v1/quote`. Filters the
|
|
18433
|
-
* configured list through the runtime health tracker
|
|
18521
|
+
* configured list through the runtime health tracker. When every
|
|
18434
18522
|
* mint is currently sick it falls back to the still-configured list rather
|
|
18435
18523
|
* than publish an empty rail — a *transient* outage shouldn't read as "no
|
|
18436
18524
|
* Cashu" and push well-behaved agents to bypass the rail. But NUT-incompatible
|
|
18437
|
-
* mints are excluded even from that fallback
|
|
18525
|
+
* mints are excluded even from that fallback: NUT-incompatibility
|
|
18438
18526
|
* is a permanent, deterministic config error the DVM can never settle
|
|
18439
|
-
* against, so
|
|
18527
|
+
* against, so such a mint is never advertised. If *every*
|
|
18440
18528
|
* configured mint is NUT-incompatible the rail correctly goes empty.
|
|
18441
18529
|
*/
|
|
18442
18530
|
advertisedMints() {
|
|
@@ -18865,9 +18953,9 @@ var DVMServer = class {
|
|
|
18865
18953
|
priceMsats,
|
|
18866
18954
|
capabilityName,
|
|
18867
18955
|
requesterTokenHash,
|
|
18868
|
-
//
|
|
18956
|
+
// everything the idempotent-replay path needs to recognise a
|
|
18869
18957
|
// retry of THIS call and re-issue the token its lost response carried.
|
|
18870
|
-
//
|
|
18958
|
+
// `jobId` is the pre-allocated id the payment's ledger draw
|
|
18871
18959
|
// already committed under.
|
|
18872
18960
|
{
|
|
18873
18961
|
requesterToken: jobToken,
|
|
@@ -18906,7 +18994,7 @@ var DVMServer = class {
|
|
|
18906
18994
|
}
|
|
18907
18995
|
}
|
|
18908
18996
|
/**
|
|
18909
|
-
* Book a committed funding as a deposit (
|
|
18997
|
+
* Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
|
|
18910
18998
|
* onto the reporter's durable queue: a funding is a liability the platform
|
|
18911
18999
|
* must show, but a reporting hiccup must never fail the paid request that
|
|
18912
19000
|
* just succeeded.
|
|
@@ -18921,7 +19009,7 @@ var DVMServer = class {
|
|
|
18921
19009
|
);
|
|
18922
19010
|
}
|
|
18923
19011
|
/**
|
|
18924
|
-
* Book a kept short payment as tagged revenue
|
|
19012
|
+
* Book a kept short payment as tagged revenue.
|
|
18925
19013
|
* The proofs are committed and the draw was settled on the spot, so the money
|
|
18926
19014
|
* is earned — but no job exists to carry it, hence the synthetic job id. It
|
|
18927
19015
|
* is deliberately **not** a deposit: nothing is owed back, so a liability
|
|
@@ -18951,7 +19039,7 @@ var DVMServer = class {
|
|
|
18951
19039
|
});
|
|
18952
19040
|
}
|
|
18953
19041
|
/**
|
|
18954
|
-
* The idempotent half of `POST /v1/job
|
|
19042
|
+
* The idempotent half of `POST /v1/job`. A paid submit whose
|
|
18955
19043
|
* response the caller lost is safe to retry — the accumulator's
|
|
18956
19044
|
* `UNIQUE(dvm_id, request_id, proof_secret)` makes the duplicate a no-op —
|
|
18957
19045
|
* but until now it was also useless: the caller got a 409 and the `job_id` /
|
|
@@ -18961,7 +19049,7 @@ var DVMServer = class {
|
|
|
18961
19049
|
* original caller. Proof is reproducing the request, **not** possessing the
|
|
18962
19050
|
* spent Cashu token: the token is a bearer secret, so gating on it alone
|
|
18963
19051
|
* would hand the job (and its result) to anyone who lifted it. The legitimate
|
|
18964
|
-
* retry always has the body — the
|
|
19052
|
+
* retry always has the body — the resume sweep re-POSTs it — while a
|
|
18965
19053
|
* token thief does not. On a descriptor-auth DVM the caller pubkey must match
|
|
18966
19054
|
* too. Anything short of that falls through to the 409 the caller has always
|
|
18967
19055
|
* gotten.
|
|
@@ -18988,8 +19076,8 @@ var DVMServer = class {
|
|
|
18988
19076
|
return buildSubmitResponse(c, prior, prior.requesterToken);
|
|
18989
19077
|
}
|
|
18990
19078
|
/**
|
|
18991
|
-
* The draw-keyed half of idempotent recovery
|
|
18992
|
-
*
|
|
19079
|
+
* The draw-keyed half of idempotent recovery. The signed body's
|
|
19080
|
+
* `(credit_id, draw_id)` matched a recorded draw;
|
|
18993
19081
|
* resolve the job it paid for (the draw carries its pre-allocated job id)
|
|
18994
19082
|
* and re-issue the original response under the SAME proof-of-origin gates
|
|
18995
19083
|
* as the header regime: reproduce the fingerprint, sign as the same caller
|
|
@@ -19078,7 +19166,6 @@ var DVMServer = class {
|
|
|
19078
19166
|
}
|
|
19079
19167
|
return void 0;
|
|
19080
19168
|
}
|
|
19081
|
-
/** Recover one still-pending draw recorded for the stable public job id. */
|
|
19082
19169
|
async recoverDrawAcceptance(opts) {
|
|
19083
19170
|
const dvmId = this.opts.dvmId;
|
|
19084
19171
|
if (!dvmId) return void 0;
|
|
@@ -19110,7 +19197,7 @@ var DVMServer = class {
|
|
|
19110
19197
|
/**
|
|
19111
19198
|
* Recompute the upfront price for a dynamic-priced capability by
|
|
19112
19199
|
* re-invoking its `onQuote.handler` with the parsed job input as `data`
|
|
19113
|
-
*
|
|
19200
|
+
* Returns a 400-style error envelope when the input doesn't
|
|
19114
19201
|
* satisfy `onQuote.schema` or the handler throws — failing closed is the
|
|
19115
19202
|
* right default, since the alternative (running the job with
|
|
19116
19203
|
* `requiredMsats=undefined`) lets unpaid jobs through.
|
|
@@ -19319,13 +19406,6 @@ var DVMServer = class {
|
|
|
19319
19406
|
}
|
|
19320
19407
|
return c.json({ messages: job.messages.filter((m) => m.seq > after) });
|
|
19321
19408
|
}
|
|
19322
|
-
/**
|
|
19323
|
-
* Structured 409 for a write (message POST / cancel) against a job that has
|
|
19324
|
-
* already reached a terminal status (internal-review). The two-audiences convention
|
|
19325
|
-
* wants a `code` an intermediary agent can branch on plus a `hint` it can
|
|
19326
|
-
* relay verbatim — a bare `{ error }` string gave the openclaw nothing to act
|
|
19327
|
-
* on and it re-paid a completed job into this 409 (internal-review step 7).
|
|
19328
|
-
*/
|
|
19329
19409
|
jobNotActiveResponse(c) {
|
|
19330
19410
|
return c.json(
|
|
19331
19411
|
{
|
|
@@ -19498,8 +19578,8 @@ var DVMServer = class {
|
|
|
19498
19578
|
}
|
|
19499
19579
|
/**
|
|
19500
19580
|
* Tear down this machine's in-memory reactivation scaffold when it stands
|
|
19501
|
-
* down
|
|
19502
|
-
* handler that won the single-execution claim
|
|
19581
|
+
* down after losing the reactivation advisory lock or deferring to a live original
|
|
19582
|
+
* handler that won the single-execution claim. Leaving
|
|
19503
19583
|
* the job in `activeJobs` with a live idle timer would, one idle window
|
|
19504
19584
|
* later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
|
|
19505
19585
|
* must not persist our stale snapshot; clearing the appender prevents that.
|
|
@@ -19535,6 +19615,7 @@ var DVMServer = class {
|
|
|
19535
19615
|
await jm.finalizeStoredTerminal(id);
|
|
19536
19616
|
return c.json({ status: "cancelled" });
|
|
19537
19617
|
}
|
|
19618
|
+
record.endedBy = "caller";
|
|
19538
19619
|
record.status = "cancelled";
|
|
19539
19620
|
record.lastActivityAt = Date.now();
|
|
19540
19621
|
await jm.store.save(record);
|
|
@@ -19880,7 +19961,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19880
19961
|
}
|
|
19881
19962
|
}
|
|
19882
19963
|
/**
|
|
19883
|
-
* Register one mount's session capability
|
|
19964
|
+
* Register one mount's session capability.
|
|
19884
19965
|
*
|
|
19885
19966
|
* A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
|
|
19886
19967
|
* platform row that token identifies is the deployment as a whole — so the
|
|
@@ -19903,7 +19984,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19903
19984
|
* mount exists, so at boot there is routinely an open probe carrying **no**
|
|
19904
19985
|
* `tempo_session` at all — handing it back to the mount that just declared
|
|
19905
19986
|
* one would leave a scale-to-zero DVM unstamped until traffic arrived, which
|
|
19906
|
-
* is exactly the container-restart case
|
|
19987
|
+
* is exactly the container-restart case is about. Same shape with
|
|
19907
19988
|
* two mounts: the second would join a probe whose URL already read the first
|
|
19908
19989
|
* one's `false`.
|
|
19909
19990
|
*/
|
|
@@ -19951,7 +20032,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19951
20032
|
}
|
|
19952
20033
|
/**
|
|
19953
20034
|
* The probe URL, carrying this deployment's session capability when a mount
|
|
19954
|
-
* has declared one
|
|
20035
|
+
* has declared one.
|
|
19955
20036
|
*
|
|
19956
20037
|
* Absent means "no signal", not "no sessions": a container running an older
|
|
19957
20038
|
* SDK build sends nothing, and the platform must leave its stamp exactly
|