@dvmkit/sdk 0.1.5-rc.9 → 0.2.1-rc.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/{chunk-MLRCSJYX.js → chunk-2D75MNNG.js} +178 -550
- package/dist/{chunk-BIFLRKMO.js → chunk-2KGG5ORW.js} +1 -1
- package/dist/{chunk-27V2ILSR.js → chunk-4A2RAKCW.js} +2 -2
- package/dist/{chunk-5URG56JJ.js → chunk-A4CM2BD3.js} +18 -2
- package/dist/{chunk-L4OYF4DQ.js → chunk-AJNFXR7J.js} +2 -2
- package/dist/{chunk-S3XAHZQY.js → chunk-COYKJRMM.js} +24 -3
- package/dist/{chunk-UP2F5RRT.js → chunk-D7YQYLLM.js} +1 -4
- package/dist/{chunk-CEOAHV2I.js → chunk-FDKRXOZO.js} +0 -5
- package/dist/{chunk-TQWGQCNV.js → chunk-FPOLJRBF.js} +5 -5
- package/dist/{chunk-BTZY7VPH.js → chunk-GAIPXGM3.js} +1 -1
- package/dist/{chunk-SSSZUVWM.js → chunk-JAHRF5X3.js} +89 -39
- package/dist/{chunk-5PBOA25N.js → chunk-JNKQZVZX.js} +472 -617
- package/dist/{chunk-U6M3ATSG.js → chunk-MD3H3BF7.js} +40 -6
- package/dist/{chunk-CGKZDODG.js → chunk-MG67KXU7.js} +0 -5
- package/dist/{chunk-KSQEFVFJ.js → chunk-MMCE7XSR.js} +1 -1
- package/dist/{chunk-JZWELPFH.js → chunk-N5B66UOS.js} +4 -16
- package/dist/{chunk-BIP6G74V.js → chunk-O6D72KYA.js} +10 -10
- package/dist/{chunk-KXCKFIJI.js → chunk-QX52F6IB.js} +1 -1
- package/dist/{chunk-RW5LP57K.js → chunk-SDK6KDJN.js} +0 -1
- package/dist/{chunk-KVEHHC7W.js → chunk-U5VNEALI.js} +24 -13
- package/dist/{chunk-FROTD5XQ.js → chunk-V5ZRATJH.js} +2 -3
- package/dist/{chunk-EVBK675R.js → chunk-VA7GT6C6.js} +31 -37
- package/dist/{chunk-2UUXIIOC.js → chunk-VIVXB4CD.js} +3 -3
- package/dist/{chunk-6BQM7TOW.js → chunk-XMDQOXHO.js} +11 -38
- package/dist/{chunk-DMNLFNTW.js → chunk-XRLKGCAW.js} +2 -2
- package/dist/{chunk-M7LHFJ5K.js → chunk-XZ7LW4S5.js} +2 -3
- package/dist/{chunk-E4EVGPDX.js → chunk-YO56VCTQ.js} +0 -6
- package/dist/{credit-ledger-2DFQHNLB.js → credit-ledger-MHA652TV.js} +2 -2
- package/dist/{credit-menu-enwMbn55.d.ts → credit-menu-kDA9RVzD.d.ts} +524 -1077
- package/dist/{fx-D860pZvP.d.ts → fx-DhyT1Cjh.d.ts} +38 -82
- package/dist/index.d.ts +11 -26
- package/dist/index.js +2 -2
- package/dist/internal/caller.d.ts +644 -2140
- package/dist/internal/caller.js +32 -66
- package/dist/internal/server.d.ts +40 -90
- package/dist/internal/server.js +16 -16
- package/dist/{job-store-BUGqvCfL.d.ts → job-store-D3CcZp_g.d.ts} +73 -66
- package/dist/{lightning-backend-BozcevPZ.d.ts → lightning-backend-BhsyrBLa.d.ts} +19 -41
- package/dist/{memory-credit-ledger-MNUOTQO5.js → memory-credit-ledger-BYBATRN3.js} +3 -3
- package/dist/{mpp-secret-state-WNAQQ6K4.js → mpp-secret-state-B5GCILVH.js} +1 -1
- package/dist/{mpp-setup-4FJD6ZHV.js → mpp-setup-MPP3ST5X.js} +1 -1
- package/dist/{payout-reporter-RG6XNGPI.js → payout-reporter-KEMUXEUO.js} +2 -2
- package/dist/{postgres-consumed-credential-store-VHBT4KEA.js → postgres-consumed-credential-store-6N2V5MJN.js} +2 -2
- package/dist/postgres-job-store-MZZZMS2B.js +7 -0
- package/dist/postgres-kv-store-DXGTKXLU.js +7 -0
- package/dist/{postgres-replay-store-UJXRT6VO.js → postgres-replay-store-ZF4JNZOW.js} +2 -2
- package/dist/{pricing-4CEB34RM.js → pricing-MU5GNUJZ.js} +1 -1
- package/dist/{processed-payment-store-HAA4SFNK.js → processed-payment-store-GKMAWOM3.js} +2 -2
- package/dist/revenue-reporter-O75BGYFK.js +7 -0
- package/dist/server/index.d.ts +47 -159
- package/dist/server/index.js +67 -138
- package/dist/{ssrf-DbFkpDv0.d.ts → ssrf-dMooihtY.d.ts} +1 -2
- package/dist/{step-cache-5dljDqrQ.d.ts → step-cache-v2yhyhSZ.d.ts} +617 -1532
- package/dist/{tempo-charge-store-RIFTALZK.js → tempo-charge-store-SDTUNTKA.js} +2 -2
- package/dist/{tempo-lifecycle-DFIXQ54Q.js → tempo-lifecycle-G5SB4HFC.js} +5 -5
- package/dist/{tempo-session-store-2JNOKJGX.js → tempo-session-store-WF3VZXNF.js} +2 -2
- package/dist/{tempo-wallet-4QKSV65O.js → tempo-wallet-ZFNUDFVO.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-Cu9G6xv2.d.ts} +14 -16
- package/dist/{wallet-CJC8lwxx.d.ts → wallet-Dwjs5n_M.d.ts} +1 -1
- package/dist/{x402-5H27DCBE.js → x402-CI6PSHNO.js} +5 -5
- package/package.json +3 -2
- package/dist/postgres-job-store-3RAXMNSY.js +0 -7
- package/dist/postgres-kv-store-JFBDP5IP.js +0 -7
- package/dist/revenue-reporter-ASZ7SHHH.js +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MemoryProcessedPaymentStore,
|
|
3
3
|
ProcessedPaymentReplayError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XRLKGCAW.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-U5VNEALI.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-MD3H3BF7.js";
|
|
39
39
|
import {
|
|
40
40
|
ADMIN_AUTH_CLOCK_SKEW_SECONDS,
|
|
41
41
|
ADMIN_AUTH_NONCE_TTL_SECONDS,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
toLockPubkey,
|
|
61
61
|
verifyCashuToken,
|
|
62
62
|
verifyChallenge
|
|
63
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-QX52F6IB.js";
|
|
64
64
|
import {
|
|
65
65
|
X402_BATCH_SETTLEMENT_SCHEME,
|
|
66
66
|
X402_DEFAULT_FACILITATOR,
|
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
msatsToUsd,
|
|
84
84
|
msatsToUsdc,
|
|
85
85
|
usdcToMsats
|
|
86
|
-
} from "./chunk-
|
|
86
|
+
} from "./chunk-A4CM2BD3.js";
|
|
87
87
|
import {
|
|
88
88
|
resolveRpcOverride
|
|
89
89
|
} from "./chunk-MKI6OVW4.js";
|
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
pinDimension,
|
|
101
101
|
withSpan,
|
|
102
102
|
withTraceContext
|
|
103
|
-
} from "./chunk-
|
|
103
|
+
} from "./chunk-YO56VCTQ.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,10 +130,10 @@ import {
|
|
|
130
130
|
isNonChannelBitcoinRail,
|
|
131
131
|
netOwedSats,
|
|
132
132
|
x402SettlementPending
|
|
133
|
-
} from "./chunk-
|
|
133
|
+
} from "./chunk-2D75MNNG.js";
|
|
134
134
|
import {
|
|
135
135
|
withSdkInitLock
|
|
136
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-COYKJRMM.js";
|
|
137
137
|
import {
|
|
138
138
|
withPostgresRecoveryContext
|
|
139
139
|
} from "./chunk-C3MTFLC6.js";
|
|
@@ -141,7 +141,7 @@ import {
|
|
|
141
141
|
advertisedMethods,
|
|
142
142
|
challengeMeta,
|
|
143
143
|
isTempoInsufficientFundsError
|
|
144
|
-
} from "./chunk-
|
|
144
|
+
} from "./chunk-XZ7LW4S5.js";
|
|
145
145
|
|
|
146
146
|
// src/sdk/server/lock-pubkey-store.ts
|
|
147
147
|
async function initLockPubkeyTable(db) {
|
|
@@ -881,22 +881,19 @@ async function buildCreditMenu(args) {
|
|
|
881
881
|
ttl_ms: args.config.ttlSeconds * 1e3,
|
|
882
882
|
currency,
|
|
883
883
|
funding,
|
|
884
|
-
//
|
|
885
|
-
// sessions only would otherwise lose the whole block the moment the gate
|
|
886
|
-
// closed, taking its capability signal with it (internal-review).
|
|
884
|
+
// Keep withheld Tempo capability discoverable even when no method is currently offered.
|
|
887
885
|
...(tempoMethods.length > 0 || tempoWithheld.length > 0) && {
|
|
888
886
|
tempo: {
|
|
889
887
|
methods: tempoMethods,
|
|
890
888
|
...tempoWithheld.length > 0 && { withheld: tempoWithheld }
|
|
891
889
|
}
|
|
892
890
|
},
|
|
893
|
-
// Tied to the rail's own presence, the way `lightning_min_micro` is below:
|
|
894
891
|
// a builder who overrides `paymentMethods` can drop x402 from `funding`
|
|
895
892
|
// while the rail stays wired, and terms for a rail the menu doesn't offer
|
|
896
893
|
// are the same misadvertisement in the other direction.
|
|
897
894
|
...funding.includes("x402") && x402Schemes.length > 0 && { x402: { schemes: x402Schemes } }
|
|
898
895
|
};
|
|
899
|
-
if (
|
|
896
|
+
if (funding.includes("lightning") && args.lightningFundingMinSats !== void 0) {
|
|
900
897
|
try {
|
|
901
898
|
const fx = await args.fxFetcher.fetch();
|
|
902
899
|
menu.lightning_min_micro = lightningFloorMicro(args.lightningFundingMinSats, fx, currency);
|
|
@@ -905,7 +902,7 @@ async function buildCreditMenu(args) {
|
|
|
905
902
|
"credit_menu_lightning_floor_unavailable",
|
|
906
903
|
`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
904
|
);
|
|
908
|
-
menu.funding =
|
|
905
|
+
menu.funding = menu.funding.filter((rail) => rail !== "lightning");
|
|
909
906
|
}
|
|
910
907
|
}
|
|
911
908
|
const echo = await selectPrimaryCredit({
|
|
@@ -1950,7 +1947,7 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
1950
1947
|
...opts.config.autoSettlement === false ? {} : opts.config.autoSettlement,
|
|
1951
1948
|
// Every claim this fleet builds is capped at earned draws — the claim
|
|
1952
1949
|
// timer, `claimAndSettle()`, and the withdraw-race rush claim all route
|
|
1953
|
-
// through this selector
|
|
1950
|
+
// through this selector. A builder-supplied
|
|
1954
1951
|
// `autoSettlement.selectClaimChannels` cannot override it: the ceiling is
|
|
1955
1952
|
// a money invariant, not a policy knob. Upstream's refund job builds its
|
|
1956
1953
|
// bundled claim from storage instead and never consults a selector, which
|
|
@@ -2071,14 +2068,13 @@ async function createX402BatchSettlementServer(opts) {
|
|
|
2071
2068
|
// move money. The voucher base does not: a cooperatively drained and
|
|
2072
2069
|
// reused channel can present the same cumulative maximum again under a
|
|
2073
2070
|
// fresh authorization, which made the second legitimate deposit
|
|
2074
|
-
// collide with the first row
|
|
2071
|
+
// collide with the first row. Conversely, keying by the
|
|
2075
2072
|
// credit effect would let one signed authorization open several rows.
|
|
2076
2073
|
//
|
|
2077
2074
|
// A cooperative refund belongs to one caller drain: after an operator
|
|
2078
2075
|
// writes a failed drain off, the unchanged channel can legitimately
|
|
2079
2076
|
// present the same voucher base under a fresh drain id. Keep that new
|
|
2080
2077
|
// effect out of the old row's `(operation, payment_id)` replay slot
|
|
2081
|
-
// (internal-review).
|
|
2082
2078
|
paymentId: isRefund ? `${paymentBase}:${settlementId}` : depositPaymentId ?? paymentBase,
|
|
2083
2079
|
amount: matched.amount
|
|
2084
2080
|
};
|
|
@@ -3631,7 +3627,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3631
3627
|
// Nothing settled, so the EIP-3009 nonce is unspent and the same
|
|
3632
3628
|
// header is still payable — unlike the post-settlement branch
|
|
3633
3629
|
// below, where saying this would be telling the caller to pay
|
|
3634
|
-
// twice
|
|
3630
|
+
// twice.
|
|
3635
3631
|
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
3632
|
}
|
|
3637
3633
|
);
|
|
@@ -3652,7 +3648,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3652
3648
|
x402Requirements
|
|
3653
3649
|
});
|
|
3654
3650
|
}
|
|
3655
|
-
const { verifyX402Payment } = await import("./x402-
|
|
3651
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
3656
3652
|
const receipt = await verifyX402Payment(
|
|
3657
3653
|
x402Payment,
|
|
3658
3654
|
x402Config,
|
|
@@ -3689,7 +3685,7 @@ async function verifyUpfrontPayment(opts) {
|
|
|
3689
3685
|
`fund.amount_micro (${creditEnv.fund.amountMicro}) does not match the settled payment's value at the quoted price (${fundedMicro}).`,
|
|
3690
3686
|
// `expected_micro` states the valuation, but carries no corrective
|
|
3691
3687
|
// hint here: the facilitator already settled, so re-presenting
|
|
3692
|
-
// this credential would be a second payment
|
|
3688
|
+
// this credential would be a second payment.
|
|
3693
3689
|
{ extra: { expected_micro: fundedMicro } }
|
|
3694
3690
|
);
|
|
3695
3691
|
}
|
|
@@ -4195,16 +4191,16 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4195
4191
|
// Only the Cashu accumulator emits this code, and it evaluates each call's
|
|
4196
4192
|
// own proofs — nothing sums a partial with a later one, on any path. So the
|
|
4197
4193
|
// retry owes the full amount: implicit upfront the shortfall was forfeited
|
|
4198
|
-
//
|
|
4199
|
-
//
|
|
4200
|
-
// instead
|
|
4194
|
+
// mid-job it sits on a credit the per-site hint names
|
|
4195
|
+
// Explicit upfront short deposits use `insufficient_credit`
|
|
4196
|
+
// instead. Telling a caller to "send the difference" produced a
|
|
4201
4197
|
// second short pay either way.
|
|
4202
4198
|
payment_insufficient: {
|
|
4203
4199
|
display: "The payment was less than the amount required, so it didn't cover the request. The full amount is still owed.",
|
|
4204
4200
|
hint: "Retry with a single payment of required_msats \u2014 a partial payment does not reduce what is owed.",
|
|
4205
4201
|
retryable: true
|
|
4206
4202
|
},
|
|
4207
|
-
//
|
|
4203
|
+
// the rail and the ledger leg commit together, but the job row is
|
|
4208
4204
|
// written by a *second* transaction (`verifyAndCredit`). When that one can't
|
|
4209
4205
|
// be made to land, the money is real and the caller's — it just isn't on the
|
|
4210
4206
|
// job. Say so rather than answering a bare 500: the per-site body names the
|
|
@@ -4251,7 +4247,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4251
4247
|
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
4248
|
retryable: false
|
|
4253
4249
|
},
|
|
4254
|
-
//
|
|
4250
|
+
// a refund on this credit's payment channel reached the chain and
|
|
4255
4251
|
// never finished booking, so the escrow behind the balance may already have
|
|
4256
4252
|
// been paid back. Spending it would buy work the service can never collect
|
|
4257
4253
|
// for. The copy must not promise the money is gone *or* that it is safe to
|
|
@@ -4283,7 +4279,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4283
4279
|
hint: "Fund at least min_micro (in the credit block of any quote) and try again.",
|
|
4284
4280
|
retryable: true
|
|
4285
4281
|
},
|
|
4286
|
-
//
|
|
4282
|
+
// a per-rail floor, not the menu-wide minimum — today only the
|
|
4287
4283
|
// lightning rail carries one (the receive channel's routing floor). Refused
|
|
4288
4284
|
// before any invoice is minted, so nothing is ever paid against this code;
|
|
4289
4285
|
// `retryable: false` because re-sending the identical request can never
|
|
@@ -4313,7 +4309,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4313
4309
|
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
4310
|
retryable: true
|
|
4315
4311
|
},
|
|
4316
|
-
//
|
|
4312
|
+
// Distinct from `nothing_to_drain` because the balance is real and
|
|
4317
4313
|
// waiting changes nothing: a reusable Bitcoin balance is a deposit the
|
|
4318
4314
|
// service owes back in the asset it received, and what is left here converts
|
|
4319
4315
|
// to less than one satoshi at the rate its own funding landed at. There is
|
|
@@ -4345,7 +4341,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4345
4341
|
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
4342
|
retryable: false
|
|
4347
4343
|
},
|
|
4348
|
-
//
|
|
4344
|
+
// The voucher verified and the ledger leg rolled back with the
|
|
4349
4345
|
// failed submission, so nothing was charged and nothing about the payment is
|
|
4350
4346
|
// wrong — which is exactly what the `payment_invalid` this replaced got
|
|
4351
4347
|
// backwards, telling an agent to discard a credential that is still good. The
|
|
@@ -4422,7 +4418,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4422
4418
|
hint: "The token's proofs are pending at the mint; retry in a few seconds.",
|
|
4423
4419
|
retryable: true
|
|
4424
4420
|
},
|
|
4425
|
-
//
|
|
4421
|
+
// Not failures — the two `lightning_*` states below are the normal
|
|
4426
4422
|
// shape of an interactive funding leg, which is why both are retryable and
|
|
4427
4423
|
// neither `display` implies anything went wrong.
|
|
4428
4424
|
lightning_settlement_pending: {
|
|
@@ -4442,7 +4438,7 @@ var PAYMENT_ERROR_COPY = {
|
|
|
4442
4438
|
},
|
|
4443
4439
|
replay_rejected: {
|
|
4444
4440
|
display: "This payment was already submitted, so it wasn't charged again.",
|
|
4445
|
-
//
|
|
4441
|
+
// this hint reaches a caller whose money already landed — the
|
|
4446
4442
|
// accumulator holds these proofs under this request id. It used to read
|
|
4447
4443
|
// "use a fresh token and a new request id for the next payment", which an
|
|
4448
4444
|
// agent could follow straight into paying twice for the same charge.
|
|
@@ -4586,7 +4582,7 @@ async function verifyIncomingPayment(body, opts, snapshot) {
|
|
|
4586
4582
|
snapshot
|
|
4587
4583
|
});
|
|
4588
4584
|
}
|
|
4589
|
-
const { verifyX402Payment } = await import("./x402-
|
|
4585
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
4590
4586
|
const receipt = await verifyX402Payment(
|
|
4591
4587
|
body.content.x402_payment,
|
|
4592
4588
|
opts.x402Config,
|
|
@@ -5003,7 +4999,7 @@ async function processIncomingPayment(job, body, opts) {
|
|
|
5003
4999
|
try {
|
|
5004
5000
|
const requiredMsats = job.pendingPaymentMsats ?? 0;
|
|
5005
5001
|
const requiredUsdcMicro = job.pendingX402AmountUsdcMicro !== void 0 ? BigInt(job.pendingX402AmountUsdcMicro) : BigInt(msatsToUsdc(requiredMsats, rate));
|
|
5006
|
-
const { verifyX402Payment } = await import("./x402-
|
|
5002
|
+
const { verifyX402Payment } = await import("./x402-CI6PSHNO.js");
|
|
5007
5003
|
const receipt = await verifyX402Payment(
|
|
5008
5004
|
body.content.x402_payment,
|
|
5009
5005
|
opts.x402Config,
|
|
@@ -5307,7 +5303,7 @@ async function verifyAccumulatorReceipt(opts, mints, requiredMsats, ledgerCtx) {
|
|
|
5307
5303
|
// requestId is non-null here because acceptAccumulatorPayment returns
|
|
5308
5304
|
// kind:"rejected" with code "missing_request_id" when unset (handled by
|
|
5309
5305
|
// the rejected branch above). Thread it as the per-call settlement
|
|
5310
|
-
// reference for revenue_events.tx_hash
|
|
5306
|
+
// reference for revenue_events.tx_hash. Unique per
|
|
5311
5307
|
// (dvm_id, request_id) by the wallet_accumulator UNIQUE constraint, so
|
|
5312
5308
|
// it serves as a stable id even though it isn't an on-chain hash.
|
|
5313
5309
|
paymentTxHash: opts.requestId,
|
|
@@ -5456,7 +5452,7 @@ async function resolveTopUpLedgerContext(opts, snapshot, binding) {
|
|
|
5456
5452
|
amount_micro: priceFiat.amountMicro,
|
|
5457
5453
|
// Which rung degraded: the whole snapshot fell back to `lastKnown()`, or
|
|
5458
5454
|
// the live fetch landed without this currency and it was carried forward
|
|
5459
|
-
// from an earlier one
|
|
5455
|
+
// from an earlier one. The first is the rate source down, the
|
|
5460
5456
|
// second is the rate source having quietly stopped carrying a currency
|
|
5461
5457
|
// this DVM advertises — a config problem, and a different repair.
|
|
5462
5458
|
...denomination.carriedForward === true && { rate_carried_forward: true },
|
|
@@ -5687,7 +5683,7 @@ async function fundAndDraw(ctx, funding, tx) {
|
|
|
5687
5683
|
creditId: creditId2,
|
|
5688
5684
|
drawId: drawId2,
|
|
5689
5685
|
addAmountMicro: fundedMicro,
|
|
5690
|
-
//
|
|
5686
|
+
// never grow the draw past what the job cumulatively asked.
|
|
5691
5687
|
// The available-balance guard inside `growDraw` cannot do this — the
|
|
5692
5688
|
// `fund` above raised the balance by exactly this amount — so a second
|
|
5693
5689
|
// payment for one ask would otherwise be absorbed and settle as revenue.
|
|
@@ -5800,7 +5796,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5800
5796
|
creditCurrency: funding.currency,
|
|
5801
5797
|
fundedAt: funding.fundedAt,
|
|
5802
5798
|
expiryMs: funding.expiryMs,
|
|
5803
|
-
//
|
|
5799
|
+
// label a session-funded deposit as one, whether or not the chain
|
|
5804
5800
|
// binding below could be read. `rail: "tempo"` alone does not say — a
|
|
5805
5801
|
// `tempo/charge` one-shot funds a credit with no channel at all — so this is
|
|
5806
5802
|
// what lets the platform tell a routine charge from a session whose binding
|
|
@@ -5808,7 +5804,7 @@ function creditDepositPayload(dvmId, funding) {
|
|
|
5808
5804
|
...funding.tempoSession && {
|
|
5809
5805
|
tempoSession: { channelId: funding.tempoSession.channelId.toLowerCase() }
|
|
5810
5806
|
},
|
|
5811
|
-
//
|
|
5807
|
+
// the channel registration rides the deposit report, so it
|
|
5812
5808
|
// commits with the voucher acceptance that created the channel's liability.
|
|
5813
5809
|
...funding.tempoChannel && funding.tempoSession && {
|
|
5814
5810
|
tempoChannel: {
|
|
@@ -6075,7 +6071,7 @@ function mapLedgerCommitError(err, x402Requirements, paymentCommitted = false) {
|
|
|
6075
6071
|
receivedProofs: [],
|
|
6076
6072
|
error: {
|
|
6077
6073
|
// A fixed message plus the reason enum, never `err.message`: that string
|
|
6078
|
-
// is upstream's
|
|
6074
|
+
// is upstream's and may include the relay lock holder's
|
|
6079
6075
|
// backend pid and `client_addr`. The detail is logged where the throw is.
|
|
6080
6076
|
body: paymentErrorBody("x402_settlement_submission_failed", {
|
|
6081
6077
|
message: "x402 batch settlement submission failed",
|
|
@@ -6674,7 +6670,7 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6674
6670
|
/** Client-generated idempotency key — a retry with the same id credits once. */
|
|
6675
6671
|
fund_id: z.string().min(1),
|
|
6676
6672
|
/**
|
|
6677
|
-
* SHA-256 hex over the raw funding artifact (
|
|
6673
|
+
* SHA-256 hex over the raw funding artifact (the funding-commitment rule).
|
|
6678
6674
|
* Required for every attach rail; absent on `method: "lightning"`, which
|
|
6679
6675
|
* carries no caller-supplied artifact to hash — see {@link CreditRequest}.
|
|
6680
6676
|
*/
|
|
@@ -6692,14 +6688,8 @@ var CREDIT_REQUEST_SCHEMA = z.object({
|
|
|
6692
6688
|
/** Client-generated idempotency key — the same id polls/collects one drain. */
|
|
6693
6689
|
drain_id: z.string().min(1),
|
|
6694
6690
|
/**
|
|
6695
|
-
*
|
|
6696
|
-
*
|
|
6697
|
-
* Deliberately a bare string rather than the `DrainMethod` enum, so a
|
|
6698
|
-
* rail this build does not carry is answered by `validateDrainTarget`
|
|
6699
|
-
* with a typed `drain_method_unsupported` naming the rails that do
|
|
6700
|
-
* work — not by a Zod parse failure that says only "invalid enum". The
|
|
6701
|
-
* caller of a removed rail (`lightning`, internal-review) and the caller of a
|
|
6702
|
-
* typo deserve the same actionable refusal.
|
|
6691
|
+
* Required on a fresh drain, optional on poll. Keep this a string so an unsupported
|
|
6692
|
+
* rail receives its specific typed refusal before payout-shape validation.
|
|
6703
6693
|
*/
|
|
6704
6694
|
method: z.string().min(1).optional(),
|
|
6705
6695
|
/**
|
|
@@ -6825,17 +6815,12 @@ async function handleBalance(c, deps, args) {
|
|
|
6825
6815
|
const primary = pickPrimary(all, args.menu.currency);
|
|
6826
6816
|
return c.json({
|
|
6827
6817
|
op: "balance",
|
|
6828
|
-
//
|
|
6829
|
-
//
|
|
6818
|
+
// Keep expired credit records visible. Nominal expiry alone does not release ownership;
|
|
6819
|
+
// an expiry sweep can later zero the undrawn balance.
|
|
6830
6820
|
credits: all.map(
|
|
6831
6821
|
(credit) => creditBody(credit, pendingChannels.has(credit.tempoChannelId ?? ""))
|
|
6832
6822
|
),
|
|
6833
6823
|
...primary ? { credit: creditBody(primary, pendingChannels.has(primary.tempoChannelId ?? "")) } : {},
|
|
6834
|
-
// internal-review: the funding menu the quote and 402 surfaces already advertise,
|
|
6835
|
-
// echoed here so one read answers "what do I hold, and what may I add".
|
|
6836
|
-
// Under the key `menu`, not `credit` — that one is the credit projection on
|
|
6837
|
-
// this route. Carried without the caller echo fields: `credits[]` above says
|
|
6838
|
-
// the same thing in more detail, so `deps.menu()` is called without a pubkey.
|
|
6839
6824
|
menu: args.menu
|
|
6840
6825
|
});
|
|
6841
6826
|
}
|
|
@@ -7114,11 +7099,8 @@ async function completeTempoDrain(c, deps, record, expectedChannelId, credit) {
|
|
|
7114
7099
|
resourcePath: c.req.path,
|
|
7115
7100
|
action: "close",
|
|
7116
7101
|
expectedChannelId,
|
|
7117
|
-
//
|
|
7118
|
-
//
|
|
7119
|
-
// broadcasts (internal-review). Run above the call instead and a credential
|
|
7120
|
-
// signed for another channel touches the reservation on its way to a
|
|
7121
|
-
// refusal that was never going to close anything.
|
|
7102
|
+
// Reassert the intent only after the verifier binds the credential to this channel, and
|
|
7103
|
+
// before broadcast. A wrong-channel signature cannot create or cancel its close intent.
|
|
7122
7104
|
onBound: async () => {
|
|
7123
7105
|
await beginClose(expectedChannelId, record.drainId);
|
|
7124
7106
|
recoveryVersion = await deps.tempoSettlementReadiness?.captureRecoveryVersion();
|
|
@@ -7420,9 +7402,6 @@ async function handleX402ChannelDrain(c, deps, args) {
|
|
|
7420
7402
|
amount: "0",
|
|
7421
7403
|
operation: "refund",
|
|
7422
7404
|
effectId: `${args.credit.creditId}:${args.drainId}`,
|
|
7423
|
-
// The voucher the caller presents is what actually gets revoked and paid,
|
|
7424
|
-
// so the settlement layer refuses one signed for any other channel before
|
|
7425
|
-
// it touches anything (internal-review).
|
|
7426
7405
|
expectedChannelId: channelId,
|
|
7427
7406
|
refundBoundNative,
|
|
7428
7407
|
commit: async (_funding, tx) => {
|
|
@@ -7641,12 +7620,9 @@ function drainBody(record) {
|
|
|
7641
7620
|
status: record.status,
|
|
7642
7621
|
amount_micro: record.amountMicro,
|
|
7643
7622
|
currency: record.currency,
|
|
7644
|
-
//
|
|
7645
|
-
//
|
|
7646
|
-
//
|
|
7647
|
-
// does not move with the market. Published beside the fiat one rather than
|
|
7648
|
-
// instead of it — the fiat figure is what the caller's balance was
|
|
7649
|
-
// denominated in, and both are things an agent relays.
|
|
7623
|
+
// Fixed in-kind Bitcoin refund obligation after the delivery reserve. It reflects the
|
|
7624
|
+
// funding lots remaining at request time; null means no whole-balance in-kind price was
|
|
7625
|
+
// available.
|
|
7650
7626
|
...record.owedSats !== null ? { owed_sats: record.owedSats } : {},
|
|
7651
7627
|
ledger_seq: record.ledgerSeq,
|
|
7652
7628
|
created_at: record.createdAt,
|
|
@@ -7660,17 +7636,7 @@ function drainCopy(record) {
|
|
|
7660
7636
|
case "pending":
|
|
7661
7637
|
if (record.method === "cashu") {
|
|
7662
7638
|
return {
|
|
7663
|
-
display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." :
|
|
7664
|
-
// Named because it is the obligation, and because it is
|
|
7665
|
-
// fixed: a Bitcoin refund returns the share of the Bitcoin you
|
|
7666
|
-
// deposited that you never spent, so it does not move if the
|
|
7667
|
-
// price does before you collect it (internal-review). The mint's own
|
|
7668
|
-
// redemption charge is named rather than glossed — it is a sat
|
|
7669
|
-
// or two at a fee-charging mint and nothing at a free one, and
|
|
7670
|
-
// a figure an agent relays to a human should not be a promise
|
|
7671
|
-
// that lands short.
|
|
7672
|
-
`The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`
|
|
7673
|
-
),
|
|
7639
|
+
display: record.owedSats === null ? "The refund is approved and being prepared. The balance is already set aside for you." : `The refund is approved and being prepared: ${record.owedSats} sats, the share of the Bitcoin you deposited that you haven't spent, less whatever the mint charges to hand it over. That figure won't move while you wait, whatever the price does.`,
|
|
7674
7640
|
hint: "Poll this op with the same drain_id until a token appears. Preparation can be delayed (e.g. the mint is temporarily down) \u2014 the refund stays owed to you either way."
|
|
7675
7641
|
};
|
|
7676
7642
|
}
|
|
@@ -7796,11 +7762,6 @@ async function handleFund(c, deps, args) {
|
|
|
7796
7762
|
fundId: fund.fund_id,
|
|
7797
7763
|
amountMicro: fund.amount_micro,
|
|
7798
7764
|
menu,
|
|
7799
|
-
// Lightning and MPP name their rail in the signed body. Header-attached
|
|
7800
|
-
// Cashu and x402 are discriminated by `verifyUpfrontPayment`, which still
|
|
7801
|
-
// refuses their mismatches before money moves. Lightning must be checked
|
|
7802
|
-
// here because past this point the bolt11 is minted and payable; MPP is
|
|
7803
|
-
// checked here so discovery never advertises the wrong Tempo instrument.
|
|
7804
7765
|
rail: fund.method,
|
|
7805
7766
|
tempoIntent: fund.method === "tempo" ? fund.intent : void 0
|
|
7806
7767
|
});
|
|
@@ -8091,9 +8052,8 @@ function lightningInvoiceBlock(invoice) {
|
|
|
8091
8052
|
invoice: invoice.bolt11,
|
|
8092
8053
|
payment_hash: invoice.paymentHash,
|
|
8093
8054
|
amount_msats: invoice.amountMsats,
|
|
8094
|
-
//
|
|
8095
|
-
//
|
|
8096
|
-
// in seconds, and a bare `expires_at` invites a reader to assume that.
|
|
8055
|
+
// Wire timestamp is milliseconds; bolt11 lifetimes are conventionally expressed in
|
|
8056
|
+
// seconds.
|
|
8097
8057
|
expires_at_ms: invoice.expiresAt
|
|
8098
8058
|
}
|
|
8099
8059
|
};
|
|
@@ -8324,26 +8284,13 @@ var DRAIN_CONFLICT_COPY = {
|
|
|
8324
8284
|
retryable: true,
|
|
8325
8285
|
drainRegistered: false
|
|
8326
8286
|
},
|
|
8327
|
-
// Deliberately rail-neutral: `refundFloorEvidence` is one floor over both
|
|
8328
|
-
// channel rails, and two doors with two answers about one shortfall is the
|
|
8329
|
-
// drift consolidating it was meant to prevent (internal-review). Tempo answers its
|
|
8330
|
-
// own through `tempoFloorRefusal`, which reaches the same conclusion a step
|
|
8331
|
-
// earlier and adds the one rail-specific fact — its channel is already closed.
|
|
8332
8287
|
refund_floor: {
|
|
8333
8288
|
display: "The refund reached the chain, but it paid less than the balance this service had recorded, so nothing was booked here. Your on-chain refund arrived; the difference has to be reconciled by the service operator.",
|
|
8334
8289
|
hint: ({ creditId, drainId }) => `Report drain ${drainId} on credit ${creditId} to the service operator \u2014 only they can reconcile the difference. Retrying this drain, or requesting a new drain_id, lands here again.`,
|
|
8335
|
-
// Terminal for the caller, unlike the two above: no amount of waiting or
|
|
8336
|
-
// re-signing changes what the chain already paid.
|
|
8337
8290
|
retryable: false,
|
|
8338
|
-
//
|
|
8339
|
-
//
|
|
8340
|
-
//
|
|
8341
|
-
// `effect_id`, which is the id `X402SettlementRepair.reconcile` books the
|
|
8342
|
-
// drain under — so the caller has to keep it. Dropping it mints a fresh one
|
|
8343
|
-
// that the wedged-channel gate refuses as `settlement_pending` anyway, and
|
|
8344
|
-
// re-presenting *this* id is that gate's own documented exemption. Tempo
|
|
8345
|
-
// reaches the same `true` one artifact earlier, off a `credit_drains` row
|
|
8346
|
-
// its close-intent reservation already committed (internal-review).
|
|
8291
|
+
// The settlement evidence survives outside the rolled-back ledger transaction. Keep
|
|
8292
|
+
// drain_registered:true so the caller retains this effect ID for repair even when no
|
|
8293
|
+
// drain row committed.
|
|
8347
8294
|
drainRegistered: true
|
|
8348
8295
|
}
|
|
8349
8296
|
};
|
|
@@ -8389,8 +8336,6 @@ function drainLedgerErrorResponse(error, creditId, drainId) {
|
|
|
8389
8336
|
message: error.message,
|
|
8390
8337
|
display: copy?.display,
|
|
8391
8338
|
hint: copy?.hint({ creditId, drainId }),
|
|
8392
|
-
// An unclassified conflict keeps the duplicate-payout copy's own
|
|
8393
|
-
// `retryable`, and the id it was never told anything about.
|
|
8394
8339
|
retryable: copy?.retryable,
|
|
8395
8340
|
extra: { drain_registered: copy?.drainRegistered ?? false }
|
|
8396
8341
|
});
|
|
@@ -8441,8 +8386,8 @@ function creditBody(credit, reconciliationPending = false) {
|
|
|
8441
8386
|
remaining_micro: credit.availableMicro,
|
|
8442
8387
|
expiry_ms: credit.expiryMs,
|
|
8443
8388
|
expired: credit.expired,
|
|
8444
|
-
//
|
|
8445
|
-
//
|
|
8389
|
+
// Nominal expiry does not itself reclaim ownership; a committed sweep can remove undrawn
|
|
8390
|
+
// value.
|
|
8446
8391
|
drainable: credit.status === "active" && credit.availableMicro > 0,
|
|
8447
8392
|
...reconciliationPending && { reconciliation_pending: true },
|
|
8448
8393
|
ledger_seq: credit.lastLedgerSeq
|
|
@@ -8976,11 +8921,9 @@ var X402ExactSettlementServer = class {
|
|
|
8976
8921
|
}
|
|
8977
8922
|
return this.finalize(intent, args.btcUsdRate, args.commit);
|
|
8978
8923
|
}
|
|
8979
|
-
/** Read the operator queue. */
|
|
8980
8924
|
list(args) {
|
|
8981
8925
|
return this.opts.store.list(args);
|
|
8982
8926
|
}
|
|
8983
|
-
/** Read durable attempt evidence for one intent. */
|
|
8984
8927
|
attempts(dvmId, intentId) {
|
|
8985
8928
|
return this.opts.store.attempts(dvmId, intentId);
|
|
8986
8929
|
}
|
|
@@ -9315,13 +9258,13 @@ function buildContext(job, opts) {
|
|
|
9315
9258
|
if (terminated("prompt")) {
|
|
9316
9259
|
return Promise.reject(new JobCancelledError(`Job already ${job.status}`));
|
|
9317
9260
|
}
|
|
9261
|
+
job.status = "awaiting-input";
|
|
9318
9262
|
providerMessage(job, "prompt", {
|
|
9319
9263
|
id,
|
|
9320
9264
|
text,
|
|
9321
9265
|
...promptOpts?.options && { options: promptOpts.options },
|
|
9322
9266
|
...promptOpts?.schema && { schema: promptOpts.schema }
|
|
9323
9267
|
});
|
|
9324
|
-
job.status = "awaiting-input";
|
|
9325
9268
|
if (job.replayHighSeq !== null) {
|
|
9326
9269
|
const response = job.messages.find(
|
|
9327
9270
|
(m) => m.type === "response" && m.from === "requester" && m.content.prompt_id === id
|
|
@@ -9435,7 +9378,7 @@ function buildContext(job, opts) {
|
|
|
9435
9378
|
poolBasis: pool.basis,
|
|
9436
9379
|
askCurrency,
|
|
9437
9380
|
// `draw` here means the pool read fine and either the ASK wouldn't pin
|
|
9438
|
-
//
|
|
9381
|
+
// an fx outage on a bare-msats ask, or a sub-micro amount — or it
|
|
9439
9382
|
// pinned in a currency this pool isn't denominated in.
|
|
9440
9383
|
...pool.basis === "draw" && { poolCurrency: pool.currency }
|
|
9441
9384
|
});
|
|
@@ -9570,8 +9513,8 @@ function buildContext(job, opts) {
|
|
|
9570
9513
|
ask_msats: amountMsats
|
|
9571
9514
|
});
|
|
9572
9515
|
}
|
|
9573
|
-
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9574
9516
|
job.status = "awaiting-input";
|
|
9517
|
+
providerMessage(job, "payment-request", paymentRequestContent);
|
|
9575
9518
|
const paidMsatsAtYield = job.paidMsats;
|
|
9576
9519
|
const p = new Promise((resolve, reject) => {
|
|
9577
9520
|
let resolved = false;
|
|
@@ -9580,12 +9523,24 @@ function buildContext(job, opts) {
|
|
|
9580
9523
|
unsubscribeNotify?.();
|
|
9581
9524
|
unsubscribeNotify = void 0;
|
|
9582
9525
|
};
|
|
9583
|
-
const doResolve = (pc) => {
|
|
9526
|
+
const doResolve = async (pc) => {
|
|
9584
9527
|
if (resolved) return;
|
|
9585
9528
|
resolved = true;
|
|
9586
9529
|
cleanup();
|
|
9587
9530
|
creditedToHandlerMsats += amountMsats;
|
|
9588
9531
|
creditedToHandlerMicro += pendingFiat?.amountMicro ?? 0;
|
|
9532
|
+
if (job.pendingPayment?.resolve === doResolve) {
|
|
9533
|
+
job.pendingPayment = null;
|
|
9534
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
9535
|
+
if (job.status === "awaiting-input" && opts.onYield) {
|
|
9536
|
+
try {
|
|
9537
|
+
await opts.onYield();
|
|
9538
|
+
} catch (err) {
|
|
9539
|
+
reject(err);
|
|
9540
|
+
return;
|
|
9541
|
+
}
|
|
9542
|
+
}
|
|
9543
|
+
}
|
|
9589
9544
|
resolve(pc);
|
|
9590
9545
|
};
|
|
9591
9546
|
const doReject = (err) => {
|
|
@@ -9602,11 +9557,17 @@ function buildContext(job, opts) {
|
|
|
9602
9557
|
if (resolved) return;
|
|
9603
9558
|
try {
|
|
9604
9559
|
const counters = await getCounters();
|
|
9605
|
-
if (!counters) return;
|
|
9560
|
+
if (!counters || resolved) return;
|
|
9606
9561
|
const satisfied = poolBasis ? counters.paidMsats - paidMsatsAtYield >= amountMsats : counters.paidMsats - creditedToHandlerMsats >= amountMsats;
|
|
9607
9562
|
if (satisfied) {
|
|
9563
|
+
const resume = async () => {
|
|
9564
|
+
job.paidMsats = counters.paidMsats;
|
|
9565
|
+
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9566
|
+
await doResolve({ amount_msats: amountMsats });
|
|
9567
|
+
};
|
|
9608
9568
|
if (opts.claimForProcessing) {
|
|
9609
|
-
const won = await opts.claimForProcessing();
|
|
9569
|
+
const won = await opts.claimForProcessing(resume);
|
|
9570
|
+
if (resolved) return;
|
|
9610
9571
|
if (!won) {
|
|
9611
9572
|
resolved = true;
|
|
9612
9573
|
cleanup();
|
|
@@ -9614,10 +9575,7 @@ function buildContext(job, opts) {
|
|
|
9614
9575
|
return;
|
|
9615
9576
|
}
|
|
9616
9577
|
}
|
|
9617
|
-
|
|
9618
|
-
job.pendingPaymentMsats = counters.pendingPaymentMsats;
|
|
9619
|
-
if (job.status === "awaiting-input") job.status = "processing";
|
|
9620
|
-
doResolve({ amount_msats: amountMsats });
|
|
9578
|
+
await resume();
|
|
9621
9579
|
}
|
|
9622
9580
|
} catch (err) {
|
|
9623
9581
|
log.error("requestPayment NOTIFY check failed", { error: String(err) });
|
|
@@ -9668,12 +9626,12 @@ function buildContext(job, opts) {
|
|
|
9668
9626
|
runTerminal();
|
|
9669
9627
|
},
|
|
9670
9628
|
// `failOpts.refund` is a caller-fault annotation and nothing more
|
|
9671
|
-
//
|
|
9629
|
+
// No rail hands value backwards: a failed job's debit is the
|
|
9672
9630
|
// credit draw the terminal funnel releases, and the caller reclaims that
|
|
9673
|
-
// balance through the
|
|
9631
|
+
// balance through the `drain` op. The Cashu branch that used to
|
|
9674
9632
|
// send proofs back here was dead — `job.receivedProofs` has been empty on
|
|
9675
9633
|
// every path since the accumulator became the commit boundary for inbound
|
|
9676
|
-
// proofs
|
|
9634
|
+
// proofs — and the accumulator-debit rewrite that would
|
|
9677
9635
|
// have revived it, was cancelled as superseded by the ledger.
|
|
9678
9636
|
fail: (error) => {
|
|
9679
9637
|
if (terminated("fail")) return;
|
|
@@ -9742,16 +9700,10 @@ var ReceiptIssuer = class {
|
|
|
9742
9700
|
this.pubkey = receiptPubkeyFromSecret(opts.secret);
|
|
9743
9701
|
}
|
|
9744
9702
|
/**
|
|
9745
|
-
*
|
|
9746
|
-
*
|
|
9747
|
-
*
|
|
9748
|
-
*
|
|
9749
|
-
* sequence number and still attest an outcome. Whether feedback is
|
|
9750
|
-
* paid-only is the feedback layer's call, not this one's.
|
|
9751
|
-
*
|
|
9752
|
-
* `credit` is the resolved draw block (internal-review) — present on every job
|
|
9753
|
-
* whose payment funded/drew the ledger, absent otherwise. Signature-
|
|
9754
|
-
* compatible either way: `canonicalize` omits absent keys.
|
|
9703
|
+
* seq is store-allocated; issuedAt is epoch seconds. Free jobs also consume a
|
|
9704
|
+
* sequence and attest their outcome. Timing/attribution comes from the committed
|
|
9705
|
+
* terminal row; legacy rows omit unknown historical fields. credit is present
|
|
9706
|
+
* only for a resolved ledger draw.
|
|
9755
9707
|
*/
|
|
9756
9708
|
issue(record, seq, issuedAt, credit) {
|
|
9757
9709
|
const unsigned = {
|
|
@@ -9762,6 +9714,14 @@ var ReceiptIssuer = class {
|
|
|
9762
9714
|
capability: record.capability,
|
|
9763
9715
|
outcome: outcomeFor(record.status),
|
|
9764
9716
|
reason: terminalReason(record),
|
|
9717
|
+
...record.timingKnown !== false && record.terminalAtMs !== void 0 && {
|
|
9718
|
+
timing: {
|
|
9719
|
+
accepted_at_ms: record.createdAt,
|
|
9720
|
+
terminal_at_ms: record.terminalAtMs,
|
|
9721
|
+
caller_wait_ms: record.callerWaitMs ?? 0
|
|
9722
|
+
}
|
|
9723
|
+
},
|
|
9724
|
+
...record.timingKnown !== false && record.status !== "completed" && record.endedBy && { ended_by: record.endedBy },
|
|
9765
9725
|
seq,
|
|
9766
9726
|
issued_at: issuedAt ?? Math.floor(Date.now() / 1e3),
|
|
9767
9727
|
requester_pubkey: record.requesterPubkey ?? null,
|
|
@@ -9780,7 +9740,7 @@ var ReceiptIssuer = class {
|
|
|
9780
9740
|
return signReceipt(unsigned, this.secret);
|
|
9781
9741
|
}
|
|
9782
9742
|
/**
|
|
9783
|
-
* Countersign one reclaim event
|
|
9743
|
+
* Countersign one reclaim event. Unlike job receipts there is
|
|
9784
9744
|
* no store-allocated sequence — the drain's own `ledger_seq` (taken under
|
|
9785
9745
|
* the credit row lock, shared with draws) already orders it in the
|
|
9786
9746
|
* per-credit evidence chain.
|
|
@@ -9854,7 +9814,6 @@ function resultHashFor(record) {
|
|
|
9854
9814
|
}
|
|
9855
9815
|
|
|
9856
9816
|
// src/lib/cashu/mint-info.ts
|
|
9857
|
-
var MINT_INFO_TTL_MS = 5 * 6e4;
|
|
9858
9817
|
async function getKeysetFinalExpiry(mintUrl, keysetId) {
|
|
9859
9818
|
try {
|
|
9860
9819
|
const wallet = await getWallet(mintUrl);
|
|
@@ -10097,12 +10056,16 @@ var JobManager = class {
|
|
|
10097
10056
|
cleanupTimers = /* @__PURE__ */ new Map();
|
|
10098
10057
|
/** Cost revisions held only by a terminal handler owner until its durable merge succeeds. */
|
|
10099
10058
|
terminalCostHandoffRetryTimers = /* @__PURE__ */ new Map();
|
|
10100
|
-
/** Per-job NOTIFY unsubscribe handles for the durable-status watch
|
|
10059
|
+
/** Per-job NOTIFY unsubscribe handles for the durable-status watch. */
|
|
10101
10060
|
statusWatchers = /* @__PURE__ */ new Map();
|
|
10102
|
-
/** Job ids whose durable-status re-read is in flight — coalesces notify storms
|
|
10061
|
+
/** Job ids whose durable-status re-read is in flight — coalesces notify storms. */
|
|
10103
10062
|
statusChecksInFlight = /* @__PURE__ */ new Set();
|
|
10104
|
-
/** Job ids that were notified mid-re-read and must be re-checked
|
|
10063
|
+
/** Job ids that were notified mid-re-read and must be re-checked. */
|
|
10105
10064
|
statusChecksQueued = /* @__PURE__ */ new Set();
|
|
10065
|
+
// Every local input resume holds this queue through its durable wait update.
|
|
10066
|
+
// A prompt response and a payment NOTIFY must not mistake each other for a
|
|
10067
|
+
// different machine's successful claim.
|
|
10068
|
+
inputResumeTails = /* @__PURE__ */ new WeakMap();
|
|
10106
10069
|
sessionId = randomBytes4(3).toString("hex");
|
|
10107
10070
|
nextJobId = 1;
|
|
10108
10071
|
accumulatorMonitor;
|
|
@@ -10110,7 +10073,7 @@ var JobManager = class {
|
|
|
10110
10073
|
processingWatchdogMs;
|
|
10111
10074
|
reactivationClaimGraceMs;
|
|
10112
10075
|
/**
|
|
10113
|
-
* Worker-heartbeat cadence
|
|
10076
|
+
* Worker-heartbeat cadence. Also floors the clock
|
|
10114
10077
|
* guard's compensation on the processing arm — see
|
|
10115
10078
|
* {@link staleSweepCompensationCapMs}.
|
|
10116
10079
|
*/
|
|
@@ -10130,7 +10093,7 @@ var JobManager = class {
|
|
|
10130
10093
|
jobRetentionResumeFrom;
|
|
10131
10094
|
/**
|
|
10132
10095
|
* A `Date.now()` reading that cannot regress within this manager's lifetime
|
|
10133
|
-
*
|
|
10096
|
+
* Anchored at construction, so a manager already running when
|
|
10134
10097
|
* the clock stepped is covered. The stale-job sweep's two cutoffs, the
|
|
10135
10098
|
* orphan-draw age gate and the reactivation claim grace all read it; see
|
|
10136
10099
|
* {@link createMonotonicClock} for what it costs and where it is clamped.
|
|
@@ -10143,7 +10106,7 @@ var JobManager = class {
|
|
|
10143
10106
|
jobRetentionSweepInFlight = false;
|
|
10144
10107
|
heartbeatInFlight = false;
|
|
10145
10108
|
/**
|
|
10146
|
-
* The denomination of every credit this DVM opens
|
|
10109
|
+
* The denomination of every credit this DVM opens. Resolved once,
|
|
10147
10110
|
* with the same boundary fallback `DVMServer` applies for direct JavaScript
|
|
10148
10111
|
* callers that force an incomplete value past the descriptor contract.
|
|
10149
10112
|
*/
|
|
@@ -10158,7 +10121,7 @@ var JobManager = class {
|
|
|
10158
10121
|
this.jobStore = new MemoryJobStore();
|
|
10159
10122
|
} else {
|
|
10160
10123
|
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).
|
|
10124
|
+
"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
10125
|
);
|
|
10163
10126
|
}
|
|
10164
10127
|
if (opts.receiptIssuer && !isReceiptIssuingStore(this.jobStore)) {
|
|
@@ -10166,7 +10129,7 @@ var JobManager = class {
|
|
|
10166
10129
|
JSON.stringify({
|
|
10167
10130
|
level: "warn",
|
|
10168
10131
|
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.
|
|
10132
|
+
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
10133
|
})
|
|
10171
10134
|
);
|
|
10172
10135
|
}
|
|
@@ -10265,7 +10228,7 @@ var JobManager = class {
|
|
|
10265
10228
|
}
|
|
10266
10229
|
}
|
|
10267
10230
|
}
|
|
10268
|
-
if (opts.dvmId && opts.onJobCost && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10231
|
+
if (opts.dvmId && (opts.onJobCost || opts.onJobTerminal) && isTerminalCostRecoveryStore(this.jobStore)) {
|
|
10269
10232
|
this.terminalCostRecoveryTimer = setInterval(() => {
|
|
10270
10233
|
void this.recoverTerminalCosts().catch((err) => {
|
|
10271
10234
|
console.error(
|
|
@@ -10279,16 +10242,14 @@ var JobManager = class {
|
|
|
10279
10242
|
this.terminalCostRecoveryTimer.unref();
|
|
10280
10243
|
}
|
|
10281
10244
|
}
|
|
10282
|
-
/** Active in-memory jobs. */
|
|
10283
10245
|
get activeJobs() {
|
|
10284
10246
|
return this._activeJobs;
|
|
10285
10247
|
}
|
|
10286
|
-
/** The backing job store. */
|
|
10287
10248
|
get store() {
|
|
10288
10249
|
return this.jobStore;
|
|
10289
10250
|
}
|
|
10290
10251
|
/**
|
|
10291
|
-
* True when this DVM will actually issue receipts
|
|
10252
|
+
* True when this DVM will actually issue receipts — a key *and*
|
|
10292
10253
|
* a store that can allocate sequence numbers and persist bytes write-once.
|
|
10293
10254
|
* `/v1/info#receipts` reads this rather than the key alone, so the flag can
|
|
10294
10255
|
* never promise something `issueReceipt` silently declines to do.
|
|
@@ -10296,16 +10257,14 @@ var JobManager = class {
|
|
|
10296
10257
|
get receiptsEnabled() {
|
|
10297
10258
|
return this.opts.receiptIssuer !== void 0 && isReceiptIssuingStore(this.jobStore);
|
|
10298
10259
|
}
|
|
10299
|
-
/** True when the named capability exists on this descriptor. */
|
|
10300
10260
|
hasCapability(name) {
|
|
10301
10261
|
return Object.prototype.hasOwnProperty.call(this.descriptor.capabilities, name);
|
|
10302
10262
|
}
|
|
10303
|
-
/** Names of every capability this descriptor exposes — used for error envelopes. */
|
|
10304
10263
|
capabilityNames() {
|
|
10305
10264
|
return Object.keys(this.descriptor.capabilities);
|
|
10306
10265
|
}
|
|
10307
10266
|
/**
|
|
10308
|
-
* Resolve a capability's static `price` to msats
|
|
10267
|
+
* Resolve a capability's static `price` to msats.
|
|
10309
10268
|
*
|
|
10310
10269
|
* `"$X.XX"` is parsed as USD and converted via the SDK's shared fx fetcher.
|
|
10311
10270
|
* Returns `undefined` for dynamic-priced capabilities (those that declare
|
|
@@ -10318,7 +10277,7 @@ var JobManager = class {
|
|
|
10318
10277
|
}
|
|
10319
10278
|
/**
|
|
10320
10279
|
* Translate a capability's static `price` into the fiat envelope used by
|
|
10321
|
-
* the per-capability `pricing.max` advertised on `/v1/info
|
|
10280
|
+
* the per-capability `pricing.max` advertised on `/v1/info`.
|
|
10322
10281
|
* Returns `undefined` for dynamic-priced capabilities and for unknown
|
|
10323
10282
|
* names (the route handler validates the name before invoking this).
|
|
10324
10283
|
*/
|
|
@@ -10328,24 +10287,24 @@ var JobManager = class {
|
|
|
10328
10287
|
return resolvePricingMax(cap.price);
|
|
10329
10288
|
}
|
|
10330
10289
|
/**
|
|
10331
|
-
* Parse a request body through a capability's input schema
|
|
10290
|
+
* Parse a request body through a capability's input schema.
|
|
10332
10291
|
*
|
|
10333
10292
|
* Resolution order when a capability declares an `input` schema:
|
|
10334
10293
|
* 1. `body.data` is preferred — agents pass structured fields directly
|
|
10335
|
-
*
|
|
10294
|
+
* (CLI builds this from `--param k=v`).
|
|
10336
10295
|
* 2. Fallback: `JSON.parse(body.input)` for clients still on the legacy
|
|
10337
|
-
*
|
|
10296
|
+
* JSON-string contract.
|
|
10338
10297
|
* 3. Neither usable → throw `MissingStructuredInputError` so the route can
|
|
10339
|
-
*
|
|
10298
|
+
* return `invalid_input` with a hint pointing at `/v1/info`.
|
|
10340
10299
|
*
|
|
10341
10300
|
* When the capability has no `input` schema, returns `body.input` raw
|
|
10342
10301
|
* (primitive path).
|
|
10343
10302
|
*
|
|
10344
|
-
* The
|
|
10345
|
-
* branches
|
|
10303
|
+
* The credit envelope is removed before the schema runs, on both
|
|
10304
|
+
* branches — it rides the signed body but is protocol-level, so a
|
|
10346
10305
|
* top-level `.strict()` capability schema would otherwise reject every
|
|
10347
10306
|
* explicit draw as an unknown key, before the envelope was even extracted.
|
|
10348
|
-
* The persisted `record.input` the
|
|
10307
|
+
* The persisted `record.input` the reactivation path re-reads is the
|
|
10349
10308
|
* pre-Zod wire form, so it comes through the second branch carrying them too.
|
|
10350
10309
|
*/
|
|
10351
10310
|
parseInput(body, capability) {
|
|
@@ -10363,7 +10322,7 @@ var JobManager = class {
|
|
|
10363
10322
|
throw new MissingStructuredInputError();
|
|
10364
10323
|
}
|
|
10365
10324
|
/**
|
|
10366
|
-
* Look up the per-capability descriptor by name
|
|
10325
|
+
* Look up the per-capability descriptor by name. Throws when the
|
|
10367
10326
|
* name doesn't exist — the route layer validates body.capability first, so
|
|
10368
10327
|
* reaching this with an unknown name is a programmer error.
|
|
10369
10328
|
*/
|
|
@@ -10389,7 +10348,7 @@ var JobManager = class {
|
|
|
10389
10348
|
return this.descriptor.capabilities[name];
|
|
10390
10349
|
}
|
|
10391
10350
|
/**
|
|
10392
|
-
* Allocate the id the next job will be created under
|
|
10351
|
+
* Allocate the id the next job will be created under. The
|
|
10393
10352
|
* submit path calls this BEFORE payment verification so the ledger draw
|
|
10394
10353
|
* commits with its job linkage, then passes the id back through
|
|
10395
10354
|
* `createJob`'s provenance. Ids allocated for requests whose payment is
|
|
@@ -10400,7 +10359,7 @@ var JobManager = class {
|
|
|
10400
10359
|
return `job-${this.sessionId}-${this.nextJobId++}`;
|
|
10401
10360
|
}
|
|
10402
10361
|
/**
|
|
10403
|
-
* Create a new job from a request body. `provenance`
|
|
10362
|
+
* Create a new job from a request body. `provenance` is what the
|
|
10404
10363
|
* idempotent-replay path on `POST /v1/job` reads back: the raw `job_token`
|
|
10405
10364
|
* to re-issue, and the fingerprint + caller pubkey a retry must reproduce
|
|
10406
10365
|
* to be given it.
|
|
@@ -10456,27 +10415,23 @@ var JobManager = class {
|
|
|
10456
10415
|
this.watchDurableStatus(job);
|
|
10457
10416
|
return job;
|
|
10458
10417
|
}
|
|
10459
|
-
/** Build an SDKJobContext and attach it to the job. */
|
|
10460
10418
|
buildAndAttachContext(job, parsedInput) {
|
|
10461
10419
|
const auth = deriveAuthFromParsedInput(this.descriptor, parsedInput);
|
|
10462
10420
|
const sdkCtx = buildContext(job, { ...this.buildContextOpts(job), parsedInput, auth });
|
|
10463
10421
|
job.sdkCtx = sdkCtx;
|
|
10464
10422
|
return sdkCtx;
|
|
10465
10423
|
}
|
|
10466
|
-
/** Start the handler for a job. */
|
|
10467
10424
|
runHandler(job, sdkCtx) {
|
|
10468
10425
|
const cap = this.getCapability(job.capability);
|
|
10469
10426
|
const onError = (err) => {
|
|
10470
10427
|
if (isTerminal(job.status)) return;
|
|
10471
10428
|
job.replayProviderSkip = 0;
|
|
10472
|
-
|
|
10473
|
-
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
10478
|
-
void job.terminalWork;
|
|
10479
|
-
}
|
|
10429
|
+
job.status = "failed";
|
|
10430
|
+
providerMessage(job, "cancel", {
|
|
10431
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
10432
|
+
});
|
|
10433
|
+
job.terminalWork = this.finalizeTerminal(job);
|
|
10434
|
+
void job.terminalWork;
|
|
10480
10435
|
};
|
|
10481
10436
|
if (!cap) {
|
|
10482
10437
|
if (!isTerminal(job.status)) {
|
|
@@ -10504,16 +10459,46 @@ var JobManager = class {
|
|
|
10504
10459
|
onError(err);
|
|
10505
10460
|
}
|
|
10506
10461
|
}
|
|
10507
|
-
/** Dispatch a validated incoming message to the appropriate handler or pending resolver. */
|
|
10508
10462
|
async dispatchMessage(job, type, content) {
|
|
10463
|
+
if (type !== "response" && type !== "payment") {
|
|
10464
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10465
|
+
}
|
|
10466
|
+
const pending = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10467
|
+
await this.queueInputResume(job, async () => {
|
|
10468
|
+
const current = type === "payment" ? job.pendingPayment : job.pendingPrompts.get(content.prompt_id);
|
|
10469
|
+
if (pending && current !== pending) return;
|
|
10470
|
+
return this.dispatchMessageNow(job, type, content);
|
|
10471
|
+
});
|
|
10472
|
+
}
|
|
10473
|
+
async queueInputResume(job, resume) {
|
|
10474
|
+
const previous = this.inputResumeTails.get(job) ?? Promise.resolve();
|
|
10475
|
+
const next = previous.catch(() => void 0).then(resume);
|
|
10476
|
+
this.inputResumeTails.set(job, next);
|
|
10477
|
+
try {
|
|
10478
|
+
return await next;
|
|
10479
|
+
} finally {
|
|
10480
|
+
if (this.inputResumeTails.get(job) === next) this.inputResumeTails.delete(job);
|
|
10481
|
+
}
|
|
10482
|
+
}
|
|
10483
|
+
async claimLocalInput(job) {
|
|
10484
|
+
await job.messageAppenderTail;
|
|
10485
|
+
return isStreamableJobStore(this.jobStore) ? this.jobStore.claimForProcessing(job.id) : true;
|
|
10486
|
+
}
|
|
10487
|
+
async dispatchMessageNow(job, type, content) {
|
|
10509
10488
|
const ctx = job.sdkCtx;
|
|
10510
10489
|
const cap = this.getCapability(job.capability);
|
|
10511
10490
|
if (type === "response") {
|
|
10512
10491
|
const rc = content;
|
|
10513
10492
|
const pending = job.pendingPrompts.get(rc.prompt_id);
|
|
10514
10493
|
if (pending) {
|
|
10494
|
+
if (!await this.claimLocalInput(job)) {
|
|
10495
|
+
await this.adoptDurableTerminal(job);
|
|
10496
|
+
return;
|
|
10497
|
+
}
|
|
10498
|
+
if (job.pendingPrompts.get(rc.prompt_id) !== pending || isTerminal(job.status)) return;
|
|
10515
10499
|
job.pendingPrompts.delete(rc.prompt_id);
|
|
10516
|
-
job.status = "processing";
|
|
10500
|
+
job.status = job.pendingPrompts.size > 0 || job.pendingPayment ? "awaiting-input" : "processing";
|
|
10501
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10517
10502
|
pending.resolve(rc);
|
|
10518
10503
|
} else if (cap?.onResponse && ctx) {
|
|
10519
10504
|
await cap.onResponse(ctx, rc);
|
|
@@ -10522,9 +10507,15 @@ var JobManager = class {
|
|
|
10522
10507
|
const pc = content;
|
|
10523
10508
|
if (job.pendingPayment) {
|
|
10524
10509
|
const pp = job.pendingPayment;
|
|
10510
|
+
if (!await this.claimLocalInput(job)) {
|
|
10511
|
+
await this.adoptDurableTerminal(job);
|
|
10512
|
+
return;
|
|
10513
|
+
}
|
|
10514
|
+
if (job.pendingPayment !== pp || isTerminal(job.status)) return;
|
|
10525
10515
|
job.pendingPayment = null;
|
|
10526
|
-
job.status = "processing";
|
|
10527
|
-
|
|
10516
|
+
job.status = job.pendingPrompts.size > 0 ? "awaiting-input" : "processing";
|
|
10517
|
+
if (job.status === "awaiting-input") await this.persistJob(job);
|
|
10518
|
+
await pp.resolve(pc);
|
|
10528
10519
|
} else if (cap?.onPayment && ctx) {
|
|
10529
10520
|
await cap.onPayment(ctx, pc);
|
|
10530
10521
|
}
|
|
@@ -10533,6 +10524,7 @@ var JobManager = class {
|
|
|
10533
10524
|
} else if (type === "cancel") {
|
|
10534
10525
|
const reason = content?.reason ?? "Job cancelled";
|
|
10535
10526
|
abortJob(job, reason);
|
|
10527
|
+
job.endedBy = "caller";
|
|
10536
10528
|
job.status = "cancelled";
|
|
10537
10529
|
if (cap?.onCancel && ctx) {
|
|
10538
10530
|
await cap.onCancel(ctx, content);
|
|
@@ -10551,17 +10543,16 @@ var JobManager = class {
|
|
|
10551
10543
|
});
|
|
10552
10544
|
}
|
|
10553
10545
|
}
|
|
10554
|
-
// ── Persistence ──────────────────────────────────────────────────────
|
|
10555
10546
|
/**
|
|
10556
10547
|
* Persist a job to the backing store.
|
|
10557
10548
|
*
|
|
10558
|
-
* Awaits the per-job appender tail
|
|
10559
|
-
*
|
|
10560
|
-
*
|
|
10561
|
-
*
|
|
10562
|
-
*
|
|
10563
|
-
*
|
|
10564
|
-
*
|
|
10549
|
+
* Awaits the per-job appender tail so that:
|
|
10550
|
+
* 1. The snapshot's `messages` is consistent with the `job_messages` table
|
|
10551
|
+
* no row is still in flight at snapshot time.
|
|
10552
|
+
* 2. For terminal saves, the `DELETE FROM job_messages` inside `save()`
|
|
10553
|
+
* can't race a still-pending `appendOutgoing` for the final yield
|
|
10554
|
+
* message (which would otherwise wipe the row before an in-flight SSE
|
|
10555
|
+
* subscriber's NOTIFY-driven fetch can see it).
|
|
10565
10556
|
*/
|
|
10566
10557
|
async persistJob(job) {
|
|
10567
10558
|
if (job.messageAppenderTail) {
|
|
@@ -10582,9 +10573,17 @@ var JobManager = class {
|
|
|
10582
10573
|
} else {
|
|
10583
10574
|
await this.jobStore.save(record);
|
|
10584
10575
|
}
|
|
10576
|
+
const saved = await this.jobStore.get(job.id);
|
|
10577
|
+
if (saved) {
|
|
10578
|
+
job.timingKnown = saved.timingKnown;
|
|
10579
|
+
job.endedBy = saved.endedBy;
|
|
10580
|
+
job.terminalAtMs = saved.terminalAtMs;
|
|
10581
|
+
job.callerWaitMs = saved.callerWaitMs;
|
|
10582
|
+
job.awaitingInputAtMs = saved.awaitingInputAtMs;
|
|
10583
|
+
}
|
|
10585
10584
|
}
|
|
10586
10585
|
/**
|
|
10587
|
-
* Cross-machine terminal guard
|
|
10586
|
+
* Cross-machine terminal guard. `buildContext`'s guard reads the
|
|
10588
10587
|
* in-memory `job.status`, so on its own it only protects the machine running
|
|
10589
10588
|
* the handler. On a multi-machine DVM a `DELETE /v1/job/:id` routinely lands
|
|
10590
10589
|
* on a machine that isn't running the job — `app.ts` cancels it in the store
|
|
@@ -10596,7 +10595,7 @@ var JobManager = class {
|
|
|
10596
10595
|
* fires `job.abort` so in-flight `ctx.fetch` calls tear down. Returns true when
|
|
10597
10596
|
* the durable state won and the caller must skip its save.
|
|
10598
10597
|
*
|
|
10599
|
-
* Two triggers: `watchDurableStatus`'s NOTIFY subscription
|
|
10598
|
+
* Two triggers: `watchDurableStatus`'s NOTIFY subscription fires
|
|
10600
10599
|
* this within a round-trip of the remote cancel committing, which is what
|
|
10601
10600
|
* actually stops the provider spend; `persistJob` calls it again on every save
|
|
10602
10601
|
* as the backstop for the window where a cancel commits between a handler's
|
|
@@ -10604,8 +10603,10 @@ var JobManager = class {
|
|
|
10604
10603
|
* stores' terminal-sticky `save`, which keeps the row itself correct).
|
|
10605
10604
|
*/
|
|
10606
10605
|
async adoptDurableTerminal(job) {
|
|
10607
|
-
const
|
|
10608
|
-
if (!
|
|
10606
|
+
const counters = isStreamableJobStore(this.jobStore) ? await this.jobStore.getCounters(job.id).catch(() => void 0) : await this.jobStore.get(job.id).catch(() => void 0);
|
|
10607
|
+
if (!counters || !isTerminal(counters.status) || counters.status === job.status) return false;
|
|
10608
|
+
const durable = await this.jobStore.get(job.id);
|
|
10609
|
+
if (!durable) return false;
|
|
10609
10610
|
console.warn(
|
|
10610
10611
|
JSON.stringify({
|
|
10611
10612
|
level: "job_terminal_elsewhere",
|
|
@@ -10616,6 +10617,11 @@ var JobManager = class {
|
|
|
10616
10617
|
);
|
|
10617
10618
|
job.status = durable.status;
|
|
10618
10619
|
job.summary = durable.summary;
|
|
10620
|
+
job.timingKnown = durable.timingKnown;
|
|
10621
|
+
job.endedBy = durable.endedBy;
|
|
10622
|
+
job.terminalAtMs = durable.terminalAtMs;
|
|
10623
|
+
job.callerWaitMs = durable.callerWaitMs;
|
|
10624
|
+
job.awaitingInputAtMs = durable.awaitingInputAtMs;
|
|
10619
10625
|
const reason = durable.status !== "completed" && durable.summary ? durable.summary : `Job ${durable.status}`;
|
|
10620
10626
|
for (const [, p] of job.pendingPrompts) p.reject(new JobCancelledError(reason));
|
|
10621
10627
|
job.pendingPayment?.reject(new JobCancelledError(reason));
|
|
@@ -10635,7 +10641,7 @@ var JobManager = class {
|
|
|
10635
10641
|
});
|
|
10636
10642
|
}
|
|
10637
10643
|
this.clearIdleTimer(job.id);
|
|
10638
|
-
void this.issueReceiptForStoredJob(job.id);
|
|
10644
|
+
void this.issueReceiptForStoredJob(job.id).then(() => this.reportTerminalJob(job.id));
|
|
10639
10645
|
this.scheduleCleanup(job);
|
|
10640
10646
|
if (durable.status !== "completed" && terminalCostPersisted) {
|
|
10641
10647
|
await this.reportJobCost(job);
|
|
@@ -10681,6 +10687,7 @@ var JobManager = class {
|
|
|
10681
10687
|
async retryTerminalCostHandoff(job, attempt) {
|
|
10682
10688
|
if (!await this.tryPersistAdoptedTerminalCost(job, attempt)) return;
|
|
10683
10689
|
await this.reportJobCost(job);
|
|
10690
|
+
await this.reportTerminalJob(job.id);
|
|
10684
10691
|
}
|
|
10685
10692
|
/** Durably merge a terminal handler owner's latest cost before reporting it. */
|
|
10686
10693
|
async persistAdoptedTerminalCost(job) {
|
|
@@ -10710,7 +10717,7 @@ var JobManager = class {
|
|
|
10710
10717
|
);
|
|
10711
10718
|
}
|
|
10712
10719
|
/**
|
|
10713
|
-
* Watch the durable status of a locally-active job
|
|
10720
|
+
* Watch the durable status of a locally-active job.
|
|
10714
10721
|
*
|
|
10715
10722
|
* `ctx.signal` is raised by `dispatchMessage`, which only runs on the machine
|
|
10716
10723
|
* holding the job in `activeJobs`. On a multi-machine DVM the `DELETE` usually
|
|
@@ -10751,7 +10758,7 @@ var JobManager = class {
|
|
|
10751
10758
|
});
|
|
10752
10759
|
}
|
|
10753
10760
|
/**
|
|
10754
|
-
* Tear down a job's durable-status subscription
|
|
10761
|
+
* Tear down a job's durable-status subscription. Must be called
|
|
10755
10762
|
* wherever a job leaves `activeJobs` — a leaked subscriber outlives the job on
|
|
10756
10763
|
* the store's shared LISTEN connection. Idempotent.
|
|
10757
10764
|
*/
|
|
@@ -10763,12 +10770,12 @@ var JobManager = class {
|
|
|
10763
10770
|
unsubscribe();
|
|
10764
10771
|
}
|
|
10765
10772
|
/**
|
|
10766
|
-
* NOTIFY-driven durable-status re-read, coalesced per job
|
|
10773
|
+
* NOTIFY-driven durable-status re-read, coalesced per job.
|
|
10767
10774
|
*
|
|
10768
10775
|
* A notify that arrives while a re-read is in flight is queued rather than
|
|
10769
10776
|
* dropped: the in-flight read may have observed the row a moment *before* the
|
|
10770
|
-
* cancel committed, and dropping its notify would
|
|
10771
|
-
*
|
|
10777
|
+
* cancel committed, and dropping its notify would leave the local handler
|
|
10778
|
+
* running until its next store write instead of aborting promptly.
|
|
10772
10779
|
*/
|
|
10773
10780
|
async checkDurableTerminal(job) {
|
|
10774
10781
|
if (this.statusChecksInFlight.has(job.id)) {
|
|
@@ -10798,13 +10805,13 @@ var JobManager = class {
|
|
|
10798
10805
|
/**
|
|
10799
10806
|
* Wire the Tx A appender when the store supports streaming.
|
|
10800
10807
|
*
|
|
10801
|
-
* Tx A
|
|
10808
|
+
* Tx A: outgoing message + `pending_payment_msats` bump land in
|
|
10802
10809
|
* the same transaction. The DB allocates the row's seq via
|
|
10803
10810
|
* `UPDATE jobs SET next_seq = next_seq + 1 RETURNING` so concurrent inbound
|
|
10804
10811
|
* traffic on different machines can never collide on the `(job_id, seq)` PK.
|
|
10805
10812
|
* In-memory `job.seq` keeps its own counter for same-machine SSE listeners.
|
|
10806
10813
|
*
|
|
10807
|
-
* Per-job serialisation
|
|
10814
|
+
* Per-job serialisation: chained through `job.messageAppenderTail`
|
|
10808
10815
|
* so two synchronous `providerMessage` calls (e.g. `artifact` followed by
|
|
10809
10816
|
* `complete`) commit their `appendOutgoing` transactions in call order.
|
|
10810
10817
|
* Without this, the two transactions race for the `jobs` row lock and the
|
|
@@ -10838,7 +10845,7 @@ var JobManager = class {
|
|
|
10838
10845
|
// In-memory counter, NOT the authoritative DB-allocated seq:
|
|
10839
10846
|
// `appendOutgoing` allocates the real seq via `UPDATE ...
|
|
10840
10847
|
// RETURNING` inside the transaction that just failed, so no DB
|
|
10841
|
-
// seq was assigned for this message
|
|
10848
|
+
// seq was assigned for this message.
|
|
10842
10849
|
localSeq: msg.seq,
|
|
10843
10850
|
note: "localSeq is the in-process counter; the DB-allocated seq was never assigned (append rolled back)",
|
|
10844
10851
|
error: err instanceof Error ? err.message : String(err)
|
|
@@ -10849,8 +10856,6 @@ var JobManager = class {
|
|
|
10849
10856
|
job.messageAppenderTail = next;
|
|
10850
10857
|
};
|
|
10851
10858
|
}
|
|
10852
|
-
// ── Idle timeout ─────────────────────────────────────────────────────
|
|
10853
|
-
/** Start (or restart) the idle timer for a job. */
|
|
10854
10859
|
startIdleTimer(job) {
|
|
10855
10860
|
this.clearIdleTimer(job.id);
|
|
10856
10861
|
const timeoutMs = this.descriptor.idleTimeout * 1e3;
|
|
@@ -10859,7 +10864,6 @@ var JobManager = class {
|
|
|
10859
10864
|
}, timeoutMs);
|
|
10860
10865
|
this.idleTimers.set(job.id, timer);
|
|
10861
10866
|
}
|
|
10862
|
-
/** Clear the idle timer for a job. */
|
|
10863
10867
|
clearIdleTimer(id) {
|
|
10864
10868
|
const timer = this.idleTimers.get(id);
|
|
10865
10869
|
if (timer) {
|
|
@@ -10867,7 +10871,7 @@ var JobManager = class {
|
|
|
10867
10871
|
this.idleTimers.delete(id);
|
|
10868
10872
|
}
|
|
10869
10873
|
}
|
|
10870
|
-
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard
|
|
10874
|
+
/** Cancel a job due to idle timeout. Exposed for the reactivation path's idle-expiry guard. */
|
|
10871
10875
|
async cancelJobIdle(job) {
|
|
10872
10876
|
if (isTerminal(job.status)) return;
|
|
10873
10877
|
const current = await this.jobStore.get(job.id).catch(() => void 0);
|
|
@@ -10886,6 +10890,7 @@ var JobManager = class {
|
|
|
10886
10890
|
if (job.pendingPayment) job.pendingPayment.reject(new Error("Job timed out (idle)"));
|
|
10887
10891
|
job.pendingPrompts.clear();
|
|
10888
10892
|
job.pendingPayment = null;
|
|
10893
|
+
job.endedBy = (current?.status ?? job.status) === "awaiting-input" ? "caller" : "provider";
|
|
10889
10894
|
job.status = "cancelled";
|
|
10890
10895
|
abortJob(job, "Job timed out (idle)");
|
|
10891
10896
|
providerMessage(job, "cancel", { reason: "Job timed out (idle)" });
|
|
@@ -10893,14 +10898,13 @@ var JobManager = class {
|
|
|
10893
10898
|
void job.terminalWork;
|
|
10894
10899
|
this.scheduleCleanup(job);
|
|
10895
10900
|
}
|
|
10896
|
-
// ── Stale-job sweeper (internal-review / internal-review) ──────────────────────────
|
|
10897
10901
|
/**
|
|
10898
10902
|
* Force-terminate stale non-terminal jobs. Two status-aware arms:
|
|
10899
|
-
*
|
|
10900
|
-
*
|
|
10901
|
-
*
|
|
10902
|
-
*
|
|
10903
|
-
*
|
|
10903
|
+
* • `awaiting-input` past `staleJobTimeoutMs` → `cancelled`
|
|
10904
|
+
* (`stale_no_terminal_status`) — the caller never paid / responded.
|
|
10905
|
+
* • `processing`/`working` past `processingWatchdogMs` → `failed`
|
|
10906
|
+
* (`worker_died_mid_job`) — the worker died mid-job. The heartbeat keeps
|
|
10907
|
+
* live workers fresh, so a stale row here means a dead process.
|
|
10904
10908
|
*
|
|
10905
10909
|
* Fires on a timer (wired in the constructor) and also callable on demand
|
|
10906
10910
|
* (tests, ops). The CAS in `cancelStaleJob` makes this safe to run
|
|
@@ -10909,17 +10913,17 @@ var JobManager = class {
|
|
|
10909
10913
|
* descriptors — the local activeJobs teardown only makes sense on the
|
|
10910
10914
|
* JobManager that actually hosted the zombie handler.
|
|
10911
10915
|
*
|
|
10912
|
-
* **Both cutoffs float off the wall clock
|
|
10916
|
+
* **Both cutoffs float off the wall clock.** Each is one
|
|
10913
10917
|
* `Date.now()` sample compared against `last_activity_at`, stamped from a
|
|
10914
10918
|
* different sample at a different moment, so a backwards step between the
|
|
10915
10919
|
* two makes every row look more recently active than it is: the query
|
|
10916
10920
|
* matches nothing and both arms go silent for the length of the skew. The
|
|
10917
10921
|
* 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
|
|
10922
|
+
* `pending`, and the paid-job-death alert — wired to this reaper —
|
|
10923
|
+
* never fires. Same defect, same host class, as orphan sweep.
|
|
10920
10924
|
*
|
|
10921
10925
|
* **The compensation is capped** — see {@link staleSweepCompensationCapMs}
|
|
10922
|
-
* for where each arm's cap comes from — which
|
|
10926
|
+
* for where each arm's cap comes from — which did not need to do. Its eagerness costs at worst an early release
|
|
10923
10927
|
* of an unclaimed hold, refused outright for any live job row; ours
|
|
10924
10928
|
* force-*fails* a running job. `cancelStaleJob`'s CAS does not cover that:
|
|
10925
10929
|
* `expectedActivityBefore` is the same compensated threshold the query used,
|
|
@@ -11055,7 +11059,7 @@ var JobManager = class {
|
|
|
11055
11059
|
}
|
|
11056
11060
|
/**
|
|
11057
11061
|
* How far one watchdog arm may lean on {@link monotonicNowMs} past the wall
|
|
11058
|
-
* clock to cover a backwards step
|
|
11062
|
+
* clock to cover a backwards step. The compensation is capped,
|
|
11059
11063
|
* not free: a claimed row must still have been silent for
|
|
11060
11064
|
* `windowMs - cap`, and unlike the orphan sweep, claiming eagerly here
|
|
11061
11065
|
* force-fails a job that may well be alive.
|
|
@@ -11075,7 +11079,7 @@ var JobManager = class {
|
|
|
11075
11079
|
* clock stepped. That pair is marginal already; the guard must not make it
|
|
11076
11080
|
* deterministic. The cap collapses to zero there, which is today's
|
|
11077
11081
|
* behaviour: late, never wrong. The `awaiting-input` arm takes no such term
|
|
11078
|
-
*
|
|
11082
|
+
* nothing heartbeats it by design, and its refresh is an inbound caller
|
|
11079
11083
|
* message on no cadence at all.
|
|
11080
11084
|
*
|
|
11081
11085
|
* **Why a magnitude and not a predicate.** The tempting sharper rule is to
|
|
@@ -11094,7 +11098,6 @@ var JobManager = class {
|
|
|
11094
11098
|
if (!heartbeated || this.heartbeatIntervalMs <= 0) return halfWindow;
|
|
11095
11099
|
return Math.max(0, Math.min(halfWindow, windowMs - 2 * this.heartbeatIntervalMs));
|
|
11096
11100
|
}
|
|
11097
|
-
// ── Terminal job content retention ───────────────────────────────────
|
|
11098
11101
|
/**
|
|
11099
11102
|
* Redact terminal job content beyond this DVM's configured window.
|
|
11100
11103
|
*
|
|
@@ -11259,12 +11262,11 @@ var JobManager = class {
|
|
|
11259
11262
|
);
|
|
11260
11263
|
}
|
|
11261
11264
|
}
|
|
11262
|
-
// ── Orphan-draw sweeper (internal-review) + terminal reconciler (internal-review) ──
|
|
11263
11265
|
/**
|
|
11264
11266
|
* Resolve `pending` credit draws the terminal funnel can no longer reach.
|
|
11265
11267
|
* Two arms over one scan, distinguished by whether the draw's job row exists.
|
|
11266
11268
|
*
|
|
11267
|
-
* **No job row
|
|
11269
|
+
* **No job row — release.** allocates the job id and
|
|
11268
11270
|
* commits the ledger draw, with its `job_id` linkage, before `recordReplay`
|
|
11269
11271
|
* and `persistJob` write the job row. A crash, a `draw_conflict` 409, or a
|
|
11270
11272
|
* fail-closed `replay_detected` 401 in that window leaves a committed hold
|
|
@@ -11274,12 +11276,12 @@ var JobManager = class {
|
|
|
11274
11276
|
* available balance stays reduced forever. Cosmetic for an implicit N=1
|
|
11275
11277
|
* credit, a silent balance shrink for an explicit N>1 one.
|
|
11276
11278
|
*
|
|
11277
|
-
* **Terminal job row
|
|
11279
|
+
* **Terminal job row — reconcile to its outcome.** The funnel
|
|
11278
11280
|
* demonstrably fails: `resolveCreditDraw` throws on a ledger/DB error,
|
|
11279
11281
|
* `issueReceipt` catches it, logs `credit_resolve_failed` and returns
|
|
11280
11282
|
* nothing, and until now nothing retried. Same stranded hold, reached through
|
|
11281
11283
|
* a different door — and for a `completed` job it strands the platform's
|
|
11282
|
-
* books too, since draws *are* the revenue events
|
|
11284
|
+
* books too, since draws *are* the revenue events: the caller is
|
|
11283
11285
|
* charged nothing, the balance never moves, and outstanding liability
|
|
11284
11286
|
* (`deposits − draw revenue`) overstates forever. So a late settle books, off
|
|
11285
11287
|
* the committed draw row, through the ordinary reporter path.
|
|
@@ -11407,8 +11409,8 @@ var JobManager = class {
|
|
|
11407
11409
|
}
|
|
11408
11410
|
}
|
|
11409
11411
|
/**
|
|
11410
|
-
* The end-of-scan line for a tick that resolved nothing
|
|
11411
|
-
*
|
|
11412
|
+
* The end-of-scan line for a tick that resolved nothing. Two shapes are
|
|
11413
|
+
* worth a warning, and neither is the ordinary
|
|
11412
11414
|
* one — the timer runs every 5 minutes per ledger in production.
|
|
11413
11415
|
*
|
|
11414
11416
|
* `examined > 0`: aged candidates were looked at and none moved. Usually
|
|
@@ -11417,7 +11419,7 @@ var JobManager = class {
|
|
|
11417
11419
|
*
|
|
11418
11420
|
* `examined === 0`: the query returned nothing. Ordinarily that means there
|
|
11419
11421
|
* is nothing to do — but it is equally what a blinded scan looks like, which
|
|
11420
|
-
* is the case the
|
|
11422
|
+
* is the case the gate could not reach. Probe for the oldest
|
|
11421
11423
|
* `pending` hold at any age and speak up when one exists the scan should
|
|
11422
11424
|
* have seen and didn't: already past the age gate outright, stamped ahead of
|
|
11423
11425
|
* our clock (some other process's clock is fast), or hidden while our own
|
|
@@ -11469,7 +11471,7 @@ var JobManager = class {
|
|
|
11469
11471
|
);
|
|
11470
11472
|
}
|
|
11471
11473
|
/**
|
|
11472
|
-
* Resolve one aged `pending` draw whose job row reached terminal
|
|
11474
|
+
* Resolve one aged `pending` draw whose job row reached terminal,
|
|
11473
11475
|
* mirroring what `resolveCreditDraw` would have done in line. Returns true
|
|
11474
11476
|
* when this call is the one that moved the draw — the caller counts it, and
|
|
11475
11477
|
* only it books.
|
|
@@ -11542,7 +11544,7 @@ var JobManager = class {
|
|
|
11542
11544
|
/**
|
|
11543
11545
|
* Refresh `lastActivityAt` for jobs this process is actively running
|
|
11544
11546
|
* (`processing`/`working`) so the processing watchdog only fires once the
|
|
11545
|
-
* worker is genuinely dead
|
|
11547
|
+
* worker is genuinely dead. `awaiting-input` jobs are
|
|
11546
11548
|
* deliberately excluded — their idle timeout must still elapse. Fires on the
|
|
11547
11549
|
* heartbeat timer; also callable on demand for tests.
|
|
11548
11550
|
*/
|
|
@@ -11562,17 +11564,16 @@ var JobManager = class {
|
|
|
11562
11564
|
this.heartbeatInFlight = false;
|
|
11563
11565
|
}
|
|
11564
11566
|
}
|
|
11565
|
-
// ── Reactivation (replay) ────────────────────────────────────────────
|
|
11566
11567
|
/**
|
|
11567
|
-
* Single-execution claim for a reactivating machine
|
|
11568
|
+
* Single-execution claim for a reactivating machine. Before
|
|
11568
11569
|
* replaying an `awaiting-input` job, wait a bounded grace window for a live
|
|
11569
11570
|
* original handler to win the `awaiting-input → processing` CAS via its
|
|
11570
11571
|
* NOTIFY wake. Resolves:
|
|
11571
|
-
*
|
|
11572
|
-
*
|
|
11573
|
-
*
|
|
11574
|
-
*
|
|
11575
|
-
*
|
|
11572
|
+
* • `false` — the row left `awaiting-input` during the window (the original
|
|
11573
|
+
* handler claimed it / drove it terminal). Stand down; do not replay.
|
|
11574
|
+
* • `true` — the window elapsed still `awaiting-input` (the original worker
|
|
11575
|
+
* is gone or has no live resolver) AND this machine won the claim CAS.
|
|
11576
|
+
* Replay for dead-worker recovery.
|
|
11576
11577
|
*
|
|
11577
11578
|
* Biasing the original handler to win kills the double execution (double
|
|
11578
11579
|
* substrate spend, clobbered artifact) without heartbeating `awaiting-input`
|
|
@@ -11595,7 +11596,7 @@ var JobManager = class {
|
|
|
11595
11596
|
}
|
|
11596
11597
|
}
|
|
11597
11598
|
/**
|
|
11598
|
-
* Record an inbound client message durably via Tx B
|
|
11599
|
+
* Record an inbound client message durably via Tx B and append
|
|
11599
11600
|
* it to the in-memory `job.messages` array. Returns the DB-allocated seq
|
|
11600
11601
|
* (`null` when the store isn't streamable — non-Postgres test fallback).
|
|
11601
11602
|
*
|
|
@@ -11639,13 +11640,13 @@ var JobManager = class {
|
|
|
11639
11640
|
* in-memory `ServerJob`, wires the appender, builds the context, starts
|
|
11640
11641
|
* the idle timer. The caller has already recorded the inbound via Tx B
|
|
11641
11642
|
* and run Tx C (verify + credit); reactivation is now decoupled from
|
|
11642
|
-
* credit
|
|
11643
|
+
* credit. The caller decides whether to run the
|
|
11643
11644
|
* handler (Pattern A: status was `awaiting-input`) or to dispatch the
|
|
11644
11645
|
* message to a custom handler (Pattern B).
|
|
11645
11646
|
*
|
|
11646
11647
|
* For DVMs that declare descriptor-level auth, the persisted
|
|
11647
11648
|
* `record.input` carries the original signed envelope. We re-verify the
|
|
11648
|
-
* signature here
|
|
11649
|
+
* signature here so a DB-layer tamper — compromised admin, SQL
|
|
11649
11650
|
* injection, malicious operator with DB access — can't silently feed an
|
|
11650
11651
|
* attacker-supplied pubkey/envelope into the handler. The drift window
|
|
11651
11652
|
* and replay store are deliberately skipped: the persisted timestamp is
|
|
@@ -11659,7 +11660,7 @@ var JobManager = class {
|
|
|
11659
11660
|
* What gets re-verified is the persisted row read back through
|
|
11660
11661
|
* {@link signedRequestInput} — `app.ts` stores the pre-Zod wire form, so
|
|
11661
11662
|
* those are the caller's own signed bytes, the same ones `/v1/quote` and the
|
|
11662
|
-
* submit checked
|
|
11663
|
+
* submit checked. That makes the tamper check strictly stronger
|
|
11663
11664
|
* than the parsed form it replaced: an injected key the capability schema
|
|
11664
11665
|
* doesn't declare used to be stripped before the signature was checked, so
|
|
11665
11666
|
* it re-verified clean.
|
|
@@ -11704,7 +11705,7 @@ var JobManager = class {
|
|
|
11704
11705
|
return { job, sdkCtx };
|
|
11705
11706
|
}
|
|
11706
11707
|
/**
|
|
11707
|
-
* Verify an inbound payment and apply the credit via Tx C
|
|
11708
|
+
* Verify an inbound payment and apply the credit via Tx C.
|
|
11708
11709
|
* The inbound message must have been recorded via Tx B already
|
|
11709
11710
|
* (`recordInboundPending` returned `inboundSeq`). For non-streamable
|
|
11710
11711
|
* stores (`inboundSeq === null`), falls back to in-memory mutation via
|
|
@@ -11714,12 +11715,12 @@ var JobManager = class {
|
|
|
11714
11715
|
* `pendingPaymentMsats` without external verification. Gated on
|
|
11715
11716
|
* `devModeSkipsPaymentVerification` — the same predicate the upfront path
|
|
11716
11717
|
* reads, so a dev server wired to a mint verifies mid-job payments for real
|
|
11717
|
-
*
|
|
11718
|
+
* plus the explicit `dev_auto` opt-out the dev console uses,
|
|
11718
11719
|
* which is the one caller that has no wallet to pay from. `dev_auto` loses to
|
|
11719
11720
|
* any proof riding the same message: money on the wire always takes the rail,
|
|
11720
11721
|
* so the flag can never leave a real token unspent against a credited job.
|
|
11721
11722
|
*
|
|
11722
|
-
* Tx C is not atomic with the rail commit
|
|
11723
|
+
* Tx C is not atomic with the rail commit. `verifyIncomingPayment`
|
|
11723
11724
|
* commits the proofs *and* the ledger leg in one transaction and returns;
|
|
11724
11725
|
* only then does `verifyAndCredit` write the job row, on its own connection.
|
|
11725
11726
|
* Everything after that call therefore runs with the caller's money already
|
|
@@ -11769,7 +11770,7 @@ var JobManager = class {
|
|
|
11769
11770
|
lockPubkeys: extra?.lockPubkeys,
|
|
11770
11771
|
mintHealthTracker: extra?.mintHealthTracker,
|
|
11771
11772
|
fxFetcher: this.opts.fxFetcher,
|
|
11772
|
-
//
|
|
11773
|
+
// the ledger leg — a mid-job payment is a top-up that funds
|
|
11773
11774
|
// the job's credit and grows its draw, atomically with the rail commit.
|
|
11774
11775
|
creditLedger: this.opts.creditLedger,
|
|
11775
11776
|
processedPayments: this.opts.processedPayments,
|
|
@@ -11904,7 +11905,7 @@ var JobManager = class {
|
|
|
11904
11905
|
return { ...unappliedCredit && { unappliedCredit } };
|
|
11905
11906
|
}
|
|
11906
11907
|
/**
|
|
11907
|
-
* Tx C with a bounded retry
|
|
11908
|
+
* Tx C with a bounded retry.
|
|
11908
11909
|
*
|
|
11909
11910
|
* By the time this runs on the verified path, the rail commit and the ledger
|
|
11910
11911
|
* leg have already committed in a transaction this call is not part of — so a
|
|
@@ -11991,7 +11992,7 @@ var JobManager = class {
|
|
|
11991
11992
|
}
|
|
11992
11993
|
/**
|
|
11993
11994
|
* Answer a payment whose rail and ledger legs committed but whose job row
|
|
11994
|
-
* could not be written
|
|
11995
|
+
* could not be written — every retry spent, the money real.
|
|
11995
11996
|
*
|
|
11996
11997
|
* The refusal names the credit the money landed on so the caller can act on
|
|
11997
11998
|
* it directly, mirroring the short mid-job pay's 402 (`payment.ts`). It is a
|
|
@@ -12000,7 +12001,7 @@ var JobManager = class {
|
|
|
12000
12001
|
* fault an operator should see in their 5xx rate. `retryable` is true because
|
|
12001
12002
|
* the ask is genuinely still outstanding.
|
|
12002
12003
|
*
|
|
12003
|
-
* Nothing is released here. The hold is left to the
|
|
12004
|
+
* Nothing is released here. The hold is left to the reconciler,
|
|
12004
12005
|
* which makes the same decision — unbound draw ⇒ release, never settle — but
|
|
12005
12006
|
* under the credit row's `FOR UPDATE`, after the job is terminal, where it
|
|
12006
12007
|
* cannot race an in-flight COMMIT. Releasing from here would also be
|
|
@@ -12067,7 +12068,7 @@ var JobManager = class {
|
|
|
12067
12068
|
}
|
|
12068
12069
|
/**
|
|
12069
12070
|
* Describe a deposit the job row never took, in the shape both the 201 and
|
|
12070
|
-
* the `payment_unapplied` 500 carry
|
|
12071
|
+
* the `payment_unapplied` 500 carry. `undefined` when the payment
|
|
12071
12072
|
* ran ledger-less (no credit ledger wired, or the top-up preflight skipped) —
|
|
12072
12073
|
* there is no credit to name, so the 201 carries no `credit` block and the
|
|
12073
12074
|
* 500 takes its words from `unappliedCopy` directly.
|
|
@@ -12081,7 +12082,7 @@ var JobManager = class {
|
|
|
12081
12082
|
...unappliedCopy(reason, creditId)
|
|
12082
12083
|
};
|
|
12083
12084
|
}
|
|
12084
|
-
/** Book a mid-job top-up's funding as a deposit (
|
|
12085
|
+
/** Book a mid-job top-up's funding as a deposit (the deposit-accounting rule). */
|
|
12085
12086
|
reportCreditDeposit(funding) {
|
|
12086
12087
|
const onCreditFunded = this.opts.onCreditFunded;
|
|
12087
12088
|
if (!funding || !onCreditFunded || !this.opts.dvmId) return;
|
|
@@ -12094,7 +12095,7 @@ var JobManager = class {
|
|
|
12094
12095
|
/**
|
|
12095
12096
|
* Internal: build the BuildContextOpts shared between `createJob` and
|
|
12096
12097
|
* `reactivateJob`. Centralised so persistence/terminal callbacks and the
|
|
12097
|
-
*
|
|
12098
|
+
* NOTIFY-driven cross-machine wake stay in one place.
|
|
12098
12099
|
*/
|
|
12099
12100
|
buildContextOpts(job) {
|
|
12100
12101
|
const jobStore = this.jobStore;
|
|
@@ -12113,7 +12114,16 @@ var JobManager = class {
|
|
|
12113
12114
|
getCounters: isStreamableJobStore(jobStore) ? () => jobStore.getCounters(job.id) : void 0,
|
|
12114
12115
|
drawPool: this.opts.creditLedger ? () => this.resolveDrawPool(job) : void 0,
|
|
12115
12116
|
pricingCurrency: this.pricingCurrency,
|
|
12116
|
-
claimForProcessing: isStreamableJobStore(jobStore) ? () =>
|
|
12117
|
+
claimForProcessing: isStreamableJobStore(jobStore) ? (resume) => {
|
|
12118
|
+
const pending = job.pendingPayment;
|
|
12119
|
+
return this.queueInputResume(job, async () => {
|
|
12120
|
+
if (!pending || job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12121
|
+
if (!await this.claimLocalInput(job)) return false;
|
|
12122
|
+
if (job.pendingPayment !== pending || isTerminal(job.status)) return false;
|
|
12123
|
+
await resume?.();
|
|
12124
|
+
return true;
|
|
12125
|
+
});
|
|
12126
|
+
} : void 0,
|
|
12117
12127
|
onSuperseded: () => {
|
|
12118
12128
|
this.clearIdleTimer(job.id);
|
|
12119
12129
|
this.unwatchDurableStatus(job.id);
|
|
@@ -12124,7 +12134,7 @@ var JobManager = class {
|
|
|
12124
12134
|
}
|
|
12125
12135
|
/**
|
|
12126
12136
|
* Internal: what unit `ctx.requestPayment`'s auto-credit gate may measure this
|
|
12127
|
-
* job's remaining pool in
|
|
12137
|
+
* job's remaining pool in.
|
|
12128
12138
|
*
|
|
12129
12139
|
* The regime is read off the DRAW, never off the job. Implicit N=1 is exactly
|
|
12130
12140
|
* `credit_id === imp:<rail>:<draw_id>`, because `fundAndDraw` derives both
|
|
@@ -12142,8 +12152,7 @@ var JobManager = class {
|
|
|
12142
12152
|
* drew and whose pool this can't read or denominate answers `unpriced`, which
|
|
12143
12153
|
* the gate declines to auto-credit from: the msat figure it would otherwise
|
|
12144
12154
|
* 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.
|
|
12155
|
+
* whenever that credit was funded, so it cannot safely fund a new ask.
|
|
12147
12156
|
*/
|
|
12148
12157
|
async resolveDrawPool(job) {
|
|
12149
12158
|
const ledger = this.opts.creditLedger;
|
|
@@ -12192,7 +12201,7 @@ var JobManager = class {
|
|
|
12192
12201
|
}
|
|
12193
12202
|
/**
|
|
12194
12203
|
* Issue the signed receipt for a job that has reached a terminal status
|
|
12195
|
-
*
|
|
12204
|
+
* Idempotent and safe to call from every terminal path — the
|
|
12196
12205
|
* store owns both the sequence allocation and the write-once persist, so
|
|
12197
12206
|
* two machines racing on the same job converge on identical bytes and burn
|
|
12198
12207
|
* exactly one sequence number.
|
|
@@ -12277,10 +12286,10 @@ var JobManager = class {
|
|
|
12277
12286
|
};
|
|
12278
12287
|
}
|
|
12279
12288
|
/**
|
|
12280
|
-
* Settle or release a terminal job's credit draw
|
|
12289
|
+
* Settle or release a terminal job's credit draw: `completed`
|
|
12281
12290
|
* settles (the hold becomes a real debit); `failed`/`cancelled` releases
|
|
12282
12291
|
* (the hold evaporates — "no debit on job failure", mechanically
|
|
12283
|
-
* superseding the
|
|
12292
|
+
* superseding the 974 no-op refund for credit-paid jobs, including
|
|
12284
12293
|
* the `ctx.fail` path and the stale-sweeper reap). Idempotent — a replayed
|
|
12285
12294
|
* resolution returns the recorded state. Returns the `ReceiptCredit` block
|
|
12286
12295
|
* for the receipt: `balance_after` is the recorded draw trajectory for a
|
|
@@ -12326,7 +12335,7 @@ var JobManager = class {
|
|
|
12326
12335
|
}
|
|
12327
12336
|
}
|
|
12328
12337
|
/**
|
|
12329
|
-
* The one place a terminal job's status becomes a ledger verb
|
|
12338
|
+
* The one place a terminal job's status becomes a ledger verb:
|
|
12330
12339
|
* `completed` settles the hold into a real debit, `failed`/`cancelled`
|
|
12331
12340
|
* release it. Read by the in-line funnel (`resolveCreditDraw`) and by the
|
|
12332
12341
|
* late reconciler (`reconcileTerminalDraw`) — two paths that must never
|
|
@@ -12341,45 +12350,47 @@ var JobManager = class {
|
|
|
12341
12350
|
* cleanup window hand back the same bytes as a cross-machine re-read.
|
|
12342
12351
|
*/
|
|
12343
12352
|
async attachReceipt(job) {
|
|
12344
|
-
const
|
|
12353
|
+
const record = await this.jobStore.get(job.id);
|
|
12354
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12355
|
+
const receipt = await this.issueReceipt(record);
|
|
12345
12356
|
if (receipt) job.receipt = receipt;
|
|
12346
12357
|
}
|
|
12347
12358
|
/**
|
|
12348
|
-
* Read-path repair for a terminal job carrying no receipt
|
|
12359
|
+
* Read-path repair for a terminal job carrying no receipt, for
|
|
12349
12360
|
* either an in-memory job or a store record. Free in the steady state — it
|
|
12350
12361
|
* returns on the `receipt` check without touching the store — so it costs
|
|
12351
12362
|
* only on the cases it exists for:
|
|
12352
12363
|
*
|
|
12353
12364
|
* - **A crash between the two store calls.** `claimReceiptSeq` commits the
|
|
12354
|
-
*
|
|
12355
|
-
*
|
|
12356
|
-
*
|
|
12357
|
-
*
|
|
12358
|
-
*
|
|
12359
|
-
*
|
|
12365
|
+
* sequence number to the job row before `saveReceipt` writes the bytes; a
|
|
12366
|
+
* process death in that window would otherwise strand that number
|
|
12367
|
+
* forever, and a permanent gap is indistinguishable from the deliberate
|
|
12368
|
+
* suppression `seq` exists to expose. Re-issuing here reuses the already
|
|
12369
|
+
* committed number (the claim is idempotent per job) rather than
|
|
12370
|
+
* allocating a second one.
|
|
12360
12371
|
* - **A transient store failure** at the terminal: `issueReceipt` logs
|
|
12361
|
-
*
|
|
12362
|
-
*
|
|
12372
|
+
* `receipt_issue_failed` and returns nothing rather than failing the job,
|
|
12373
|
+
* so the next read retries.
|
|
12363
12374
|
* - **Jobs that terminated before the DVM had a receipt key.** They pick one
|
|
12364
|
-
*
|
|
12375
|
+
* up on first read, with `issued_at` reflecting when it was signed.
|
|
12365
12376
|
*/
|
|
12366
12377
|
async ensureReceipt(target) {
|
|
12367
|
-
if (target.receipt)
|
|
12368
|
-
|
|
12378
|
+
if (target.receipt) {
|
|
12379
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12380
|
+
return;
|
|
12381
|
+
}
|
|
12382
|
+
const record = "listeners" in target ? await this.jobStore.get(target.id) : target;
|
|
12383
|
+
if (!record || !isTerminal(record.status)) return;
|
|
12369
12384
|
const receipt = await this.issueReceipt(record);
|
|
12370
12385
|
if (receipt) target.receipt = receipt;
|
|
12386
|
+
if (isTerminal(target.status)) await this.reportTerminalJob(target.id);
|
|
12371
12387
|
}
|
|
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
|
-
*/
|
|
12388
|
+
/** Commit the terminal snapshot before resolving money and signing its durable evidence. */
|
|
12379
12389
|
async finalizeTerminal(job) {
|
|
12390
|
+
await this.persistJob(job);
|
|
12380
12391
|
await this.attachReceipt(job);
|
|
12381
|
-
await this.persistJob(job).catch(() => void 0);
|
|
12382
12392
|
await this.reportJobCost(job);
|
|
12393
|
+
await this.reportTerminalJob(job.id);
|
|
12383
12394
|
}
|
|
12384
12395
|
/**
|
|
12385
12396
|
* Issue a receipt for a job this process doesn't hold in `activeJobs` — a
|
|
@@ -12398,20 +12409,68 @@ var JobManager = class {
|
|
|
12398
12409
|
}
|
|
12399
12410
|
/**
|
|
12400
12411
|
* Finish accounting for a terminal written directly to the store — the
|
|
12401
|
-
* stale reaper and a cancel handled on a different machine
|
|
12412
|
+
* stale reaper and a cancel handled on a different machine.
|
|
12402
12413
|
*/
|
|
12403
12414
|
async finalizeStoredTerminal(jobId) {
|
|
12404
12415
|
const receipt = await this.issueReceiptForStoredJob(jobId);
|
|
12405
12416
|
const record = await this.jobStore.get(jobId).catch(() => void 0);
|
|
12406
|
-
if (record && isTerminal(record.status))
|
|
12417
|
+
if (record && isTerminal(record.status)) {
|
|
12418
|
+
await this.reportJobCost(record);
|
|
12419
|
+
await this.reportTerminalJob(jobId);
|
|
12420
|
+
}
|
|
12407
12421
|
return receipt;
|
|
12408
12422
|
}
|
|
12409
|
-
|
|
12423
|
+
async reportTerminalJob(jobId) {
|
|
12424
|
+
try {
|
|
12425
|
+
const callback = this.opts.onJobTerminal;
|
|
12426
|
+
const dvmId = this.opts.dvmId;
|
|
12427
|
+
if (!callback || !dvmId) return;
|
|
12428
|
+
const record = await this.jobStore.get(jobId);
|
|
12429
|
+
if (!record || !isReceiptableStatus(record.status) || record.timingKnown === false || record.terminalAtMs === void 0 || record.status !== "completed" && !record.endedBy) return;
|
|
12430
|
+
const receipt = await this.issueReceipt(record);
|
|
12431
|
+
if (this.opts.receiptIssuer && !receipt) return;
|
|
12432
|
+
if (record.creditId && record.drawId && this.opts.creditLedger) {
|
|
12433
|
+
const draw = await this.opts.creditLedger.getDraw({ creditId: record.creditId, drawId: record.drawId });
|
|
12434
|
+
if (!draw || draw.status === "pending") return;
|
|
12435
|
+
}
|
|
12436
|
+
await callback({
|
|
12437
|
+
dvmId,
|
|
12438
|
+
jobId: record.id,
|
|
12439
|
+
capability: record.capability,
|
|
12440
|
+
outcome: record.status,
|
|
12441
|
+
endedBy: record.status === "completed" ? null : record.endedBy ?? "provider",
|
|
12442
|
+
paid: record.paidMsats > 0 || (record.nativeAmount ?? 0) > 0 || !!record.drawId,
|
|
12443
|
+
acceptedAtMs: record.createdAt,
|
|
12444
|
+
terminalAtMs: record.terminalAtMs,
|
|
12445
|
+
callerWaitMs: record.callerWaitMs ?? 0,
|
|
12446
|
+
receiptSeq: receipt?.seq ?? null
|
|
12447
|
+
});
|
|
12448
|
+
if ("markTerminalReported" in this.jobStore) {
|
|
12449
|
+
await this.jobStore.markTerminalReported(jobId);
|
|
12450
|
+
}
|
|
12451
|
+
} catch (error) {
|
|
12452
|
+
console.error(`[${jobId}] Terminal reporting failed:`, error);
|
|
12453
|
+
}
|
|
12454
|
+
}
|
|
12455
|
+
terminalReportRecoveryCursor;
|
|
12456
|
+
async recoverTerminalReports() {
|
|
12457
|
+
const store = this.jobStore;
|
|
12458
|
+
const dvmId = this.opts.dvmId;
|
|
12459
|
+
if (!this.opts.onJobTerminal || !dvmId || !("listUnreportedTerminalReports" in store)) return;
|
|
12460
|
+
const recovery = store;
|
|
12461
|
+
const records = await recovery.listUnreportedTerminalReports(dvmId, this.capabilityNames(), this.terminalReportRecoveryCursor);
|
|
12462
|
+
for (const record of records) await this.reportTerminalJob(record.id);
|
|
12463
|
+
this.terminalReportRecoveryCursor = records.length === 100 ? records[records.length - 1].id : void 0;
|
|
12464
|
+
}
|
|
12410
12465
|
async handleJobTerminal(job) {
|
|
12411
12466
|
this.clearIdleTimer(job.id);
|
|
12467
|
+
if (await this.adoptDurableTerminal(job)) return;
|
|
12468
|
+
const durable = await this.jobStore.get(job.id);
|
|
12469
|
+
if (!durable || !isTerminal(durable.status)) await this.persistJob(job);
|
|
12412
12470
|
await this.attachReceipt(job);
|
|
12413
12471
|
const booked = await this.bookRevenue(job);
|
|
12414
12472
|
if (!booked) await this.reportJobCost(job);
|
|
12473
|
+
await this.reportTerminalJob(job.id);
|
|
12415
12474
|
this.scheduleCleanup(job);
|
|
12416
12475
|
}
|
|
12417
12476
|
/**
|
|
@@ -12424,6 +12483,7 @@ var JobManager = class {
|
|
|
12424
12483
|
async recoverTerminalCosts() {
|
|
12425
12484
|
const store = this.jobStore;
|
|
12426
12485
|
const dvmId = this.opts.dvmId;
|
|
12486
|
+
await this.recoverTerminalReports();
|
|
12427
12487
|
if (!dvmId || !this.opts.onJobCost || !isTerminalCostRecoveryStore(store)) {
|
|
12428
12488
|
return { examined: 0, queued: 0, complete: true };
|
|
12429
12489
|
}
|
|
@@ -12508,11 +12568,11 @@ var JobManager = class {
|
|
|
12508
12568
|
return true;
|
|
12509
12569
|
}
|
|
12510
12570
|
/**
|
|
12511
|
-
* Report a completed paid job's revenue
|
|
12571
|
+
* Report a completed paid job's revenue under its durable job identity.
|
|
12512
12572
|
* Fire-and-forget — the `RevenueReporter` owns persistence and retry.
|
|
12513
12573
|
*
|
|
12514
12574
|
* For a **credit-backed** job the revenue event is the *settled draw* (spec
|
|
12515
|
-
* §10), and since
|
|
12575
|
+
* §10), and since the behavior was introduced the sats figure corrects the job's mirror of that
|
|
12516
12576
|
* draw against the draw itself: `withDrawBasis` stamps the job when the
|
|
12517
12577
|
* payment lands, but a Bitcoin credit's settle re-prices the draw against
|
|
12518
12578
|
* the funding lots it consumed. The correction is a delta, because a job's
|
|
@@ -12527,7 +12587,7 @@ var JobManager = class {
|
|
|
12527
12587
|
* whose price couldn't be fiat-denominated) nothing changes: the pre-credits
|
|
12528
12588
|
* payload is reported verbatim.
|
|
12529
12589
|
*
|
|
12530
|
-
* Takes the fields rather than a `ServerJob` so the
|
|
12590
|
+
* Takes the fields rather than a `ServerJob` so the reconciler — a
|
|
12531
12591
|
* background sweep that holds no in-process job — books through this exact
|
|
12532
12592
|
* path off the row it read. `paymentTxHash` is passed **verbatim**, never
|
|
12533
12593
|
* recomputed as a `drawSettlementRef`: it is the same column the in-line
|
|
@@ -12544,7 +12604,7 @@ var JobManager = class {
|
|
|
12544
12604
|
* DVM with no reporter). Every drop that costs a booking logs first.
|
|
12545
12605
|
*
|
|
12546
12606
|
* The rail check sits **below** the draw re-read, not in the entry guard
|
|
12547
|
-
*
|
|
12607
|
+
* a settled draw the terminal can't report under is a real debit
|
|
12548
12608
|
* with no revenue event, permanently overstating outstanding liability
|
|
12549
12609
|
* (`deposits − draw revenue`), and it used to return here in silence. Placed
|
|
12550
12610
|
* after the re-read, `revenue_skipped_no_rail` can name the draw and its
|
|
@@ -12607,9 +12667,9 @@ var JobManager = class {
|
|
|
12607
12667
|
paidMsats: job.paidMsats,
|
|
12608
12668
|
creditRail: creditRail ?? null,
|
|
12609
12669
|
// Four distinct causes needing four distinct fixes, so none of them
|
|
12610
|
-
// is the else-branch: the credit predates
|
|
12670
|
+
// is the else-branch: the credit predates and has no rail
|
|
12611
12671
|
// (`credit_rail_null`); it carries one this build doesn't list, the
|
|
12612
|
-
//
|
|
12672
|
+
// sibling class (`credit_rail_unrecognized`); the credit's
|
|
12613
12673
|
// rail is perfectly good and it is the *job row* that lost its own
|
|
12614
12674
|
// (`job_rail_missing`), which no change to the ledger would fix; or
|
|
12615
12675
|
// this manager was built with no `creditLedger` to read the draw
|
|
@@ -12657,7 +12717,7 @@ var JobManager = class {
|
|
|
12657
12717
|
cashuFlow: job.cashuFlow,
|
|
12658
12718
|
// Absent unless the handler declared one, and absent is a fact: the
|
|
12659
12719
|
// platform shows "no costs reported" rather than a zero cost and a
|
|
12660
|
-
// 100% margin
|
|
12720
|
+
// 100% margin. Both halves or neither — a bare amount
|
|
12661
12721
|
// in an unnamed currency is not a cost.
|
|
12662
12722
|
...job.costAmountMicro !== void 0 && job.costCurrency !== void 0 ? { cost: { amountMicro: job.costAmountMicro, currency: job.costCurrency } } : {},
|
|
12663
12723
|
...credit
|
|
@@ -12677,7 +12737,6 @@ var JobManager = class {
|
|
|
12677
12737
|
}, 6e4);
|
|
12678
12738
|
this.cleanupTimers.set(job.id, timer);
|
|
12679
12739
|
}
|
|
12680
|
-
/** Clear all timers. Called on server shutdown to allow clean exit. */
|
|
12681
12740
|
shutdown() {
|
|
12682
12741
|
for (const [id] of this.idleTimers) {
|
|
12683
12742
|
this.clearIdleTimer(id);
|
|
@@ -12840,8 +12899,8 @@ var MintHealthTracker = class {
|
|
|
12840
12899
|
firstProbeComplete = false;
|
|
12841
12900
|
constructor(opts) {
|
|
12842
12901
|
this.mints = [...opts.mints];
|
|
12843
|
-
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs();
|
|
12844
|
-
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick();
|
|
12902
|
+
this.tickIntervalMs = opts.tickIntervalMs ?? resolveEnvTickMs(opts.env);
|
|
12903
|
+
this.failuresToSick = opts.consecutiveFailuresToSick ?? resolveEnvFailuresToSick(opts.env);
|
|
12845
12904
|
this.fetchImpl = opts.fetchImpl;
|
|
12846
12905
|
this.probeOpts = { ...opts.probe, fetchImpl: opts.fetchImpl };
|
|
12847
12906
|
this.nowFn = opts.now ?? Date.now;
|
|
@@ -12892,7 +12951,7 @@ var MintHealthTracker = class {
|
|
|
12892
12951
|
/**
|
|
12893
12952
|
* True once the first full probe tick has landed. Before this, paid calls
|
|
12894
12953
|
* are held at `503 mint_health_pending` rather than accepted against
|
|
12895
|
-
* unvalidated mints
|
|
12954
|
+
* unvalidated mints.
|
|
12896
12955
|
*/
|
|
12897
12956
|
initialProbeComplete() {
|
|
12898
12957
|
return this.firstProbeComplete;
|
|
@@ -12917,12 +12976,11 @@ var MintHealthTracker = class {
|
|
|
12917
12976
|
* gate — a deterministic, permanent config error, unlike a transient outage.
|
|
12918
12977
|
* Unknown mints return false. `advertisedMints()` uses this to keep a
|
|
12919
12978
|
* permanently-unsettleable mint off `/v1/info` even in the all-sick fallback,
|
|
12920
|
-
* where a merely-flapping mint is still published
|
|
12979
|
+
* where a merely-flapping mint is still published.
|
|
12921
12980
|
*/
|
|
12922
12981
|
isNutIncompatible(mintUrl) {
|
|
12923
12982
|
return this.state.get(normaliseMintUrl(mintUrl))?.nutIncompatible ?? false;
|
|
12924
12983
|
}
|
|
12925
|
-
/** Plain-object snapshot keyed by mint URL — for tests + observability. */
|
|
12926
12984
|
snapshot() {
|
|
12927
12985
|
return Object.fromEntries(Array.from(this.state.entries()).map(([k, v]) => [k, { ...v }]));
|
|
12928
12986
|
}
|
|
@@ -12980,12 +13038,7 @@ var MintHealthTracker = class {
|
|
|
12980
13038
|
);
|
|
12981
13039
|
}
|
|
12982
13040
|
}
|
|
12983
|
-
/**
|
|
12984
|
-
* One-shot structured summary emitted after the first probe tick. Replaces
|
|
12985
|
-
* the boot check's per-mint `cashu_mint_startup_health` lines and surfaces an
|
|
12986
|
-
* all-mints-unhealthy warning — the operator alert that the Fly restart-loop
|
|
12987
|
-
* used to be before boot stopped refusing to start (internal-review).
|
|
12988
|
-
*/
|
|
13041
|
+
/** One boot summary, including an all-unhealthy warning. */
|
|
12989
13042
|
logBootHealth() {
|
|
12990
13043
|
const mints = Object.entries(this.snapshot()).map(([mintUrl, s]) => ({
|
|
12991
13044
|
mintUrl,
|
|
@@ -13009,15 +13062,15 @@ var MintHealthTracker = class {
|
|
|
13009
13062
|
);
|
|
13010
13063
|
}
|
|
13011
13064
|
};
|
|
13012
|
-
function resolveEnvTickMs() {
|
|
13013
|
-
const raw =
|
|
13065
|
+
function resolveEnvTickMs(env = process.env) {
|
|
13066
|
+
const raw = env.DVMKIT_MINT_HEALTH_TICK_MS;
|
|
13014
13067
|
if (!raw) return DEFAULT_TICK_INTERVAL_MS2;
|
|
13015
13068
|
const parsed = Number(raw);
|
|
13016
13069
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_TICK_INTERVAL_MS2;
|
|
13017
13070
|
return Math.floor(parsed);
|
|
13018
13071
|
}
|
|
13019
|
-
function resolveEnvFailuresToSick() {
|
|
13020
|
-
const raw =
|
|
13072
|
+
function resolveEnvFailuresToSick(env = process.env) {
|
|
13073
|
+
const raw = env.DVMKIT_MINT_FAILURES_TO_SICK;
|
|
13021
13074
|
if (!raw) return DEFAULT_FAILURES_TO_SICK;
|
|
13022
13075
|
const parsed = Number(raw);
|
|
13023
13076
|
if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_FAILURES_TO_SICK;
|
|
@@ -13116,7 +13169,6 @@ var PostgresX402ExactSettlementStore = class {
|
|
|
13116
13169
|
this.pool = pool;
|
|
13117
13170
|
}
|
|
13118
13171
|
pool;
|
|
13119
|
-
/** Create the exact settlement tables. */
|
|
13120
13172
|
async init() {
|
|
13121
13173
|
await withSdkInitLock(this.pool, async () => {
|
|
13122
13174
|
await this.pool.query(`
|
|
@@ -14062,20 +14114,11 @@ async function handlePendingDrains(c, opts) {
|
|
|
14062
14114
|
amount_micro: d.amountMicro,
|
|
14063
14115
|
amount_sats: amountSats,
|
|
14064
14116
|
amount_msats: amountSats === null ? null : amountSats * 1e3,
|
|
14065
|
-
// Whether this row's sats figure is the in-kind obligation or a live-rate
|
|
14066
|
-
// conversion. An operator settling a refund should be able to see which,
|
|
14067
|
-
// and the servicer's own logs carry it for the same reason.
|
|
14068
14117
|
sats_basis: amountSats === null ? null : d.owedSats === null ? "rate" : "in_kind",
|
|
14069
|
-
//
|
|
14070
|
-
//
|
|
14071
|
-
// `completeX402ChannelDrain` treats as its settlement floor (internal-review),
|
|
14072
|
-
// so publishing it is what lets an operator settle from the row instead
|
|
14073
|
-
// of assuming their stablecoin happens to be six-decimal (internal-review).
|
|
14118
|
+
// Rail-native debit, distinct from fiat amount_micro. This is the channel refund floor
|
|
14119
|
+
// and the size a manual payout must cover.
|
|
14074
14120
|
amount_native: d.drainedNative,
|
|
14075
14121
|
settlement,
|
|
14076
|
-
// Why this row is classified as it is, and the channel an operator would
|
|
14077
|
-
// look up to see the close land. `payout` cannot answer it: a
|
|
14078
|
-
// channel-backed mpp drain carries `{}`.
|
|
14079
14122
|
channel_id: d.channelId,
|
|
14080
14123
|
created_at: d.createdAt
|
|
14081
14124
|
};
|
|
@@ -14086,13 +14129,7 @@ async function handlePendingDrains(c, opts) {
|
|
|
14086
14129
|
acc[method] = pending.filter((d) => d.method === method).reduce((sum, d) => sum + d.amountMicro, 0);
|
|
14087
14130
|
return acc;
|
|
14088
14131
|
}, {}),
|
|
14089
|
-
// What no job will ever pay down: the operator's own worklist, in the one
|
|
14090
|
-
// denomination these rows have.
|
|
14091
14132
|
manual_micro: drains.filter((d) => d.settlement === "manual").reduce((sum, d) => sum + d.amount_micro, 0),
|
|
14092
|
-
// The rest of the stablecoin liability — owed, but by a channel close in
|
|
14093
|
-
// flight rather than by a person. Published beside `manual_micro` so the
|
|
14094
|
-
// difference between the two is visible rather than inferred from a
|
|
14095
|
-
// `by_method_micro` figure neither of them matches.
|
|
14096
14133
|
channel_micro: drains.filter((d) => d.settlement === "channel").reduce((sum, d) => sum + d.amount_micro, 0)
|
|
14097
14134
|
};
|
|
14098
14135
|
return c.json({ drains, liability, mints: opts.mints?.() ?? [] });
|
|
@@ -14121,17 +14158,21 @@ async function handleBitcoinLiability(c, opts) {
|
|
|
14121
14158
|
drain_liability_sats: drainSats,
|
|
14122
14159
|
drain_count: promised.length,
|
|
14123
14160
|
total_owed_sats: deposit.depositSats + drainSats,
|
|
14124
|
-
//
|
|
14125
|
-
//
|
|
14126
|
-
//
|
|
14127
|
-
// prices the whole balance off a live rate, so both are reported whole and
|
|
14128
|
-
// neither contributes to `deposit_liability_sats` — a sweep computed
|
|
14129
|
-
// against `total_owed_sats` alone is short by whatever they turn out to be
|
|
14130
|
-
// worth.
|
|
14161
|
+
// Uncovered or basisless balances require whole-balance live-rate pricing and do not
|
|
14162
|
+
// contribute to deposit_liability_sats. total_owed_sats alone cannot reserve their
|
|
14163
|
+
// eventual refunds.
|
|
14131
14164
|
uncovered_credits: deposit.uncoveredCredits,
|
|
14132
|
-
|
|
14165
|
+
uncovered_by_currency: deposit.uncoveredByCurrency.map((currency) => ({
|
|
14166
|
+
currency: currency.currency,
|
|
14167
|
+
micro: currency.micro,
|
|
14168
|
+
credits: currency.credits
|
|
14169
|
+
})),
|
|
14133
14170
|
unbacked_credits: deposit.unbackedCredits,
|
|
14134
|
-
|
|
14171
|
+
unbacked_by_currency: deposit.unbackedByCurrency.map((currency) => ({
|
|
14172
|
+
currency: currency.currency,
|
|
14173
|
+
micro: currency.micro,
|
|
14174
|
+
credits: currency.credits
|
|
14175
|
+
})),
|
|
14135
14176
|
unpriced_drains: drainsUnpriced
|
|
14136
14177
|
});
|
|
14137
14178
|
}
|
|
@@ -14569,9 +14610,6 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14569
14610
|
const result = await opts.ledger.reconcileBlockedInvoice({
|
|
14570
14611
|
paymentHash,
|
|
14571
14612
|
targetCreditId,
|
|
14572
|
-
// `fund`'s upsert OVERWRITES `expiry_ms`, so a bare `now + ttl` would
|
|
14573
|
-
// shorten a target credit with more life left on it — destroying
|
|
14574
|
-
// spendable value while purporting to add some.
|
|
14575
14613
|
expiryMs,
|
|
14576
14614
|
basis,
|
|
14577
14615
|
...settledAtMs === void 0 ? {} : { settledAtMs },
|
|
@@ -14592,9 +14630,6 @@ async function handleReconcileInvoice(c, opts) {
|
|
|
14592
14630
|
creditCurrency: invoice.currency,
|
|
14593
14631
|
fundedAt: settledAtMs ?? invoice.settledAt ?? nowMs,
|
|
14594
14632
|
expiryMs,
|
|
14595
|
-
// The settlements table types this deposit `repair` off the
|
|
14596
|
-
// flag (internal-review): the sats landed at payment time and the
|
|
14597
|
-
// deposit is the one row they get.
|
|
14598
14633
|
repaired: true
|
|
14599
14634
|
}
|
|
14600
14635
|
}
|
|
@@ -14677,8 +14712,6 @@ async function handleWriteOffInvoice(c, opts) {
|
|
|
14677
14712
|
amount_micro: written.amountMicro,
|
|
14678
14713
|
currency: written.currency,
|
|
14679
14714
|
note: written.writeOffNote,
|
|
14680
|
-
// Mirrors the reconcile's `replayed`: the operator asked for a decision and
|
|
14681
|
-
// finds one already recorded, possibly with someone else's note on it.
|
|
14682
14715
|
replayed: !outcome.written
|
|
14683
14716
|
});
|
|
14684
14717
|
}
|
|
@@ -16588,7 +16621,7 @@ function autoApprovePayment(content) {
|
|
|
16588
16621
|
headers: jobHeaders(true),
|
|
16589
16622
|
// amount_msats rides along so an onPayment handler sees the same shape a
|
|
16590
16623
|
// real rail would hand it. dev_auto is admitted on dev-mode servers only
|
|
16591
|
-
//
|
|
16624
|
+
// hence the status check below rather than a silent catch.
|
|
16592
16625
|
body: JSON.stringify({ type: "payment", content: { dev_auto: true, amount_msats: content.amount_msats || 0 } }),
|
|
16593
16626
|
}).then(function(r) {
|
|
16594
16627
|
if (!r.ok) addMsg("Auto-approve rejected (" + r.status + ") \u2014 dev mode only.", "msg-system");
|
|
@@ -16735,7 +16768,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16735
16768
|
}
|
|
16736
16769
|
})();
|
|
16737
16770
|
try {
|
|
16738
|
-
if (
|
|
16771
|
+
if (opts.env.DVMKIT_CASHU_MINTS && opts.cashuMode === void 0) {
|
|
16739
16772
|
console.warn(
|
|
16740
16773
|
JSON.stringify({
|
|
16741
16774
|
level: "cashu_opts_env_ignored",
|
|
@@ -16748,12 +16781,12 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16748
16781
|
const platformUrl = opts.platformUrl ?? envMaybe.DVMKIT_PLATFORM_URL;
|
|
16749
16782
|
const dvmId = opts.dvmId;
|
|
16750
16783
|
if (!opts.onJobCompleted && platformToken && platformUrl && dvmId && opts.db) {
|
|
16751
|
-
const { RevenueReporter } = await import("./revenue-reporter-
|
|
16784
|
+
const { RevenueReporter } = await import("./revenue-reporter-O75BGYFK.js");
|
|
16752
16785
|
const reporter = new RevenueReporter(opts.db, platformUrl, platformToken);
|
|
16753
16786
|
revenueReporter = reporter;
|
|
16754
16787
|
await reporter.init();
|
|
16755
16788
|
reporter.startRetryLoop();
|
|
16756
|
-
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-
|
|
16789
|
+
const { MemoryX402BatchStore, PayoutReporter } = await import("./payout-reporter-KEMUXEUO.js");
|
|
16757
16790
|
payoutReporter = new PayoutReporter({
|
|
16758
16791
|
dvmId,
|
|
16759
16792
|
db: opts.db,
|
|
@@ -16896,6 +16929,7 @@ async function createDVMServer(descriptor, opts) {
|
|
|
16896
16929
|
processedPayments,
|
|
16897
16930
|
onJobCompleted: effectiveOnJobCompleted,
|
|
16898
16931
|
onJobCost: effectiveOnJobCost,
|
|
16932
|
+
onJobTerminal: activeRevenueReporter?.reportTerminal.bind(activeRevenueReporter),
|
|
16899
16933
|
onPaidJobDeath: effectiveOnPaidJobDeath,
|
|
16900
16934
|
onRevenueSkippedNoRail: effectiveOnRevenueSkippedNoRail,
|
|
16901
16935
|
onCreditFunded: opts.onCreditFunded,
|
|
@@ -16939,7 +16973,7 @@ var DVMServer = class {
|
|
|
16939
16973
|
fxFetcher;
|
|
16940
16974
|
/**
|
|
16941
16975
|
* The currency this DVM prices in and denominates its credit ledger in,
|
|
16942
|
-
* resolved once at construction
|
|
16976
|
+
* resolved once at construction.
|
|
16943
16977
|
*
|
|
16944
16978
|
* `configureDVM` and the CLI loader both resolve this field. The server is
|
|
16945
16979
|
* also exported for direct JavaScript callers, so defaulting at the one
|
|
@@ -16948,29 +16982,24 @@ var DVMServer = class {
|
|
|
16948
16982
|
*/
|
|
16949
16983
|
pricingCurrency;
|
|
16950
16984
|
/**
|
|
16951
|
-
* Last-good lock-pubkey state observed from Postgres
|
|
16985
|
+
* Last-good lock-pubkey state observed from Postgres. Used as the
|
|
16952
16986
|
* fallback when `loadLockPubkeyState` throws — degrades more gracefully than
|
|
16953
16987
|
* falling back to the env-derived seed, which post-rotation is the *retired*
|
|
16954
16988
|
* pubkey. Null until the first successful read.
|
|
16955
16989
|
*/
|
|
16956
16990
|
cachedLockPubkeyState = null;
|
|
16957
|
-
/** Guards the opportunistic Tempo settlement cadence (internal-review). */
|
|
16958
16991
|
tempoSweepInFlight = false;
|
|
16959
16992
|
tempoSweepLastAttemptMs = 0;
|
|
16960
|
-
/** Receipt signer for this DVM (internal-review). Undefined when no key is wired. */
|
|
16961
16993
|
receiptIssuer;
|
|
16962
|
-
/** Credit ledger the payment gate funds/draws through (internal-review). */
|
|
16963
16994
|
creditLedger;
|
|
16964
|
-
/** Durable x402/mpp funding markers (internal-review). */
|
|
16965
16995
|
processedPayments;
|
|
16966
16996
|
authAudience;
|
|
16967
16997
|
/** Lazily constructed when a refreshed facilitator probe gains batch support. */
|
|
16968
16998
|
x402BatchSettlement;
|
|
16969
16999
|
/** Coalesces concurrent requests that first observe batch support. */
|
|
16970
17000
|
x402BatchSettlementInit;
|
|
16971
|
-
/** Durable exact-settlement coordinator shared by every exact payment surface. */
|
|
16972
17001
|
x402ExactSettlement;
|
|
16973
|
-
/** Withdraws this mount's Tempo session capability from the poll
|
|
17002
|
+
/** Withdraws this mount's Tempo session capability from the poll. */
|
|
16974
17003
|
undeclareSessionCapability;
|
|
16975
17004
|
constructor(descriptor, opts) {
|
|
16976
17005
|
this.descriptor = descriptor;
|
|
@@ -16994,7 +17023,7 @@ var DVMServer = class {
|
|
|
16994
17023
|
this.mintHealthTracker = opts.mintHealthTracker;
|
|
16995
17024
|
this.ownsMintHealthTracker = false;
|
|
16996
17025
|
} else if (opts.mints && opts.mints.length > 0) {
|
|
16997
|
-
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints });
|
|
17026
|
+
this.mintHealthTracker = new MintHealthTracker({ mints: opts.mints, env: opts.env });
|
|
16998
17027
|
this.ownsMintHealthTracker = true;
|
|
16999
17028
|
} else {
|
|
17000
17029
|
this.ownsMintHealthTracker = false;
|
|
@@ -17015,17 +17044,9 @@ var DVMServer = class {
|
|
|
17015
17044
|
mints: opts.mints,
|
|
17016
17045
|
jobStore: opts.jobStore,
|
|
17017
17046
|
devMode: opts.devMode,
|
|
17018
|
-
//
|
|
17019
|
-
//
|
|
17020
|
-
//
|
|
17021
|
-
// raw option meant a DVM that never set `paymentMethods` (no first-party
|
|
17022
|
-
// one does; the descriptor documents it as "derived from configured
|
|
17023
|
-
// rails") advertised both rails, took the upfront payment on either, and
|
|
17024
|
-
// then emitted a mid-job payment-request carrying no rail at all. The
|
|
17025
|
-
// caller's responder had nothing to sign and the job stalled at
|
|
17026
|
-
// needs-approval — reachable only from a rung that raises a real mid-job
|
|
17027
|
-
// ask, which nothing above L1 did. Facilitator availability is still
|
|
17028
|
-
// re-checked per ask in `context.ts`, so a static list is safe here.
|
|
17047
|
+
// Use the same derived rail list as discovery/upfront payment; otherwise
|
|
17048
|
+
// default-configured rails vanish from mid-job asks. Availability is rechecked
|
|
17049
|
+
// per ask.
|
|
17029
17050
|
paymentMethods: (opts.paymentMethods ?? this.defaultPaymentMethods()).filter(
|
|
17030
17051
|
(method) => method !== "x402" || this.x402CurrencySupported()
|
|
17031
17052
|
),
|
|
@@ -17040,21 +17061,20 @@ var DVMServer = class {
|
|
|
17040
17061
|
dvmId: opts.dvmId,
|
|
17041
17062
|
authAudience: this.authAudience,
|
|
17042
17063
|
fxFetcher: this.fxFetcher,
|
|
17043
|
-
//
|
|
17064
|
+
// the mid-job top-up leg needs the same rail wiring the upfront
|
|
17044
17065
|
// path gets — a divergence here would be a DVM whose upfront payment funds
|
|
17045
17066
|
// a credit and whose mid-job payment silently doesn't.
|
|
17046
17067
|
processedPayments: this.processedPayments,
|
|
17047
17068
|
enqueueCreditDeposit: opts.enqueueCreditDeposit,
|
|
17048
17069
|
enqueueCreditDrawRelease: opts.enqueueCreditDrawRelease,
|
|
17049
17070
|
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
17071
|
pricingCurrency: this.pricingCurrency,
|
|
17053
17072
|
onCreditFunded: opts.onCreditFunded,
|
|
17054
17073
|
onJobCompleted: opts.onJobCompleted,
|
|
17055
17074
|
onPaidJobCompleted: opts.onPaidJobCompleted,
|
|
17056
17075
|
paymentMode: opts.paymentMode,
|
|
17057
17076
|
onJobCost: opts.onJobCost,
|
|
17077
|
+
onJobTerminal: opts.onJobTerminal,
|
|
17058
17078
|
onPaidJobDeath: opts.onPaidJobDeath,
|
|
17059
17079
|
onRevenueSkippedNoRail: opts.onRevenueSkippedNoRail,
|
|
17060
17080
|
onJobRetentionSweepFailed: opts.onJobRetentionSweepFailed,
|
|
@@ -17092,7 +17112,7 @@ var DVMServer = class {
|
|
|
17092
17112
|
* Tear down this mount. Every leg is synchronous except the batch-settlement
|
|
17093
17113
|
* server's own `stop()`; the returned promise settles once that has finished,
|
|
17094
17114
|
* so a caller holding a resource behind it — the settlement pool the spend
|
|
17095
|
-
* gate's store owns
|
|
17115
|
+
* gate's store owns — can sequence its close instead of pulling it
|
|
17096
17116
|
* out from under an in-flight stop. A rejection remains observable so the app
|
|
17097
17117
|
* and host teardown layers can finish later cleanup before surfacing it.
|
|
17098
17118
|
*/
|
|
@@ -17193,12 +17213,12 @@ var DVMServer = class {
|
|
|
17193
17213
|
(c) => c.json({
|
|
17194
17214
|
ok: true,
|
|
17195
17215
|
// shortSha, not .slice(0, 7): a dirty-tree deploy bakes a `-dirty`
|
|
17196
|
-
// marker
|
|
17216
|
+
// marker that a raw truncation would cut off the end,
|
|
17197
17217
|
// leaving this endpoint claiming a clean commit while /v1/info's
|
|
17198
17218
|
// untruncated sdk_sha says otherwise — same image, two answers.
|
|
17199
|
-
gitSha: shortSha(
|
|
17200
|
-
deployedAt:
|
|
17201
|
-
deployId:
|
|
17219
|
+
gitSha: shortSha(this.opts.env.GIT_SHA ?? "unknown"),
|
|
17220
|
+
deployedAt: this.opts.env.BUILD_TIME ?? "unknown",
|
|
17221
|
+
deployId: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17202
17222
|
})
|
|
17203
17223
|
);
|
|
17204
17224
|
if (this.opts.devMode) {
|
|
@@ -17231,8 +17251,6 @@ var DVMServer = class {
|
|
|
17231
17251
|
getLockPubkeyState: () => this.loadActiveLockPubkeyState(db, dvmId),
|
|
17232
17252
|
graceSeconds: this.graceSeconds(),
|
|
17233
17253
|
nonceStore: this.adminCashuNonceStore,
|
|
17234
|
-
// internal-review: a completed melt is a landed payout, queued in the
|
|
17235
|
-
// transaction that marks it.
|
|
17236
17254
|
...payoutReporter ? { onMelted: payoutReporter.meltHook() } : {}
|
|
17237
17255
|
});
|
|
17238
17256
|
}
|
|
@@ -17250,13 +17268,13 @@ var DVMServer = class {
|
|
|
17250
17268
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
17251
17269
|
enqueueCreditDrain: this.opts.enqueueCreditDrain,
|
|
17252
17270
|
lightningReceive: this.opts.lightningReceive,
|
|
17253
|
-
//
|
|
17271
|
+
// x402 repair queue rides the same surface, and resolves
|
|
17254
17272
|
// the rail the same lazy way the verify path does — constructing the
|
|
17255
17273
|
// settlement server probes the facilitator, which must not happen just
|
|
17256
17274
|
// because the admin routes were mounted.
|
|
17257
17275
|
x402BatchSettlement: () => this.resolveX402BatchSettlement(),
|
|
17258
17276
|
x402ExactSettlement: this.x402ExactSettlement,
|
|
17259
|
-
//
|
|
17277
|
+
// Tempo repair queue, same surface again. The mppx handle is
|
|
17260
17278
|
// already built by the time routes mount, so the thunk is uniformity
|
|
17261
17279
|
// rather than laziness — and it is the *handle*, not the store, so a
|
|
17262
17280
|
// DVM wired with an in-memory session store resolves to a rail with no
|
|
@@ -17264,10 +17282,8 @@ var DVMServer = class {
|
|
|
17264
17282
|
tempoDrainRail: () => Promise.resolve(this.opts.mpp),
|
|
17265
17283
|
// Health-filtered, and read per request rather than captured: the
|
|
17266
17284
|
// servicer picking a mint to buy a refund at wants the list that
|
|
17267
|
-
// excludes a mint currently failing its probe
|
|
17285
|
+
// excludes a mint currently failing its probe.
|
|
17268
17286
|
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
17287
|
...payoutReporter ? { onTempoDrainReconciled: payoutReporter.tempoRepairHook() } : {}
|
|
17272
17288
|
});
|
|
17273
17289
|
}
|
|
@@ -17278,7 +17294,7 @@ var DVMServer = class {
|
|
|
17278
17294
|
/**
|
|
17279
17295
|
* Load the live lock-pubkey state for this DVM. Reads from
|
|
17280
17296
|
* `dvm_lock_pubkeys` on every call so rotations propagate without needing
|
|
17281
|
-
* a process restart
|
|
17297
|
+
* a process restart. On success the result is cached so a later
|
|
17282
17298
|
* DB outage can degrade to the last-observed state instead of the
|
|
17283
17299
|
* env-derived seed (which post-rotation is the *retired* pubkey).
|
|
17284
17300
|
*/
|
|
@@ -17288,12 +17304,8 @@ var DVMServer = class {
|
|
|
17288
17304
|
return state;
|
|
17289
17305
|
}
|
|
17290
17306
|
/**
|
|
17291
|
-
*
|
|
17292
|
-
*
|
|
17293
|
-
* upfront flow uses at lines 709-718 so accept-time rotation tolerance is
|
|
17294
|
-
* consistent across upfront and mid-job. Falls back to `undefined` when
|
|
17295
|
-
* accumulator mode isn't wired (the receive path then rejects with
|
|
17296
|
-
* `cashu_not_p2pk_locked`).
|
|
17307
|
+
* Mid-job and upfront receive paths use current plus grace-period retired keys.
|
|
17308
|
+
* Without accumulator mode this is undefined and the receive path refuses it.
|
|
17297
17309
|
*/
|
|
17298
17310
|
async resolveLockPubkeysForReceive() {
|
|
17299
17311
|
if (this.opts.disposableCashu && this.opts.lockPubkey) return [toLockPubkey(this.opts.lockPubkey)];
|
|
@@ -17308,12 +17320,8 @@ var DVMServer = class {
|
|
|
17308
17320
|
}
|
|
17309
17321
|
}
|
|
17310
17322
|
/**
|
|
17311
|
-
*
|
|
17312
|
-
*
|
|
17313
|
-
* cached state, and only as a final resort to the env-derived seed in a
|
|
17314
|
-
* single-entry array. The cache makes the degradation safe across a
|
|
17315
|
-
* rotation — without it the env seed advertises and accepts the retired
|
|
17316
|
-
* pubkey while the DB is unreachable.
|
|
17323
|
+
* Read Postgres first, then fall back to last-observed state, then the env seed.
|
|
17324
|
+
* Caching avoids reverting an observed rotation to the old seed during an outage.
|
|
17317
17325
|
*/
|
|
17318
17326
|
async lockPubkeyStateForRead(db, dvmId) {
|
|
17319
17327
|
try {
|
|
@@ -17325,22 +17333,9 @@ var DVMServer = class {
|
|
|
17325
17333
|
}
|
|
17326
17334
|
}
|
|
17327
17335
|
/**
|
|
17328
|
-
*
|
|
17329
|
-
*
|
|
17330
|
-
*
|
|
17331
|
-
* Routine settlement used to ride the per-DVM watcher machine's four-minute
|
|
17332
|
-
* poll, which is exactly what kept an idle DVM awake and billing. It cannot
|
|
17333
|
-
* move to a timer for the same reason — a timer on a machine that ought to
|
|
17334
|
-
* be suspended either does not fire or defeats the suspension. So it rides
|
|
17335
|
-
* requests instead, the way Lightning funding is credited (internal-review): a DVM
|
|
17336
|
-
* with no traffic has nothing to settle on a 24-hour cadence anyway, and the
|
|
17337
|
-
* forced-close guarantee does not depend on this path at all — that is the
|
|
17338
|
-
* chain-event wake, which reaches a suspended DVM through Fly Proxy.
|
|
17339
|
-
*
|
|
17340
|
-
* Fire-and-forget and self-throttled: the request never waits on it, only
|
|
17341
|
-
* one runs at a time, and a failure is logged rather than surfaced. The
|
|
17342
|
-
* sweep re-checks each channel's own schedule, so the throttle here only
|
|
17343
|
-
* bounds how often we pay for the store scan.
|
|
17336
|
+
* Opportunistic traffic avoids a timer that keeps an idle machine awake. Forced
|
|
17337
|
+
* close servicing is independent. Fire-and-forget, one sweep at a time; failures
|
|
17338
|
+
* are logged. The throttle bounds scans, not each channel's settlement schedule.
|
|
17344
17339
|
*/
|
|
17345
17340
|
maybeSweepTempoSessions() {
|
|
17346
17341
|
const sweep = this.opts.mpp?.sweepTempoSessions;
|
|
@@ -17372,7 +17367,7 @@ var DVMServer = class {
|
|
|
17372
17367
|
});
|
|
17373
17368
|
}
|
|
17374
17369
|
/**
|
|
17375
|
-
* Whether one advertised MPP method may still be offered
|
|
17370
|
+
* Whether one advertised MPP method may still be offered.
|
|
17376
17371
|
*
|
|
17377
17372
|
* Only `tempo/session` is gated, and only when this DVM is platform-hosted:
|
|
17378
17373
|
* a session channel is safe to accept exactly while something is watching
|
|
@@ -17388,7 +17383,6 @@ var DVMServer = class {
|
|
|
17388
17383
|
tempoSessionOffered(method) {
|
|
17389
17384
|
return this.tempoSessionWithheldReason(method) === void 0;
|
|
17390
17385
|
}
|
|
17391
|
-
/** Stable caller-facing reason for withholding one newly opened session. */
|
|
17392
17386
|
tempoSessionWithheldReason(method) {
|
|
17393
17387
|
if (method.method !== "tempo" || method.intent !== "session") return void 0;
|
|
17394
17388
|
if (this.opts.tempoSettlementReadiness && !this.opts.tempoSettlementReadiness.available()) {
|
|
@@ -17399,7 +17393,6 @@ var DVMServer = class {
|
|
|
17399
17393
|
}
|
|
17400
17394
|
return void 0;
|
|
17401
17395
|
}
|
|
17402
|
-
/** Await only the cold-start reads each fail-closed session gate needs. */
|
|
17403
17396
|
async ensureTempoSessionHealth() {
|
|
17404
17397
|
await Promise.all([
|
|
17405
17398
|
this.opts.tempoObserverHealth?.ensureFresh?.(),
|
|
@@ -17407,28 +17400,13 @@ var DVMServer = class {
|
|
|
17407
17400
|
]);
|
|
17408
17401
|
}
|
|
17409
17402
|
/**
|
|
17410
|
-
*
|
|
17411
|
-
*
|
|
17412
|
-
*
|
|
17413
|
-
* (internal-review).
|
|
17414
|
-
*
|
|
17415
|
-
* Deliberately the same question the platform's post-deploy `/v1/info` probe
|
|
17416
|
-
* answers (`infoAdvertisesTempoSession`), from the same two facts: the credit
|
|
17417
|
-
* menu exists at all, and the configured MPP methods include `tempo/session`.
|
|
17418
|
-
* Credit-scoped because sessions are confined to `POST /v1/credit` — a
|
|
17419
|
-
* top-level session method with no credit menu is not a channel anyone can
|
|
17420
|
-
* open, so it is not a candidate the payee fallback should reach for.
|
|
17421
|
-
*
|
|
17422
|
-
* It reads `advertisedMethods` rather than {@link creditMppSplit}'s `offered`,
|
|
17423
|
-
* and never {@link tempoSessionOffered}: what the column records is
|
|
17424
|
-
* capability, not availability, so an observer blip must leave it alone. That
|
|
17425
|
-
* distinction is the whole of internal-review, and answering it here from the gate
|
|
17426
|
-
* would reintroduce it on a one-minute cadence.
|
|
17403
|
+
* Report configured session capability independently of current availability.
|
|
17404
|
+
* A temporary observer outage must not remove the platform's watch signal.
|
|
17405
|
+
* Credit menu, auth and session method configuration still gate that capability.
|
|
17427
17406
|
*/
|
|
17428
17407
|
tempoSessionCapability() {
|
|
17429
17408
|
if (!this.opts.mpp) return false;
|
|
17430
17409
|
if (!creditMenuConfigured({
|
|
17431
|
-
// The SDK server only ever runs the container runtime — see `creditMenu`.
|
|
17432
17410
|
runtime: "container",
|
|
17433
17411
|
config: this.descriptor.credit,
|
|
17434
17412
|
hasAuth: !!this.descriptor.auth,
|
|
@@ -17442,25 +17420,11 @@ var DVMServer = class {
|
|
|
17442
17420
|
);
|
|
17443
17421
|
}
|
|
17444
17422
|
/**
|
|
17445
|
-
*
|
|
17446
|
-
*
|
|
17447
|
-
*
|
|
17448
|
-
*
|
|
17449
|
-
*
|
|
17450
|
-
* its own Tempo RPC and verifies chain, escrow, topic and local channel
|
|
17451
|
-
* ownership before touching money (see `handleTempoCloseEvent`). An
|
|
17452
|
-
* unverifiable locator can therefore do nothing, which is what lets the
|
|
17453
|
-
* platform wake a DVM without holding a signing key or a recoverable copy of
|
|
17454
|
-
* the DVM's bearer token.
|
|
17455
|
-
*
|
|
17456
|
-
* What an unauthenticated caller *can* do is spend the DVM's resources: each
|
|
17457
|
-
* POST is an unthrottled outbound `eth_getTransactionReceipt` against the
|
|
17458
|
-
* builder's own Tempo provider, billed to them, before anything about the
|
|
17459
|
-
* locator has been checked — the verification needs the receipt to happen at
|
|
17460
|
-
* all. So the cost of an open endpoint is metered RPC calls, not money: no
|
|
17461
|
-
* locator can move value except one naming a real close of a channel this
|
|
17462
|
-
* DVM owns, and settling that is what the routine cadence would do anyway.
|
|
17463
|
-
* Watch the provider's quota rather than the ledger.
|
|
17423
|
+
* Unauthenticated wake carrying only an event locator. After parsing, verify
|
|
17424
|
+
* the receipt from the DVM's own RPC, including chain, escrow, topic and local
|
|
17425
|
+
* channel ownership before settlement. The observer needs no spending secret.
|
|
17426
|
+
* Syntactically valid requests can still consume unthrottled provider RPC quota;
|
|
17427
|
+
* malformed locators return 400 before that read.
|
|
17464
17428
|
*/
|
|
17465
17429
|
registerTempoCloseEventRoute() {
|
|
17466
17430
|
const mpp = this.opts.mpp;
|
|
@@ -17521,22 +17485,14 @@ var DVMServer = class {
|
|
|
17521
17485
|
});
|
|
17522
17486
|
}
|
|
17523
17487
|
/**
|
|
17524
|
-
*
|
|
17525
|
-
*
|
|
17526
|
-
* field is independently sourced (opts wins per-field, env fills gaps);
|
|
17527
|
-
* all three of pubkey / attestation / signature must resolve to emit the
|
|
17528
|
-
* block — partial state is treated as absent rather than half-emitted.
|
|
17529
|
-
*
|
|
17530
|
-
* Env-var form: `DVMKIT_BUILDER_ATTESTATION` is a JSON string (Fly secrets
|
|
17531
|
-
* can't carry structured data), so it's parsed here. A malformed JSON
|
|
17532
|
-
* string drops the block rather than throwing, so a corrupted env var
|
|
17533
|
-
* can't take the DVM down.
|
|
17488
|
+
* Options override environment per field. Emit only when pubkey, attestation
|
|
17489
|
+
* and signature all exist; malformed attestation JSON omits the block.
|
|
17534
17490
|
*/
|
|
17535
17491
|
builderAttestationBlock() {
|
|
17536
17492
|
const builder = this.opts.builder;
|
|
17537
17493
|
const env = this.opts.env;
|
|
17538
|
-
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY
|
|
17539
|
-
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE
|
|
17494
|
+
const pubkey = builder?.pubkey ?? env.DVMKIT_BUILDER_PUBKEY;
|
|
17495
|
+
const signature = builder?.signature ?? env.DVMKIT_BUILDER_SIGNATURE;
|
|
17540
17496
|
const attestation = builder?.attestation ? { ...builder.attestation } : this.parseAttestationFromEnv(env);
|
|
17541
17497
|
if (!pubkey || !signature || !attestation) return null;
|
|
17542
17498
|
return { pubkey, attestation, signature };
|
|
@@ -17568,7 +17524,7 @@ var DVMServer = class {
|
|
|
17568
17524
|
throw new AuthAudienceError();
|
|
17569
17525
|
}
|
|
17570
17526
|
/**
|
|
17571
|
-
* The slug every receipt is stamped with
|
|
17527
|
+
* The slug every receipt is stamped with. Read off the signed
|
|
17572
17528
|
* attestation payload, which is the only place the *attested* slug lives —
|
|
17573
17529
|
* a receipt claiming a different name than the attestation binds would be
|
|
17574
17530
|
* unverifiable. Falls back to the descriptor name for dev/self-host boots
|
|
@@ -17579,14 +17535,13 @@ var DVMServer = class {
|
|
|
17579
17535
|
const slug = attestation?.slug;
|
|
17580
17536
|
return typeof slug === "string" && slug.length > 0 ? slug : this.descriptor.name;
|
|
17581
17537
|
}
|
|
17582
|
-
/** Immutable identity written into every signed receipt. */
|
|
17583
17538
|
receiptDvmId() {
|
|
17584
17539
|
const attestation = this.builderAttestationBlock()?.attestation;
|
|
17585
17540
|
const dvmId = attestation?.dvm_id;
|
|
17586
17541
|
return typeof dvmId === "string" && dvmId.length > 0 ? dvmId : this.opts.dvmId ?? this.descriptor.name;
|
|
17587
17542
|
}
|
|
17588
17543
|
parseAttestationFromEnv(env) {
|
|
17589
|
-
const raw = env.DVMKIT_BUILDER_ATTESTATION
|
|
17544
|
+
const raw = env.DVMKIT_BUILDER_ATTESTATION;
|
|
17590
17545
|
if (!raw) return null;
|
|
17591
17546
|
try {
|
|
17592
17547
|
const parsed = JSON.parse(raw);
|
|
@@ -17597,7 +17552,7 @@ var DVMServer = class {
|
|
|
17597
17552
|
}
|
|
17598
17553
|
}
|
|
17599
17554
|
/**
|
|
17600
|
-
* Owner display block for `/v1/info#owner
|
|
17555
|
+
* Owner display block for `/v1/info#owner`. Resolved from
|
|
17601
17556
|
* `opts.owner` or env vars injected by the platform deploy pipeline.
|
|
17602
17557
|
* Returns null when no owner identity is configured — pre-unification
|
|
17603
17558
|
* DVMs that haven't been redeployed omit the block.
|
|
@@ -17605,11 +17560,11 @@ var DVMServer = class {
|
|
|
17605
17560
|
ownerDisplayBlock() {
|
|
17606
17561
|
const owner = this.opts.owner;
|
|
17607
17562
|
const env = this.opts.env;
|
|
17608
|
-
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE
|
|
17563
|
+
const handle = owner?.handle ?? env.DVMKIT_OWNER_HANDLE;
|
|
17609
17564
|
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 ??
|
|
17565
|
+
const type = owner?.type ?? env.DVMKIT_OWNER_TYPE ?? "builder";
|
|
17566
|
+
const displayName = owner?.displayName ?? env.DVMKIT_OWNER_DISPLAY_NAME ?? null;
|
|
17567
|
+
const avatarUrl = owner?.avatarUrl ?? env.DVMKIT_OWNER_AVATAR_URL ?? null;
|
|
17613
17568
|
return { handle, display_name: displayName, avatar_url: avatarUrl, type };
|
|
17614
17569
|
}
|
|
17615
17570
|
defaultPaymentMethods() {
|
|
@@ -17628,14 +17583,13 @@ var DVMServer = class {
|
|
|
17628
17583
|
if (!this.opts.x402.batchSettlement?.selfRelay) return support;
|
|
17629
17584
|
return { ...support, batchSettlement: true };
|
|
17630
17585
|
}
|
|
17631
|
-
/** Whether this DVM can state its price exactly in x402's USDC unit. */
|
|
17632
17586
|
x402CurrencySupported() {
|
|
17633
17587
|
return this.pricingCurrency === "usd";
|
|
17634
17588
|
}
|
|
17635
17589
|
/**
|
|
17636
17590
|
* Settleable rails for this DVM, honouring an explicit `opts.paymentMethods`
|
|
17637
17591
|
* override and otherwise deriving from wired rails. Single source of truth so
|
|
17638
|
-
* the `/v1/info` and `/v1/quote` rail enumerations can't drift
|
|
17592
|
+
* the `/v1/info` and `/v1/quote` rail enumerations can't drift.
|
|
17639
17593
|
*/
|
|
17640
17594
|
resolvePaymentMethods() {
|
|
17641
17595
|
const configured = this.opts.paymentMethods ?? this.defaultPaymentMethods();
|
|
@@ -17648,13 +17602,13 @@ var DVMServer = class {
|
|
|
17648
17602
|
}
|
|
17649
17603
|
/**
|
|
17650
17604
|
* Rails a **credit top-up** may arrive on — the funding menu's list
|
|
17651
|
-
*
|
|
17605
|
+
* A superset of {@link resolvePaymentMethods}, and deliberately
|
|
17652
17606
|
* a separate read: `lightning` funds a credit but is not an attached-proof
|
|
17653
17607
|
* wire method, so it must never reach `/v1/info` or `/v1/quote`'s
|
|
17654
|
-
* `payment.methods
|
|
17608
|
+
* `payment.methods`.
|
|
17655
17609
|
*
|
|
17656
|
-
* The rail drops out when the receive wallet is unreachable
|
|
17657
|
-
*
|
|
17610
|
+
* The rail drops out of the funding menu when the receive wallet is unreachable;
|
|
17611
|
+
* advertising it then would hand the caller an option that
|
|
17658
17612
|
* fails on use, while the other rails would have closed the sale.
|
|
17659
17613
|
*/
|
|
17660
17614
|
resolveFundingMethods() {
|
|
@@ -17673,7 +17627,7 @@ var DVMServer = class {
|
|
|
17673
17627
|
}
|
|
17674
17628
|
/**
|
|
17675
17629
|
* The credit menu's MPP instruments, split into what is on offer right now
|
|
17676
|
-
* and what this DVM is configured for but withholding
|
|
17630
|
+
* and what this DVM is configured for but withholding.
|
|
17677
17631
|
*
|
|
17678
17632
|
* The split has to survive into the response rather than being filtered
|
|
17679
17633
|
* away, because "not offered right now" and "not configured" are different
|
|
@@ -17701,7 +17655,7 @@ var DVMServer = class {
|
|
|
17701
17655
|
}
|
|
17702
17656
|
/**
|
|
17703
17657
|
* Which x402 flavours a credit top-up may arrive on — the funding menu's
|
|
17704
|
-
* `x402` sub-block
|
|
17658
|
+
* `x402` sub-block. Hosted health buys `exact`; a genuinely wired
|
|
17705
17659
|
* batch server buys `batch-settlement` independently, including while the
|
|
17706
17660
|
* hosted facilitator is unavailable or stale.
|
|
17707
17661
|
*
|
|
@@ -17718,13 +17672,12 @@ var DVMServer = class {
|
|
|
17718
17672
|
...this.x402BatchAvailable() ? [{ scheme: X402_BATCH_SETTLEMENT_SCHEME, network: x402.network }] : []
|
|
17719
17673
|
];
|
|
17720
17674
|
}
|
|
17721
|
-
/** Whether a channel server is both supported and constructible for credit funding. */
|
|
17722
17675
|
x402BatchAvailable() {
|
|
17723
17676
|
return !!this.x402Support()?.batchSettlement && !!(this.opts.x402BatchSettlement ?? this.opts.x402BatchSettlementFactory);
|
|
17724
17677
|
}
|
|
17725
17678
|
/**
|
|
17726
17679
|
* Apply any Lightning funding invoice this caller has paid since we last
|
|
17727
|
-
* looked — the pull half of pull-based crediting
|
|
17680
|
+
* looked — the pull half of pull-based crediting.
|
|
17728
17681
|
*
|
|
17729
17682
|
* Called from every authenticated surface where the balance is about to
|
|
17730
17683
|
* matter: the `/v1/credit` ops and the `/v1/job` submit. Deliberately **not**
|
|
@@ -17737,7 +17690,7 @@ var DVMServer = class {
|
|
|
17737
17690
|
* outstanding, and never blocks the request when it fails.
|
|
17738
17691
|
*
|
|
17739
17692
|
* The SDK-owned reporter queues each Lightning deposit inside
|
|
17740
|
-
* `settleInvoice`'s transaction
|
|
17693
|
+
* `settleInvoice`'s transaction. The loop below only preserves
|
|
17741
17694
|
* the explicit `onCreditFunded` override, whose caller owns its durability.
|
|
17742
17695
|
*/
|
|
17743
17696
|
async settleLightningFunding(callerPubkey, creditId) {
|
|
@@ -17768,7 +17721,6 @@ var DVMServer = class {
|
|
|
17768
17721
|
});
|
|
17769
17722
|
}
|
|
17770
17723
|
}
|
|
17771
|
-
/** Credit lifetime this DVM mints, in ms — the descriptor's `credit.ttl` or the platform default. */
|
|
17772
17724
|
creditTtlMs() {
|
|
17773
17725
|
return this.descriptor.credit ? this.descriptor.credit.ttlSeconds * 1e3 : IMPLICIT_CREDIT_TTL_MS;
|
|
17774
17726
|
}
|
|
@@ -17789,7 +17741,7 @@ var DVMServer = class {
|
|
|
17789
17741
|
* The per-DVM + per-request rail wiring every payment verification needs —
|
|
17790
17742
|
* everything except what the *operation* contributes (its price, its job or
|
|
17791
17743
|
* fund id, the caller's credit fields). Shared by `/v1/job`'s upfront gate
|
|
17792
|
-
* and `/v1/credit`'s top-up
|
|
17744
|
+
* and `/v1/credit`'s top-up so a rail wired into one is wired
|
|
17793
17745
|
* into both by construction; a divergence here would be a rail that funds
|
|
17794
17746
|
* jobs but silently can't fund credits.
|
|
17795
17747
|
*/
|
|
@@ -17822,7 +17774,7 @@ var DVMServer = class {
|
|
|
17822
17774
|
mppCredential,
|
|
17823
17775
|
mpp: this.opts.mpp,
|
|
17824
17776
|
resourcePath: c.req.path,
|
|
17825
|
-
//
|
|
17777
|
+
// the gate covers issuance and new-channel acceptance,
|
|
17826
17778
|
// not just the two advertisement surfaces. `ensureFresh` above makes this
|
|
17827
17779
|
// an observation rather than a cold-start default on the one surface that
|
|
17828
17780
|
// can act on it.
|
|
@@ -17840,8 +17792,6 @@ var DVMServer = class {
|
|
|
17840
17792
|
requestId: c.req.header("X-Cashu-Request-Id"),
|
|
17841
17793
|
mintHealthTracker: this.mintHealthTracker,
|
|
17842
17794
|
fxFetcher: this.fxFetcher,
|
|
17843
|
-
// internal-review: the ledger leg — every rail payment becomes fund_credit +
|
|
17844
|
-
// draw, atomic with the rail commit.
|
|
17845
17795
|
creditLedger: this.creditLedger,
|
|
17846
17796
|
processedPayments: this.processedPayments,
|
|
17847
17797
|
enqueueCreditDeposit: this.opts.enqueueCreditDeposit,
|
|
@@ -17851,7 +17801,7 @@ var DVMServer = class {
|
|
|
17851
17801
|
}
|
|
17852
17802
|
/**
|
|
17853
17803
|
* Assemble this DVM's funding menu for a response denominated in
|
|
17854
|
-
* `currency
|
|
17804
|
+
* `currency`. All the refusal logic — including the credits-spec
|
|
17855
17805
|
* §3 isolate gate — lives in {@link buildCreditMenu}; this just supplies the
|
|
17856
17806
|
* server's state. `callerPubkey` must be a *verified* envelope pubkey: the
|
|
17857
17807
|
* balance echo is per-caller money and there is no safe fallback identity.
|
|
@@ -17860,8 +17810,6 @@ var DVMServer = class {
|
|
|
17860
17810
|
await this.ensureTempoSessionHealth();
|
|
17861
17811
|
const mppSplit = this.creditMppSplit();
|
|
17862
17812
|
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
17813
|
runtime: "container",
|
|
17866
17814
|
config: this.descriptor.credit,
|
|
17867
17815
|
currency,
|
|
@@ -17870,7 +17818,7 @@ var DVMServer = class {
|
|
|
17870
17818
|
tempoWithheld: mppSplit.withheld,
|
|
17871
17819
|
x402Schemes: this.creditX402Schemes(),
|
|
17872
17820
|
lightningFundingMinSats: this.opts.lightningReceive?.fundingMinSats,
|
|
17873
|
-
// Configured, not advertised
|
|
17821
|
+
// Configured, not advertised: `advertisedMints()` is health
|
|
17874
17822
|
// filtered, and a rail that comes and goes with a mint's uptime is worse
|
|
17875
17823
|
// product than one that is honestly absent.
|
|
17876
17824
|
hasAcceptedMint: (this.opts.mints?.length ?? 0) > 0,
|
|
@@ -17907,7 +17855,7 @@ var DVMServer = class {
|
|
|
17907
17855
|
}
|
|
17908
17856
|
/**
|
|
17909
17857
|
* Currency this DVM's credit ledger is denominated in — the builder's
|
|
17910
|
-
* declared pricing currency, `"usd"` unless they said otherwise
|
|
17858
|
+
* declared pricing currency, `"usd"` unless they said otherwise.
|
|
17911
17859
|
*
|
|
17912
17860
|
* Every denomination surface reads this one answer: the funding menu on
|
|
17913
17861
|
* `/v1/quote` and the 402, `POST /v1/credit`, and the quote balance echo.
|
|
@@ -17916,20 +17864,13 @@ var DVMServer = class {
|
|
|
17916
17864
|
* static (USD-literal) price on a non-USD DVM, and a quote denominated
|
|
17917
17865
|
* elsewhere is refused as `quote_currency_mismatch` — so what a job draws in
|
|
17918
17866
|
* 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
17867
|
*/
|
|
17924
17868
|
creditCurrency() {
|
|
17925
17869
|
return this.pricingCurrency;
|
|
17926
17870
|
}
|
|
17927
17871
|
/**
|
|
17928
|
-
*
|
|
17929
|
-
*
|
|
17930
|
-
* caller can see which rails return funds on failure before committing. The
|
|
17931
|
-
* `methods` shape stays identical to `/v1/info` and the platform isolate route
|
|
17932
|
-
* (post-internal-review nested `payment.methods`); refundability rides alongside.
|
|
17872
|
+
* Advertise the same method list as info. Refundability describes the rail
|
|
17873
|
+
* itself; failed jobs release their pending credit draw separately.
|
|
17933
17874
|
*/
|
|
17934
17875
|
quotePaymentBlock() {
|
|
17935
17876
|
const methods = this.resolvePaymentMethods();
|
|
@@ -17938,8 +17879,6 @@ var DVMServer = class {
|
|
|
17938
17879
|
refundable: Object.fromEntries(methods.map((m) => [m, RAIL_REFUNDABLE[m]]))
|
|
17939
17880
|
};
|
|
17940
17881
|
}
|
|
17941
|
-
// ── Route handlers ──────────────────────────────────────────────────
|
|
17942
|
-
/** Dev-mode proxy: forwards requests to a remote DVM specified by cookie. */
|
|
17943
17882
|
async handleProxy(c) {
|
|
17944
17883
|
const raw = getCookie(c, "dvmkit_target");
|
|
17945
17884
|
const target = raw ? decodeURIComponent(raw) : null;
|
|
@@ -17975,8 +17914,8 @@ var DVMServer = class {
|
|
|
17975
17914
|
auth: this.descriptor.auth ? [this.descriptor.auth.id] : ["none"],
|
|
17976
17915
|
...this.authAudience ? { auth_audience: this.authAudience } : {},
|
|
17977
17916
|
protocol_version: 1,
|
|
17978
|
-
sdk_sha:
|
|
17979
|
-
deploy_id:
|
|
17917
|
+
sdk_sha: this.opts.env.GIT_SHA ?? "unknown",
|
|
17918
|
+
deploy_id: this.opts.env.DVMKIT_DEPLOY_ID ?? "unknown"
|
|
17980
17919
|
};
|
|
17981
17920
|
const ownerBlock = this.ownerDisplayBlock();
|
|
17982
17921
|
if (ownerBlock) info.owner = ownerBlock;
|
|
@@ -17997,8 +17936,7 @@ var DVMServer = class {
|
|
|
17997
17936
|
network: this.opts.x402.network,
|
|
17998
17937
|
asset: this.opts.x402.asset ?? "USDC",
|
|
17999
17938
|
...this.opts.x402.facilitator && { facilitator: this.opts.x402.facilitator },
|
|
18000
|
-
//
|
|
18001
|
-
// carry their PaymentRequirements before submitting (internal-review).
|
|
17939
|
+
// Advertises whether 402 challenges include x402 PaymentRequirements.
|
|
18002
17940
|
dual_serve: this.opts.x402.dualServe ?? true
|
|
18003
17941
|
};
|
|
18004
17942
|
}
|
|
@@ -18009,11 +17947,11 @@ var DVMServer = class {
|
|
|
18009
17947
|
return reason ? [{ ...method, reason }] : [];
|
|
18010
17948
|
});
|
|
18011
17949
|
info.tempo = {
|
|
18012
|
-
//
|
|
17950
|
+
// withheld sessions are withheld here too. `/v1/info` is
|
|
18013
17951
|
// where a caller decides what to attempt, so advertising a session the
|
|
18014
17952
|
// credit menu will refuse just sends them down a dead end.
|
|
18015
17953
|
methods: configured.filter((method) => this.tempoSessionOffered(method)),
|
|
18016
|
-
// …but say so rather than letting it vanish
|
|
17954
|
+
// …but say so rather than letting it vanish: an agent that
|
|
18017
17955
|
// sees only the shortened list cannot tell a DVM that never did
|
|
18018
17956
|
// sessions from one whose observer is down for a minute.
|
|
18019
17957
|
...withheld.length && { withheld },
|
|
@@ -18045,12 +17983,8 @@ var DVMServer = class {
|
|
|
18045
17983
|
return c.json(info);
|
|
18046
17984
|
}
|
|
18047
17985
|
/**
|
|
18048
|
-
*
|
|
18049
|
-
*
|
|
18050
|
-
* `pricing` block — `pricing.quote.schema` for dynamic-priced capabilities,
|
|
18051
|
-
* `pricing.max` for static-priced ones. Capabilities without an `input`
|
|
18052
|
-
* schema or any pricing surface omit those fields entirely; free
|
|
18053
|
-
* capabilities (no `price`) omit `pricing` entirely.
|
|
17986
|
+
* Input-mode JSON Schema keeps defaulted fields optional on the advertised
|
|
17987
|
+
* wire contract. Capability names are the descriptor's keys.
|
|
18054
17988
|
*/
|
|
18055
17989
|
renderCapabilities() {
|
|
18056
17990
|
const out = {};
|
|
@@ -18251,11 +18185,8 @@ var DVMServer = class {
|
|
|
18251
18185
|
return c.json(res);
|
|
18252
18186
|
}
|
|
18253
18187
|
/**
|
|
18254
|
-
*
|
|
18255
|
-
*
|
|
18256
|
-
* path — cannot drift: a field added for one is present in the other by
|
|
18257
|
-
* construction, and both paths price against the same view of the caller's
|
|
18258
|
-
* credit (internal-review, spec §7).
|
|
18188
|
+
* Callers supply quote-schema-parsed data when a quote schema exists, alongside
|
|
18189
|
+
* raw input text and params. Quote and job pricing share these runtime helpers.
|
|
18259
18190
|
*/
|
|
18260
18191
|
async buildQuoteContext(args) {
|
|
18261
18192
|
const ctx = {
|
|
@@ -18277,16 +18208,8 @@ var DVMServer = class {
|
|
|
18277
18208
|
return ctx;
|
|
18278
18209
|
}
|
|
18279
18210
|
/**
|
|
18280
|
-
*
|
|
18281
|
-
*
|
|
18282
|
-
* structured 400 envelope agents can relay verbatim to a human caller.
|
|
18283
|
-
*
|
|
18284
|
-
* - **Missing** — `body.capability` is absent or empty. Surface as
|
|
18285
|
-
* `capability_required` with a hint pointing at `/v1/info` so the agent
|
|
18286
|
-
* can re-discover the capability set.
|
|
18287
|
-
* - **Unknown** — `body.capability` is set but doesn't match any
|
|
18288
|
-
* capability on this descriptor. Surface as `unknown_capability` listing
|
|
18289
|
-
* the known names so the agent can correct without a round-trip.
|
|
18211
|
+
* Missing or unknown capabilities return structured errors with the current
|
|
18212
|
+
* info-schema URL.
|
|
18290
18213
|
*/
|
|
18291
18214
|
requireCapabilityFromBody(c, capability) {
|
|
18292
18215
|
const known = this.jobManager.capabilityNames();
|
|
@@ -18345,25 +18268,9 @@ var DVMServer = class {
|
|
|
18345
18268
|
}
|
|
18346
18269
|
}
|
|
18347
18270
|
/**
|
|
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.
|
|
18271
|
+
* Shared schema-violation response for quote and submit, including auth-less
|
|
18272
|
+
* DVMs. A 400 classifies caller input without requiring an auth gate; `display`
|
|
18273
|
+
* and `message` carry the same relayable explanation.
|
|
18367
18274
|
*/
|
|
18368
18275
|
invalidInputResponse(c, err) {
|
|
18369
18276
|
const schemaUrl = infoSchemaUrl(c.req.url, this.publicOrigin);
|
|
@@ -18406,22 +18313,9 @@ var DVMServer = class {
|
|
|
18406
18313
|
);
|
|
18407
18314
|
}
|
|
18408
18315
|
/**
|
|
18409
|
-
*
|
|
18410
|
-
*
|
|
18411
|
-
*
|
|
18412
|
-
* A capability schema may declare the signing-envelope fields required
|
|
18413
|
-
* (cast's `addEpisodeSchema`) or optional (scribe's `transcribeSchema`). On
|
|
18414
|
-
* the required shape an *unsigned* request fails the input parse naming
|
|
18415
|
-
* `pubkey` / `signature` — a signing failure, and answering it with "your
|
|
18416
|
-
* input is invalid: pubkey required" is the internal-review mistake in reverse. So a
|
|
18417
|
-
* parse whose issues are **only** envelope-side falls through to the auth
|
|
18418
|
-
* gate, which answers 401 `signature_invalid` with the remedy that works.
|
|
18419
|
-
*
|
|
18420
|
-
* Deferring is safe because the gate below re-parses the same schema over the
|
|
18421
|
-
* same bytes, so a caller that reaches it with an envelope-side failure is
|
|
18422
|
-
* rejected there for certain; both routes still carry a backstop for the
|
|
18423
|
-
* impossible case. It applies only to a DVM that declares `auth` — with no
|
|
18424
|
-
* gate below, the 400 is the only answer available.
|
|
18316
|
+
* On authenticated DVMs, envelope-only parse failures defer to auth
|
|
18317
|
+
* (401 signature_invalid). Builder input issues remain 400 invalid_input.
|
|
18318
|
+
* Auth reparses the same schema/raw input; a backstop refuses disagreement.
|
|
18425
18319
|
*/
|
|
18426
18320
|
deferToAuthGate(err) {
|
|
18427
18321
|
if (!this.descriptor.auth) return false;
|
|
@@ -18430,13 +18324,13 @@ var DVMServer = class {
|
|
|
18430
18324
|
}
|
|
18431
18325
|
/**
|
|
18432
18326
|
* Mints to advertise to clients on `/v1/info` and `/v1/quote`. Filters the
|
|
18433
|
-
* configured list through the runtime health tracker
|
|
18327
|
+
* configured list through the runtime health tracker. When every
|
|
18434
18328
|
* mint is currently sick it falls back to the still-configured list rather
|
|
18435
18329
|
* than publish an empty rail — a *transient* outage shouldn't read as "no
|
|
18436
18330
|
* Cashu" and push well-behaved agents to bypass the rail. But NUT-incompatible
|
|
18437
|
-
* mints are excluded even from that fallback
|
|
18331
|
+
* mints are excluded even from that fallback: NUT-incompatibility
|
|
18438
18332
|
* is a permanent, deterministic config error the DVM can never settle
|
|
18439
|
-
* against, so
|
|
18333
|
+
* against, so such a mint is never advertised. If *every*
|
|
18440
18334
|
* configured mint is NUT-incompatible the rail correctly goes empty.
|
|
18441
18335
|
*/
|
|
18442
18336
|
advertisedMints() {
|
|
@@ -18865,10 +18759,6 @@ var DVMServer = class {
|
|
|
18865
18759
|
priceMsats,
|
|
18866
18760
|
capabilityName,
|
|
18867
18761
|
requesterTokenHash,
|
|
18868
|
-
// internal-review: everything the idempotent-replay path needs to recognise a
|
|
18869
|
-
// retry of THIS call and re-issue the token its lost response carried.
|
|
18870
|
-
// internal-review: `jobId` is the pre-allocated id the payment's ledger draw
|
|
18871
|
-
// already committed under.
|
|
18872
18762
|
{
|
|
18873
18763
|
requesterToken: jobToken,
|
|
18874
18764
|
requestFingerprint: fingerprint,
|
|
@@ -18906,7 +18796,7 @@ var DVMServer = class {
|
|
|
18906
18796
|
}
|
|
18907
18797
|
}
|
|
18908
18798
|
/**
|
|
18909
|
-
* Book a committed funding as a deposit (
|
|
18799
|
+
* Book a committed funding as a deposit (the deposit-accounting rule). Fire-and-forget
|
|
18910
18800
|
* onto the reporter's durable queue: a funding is a liability the platform
|
|
18911
18801
|
* must show, but a reporting hiccup must never fail the paid request that
|
|
18912
18802
|
* just succeeded.
|
|
@@ -18921,7 +18811,7 @@ var DVMServer = class {
|
|
|
18921
18811
|
);
|
|
18922
18812
|
}
|
|
18923
18813
|
/**
|
|
18924
|
-
* Book a kept short payment as tagged revenue
|
|
18814
|
+
* Book a kept short payment as tagged revenue.
|
|
18925
18815
|
* The proofs are committed and the draw was settled on the spot, so the money
|
|
18926
18816
|
* is earned — but no job exists to carry it, hence the synthetic job id. It
|
|
18927
18817
|
* is deliberately **not** a deposit: nothing is owed back, so a liability
|
|
@@ -18951,20 +18841,9 @@ var DVMServer = class {
|
|
|
18951
18841
|
});
|
|
18952
18842
|
}
|
|
18953
18843
|
/**
|
|
18954
|
-
*
|
|
18955
|
-
*
|
|
18956
|
-
*
|
|
18957
|
-
* but until now it was also useless: the caller got a 409 and the `job_id` /
|
|
18958
|
-
* `job_token` it paid for stayed unreachable forever.
|
|
18959
|
-
*
|
|
18960
|
-
* Returns the original job's submit response iff the retry proves it is the
|
|
18961
|
-
* original caller. Proof is reproducing the request, **not** possessing the
|
|
18962
|
-
* spent Cashu token: the token is a bearer secret, so gating on it alone
|
|
18963
|
-
* would hand the job (and its result) to anyone who lifted it. The legitimate
|
|
18964
|
-
* retry always has the body — the internal-review resume sweep re-POSTs it — while a
|
|
18965
|
-
* token thief does not. On a descriptor-auth DVM the caller pubkey must match
|
|
18966
|
-
* too. Anything short of that falls through to the 409 the caller has always
|
|
18967
|
-
* gotten.
|
|
18844
|
+
* Recover a lost paid response only for matching request fingerprint,
|
|
18845
|
+
* authenticated pubkey and requester identity. A spent bearer token alone
|
|
18846
|
+
* is not proof of request ownership.
|
|
18968
18847
|
*/
|
|
18969
18848
|
async recoverReplayedJob(c, opts) {
|
|
18970
18849
|
if (!opts.requestId) return void 0;
|
|
@@ -18988,16 +18867,9 @@ var DVMServer = class {
|
|
|
18988
18867
|
return buildSubmitResponse(c, prior, prior.requesterToken);
|
|
18989
18868
|
}
|
|
18990
18869
|
/**
|
|
18991
|
-
*
|
|
18992
|
-
*
|
|
18993
|
-
*
|
|
18994
|
-
* and re-issue the original response under the SAME proof-of-origin gates
|
|
18995
|
-
* as the header regime: reproduce the fingerprint, sign as the same caller
|
|
18996
|
-
* (explicit draws only exist on descriptor-auth DVMs), match requesterId.
|
|
18997
|
-
*
|
|
18998
|
-
* A recorded draw whose job row never landed (crash between the ledger
|
|
18999
|
-
* commit and the job persist) has nothing to hand back — refuse with the
|
|
19000
|
-
* 409 and log loudly; the pending hold is surfaced by `listPendingDraws`.
|
|
18870
|
+
* Resolve the recorded draw's job under the same fingerprint/pubkey/requester
|
|
18871
|
+
* checks as header recovery. If no job exists, the caller may still recover a
|
|
18872
|
+
* pending durable acceptance under its bound public request-id claim.
|
|
19001
18873
|
*/
|
|
19002
18874
|
async recoverDrawnJob(c, opts) {
|
|
19003
18875
|
let jobId = opts.jobId;
|
|
@@ -19078,7 +18950,6 @@ var DVMServer = class {
|
|
|
19078
18950
|
}
|
|
19079
18951
|
return void 0;
|
|
19080
18952
|
}
|
|
19081
|
-
/** Recover one still-pending draw recorded for the stable public job id. */
|
|
19082
18953
|
async recoverDrawAcceptance(opts) {
|
|
19083
18954
|
const dvmId = this.opts.dvmId;
|
|
19084
18955
|
if (!dvmId) return void 0;
|
|
@@ -19108,12 +18979,9 @@ var DVMServer = class {
|
|
|
19108
18979
|
return payment;
|
|
19109
18980
|
}
|
|
19110
18981
|
/**
|
|
19111
|
-
* Recompute
|
|
19112
|
-
*
|
|
19113
|
-
*
|
|
19114
|
-
* satisfy `onQuote.schema` or the handler throws — failing closed is the
|
|
19115
|
-
* right default, since the alternative (running the job with
|
|
19116
|
-
* `requiredMsats=undefined`) lets unpaid jobs through.
|
|
18982
|
+
* Recompute dynamic upfront pricing from parsed job input, applying the quote
|
|
18983
|
+
* schema separately. Refuse invalid input/results, currency mismatch or missing
|
|
18984
|
+
* FX rather than running without a price (400/500/503 as classified below).
|
|
19117
18985
|
*/
|
|
19118
18986
|
async computeDynamicPrice(onQuote, parsedInput, body, callerPubkey) {
|
|
19119
18987
|
let data = parsedInput;
|
|
@@ -19319,13 +19187,6 @@ var DVMServer = class {
|
|
|
19319
19187
|
}
|
|
19320
19188
|
return c.json({ messages: job.messages.filter((m) => m.seq > after) });
|
|
19321
19189
|
}
|
|
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
19190
|
jobNotActiveResponse(c) {
|
|
19330
19191
|
return c.json(
|
|
19331
19192
|
{
|
|
@@ -19498,8 +19359,8 @@ var DVMServer = class {
|
|
|
19498
19359
|
}
|
|
19499
19360
|
/**
|
|
19500
19361
|
* Tear down this machine's in-memory reactivation scaffold when it stands
|
|
19501
|
-
* down
|
|
19502
|
-
* handler that won the single-execution claim
|
|
19362
|
+
* down after losing the reactivation advisory lock or deferring to a live original
|
|
19363
|
+
* handler that won the single-execution claim. Leaving
|
|
19503
19364
|
* the job in `activeJobs` with a live idle timer would, one idle window
|
|
19504
19365
|
* later, fire `cancelJobIdle` → Tx A `cancel` and clobber the winner. We also
|
|
19505
19366
|
* must not persist our stale snapshot; clearing the appender prevents that.
|
|
@@ -19535,6 +19396,7 @@ var DVMServer = class {
|
|
|
19535
19396
|
await jm.finalizeStoredTerminal(id);
|
|
19536
19397
|
return c.json({ status: "cancelled" });
|
|
19537
19398
|
}
|
|
19399
|
+
record.endedBy = "caller";
|
|
19538
19400
|
record.status = "cancelled";
|
|
19539
19401
|
record.lastActivityAt = Date.now();
|
|
19540
19402
|
await jm.store.save(record);
|
|
@@ -19880,7 +19742,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19880
19742
|
}
|
|
19881
19743
|
}
|
|
19882
19744
|
/**
|
|
19883
|
-
* Register one mount's session capability
|
|
19745
|
+
* Register one mount's session capability.
|
|
19884
19746
|
*
|
|
19885
19747
|
* A host can serve several DVMs behind one `DVMKIT_PLATFORM_TOKEN`, and the
|
|
19886
19748
|
* platform row that token identifies is the deployment as a whole — so the
|
|
@@ -19896,16 +19758,9 @@ var PlatformTempoObserverHealth = class {
|
|
|
19896
19758
|
};
|
|
19897
19759
|
}
|
|
19898
19760
|
/**
|
|
19899
|
-
*
|
|
19900
|
-
*
|
|
19901
|
-
*
|
|
19902
|
-
* the current set of declarations. `host.ts` primes this client before any
|
|
19903
|
-
* mount exists, so at boot there is routinely an open probe carrying **no**
|
|
19904
|
-
* `tempo_session` at all — handing it back to the mount that just declared
|
|
19905
|
-
* one would leave a scale-to-zero DVM unstamped until traffic arrived, which
|
|
19906
|
-
* is exactly the container-restart case internal-review is about. Same shape with
|
|
19907
|
-
* two mounts: the second would join a probe whose URL already read the first
|
|
19908
|
-
* one's `false`.
|
|
19761
|
+
* De-duplicate probes only while their declaration epoch is current. A mount
|
|
19762
|
+
* declared during boot or another mount's probe must await a fresh probe so
|
|
19763
|
+
* the platform receives the updated capability, even before traffic arrives.
|
|
19909
19764
|
*/
|
|
19910
19765
|
async refresh() {
|
|
19911
19766
|
const open = this.inFlight;
|
|
@@ -19951,7 +19806,7 @@ var PlatformTempoObserverHealth = class {
|
|
|
19951
19806
|
}
|
|
19952
19807
|
/**
|
|
19953
19808
|
* The probe URL, carrying this deployment's session capability when a mount
|
|
19954
|
-
* has declared one
|
|
19809
|
+
* has declared one.
|
|
19955
19810
|
*
|
|
19956
19811
|
* Absent means "no signal", not "no sessions": a container running an older
|
|
19957
19812
|
* SDK build sends nothing, and the platform must leave its stamp exactly
|