@t2000/engine 1.24.11 → 1.24.12

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.d.ts CHANGED
@@ -53,25 +53,6 @@ declare function extractAllProactiveMarkers(text: string): Array<{
53
53
  subjectKey: string;
54
54
  }>;
55
55
 
56
- type PostWritePollOutcome = 'detected_change' | 'ceiling' | 'aborted' | 'fallback_no_baseline' | 'fallback_no_address' | 'fallback_no_rpc';
57
- interface PostWritePollResult {
58
- outcome: PostWritePollOutcome;
59
- /** Number of poll attempts made (0 if fallback fired before polling). */
60
- attempts: number;
61
- /** Wall-clock ms from `pollForIndexerCatchup` entry to return. */
62
- resolvedAtMs: number;
63
- }
64
- interface PostWritePollOptions {
65
- suiRpcUrl: string | undefined;
66
- address: string | undefined;
67
- /** Hard ceiling on total wait time. Default 1500ms (matches old fixed sleep). */
68
- ceilingMs: number;
69
- /** Wait between poll attempts. Default 250ms. */
70
- pollIntervalMs: number;
71
- signal: AbortSignal;
72
- }
73
- declare function pollForIndexerCatchup(options: PostWritePollOptions): Promise<PostWritePollResult>;
74
-
75
56
  /** Rough token count for a message array. */
76
57
  declare function estimateTokens(messages: Message[]): number;
77
58
  interface CompactOptions {
@@ -1736,41 +1717,6 @@ interface EngineConfig {
1736
1717
  * Omit (undefined / empty map) to disable post-write refresh entirely.
1737
1718
  */
1738
1719
  postWriteRefresh?: Record<string, string[]>;
1739
- /**
1740
- * [SPEC 19 Phase B / 2026-05-09] Pre-warmed indexer-catchup poll Promise.
1741
- *
1742
- * Hosts that know the wallet address + Sui RPC URL the moment a resume
1743
- * request lands (e.g. `app/api/engine/resume/route.ts`, where both are
1744
- * available immediately after JWT validation) can fire
1745
- * `pollForIndexerCatchup` in parallel with `createEngine` and pass the
1746
- * returned Promise here. The engine awaits this Promise inside
1747
- * `runPostWriteRefresh` instead of starting a fresh poll, eliminating
1748
- * up to ~1.5s of serial wait when engine boot wall-clock ≥ poll
1749
- * wall-clock.
1750
- *
1751
- * Behavior:
1752
- * - Promise resolves before refresh starts → instant pass-through
1753
- * (poll already done during boot). This is the typical case.
1754
- * - Promise still pending when refresh starts → engine awaits it.
1755
- * Net effect: same as pre-Phase-B fresh-poll behavior.
1756
- * - Promise undefined → engine starts a fresh poll (pre-Phase-B path).
1757
- * - Promise rejects → engine logs warn + falls back to fresh poll
1758
- * (correctness preserved; same as `outcome: 'fallback_*'`).
1759
- *
1760
- * Telemetry: `engine.pwr.sleep_ms` carries `source` tag = `'pre-warmed' | 'engine'`
1761
- * so the host can verify the parallelization is shaving wall-clock.
1762
- * On the pre-warmed path, the `outcome` + `attempts` tags are inherited
1763
- * from the host's poll result — same shape, just resolved earlier.
1764
- *
1765
- * Safe because:
1766
- * - Poll baseline is captured via Sui RPC `getAllBalances` and is
1767
- * independent of any BlockVision cache state. Pre-firing the poll
1768
- * before engine boot does NOT race the engine's cache invalidation.
1769
- * - The poll's correctness contract (never proceed until indexer
1770
- * reflects the write) is preserved — it just runs on a different
1771
- * timeline.
1772
- */
1773
- indexerCatchupPromise?: Promise<PostWritePollResult>;
1774
1720
  }
1775
1721
  interface LLMProvider {
1776
1722
  chat(params: ChatParams): AsyncGenerator<ProviderEvent>;
@@ -1921,7 +1867,6 @@ declare class QueryEngine {
1921
1867
  private readonly blockvisionApiKey;
1922
1868
  private readonly portfolioCache;
1923
1869
  private readonly postWriteRefresh;
1924
- private readonly indexerCatchupPromise;
1925
1870
  private matchedRecipe;
1926
1871
  private messages;
1927
1872
  private abortController;
@@ -2071,6 +2016,25 @@ declare class QueryEngine {
2071
2016
  */
2072
2017
  declare function validateHistory(messages: Message[]): Message[];
2073
2018
 
2019
+ type PostWritePollOutcome = 'detected_change' | 'ceiling' | 'aborted' | 'fallback_no_baseline' | 'fallback_no_address' | 'fallback_no_rpc';
2020
+ interface PostWritePollResult {
2021
+ outcome: PostWritePollOutcome;
2022
+ /** Number of poll attempts made (0 if fallback fired before polling). */
2023
+ attempts: number;
2024
+ /** Wall-clock ms from `pollForIndexerCatchup` entry to return. */
2025
+ resolvedAtMs: number;
2026
+ }
2027
+ interface PostWritePollOptions {
2028
+ suiRpcUrl: string | undefined;
2029
+ address: string | undefined;
2030
+ /** Hard ceiling on total wait time. Default 1500ms (matches old fixed sleep). */
2031
+ ceilingMs: number;
2032
+ /** Wait between poll attempts. Default 250ms. */
2033
+ pollIntervalMs: number;
2034
+ signal: AbortSignal;
2035
+ }
2036
+ declare function pollForIndexerCatchup(options: PostWritePollOptions): Promise<PostWritePollResult>;
2037
+
2074
2038
  /**
2075
2039
  * SPEC 7 v0.3 Quote-Refresh ReviewCard — per-tool result freshness budgets.
2076
2040
  *
package/dist/index.js CHANGED
@@ -5786,120 +5786,6 @@ Only offer to execute actions you have tools for. If you retrieved a quote, data
5786
5786
  - Cap: at most ONE proactive block per turn.
5787
5787
  - Skip proactive blocks when nothing notable changed since the last turn, when the user is mid-flow on something else, or when you'd just be restating the financial-context block. Quality over quantity \u2014 a block ignored is worse than no block.`;
5788
5788
 
5789
- // src/post-write-poll.ts
5790
- async function pollForIndexerCatchup(options) {
5791
- const { suiRpcUrl, address, ceilingMs, pollIntervalMs, signal } = options;
5792
- const start = Date.now();
5793
- if (!suiRpcUrl) {
5794
- await sleepWithFallback(ceilingMs, signal);
5795
- return {
5796
- outcome: "fallback_no_rpc",
5797
- attempts: 0,
5798
- resolvedAtMs: Date.now() - start
5799
- };
5800
- }
5801
- if (!address) {
5802
- await sleepWithFallback(ceilingMs, signal);
5803
- return {
5804
- outcome: "fallback_no_address",
5805
- attempts: 0,
5806
- resolvedAtMs: Date.now() - start
5807
- };
5808
- }
5809
- let baseline;
5810
- try {
5811
- const coins = await fetchWalletCoins(address, suiRpcUrl);
5812
- baseline = new Map(
5813
- coins.map((c) => [c.coinType, BigInt(c.totalBalance)])
5814
- );
5815
- } catch (err) {
5816
- console.warn(
5817
- "[post-write-poll] baseline fetch failed; falling back to fixed sleep:",
5818
- err
5819
- );
5820
- await sleepWithFallback(ceilingMs, signal);
5821
- return {
5822
- outcome: "fallback_no_baseline",
5823
- attempts: 0,
5824
- resolvedAtMs: Date.now() - start
5825
- };
5826
- }
5827
- const maxAttempts = Math.max(1, Math.floor(ceilingMs / pollIntervalMs));
5828
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
5829
- if (signal.aborted) {
5830
- return {
5831
- outcome: "aborted",
5832
- attempts: attempt - 1,
5833
- resolvedAtMs: Date.now() - start
5834
- };
5835
- }
5836
- await new Promise((resolve) => {
5837
- const t = setTimeout(resolve, pollIntervalMs);
5838
- signal.addEventListener(
5839
- "abort",
5840
- () => {
5841
- clearTimeout(t);
5842
- resolve();
5843
- },
5844
- { once: true }
5845
- );
5846
- });
5847
- if (signal.aborted) {
5848
- return {
5849
- outcome: "aborted",
5850
- attempts: attempt,
5851
- resolvedAtMs: Date.now() - start
5852
- };
5853
- }
5854
- let current;
5855
- try {
5856
- const coins = await fetchWalletCoins(address, suiRpcUrl);
5857
- current = new Map(
5858
- coins.map((c) => [c.coinType, BigInt(c.totalBalance)])
5859
- );
5860
- } catch (err) {
5861
- console.warn(
5862
- `[post-write-poll] poll attempt ${attempt} failed; continuing:`,
5863
- err
5864
- );
5865
- continue;
5866
- }
5867
- if (balancesDiffer(baseline, current)) {
5868
- return {
5869
- outcome: "detected_change",
5870
- attempts: attempt,
5871
- resolvedAtMs: Date.now() - start
5872
- };
5873
- }
5874
- }
5875
- return {
5876
- outcome: "ceiling",
5877
- attempts: maxAttempts,
5878
- resolvedAtMs: Date.now() - start
5879
- };
5880
- }
5881
- function balancesDiffer(a, b) {
5882
- if (a.size !== b.size) return true;
5883
- for (const [coinType, balance] of a) {
5884
- if (b.get(coinType) !== balance) return true;
5885
- }
5886
- return false;
5887
- }
5888
- async function sleepWithFallback(ms, signal) {
5889
- if (signal.aborted) return;
5890
- await new Promise((resolve) => {
5891
- const t = setTimeout(resolve, ms);
5892
- signal.addEventListener(
5893
- "abort",
5894
- () => {
5895
- clearTimeout(t);
5896
- resolve();
5897
- },
5898
- { once: true }
5899
- );
5900
- });
5901
- }
5902
-
5903
5789
  // src/proactive-marker.ts
5904
5790
  var VALID_TYPES = /* @__PURE__ */ new Set([
5905
5791
  "idle_balance",
@@ -7506,12 +7392,6 @@ var QueryEngine = class {
7506
7392
  // [v1.5] See `EngineConfig.postWriteRefresh` — drives the post-write
7507
7393
  // synthetic read injection in `resumeWithToolResult`.
7508
7394
  postWriteRefresh;
7509
- // [SPEC 19 Phase B / 2026-05-09] Pre-warmed indexer-catchup poll Promise.
7510
- // When provided by the host (resume route fires `pollForIndexerCatchup`
7511
- // in parallel with `createEngine`), `runPostWriteRefresh` awaits this
7512
- // instead of starting a fresh poll. See `EngineConfig.indexerCatchupPromise`
7513
- // for the full design rationale.
7514
- indexerCatchupPromise;
7515
7395
  matchedRecipe = null;
7516
7396
  messages = [];
7517
7397
  abortController = null;
@@ -7587,7 +7467,6 @@ var QueryEngine = class {
7587
7467
  this.onAutoExecuted = config.onAutoExecuted;
7588
7468
  this.onGuardFired = config.onGuardFired;
7589
7469
  this.postWriteRefresh = config.postWriteRefresh;
7590
- this.indexerCatchupPromise = config.indexerCatchupPromise;
7591
7470
  this.blockvisionApiKey = config.blockvisionApiKey;
7592
7471
  this.portfolioCache = config.portfolioCache;
7593
7472
  this.tools = config.tools ?? (config.agent ? getDefaultTools() : []);
@@ -7977,6 +7856,10 @@ var QueryEngine = class {
7977
7856
  blockvisionApiKey: this.blockvisionApiKey,
7978
7857
  portfolioCache: this.portfolioCache
7979
7858
  };
7859
+ const canSafetyNet = !!(this.walletAddress && this.suiRpcUrl);
7860
+ const safetyNetBaseline = canSafetyNet ? fetchWalletCoins(this.walletAddress, this.suiRpcUrl).then(
7861
+ (coins) => new Map(coins.map((c) => [c.coinType, BigInt(c.totalBalance)]))
7862
+ ).catch(() => null) : null;
7980
7863
  const cacheInvalidationStart = Date.now();
7981
7864
  if (this.walletAddress) {
7982
7865
  this.portfolioCache?.delete(this.walletAddress);
@@ -7987,45 +7870,10 @@ var QueryEngine = class {
7987
7870
  Date.now() - cacheInvalidationStart,
7988
7871
  { has_wallet: this.walletAddress ? "1" : "0" }
7989
7872
  );
7990
- const sleepStart = Date.now();
7991
- let pollResult;
7992
- let sleepSource = "engine";
7993
- if (this.indexerCatchupPromise) {
7994
- try {
7995
- pollResult = await this.indexerCatchupPromise;
7996
- sleepSource = "pre-warmed";
7997
- } catch (err) {
7998
- console.warn(
7999
- "[engine.pwr] pre-warmed indexer catchup promise rejected; falling back to fresh poll:",
8000
- err
8001
- );
8002
- pollResult = await pollForIndexerCatchup({
8003
- suiRpcUrl: this.suiRpcUrl,
8004
- address: this.walletAddress,
8005
- ceilingMs: 1500,
8006
- pollIntervalMs: 250,
8007
- signal
8008
- });
8009
- }
8010
- } else {
8011
- pollResult = await pollForIndexerCatchup({
8012
- suiRpcUrl: this.suiRpcUrl,
8013
- address: this.walletAddress,
8014
- ceilingMs: 1500,
8015
- pollIntervalMs: 250,
8016
- signal
8017
- });
8018
- }
8019
- getTelemetrySink().histogram(
8020
- "engine.pwr.sleep_ms",
8021
- Date.now() - sleepStart,
8022
- {
8023
- aborted: signal.aborted ? "1" : "0",
8024
- outcome: pollResult.outcome,
8025
- attempts: String(pollResult.attempts),
8026
- source: sleepSource
8027
- }
8028
- );
7873
+ getTelemetrySink().counter("engine.pwr.skipped_sleep_count", {
7874
+ has_wallet: this.walletAddress ? "1" : "0",
7875
+ can_safety_net: canSafetyNet ? "1" : "0"
7876
+ });
8029
7877
  if (signal.aborted) return;
8030
7878
  const refreshStart = Date.now();
8031
7879
  const idStem = `pwr_${action.toolUseId.slice(-6)}`;
@@ -8130,6 +7978,35 @@ var QueryEngine = class {
8130
7978
  is_bundle: isBundle ? "1" : "0"
8131
7979
  }
8132
7980
  );
7981
+ if (safetyNetBaseline && canSafetyNet) {
7982
+ const wallet = this.walletAddress;
7983
+ const rpc = this.suiRpcUrl;
7984
+ void (async () => {
7985
+ try {
7986
+ const [baseline, current] = await Promise.all([
7987
+ safetyNetBaseline,
7988
+ fetchWalletCoins(wallet, rpc).then(
7989
+ (coins) => new Map(
7990
+ coins.map((c) => [c.coinType, BigInt(c.totalBalance)])
7991
+ )
7992
+ ).catch(() => null)
7993
+ ]);
7994
+ if (!baseline || !current) return;
7995
+ const stale = !walletStateChanged(baseline, current);
7996
+ if (stale) {
7997
+ getTelemetrySink().counter(
7998
+ "engine.pwr.observed_stale_balance_check",
7999
+ {
8000
+ stale: "1",
8001
+ is_bundle: isBundle ? "1" : "0",
8002
+ tool_count: String(refreshTools.length)
8003
+ }
8004
+ );
8005
+ }
8006
+ } catch {
8007
+ }
8008
+ })();
8009
+ }
8133
8010
  }
8134
8011
  interrupt() {
8135
8012
  this.abortController?.abort();
@@ -9060,6 +8937,13 @@ ${recipeCtx}`;
9060
8937
  }
9061
8938
  }
9062
8939
  };
8940
+ function walletStateChanged(before, after) {
8941
+ if (before.size !== after.size) return true;
8942
+ for (const [coinType, balance] of before) {
8943
+ if (after.get(coinType) !== balance) return true;
8944
+ }
8945
+ return false;
8946
+ }
9063
8947
  function isCorruptHistoryError(err) {
9064
8948
  const msg = err instanceof Error ? err.message : String(err);
9065
8949
  return msg.includes("tool_use") && msg.includes("tool_result") || msg.includes("roles must alternate") || msg.includes("400") && msg.includes("invalid_request_error");
@@ -9169,6 +9053,120 @@ function harnessShapeForEffort(effort) {
9169
9053
  return "max";
9170
9054
  }
9171
9055
  }
9056
+
9057
+ // src/post-write-poll.ts
9058
+ async function pollForIndexerCatchup(options) {
9059
+ const { suiRpcUrl, address, ceilingMs, pollIntervalMs, signal } = options;
9060
+ const start = Date.now();
9061
+ if (!suiRpcUrl) {
9062
+ await sleepWithFallback(ceilingMs, signal);
9063
+ return {
9064
+ outcome: "fallback_no_rpc",
9065
+ attempts: 0,
9066
+ resolvedAtMs: Date.now() - start
9067
+ };
9068
+ }
9069
+ if (!address) {
9070
+ await sleepWithFallback(ceilingMs, signal);
9071
+ return {
9072
+ outcome: "fallback_no_address",
9073
+ attempts: 0,
9074
+ resolvedAtMs: Date.now() - start
9075
+ };
9076
+ }
9077
+ let baseline;
9078
+ try {
9079
+ const coins = await fetchWalletCoins(address, suiRpcUrl);
9080
+ baseline = new Map(
9081
+ coins.map((c) => [c.coinType, BigInt(c.totalBalance)])
9082
+ );
9083
+ } catch (err) {
9084
+ console.warn(
9085
+ "[post-write-poll] baseline fetch failed; falling back to fixed sleep:",
9086
+ err
9087
+ );
9088
+ await sleepWithFallback(ceilingMs, signal);
9089
+ return {
9090
+ outcome: "fallback_no_baseline",
9091
+ attempts: 0,
9092
+ resolvedAtMs: Date.now() - start
9093
+ };
9094
+ }
9095
+ const maxAttempts = Math.max(1, Math.floor(ceilingMs / pollIntervalMs));
9096
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
9097
+ if (signal.aborted) {
9098
+ return {
9099
+ outcome: "aborted",
9100
+ attempts: attempt - 1,
9101
+ resolvedAtMs: Date.now() - start
9102
+ };
9103
+ }
9104
+ await new Promise((resolve) => {
9105
+ const t = setTimeout(resolve, pollIntervalMs);
9106
+ signal.addEventListener(
9107
+ "abort",
9108
+ () => {
9109
+ clearTimeout(t);
9110
+ resolve();
9111
+ },
9112
+ { once: true }
9113
+ );
9114
+ });
9115
+ if (signal.aborted) {
9116
+ return {
9117
+ outcome: "aborted",
9118
+ attempts: attempt,
9119
+ resolvedAtMs: Date.now() - start
9120
+ };
9121
+ }
9122
+ let current;
9123
+ try {
9124
+ const coins = await fetchWalletCoins(address, suiRpcUrl);
9125
+ current = new Map(
9126
+ coins.map((c) => [c.coinType, BigInt(c.totalBalance)])
9127
+ );
9128
+ } catch (err) {
9129
+ console.warn(
9130
+ `[post-write-poll] poll attempt ${attempt} failed; continuing:`,
9131
+ err
9132
+ );
9133
+ continue;
9134
+ }
9135
+ if (balancesDiffer(baseline, current)) {
9136
+ return {
9137
+ outcome: "detected_change",
9138
+ attempts: attempt,
9139
+ resolvedAtMs: Date.now() - start
9140
+ };
9141
+ }
9142
+ }
9143
+ return {
9144
+ outcome: "ceiling",
9145
+ attempts: maxAttempts,
9146
+ resolvedAtMs: Date.now() - start
9147
+ };
9148
+ }
9149
+ function balancesDiffer(a, b) {
9150
+ if (a.size !== b.size) return true;
9151
+ for (const [coinType, balance] of a) {
9152
+ if (b.get(coinType) !== balance) return true;
9153
+ }
9154
+ return false;
9155
+ }
9156
+ async function sleepWithFallback(ms, signal) {
9157
+ if (signal.aborted) return;
9158
+ await new Promise((resolve) => {
9159
+ const t = setTimeout(resolve, ms);
9160
+ signal.addEventListener(
9161
+ "abort",
9162
+ () => {
9163
+ clearTimeout(t);
9164
+ resolve();
9165
+ },
9166
+ { once: true }
9167
+ );
9168
+ });
9169
+ }
9172
9170
  async function regenerateBundle(engine, action) {
9173
9171
  if (action.canRegenerate !== true) {
9174
9172
  return {