solana-traderclaw 1.0.146 → 1.0.148

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.
@@ -80,11 +80,11 @@
80
80
  // ── Portfolio Maintenance ───────────────────────────────────────
81
81
  {
82
82
  id: "subscription-cleanup",
83
- schedule: "15 */8 * * *",
83
+ schedule: "15 */2 * * *",
84
84
  agentId: "main",
85
85
  sessionTarget: "isolated",
86
86
  delivery: { mode: "announce", channel: "last", bestEffort: true },
87
- message: "CRON_JOB: subscription_cleanup\n\nsolana_positions for open CAs solana_bitquery_subscriptions for active subs (if AUTH_SCOPE_MISSING, log and stop) match subs to positions → solana_bitquery_unsubscribe orphaned subs → solana_memory_write tag 'subscription_cleanup'. Summarize before/after counts.",
87
+ message: "CRON_JOB: subscription_cleanup\n\nGOAL: Keep Bitquery subscriptions healthy — one realtimeTokenPricesSolana per OPEN position, total active subs UNDER 20. Do NOT touch alpha (solana_alpha_unsubscribe forbidden).\n\nSTEP 1 — Baseline:\n solana_runtime_status → note alphaStream.subscribed + bitqueryStream\n solana_positions → open position CAs (mint addresses)\n solana_bitquery_subscriptions active subs + connectedWebSocketClients (if AUTH_SCOPE_MISSING, log and stop)\n\nSTEP 2 Compute targets:\n expectedSubs = count(open positions)\n healthy TCP clients to orchestrator WS ≈ 1–2 (not 10+)\n per-client Bitquery sub cap = 20 (OPENCLAW_WS_MAX_SUBS_PER_CLIENT)\n\nSTEP 3 — Reconcile:\n Each active subscription where variables.token NOT in open CAs → solana_bitquery_unsubscribe({ subscriptionId })\n Each open CA missing realtimeTokenPricesSolana solana_bitquery_subscribe({ templateKey: \"realtimeTokenPricesSolana\", variables: { token: \"<CA>\" }, agentId: \"main\" })\n Subs nearing 24h expiry or subscription_expiring → solana_bitquery_subscription_reopen\n\nSTEP 4 — Escalate if still unhealthy after reconcile:\n If reported activeSubscriptionCount > 20 OR subscribe errors WS_PER_KEY_LIMIT / WS_SUBSCRIPTION_LIMIT_REACHED OR absurd connectedWebSocketClients:\n solana_memory_write tag subscription_cleanup severity CRITICAL — gateway restart may be required to drop leaked TCP sockets (cron cannot restart the gateway).\n\nSTEP 5 — Report:\n solana_memory_write tag subscription_cleanup — before/after: openPositions, activeSubs, connectedWebSocketClients, orphansRemoved, subsAdded, reopenedCount.",
88
88
  enabled: true
89
89
  },
90
90
 
@@ -69,11 +69,11 @@
69
69
  // ── Portfolio Maintenance ───────────────────────────────────────
70
70
  {
71
71
  id: "subscription-cleanup",
72
- schedule: "15 */8 * * *",
72
+ schedule: "15 */2 * * *",
73
73
  agentId: "main",
74
74
  sessionTarget: "isolated",
75
75
  delivery: { mode: "announce", channel: "last", bestEffort: true },
76
- message: "CRON_JOB: subscription_cleanup\n\nsolana_positions for open CAs solana_bitquery_subscriptions for active subs (if AUTH_SCOPE_MISSING, log and stop) match subs to positions → solana_bitquery_unsubscribe orphaned subs → solana_memory_write tag 'subscription_cleanup'. Summarize before/after counts.",
76
+ message: "CRON_JOB: subscription_cleanup\n\nGOAL: Keep Bitquery subscriptions healthy — one realtimeTokenPricesSolana per OPEN position, total active subs UNDER 20. Do NOT touch alpha (solana_alpha_unsubscribe forbidden).\n\nSTEP 1 — Baseline:\n solana_runtime_status → note alphaStream.subscribed + bitqueryStream\n solana_positions → open position CAs (mint addresses)\n solana_bitquery_subscriptions active subs + connectedWebSocketClients (if AUTH_SCOPE_MISSING, log and stop)\n\nSTEP 2 Compute targets:\n expectedSubs = count(open positions)\n healthy TCP clients to orchestrator WS ≈ 1–2 (not 10+)\n per-client Bitquery sub cap = 20 (OPENCLAW_WS_MAX_SUBS_PER_CLIENT)\n\nSTEP 3 — Reconcile:\n Each active subscription where variables.token NOT in open CAs → solana_bitquery_unsubscribe({ subscriptionId })\n Each open CA missing realtimeTokenPricesSolana solana_bitquery_subscribe({ templateKey: \"realtimeTokenPricesSolana\", variables: { token: \"<CA>\" }, agentId: \"main\" })\n Subs nearing 24h expiry or subscription_expiring → solana_bitquery_subscription_reopen\n\nSTEP 4 — Escalate if still unhealthy after reconcile:\n If reported activeSubscriptionCount > 20 OR subscribe errors WS_PER_KEY_LIMIT / WS_SUBSCRIPTION_LIMIT_REACHED OR absurd connectedWebSocketClients:\n solana_memory_write tag subscription_cleanup severity CRITICAL — gateway restart may be required to drop leaked TCP sockets (cron cannot restart the gateway).\n\nSTEP 5 — Report:\n solana_memory_write tag subscription_cleanup — before/after: openPositions, activeSubs, connectedWebSocketClients, orphansRemoved, subsAdded, reopenedCount.",
77
77
  enabled: true
78
78
  },
79
79
 
@@ -62,23 +62,72 @@ var BitqueryStreamManager = class {
62
62
  async unsubscribe(subscriptionId) {
63
63
  this.activeSubscriptions.delete(subscriptionId);
64
64
  if (!this.ws || this.ws.readyState !== 1) {
65
+ this.disconnectIfIdle();
65
66
  return { unsubscribed: true };
66
67
  }
67
68
  return new Promise((resolve) => {
69
+ const finish = () => {
70
+ resolve({ unsubscribed: true });
71
+ this.disconnectIfIdle();
72
+ };
68
73
  const timeout = setTimeout(() => {
69
74
  this.pendingUnsubscribes.delete(subscriptionId);
70
- resolve({ unsubscribed: true });
75
+ finish();
71
76
  }, 1e4);
72
- this.pendingUnsubscribes.set(subscriptionId, { resolve, timeout });
77
+ this.pendingUnsubscribes.set(subscriptionId, { resolve: finish, timeout });
73
78
  try {
74
79
  this.ws.send(JSON.stringify({ type: "bitquery_unsubscribe", subscriptionId }));
75
80
  } catch {
76
81
  clearTimeout(timeout);
77
82
  this.pendingUnsubscribes.delete(subscriptionId);
78
- resolve({ unsubscribed: true });
83
+ finish();
79
84
  }
80
85
  });
81
86
  }
87
+ /** Best-effort: notify orchestrator before gateway reconnect / dispose clears local mux. Sequential to avoid reorder races on the FIFO pending queue. */
88
+ async unsubscribeAll() {
89
+ const ids = [...this.activeSubscriptions.keys()];
90
+ let errors = 0;
91
+ for (const id of ids) {
92
+ try {
93
+ await this.unsubscribe(id);
94
+ } catch {
95
+ errors += 1;
96
+ }
97
+ }
98
+ this.disconnectIfIdle();
99
+ return { attempted: ids.length, errors };
100
+ }
101
+ /** WebSocket OPEN (TCP connected), not necessarily post-auth Bitquery-ready. */
102
+ isWebsocketOpen() {
103
+ return this.ws !== null && this.ws.readyState === 1;
104
+ }
105
+ getActiveTokens() {
106
+ const out = [];
107
+ for (const sub of this.activeSubscriptions.values()) {
108
+ const t = sub.variables?.token;
109
+ if (typeof t === "string" && t.trim()) out.push(t.trim());
110
+ }
111
+ return [...new Set(out)];
112
+ }
113
+ getActiveSubscriptionIds() {
114
+ return [...this.activeSubscriptions.keys()];
115
+ }
116
+ getStats() {
117
+ const ls = this.config.lifetimeState;
118
+ return {
119
+ lifetimeConnectCount: ls?.lifetimeConnectCount ?? 0,
120
+ reconnectAttempt: this.reconnectAttempt,
121
+ intentionalClose: this.intentionalClose,
122
+ websocketOpen: this.isWebsocketOpen(),
123
+ authenticated: this.authenticated,
124
+ activeSubscriptionCount: this.activeSubscriptions.size
125
+ };
126
+ }
127
+ bumpLifetimeConnect() {
128
+ const ls = this.config.lifetimeState;
129
+ if (ls) ls.lifetimeConnectCount += 1;
130
+ }
82
131
  /** Close the WS if no active subscriptions remain. */
83
132
  disconnectIfIdle() {
84
133
  if (this.activeSubscriptions.size === 0) {
@@ -190,6 +239,7 @@ var BitqueryStreamManager = class {
190
239
  ws.on("open", () => {
191
240
  clearTimeout(connectTimeout);
192
241
  this.reconnectAttempt = 0;
242
+ this.bumpLifetimeConnect();
193
243
  this.log("info", "Connected");
194
244
  pingInterval = setInterval(() => {
195
245
  if (!this.ws || this.ws.readyState !== 1) return;
@@ -274,7 +324,7 @@ var BitqueryStreamManager = class {
274
324
  if (pending) {
275
325
  clearTimeout(pending.timeout);
276
326
  this.pendingUnsubscribes.delete(subscriptionId);
277
- pending.resolve({ unsubscribed: true });
327
+ pending.resolve();
278
328
  }
279
329
  this.disconnectIfIdle();
280
330
  break;
@@ -286,6 +336,7 @@ var BitqueryStreamManager = class {
286
336
  "WS_SUBSCRIBE_VALIDATION_ERROR",
287
337
  "BITQUERY_SUBSCRIPTION_TEMPLATE_NOT_FOUND",
288
338
  "WS_SUBSCRIPTION_LIMIT_REACHED",
339
+ "WS_PER_KEY_LIMIT",
289
340
  "WS_BRIDGE_UNAVAILABLE"
290
341
  ].includes(code)) {
291
342
  const pending = this.pendingSubscribeQueue.shift();
@@ -309,7 +360,7 @@ var BitqueryStreamManager = class {
309
360
  this.pendingSubscribeQueue = [];
310
361
  for (const [, pending] of this.pendingUnsubscribes) {
311
362
  clearTimeout(pending.timeout);
312
- pending.resolve({ unsubscribed: true });
363
+ pending.resolve();
313
364
  }
314
365
  this.pendingUnsubscribes.clear();
315
366
  }
package/dist/index.js CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  } from "./chunk-GX4HA22L.js";
23
23
  import {
24
24
  BitqueryStreamManager
25
- } from "./chunk-S2DLZKMQ.js";
25
+ } from "./chunk-QIURFAOS.js";
26
26
  import {
27
27
  shouldSyncGatewayCredentials
28
28
  } from "./chunk-R24UDHQG.js";
@@ -48,6 +48,7 @@ import { Type } from "@sinclair/typebox";
48
48
  import kayba, { SpanType } from "@kayba_ai/tracing";
49
49
  import * as fs from "fs";
50
50
  import * as path from "path";
51
+ import { fileURLToPath } from "node:url";
51
52
  import { homedir } from "os";
52
53
 
53
54
  // lib/x-client.mjs
@@ -783,6 +784,9 @@ var SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY = Symbol.for(
783
784
  var SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY = Symbol.for(
784
785
  "openclaw.solana-trader.alpha-lifetime.v1"
785
786
  );
787
+ var SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY = Symbol.for(
788
+ "openclaw.solana-trader.bitquery-lifetime.v1"
789
+ );
786
790
  var __solanaTraderAlphaSingletonHolder = globalThis;
787
791
  function getOrCreateAlphaBuffer() {
788
792
  const existing = __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY];
@@ -802,6 +806,23 @@ function getOrCreateAlphaLifetimeState() {
802
806
  __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY] = fresh;
803
807
  return fresh;
804
808
  }
809
+ function getOrCreateBitqueryLifetimeState() {
810
+ const existing = __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY];
811
+ if (existing) return existing;
812
+ const fresh = { lifetimeConnectCount: 0 };
813
+ __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY] = fresh;
814
+ return fresh;
815
+ }
816
+ var __solanaTraderStatusQueriesMd = (() => {
817
+ try {
818
+ const distPath = fileURLToPath(import.meta.url);
819
+ const packageRoot = path.dirname(path.dirname(distPath));
820
+ const mdPath = path.join(packageRoot, "lib", "status-queries.md");
821
+ return fs.readFileSync(mdPath, "utf-8");
822
+ } catch {
823
+ return void 0;
824
+ }
825
+ })();
805
826
  function __solanaTraderDisposePreviousLifecycle(logger) {
806
827
  const prev = __solanaTraderGlobalSingletonHolder[SOLANA_TRADER_LIFECYCLE_SINGLETON_KEY];
807
828
  if (!prev) return;
@@ -2053,7 +2074,7 @@ ${notes}
2053
2074
  });
2054
2075
  api.registerTool({
2055
2076
  name: "trade_size_limit_get",
2056
- description: "Read the per-wallet maximum **buy** size in SOL enforced by the API (stored in `wallet.limits.maxTradeSizeSol`; platform default 1.5 SOL when unset). Always use this before sizing buys; never guess the limit.",
2077
+ description: "Read the effective per-wallet **maximum buy** size in SOL from `GET /api/wallet/max-trade-size` (merges `buyAmounts.maxBuyAmountSol` with legacy `maxTradeSizeSol`). There is no platform default cap when unset \u2014 use the response before sizing buys.",
2057
2078
  parameters: Type.Object({}),
2058
2079
  execute: wrapExecute(
2059
2080
  "trade_size_limit_get",
@@ -2062,7 +2083,7 @@ ${notes}
2062
2083
  });
2063
2084
  api.registerTool({
2064
2085
  name: "trade_size_limit_set",
2065
- description: "Set the per-wallet maximum buy size in SOL (persisted on the wallet `limits` JSON). Subject to a platform ceiling.",
2086
+ description: "Set the maximum buy size in SOL via `PUT /api/wallet/max-trade-size`. The server also mirrors this into `buyAmounts.maxBuyAmountSol` and sets `buyAmountEnforcement` to `hard` when it was `off`, so executes clamp to this max without denying the trade.",
2066
2087
  parameters: Type.Object({
2067
2088
  maxTradeSizeSol: Type.Number({ exclusiveMinimum: 0 })
2068
2089
  }),
@@ -2074,6 +2095,44 @@ ${notes}
2074
2095
  })
2075
2096
  )
2076
2097
  });
2098
+ const buyAmountSolOptional = Type.Union([
2099
+ Type.Number({ exclusiveMinimum: 0 }),
2100
+ Type.Null()
2101
+ ]);
2102
+ api.registerTool({
2103
+ name: "buy_amount_policy_get",
2104
+ description: "Read `buyAmountEnforcement` and `buyAmounts` (fixed / min / max buy in SOL) from the wallet trading policy \u2014 same settings as the dashboard **Buy amount** card. Use when the user enforces sizing so you do not fight precheck/execute clamps.",
2105
+ parameters: Type.Object({}),
2106
+ execute: wrapExecute("buy_amount_policy_get", async () => {
2107
+ const data = await get(`/api/wallet/trading-policy?walletId=${walletId}`);
2108
+ return {
2109
+ buyAmountEnforcement: data.buyAmountEnforcement ?? "off",
2110
+ buyAmounts: data.buyAmounts ?? {}
2111
+ };
2112
+ })
2113
+ });
2114
+ api.registerTool({
2115
+ name: "buy_amount_policy_set",
2116
+ description: "Update buy sizing policy (`PATCH /api/wallet/trading-policy`). `hard` clamps each buy to fixed or min/max SOL without denying for size alone. `soft` only adds warnings; requested size still runs. Pass `null` for a bound to clear it.",
2117
+ parameters: Type.Object({
2118
+ buyAmountEnforcement: Type.Optional(
2119
+ Type.Union([Type.Literal("off"), Type.Literal("soft"), Type.Literal("hard")])
2120
+ ),
2121
+ fixedBuyAmountSol: Type.Optional(buyAmountSolOptional),
2122
+ minBuyAmountSol: Type.Optional(buyAmountSolOptional),
2123
+ maxBuyAmountSol: Type.Optional(buyAmountSolOptional)
2124
+ }),
2125
+ execute: wrapExecute("buy_amount_policy_set", async (_id, params) => {
2126
+ const body = { walletId };
2127
+ if (params.buyAmountEnforcement !== void 0) body.buyAmountEnforcement = params.buyAmountEnforcement;
2128
+ const buyAmounts = {};
2129
+ if (params.fixedBuyAmountSol !== void 0) buyAmounts.fixedBuyAmountSol = params.fixedBuyAmountSol;
2130
+ if (params.minBuyAmountSol !== void 0) buyAmounts.minBuyAmountSol = params.minBuyAmountSol;
2131
+ if (params.maxBuyAmountSol !== void 0) buyAmounts.maxBuyAmountSol = params.maxBuyAmountSol;
2132
+ if (Object.keys(buyAmounts).length) body.buyAmounts = buyAmounts;
2133
+ return patch("/api/wallet/trading-policy", body);
2134
+ })
2135
+ });
2077
2136
  api.registerTool({
2078
2137
  name: "risk_management_set_default",
2079
2138
  description: "Save per-wallet default exit parameters (`tpExits`, `slExits`, `trailingStop.levels`) applied on buys that omit risk fields. Same shape as trade execute exit payloads.",
@@ -2460,9 +2519,11 @@ ${notes}
2460
2519
  })
2461
2520
  )
2462
2521
  });
2522
+ const bitqueryLifetimeState = getOrCreateBitqueryLifetimeState();
2463
2523
  const bitqueryStreamManager = new BitqueryStreamManager({
2464
2524
  wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
2465
2525
  walletId,
2526
+ lifetimeState: bitqueryLifetimeState,
2466
2527
  getAccessToken: () => sessionManager.getAccessToken(),
2467
2528
  logger: {
2468
2529
  info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
@@ -2471,10 +2532,16 @@ ${notes}
2471
2532
  }
2472
2533
  });
2473
2534
  __solanaTraderDisposers.push(() => {
2474
- try {
2475
- bitqueryStreamManager.close();
2476
- } catch {
2477
- }
2535
+ void (async () => {
2536
+ try {
2537
+ await bitqueryStreamManager.unsubscribeAll();
2538
+ } catch {
2539
+ }
2540
+ try {
2541
+ bitqueryStreamManager.close();
2542
+ } catch {
2543
+ }
2544
+ })();
2478
2545
  });
2479
2546
  api.registerTool({
2480
2547
  name: "solana_bitquery_subscribe",
@@ -3021,18 +3088,30 @@ ${notes}
3021
3088
  });
3022
3089
  api.registerTool({
3023
3090
  name: "solana_runtime_status",
3024
- description: "Return plugin runtime diagnostics including startup-gate cache, alpha stream status, and latest forwarding probe result.",
3091
+ description: "Return plugin runtime diagnostics: startup-gate cache, alpha stream (lifetime stats vs current socket), Bitquery mux (lifetime connect count, websocket/auth flags, tracked subscription IDs + mint tokens vs orchestrator diagnostics from solana_bitquery_subscriptions), and latest forwarding probe result. Use subscription_cleanup cron baseline.",
3025
3092
  parameters: Type.Object({}),
3026
- execute: wrapExecute("solana_runtime_status", async () => ({
3027
- startupGate: startupGateState,
3028
- alphaStream: {
3029
- subscribed: alphaStreamManager.isSubscribed(),
3030
- ingestionStale: alphaStreamManager.isIngestionStale(),
3031
- stats: alphaStreamManager.getStats(),
3032
- bufferSize: alphaBuffer.getBufferSize()
3033
- },
3034
- lastForwardProbe: lastForwardProbeState
3035
- }))
3093
+ execute: wrapExecute("solana_runtime_status", async () => {
3094
+ const bitqueryStats = bitqueryStreamManager.getStats();
3095
+ const wsOpen = bitqueryStreamManager.isWebsocketOpen();
3096
+ return {
3097
+ startupGate: startupGateState,
3098
+ alphaStream: {
3099
+ subscribed: alphaStreamManager.isSubscribed(),
3100
+ ingestionStale: alphaStreamManager.isIngestionStale(),
3101
+ stats: alphaStreamManager.getStats(),
3102
+ bufferSize: alphaBuffer.getBufferSize()
3103
+ },
3104
+ bitqueryStream: {
3105
+ stats: bitqueryStats,
3106
+ connected: wsOpen && bitqueryStats.authenticated,
3107
+ websocketOpen: wsOpen,
3108
+ activeSubscriptionCount: bitqueryStats.activeSubscriptionCount,
3109
+ activeSubscriptionIds: bitqueryStreamManager.getActiveSubscriptionIds(),
3110
+ activeTokens: bitqueryStreamManager.getActiveTokens()
3111
+ },
3112
+ lastForwardProbe: lastForwardProbeState
3113
+ };
3114
+ })
3036
3115
  });
3037
3116
  api.registerTool({
3038
3117
  name: "solana_state_save",
@@ -4298,6 +4377,14 @@ ${String(params.summary)}
4298
4377
  content: "# Live alpha status queries \u2014 always call the tool\n\nWhen the user asks anything about CURRENT alpha activity, call the matching tool **on this turn**. Do NOT answer from memory, heartbeat history, journal logs, or earlier turn context \u2014 alpha signals are not journalled per-message, so 'I don't see any / 0' is wrong by default.\n\n## Routing\n\n| User question shape | Tool(s) to call | Key fields in response |\n|---|---|---|\n| how many alpha signals / are we getting alpha / activity since gateway start | `solana_alpha_signals` (unseen:false) | stats.messageCount, stats.lifetimeUptimeSeconds, stats.lastEventTs, subscribed, bufferSize |\n| is alpha connected / healthy / stream status | `solana_alpha_signals` (unseen:false) | subscribed, stats.reconnectAttempt, stats.unhealthyStreak, stats.circuitBackoff, stats.lastEventTs |\n| how many alpha signals in last hour / today / this week / since <day> | `solana_alpha_signals` (live state) **AND** `solana_alpha_history` (days=\u2026 or compute from window) | live: stats.messageCount + bufferSize; historical: pings[] in window |\n| any alpha on token <X> (recent / historical) | `solana_alpha_signals` for in-buffer signals, then `solana_alpha_history` (tokenAddress=X, days=N) for older | both responses merged by ts |\n| what alpha sources / channels are active | `solana_alpha_sources` | sources[] (name, type, count, avgScore) |\n| latest signals / new signals | `solana_alpha_signals` (unseen:false) | signals[] sorted newest last |\n\n## Field meanings \u2014 IMPORTANT\n\n`solana_alpha_signals` returns `stats` with both lifetime and current-WS fields. Use LIFETIME fields for user-facing answers:\n\n- `stats.messageCount` = **lifetime** total alpha_signal messages received since the gateway process started. Survives plugin re-registers and WS reconnects. **This is the headline number.**\n- `stats.lifetimeUptimeSeconds` = seconds since the first WS open in this process.\n- `stats.lastEventTs` = wall-clock ms of the most recent signal (lifetime).\n- `stats.firstConnectedAt` = wall-clock ms of the first WS open in this process.\n\nDebug-only (do NOT report these as 'totals' \u2014 they reset on every WS reconnect / plugin re-register):\n\n- `stats.currentWsMessageCount` \u2014 messages since the current WS opened.\n- `stats.uptimeSeconds` \u2014 current WS uptime.\n- `stats.connectedAt` \u2014 current WS open ts.\n\n## When to also call `solana_alpha_history`\n\nCall `solana_alpha_history` in addition to `solana_alpha_signals` whenever the user's window pre-dates the current gateway-process lifetime (e.g. `stats.lifetimeUptimeSeconds` is shorter than the asked window, or `stats.messageCount` is 0 because the gateway just restarted). Tier=enterprise returns up to 200 pings, up to ~1 year back.\n\n## Reply template (live-only)\n\n```\nLive alpha state:\n- subscribed: <bool>\n- lifetime: <messageCount> messages over <lifetimeUptimeSeconds>s (\u2248 <rate>/min) since gateway start\n- last signal: <Xs/Xm ago> (lastEventTs)\n- buffer (deduped): <bufferSize>\n- reconnects: <reconnectAttempt>, unhealthy streak: <unhealthyStreak>\n```\n\n## Reply template (with historical window, e.g. 'last hour')\n\n```\nAlpha activity (<window>):\n- live (gateway lifetime): <messageCount> messages over <lifetimeUptimeSeconds>s\n- historical (`solana_alpha_history` days=<N>): <pings.length> pings\n- combined unique within <window>: <merged count>\n- subscribed: <bool>, last signal: <ago>\n```\n\nThis routing overrides any heartbeat-cycle 'minimal calls' cap: ad-hoc user status questions are NOT subject to per-cycle envelopes.\n",
4299
4378
  source: "solana-trader:live-queries"
4300
4379
  });
4380
+ if (__solanaTraderStatusQueriesMd) {
4381
+ context.bootstrapFiles.push({
4382
+ name: "STATUS_QUERIES.md",
4383
+ path: "STATUS_QUERIES.md",
4384
+ content: __solanaTraderStatusQueriesMd,
4385
+ source: "solana-trader:status-queries"
4386
+ });
4387
+ }
4301
4388
  api.logger.info(`[solana-trader] Bootstrap: injected ${context.bootstrapFiles.length} files for agent ${bootAgentId}`);
4302
4389
  },
4303
4390
  {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  BitqueryStreamManager
3
- } from "../chunk-S2DLZKMQ.js";
3
+ } from "../chunk-QIURFAOS.js";
4
4
  export {
5
5
  BitqueryStreamManager
6
6
  };
@@ -0,0 +1,103 @@
1
+ # Live status queries — always call the tool, never answer from memory
2
+
3
+ When the user asks any question about **current alpha stream state**, you MUST call the matching plugin tool **on this turn**, then summarize what the tool returned. Do NOT answer from memory, heartbeat history, journal logs, log snippets, or earlier conversation context: alpha counts grow second-by-second and stale answers (especially "none" / "zero") are wrong by default.
4
+
5
+ ## Why this rule exists
6
+
7
+ The plugin does NOT log every received alpha signal to the journal (that would be noisy at sustained throughput). Signal counts and the buffer of recent signals live **only inside the running plugin process** and are exposed via these tools. If you don't call the tool, you have no data — you only have the absence of log lines, which is not the same thing.
8
+
9
+ The heartbeat history (`memory/<date>.md`, `MEMORY.md`) only captures signals the agent **personally evaluated** during a heartbeat cycle. It is NOT a record of what the WebSocket received. Using heartbeat history to answer "how many signals" gives a count that is typically much smaller than the real number (because most signals never reach a cycle where the agent processes them).
10
+
11
+ ## Alpha question → tool routing
12
+
13
+ When the user's question matches a row in column 1, call the tool(s) in column 2 (always on this turn, before replying), read the response, and answer from that data.
14
+
15
+ | User question (or similar) | Call this tool | What to report back |
16
+ |---|---|---|
17
+ | "how many alpha signals have we gotten?" / "are we getting alpha?" / "anything from alpha lately?" | `solana_alpha_signals` (with `unseen: false`) | `stats.messageCount` (LIFETIME — survives re-registers/reconnects), `stats.lifetimeUptimeSeconds`, `stats.lastEventTs`, `subscribed`, `bufferSize` |
18
+ | "what's the alpha stream doing right now?" / "is alpha connected?" / "alpha health?" | `solana_alpha_signals` (with `unseen: false`) | `subscribed`, `stats.reconnectAttempt`, `stats.unhealthyStreak`, `stats.circuitBackoff`, `stats.lastEventTs` (interpret as "Xs ago") |
19
+ | "how many alpha signals in the last hour / today / this week / since Monday?" | `solana_alpha_signals` (live) **AND** `solana_alpha_history` (`days=` covering the window) | Live: `stats.messageCount` since gateway start + signals in `signals[]` within the window. Historical: pings in window from REST. Report both. |
20
+ | "any alpha on `<token>` recently / in the last 24h?" | `solana_alpha_signals` (filter `signals[]` by `tokenAddress`/`tokenSymbol`) **AND** `solana_alpha_history` (`tokenAddress=<addr>&days=N`) | Merge live + historical signals for that token by timestamp. |
21
+ | "what alpha sources are active?" / "which channels are sending signals?" | `solana_alpha_sources` | `sources` array — name, type, count, avgScore per channel |
22
+ | "show me the latest alpha signals" / "new signals?" | `solana_alpha_signals` (with `unseen: false` for full buffer, `unseen: true` only if you intend to consume) | Up to N signals: token, source, kind, signalStage, marketCap, systemScore, ts |
23
+
24
+ ## `stats` fields — what to use vs. what to ignore
25
+
26
+ `solana_alpha_signals` returns a `stats` object with both **lifetime** and **per-current-WS** fields. **Always quote the lifetime fields to the user.**
27
+
28
+ USE these for user-facing answers (stable across plugin re-registers and WS reconnects):
29
+
30
+ - `stats.messageCount` — **lifetime** total alpha_signal messages received since the gateway process started.
31
+ - `stats.lifetimeUptimeSeconds` — seconds since the first WebSocket open in this gateway process.
32
+ - `stats.lastEventTs` — wall-clock ms of the most recent signal (lifetime).
33
+ - `stats.firstConnectedAt` — wall-clock ms of the first WS open in this process.
34
+
35
+ DO NOT use as "totals" (they reset on every WS reconnect / plugin re-register, which happens every agent turn — so the numbers are misleading as standalone answers):
36
+
37
+ - `stats.currentWsMessageCount` — messages since the CURRENT WS opened. Useful only for debugging "why is the WS cycling?".
38
+ - `stats.uptimeSeconds` — current WS uptime.
39
+ - `stats.connectedAt` — current WS open ts.
40
+
41
+ ## When to also call `solana_alpha_history`
42
+
43
+ The live tool gives you "messages since this gateway process started" and a buffer of ~200 deduped signals. For windows that exceed those:
44
+
45
+ - The asked window predates `stats.firstConnectedAt` (e.g. user asks "last 24h" but gateway started 2h ago).
46
+ - The user asks about a specific date or named window ("yesterday", "since Monday", "last week").
47
+ - The buffer is empty (e.g. just after a gateway restart) but the user is asking about a real time range.
48
+
49
+ …ALSO call `solana_alpha_history` (`days=N` covering the window) and combine the two responses. Tier=enterprise → up to 200 results back ~1 year.
50
+
51
+ ## How to answer (template — live-only)
52
+
53
+ After the tool returns, structure your reply like this so the user can verify you queried live data:
54
+
55
+ ```
56
+ Live alpha state (as of <now>):
57
+ - subscribed: <subscribed>
58
+ - lifetime: <stats.messageCount> messages over <stats.lifetimeUptimeSeconds>s (≈ <rate>/min) since gateway start
59
+ - last signal: <stats.lastEventTs as "Xs/Xm ago">
60
+ - buffer (deduped, ≤200): <bufferSize>
61
+ - reconnects: <stats.reconnectAttempt>, unhealthy streak: <stats.unhealthyStreak>
62
+
63
+ Top sources (from `solana_alpha_sources`):
64
+ - <sourceName> (<sourceType>): <count> signals, avg score <avgScore>
65
+
66
+ Sample of latest signals (newest first):
67
+ - <tokenSymbol> (<tokenName>) — <kind>/<signalStage>, MC $<marketCap>, score <systemScore>, from <sourceName>, <ts ago>
68
+ ```
69
+
70
+ ## How to answer (template — when window > gateway lifetime)
71
+
72
+ ```
73
+ Alpha activity (<window>):
74
+ - live (gateway lifetime, <lifetimeUptimeSeconds>s): <messageCount> messages, currently <bufferSize> in buffer
75
+ - historical (`solana_alpha_history` days=<N>): <pings.length> pings in window
76
+ - combined unique: <merged count>
77
+ - subscribed: <bool>, last signal: <ago>
78
+
79
+ Top historical sources / tokens: …
80
+ ```
81
+
82
+ ## Common mistakes to avoid
83
+
84
+ - **Don't answer "zero" or "none" without calling the tool.** "I don't see any in the logs / heartbeat history" is wrong — signals are NOT logged per-message and heartbeat history only captures what the agent itself touched.
85
+ - **Don't reuse a count from earlier in the conversation.** The buffer is continuously updated; quote only freshly-fetched numbers.
86
+ - **Don't quote `currentWsMessageCount` or `uptimeSeconds` as "totals".** Those reset every time you (or anyone) interacts with the agent, because each interaction re-registers the plugin. Always use `stats.messageCount` and `stats.lifetimeUptimeSeconds` for user-facing answers.
87
+ - **Don't mix heartbeat history with live tool data without flagging it.** If the user asks "how many in the last hour" and you can only see live state, say so and call `solana_alpha_history` to fill the window.
88
+ - **Don't claim "the stream is offline" from log gaps alone.** Check `subscribed` and `stats.lastEventTs` — a stream can be healthy with low-traffic minutes.
89
+ - **Don't paste raw JSON to the user.** Summarize per the templates above; offer raw JSON only if asked.
90
+
91
+ ## When the user does NOT need a tool call
92
+
93
+ These are NOT live-state queries; answer from your knowledge:
94
+
95
+ - "how does the alpha stream work?" (explain the design from `skills/solana-trader/refs/alpha-signals.md` if available)
96
+ - "what's a `ca_drop`?" (definition)
97
+ - "which alpha sources are premium?" (static info from refs)
98
+
99
+ ## Related
100
+
101
+ - `HEARTBEAT.md` — per-heartbeat cycle envelope (separate from ad-hoc Q&A).
102
+ - `skills/solana-trader/refs/alpha-signals.md` — full alpha pipeline reference.
103
+ - Tool catalog: `solana_alpha_subscribe`, `solana_alpha_unsubscribe`, `solana_alpha_signals`, `solana_alpha_sources`, `solana_alpha_history`.
@@ -252,6 +252,8 @@
252
252
  "x_search_tweets",
253
253
  "risk_management_get_default",
254
254
  "risk_management_set_default",
255
+ "buy_amount_policy_get",
256
+ "buy_amount_policy_set",
255
257
  "trade_size_limit_get",
256
258
  "trade_size_limit_set",
257
259
  "position_risk_management_update"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.146",
3
+ "version": "1.0.148",
4
4
  "description": "TraderClaw V1-Upgraded — Solana trading for OpenClaw with intelligence lab, tool envelopes, prompt scrubbing, read-only X social intel, and split skill docs",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -75,12 +75,22 @@ Configured by the user on the **Buy Strategy** page. Checks token metrics before
75
75
 
76
76
  **Agent impact:** When `hard`, if you try to buy a token outside user bounds, the orchestrator returns a denial. Do not retry with the same token. Report the bound that was exceeded.
77
77
 
78
- ### Soft-Enforced Limits (size reduction, not denial)
78
+ ### Buy Amount Policy (`buyAmountEnforcement`)
79
+
80
+ Configured on the **Buy Strategy** page (SOL sizing). This is separate from **buy filters** (market cap, liquidity, etc.).
79
81
 
80
- Some limits adjust position size rather than deny outright:
82
+ | Mode | Behavior |
83
+ |---|---|
84
+ | `off` | No discretionary buy-amount policy. **Exception:** if the user previously set only legacy `maxTradeSizeSol` / `trade_size_limit_set` with no `buyAmounts` keys, the orchestrator still clamps buys to that maximum. |
85
+ | `soft` | Buy runs at your requested `sizeSol`, but precheck/execute include **warnings** in `metadata.reasons` when fixed/min/max bounds are violated. |
86
+ | `hard` | Buy **size is clamped** to the configured fixed SOL amount, or bounded by min/max SOL. Trades are **not** denied for sizing alone. Precheck returns `metadata.cappedSizeSol`; when it changed, `metadata.buyAmountAdjusted` is true and execute may include `policySizing`. |
87
+
88
+ **Tools:** `buy_amount_policy_get`, `buy_amount_policy_set`, and `trade_size_limit_get` / `trade_size_limit_set` (max SOL, mirrored into `buyAmounts`).
89
+
90
+ ### Soft-Enforced Limits (size reduction, not denial)
81
91
 
82
- - **Top-10 holder concentration** (`maxTop10ConcentrationPct` in buy filters, if `soft`): When the top 10 wallets own too high a percentage, the orchestrator halves the proposed buy size.
83
- - **Max position USD** (`maxPositionUsd`): Orchestrator caps buy size to this limit silently if your proposed size exceeds it.
92
+ - **Buy amount `hard` mode** and **legacy max trade size** clamp executed buy size. Treat precheck `metadata.cappedSizeSol` as authoritative when present; align execute `sizeSol` with that value to avoid policy drift.
93
+ - Older notes about automatic halving from top-10 concentration or silent `maxPositionUsd` caps may not match every response **always trust `solana_trade_precheck` metadata** over static tables.
84
94
 
85
95
  ### Risk Exit Enforcement (`riskEnforcement`)
86
96
 
@@ -719,3 +729,4 @@ All decision making, evaluation, and learning MUST use SOL-based values.
719
729
  | `bitquery-schema.md` | Bitquery v2 EAP schema reference |
720
730
  | `query-catalog.md` | Bitquery query template catalog |
721
731
  | `websocket-streaming.md` | WebSocket message contract and subscription lifecycle |
732
+ | `refs/ws-subscription-health.md` | Cron + mux health, 20-cap, leaked-socket escalation |
@@ -95,13 +95,22 @@ At start of every cron job, check whether sufficient new data exists since last
95
95
 
96
96
  ## Job: `subscription_cleanup`
97
97
 
98
- **Schedule:** Every 8 hours, offset by 15 min (`15 */8 * * *`) — 3 runs/day
98
+ **Schedule:** Every 2 hours, offset by 15 min (`15 */2 * * *`) — ~12 runs/day
99
99
 
100
- **Purpose:** Manage Bitquery subscription lifecycle — remove orphaned subscriptions, reopen expiring ones.
100
+ **Purpose:** Keep Bitquery subscription count healthy — remove orphaned subscriptions, reopen expiring streams, reconcile against **open positions**, and guard the **under-20 logical subscription cap** per client. Helps avoid `WS_SUBSCRIPTION_LIMIT_REACHED`; **never** unsubscribes alpha.
101
101
 
102
- **Tools:** `solana_positions`, `solana_bitquery_subscriptions`, `solana_bitquery_unsubscribe`, `solana_bitquery_subscription_reopen`, `solana_memory_write`
102
+ **Tools:** `solana_runtime_status`, `solana_positions`, `solana_bitquery_subscriptions`, `solana_bitquery_subscribe`, `solana_bitquery_unsubscribe`, `solana_bitquery_subscription_reopen`, `solana_memory_write`
103
103
 
104
- **Workflow:** List open position CAs → list active subs (if AUTH_SCOPE_MISSING, log and stop) → match subs to positions → unsubscribe orphans → write tag 'subscription_cleanup'.
104
+ **Workflow:**
105
+ 1. `solana_runtime_status` → local alpha + Bitquery mux snapshot (`bitqueryStream`).
106
+ 2. `solana_positions` → open mint addresses.
107
+ 3. `solana_bitquery_subscriptions` → orchestrator diagnostics (subscriber counts / connected websocket clients — field names mirror API response).
108
+ 4. Unsubscribe mismatched streams → subscribe missing `realtimeTokenPricesSolana` with **`agentId` matching the cron job's agent**.
109
+ 5. `solana_bitquery_subscription_reopen` for subs nearing TTL.
110
+ 6. If limits persist (`WS_PER_KEY_LIMIT`, stalled counts, absurd client fan-out) log **CRITICAL** via `solana_memory_write` — leaked TCP sockets may require a **manual gateway restart** (outside agent tools).
111
+ 7. Memory tag **`subscription_cleanup`** with before / after telemetry.
112
+
113
+ See also: `refs/ws-subscription-health.md`.
105
114
 
106
115
  **Configuration:**
107
116
  - Model: Haiku (mechanical — match subs to positions, unsubscribe orphans)
@@ -209,9 +218,9 @@ At start of every cron job, check whether sufficient new data exists since last
209
218
  | 2 | `trust-refresh` | `0 */8 * * *` | 3 | Haiku | off | on | none |
210
219
  | 3 | `meta-rotation` | `30 */8 * * *` | 3 | Sonnet | off | on | announce/last |
211
220
  | 4 | `strategy-evolution` | `0 6 * * *` | 1 | Sonnet | **on** | **off** | announce/last |
212
- | 5 | `subscription-cleanup` | `15 */8 * * *` | 3 | Haiku | off | on | announce/last |
221
+ | 5 | `subscription-cleanup` | `15 */2 * * *` | ~12 | Haiku | off | on | announce/last |
213
222
  | 6 | `daily-performance-report` | `0 4 * * *` | 1 | Sonnet | off | **off** | announce/telegram |
214
223
  | 7 | `intelligence-lab-eval` | `0 16 * * *` | 1 | Sonnet | **on** | **off** | none |
215
224
  | 8 | `memory-trim` | `0 3 * * *` | 1 | Haiku | off | on | none |
216
225
  | 9 | `balance-watchdog` | `0 */2 * * *` | 12 | Haiku | off | on | announce/telegram |
217
- | | **Total** | | **31** | | | | |
226
+ | | **Total** | | **40** | | | | |
@@ -5,8 +5,8 @@
5
5
  Call `solana_trade_precheck` with your intended trade parameters.
6
6
 
7
7
  - **If `approved: false` with hard denials:** STOP. Do not trade. Journal the denial reason.
8
- - **If approved with soft flags:** Reduce size to `cappedSizeSol`. Consider SERVER_MANAGED. Tighten stops.
9
- - **If approved cleanly:** Proceed to execute.
8
+ - **If approved with clamps or sizing notes:** When precheck sets `metadata.cappedSizeSol`, use it as the executed buy size for the matching `trade_execute`. If `metadata.buyAmountAdjusted` is true (or execute returns `policySizing`), the orchestrator changed your requested `sizeSol` (buy-amount policy **hard** mode or legacy max). For buy-amount **soft** mode, you may keep your requested size but review `metadata.reasons`.
9
+ - **If approved cleanly:** Proceed to execute with the intended size (still reconcile with `cappedSizeSol` when the server provides one).
10
10
 
11
11
  **Non-negotiable:** Never override hard denials. Never argue with the policy engine.
12
12
 
@@ -0,0 +1,40 @@
1
+ # WebSocket + subscription health
2
+
3
+ Use this alongside `websocket-streaming.md` when diagnosing Bitquery churn, orphaned subscriptions, or `WS_PER_KEY_LIMIT` / subscription-cap errors.
4
+
5
+ ## Two connection types
6
+
7
+ 1. **Alpha stream** — one WebSocket carrying buffered `alpha_signal` traffic. Managed by `solana_alpha_subscribe`; the plugin watchdog reconnects automatically. Cron and subscription cleanup **must never** call `solana_alpha_unsubscribe` unless deliberately shutting alpha down.
8
+
9
+ 2. **Bitquery mux** — one WebSocket from the gateway plugin carries many **logical subscriptions** (`bitquery_subscribe` / `bitquery_unsubscribe`). The orchestrator multiplexes upstream Bitquery streams. Steady state is typically **one** plugin Bitquery client + **one** alpha client (**~2 TCP connections**) to TraderClaw, not one socket per mint.
10
+
11
+ ## Health signals
12
+
13
+ **Good**
14
+
15
+ - Orchestrator diagnostics (`solana_bitquery_subscriptions`): Bitquery subscriber counts aligned with **open positions** (usually one `realtimeTokenPricesSolana` per mint you hold).
16
+ - Active Bitquery subscriptions **below 20** per client (`OPENCLAW_WS_MAX_SUBS_PER_CLIENT` default — see `websocket-streaming.md`).
17
+ - Plugin `solana_runtime_status.bitqueryStream` matches intuition: sane `activeSubscriptionCount`, tokens match `solana_positions` mints.
18
+
19
+ **Bad**
20
+
21
+ - Subscriptions still listed for mints **without** an open position (heartbeat Step 7 missed or failed).
22
+ - `activeSubscriptionCount` **at or above 20**, or subscribe errors **`WS_SUBSCRIPTION_LIMIT_REACHED`**.
23
+ - **`WS_PER_KEY_LIMIT`** or very high **TCP** connection counts to `api.traderclaw.ai` (**25** API-key ceiling) despite few logical subs — indicates **orphan TCP / leaked clients** across plugin lifecycle. Unsubscribe tools alone cannot always drop leaked sockets.
24
+
25
+ ## What `subscription_cleanup` does
26
+
27
+ Scheduled job **`subscription_cleanup`** (OpenClaw cron store, id `subscription-cleanup`):
28
+
29
+ - Baseline via `solana_runtime_status`, `solana_positions`, `solana_bitquery_subscriptions`.
30
+ - Unsubscribe Bitquery subscriptions whose token is **not** in open positions (`solana_bitquery_unsubscribe`).
31
+ - Subscribe **`realtimeTokenPricesSolana`** for any open CA missing coverage; pass **`agentId` matching your trading agent** (job agent id — `main` on V1 installs).
32
+ - Reopen nearing-expiry subscriptions (`solana_bitquery_subscription_reopen`).
33
+ - Writes memory tag **`subscription_cleanup`** with before/after metrics.
34
+
35
+ Escalate with **CRITICAL** in memory when limits persist after reconcile; **restart the gateway** is the corrective action for leaked TLS sockets (`systemctl --user restart openclaw-gateway`).
36
+
37
+ ## Operational rule of thumb
38
+
39
+ - **Logical Bitquery subscriptions** should mirror **open positions** (typically one price stream per held mint). Keep count **below 20** (`OPENCLAW_WS_MAX_SUBS_PER_CLIENT`).
40
+ - **Outbound TLS sessions** from the gateway toward TraderClaw should stay near **two** — one mux for Bitquery, one for Alpha — unless you know you intentionally run multiple clients.
@@ -127,3 +127,21 @@ These are permanent directives. They apply every session, every cycle, without e
127
127
  4. **Always check kill switch before new entries.** Call `solana_killswitch_status()` before any new trade execution. If active, halt immediately — no exceptions, no overrides.
128
128
 
129
129
  5. **Always report every cycle.** Never run a silent heartbeat. Every cycle produces a report: what you scanned, what you found, what you did, what you skipped and why. Crypto is 24/7. Every cycle reports.
130
+
131
+ ## Live status questions → ALWAYS call the tool
132
+
133
+ When the user asks about the **current** state of the alpha stream (counts, sources, recent signals, subscription health, or any time window), you MUST call the matching plugin tool **on the same turn** before replying. Do not answer "zero / none / I don't see anything" from memory, heartbeat history, journal logs, or log impressions — signals are not logged per-message; they live only inside the running plugin (live state) and the orchestrator REST (historical) and are surfaced via tools.
134
+
135
+ See `STATUS_QUERIES.md` (auto-injected by the plugin at every register) for the full question → tool routing table, field semantics (lifetime vs. current-WS), and the standard answer template. The short version:
136
+
137
+ | Question shape | Tool(s) to call |
138
+ |---|---|
139
+ | "how many alpha signals / are we getting alpha?" | `solana_alpha_signals` (`unseen: false`) — quote `stats.messageCount` (LIFETIME, survives re-registers) and `stats.lifetimeUptimeSeconds` |
140
+ | "is alpha connected / healthy?" | `solana_alpha_signals` (`unseen: false`) — read `subscribed`, `stats.lastEventTs`, `stats.reconnectAttempt`, `stats.unhealthyStreak`, `stats.circuitBackoff` |
141
+ | "how many in last hour / today / since `<day>`?" / "any alpha on `<token>` in 24h?" | `solana_alpha_signals` (live) **AND** `solana_alpha_history` (`days=N` covering the window, `tokenAddress=` when filtering) |
142
+ | "which alpha sources / channels?" | `solana_alpha_sources` |
143
+ | "latest / new alpha signals?" | `solana_alpha_signals` (with or without `unseen: true` depending on intent) |
144
+
145
+ Field reminder: use `stats.messageCount` and `stats.lifetimeUptimeSeconds` as the headline numbers. Do NOT quote `stats.currentWsMessageCount` or `stats.uptimeSeconds` as "totals" — they reset on every plugin re-register (every agent turn / Telegram message).
146
+
147
+ This rule applies to all sessions — direct chats, Telegram, anywhere the user asks. It overrides the heartbeat-cycle envelope: ad-hoc live-state questions are NOT subject to the "minimal per-cycle calls" cap.
@@ -27,7 +27,7 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
27
27
  | `solana_gateway_credentials_set` | Register gateway URL and token | When gateway is not registered (startup) |
28
28
  | `solana_gateway_forward_probe` | Test forwarding path health | Startup sequence; when events stop arriving |
29
29
 
30
- ### Wallet & Capital (11)
30
+ ### Wallet & Capital (13)
31
31
  | Tool | Purpose | When to Call |
32
32
  |---|---|---|
33
33
  | `solana_wallets` | List all wallets | Startup; when user asks about wallets |
@@ -39,8 +39,10 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
39
39
  | `solana_killswitch_status` | Check kill switch state | Step 0 every heartbeat |
40
40
  | `risk_management_get_default` | Read per-wallet default TP/SL/trailing used when buys omit risk | Before relying on implicit defaults; after user asks about protection |
41
41
  | `risk_management_set_default` | Save per-wallet default exit plan for future buys | When user or policy wants custom defaults instead of platform system default |
42
- | `trade_size_limit_get` | Read max **buy** size (SOL) from wallet `limits` (default 1.5) | Before every buy or when user asks about size limits |
43
- | `trade_size_limit_set` | Set max **buy** size (SOL) on wallet `limits` | When user asks to change the per-order cap |
42
+ | `trade_size_limit_get` | Read effective max **buy** size (SOL); merges legacy max with `buyAmounts.max` | Before every buy or when user asks about size limits |
43
+ | `trade_size_limit_set` | Set max buy SOL (also mirrors into buy-amount max + hard enforcement when off) | When user asks to change the per-order cap |
44
+ | `buy_amount_policy_get` | Read `buyAmountEnforcement` and fixed/min/max buy SOL | Before sizing when user may clamp or warn on amount |
45
+ | `buy_amount_policy_set` | Update buy amount policy (off/soft/hard + bounds) | When user configures buy sizing on the Buy Strategy card |
44
46
  | `position_risk_management_update` | Adjust TP/SL/trailing **numbers** on an open position (same level count) | After entry when refining exits without removing levels |
45
47
 
46
48
  ### Scanning & Discovery (4)
@@ -120,9 +122,9 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
120
122
  | `solana_bitquery_catalog` | Run pre-built template | Step 2: MANDATORY for FRESH tokens (first100Buyers, devHoldings); Step 2: website metadata check for tokens >0.60 |
121
123
  | `solana_bitquery_query` | Run custom raw GraphQL | When no catalog template fits your query need |
122
124
  | `solana_bitquery_subscribe` | Subscribe to real-time stream | Step 5: after every successful buy (realtimeTokenPricesSolana); startup for discovery streams |
123
- | `solana_bitquery_unsubscribe` | Unsubscribe from stream | Step 7: after every exit; `subscription_cleanup` cron for orphaned subscriptions |
124
- | `solana_bitquery_subscriptions` | List active subscriptions | Step 1: check for buffered events; `subscription_cleanup` cron |
125
- | `solana_bitquery_subscription_reopen` | Renew expiring subscription | `subscription_cleanup` cron for subscriptions nearing 24h expiry |
125
+ | `solana_bitquery_unsubscribe` | Unsubscribe from stream | Step 7: after every exit; `subscription_cleanup` cron (`refs/ws-subscription-health.md`) |
126
+ | `solana_bitquery_subscriptions` | List active subscriptions | Step 1: check for buffered events; `subscription_cleanup` cron diagnostics |
127
+ | `solana_bitquery_subscription_reopen` | Renew expiring subscription | `subscription_cleanup` cron (24h TTL) |
126
128
 
127
129
  ### Memory & State (13)
128
130
  | Tool | Purpose | When to Call |
@@ -191,7 +193,7 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
191
193
  ### Runtime (3)
192
194
  | Tool | Purpose | When to Call |
193
195
  |---|---|---|
194
- | `solana_runtime_status` | Plugin runtime health | Diagnostics; when tools behave unexpectedly |
196
+ | `solana_runtime_status` | Plugin runtime health (alpha + Bitquery mux) | Diagnostics; `subscription_cleanup` cron baseline (`refs/ws-subscription-health.md`) |
195
197
  | `solana_agent_sessions` | List agent sessions | Diagnostics; when checking session state |
196
198
  | `solana_classify_deployer_risk` | Deployer risk (alias) | Same as `solana_compute_deployer_risk` — use either |
197
199