@t2000/engine 0.54.1 → 0.54.2

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/dist/index.js CHANGED
@@ -537,6 +537,86 @@ var BLOCKVISION_BASE = "https://api.blockvision.org/v2/sui";
537
537
  var PORTFOLIO_TIMEOUT_MS = 4e3;
538
538
  var PRICES_TIMEOUT_MS = 3e3;
539
539
  var CACHE_TTL_MS = 6e4;
540
+ var BV_RETRY_MAX_ATTEMPTS = 3;
541
+ var BV_RETRY_BASE_DELAY_MS = 250;
542
+ var BV_RETRY_BACKOFF_FACTOR = 3;
543
+ var BV_RETRY_JITTER = 0.25;
544
+ var BV_RETRY_AFTER_CAP_MS = 5e3;
545
+ var CB_WINDOW_MS = 5e3;
546
+ var CB_THRESHOLD = 10;
547
+ var CB_COOLDOWN_MS = 3e4;
548
+ var cb429Timestamps = [];
549
+ var cbOpenUntil = 0;
550
+ function cbIsOpen(now) {
551
+ return now < cbOpenUntil;
552
+ }
553
+ function cbRecord429(now) {
554
+ cb429Timestamps.push(now);
555
+ cb429Timestamps = cb429Timestamps.filter((t) => now - t < CB_WINDOW_MS);
556
+ if (cb429Timestamps.length >= CB_THRESHOLD && !cbIsOpen(now)) {
557
+ cbOpenUntil = now + CB_COOLDOWN_MS;
558
+ console.warn(
559
+ `[blockvision] circuit breaker OPEN \u2014 ${CB_THRESHOLD} 429s in ${CB_WINDOW_MS}ms, retries disabled for ${CB_COOLDOWN_MS / 1e3}s`
560
+ );
561
+ cb429Timestamps = [];
562
+ }
563
+ }
564
+ async function fetchBlockVisionWithRetry(url, init, opts = {}) {
565
+ const rng = opts.rng ?? Math.random;
566
+ const sleep2 = opts.sleep ?? ((ms) => new Promise((resolve, reject) => {
567
+ const timer = setTimeout(resolve, ms);
568
+ if (opts.signal) {
569
+ const onAbort = () => {
570
+ clearTimeout(timer);
571
+ reject(new DOMException("Aborted", "AbortError"));
572
+ };
573
+ if (opts.signal.aborted) onAbort();
574
+ else opts.signal.addEventListener("abort", onAbort, { once: true });
575
+ }
576
+ }));
577
+ let lastError = null;
578
+ let lastResponse = null;
579
+ for (let attempt = 0; attempt < BV_RETRY_MAX_ATTEMPTS; attempt++) {
580
+ if (attempt > 0) {
581
+ let waitMs = BV_RETRY_BASE_DELAY_MS * Math.pow(BV_RETRY_BACKOFF_FACTOR, attempt - 1);
582
+ const retryAfter = lastResponse?.headers.get("retry-after");
583
+ if (retryAfter) {
584
+ const secs = Number(retryAfter);
585
+ if (Number.isFinite(secs) && secs > 0) {
586
+ waitMs = Math.min(secs * 1e3, BV_RETRY_AFTER_CAP_MS);
587
+ }
588
+ }
589
+ const jitterPx = (rng() * 2 - 1) * BV_RETRY_JITTER * waitMs;
590
+ const delay = Math.max(0, waitMs + jitterPx);
591
+ try {
592
+ await sleep2(delay);
593
+ } catch (err) {
594
+ if (lastResponse) return lastResponse;
595
+ throw err;
596
+ }
597
+ }
598
+ try {
599
+ lastResponse = await fetch(url, init);
600
+ } catch (err) {
601
+ lastError = err;
602
+ if (err?.name === "AbortError") throw err;
603
+ continue;
604
+ }
605
+ if (lastResponse.ok) return lastResponse;
606
+ if (lastResponse.status !== 429 && lastResponse.status < 500) {
607
+ return lastResponse;
608
+ }
609
+ if (lastResponse.status === 429) {
610
+ const now = (opts.now ?? Date.now)();
611
+ cbRecord429(now);
612
+ if (cbIsOpen(now)) {
613
+ return lastResponse;
614
+ }
615
+ }
616
+ }
617
+ if (lastResponse) return lastResponse;
618
+ throw lastError ?? new Error("fetch failed after retries");
619
+ }
540
620
  var DEGRADED_CACHE_TTL_MS = 15e3;
541
621
  var PRICE_LIST_CHUNK = 10;
542
622
  var STABLE_USD_PRICES = {
@@ -582,12 +662,17 @@ async function fetchAddressPortfolio(address, apiKey, fallbackRpcUrl) {
582
662
  }
583
663
  async function fetchPortfolioFromBlockVision(address, apiKey) {
584
664
  const url = `${BLOCKVISION_BASE}/account/coins?account=${encodeURIComponent(address)}`;
665
+ const signal = AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS);
585
666
  let res;
586
667
  try {
587
- res = await fetch(url, {
588
- headers: { "x-api-key": apiKey, accept: "application/json" },
589
- signal: AbortSignal.timeout(PORTFOLIO_TIMEOUT_MS)
590
- });
668
+ res = await fetchBlockVisionWithRetry(
669
+ url,
670
+ {
671
+ headers: { "x-api-key": apiKey, accept: "application/json" },
672
+ signal
673
+ },
674
+ { signal }
675
+ );
591
676
  } catch (err) {
592
677
  console.warn("[blockvision-prices] portfolio fetch threw, degrading:", err);
593
678
  return null;
@@ -718,12 +803,17 @@ async function fetchPricesFromBlockVision(coinTypes, apiKey) {
718
803
  const chunk = longForms.slice(i, i + PRICE_LIST_CHUNK);
719
804
  const tokenIds = encodeURIComponent(chunk.join(","));
720
805
  const url = `${BLOCKVISION_BASE}/coin/price/list?tokenIds=${tokenIds}&show24hChange=true`;
806
+ const signal = AbortSignal.timeout(PRICES_TIMEOUT_MS);
721
807
  let res;
722
808
  try {
723
- res = await fetch(url, {
724
- headers: { "x-api-key": apiKey, accept: "application/json" },
725
- signal: AbortSignal.timeout(PRICES_TIMEOUT_MS)
726
- });
809
+ res = await fetchBlockVisionWithRetry(
810
+ url,
811
+ {
812
+ headers: { "x-api-key": apiKey, accept: "application/json" },
813
+ signal
814
+ },
815
+ { signal }
816
+ );
727
817
  } catch (err) {
728
818
  console.warn("[blockvision-prices] price chunk threw, skipping:", err);
729
819
  continue;
@@ -914,12 +1004,17 @@ async function fetchAddressDefiPortfolio(address, apiKey, priceHints = {}) {
914
1004
  }
915
1005
  async function fetchOneDefiProtocol(address, protocol, apiKey) {
916
1006
  const url = `${BLOCKVISION_BASE}/account/defiPortfolio?address=${encodeURIComponent(address)}&protocol=${protocol}`;
1007
+ const signal = AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS);
917
1008
  let res;
918
1009
  try {
919
- res = await fetch(url, {
920
- headers: { "x-api-key": apiKey, accept: "application/json" },
921
- signal: AbortSignal.timeout(DEFI_PORTFOLIO_TIMEOUT_MS)
922
- });
1010
+ res = await fetchBlockVisionWithRetry(
1011
+ url,
1012
+ {
1013
+ headers: { "x-api-key": apiKey, accept: "application/json" },
1014
+ signal
1015
+ },
1016
+ { signal }
1017
+ );
923
1018
  } catch (err) {
924
1019
  console.warn(`[defi] ${protocol} fetch threw:`, err);
925
1020
  return null;
@@ -3329,12 +3424,31 @@ var portfolioAnalysisTool = buildTool({
3329
3424
  `${apiUrl}/api/analytics/portfolio-history?days=7`,
3330
3425
  { headers: { "x-sui-address": address }, signal: context.signal }
3331
3426
  ).then((res) => res.ok ? res.json() : null).catch(() => null) : Promise.resolve(null),
3332
- // DeFi fetch — prefer the audric snapshot's already-computed value
3333
- // (when present and not 'degraded'), otherwise call the engine's
3334
- // direct aggregator. The 'degraded' check prevents the audric
3335
- // path from masking a useful direct read when the audric route's
3336
- // own DeFi field came back empty.
3337
- audricSnapshot && audricSnapshot.defiSource !== "degraded" ? Promise.resolve({
3427
+ // DeFi fetch — prefer the audric snapshot's already-computed
3428
+ // value, but only when we can trust it. Two trust signals:
3429
+ // 1. `source === 'blockvision'` — fully successful fresh read
3430
+ // (even if value is 0, that's a confirmed empty position).
3431
+ // 2. `defiValueUsd > 0` — any positive value, regardless
3432
+ // of source. `partial-stale` with a positive total is fine,
3433
+ // `partial` with a positive total is the live equivalent.
3434
+ //
3435
+ // [Bug — 2026-04-28 round 2] Pre-fix the trust gate was
3436
+ // `defiSource !== 'degraded'`, which let `partial + 0` through
3437
+ // as authoritative. During a BlockVision 429 burst the audric
3438
+ // host's `/api/portfolio` returns `partial + 0` (some protocols
3439
+ // failed, the rest reported $0, no sticky-positive available
3440
+ // *in that process*) — but the engine's direct fetcher in the
3441
+ // chat route may have a sticky-positive in *this* Vercel
3442
+ // instance's cache. Trusting audric's $0 silently dropped the
3443
+ // DeFi line that `balance_check` (which always calls direct)
3444
+ // showed correctly on the same turn — same SSOT-divergence bug
3445
+ // class, manifested in a different layer.
3446
+ //
3447
+ // The new condition routes around audric's $0 in exactly that
3448
+ // case. When the direct fetch ALSO returns $0 the answer is
3449
+ // consistent across tools (both report degraded), which is the
3450
+ // honest UX during a real outage.
3451
+ audricSnapshot && (audricSnapshot.defiSource === "blockvision" || audricSnapshot.defiValueUsd > 0) ? Promise.resolve({
3338
3452
  totalUsd: audricSnapshot.defiValueUsd,
3339
3453
  perProtocol: {},
3340
3454
  pricedAt: Date.now(),