shinjuku-shielded 0.3.155 → 0.3.157

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 CHANGED
@@ -6,19 +6,19 @@ answers privately, in a terminal chat or for your agent app. Its `mcp`
6
6
  command runs a local MCP server (stdio) for your agent. Your keys never
7
7
  leave your machine.
8
8
 
9
- This is version 0.3.155. The npm packages `shinjuku-shield` and
9
+ This is version 0.3.157. The npm packages `shinjuku-shield` and
10
10
  `shinjuku-shielded` hold the same files. The command is `shinjuku`.
11
11
 
12
12
  ## Set up
13
13
 
14
14
  ```sh
15
- npm install -g shinjuku@0.3.155
15
+ npm install -g shinjuku@0.3.157
16
16
  shinjuku setup
17
17
  ```
18
18
 
19
19
  Type the two lines as they are. `npx install shinjuku` does not work: npx
20
20
  reads `install` as the name of another npm package. To set up without
21
- installing, type `npx shinjuku@0.3.155 setup`.
21
+ installing, type `npx shinjuku@0.3.157 setup`.
22
22
 
23
23
  Setup makes the wallet on this machine, a private passphrase file, and the
24
24
  checked proof tools (about 191 MB), and writes `config.json`. It also writes
@@ -36,13 +36,13 @@ Every wallet command works after `shinjuku`, for example `shinjuku balance`,
36
36
  Claude Code:
37
37
 
38
38
  ```sh
39
- claude mcp add --scope user shinjuku -- npx -y --no-audit --no-fund shinjuku-shield@0.3.155 mcp
39
+ claude mcp add --scope user shinjuku -- npx -y --no-audit --no-fund shinjuku-shield@0.3.157 mcp
40
40
  ```
41
41
 
42
42
  Claude Desktop (`claude_desktop_config.json`) or Cursor (`~/.cursor/mcp.json`):
43
43
 
44
44
  ```json
45
- { "mcpServers": { "shinjuku": { "command": "npx", "args": ["-y", "--no-audit", "--no-fund", "shinjuku-shield@0.3.155", "mcp"] } } }
45
+ { "mcpServers": { "shinjuku": { "command": "npx", "args": ["-y", "--no-audit", "--no-fund", "shinjuku-shield@0.3.157", "mcp"] } } }
46
46
  ```
47
47
 
48
48
  On native Windows, if the host cannot find `npx`, use `"command": "cmd"` and
@@ -55,7 +55,7 @@ Codex (the Codex app and Codex CLI read the same file, `~/.codex/config.toml`):
55
55
  ```toml
56
56
  [mcp_servers.shinjuku]
57
57
  command = "npx"
58
- args = ["-y", "--no-audit", "--no-fund", "shinjuku-shield@0.3.155", "mcp"]
58
+ args = ["-y", "--no-audit", "--no-fund", "shinjuku-shield@0.3.157", "mcp"]
59
59
  startup_timeout_sec = 120
60
60
  tool_timeout_sec = 300
61
61
  ```
package/manifest.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schema": "carbon-shielded-wallet/bundle-manifest/v1",
3
3
  "file": "shinjuku-wallet.mjs",
4
- "sha256": "65d2837cf983622ed22a9d2bfb488a3a6ff0f276cb0d561b1be6074415daab0f",
5
- "bytes": 9055610,
6
- "sourceCommit": "5645304267acd2efd47b75cf75ae25ffae3096c5",
4
+ "sha256": "46b6176b85190a52572566a760686111c9f01d07b8057e83a3ea1d76a20a3f5f",
5
+ "bytes": 9069628,
6
+ "sourceCommit": "4c41d8d68ac102c3b34f13def9057f631c923ce7",
7
7
  "sourceDirty": false,
8
8
  "installRecordMatchesLockfile": true,
9
9
  "releaseRule": "valid only when two fresh npm ci --ignore-scripts builds on different OSes give the same sha256; installRecordMatchesLockfile true means every entry npm installed equals its package-lock.json entry and every entry not marked optional, devOptional included, is installed: an optional entry (a package for another platform) may be absent",
@@ -1038,5 +1038,5 @@
1038
1038
  "version": "1.8.0"
1039
1039
  }
1040
1040
  ],
1041
- "publishedAt": "2026-11-03T05:58:09.739Z"
1041
+ "publishedAt": "2026-11-03T09:32:37.020Z"
1042
1042
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "shinjuku-shielded",
3
- "version": "0.3.155",
3
+ "version": "0.3.157",
4
4
  "mcpName": "io.github.ShinjukuStaition/shinjuku-mcp",
5
5
  "description": "Shinjuku Shielded, the private x402 facilitator on Solana: the self-custody wallet CLI and its MCP server. Shield, pay, and unshield USDC from any MCP agent, under caps you set.",
6
- "shinjukuRelease": "65d2837c",
7
- "shinjukuSha256": "65d2837cf983622ed22a9d2bfb488a3a6ff0f276cb0d561b1be6074415daab0f",
6
+ "shinjukuRelease": "46b6176b",
7
+ "shinjukuSha256": "46b6176b85190a52572566a760686111c9f01d07b8057e83a3ea1d76a20a3f5f",
8
8
  "license": "SEE LICENSE IN LICENSE.txt",
9
9
  "author": "Shinjuku Shielded",
10
10
  "homepage": "https://github.com/ShinjukuStaition/shinjuku-shield#readme",
@@ -42618,6 +42618,15 @@ function markPhase(phase, outcome) {
42618
42618
  } catch {
42619
42619
  }
42620
42620
  }
42621
+ function boundMarkPhase() {
42622
+ const write = scope.getStore();
42623
+ return (phase, outcome) => {
42624
+ try {
42625
+ write?.(phase, outcome);
42626
+ } catch {
42627
+ }
42628
+ };
42629
+ }
42621
42630
  function poolReadOutcome(slot3, moved) {
42622
42631
  let text3;
42623
42632
  try {
@@ -42917,6 +42926,9 @@ function tunnelRefused(statusCode) {
42917
42926
  }
42918
42927
  function optimisticConnect(params, { proxyHost, proxyPort, closedInRow }) {
42919
42928
  const target = String(params.path);
42929
+ const mark = boundMarkPhase();
42930
+ const startedAt = Date.now();
42931
+ let dials = 0;
42920
42932
  const head = `CONNECT ${target} HTTP/1.1\r
42921
42933
  ${Object.entries(params.headers ?? {}).filter(([name, value2]) => value2 !== void 0 && !/[\r\n]/.test(`${name}${value2}`)).map(([name, value2]) => `${name}: ${value2}\r
42922
42934
  `).join("")}\r
@@ -42969,6 +42981,7 @@ ${Object.entries(params.headers ?? {}).filter(([name, value2]) => value2 !== voi
42969
42981
  if (first) pendingReject = reject;
42970
42982
  reply = Buffer.alloc(0);
42971
42983
  ready = false;
42984
+ dials += 1;
42972
42985
  socket = net.connect(proxyPort, proxyHost);
42973
42986
  socket.setNoDelay(true);
42974
42987
  let connected = false;
@@ -42977,6 +42990,7 @@ ${Object.entries(params.headers ?? {}).filter(([name, value2]) => value2 !== voi
42977
42990
  socket.write(head);
42978
42991
  for (const chunk of sent) socket.write(chunk);
42979
42992
  ready = true;
42993
+ mark("tunnel-sent", `dial${dials}`);
42980
42994
  if (first) pendingReject = void 0;
42981
42995
  resolve39();
42982
42996
  });
@@ -42990,9 +43004,11 @@ ${Object.entries(params.headers ?? {}).filter(([name, value2]) => value2 !== voi
42990
43004
  if (end < 0) return;
42991
43005
  const status = Number(/^HTTP\/1\.[01] (\d{3})/.exec(reply.subarray(0, reply.indexOf("\r\n")).toString("latin1"))?.[1] ?? 0);
42992
43006
  if (status !== 200) {
43007
+ mark("tunnel-refused", `http${status}`);
42993
43008
  stream.destroy(tunnelRefused(status));
42994
43009
  return;
42995
43010
  }
43011
+ mark("tunnel-200", `ms${Date.now() - startedAt}_dial${dials}`);
42996
43012
  open4 = true;
42997
43013
  sent.length = 0;
42998
43014
  closedInRow.delete(target);
@@ -43019,6 +43035,7 @@ ${Object.entries(params.headers ?? {}).filter(([name, value2]) => value2 !== voi
43019
43035
  return;
43020
43036
  }
43021
43037
  const closed = (closedInRow.get(target) ?? 0) + 1;
43038
+ mark("tunnel-closed", `n${closed}`);
43022
43039
  if (closed < TUNNEL_CLOSED_TRIES) {
43023
43040
  closedInRow.set(target, closed);
43024
43041
  dial(false).catch((error) => stream.destroy(error));
@@ -43042,6 +43059,7 @@ var init_tunnelGuard = __esm({
43042
43059
  "wallet/src/net/tunnelGuard.mjs"() {
43043
43060
  "use strict";
43044
43061
  init_keepAlive();
43062
+ init_phaseTiming();
43045
43063
  RUN_MARK = "SHINJUKU_WALLET_PROXIED";
43046
43064
  TUNNEL_CLOSED_TRIES = 3;
43047
43065
  }
@@ -67009,6 +67027,7 @@ var init_errorCodes = __esm({
67009
67027
  "inference_no_provider",
67010
67028
  "inference_origin_refused",
67011
67029
  "inference_out_of_tokens",
67030
+ "inference_empty_answer",
67012
67031
  // wallet_unshield of a balance in several accounts: the stage code while the automatic join runs (mcp/tools.mjs joiningStage).
67013
67032
  "unshield_joining_accounts",
67014
67033
  // unshield of a fixed amount in a terminal: its own join (joinForFixedSend, AB186) did not get the balance ready.
@@ -83564,15 +83583,19 @@ function memoryRequestMatches(record, { url, request, programId, anyProgram = fa
83564
83583
  function dropOldAnswers(now) {
83565
83584
  for (const [key, entry] of answers) if (now >= entry.atMs + MEMORY_ANSWER_KEEP_MS) answers.delete(key);
83566
83585
  }
83567
- function keepMemoryAnswer(record, resultBase64, now = Date.now()) {
83586
+ function keepMemoryAnswer(record, resultBase64, now = Date.now(), sellerHeaders = []) {
83568
83587
  if (!isMemoryFingerprint(record?.fingerprint) || typeof resultBase64 !== "string") return;
83569
83588
  dropOldAnswers(now);
83570
- keepBounded(answers, record.fingerprint, { resultBase64, atMs: now });
83589
+ keepBounded(answers, record.fingerprint, { resultBase64, atMs: now, sellerHeaders: Array.isArray(sellerHeaders) ? sellerHeaders : [] });
83571
83590
  }
83572
83591
  function memoryAnswer(record, now = Date.now()) {
83573
83592
  dropOldAnswers(now);
83574
83593
  return isMemoryFingerprint(record?.fingerprint) ? answers.get(record.fingerprint)?.resultBase64 : void 0;
83575
83594
  }
83595
+ function memoryAnswerHeaders(record, now = Date.now()) {
83596
+ dropOldAnswers(now);
83597
+ return (isMemoryFingerprint(record?.fingerprint) ? answers.get(record.fingerprint)?.sellerHeaders : void 0) ?? [];
83598
+ }
83576
83599
  function forgetMemoryOnly() {
83577
83600
  requests.clear();
83578
83601
  answers.clear();
@@ -102203,7 +102226,7 @@ function floatOutcomeWords(code2, error, t) {
102203
102226
  case "p03_float_answer_invalid_refund_pending": {
102204
102227
  const amount = amountText(error?.paidAtomic, "the amount paid");
102205
102228
  const fixable = floatSellerCanBeFixed(error);
102206
- const seller = fixable && typeof error?.sellerReason === "string" ? ` ${p.The} refused this request (HTTP ${error.httpStatus}) and said why: ${JSON.stringify(error.sellerReason)}.` : fixable ? ` ${p.The} refused this request (HTTP ${error.httpStatus}) and said why in its first answer; the wallet does not keep ${p.the}'s words.` : typeof error?.sellerReason === "string" ? ` ${p.The} answered HTTP ${error.httpStatus}: ${JSON.stringify(error.sellerReason)}.` : Number.isInteger(error?.httpStatus) ? ` ${p.The} answered HTTP ${error.httpStatus}.` : "";
102229
+ const seller = fixable && error?.sellerReasonHidden === true ? ` ${p.The} refused this request (HTTP ${error.httpStatus}); its own words are not shown, because they can repeat your question.` : fixable && typeof error?.sellerReason === "string" ? ` ${p.The} refused this request (HTTP ${error.httpStatus}) and said why: ${JSON.stringify(error.sellerReason)}.` : fixable ? ` ${p.The} refused this request (HTTP ${error.httpStatus}) and said why in its first answer; the wallet does not keep ${p.the}'s words.` : typeof error?.sellerReason === "string" ? ` ${p.The} answered HTTP ${error.httpStatus}: ${JSON.stringify(error.sellerReason)}.` : Number.isInteger(error?.httpStatus) ? ` ${p.The} answered HTTP ${error.httpStatus}.` : "";
102207
102230
  const late2 = floatChargeTooLate(error) ? ` ${p.The} worked ${error.sellerAnswerSeconds} s before it tried to charge, and its answer says the charge failed. A Solana payment expires about 30 s after it is signed, so no wallet can pay ${p.a} that works this long before it charges.` : "";
102208
102231
  const tryAgain = fixable ? t.ai ? `${p.The} said what is wrong with the request. If ${stillWants(t)} an answer, change the question as it says, then ${t.rerun.toLowerCase()} with ${t.newId}` : `The seller said what is wrong with the request. If ${stillWants(t)} the data, fix the request as the seller says (for example add the query values, headers, or body fields it names), then ${t.rerun.toLowerCase()} with the fixed request and ${t.newId}` : late2 ? t.ai ? `${p.The} works too long before it charges. ${t.ai.other}` : "Do not pay this seller again: it works too long before it charges" : t.ai ? `If ${stillWants(t)} an answer, ${t.askNew}` : `If ${stillWants(t)} the data, try this seller again with ${t.newId}`;
102209
102232
  const why = floatRefundWhy(error, Boolean(t.ai));
@@ -102380,7 +102403,7 @@ function floatOutcomeWords(code2, error, t) {
102380
102403
  return void 0;
102381
102404
  }
102382
102405
  }
102383
- var ATOMIC4, TRANSFER_DEAD_WORDS, payeeOf, floatSellerCanBeFixed, floatSeller4xx, answeredInstead, floatStillSettling, floatChargeTooLate, amountText, withTransaction, PAID_SELLER_FAILED_CODE, stillWants, AI_SERVER_WRONG, QUOTE_REFUSED_WHY;
102406
+ var ATOMIC4, TRANSFER_DEAD_WORDS, payeeOf, floatSellerCanBeFixed, withoutProviderText, floatSeller4xx, answeredInstead, floatStillSettling, floatChargeTooLate, amountText, withTransaction, PAID_SELLER_FAILED_CODE, stillWants, AI_SERVER_WRONG, QUOTE_REFUSED_WHY;
102384
102407
  var init_floatWords = __esm({
102385
102408
  "wallet/src/pay/floatWords.mjs"() {
102386
102409
  "use strict";
@@ -102393,6 +102416,11 @@ var init_floatWords = __esm({
102393
102416
  };
102394
102417
  payeeOf = (ai) => ai ? { the: "the AI provider", The: "The AI provider", this: "the AI provider", a: "an AI provider" } : { the: "the seller", The: "The seller", this: "this seller", a: "a seller" };
102395
102418
  floatSellerCanBeFixed = (error) => floatSeller4xx(error) && !floatChargeTooLate(error) && ![402, 408, 429].includes(error.httpStatus) && (typeof error?.sellerReason === "string" && error.sellerReason.trim() !== "" || error?.sellerFixable === true);
102419
+ withoutProviderText = (facts) => {
102420
+ const { sellerReason, ...rest } = facts ?? {};
102421
+ if (typeof sellerReason !== "string" || !sellerReason.trim()) return rest;
102422
+ return { ...rest, sellerReasonHidden: true, ...floatSellerCanBeFixed(facts) ? { sellerFixable: true } : {} };
102423
+ };
102396
102424
  floatSeller4xx = (error) => Number.isInteger(error?.httpStatus) && error.httpStatus >= 400 && error.httpStatus < 500;
102397
102425
  answeredInstead = (p, status, data) => `${p.the} answered HTTP ${status}${status >= 500 ? " (an error on its side)" : ""} instead of ${data}`;
102398
102426
  floatStillSettling = (code2, error) => code2 === "p03_float_paid_finishing_retry_same_request" && error?.sellerCode === "payment_pending_retry_same_payment";
@@ -103664,10 +103692,10 @@ function markFloatRefundOutcomesShown(store, shown) {
103664
103692
  }
103665
103693
  }
103666
103694
  function toMemoryOnly(store, walName2, record) {
103667
- const { resultBase64, resultSha256: _hash, sellerReason: _reason, ...rest } = record;
103695
+ const { resultBase64, resultSha256: _hash, sellerReason: _reason, sellerHeaders, ...rest } = record;
103668
103696
  const next = { ...rest, fingerprint: memoryOnlyFingerprint(), request: memoryOnlyRequest(record.request), memoryOnly: true };
103669
103697
  store.writeWal(walName2, next);
103670
- if (next.state === "receipt") keepMemoryAnswer(next, resultBase64);
103698
+ if (next.state === "receipt") keepMemoryAnswer(next, resultBase64, void 0, sellerHeaders);
103671
103699
  return next;
103672
103700
  }
103673
103701
  function finishedAtMs(record) {
@@ -104428,7 +104456,7 @@ async function payViaFloat({
104428
104456
  if (record.state === "receipt" && record.memoryOnly === true) {
104429
104457
  const kept3 = memoryAnswer(record);
104430
104458
  if (kept3 === void 0) finished(record);
104431
- return paidResult({ ...record, resultBase64: kept3 }, true);
104459
+ return paidResult({ ...record, resultBase64: kept3, sellerHeaders: memoryAnswerHeaders(record) }, true);
104432
104460
  }
104433
104461
  if (record.state === "receipt") return paidResult(record, true);
104434
104462
  if (record.state === "refund_pending") refundPendingAnswer(record);
@@ -104805,18 +104833,18 @@ async function payViaFloat({
104805
104833
  transaction: isSolanaSignature(settled?.transaction) ? settled.transaction : null,
104806
104834
  network: record.requirement.network
104807
104835
  };
104836
+ const sellerHeaders = keptSellerHeaders(response.headers);
104808
104837
  record = {
104809
104838
  ...record,
104810
104839
  state: "receipt",
104811
104840
  status: response.status,
104812
104841
  receipt,
104813
- ...answerFields(record, resultBase64),
104814
- sellerHeaders: keptSellerHeaders(response.headers),
104842
+ ...answerFields(record, resultBase64, sellerHeaders),
104815
104843
  answeredAtMs: Date.now()
104816
104844
  };
104817
104845
  store.writeWal(walName2, record);
104818
104846
  mark("float-receipt");
104819
- return answered(store, walName2, record, resultBase64);
104847
+ return answered(store, walName2, record, resultBase64, sellerHeaders);
104820
104848
  }
104821
104849
  const sellerReason = await sellerAnswerText(response);
104822
104850
  if (wasKept) keptAgain(response.status, sellerReason);
@@ -105087,18 +105115,18 @@ async function selfBroadcastLeg({ store, walName: walName2, record, sendRequest,
105087
105115
  transaction: isSolanaSignature(settled?.transaction) ? settled.transaction : record.broadcastSignature,
105088
105116
  network: record.requirement.network
105089
105117
  };
105118
+ const sellerHeaders = keptSellerHeaders(response.headers);
105090
105119
  record = {
105091
105120
  ...record,
105092
105121
  state: "receipt",
105093
105122
  status: response.status,
105094
105123
  receipt,
105095
- ...answerFields(record, resultBase64),
105096
- sellerHeaders: keptSellerHeaders(response.headers),
105124
+ ...answerFields(record, resultBase64, sellerHeaders),
105097
105125
  answeredAtMs: Date.now()
105098
105126
  };
105099
105127
  store.writeWal(walName2, record);
105100
105128
  mark("float-receipt");
105101
- return answered(store, walName2, record, resultBase64);
105129
+ return answered(store, walName2, record, resultBase64, sellerHeaders);
105102
105130
  }
105103
105131
  if (response.status >= 500 || response.status === 202) {
105104
105132
  const unansweredReason = await sellerRefusalText(response);
@@ -105170,13 +105198,13 @@ function keptSellerHeaders(headers) {
105170
105198
  }
105171
105199
  return kept3;
105172
105200
  }
105173
- function answerFields(record, resultBase64) {
105174
- return record.memoryOnly === true ? {} : { resultBase64 };
105201
+ function answerFields(record, resultBase64, sellerHeaders) {
105202
+ return record.memoryOnly === true ? {} : { resultBase64, sellerHeaders };
105175
105203
  }
105176
- function answered(store, walName2, record, resultBase64) {
105204
+ function answered(store, walName2, record, resultBase64, sellerHeaders) {
105177
105205
  if (record.memoryOnly !== true) return paidResult(record, false);
105178
- keepMemoryAnswer(record, resultBase64);
105179
- return paidResult({ ...record, resultBase64 }, false);
105206
+ keepMemoryAnswer(record, resultBase64, void 0, sellerHeaders);
105207
+ return paidResult({ ...record, resultBase64, sellerHeaders }, false);
105180
105208
  }
105181
105209
  function paidResult(record, replayed) {
105182
105210
  return {
@@ -105270,7 +105298,7 @@ var init_floatPay = __esm({
105270
105298
  floatPaymentWalName = (requestKey) => `${FLOAT_PAYMENT_WAL_PREFIX2}${requestKey}`;
105271
105299
  floatRecordName = (requestId) => floatPaymentWalName(hash7(requestId));
105272
105300
  FLOAT_KEEP_BYTES_MS = 6e5;
105273
- memoryOnlyNeedsScrub = (record) => record?.memoryOnly === true && (record.resultSha256 !== void 0 || record.request?.bodySha256 !== void 0 || record.request?.headersSha256 !== void 0 || !/^memory-[0-9a-f]{64}$/.test(String(record.fingerprint ?? "")));
105301
+ memoryOnlyNeedsScrub = (record) => record?.memoryOnly === true && (record.resultSha256 !== void 0 || Array.isArray(record.sellerHeaders) && record.sellerHeaders.length > 0 || record.request?.bodySha256 !== void 0 || record.request?.headersSha256 !== void 0 || !/^memory-[0-9a-f]{64}$/.test(String(record.fingerprint ?? "")));
105274
105302
  FLOAT_KEEP_OPEN_REQUEST_MS = 36e5;
105275
105303
  configCache = /* @__PURE__ */ new Map();
105276
105304
  configGeneration = 0;
@@ -109479,6 +109507,16 @@ function refusalSay(a, tool, again, npmVersion) {
109479
109507
  const head = `${paid ? `Paid ${paid} USDC${price2 && positive(fee) ? ` (${price2} price + ${fee} Shinjuku fee)` : ""}` : "Paid"}, but`;
109480
109508
  return firstFit(`${head} the model reached the answer's length limit before it answered.${more}`, `${head} the answer hit its length limit.${more}`);
109481
109509
  }
109510
+ if (code2 === "inference_empty_answer") {
109511
+ const again2 = " Ask again (it pays again) or pick another model.";
109512
+ if (a.replayed === true && a.paid === false) return `Nothing was paid, and the stored answer is empty.${again2}`;
109513
+ if (a.replayed === true) return `Already paid; the stored answer is empty and nothing new was paid.${again2}`;
109514
+ const paid = usdcText2(a.paidAtomic);
109515
+ const price2 = usdcText2(a.priceAtomic);
109516
+ const fee = usdcText2(a.feeAtomic);
109517
+ const head = `${paid ? `Paid ${paid} USDC${price2 && positive(fee) ? ` (${price2} price + ${fee} Shinjuku fee)` : ""}` : "Paid"}, but`;
109518
+ return firstFit(`${head} the AI returned no answer, and no refund comes.${again2}`, `${head} the AI returned no answer.${again2}`);
109519
+ }
109482
109520
  if (code2 === "inference_no_provider") {
109483
109521
  return "Nothing was paid: no AI provider can take this wallet's payment for this model right now. Ask for another model, or ask again later.";
109484
109522
  }
@@ -110935,8 +110973,8 @@ async function offeredPrice(paymentRequired, store, serviceUrl2, target, timing,
110935
110973
  }
110936
110974
  return { rail, amountAtomic: highest.toString(), priceAtomic: highest.toString() };
110937
110975
  }
110938
- function ownRecord(store, requestId) {
110939
- const float = floatRecordOf(store, requestId);
110976
+ function ownRecord(store, requestId, { strict = false } = {}) {
110977
+ const float = strict ? store.readWal(floatRecordName(requestId)) : floatRecordOf(store, requestId);
110940
110978
  if (float) return { kind: "float", record: float };
110941
110979
  const key = sha256Hex2(Buffer.from(requestId, "utf8"));
110942
110980
  const shielded = store.readWal(`resource-payment-${key}`);
@@ -111049,9 +111087,9 @@ async function payTurn({ session, target, method, headers, body, maxPrice, polic
111049
111087
  if (session.closed) fail26("fetch_session_closed", "this payer session is closed; open a new one");
111050
111088
  const paymentStartedAt = Date.now();
111051
111089
  const timing = typeof opts?.timing === "function" ? opts.timing : session.timing();
111052
- const mark = (phase) => {
111090
+ const mark = (phase, outcome) => {
111053
111091
  try {
111054
- timing?.(phase);
111092
+ timing?.(phase, outcome);
111055
111093
  } catch {
111056
111094
  }
111057
111095
  };
@@ -111069,12 +111107,12 @@ async function payTurn({ session, target, method, headers, body, maxPrice, polic
111069
111107
  warmFloatConfig({ origin: new URL(session.memoServiceUrl).origin, timing: mark });
111070
111108
  startRecoveredPaymentTools(lazyProofTools(session));
111071
111109
  }
111072
- const response = await unpaidRequest(target, () => keepAliveInit({
111110
+ const response = await withPhaseScope(mark, "fetch", () => unpaidRequest(target, () => keepAliveInit({
111073
111111
  method,
111074
111112
  headers,
111075
111113
  ...body ? { body } : {},
111076
111114
  redirect: "manual"
111077
- }), { timeoutMs: Number.isSafeInteger(opts?.unpaidTimeoutMs) && opts.unpaidTimeoutMs > 0 ? opts.unpaidTimeoutMs : void 0, onCut: opts?.onUnpaidCut });
111115
+ }), { timeoutMs: Number.isSafeInteger(opts?.unpaidTimeoutMs) && opts.unpaidTimeoutMs > 0 ? opts.unpaidTimeoutMs : void 0, onCut: opts?.onUnpaidCut }));
111078
111116
  if (response.status !== 402) return response;
111079
111117
  const encoded = response.headers.get("PAYMENT-REQUIRED");
111080
111118
  void drainBody(response);
@@ -111474,6 +111512,23 @@ async function paidCall({ session, target, method, headers, body, cap, maxPrice,
111474
111512
  }
111475
111513
  mark("fetch-ready");
111476
111514
  const fresh = challenge !== void 0 && Date.now() - challenge.atMs <= PREFETCH_MAX_AGE_MS;
111515
+ const leftDecision = () => {
111516
+ let left = true;
111517
+ let unreadable = false;
111518
+ let after;
111519
+ try {
111520
+ after = ownRecord(store, requestId, { strict: true });
111521
+ left = after === void 0 ? own !== void 0 : committed(after, store) && !closedUnpaid(after, store) && !preparedOnly(after, store);
111522
+ } catch {
111523
+ after = void 0;
111524
+ unreadable = true;
111525
+ }
111526
+ const atomic4 = left ? (after === void 0 ? void 0 : paidAmount(after) ?? price2?.amountAtomic) ?? String(cap) : void 0;
111527
+ return Object.freeze({ left, atomic: atomic4, unreadable, before: leftBefore });
111528
+ };
111529
+ const recordLeft = (decision) => {
111530
+ if (policy && decision.left) policy.record({ amountAtomic: earlierRun ? "0" : decision.atomic, url: target, requestId });
111531
+ };
111477
111532
  const result = await payResource({
111478
111533
  store,
111479
111534
  connection: session.connection,
@@ -111517,19 +111572,54 @@ async function paidCall({ session, target, method, headers, body, cap, maxPrice,
111517
111572
  onCompactList: memoPrefetchOnList(session, store)
111518
111573
  })
111519
111574
  }
111520
- }).finally(markMade);
111521
- const amountAtomic = paidAmount(ownRecord(store, requestId)) ?? price2?.amountAtomic;
111575
+ }).finally(markMade).catch((error) => {
111576
+ const failure2 = error !== null && typeof error === "object" ? error : Object.assign(new Error(String(error)), { cause: error });
111577
+ try {
111578
+ const decision = leftDecision();
111579
+ try {
111580
+ failure2.paidLeft = decision;
111581
+ } catch {
111582
+ }
111583
+ recordLeft(decision);
111584
+ } catch {
111585
+ }
111586
+ throw failure2;
111587
+ });
111588
+ let ownAfter;
111589
+ let afterUnreadable = false;
111590
+ try {
111591
+ ownAfter = ownRecord(store, requestId, { strict: true });
111592
+ } catch {
111593
+ ownAfter = void 0;
111594
+ afterUnreadable = true;
111595
+ }
111596
+ const resultAtomic = POSITIVE_ATOMIC4.test(String(result.paidAtomic ?? "")) ? String(result.paidAtomic) : void 0;
111597
+ const amountAtomic = paidAmount(ownAfter) ?? resultAtomic ?? price2?.amountAtomic ?? (result.replayed ? void 0 : String(cap));
111522
111598
  if (!result.replayed && policy && amountAtomic) policy.record({ amountAtomic: earlierRun ? "0" : amountAtomic, url: target, requestId });
111523
- const bytes2 = Buffer.from(result.resultBase64 ?? "", "base64");
111524
- const payment = {
111525
- transaction: result.receipt?.transaction,
111526
- scheme: result.scheme === "exact" ? "exact" : "shielded-exact",
111527
- amountAtomic: amountAtomic ?? null,
111528
- requestId,
111529
- replayed: result.replayed === true,
111530
- receiptFinality: result.receiptFinality ?? null
111531
- };
111532
- return new Response(bytes2, { status: 200, headers: paidResponseHeaders(result.sellerHeaders, sniffContentType(bytes2), payment) });
111599
+ try {
111600
+ const bytes2 = Buffer.from(result.resultBase64 ?? "", "base64");
111601
+ const payment = {
111602
+ transaction: result.receipt?.transaction,
111603
+ scheme: result.scheme === "exact" ? "exact" : "shielded-exact",
111604
+ amountAtomic: amountAtomic ?? null,
111605
+ requestId,
111606
+ replayed: result.replayed === true,
111607
+ receiptFinality: result.receiptFinality ?? null
111608
+ };
111609
+ return new Response(bytes2, { status: 200, headers: paidResponseHeaders(result.sellerHeaders, sniffContentType(bytes2), payment) });
111610
+ } catch (error) {
111611
+ const failure2 = error !== null && typeof error === "object" ? error : Object.assign(new Error(String(error)), { cause: error });
111612
+ try {
111613
+ failure2.paidLeft = Object.freeze({
111614
+ left: true,
111615
+ atomic: amountAtomic ?? String(cap),
111616
+ unreadable: afterUnreadable,
111617
+ before: leftBefore || result.replayed === true
111618
+ });
111619
+ } catch {
111620
+ }
111621
+ throw failure2;
111622
+ }
111533
111623
  } finally {
111534
111624
  await release();
111535
111625
  }
@@ -139030,6 +139120,21 @@ ${canonical5}`;
139030
139120
  "inference_chat"
139031
139121
  )), paidStore);
139032
139122
  }
139123
+ if (typeof content === "string" && !content.trim()) {
139124
+ const paidText = usdcText3(paidAtomic ?? "");
139125
+ const reasonLogged = result.finishReason === void 0 || result.finishReason === null ? "none" : ["stop", "length", "content_filter", "tool_calls", "function_call"].includes(result.finishReason) ? result.finishReason : "other";
139126
+ log(`MCP_INFERENCE_EMPTY requestId=${requestId} finishReason=${reasonLogged}`);
139127
+ return withRefundNotices(json3(sayFirst(
139128
+ {
139129
+ ok: false,
139130
+ error: "inference_empty_answer",
139131
+ detail: `the AI provider returned an empty answer (finish reason ${JSON.stringify(result.finishReason ?? null)}); ` + (unpaid ? "nothing was paid: this request id's payment never landed (the pool's public history proves it)" : `this call was paid${paidText ? ` (${paidText} USDC)` : ""}${transaction ? ` in transaction ${transaction}` : ""} and no refund comes`),
139132
+ ...paidFields,
139133
+ next: unpaid ? "nothing was paid, and this request id's stored answer is empty. If the user still wants an answer, call inference_chat with a NEW request_id, or another model; it pays then" : receipt.replayed === true ? "this request id was already paid and its stored answer is empty; nothing new was paid. If the user still wants an answer, call inference_chat with a NEW request_id, or another model; it pays again" : `Tell the user: the AI returned no answer, and this call paid ${paidText ? `${paidText} USDC` : "(see paidAtomic)"}; no refund comes. If the user still wants an answer, call inference_chat again with a NEW request_id (reword the question, or pick another model); it pays again. The same request_id returns this same empty answer`
139134
+ },
139135
+ "inference_chat"
139136
+ )), paidStore);
139137
+ }
139033
139138
  return withRefundNotices(json3(sayFirst({
139034
139139
  ok: true,
139035
139140
  ...paidFields,
@@ -145139,7 +145244,8 @@ async function runChat({ io, values, deps = {} }) {
145139
145244
  empty: 0,
145140
145245
  pending: void 0,
145141
145246
  busy: false,
145142
- interrupts: 0
145247
+ interrupts: 0,
145248
+ epoch: 0
145143
145249
  };
145144
145250
  const servedNow = catalogNow;
145145
145251
  servedNow();
@@ -145514,8 +145620,15 @@ ${costLine2(picked.entry.id)}.` + (start ? "" : `
145514
145620
  The next question uses it${state.mode === "talk" ? ", with the conversation so far" : ""}.`);
145515
145621
  };
145516
145622
  let reader;
145623
+ const warnPaidPending = (how) => {
145624
+ if (!state.pending || !state.unsure.has(state.pending) || state.dropWarned === state.pending) return false;
145625
+ state.dropWarned = state.pending;
145626
+ say3(`An answer you may have paid for (${usdcWords(state.unsure.get(state.pending))}) is not finished. Type /retry to finish it: it never pays twice. Or ${how}.`);
145627
+ return true;
145628
+ };
145517
145629
  const onInterrupt = () => {
145518
145630
  if (!state.busy) {
145631
+ if (warnPaidPending("press Ctrl+C again to end the chat and drop it")) return;
145519
145632
  reader?.close();
145520
145633
  return;
145521
145634
  }
@@ -145728,6 +145841,7 @@ When it says ready, type your message here.`);
145728
145841
  state.interrupts = 0;
145729
145842
  }
145730
145843
  state.pending = void 0;
145844
+ const wasUnsure = state.unsure.get(turn);
145731
145845
  state.unsure.delete(turn);
145732
145846
  const replayed = result.receipt?.replayed === true;
145733
145847
  let unpaid = false;
@@ -145738,6 +145852,7 @@ When it says ready, type your message here.`);
145738
145852
  }
145739
145853
  const paid = unpaid ? 0n : typeof result.paidAtomic === "bigint" ? result.paidAtomic : void 0;
145740
145854
  if (paid !== void 0) state.spent += paid;
145855
+ else if (wasUnsure !== void 0 && !unpaid) state.spent += wasUnsure;
145741
145856
  const content = typeof result.content === "string" ? result.content : "";
145742
145857
  const paidText = unpaid ? "nothing was paid: the earlier payment for this answer never went through" : paid === void 0 ? "paid (the amount was not reported)" : replayed ? `already paid ${usdcWords(paid)} earlier for this answer; nothing new was paid` : `paid ${usdcWords(paid)}`;
145743
145858
  const dayRoom2 = dayRoomSentence();
@@ -145749,7 +145864,7 @@ When it says ready, type your message here.`);
145749
145864
  return;
145750
145865
  }
145751
145866
  state.answers += 1;
145752
- if (state.mode === "talk") state.history = [...turn.messages, { role: "assistant", content }];
145867
+ if (state.mode === "talk" && (turn.epoch ?? 0) === state.epoch) state.history = [...turn.messages, { role: "assistant", content }];
145753
145868
  io.out(`${fitColumns(
145754
145869
  content,
145755
145870
  process.stdout.isTTY ? process.stdout.columns : 0,
@@ -145770,28 +145885,45 @@ When it says ready, type your message here.`);
145770
145885
  const failed = async (error, turn) => {
145771
145886
  const code2 = errorCodeOf(error);
145772
145887
  const surelyNothing = nothingPaid(error);
145888
+ const paidLeft = [error, error?.cause].map((one) => one?.paidLeft).find((one) => one && typeof one === "object" && typeof one.left === "boolean");
145889
+ const leftAtomic = paidLeft?.left === true && /^[1-9][0-9]{0,30}$/.test(String(paidLeft.atomic ?? "")) ? BigInt(paidLeft.atomic) : void 0;
145773
145890
  const paidAtomic = error?.details?.receipt ? error.details.paidAtomic : void 0;
145891
+ const wasUnsure = state.unsure.get(turn);
145774
145892
  state.pending = void 0;
145775
145893
  if (error?.details?.receipt) {
145894
+ state.unsure.delete(turn);
145776
145895
  if (typeof paidAtomic === "bigint") state.spent += paidAtomic;
145896
+ else if (wasUnsure !== void 0) state.spent += wasUnsure;
145777
145897
  state.empty += 1;
145778
145898
  say3(`This answer was paid${typeof paidAtomic === "bigint" ? ` (${usdcWords(paidAtomic)})` : ""}, but no usable answer came back from the AI provider. A new question pays again: ask again, or /model to try another model (${modelIds2}). This chat paid ${paidSoFar()} so far.`);
145779
145899
  return;
145780
145900
  }
145781
145901
  if (code2 === "p03_resource_payment_answer_not_kept") {
145902
+ const earlier = error?.earlierPaymentAtomic ?? error?.cause?.earlierPaymentAtomic;
145903
+ state.unsure.delete(turn);
145904
+ if (/^[1-9][0-9]{0,30}$/.test(String(earlier ?? ""))) state.spent += BigInt(earlier);
145905
+ else if (wasUnsure !== void 0) state.spent += wasUnsure;
145782
145906
  state.empty += 1;
145783
145907
  say3(`${answerNotKeptWords(error?.earlierPaymentAtomic ?? error?.cause?.earlierPaymentAtomic)} This chat paid ${paidSoFar()} so far.`);
145784
145908
  return;
145785
145909
  }
145786
- if (/another_request_unresolved$/.test(code2 ?? "") && (error?.memoryOnlyOpen ?? error?.cause?.memoryOnlyOpen) === true) {
145910
+ if (paidLeft?.left !== true && /another_request_unresolved$/.test(code2 ?? "") && (error?.memoryOnlyOpen ?? error?.cause?.memoryOnlyOpen) === true) {
145787
145911
  say3(memoryOnlyOpenWords(error?.closesAtUnix ?? error?.cause?.closesAtUnix));
145788
145912
  return;
145789
145913
  }
145790
145914
  if (code2 === "fetch_proof_tools_unavailable") {
145915
+ const earlier = error?.earlierPaymentAtomic ?? error?.cause?.earlierPaymentAtomic;
145916
+ const heldAtomic = /^[1-9][0-9]{0,30}$/.test(String(earlier ?? "")) ? BigInt(earlier) : leftAtomic ?? wasUnsure;
145917
+ if (heldAtomic !== void 0) {
145918
+ state.unsure.set(turn, heldAtomic);
145919
+ state.pending = turn;
145920
+ say3(`This wallet is missing some setup files, so this try sent nothing and paid nothing new. The earlier payment for this answer (${usdcWords(heldAtomic)}) may be paid already: type /retry to try again (it never pays twice). If you end the chat to run ${commandLine("setup")}, the wallet closes that payment by itself: if it never reached the AI provider, the money comes back.`);
145921
+ return;
145922
+ }
145791
145923
  say3(`This wallet is missing some setup files, so nothing was paid. End the chat (/exit), run ${commandLine("setup")} to install them, then start the chat again: ${commandLine("chat")}`);
145792
145924
  return;
145793
145925
  }
145794
- if (localErrorCodes(error).includes(SPLIT_CODE)) {
145926
+ if (paidLeft?.left !== true && localErrorCodes(error).includes(SPLIT_CODE)) {
145795
145927
  state.pending = turn;
145796
145928
  const why = joinOutcome === "stale" ? "the wallet could not read your balance again just now" : joinOutcome === "unfinished" ? "the wallet did not finish getting it ready in this turn" : joinOutcome === "files" ? void 0 : "part of it is not ready to spend yet (another payment of this wallet, or its change, is not final yet)";
145797
145929
  say3(why === void 0 ? `Your private balance covers this answer in total, but not yet in one payment. Nothing was paid. Get it ready in another terminal: ${commandLine("prepare")}, then type /retry: it asks the same question again.` : `Your private balance covers this answer in total, but ${why}. Nothing was paid. Type /retry in a minute: it gets the balance ready by itself when it needs to, and never pays twice.`);
@@ -145843,14 +145975,36 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
145843
145975
  say3(held2 !== void 0 && facts.preparedNotSent === true ? `Nothing was paid: a payment of ${usdcWords(held2)} is prepared for an earlier try of this question and not sent, and the wallet never pays twice for one question. This chat cannot send it. ${closeFirst}` : held2 !== void 0 ? `This question already has a payment of ${usdcWords(held2)} from an earlier try, made with other details, and the wallet never pays twice for one question; nothing more was paid. This chat cannot finish that payment, and the same question typed again is a new question that pays again.` : open4 ? `Nothing was paid: an earlier try of this question was made with other details and is not finished; nothing of it was sent, and it blocks every new question until it is closed. ${closeFirst}` : "Nothing was paid: an earlier try of this question was made with other details. If you still want an answer, type the question again: it goes out as a new question.");
145844
145976
  return;
145845
145977
  }
145846
- const float = typeof code2 === "string" && !CAP_CODES.test(code2) ? floatOutcomeWords(code2, facts, chatFloatTerms()) : void 0;
145978
+ const float = typeof code2 === "string" && !CAP_CODES.test(code2) ? floatOutcomeWords(code2, withoutProviderText(facts), chatFloatTerms()) : void 0;
145847
145979
  if (float) {
145848
- const spent = floatSpent(code2, facts) && /^[1-9][0-9]{0,30}$/.test(String(facts.paidAtomic ?? ""));
145849
- if (spent) state.spent += BigInt(facts.paidAtomic);
145850
- if (["p03_float_seller_unanswered_retry_same_request", "p03_float_seller_refused_after_payment", "p03_float_outcome_not_certain"].includes(code2) && /^[1-9][0-9]{0,30}$/.test(String(facts.paidAtomic ?? ""))) {
145851
- state.unsure.set(turn, BigInt(facts.paidAtomic));
145852
- } else state.unsure.delete(turn);
145853
- if (floatSpent(code2, facts)) state.empty += 1;
145980
+ const keptAmount = () => {
145981
+ try {
145982
+ const kept3 = keptPayment(session.openStore(), turn.requestId);
145983
+ return kept3?.committed === true && kept3.cancelled !== true && /^[1-9][0-9]{0,30}$/.test(String(kept3.amountAtomic ?? "")) ? BigInt(kept3.amountAtomic) : void 0;
145984
+ } catch {
145985
+ return typeof turn.capAtomic === "bigint" && turn.capAtomic > 0n ? turn.capAtomic : void 0;
145986
+ }
145987
+ };
145988
+ const amount = /^[1-9][0-9]{0,30}$/.test(String(facts.paidAtomic ?? "")) ? BigInt(facts.paidAtomic) : paidLeft ? leftAtomic : FLOAT_NOT_PAID.has(code2) ? void 0 : keptAmount();
145989
+ const paidForSure = floatSpent(code2, facts) || code2 === PAID_SELLER_FAILED_CODE;
145990
+ const spent = paidForSure && (amount !== void 0 || wasUnsure !== void 0);
145991
+ if (paidLeft?.left === true && !paidForSure && (FLOAT_NOT_PAID.has(code2) || NOTHING_PAID.test(`${float[1]} ${float[2]}`))) {
145992
+ const held3 = amount ?? leftAtomic ?? (typeof turn.capAtomic === "bigint" && turn.capAtomic > 0n ? turn.capAtomic : void 0);
145993
+ if (held3 !== void 0) state.unsure.set(turn, held3);
145994
+ state.pending = turn;
145995
+ try {
145996
+ (typeof session.timing === "function" ? session.timing() : void 0)?.("chat-answer-failed", code2);
145997
+ } catch {
145998
+ }
145999
+ say3(`The answer did not come back, and the wallet cannot prove this payment${held3 !== void 0 ? ` (${usdcWords(held3)})` : ""} stayed unpaid: it may be paid already. Type /retry to finish it (it never pays twice). This chat counts it until it is certain.`);
146000
+ return;
146001
+ }
146002
+ if (paidForSure) {
146003
+ state.unsure.delete(turn);
146004
+ if (spent) state.spent += amount ?? wasUnsure;
146005
+ } else if (FLOAT_NOT_PAID.has(code2)) state.unsure.delete(turn);
146006
+ else if (amount !== void 0) state.unsure.set(turn, amount);
146007
+ if (paidForSure) state.empty += 1;
145854
146008
  if (floatRetry(code2, facts)) state.pending = turn;
145855
146009
  try {
145856
146010
  const mark = typeof session.timing === "function" ? session.timing() : void 0;
@@ -145863,17 +146017,33 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
145863
146017
  }
145864
146018
  let prepared;
145865
146019
  let neverLeft = false;
145866
- if (!surelyNothing) {
146020
+ let keptAtomic;
146021
+ if (paidLeft) {
146022
+ if (paidLeft.left) keptAtomic = leftAtomic ?? (typeof turn.capAtomic === "bigint" && turn.capAtomic > 0n ? turn.capAtomic : void 0);
146023
+ else {
146024
+ let kept3;
146025
+ try {
146026
+ kept3 = keptPayment(session.openStore(), turn.requestId);
146027
+ } catch {
146028
+ kept3 = void 0;
146029
+ }
146030
+ if (kept3?.prepared === true) prepared = /^[1-9][0-9]{0,30}$/.test(String(kept3.amountAtomic ?? "")) ? ` of ${usdcWords(BigInt(kept3.amountAtomic))}` : "";
146031
+ else neverLeft = true;
146032
+ }
146033
+ } else if (!surelyNothing) {
145867
146034
  try {
145868
146035
  const kept3 = keptPayment(session.openStore(), turn.requestId);
146036
+ if (/^[1-9][0-9]{0,30}$/.test(String(kept3?.amountAtomic ?? ""))) keptAtomic = BigInt(kept3.amountAtomic);
145869
146037
  if (kept3 === void 0 || kept3.committed !== true || kept3.cancelled === true) neverLeft = true;
145870
- else if (kept3.prepared === true) prepared = /^[1-9][0-9]{0,30}$/.test(String(kept3.amountAtomic ?? "")) ? ` of ${usdcWords(BigInt(kept3.amountAtomic))}` : "";
146038
+ else if (kept3.prepared === true) prepared = keptAtomic !== void 0 ? ` of ${usdcWords(keptAtomic)}` : "";
145871
146039
  } catch {
145872
146040
  prepared = void 0;
145873
146041
  neverLeft = false;
146042
+ if (typeof turn.capAtomic === "bigint" && turn.capAtomic > 0n) keptAtomic = turn.capAtomic;
145874
146043
  }
145875
146044
  }
145876
- const nothing = surelyNothing || neverLeft;
146045
+ const nothing = paidLeft ? neverLeft : surelyNothing || neverLeft;
146046
+ if (!nothing && prepared === void 0 && keptAtomic !== void 0) state.unsure.set(turn, keptAtomic);
145877
146047
  const again = nothing ? "ask again" : prepared !== void 0 ? "type /retry (it sends that same payment, never a second one)" : "type /retry (it finishes the same payment and never pays twice)";
145878
146048
  if (!nothing) state.pending = turn;
145879
146049
  const paidWords = nothing ? "Nothing was paid." : prepared !== void 0 ? `Nothing was paid: a payment${prepared} is prepared for this question and not sent yet.` : "It may be paid already.";
@@ -145915,8 +146085,10 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
145915
146085
  const command = async (text3) => {
145916
146086
  const [name, ...rest] = text3.split(/\s+/);
145917
146087
  const arg = rest.join(" ").trim();
145918
- if (name === "/exit" || name === "/quit" || name === "/q") return true;
146088
+ if (name === "/exit" || name === "/quit" || name === "/q") return !warnPaidPending(`${name} again ends the chat and drops it`);
145919
146089
  if (name === "/new") {
146090
+ if (warnPaidPending("/new again starts a new conversation and drops it")) return false;
146091
+ state.epoch += 1;
145920
146092
  state.history = [];
145921
146093
  state.pending = void 0;
145922
146094
  forgetMemoryOnly();
@@ -145930,10 +146102,10 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
145930
146102
  if (!next) say3("Type /mode 1 for one question at a time, or /mode 2 for a conversation.");
145931
146103
  else {
145932
146104
  state.mode = next;
146105
+ state.epoch += 1;
145933
146106
  state.history = [];
145934
- state.pending = void 0;
145935
- forgetMemoryOnly();
145936
- say3(next === "each" ? "Mode now: one question at a time. Each question goes alone, and the chat names each payment." : "Mode now: a conversation. The AI remembers from your next question on, and the running total shows beside you.");
146107
+ if (!state.pending) forgetMemoryOnly();
146108
+ say3((next === "each" ? "Mode now: one question at a time. Each question goes alone, and the chat names each payment." : "Mode now: a conversation. The AI remembers from your next question on, and the running total shows beside you.") + (state.pending ? " Your unfinished answer is kept: /retry finishes it." : ""));
145937
146109
  }
145938
146110
  } else if (name === "/model" || name === "/provider") {
145939
146111
  const now = await servedNow();
@@ -146022,6 +146194,7 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
146022
146194
  if (text3.startsWith("/")) {
146023
146195
  if (await command(text3)) break;
146024
146196
  } else {
146197
+ if (warnPaidPending("type your new question again to drop it and ask the new one")) continue;
146025
146198
  if (state.pending) say3("(The unfinished answer is dropped; this is a new question.)");
146026
146199
  state.pending = void 0;
146027
146200
  const now = await servedNow();
@@ -146033,12 +146206,16 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
146033
146206
  requestId: `chat-${randomBytes29(8).toString("hex")}`,
146034
146207
  model: state.model,
146035
146208
  liveEntry: liveOf(state.model),
146209
+ epoch: state.epoch,
146036
146210
  messages: state.mode === "each" ? [{ role: "user", content: text3 }] : [...state.history, { role: "user", content: text3 }],
146037
146211
  capAtomic: void 0
146038
146212
  });
146039
146213
  }
146040
146214
  }
146041
146215
  }
146216
+ if (state.pending && state.unsure.has(state.pending)) {
146217
+ say3(`An answer you may have paid for (${usdcWords(state.unsure.get(state.pending))}) was not finished, and the chat keeps no copy of it. The wallet closes that payment by itself: if it never reached the AI provider, the money comes back to your private balance; if it did, your next private question tells you that it was paid.`);
146218
+ }
146042
146219
  say3([
146043
146220
  `Chat ended. It paid ${paidSoFar()} for ${questionsWords()}. The conversation is gone from memory.`,
146044
146221
  ...chatEndLines({ color, columns: process.stderr.columns ?? 0 })
@@ -146051,7 +146228,7 @@ When it says ready, type /retry: it asks the same question again.` : receivedOnl
146051
146228
  await session.close();
146052
146229
  }
146053
146230
  }
146054
- var SPLIT_CODE, DEFAULT_MODEL2, CHAT_DEFAULT_MODEL, CHAT_MAX_TOKENS, CHAT_SHORT_TOKENS, DEFAULT_ANSWER_CAP_ATOMIC, DEFAULT_CHAT_CAP_ATOMIC, CHEAPEST_MODEL, PICK_PAGE, PICK_MOST_BROWSED, PICK_MATCHES, PICK_COLUMNS, PICK_ID_COLUMNS, PICK_NAME_COLUMNS, STILL_WAITING_MS, higherCap, shortInChat, POSITIVE_ATOMIC9, SHORT_QUESTION, SHORT_QUESTION_TOKENS, PASTE_JOIN_MS, CAP_CODES, WAIT_CODES, NO_ANSWER_CODES, HISTORY_CODES, chatFloatTerms, floatRetry, floatSpent, asSentence, upFirst, lowFirst, NOTHING_PAID, CAP_FROM, PROGRESS_LINE, SHOWN_PROGRESS, isProgressLine, fail46, nothingPaid;
146231
+ var SPLIT_CODE, DEFAULT_MODEL2, CHAT_DEFAULT_MODEL, CHAT_MAX_TOKENS, CHAT_SHORT_TOKENS, DEFAULT_ANSWER_CAP_ATOMIC, DEFAULT_CHAT_CAP_ATOMIC, CHEAPEST_MODEL, PICK_PAGE, PICK_MOST_BROWSED, PICK_MATCHES, PICK_COLUMNS, PICK_ID_COLUMNS, PICK_NAME_COLUMNS, STILL_WAITING_MS, higherCap, shortInChat, POSITIVE_ATOMIC9, SHORT_QUESTION, SHORT_QUESTION_TOKENS, PASTE_JOIN_MS, CAP_CODES, WAIT_CODES, NO_ANSWER_CODES, HISTORY_CODES, chatFloatTerms, floatRetry, floatSpent, FLOAT_NOT_PAID, asSentence, upFirst, lowFirst, NOTHING_PAID, CAP_FROM, PROGRESS_LINE, SHOWN_PROGRESS, isProgressLine, fail46, nothingPaid;
146055
146232
  var init_chat = __esm({
146056
146233
  "wallet/src/inference/chat.mjs"() {
146057
146234
  "use strict";
@@ -146133,6 +146310,7 @@ var init_chat = __esm({
146133
146310
  });
146134
146311
  floatRetry = (code2, facts) => /_retry_same_request$|_retry_later$/.test(code2) || ["p03_float_already_paid", "p03_float_config_unreadable"].includes(code2) || code2 === "p03_float_request_refused" && facts.floatCode === "float_quote_in_progress" || code2 === "p03_float_seller_kept_payment" && facts.resent !== true && facts.pruned !== true && facts.httpStatus !== 402;
146135
146312
  floatSpent = (code2, facts) => code2 === "p03_float_receipt_pruned" || code2 === "p03_float_seller_kept_payment" && !floatRetry(code2, facts);
146313
+ FLOAT_NOT_PAID = /* @__PURE__ */ new Set(["p03_float_request_expired_nothing_paid"]);
146136
146314
  asSentence = (text3) => `${text3.charAt(0).toUpperCase()}${text3.slice(1).replace(/[.;\s]+$/, "")}.`;
146137
146315
  upFirst = (text3) => `${text3.charAt(0).toUpperCase()}${text3.slice(1)}`;
146138
146316
  lowFirst = (text3) => `${text3.charAt(0).toLowerCase()}${text3.slice(1)}`;
@@ -153380,7 +153558,7 @@ var INFER_MODEL_IDS = ["claude-opus-5", "deepseek-v4-1-flash", "qwen3-8-flash"];
153380
153558
  var INFER_EXAMPLE_TAIL = `--max-tokens 512 --max-payment 0.02 --max-session 0.1 ${PAYER_SESSION_BASE} --tor`;
153381
153559
  var INFER_COST_LINE = "The AI provider charges its quoted cap for --max-tokens a request (often far below 0.01 USDC; 0.01 USDC on its standard offer, so --max-payment must cover that) and keeps what the answer does not use (no refund of the rest); the wallet pays the AI provider's price plus Shinjuku's fee (Shinjuku pays the AI provider for you), never more than --max-payment a request; the AI provider and the company that runs the model see your prompt and the answer. The chain shows one private payment, not who paid";
153382
153560
  var INFER_STDIN_MAX_BYTES = 1024 * 1024;
153383
- var INFER_STDIN_EXAMPLE = `echo "Summarize x402 in one line" | ${CLI_WORD} infer - --model qwen3-8-flash`;
153561
+ var INFER_STDIN_EXAMPLE = `cat question.txt | ${CLI_WORD} infer -`;
153384
153562
  var CURL_USEPOD_NEXT = `use \`${CLI_WORD} infer\` for the AI provider: it sends the prompt only through --tor or --proxy`;
153385
153563
  function unshieldArgs(command, args) {
153386
153564
  if (args.includes("--amount")) return args;
@@ -153411,6 +153589,7 @@ function inferArgs(command, args) {
153411
153589
  index += 1;
153412
153590
  }
153413
153591
  }
153592
+ if (!out.includes("--prompt") && !out.includes("--help") && !out.includes("-h") && process.stdin.isTTY) out.push("--prompt", "-");
153414
153593
  return out;
153415
153594
  }
153416
153595
  function inferTorGate(env) {
@@ -153430,7 +153609,7 @@ function validateInfer(values, env) {
153430
153609
  }
153431
153610
  if (values.has("--max-tokens") && Number(values.get("--max-tokens")) < 1) throw new UsageError("--max-tokens must be at least 1 (give 256 or more, 512 answers a short question: a reasoning model like qwen3-8-flash thinks first, and with fewer tokens it can stop before it answers, still paid): it sets the most the request can cost");
153432
153611
  if (!values.get("--prompt").trim()) {
153433
- throw new UsageError(`the prompt is empty: pipe it in and write - in its place (private: not in the process list or shell history), like ${INFER_STDIN_EXAMPLE}`);
153612
+ throw new UsageError(`the prompt is empty: run ${CLI_WORD} infer alone in a terminal and type the question when it asks, or pipe it in from a file, like ${INFER_STDIN_EXAMPLE}`);
153434
153613
  }
153435
153614
  if (!values.has(WALLET_FROM_SETUP) && (values.has("--pool") || values.has("--wallet"))) inferTorGate(env);
153436
153615
  }
@@ -153466,6 +153645,22 @@ async function readStdinBytes({ maxBytes }) {
153466
153645
  async function inferPrompt(values, deps) {
153467
153646
  const given = values.get("--prompt");
153468
153647
  if (given !== "-") return given;
153648
+ if (!deps.readStdin && process.stdin.isTTY) {
153649
+ const asker = createInterface2({ input: process.stdin, output: process.stderr, terminal: true });
153650
+ let line;
153651
+ try {
153652
+ line = await new Promise((resolveLine) => {
153653
+ asker.once("close", () => resolveLine(null));
153654
+ asker.question("Type your question, then press Enter: ", resolveLine);
153655
+ });
153656
+ } finally {
153657
+ asker.close();
153658
+ }
153659
+ if (line === null || !String(line).trim()) {
153660
+ throw new UsageError(`no question was typed, so nothing was sent or paid. Run ${cli4()} infer again and type your question, or use ${cli4()} chat`);
153661
+ }
153662
+ return String(line);
153663
+ }
153469
153664
  const raw = await (deps.readStdin ?? readStdinBytes)({ maxBytes: INFER_STDIN_MAX_BYTES });
153470
153665
  const bytes2 = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw ?? []);
153471
153666
  if (bytes2.length > INFER_STDIN_MAX_BYTES) {
@@ -153625,10 +153820,13 @@ function inferOutput(result, { values, io, model: model2, quiet, requestId, held
153625
153820
  ` : `paid ${paidText}${quiet ? "" : ` request-id ${result.requestId ?? requestId}`}${stopped}
153626
153821
  `);
153627
153822
  }
153823
+ const empty = !String(result.content ?? "").trim();
153824
+ const emptyWords = result.finishReason === "length" ? `The model used all its tokens before its answer started, so no answer came back (${replayed || held ? "paid earlier" : `paid ${paidText}`}). Ask again with a higher --max-tokens, or use another model (${cli4()} infer models).` : `The model sent an empty answer (${replayed || held ? "paid earlier" : `paid ${paidText}`}). Ask again: that is a new question and pays again.`;
153628
153825
  if (values.has("--json")) {
153629
153826
  io.out(`${JSON.stringify(
153630
153827
  {
153631
- ok: true,
153828
+ ok: !empty,
153829
+ ...empty ? { error: "inference_empty_answer", message: emptyWords } : {},
153632
153830
  model: result.model ?? model2,
153633
153831
  content: result.content,
153634
153832
  finishReason: result.finishReason,
@@ -153643,10 +153841,16 @@ function inferOutput(result, { values, io, model: model2, quiet, requestId, held
153643
153841
  (key, value2) => typeof value2 === "bigint" ? value2.toString() : value2
153644
153842
  )}
153645
153843
  `);
153844
+ if (empty) throw new PrintedFailure("inference_empty_answer");
153646
153845
  return;
153647
153846
  }
153648
153847
  for (const line of paidNotices) io.err(`${line}
153649
153848
  `);
153849
+ if (empty) {
153850
+ io.err(`${emptyWords}${quiet ? "" : ` request-id ${result.requestId ?? requestId}`}
153851
+ `);
153852
+ throw new PrintedFailure("inference_empty_answer");
153853
+ }
153650
153854
  io.out(`${result.content ?? ""}
153651
153855
  `);
153652
153856
  if (plainPaid) {
@@ -154225,6 +154429,13 @@ var RanBehindTorFailed = class extends Error {
154225
154429
  this.exitCode = code2;
154226
154430
  }
154227
154431
  };
154432
+ var PrintedFailure = class extends Error {
154433
+ constructor(code2) {
154434
+ super(code2);
154435
+ this.code = code2;
154436
+ this.exitCode = 1;
154437
+ }
154438
+ };
154228
154439
  function chatBeforeValidate(values, deps, args) {
154229
154440
  values.set(CHAT_RAW_ARGS, args);
154230
154441
  if (values.has("--wallet") || values.has("--pool")) return void 0;
@@ -156392,7 +156603,7 @@ var COMMANDS = [
156392
156603
  summary: "Ask an AI model one question and pay for the answer from your private balance (Shielded Inference, through the AI provider's x402 route).",
156393
156604
  env: [],
156394
156605
  flags: [
156395
- flag2("--prompt", "<text>", `what to ask; write - to read it from stdin (the private form), or write it alone, in quotes (${CLI_WORD} infer "Summarize x402 in one line" ...)`, { required: true }),
156606
+ flag2("--prompt", "<text>", `what to ask; with no question in a terminal, infer asks for it (the words stay out of your shell history); write - to read it from stdin (a file or a script), or write it alone, in quotes (${CLI_WORD} infer "Summarize x402 in one line" ...)`, { required: true }),
156396
156607
  flag2("--model", "<id>", `the model (default ${DEFAULT_MODEL2}): ${INFER_MODEL_IDS.join(", ")} (${CLI_WORD} infer models lists their prices)`),
156397
156608
  flag2("--max-tokens", "<n>", "the longest answer to pay for, in tokens; default 4096, 512 for claude-opus-5 (give 256 or more; 512 answers a short question: a reasoning model like qwen3-8-flash thinks first, and with fewer tokens it can stop before it answers, still paid); it sets the most this request can cost", { check: "count" }),
156398
156609
  flag2("--max-payment", "<usdc>", `the most this request may pay in all, in USDC (0.02 = 2 cents). It must be at least 0.011: the wallet never sets a call's ceiling below the AI provider's 0.01 USDC floor on its standard offer, and a call also pays Shinjuku's 0.001 USDC fee, so 0.02 covers it. What a call pays is the AI provider's quote for --max-tokens plus Shinjuku's fee: with ${DEFAULT_MODEL2} and the default ${DEFAULT_MAX_TOKENS} tokens, a short question costs about ${usdcOr(DEFAULT_ANSWER_PAID_ATOMIC)}, at most 0.011 USDC: 0.01 USDC plus the fee when the AI provider offers only its standard payment. A higher ceiling is refused before anything is paid. Default: from your limits (setup or limits sets them), else 0.05`, { check: "atomic", usdc: true }),
@@ -156405,12 +156616,12 @@ var COMMANDS = [
156405
156616
  ],
156406
156617
  examples: [
156407
156618
  `${CLI_WORD} infer "Say hello in three words"`,
156408
- `echo "Summarize x402 in one line" | ${CLI_WORD} infer -`,
156409
- `echo "Write a haiku about tunnels" | ${CLI_WORD} infer - --model claude-opus-5 --json ${INFER_EXAMPLE_TAIL}`
156619
+ `${CLI_WORD} infer`,
156620
+ `cat question.txt | ${CLI_WORD} infer - --model claude-opus-5 --json ${INFER_EXAMPLE_TAIL}`
156410
156621
  ],
156411
156622
  notes: [
156412
156623
  `After setup it needs no flags, as chat: config.json gives the wallet, the model is ${DEFAULT_MODEL2}, the caps are from your limits (setup or limits sets them; else 0.05 USDC a call and 1 USDC a run), and it runs behind Tor by itself (Tor must run, as setup recorded it). A flag you give still wins. With --pool or --wallet the command line is the whole config: then add --tor (or --proxy <url>).`,
156413
- `Private form (recommended): pipe the prompt in and write - in its place, so it is not in the process list or your shell history: ${INFER_STDIN_EXAMPLE}. stdin is read until it ends (UTF-8, at most 1 MiB); an empty one is refused before anything is paid.`,
156624
+ `Private form (recommended): type ${CLI_WORD} infer alone in a terminal, and type the question when it asks: the words are not in the command line, the process list or your shell history. A question typed on the command line (in quotes) is saved in your shell history. For a file or a script, write - and pipe the question in: ${INFER_STDIN_EXAMPLE}; stdin is read until it ends (UTF-8, at most 1 MiB); an empty one is refused before anything is paid.`,
156414
156625
  "It runs only through Tor (or --proxy <url>), never a direct connection: the AI provider would see your IP address next to your prompt, so the command refuses before anything is sent.",
156415
156626
  `${INFER_COST_LINE}. stderr says this before the payment, with the most the request can cost.`,
156416
156627
  "stdout holds the answer; stderr says paid <amount> USDC request-id <id>. An answer cut short by --max-tokens says so. The wallet's own progress lines (Tor, the RPC relay, the pool history walk, the memo reads) show only with --verbose; warnings and failures always show.",
@@ -156435,7 +156646,7 @@ var COMMANDS = [
156435
156646
  flags: [bool2("--json", "print the list as one JSON line (prices in atomic USDC per 1M tokens)")],
156436
156647
  examples: [`${CLI_WORD} infer models`, `${CLI_WORD} infer models --json`],
156437
156648
  notes: [`${INFER_COST_LINE}.`],
156438
- next: `ask one: echo "Summarize x402 in one line" | ${CLI_WORD} infer - --model ${DEFAULT_MODEL2}`,
156649
+ next: `ask one: ${CLI_WORD} infer (it asks for the question), or chat: ${CLI_WORD} chat`,
156439
156650
  run: (args, values, deps, io) => deps.inferModelsMain(values, { ...deps, io })
156440
156651
  },
156441
156652
  {
@@ -159815,7 +160026,7 @@ function failureHint(command, reason, deps, values, floatError) {
159815
160026
  }
159816
160027
  const floatCode = /\b(p03_float_[a-z_]+|wallet_quote_refused_[a-z0-9_]+|p03_resource_paid_seller_failed)\b/.exec(reason)?.[1];
159817
160028
  const facts = REQUEST_ID_TAKEN2.test(reason) ? { ...floatError, ...conflictRecordFacts(reason, values, deps) } : floatError;
159818
- const float = floatCode ? floatOutcomeWords(floatCode, facts, CLI_FLOAT_TERMS(values, deps, command)) : void 0;
160029
+ const float = floatCode ? floatOutcomeWords(floatCode, command?.name === "infer" ? withoutProviderText(facts) : facts, CLI_FLOAT_TERMS(values, deps, command)) : void 0;
159819
160030
  if (float) return `${float[1].replace(/[.;\s]+$/, "")}. ${float[2]}`;
159820
160031
  const pocketHint = pocketFailureHint(reason, command);
159821
160032
  if (pocketHint) return pocketHint;
@@ -160643,12 +160854,14 @@ This copy: ${walletVersionLine(deps.walletVersion())}
160643
160854
  }
160644
160855
  if (isFamily(name)) {
160645
160856
  if (INTERNAL_FAMILIES.has(name) && args.length === 0) throw new UsageError("Unknown command.");
160646
- if (args.length === 0) {
160857
+ const aloneInTerminal = args.length === 0 && COMMAND_BY_NAME.has(name) && process.stdin.isTTY === true;
160858
+ if (args.length === 0 && !aloneInTerminal) {
160647
160859
  io.out(familyOverviewText(name));
160648
160860
  return 0;
160649
160861
  }
160650
160862
  const three = `${name} ${args[0]} ${args[1]}`;
160651
- if (COMMAND_BY_NAME.has(name) && !COMMAND_BY_NAME.has(`${name} ${args[0]}`) && !COMMAND_BY_NAME.has(three)) command = COMMAND_BY_NAME.get(name);
160863
+ if (aloneInTerminal) command = COMMAND_BY_NAME.get(name);
160864
+ else if (COMMAND_BY_NAME.has(name) && !COMMAND_BY_NAME.has(`${name} ${args[0]}`) && !COMMAND_BY_NAME.has(three)) command = COMMAND_BY_NAME.get(name);
160652
160865
  else command = COMMAND_BY_NAME.has(three) ? (args.splice(0, 2), COMMAND_BY_NAME.get(three)) : COMMAND_BY_NAME.get(`${name} ${args.shift()}`);
160653
160866
  if (!command && INTERNAL_FAMILIES.has(name)) throw new UsageError("Unknown command.");
160654
160867
  if (!command) {
@@ -160724,6 +160937,10 @@ This copy: ${walletVersionLine(deps.walletVersion())}
160724
160937
  if (endsProcess) showProgressAgain = () => {
160725
160938
  };
160726
160939
  if (error instanceof RanBehindTorFailed) return error.exitCode;
160940
+ if (error instanceof PrintedFailure) {
160941
+ writeRunResult(deps, command, error.exitCode, error);
160942
+ return error.exitCode;
160943
+ }
160727
160944
  if (error instanceof UsageError) {
160728
160945
  let setUp = false;
160729
160946
  try {
@@ -160843,7 +161060,7 @@ Next: fund that address, then rerun the same command; it continues where it stop
160843
161060
  const blockingLine = typeof blocking === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(blocking) ? `earlier payment: --request-id ${blocking}
160844
161061
  ` : "";
160845
161062
  const payee = command.name === "infer" ? "the AI provider" : "the seller";
160846
- const sellerSaid = error?.sellerReason ?? error?.cause?.sellerReason;
161063
+ const sellerSaid = command.name === "infer" ? void 0 : error?.sellerReason ?? error?.cause?.sellerReason;
160847
161064
  const sellerLine = typeof sellerSaid === "string" && sellerSaid ? `${payee === "the seller" ? "seller" : payee} answered HTTP ${error?.httpStatus ?? error?.cause?.httpStatus}: ${JSON.stringify(sellerSaid)}
160848
161065
  ` : "";
160849
161066
  const landedTx = error?.transaction ?? error?.cause?.transaction;