solana-traderclaw 1.0.147 → 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";
@@ -784,6 +784,9 @@ var SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY = Symbol.for(
784
784
  var SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY = Symbol.for(
785
785
  "openclaw.solana-trader.alpha-lifetime.v1"
786
786
  );
787
+ var SOLANA_TRADER_BITQUERY_LIFETIME_SINGLETON_KEY = Symbol.for(
788
+ "openclaw.solana-trader.bitquery-lifetime.v1"
789
+ );
787
790
  var __solanaTraderAlphaSingletonHolder = globalThis;
788
791
  function getOrCreateAlphaBuffer() {
789
792
  const existing = __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_BUFFER_SINGLETON_KEY];
@@ -803,6 +806,13 @@ function getOrCreateAlphaLifetimeState() {
803
806
  __solanaTraderAlphaSingletonHolder[SOLANA_TRADER_ALPHA_LIFETIME_SINGLETON_KEY] = fresh;
804
807
  return fresh;
805
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
+ }
806
816
  var __solanaTraderStatusQueriesMd = (() => {
807
817
  try {
808
818
  const distPath = fileURLToPath(import.meta.url);
@@ -2064,7 +2074,7 @@ ${notes}
2064
2074
  });
2065
2075
  api.registerTool({
2066
2076
  name: "trade_size_limit_get",
2067
- 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.",
2068
2078
  parameters: Type.Object({}),
2069
2079
  execute: wrapExecute(
2070
2080
  "trade_size_limit_get",
@@ -2073,7 +2083,7 @@ ${notes}
2073
2083
  });
2074
2084
  api.registerTool({
2075
2085
  name: "trade_size_limit_set",
2076
- 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.",
2077
2087
  parameters: Type.Object({
2078
2088
  maxTradeSizeSol: Type.Number({ exclusiveMinimum: 0 })
2079
2089
  }),
@@ -2085,6 +2095,44 @@ ${notes}
2085
2095
  })
2086
2096
  )
2087
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
+ });
2088
2136
  api.registerTool({
2089
2137
  name: "risk_management_set_default",
2090
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.",
@@ -2471,9 +2519,11 @@ ${notes}
2471
2519
  })
2472
2520
  )
2473
2521
  });
2522
+ const bitqueryLifetimeState = getOrCreateBitqueryLifetimeState();
2474
2523
  const bitqueryStreamManager = new BitqueryStreamManager({
2475
2524
  wsUrl: orchestratorUrl.replace(/^http/, "ws").replace(/\/$/, "") + "/ws",
2476
2525
  walletId,
2526
+ lifetimeState: bitqueryLifetimeState,
2477
2527
  getAccessToken: () => sessionManager.getAccessToken(),
2478
2528
  logger: {
2479
2529
  info: (msg) => api.logger.info(`[solana-trader] ${msg}`),
@@ -2482,10 +2532,16 @@ ${notes}
2482
2532
  }
2483
2533
  });
2484
2534
  __solanaTraderDisposers.push(() => {
2485
- try {
2486
- bitqueryStreamManager.close();
2487
- } catch {
2488
- }
2535
+ void (async () => {
2536
+ try {
2537
+ await bitqueryStreamManager.unsubscribeAll();
2538
+ } catch {
2539
+ }
2540
+ try {
2541
+ bitqueryStreamManager.close();
2542
+ } catch {
2543
+ }
2544
+ })();
2489
2545
  });
2490
2546
  api.registerTool({
2491
2547
  name: "solana_bitquery_subscribe",
@@ -3032,18 +3088,30 @@ ${notes}
3032
3088
  });
3033
3089
  api.registerTool({
3034
3090
  name: "solana_runtime_status",
3035
- 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.",
3036
3092
  parameters: Type.Object({}),
3037
- execute: wrapExecute("solana_runtime_status", async () => ({
3038
- startupGate: startupGateState,
3039
- alphaStream: {
3040
- subscribed: alphaStreamManager.isSubscribed(),
3041
- ingestionStale: alphaStreamManager.isIngestionStale(),
3042
- stats: alphaStreamManager.getStats(),
3043
- bufferSize: alphaBuffer.getBufferSize()
3044
- },
3045
- lastForwardProbe: lastForwardProbeState
3046
- }))
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
+ })
3047
3115
  });
3048
3116
  api.registerTool({
3049
3117
  name: "solana_state_save",
@@ -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
  };
@@ -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.147",
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.
@@ -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