indelible-mcp 5.7.8 → 5.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/LICENSE +1 -1
  2. package/package.json +1 -1
  3. package/src/index.js +335 -53
package/LICENSE CHANGED
@@ -15,7 +15,7 @@ Additional Use Grant: You may make use of the Licensed Work for personal,
15
15
  commercial blockchain storage, session saving, or
16
16
  encrypted vault service without written permission
17
17
  from the Licensor.
18
- Change Date: August 14, 2030
18
+ Change Date: August 18, 2030
19
19
  Change License: MIT License
20
20
 
21
21
  Terms
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indelible-mcp",
3
- "version": "5.7.8",
3
+ "version": "5.8.0",
4
4
  "description": "Blockchain-backed memory and code storage for Claude Code. Save AI conversations and source code permanently on BSV.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/index.js CHANGED
@@ -744,6 +744,52 @@ var init_utxo_cache = __esm({
744
744
  }
745
745
  });
746
746
 
747
+ // mcp-server/lib/tx-size.js
748
+ function sdkCompatVarIntSize(i) {
749
+ if (i > 2 ** 32) return 9;
750
+ if (i > 2 ** 16) return 5;
751
+ if (i > 253) return 3;
752
+ return 1;
753
+ }
754
+ function pushDataHeaderSize(n) {
755
+ if (n <= 75) return 1;
756
+ if (n <= 255) return 2;
757
+ if (n <= 65535) return 3;
758
+ return 5;
759
+ }
760
+ function txBytes(inputs, { dataBytes = 0, prefixBytes = SAVE_PREFIX_BYTES, p2pkhOuts = 0 } = {}) {
761
+ const k = Math.max(0, Math.floor(inputs));
762
+ let outCount = 1 + p2pkhOuts;
763
+ let outBytes = 8 + 1 + 25;
764
+ outBytes += p2pkhOuts * (8 + 1 + 25);
765
+ if (dataBytes > 0) {
766
+ outCount += 1;
767
+ const script = 1 + 1 + pushDataHeaderSize(prefixBytes) + prefixBytes + pushDataHeaderSize(dataBytes) + dataBytes;
768
+ outBytes += 8 + sdkCompatVarIntSize(script) + script;
769
+ }
770
+ return 4 + sdkCompatVarIntSize(k) + k * INPUT_BYTES + sdkCompatVarIntSize(outCount) + outBytes + 4;
771
+ }
772
+ function feeForInputs(inputs, shape = {}) {
773
+ return Math.ceil(txBytes(inputs, shape) / 1e3 * FEE_PER_KB);
774
+ }
775
+ function fragmentationBudget(shape = {}) {
776
+ return Math.max(FRAG_BUDGET_FLOOR_SATS, Math.ceil(FRAG_BUDGET_RATIO * feeForInputs(1, shape)));
777
+ }
778
+ var INPUT_BYTES, FEE_PER_KB, MIN_SPENDABLE_SATS, MIN_RECORDED_CHANGE, SAVE_PREFIX_BYTES, FRAG_BUDGET_FLOOR_SATS, FRAG_BUDGET_RATIO, MAX_TX_BYTES, MAX_INPUTS_BY_SIGN_BUDGET;
779
+ var init_tx_size = __esm({
780
+ "mcp-server/lib/tx-size.js"() {
781
+ INPUT_BYTES = 149;
782
+ FEE_PER_KB = 150;
783
+ MIN_SPENDABLE_SATS = 24;
784
+ MIN_RECORDED_CHANGE = 547;
785
+ SAVE_PREFIX_BYTES = 19;
786
+ FRAG_BUDGET_FLOOR_SATS = 1e3;
787
+ FRAG_BUDGET_RATIO = 1;
788
+ MAX_TX_BYTES = 12 * 1024 * 1024 - 512;
789
+ MAX_INPUTS_BY_SIGN_BUDGET = 3e3;
790
+ }
791
+ });
792
+
747
793
  // mcp-server/lib/spent-by.js
748
794
  var spent_by_exports = {};
749
795
  __export(spent_by_exports, {
@@ -807,6 +853,7 @@ var utxo_reservation_exports = {};
807
853
  __export(utxo_reservation_exports, {
808
854
  CONFLICT_RE: () => CONFLICT_RE,
809
855
  TX_KNOWN_RE: () => TX_KNOWN_RE,
856
+ claimRefusalMessage: () => claimRefusalMessage,
810
857
  claimSpendableUtxos: () => claimSpendableUtxos,
811
858
  linkOrRefuse: () => linkOrRefuse,
812
859
  linkReservation: () => linkReservation,
@@ -814,6 +861,7 @@ __export(utxo_reservation_exports, {
814
861
  probeTxVerdict: () => probeTxVerdict,
815
862
  reconcileHooks: () => reconcileHooks,
816
863
  reconcileReservations: () => reconcileReservations,
864
+ selectInputs: () => selectInputs,
817
865
  settleFromWriteResult: () => settleFromWriteResult,
818
866
  settleReservation: () => settleReservation
819
867
  });
@@ -911,6 +959,82 @@ function reservedInputSet(store, { exceptToken } = {}) {
911
959
  }
912
960
  return s;
913
961
  }
962
+ function claimRefusalMessage(claim2) {
963
+ const r = claim2 && claim2.reason;
964
+ if (r === "UNECONOMIC_FRAGMENTATION") {
965
+ return `This save is blocked to protect your money: your wallet holds enough (${claim2.available} sats), but it is spread across so many small coins that the transaction fee would be ${claim2.feeTotal} sats - ${claim2.fragSats} sats MORE than the ${claim2.baseline} sats this save would cost from a single coin (limit: ${claim2.budgetSats} sats extra). Do NOT add funds - you already have them, and adding more will not help. This needs a consolidation step the software does not offer yet; contact support at indeliblebsv@gmail.com and mention UNECONOMIC_FRAGMENTATION. Your data is not lost - nothing was broadcast and nothing was spent.`;
966
+ }
967
+ if (r === "TOO_MANY_INPUTS") {
968
+ return `This save cannot be funded in one transaction: it would need ${claim2.inputsNeeded} coins, above the ${claim2.cap}-input signing limit. Nothing was spent. Contact support at indeliblebsv@gmail.com and mention TOO_MANY_INPUTS.`;
969
+ }
970
+ if (r === "TX_TOO_LARGE") {
971
+ return `This save would produce a ${claim2.txBytes.toLocaleString()}-byte transaction, above the ${claim2.maxTxBytes.toLocaleString()}-byte network envelope - too large to broadcast. Nothing was spent. Split the content into smaller saves.`;
972
+ }
973
+ if (r === "NO_ECONOMIC_COINS") {
974
+ return `Your wallet holds ${claim2.dust && claim2.dust.sats ? claim2.dust.sats : 0} sats, but only in coins so small that each costs more in fees (~23 sats) than it adds. These cannot fund a save. Add funds in ONE normal-sized payment - the small coins remain yours, but stay unusable until a consolidation feature exists.`;
975
+ }
976
+ if (r === "INSUFFICIENT_FUNDS") {
977
+ const held = claim2.reservedSats > 0 ? ` (${claim2.reservedSats} more sats are temporarily reserved by another agent on this box and free up shortly)` : "";
978
+ return `Not enough funds: this save needs about ${claim2.need} sats and the wallet holds ${claim2.available} spendable${held}. Fund your wallet - a single payment of at least ${claim2.need} sats always suffices.`;
979
+ }
980
+ if (r === "NO_COINS") return "No UTXOs available. Fund your wallet.";
981
+ return "No UTXOs available. Fund your wallet.";
982
+ }
983
+ function selectInputs(free, spend = {}) {
984
+ const {
985
+ dataBytes = 0,
986
+ prefixBytes,
987
+ p2pkhOuts = 0,
988
+ extraOutSats = 0,
989
+ allowFragmentationCost = false,
990
+ maxInputs = MAX_INPUTS_BY_SIGN_BUDGET,
991
+ maxTxBytes = MAX_TX_BYTES
992
+ } = spend;
993
+ const shape = { dataBytes, p2pkhOuts, ...prefixBytes !== void 0 ? { prefixBytes } : {} };
994
+ const raw = (free || []).filter((u) => Number.isFinite(u?.value) && u.value > 0);
995
+ const coins = raw.filter((u) => u.value >= MIN_SPENDABLE_SATS);
996
+ const rawSats = raw.reduce((n, u) => n + u.value, 0);
997
+ const available = coins.reduce((n, u) => n + u.value, 0);
998
+ const dust = { count: raw.length - coins.length, sats: rawSats - available };
999
+ const target = (k) => feeForInputs(k, shape) + extraOutSats + MIN_RECORDED_CHANGE;
1000
+ if (raw.length === 0) return { ok: false, reason: "NO_COINS", need: target(1), available: 0, dust };
1001
+ if (coins.length === 0) return { ok: false, reason: "NO_ECONOMIC_COINS", need: target(1), available: 0, dust };
1002
+ if (txBytes(1, shape) > maxTxBytes) {
1003
+ return { ok: false, reason: "TX_TOO_LARGE", txBytes: txBytes(1, shape), maxTxBytes, available, dust };
1004
+ }
1005
+ const ascending = [...coins].sort((a, b) => a.value - b.value);
1006
+ const single = ascending.find((u) => u.value >= target(1));
1007
+ if (single) return { ok: true, inputs: [single], fee: feeForInputs(1, shape), fragSats: 0, dust };
1008
+ const descending = [...coins].sort((a, b) => b.value - a.value);
1009
+ const picked = [];
1010
+ let total = 0;
1011
+ let kMin = 0;
1012
+ for (const u of descending) {
1013
+ picked.push(u);
1014
+ total += u.value;
1015
+ if (total >= target(picked.length)) {
1016
+ kMin = picked.length;
1017
+ break;
1018
+ }
1019
+ }
1020
+ if (!kMin) {
1021
+ return { ok: false, reason: "INSUFFICIENT_FUNDS", need: target(1), available, dust };
1022
+ }
1023
+ if (txBytes(kMin, shape) > maxTxBytes) {
1024
+ return { ok: false, reason: "TX_TOO_LARGE", txBytes: txBytes(kMin, shape), maxTxBytes, available, dust };
1025
+ }
1026
+ if (kMin > maxInputs) {
1027
+ return { ok: false, reason: "TOO_MANY_INPUTS", inputsNeeded: kMin, cap: maxInputs, available, dust };
1028
+ }
1029
+ const baseline = feeForInputs(1, shape);
1030
+ const feeTotal = feeForInputs(kMin, shape);
1031
+ const fragSats = feeTotal - baseline;
1032
+ const budgetSats = fragmentationBudget(shape);
1033
+ if (fragSats > budgetSats && !allowFragmentationCost) {
1034
+ return { ok: false, reason: "UNECONOMIC_FRAGMENTATION", fragSats, budgetSats, feeTotal, baseline, inputsNeeded: kMin, available, dust };
1035
+ }
1036
+ return { ok: true, inputs: picked, fee: feeTotal, fragSats, dust };
1037
+ }
914
1038
  async function claimSpendableUtxos(address, bridgeFallback, opts = {}) {
915
1039
  const { waitMs = DEFAULT_WAIT_MS, pollMs = DEFAULT_POLL_MS, leaseMs = DEFAULT_LEASE_MS, now = Date.now } = opts;
916
1040
  const deadline = now() + waitMs;
@@ -933,12 +1057,20 @@ async function claimSpendableUtxos(address, bridgeFallback, opts = {}) {
933
1057
  if (free.length === 0) {
934
1058
  return { busy: (candidates || []).length > 0 };
935
1059
  }
1060
+ const selected = selectInputs(free, opts.spend);
1061
+ if (!selected.ok) {
1062
+ const wholeWallet = selectInputs(candidates || [], opts.spend);
1063
+ if (wholeWallet.ok) return { busy: true };
1064
+ const { ok: _ok, ...evidence } = selected;
1065
+ const reservedSats = (candidates || []).filter((u) => reserved.has(inputKey(u))).reduce((s, u) => s + (u?.value || 0), 0);
1066
+ return { refused: true, ...evidence, reservedSats };
1067
+ }
936
1068
  const token = randomUUID();
937
1069
  store.reservations[token] = {
938
1070
  owner_token: token,
939
1071
  pid: process.pid,
940
1072
  address,
941
- inputs: free,
1073
+ inputs: selected.inputs,
942
1074
  claimed_at: now(),
943
1075
  lease_until: now() + leaseMs,
944
1076
  build_cache_key: null,
@@ -946,9 +1078,10 @@ async function claimSpendableUtxos(address, bridgeFallback, opts = {}) {
946
1078
  unknown_cycles: 0
947
1079
  };
948
1080
  writeReservations(store);
949
- return { token, utxos: free };
1081
+ return { token, utxos: selected.inputs };
950
1082
  }, LOCK_OPTS);
951
1083
  if (claimed.token) return claimed;
1084
+ if (claimed.refused) return { token: null, utxos: [], ...claimed };
952
1085
  if (!claimed.busy) return { token: null, utxos: [] };
953
1086
  if (!_saidWaiting) {
954
1087
  _saidWaiting = true;
@@ -1200,6 +1333,7 @@ var init_utxo_reservation = __esm({
1200
1333
  "mcp-server/lib/utxo-reservation.js"() {
1201
1334
  init_file_lock();
1202
1335
  init_utxo_cache();
1336
+ init_tx_size();
1203
1337
  CACHE_PATH2 = process.env.INDELIBLE_UTXO_CACHE || join6(homedir5(), ".indelible", "utxo-cache.json");
1204
1338
  RES_PATH = `${CACHE_PATH2}.reservations.json`;
1205
1339
  LOCK_PATH = `${CACHE_PATH2}.g401.lock`;
@@ -1233,6 +1367,7 @@ __export(spv_exports, {
1233
1367
  checkConfirmation: () => checkConfirmation,
1234
1368
  checkHealth: () => checkHealth,
1235
1369
  extractEncryptedFromTx: () => extractEncryptedFromTx,
1370
+ fetchSourceTransactions: () => fetchSourceTransactions,
1236
1371
  getAddressHistory: () => getAddressHistory,
1237
1372
  getBridges: () => getBridges,
1238
1373
  getRawTx: () => getRawTx,
@@ -1435,6 +1570,26 @@ async function getRawTx(txid) {
1435
1570
  `);
1436
1571
  return text;
1437
1572
  }
1573
+ async function fetchSourceTransactions(utxos, fetchRaw = getRawTx) {
1574
+ const wanted = [...new Set((utxos || []).map((u) => String(u.tx_hash).trim()))];
1575
+ const byTxid = /* @__PURE__ */ new Map();
1576
+ const t0 = Date.now();
1577
+ let cursor = 0;
1578
+ const worker = async () => {
1579
+ for (; ; ) {
1580
+ const i = cursor++;
1581
+ if (i >= wanted.length) return;
1582
+ const txid = wanted[i];
1583
+ byTxid.set(txid, Transaction.fromHex(await fetchRaw(txid)));
1584
+ }
1585
+ };
1586
+ await Promise.all(Array.from({ length: Math.min(SOURCE_FETCH_CONCURRENCY, wanted.length) }, worker));
1587
+ if ((utxos || []).length > 1) {
1588
+ process.stderr.write(`[MCP] sources: ${wanted.length} distinct for ${utxos.length} inputs (${Date.now() - t0}ms)
1589
+ `);
1590
+ }
1591
+ return byTxid;
1592
+ }
1438
1593
  function bcastTimeoutMs(rawTxHex) {
1439
1594
  const txMB = (rawTxHex?.length || 0) / 2 / 1048576;
1440
1595
  return Math.min(18e4, Math.round(15e3 + txMB * 12e3));
@@ -1702,9 +1857,10 @@ async function buildOpReturnTx(wif, utxos, dataStr) {
1702
1857
  const tx = new Transaction();
1703
1858
  const p2pkh = new P2PKH();
1704
1859
  const lockingScript = p2pkh.lock(address);
1860
+ const sourceByTxid = await fetchSourceTransactions(utxos);
1705
1861
  for (const utxo of utxos) {
1706
- const rawHex = await getRawTx(utxo.tx_hash);
1707
- const sourceTransaction = Transaction.fromHex(rawHex);
1862
+ const sourceTransaction = sourceByTxid.get(String(utxo.tx_hash).trim());
1863
+ if (!sourceTransaction) throw new Error(`source transaction unavailable for input ${utxo.tx_hash}:${utxo.tx_pos}`);
1708
1864
  tx.addInput({
1709
1865
  sourceTransaction,
1710
1866
  sourceOutputIndex: utxo.tx_pos,
@@ -1734,6 +1890,29 @@ async function buildOpReturnTx(wif, utxos, dataStr) {
1734
1890
  change: true
1735
1891
  });
1736
1892
  await tx.fee(new SatoshisPerKilobyte(150));
1893
+ {
1894
+ const _ci = tx.outputs.findIndex((o) => o.change);
1895
+ if (_ci >= 0) {
1896
+ const _cv = tx.outputs[_ci].satoshis || 0;
1897
+ if (_cv > 0 && _cv <= 546) tx.outputs.splice(_ci, 1);
1898
+ }
1899
+ }
1900
+ {
1901
+ if (tx.inputs.length === 0) {
1902
+ const e = new Error("ZERO_INPUT_BUILD: refusing to sign a transaction with no inputs \u2014 the caller handed an empty coin set");
1903
+ e.code = "ZERO_INPUT_BUILD";
1904
+ throw e;
1905
+ }
1906
+ const _totalIn = (utxos || []).reduce((s, u) => s + (u?.value || 0), 0);
1907
+ const _outSum = tx.outputs.reduce((s, o) => s + (o.satoshis || 0), 0);
1908
+ const _residual = _totalIn - _outSum;
1909
+ const _model = await new SatoshisPerKilobyte(150).computeFee(tx);
1910
+ if (_residual < _model) {
1911
+ const e = new Error(`UNDERFUNDED_BUILD: inputs ${_totalIn} sats cannot cover outputs ${_outSum} + fee ${_model} \u2014 refusing to sign an under-paying transaction`);
1912
+ e.code = "UNDERFUNDED_BUILD";
1913
+ throw e;
1914
+ }
1915
+ }
1737
1916
  await tx.sign();
1738
1917
  return {
1739
1918
  txHex: tx.toHex(),
@@ -1747,9 +1926,10 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
1747
1926
  const p2pkh = new P2PKH();
1748
1927
  const lockingScript = p2pkh.lock(address);
1749
1928
  let totalInput = 0;
1929
+ const sourceByTxid = await fetchSourceTransactions(utxos);
1750
1930
  for (const utxo of utxos) {
1751
- const rawHex = await getRawTx(utxo.tx_hash);
1752
- const sourceTransaction = Transaction.fromHex(rawHex);
1931
+ const sourceTransaction = sourceByTxid.get(String(utxo.tx_hash).trim());
1932
+ if (!sourceTransaction) throw new Error(`source transaction unavailable for input ${utxo.tx_hash}:${utxo.tx_pos}`);
1753
1933
  tx.addInput({
1754
1934
  sourceTransaction,
1755
1935
  sourceOutputIndex: utxo.tx_pos,
@@ -1780,6 +1960,29 @@ async function buildOpReturnTxWithChange(wif, utxos, dataStr) {
1780
1960
  change: true
1781
1961
  });
1782
1962
  await tx.fee(new SatoshisPerKilobyte(150));
1963
+ {
1964
+ const _ci = tx.outputs.findIndex((o) => o.change);
1965
+ if (_ci >= 0) {
1966
+ const _cv = tx.outputs[_ci].satoshis || 0;
1967
+ if (_cv > 0 && _cv <= 546) tx.outputs.splice(_ci, 1);
1968
+ }
1969
+ }
1970
+ {
1971
+ if (tx.inputs.length === 0) {
1972
+ const e = new Error("ZERO_INPUT_BUILD: refusing to sign a transaction with no inputs \u2014 the caller handed an empty coin set");
1973
+ e.code = "ZERO_INPUT_BUILD";
1974
+ throw e;
1975
+ }
1976
+ const _totalIn = (utxos || []).reduce((s, u) => s + (u?.value || 0), 0);
1977
+ const _outSum = tx.outputs.reduce((s, o) => s + (o.satoshis || 0), 0);
1978
+ const _residual = _totalIn - _outSum;
1979
+ const _model = await new SatoshisPerKilobyte(150).computeFee(tx);
1980
+ if (_residual < _model) {
1981
+ const e = new Error(`UNDERFUNDED_BUILD: inputs ${_totalIn} sats cannot cover outputs ${_outSum} + fee ${_model} \u2014 refusing to sign an under-paying transaction`);
1982
+ e.code = "UNDERFUNDED_BUILD";
1983
+ throw e;
1984
+ }
1985
+ }
1783
1986
  await tx.sign();
1784
1987
  const txId = tx.id("hex");
1785
1988
  const txHex = tx.toHex();
@@ -1805,9 +2008,10 @@ async function buildPaymentTx(wif, utxos, payToAddress, satoshis) {
1805
2008
  const p2pkh = new P2PKH();
1806
2009
  const lockingScript = p2pkh.lock(address);
1807
2010
  let totalInput = 0;
2011
+ const sourceByTxid = await fetchSourceTransactions(utxos);
1808
2012
  for (const utxo of utxos) {
1809
- const rawHex = await getRawTx(utxo.tx_hash);
1810
- const sourceTransaction = Transaction.fromHex(rawHex);
2013
+ const sourceTransaction = sourceByTxid.get(String(utxo.tx_hash).trim());
2014
+ if (!sourceTransaction) throw new Error(`source transaction unavailable for input ${utxo.tx_hash}:${utxo.tx_pos}`);
1811
2015
  tx.addInput({
1812
2016
  sourceTransaction,
1813
2017
  sourceOutputIndex: utxo.tx_pos,
@@ -1830,6 +2034,29 @@ async function buildPaymentTx(wif, utxos, payToAddress, satoshis) {
1830
2034
  change: true
1831
2035
  });
1832
2036
  await tx.fee(new SatoshisPerKilobyte(150));
2037
+ {
2038
+ const _ci = tx.outputs.findIndex((o) => o.change);
2039
+ if (_ci >= 0) {
2040
+ const _cv = tx.outputs[_ci].satoshis || 0;
2041
+ if (_cv > 0 && _cv <= 546) tx.outputs.splice(_ci, 1);
2042
+ }
2043
+ }
2044
+ {
2045
+ if (tx.inputs.length === 0) {
2046
+ const e = new Error("ZERO_INPUT_BUILD: refusing to sign a transaction with no inputs \u2014 the caller handed an empty coin set");
2047
+ e.code = "ZERO_INPUT_BUILD";
2048
+ throw e;
2049
+ }
2050
+ const _totalIn = (utxos || []).reduce((s, u) => s + (u?.value || 0), 0);
2051
+ const _outSum = tx.outputs.reduce((s, o) => s + (o.satoshis || 0), 0);
2052
+ const _residual = _totalIn - _outSum;
2053
+ const _model = await new SatoshisPerKilobyte(150).computeFee(tx);
2054
+ if (_residual < _model) {
2055
+ const e = new Error(`UNDERFUNDED_BUILD: inputs ${_totalIn} sats cannot cover outputs ${_outSum} + fee ${_model} \u2014 refusing to sign an under-paying transaction`);
2056
+ e.code = "UNDERFUNDED_BUILD";
2057
+ throw e;
2058
+ }
2059
+ }
1833
2060
  await tx.sign();
1834
2061
  const txId = tx.id("hex");
1835
2062
  const txHex = tx.toHex();
@@ -1857,7 +2084,7 @@ function extractEncryptedFromTx(tx) {
1857
2084
  const match = str.match(/([A-Za-z0-9+/=]{12,}):([A-Za-z0-9+/=]{20,}):([A-Za-z0-9+/=]{20,})/);
1858
2085
  return match ? match[0] : null;
1859
2086
  }
1860
- var SEED_BRIDGES, OLD_BRIDGE_IPS, bridgeHealth, HEALTH_CHECK_INTERVAL, MAX_FAILURES, migrationDone, ARC_NETWORK_STATUSES, StoredError, _stickyBridge, STICKY_WINDOW_MS;
2087
+ var SEED_BRIDGES, OLD_BRIDGE_IPS, bridgeHealth, HEALTH_CHECK_INTERVAL, MAX_FAILURES, migrationDone, SOURCE_FETCH_CONCURRENCY, ARC_NETWORK_STATUSES, StoredError, _stickyBridge, STICKY_WINDOW_MS;
1861
2088
  var init_spv = __esm({
1862
2089
  "mcp-server/lib/spv.js"() {
1863
2090
  init_config_customer();
@@ -1884,6 +2111,7 @@ var init_spv = __esm({
1884
2111
  HEALTH_CHECK_INTERVAL = 6e4;
1885
2112
  MAX_FAILURES = 3;
1886
2113
  migrationDone = false;
2114
+ SOURCE_FETCH_CONCURRENCY = 8;
1887
2115
  ARC_NETWORK_STATUSES = /* @__PURE__ */ new Set([
1888
2116
  // Tightened 2026-06-17 to match the relay-federation bridge: a node must have
1889
2117
  // TAKEN the tx — dropped ANNOUNCED_TO_NETWORK/REQUESTED_BY_NETWORK (ARC merely
@@ -2392,17 +2620,19 @@ async function commitSession(session, wif) {
2392
2620
  const apiKey = config2?.api_key || null;
2393
2621
  await checkTier(apiKey);
2394
2622
  const RECONCILE_HOOKS = { checkConfirmation: (id) => checkConfirmation(id), broadcast: (hex) => broadcastTx(hex), readEntry };
2623
+ const payload = {
2624
+ protocol: "indelible.claude-code",
2625
+ encrypted: session.encrypted,
2626
+ wrap_owner: session.wrap_owner || null
2627
+ };
2628
+ const payloadStr = JSON.stringify(payload);
2629
+ const SPEND = { dataBytes: Buffer.byteLength(payloadStr) };
2395
2630
  let claim2 = null;
2396
2631
  try {
2397
- claim2 = await claimSpendableUtxos(session.address, getUtxos, { reconcile: RECONCILE_HOOKS });
2632
+ claim2 = await claimSpendableUtxos(session.address, getUtxos, { reconcile: RECONCILE_HOOKS, spend: SPEND });
2398
2633
  const utxos = claim2.utxos;
2399
2634
  if (utxos && utxos.length > 0) {
2400
- const payload = {
2401
- protocol: "indelible.claude-code",
2402
- encrypted: session.encrypted,
2403
- wrap_owner: session.wrap_owner || null
2404
- };
2405
- let { txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload));
2635
+ let { txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, utxos, payloadStr);
2406
2636
  await linkOrRefuse(claim2.token, { txId, txHex, changeUtxos });
2407
2637
  let writeReceipt;
2408
2638
  try {
@@ -2410,10 +2640,10 @@ async function commitSession(session, wif) {
2410
2640
  } catch (bErr) {
2411
2641
  const settled = await settleFromWriteResult(claim2.token, { error: bErr, changeUtxos });
2412
2642
  if (settled.reason !== "conflict") throw bErr;
2413
- claim2 = await claimSpendableUtxos(session.address, getUtxos, { reconcile: RECONCILE_HOOKS });
2643
+ claim2 = await claimSpendableUtxos(session.address, getUtxos, { reconcile: RECONCILE_HOOKS, spend: SPEND });
2414
2644
  const fresh = claim2.utxos;
2415
2645
  if (!fresh || fresh.length === 0) throw bErr;
2416
- ({ txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, fresh, JSON.stringify(payload)));
2646
+ ({ txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, fresh, payloadStr));
2417
2647
  await linkOrRefuse(claim2.token, { txId, txHex, changeUtxos });
2418
2648
  try {
2419
2649
  writeReceipt = await broadcastTx(txHex);
@@ -2465,7 +2695,7 @@ async function commitSession(session, wif) {
2465
2695
  }
2466
2696
  if (claim2?.token) await settleReservation(claim2.token, "abort").catch(() => {
2467
2697
  });
2468
- throw new Error("No UTXOs available. Fund your wallet or check gateway connection.");
2698
+ throw new Error(claimRefusalMessage(claim2));
2469
2699
  }
2470
2700
  function buildReceipt(writeResult, { txId, fee, txSize, indexed } = {}) {
2471
2701
  const w = writeResult || {};
@@ -3379,7 +3609,7 @@ async function saveStyle(rulesText, styleName, description) {
3379
3609
  };
3380
3610
  let claim2 = null;
3381
3611
  try {
3382
- claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports) });
3612
+ claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports), spend: { dataBytes: Buffer.byteLength(JSON.stringify(payload)) } });
3383
3613
  } catch (e) {
3384
3614
  if (e.code === "WALLET_BUSY_RESERVED") return { success: false, error: e.message, retryable: true };
3385
3615
  throw e;
@@ -3388,7 +3618,7 @@ async function saveStyle(rulesText, styleName, description) {
3388
3618
  const utxos = claim2.utxos;
3389
3619
  if (!utxos || utxos.length === 0) {
3390
3620
  await settleReservation(claim2.token, "abort");
3391
- return { success: false, error: "No UTXOs available. Fund your wallet first." };
3621
+ return { success: false, error: claimRefusalMessage(claim2) };
3392
3622
  }
3393
3623
  const { txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(wif, utxos, JSON.stringify(payload));
3394
3624
  await linkOrRefuse(claim2.token, { txId, txHex, changeUtxos });
@@ -8378,9 +8608,16 @@ async function fulfilNotaryOrder({ scope = "", agentName, order, spv = null } =
8378
8608
  };
8379
8609
  }
8380
8610
  const claimRes = await claimSpendableUtxos(agentAddress, async () => paymentUtxos, {
8381
- reconcile: reconcileHooks({ checkConfirmation: spv?.checkConfirmation || checkConfirmation, broadcast: spv?.broadcastTx || broadcastTx })
8611
+ reconcile: reconcileHooks({ checkConfirmation: spv?.checkConfirmation || checkConfirmation, broadcast: spv?.broadcastTx || broadcastTx }),
8612
+ // AUDIT F5 (wf_dda0064a): the claim used the DEFAULT shape (dataBytes 0), so a payment
8613
+ // could pass the ~576-sat default target yet underfund the real anchor build. Size from
8614
+ // the anchor's actual bytes; the F6 builder invariant backstops whatever remains.
8615
+ spend: { dataBytes: Buffer.byteLength(data) }
8382
8616
  });
8383
8617
  if (!claimRes || !claimRes.token || !claimRes.utxos?.length) {
8618
+ if (claimRes?.refused) {
8619
+ return { state: "error", reason: "PAYMENT_OUTPOINT_UNFUNDABLE", detail: claimRefusalMessage(claimRes).slice(0, 300) };
8620
+ }
8384
8621
  return { state: "error", reason: "PAYMENT_OUTPOINT_RESERVED", detail: "another worker holds the payment coin right now \u2014 retry shortly" };
8385
8622
  }
8386
8623
  claimTok = claimRes.token;
@@ -8403,7 +8640,7 @@ async function fulfilNotaryOrder({ scope = "", agentName, order, spv = null } =
8403
8640
  } catch (e) {
8404
8641
  if (claimTok) {
8405
8642
  try {
8406
- await settleReservation(claimTok, "failed", {});
8643
+ await settleReservation(claimTok, "abort", {});
8407
8644
  } catch {
8408
8645
  }
8409
8646
  }
@@ -10968,6 +11205,7 @@ init_save_log();
10968
11205
  init_spv();
10969
11206
  init_build_cache();
10970
11207
  init_utxo_cache();
11208
+ init_tx_size();
10971
11209
  init_utxo_reservation();
10972
11210
  import { gzipSync } from "zlib";
10973
11211
  import { readFile as readFile4, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
@@ -11086,10 +11324,33 @@ async function saveFile(filePath, options = {}) {
11086
11324
  const encrypted = encrypt("gz:" + compressed.toString("base64"), wif);
11087
11325
  const heldClaims = [];
11088
11326
  let utxos = options.utxos || null;
11327
+ let claim2 = null;
11089
11328
  if (!utxos) {
11090
- let claim2;
11091
11329
  try {
11092
- claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports, readEntry) });
11330
+ claim2 = await claimSpendableUtxos(config2.address, getUtxos, {
11331
+ reconcile: reconcileHooks(spv_exports, readEntry),
11332
+ // ⚠️ AUDIT F2/F3 (wf_dda0064a): sizing the claim as ONE tx of the WHOLE payload was
11333
+ // wrong in both directions at once — TX_TOO_LARGE refused every >12.58MB payload the
11334
+ // chunker exists to SPLIT (a hard regression: each chunk tx is individually legal), and
11335
+ // when it did pass, the chain was UNDER-funded by construction (per-chunk envelopes +
11336
+ // per-tx overhead + the master tx were never budgeted; proven end-to-end: 1.84M sats
11337
+ // spent, no master tx, file unrestorable). Now the claim is sized for the CHAIN: the
11338
+ // first chunk as the tx shape, and every remaining fee — later chunks + the master —
11339
+ // as extraOutSats, which is exactly what the first tx's change must retain. TX_TOO_LARGE
11340
+ // then gates the CHUNK size, never the whole payload.
11341
+ spend: (() => {
11342
+ const E = Buffer.byteLength(String(encrypted || ""));
11343
+ if (E <= MAX_CHUNK_SIZE) return { dataBytes: E };
11344
+ const CHUNK_ENV = 400;
11345
+ const n = Math.ceil(E / MAX_CHUNK_SIZE);
11346
+ let restFees = 0;
11347
+ for (let i = 1; i < n; i++) {
11348
+ restFees += feeForInputs(1, { dataBytes: Math.min(MAX_CHUNK_SIZE, E - i * MAX_CHUNK_SIZE) + CHUNK_ENV });
11349
+ }
11350
+ restFees += feeForInputs(1, { dataBytes: 2e3 + n * 70 });
11351
+ return { dataBytes: Math.min(E, MAX_CHUNK_SIZE) + CHUNK_ENV, extraOutSats: restFees };
11352
+ })()
11353
+ });
11093
11354
  } catch (e) {
11094
11355
  if (e.code === "WALLET_BUSY_RESERVED") return { success: false, error: e.message, retryable: true };
11095
11356
  throw e;
@@ -11099,7 +11360,7 @@ async function saveFile(filePath, options = {}) {
11099
11360
  }
11100
11361
  if (!utxos || utxos.length === 0) {
11101
11362
  if (heldClaims[0]) await settleReservation(heldClaims[0].token, "abort");
11102
- return { success: false, error: "No UTXOs available. Fund your wallet first." };
11363
+ return { success: false, error: claimRefusalMessage(claim2) };
11103
11364
  }
11104
11365
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11105
11366
  try {
@@ -11171,7 +11432,16 @@ async function saveFile(filePath, options = {}) {
11171
11432
  if (!utxos || utxos.length === 0) {
11172
11433
  await new Promise((r) => setTimeout(r, 500));
11173
11434
  if (heldClaims.length) {
11174
- const extra = await claimSpendableUtxos(config2.address, getUtxos);
11435
+ const extra = await claimSpendableUtxos(config2.address, getUtxos, {
11436
+ // g-419: sized from the CURRENT chunk's exact payload — the right magnitude for
11437
+ // the next chunk it funds.
11438
+ spend: { dataBytes: Buffer.byteLength(chunkPayload) }
11439
+ });
11440
+ if (extra.refused || !extra.utxos || extra.utxos.length === 0) {
11441
+ const e = new Error(`INSUFFICIENT_FUNDS_MID_CHAIN: the wallet ran dry after ${i + 1} of ${chunks.length} chunks \u2014 ${claimRefusalMessage(extra)}`);
11442
+ e.code = "INSUFFICIENT_FUNDS_MID_CHAIN";
11443
+ throw e;
11444
+ }
11175
11445
  heldClaims.push(extra);
11176
11446
  utxos = extra.utxos;
11177
11447
  } else {
@@ -12806,15 +13076,18 @@ async function _saveSessionInner(transcriptPath, summary, mode, hostBinding = nu
12806
13076
  let memoryTxId = config2.memory_file_txid || null;
12807
13077
  let historyTxId = config2.session_history_txid || null;
12808
13078
  let changeUtxos = result.changeUtxos || [];
12809
- if (config2.memory_auto_save !== false && changeUtxos.length > 0) {
13079
+ if (config2.memory_auto_save !== false) {
12810
13080
  try {
12811
13081
  const memoryDir = getMemoryDir(actualPath);
12812
13082
  const memoryPath = join15(memoryDir, "MEMORY.md");
12813
13083
  const historyPath = join15(memoryDir, "session-history.md");
13084
+ if (changeUtxos.length === 0) {
13085
+ process.stderr.write("[save_session] change folded to fee (g-419 dust backstop) \u2014 memory/history saves will claim their own coin\n");
13086
+ }
12814
13087
  if (existsSync10(memoryPath)) {
12815
13088
  const memResult = await saveFile(memoryPath, {
12816
13089
  relativePath: "memory/MEMORY.md",
12817
- utxos: changeUtxos,
13090
+ ...changeUtxos.length > 0 ? { utxos: changeUtxos } : {},
12818
13091
  skipVaultIndex: true
12819
13092
  });
12820
13093
  if (memResult.success) {
@@ -12822,10 +13095,10 @@ async function _saveSessionInner(transcriptPath, summary, mode, hostBinding = nu
12822
13095
  changeUtxos = memResult.changeUtxos || [];
12823
13096
  }
12824
13097
  }
12825
- if (existsSync10(historyPath) && changeUtxos.length > 0) {
13098
+ if (existsSync10(historyPath)) {
12826
13099
  const histResult = await saveFile(historyPath, {
12827
13100
  relativePath: "memory/session-history.md",
12828
- utxos: changeUtxos,
13101
+ ...changeUtxos.length > 0 ? { utxos: changeUtxos } : {},
12829
13102
  skipVaultIndex: true
12830
13103
  });
12831
13104
  if (histResult.success) {
@@ -13199,7 +13472,7 @@ async function saveProject(dirPath, options = {}) {
13199
13472
  };
13200
13473
  let claim2;
13201
13474
  try {
13202
- claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports, readEntry) });
13475
+ claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports, readEntry), spend: { dataBytes: Buffer.byteLength(JSON.stringify(payload)) } });
13203
13476
  } catch (e) {
13204
13477
  if (e.code === "WALLET_BUSY_RESERVED") return { success: false, error: e.message, retryable: true };
13205
13478
  throw e;
@@ -13208,7 +13481,7 @@ async function saveProject(dirPath, options = {}) {
13208
13481
  const utxos = claim2.utxos;
13209
13482
  if (!utxos || utxos.length === 0) {
13210
13483
  await settleReservation(claim2.token, "abort");
13211
- return { success: false, error: "No UTXOs available. Fund your wallet first." };
13484
+ return { success: false, error: claimRefusalMessage(claim2) };
13212
13485
  }
13213
13486
  const bundleKey = sha256(bundleFiles.map((f) => f.content_hash).join(","));
13214
13487
  const bc = await idempotentBuildAndBroadcast({
@@ -13748,7 +14021,7 @@ async function updateVaultIndex() {
13748
14021
  };
13749
14022
  let claim2 = null;
13750
14023
  try {
13751
- claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports) });
14024
+ claim2 = await claimSpendableUtxos(config2.address, getUtxos, { reconcile: reconcileHooks(spv_exports), spend: { dataBytes: Buffer.byteLength(JSON.stringify(payload)) } });
13752
14025
  } catch (e) {
13753
14026
  if (e.code === "WALLET_BUSY_RESERVED") return { success: false, error: e.message, retryable: true };
13754
14027
  throw e;
@@ -13757,7 +14030,7 @@ async function updateVaultIndex() {
13757
14030
  let utxos = claim2.utxos;
13758
14031
  if (!utxos || utxos.length === 0) {
13759
14032
  await settleReservation(claim2.token, "abort");
13760
- return { success: false, error: "No UTXOs for vault index tx" };
14033
+ return { success: false, error: claimRefusalMessage(claim2) };
13761
14034
  }
13762
14035
  const { txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(
13763
14036
  wif,
@@ -14275,7 +14548,13 @@ async function x402Fetch({ url, method = "GET", headers = {}, body, maxSats }) {
14275
14548
  const address = privateKey.toPublicKey().toAddress();
14276
14549
  let claim2;
14277
14550
  try {
14278
- claim2 = await claimSpendableUtxos(address, getUtxos, { reconcile: reconcileHooks(spv_exports) });
14551
+ claim2 = await claimSpendableUtxos(address, getUtxos, {
14552
+ reconcile: reconcileHooks(spv_exports),
14553
+ // ⚠️ SELECT AGAINST WHAT THIS CAN ACTUALLY PAY (codex-money 41657e3b). This route pays up
14554
+ // to `cap` sats to a third party via buildPaymentTx — a P2PKH payment output plus change,
14555
+ // no OP_RETURN. The exact shape, not a floor.
14556
+ spend: { dataBytes: 0, p2pkhOuts: 1, extraOutSats: cap }
14557
+ });
14279
14558
  } catch (err9) {
14280
14559
  if (err9.code === "WALLET_BUSY_RESERVED") return { success: false, error: err9.message, retryable: true };
14281
14560
  return { success: false, error: `Failed to get UTXOs: ${err9.message}` };
@@ -14283,7 +14562,7 @@ async function x402Fetch({ url, method = "GET", headers = {}, body, maxSats }) {
14283
14562
  const utxos = claim2.utxos;
14284
14563
  if (!utxos || utxos.length === 0) {
14285
14564
  await settleReservation(claim2.token, "abort");
14286
- return { success: false, error: "No UTXOs available. Wallet may be empty." };
14565
+ return { success: false, error: claimRefusalMessage(claim2) };
14287
14566
  }
14288
14567
  let txResult;
14289
14568
  try {
@@ -14416,9 +14695,23 @@ async function saveGoalsToChain() {
14416
14695
  const completed = goalsData.completed || [];
14417
14696
  process.stderr.write(`[save_goals_to_chain] Bootstrapping ${goals.length} active + ${completed.length} completed goals to chain for ${address}...
14418
14697
  `);
14698
+ const encrypted = encryptGoals(goalsData, wif);
14699
+ const payload = {
14700
+ protocol: "indelible.goals-snapshot",
14701
+ version: 1,
14702
+ owner: address,
14703
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14704
+ count: goals.length,
14705
+ completed_count: completed.length,
14706
+ encrypted
14707
+ };
14708
+ const snapshotAt = payload.timestamp;
14419
14709
  let claim2;
14420
14710
  try {
14421
- claim2 = await claimSpendableUtxos(address, getUtxos, { reconcile: reconcileHooks(spv_exports) });
14711
+ claim2 = await claimSpendableUtxos(address, getUtxos, {
14712
+ reconcile: reconcileHooks(spv_exports),
14713
+ spend: { dataBytes: Buffer.byteLength(JSON.stringify(payload)) }
14714
+ });
14422
14715
  } catch (e) {
14423
14716
  if (e.code === "WALLET_BUSY_RESERVED") {
14424
14717
  return { content: [{ type: "text", text: `${e.message}` }] };
@@ -14429,22 +14722,11 @@ async function saveGoalsToChain() {
14429
14722
  if (!utxos || utxos.length === 0) {
14430
14723
  await settleReservation(claim2.token, "abort");
14431
14724
  return {
14432
- content: [{ type: "text", text: "No UTXOs available. Fund your wallet." }]
14725
+ content: [{ type: "text", text: claimRefusalMessage(claim2) }]
14433
14726
  };
14434
14727
  }
14435
- let txId, fee, txSize, writeReceipt, snapshotAt;
14728
+ let txId, fee, txSize, writeReceipt;
14436
14729
  try {
14437
- const encrypted = encryptGoals(goalsData, wif);
14438
- const payload = {
14439
- protocol: "indelible.goals-snapshot",
14440
- version: 1,
14441
- owner: address,
14442
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
14443
- count: goals.length,
14444
- completed_count: completed.length,
14445
- encrypted
14446
- };
14447
- snapshotAt = payload.timestamp;
14448
14730
  let txHex, changeUtxos;
14449
14731
  ({ txHex, txId, changeUtxos, fee, txSize } = await buildOpReturnTxWithChange(
14450
14732
  wif,
@@ -17039,7 +17321,7 @@ Answer THAT message and nothing else \u2014 the wire also carries unrelated conv
17039
17321
  }
17040
17322
  function printHelp() {
17041
17323
  console.log(`
17042
- Indelible MCP \u2014 Blockchain memory for Claude Code (v5.7.8)
17324
+ Indelible MCP \u2014 Blockchain memory for Claude Code (v5.8.0)
17043
17325
 
17044
17326
  Setup:
17045
17327
  indelible-mcp Set up interactively (recommended \u2014 your key is never written to shell history)
@@ -17368,7 +17650,7 @@ function readStdin() {
17368
17650
  }
17369
17651
  var SERVER_INFO = {
17370
17652
  name: "indelible",
17371
- version: "5.7.8",
17653
+ version: "5.8.0",
17372
17654
  description: "Blockchain-backed memory and code storage for Claude Code"
17373
17655
  };
17374
17656
  var TOOLS = [