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.
- package/config/gateway-v1-upgraded.json5 +2 -2
- package/config/gateway-v1.json5 +2 -2
- package/dist/{chunk-S2DLZKMQ.js → chunk-QIURFAOS.js} +56 -5
- package/dist/index.js +105 -18
- package/dist/src/bitquery-ws.js +1 -1
- package/lib/status-queries.md +103 -0
- package/openclaw.plugin.json +2 -0
- package/package.json +1 -1
- package/skills/solana-trader/SKILL.md +15 -4
- package/skills/solana-trader/refs/cron-jobs.md +15 -6
- package/skills/solana-trader/refs/trade-execution.md +2 -2
- package/skills/solana-trader/refs/ws-subscription-health.md +40 -0
- package/skills/solana-trader/workspace/AGENTS.md +18 -0
- package/skills/solana-trader/workspace/TOOLS.md +9 -7
|
@@ -80,11 +80,11 @@
|
|
|
80
80
|
// ── Portfolio Maintenance ───────────────────────────────────────
|
|
81
81
|
{
|
|
82
82
|
id: "subscription-cleanup",
|
|
83
|
-
schedule: "15 */
|
|
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\
|
|
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
|
|
package/config/gateway-v1.json5
CHANGED
|
@@ -69,11 +69,11 @@
|
|
|
69
69
|
// ── Portfolio Maintenance ───────────────────────────────────────
|
|
70
70
|
{
|
|
71
71
|
id: "subscription-cleanup",
|
|
72
|
-
schedule: "15 */
|
|
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\
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
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
|
|
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
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
{
|
package/dist/src/bitquery-ws.js
CHANGED
|
@@ -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`.
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "solana-traderclaw",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
- **
|
|
83
|
-
-
|
|
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
|
|
98
|
+
**Schedule:** Every 2 hours, offset by 15 min (`15 */2 * * *`) — ~12 runs/day
|
|
99
99
|
|
|
100
|
-
**Purpose:**
|
|
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:**
|
|
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 */
|
|
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** | | **
|
|
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
|
|
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 (
|
|
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)
|
|
43
|
-
| `trade_size_limit_set` | Set max
|
|
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
|
|
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
|
|
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;
|
|
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
|
|