solana-traderclaw 1.0.99 → 1.0.101

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/README.md CHANGED
@@ -113,6 +113,8 @@ Run mandatory startup sequence and report pass/fail for each:
113
113
  6) solana_killswitch_status
114
114
  ```
115
115
 
116
+ If alpha signals stop but the gateway looks healthy, call `solana_alpha_subscribe({ force: true })` or inspect `solana_runtime_status` (alpha `stats.lastEventTs`, `subscribed`). The plugin also runs a background watchdog for stale ingestion and gateway forward probes.
117
+
116
118
  ### Non-interactive setup
117
119
 
118
120
  ```bash
@@ -379,7 +381,7 @@ Pay-as-you-go or Basic tier is required for the read-only social intel tools.
379
381
  ### Alpha Signal Processing
380
382
  | Tool | Description |
381
383
  |------|-------------|
382
- | `solana_alpha_subscribe` | Subscribe to alpha signal WebSocket feed |
384
+ | `solana_alpha_subscribe` | Subscribe to alpha WebSocket feed (`force: true` reconnects if ingestion stalls) |
383
385
  | `solana_alpha_unsubscribe` | Unsubscribe from alpha feed |
384
386
  | `solana_alpha_signals` | Retrieve buffered alpha signals |
385
387
  | `solana_alpha_history` | Query historical alpha signals |
@@ -413,7 +415,7 @@ Pay-as-you-go or Basic tier is required for the read-only social intel tools.
413
415
  | `solana_gateway_forward_probe` | Probe gateway forwarding connectivity |
414
416
  | `solana_agent_sessions` | View agent session diagnostics |
415
417
  | `solana_startup_gate` | Run startup gate sequence |
416
- | `solana_runtime_status` | Get runtime status diagnostics |
418
+ | `solana_runtime_status` | Runtime diagnostics (startup gate, alpha stats / `lastEventTs`, last forward probe) |
417
419
 
418
420
  ### Local Durable State
419
421
  | Tool | Description |
@@ -2,6 +2,8 @@
2
2
  var RECONNECT_DELAYS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
3
3
  var PING_INTERVAL_MS = 3e4;
4
4
  var PONG_TIMEOUT_MS = 1e4;
5
+ var ALPHA_INGESTION_STALE_MS = 20 * 60 * 1e3;
6
+ var ALPHA_STALE_GRACE_AFTER_CONNECT_MS = 3 * 60 * 1e3;
5
7
  var AlphaStreamManager = class {
6
8
  config;
7
9
  ws = null;
@@ -19,10 +21,50 @@ var AlphaStreamManager = class {
19
21
  constructor(config) {
20
22
  this.config = config;
21
23
  }
22
- async subscribe() {
23
- if (this.subscribed && this.ws && this.ws.readyState === 1) {
24
+ /**
25
+ * @param opts.force If true, drop the existing WebSocket (when connected) and subscribe again.
26
+ * Use when the socket looks healthy but alpha_signal delivery may have stalled.
27
+ */
28
+ async subscribe(opts = {}) {
29
+ const force = Boolean(opts.force);
30
+ if (!force && this.subscribed && this.ws && this.ws.readyState === 1) {
24
31
  return { subscribed: true, premiumAccess: this.premiumAccess, tier: this.tier };
25
32
  }
33
+ if (force && this.ws) {
34
+ if (this.reconnectTimer) {
35
+ clearTimeout(this.reconnectTimer);
36
+ this.reconnectTimer = null;
37
+ }
38
+ const oldWs = this.ws;
39
+ this.intentionalClose = true;
40
+ this.log("info", "Force subscribe: closing WebSocket for clean reconnect");
41
+ await new Promise((resolve) => {
42
+ if (oldWs.readyState === 3) {
43
+ resolve();
44
+ return;
45
+ }
46
+ const t = setTimeout(resolve, 5e3);
47
+ oldWs.once("close", () => {
48
+ clearTimeout(t);
49
+ resolve();
50
+ });
51
+ try {
52
+ if (oldWs.readyState === 1 || oldWs.readyState === 0) {
53
+ oldWs.close();
54
+ } else {
55
+ clearTimeout(t);
56
+ resolve();
57
+ }
58
+ } catch {
59
+ clearTimeout(t);
60
+ resolve();
61
+ }
62
+ });
63
+ this.ws = null;
64
+ this.subscribed = false;
65
+ this.authenticated = false;
66
+ this.intentionalClose = false;
67
+ }
26
68
  this.intentionalClose = false;
27
69
  await this.connect();
28
70
  return new Promise((resolve, reject) => {
@@ -39,6 +81,26 @@ var AlphaStreamManager = class {
39
81
  }, 15e3);
40
82
  });
41
83
  }
84
+ /**
85
+ * True when the socket reports subscribed but no alpha_signal has been received for
86
+ * {@link ALPHA_INGESTION_STALE_MS} (after {@link ALPHA_STALE_GRACE_AFTER_CONNECT_MS}).
87
+ */
88
+ isIngestionStale(now = Date.now()) {
89
+ if (!this.isSubscribed()) return false;
90
+ const uptime = now - this.connectedAt;
91
+ if (uptime < ALPHA_STALE_GRACE_AFTER_CONNECT_MS) return false;
92
+ const lastActivity = this.lastEventTs > 0 ? this.lastEventTs : this.connectedAt;
93
+ return now - lastActivity >= ALPHA_INGESTION_STALE_MS;
94
+ }
95
+ /**
96
+ * Re-send alpha_stream_subscribe on the existing connection (soft recovery).
97
+ * @returns true if the message was sent
98
+ */
99
+ resendApplicationSubscribe() {
100
+ if (!this.authenticated || !this.ws || this.ws.readyState !== 1) return false;
101
+ this.sendAlphaSubscribe();
102
+ return true;
103
+ }
42
104
  async unsubscribe() {
43
105
  this.intentionalClose = true;
44
106
  this.subscribed = false;
@@ -266,5 +328,7 @@ var AlphaStreamManager = class {
266
328
  };
267
329
 
268
330
  export {
331
+ ALPHA_INGESTION_STALE_MS,
332
+ ALPHA_STALE_GRACE_AFTER_CONNECT_MS,
269
333
  AlphaStreamManager
270
334
  };
@@ -0,0 +1,21 @@
1
+ // src/gateway-config-sync.ts
2
+ function normalizeGatewayBaseUrl(url) {
3
+ return String(url || "").trim().replace(/\/+$/, "");
4
+ }
5
+ function shouldSyncGatewayCredentials(localBaseUrl, localToken, active) {
6
+ const gbu = String(localBaseUrl || "").trim();
7
+ const gt = String(localToken || "").trim();
8
+ if (!gbu || !gt || !active) return false;
9
+ const lUrl = normalizeGatewayBaseUrl(gbu);
10
+ const rUrl = normalizeGatewayBaseUrl(String(active.gatewayBaseUrl ?? ""));
11
+ if (lUrl !== rUrl) return true;
12
+ if (typeof active.gatewayToken === "string") {
13
+ return active.gatewayToken.trim() !== gt;
14
+ }
15
+ return false;
16
+ }
17
+
18
+ export {
19
+ normalizeGatewayBaseUrl,
20
+ shouldSyncGatewayCredentials
21
+ };
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import {
2
+ readRecoverySecretFromDisk
3
+ } from "./chunk-SBYHSJLU.js";
1
4
  import {
2
5
  SessionManager
3
6
  } from "./chunk-VVEKPKW3.js";
@@ -15,10 +18,13 @@ import {
15
18
  } from "./chunk-3UQIQJPQ.js";
16
19
  import {
17
20
  AlphaStreamManager
18
- } from "./chunk-ATWB7K63.js";
21
+ } from "./chunk-JVQNXWBW.js";
19
22
  import {
20
23
  BitqueryStreamManager
21
24
  } from "./chunk-S2DLZKMQ.js";
25
+ import {
26
+ shouldSyncGatewayCredentials
27
+ } from "./chunk-R24UDHQG.js";
22
28
  import {
23
29
  orchestratorRequest
24
30
  } from "./chunk-6GSGHMUH.js";
@@ -35,9 +41,6 @@ import {
35
41
  import {
36
42
  scrubUntrustedText
37
43
  } from "./chunk-AI6MTHUN.js";
38
- import {
39
- readRecoverySecretFromDisk
40
- } from "./chunk-SBYHSJLU.js";
41
44
 
42
45
  // index.ts
43
46
  import { Type } from "@sinclair/typebox";
@@ -2593,6 +2596,18 @@ ${notes}
2593
2596
  refreshed = await get("/api/agents/gateway-credentials");
2594
2597
  activeCredential = getActiveCredential(refreshed);
2595
2598
  }
2599
+ if (activeCredential && autoFixGateway && gbu && gt && shouldSyncGatewayCredentials(gbu, gt, activeCredential)) {
2600
+ const driftBody = {
2601
+ gatewayBaseUrl: gbu,
2602
+ gatewayToken: gt,
2603
+ active: true
2604
+ };
2605
+ if (config.agentId) driftBody.agentId = config.agentId;
2606
+ if (forwardChatId) driftBody.forwardTelegramChatId = forwardChatId;
2607
+ await put("/api/agents/gateway-credentials", driftBody);
2608
+ refreshed = await get("/api/agents/gateway-credentials");
2609
+ activeCredential = getActiveCredential(refreshed);
2610
+ }
2596
2611
  gatewayStepOk = Boolean(activeCredential);
2597
2612
  if (!gatewayStepOk) throw new Error("Gateway credentials are missing or inactive");
2598
2613
  pushStep({
@@ -2697,10 +2712,13 @@ ${notes}
2697
2712
  };
2698
2713
  api.registerTool({
2699
2714
  name: "solana_alpha_subscribe",
2700
- description: "Subscribe to the SpyFly alpha signal stream via WebSocket. Signals are buffered locally and retrieved with solana_alpha_signals. The startup gate calls this automatically. Optionally set agentId and subscriberType for event-to-agent forwarding.",
2715
+ description: "Subscribe to the SpyFly alpha signal stream via WebSocket. Signals are buffered locally and retrieved with solana_alpha_signals. The startup gate calls this automatically. Optionally set agentId and subscriberType for event-to-agent forwarding. Use force=true to close an existing connection and resubscribe when the socket looks healthy but alpha_signal ingestion has stalled (zombie subscription).",
2701
2716
  parameters: Type.Object({
2702
2717
  agentId: Type.Optional(Type.String({ description: "Agent ID for event-to-agent forwarding. Uses plugin config agentId as default." })),
2703
- subscriberType: Type.Optional(Type.String({ description: "Subscriber type: 'agent' or 'client'." }))
2718
+ subscriberType: Type.Optional(Type.String({ description: "Subscriber type: 'agent' or 'client'." })),
2719
+ force: Type.Optional(Type.Boolean({
2720
+ description: "If true, disconnect any existing WebSocket and subscribe again. Use when resubscribe would otherwise be a no-op but signals stopped arriving."
2721
+ }))
2704
2722
  }),
2705
2723
  execute: wrapExecute("solana_alpha_subscribe", async (_id, params) => {
2706
2724
  const effectiveAgentId = params.agentId || config.agentId;
@@ -2711,7 +2729,7 @@ ${notes}
2711
2729
  if (effectiveSubscriberType) {
2712
2730
  alphaStreamManager.setSubscriberType(effectiveSubscriberType);
2713
2731
  }
2714
- return alphaStreamManager.subscribe();
2732
+ return alphaStreamManager.subscribe({ force: params.force === true });
2715
2733
  })
2716
2734
  });
2717
2735
  api.registerTool({
@@ -2923,6 +2941,7 @@ ${notes}
2923
2941
  startupGate: startupGateState,
2924
2942
  alphaStream: {
2925
2943
  subscribed: alphaStreamManager.isSubscribed(),
2944
+ ingestionStale: alphaStreamManager.isIngestionStale(),
2926
2945
  stats: alphaStreamManager.getStats(),
2927
2946
  bufferSize: alphaBuffer.getBufferSize()
2928
2947
  },
@@ -4285,8 +4304,16 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
4285
4304
  api.logger.warn(`[solana-trader] Forward probe failed: ${err instanceof Error ? err.message : String(err)}`);
4286
4305
  }
4287
4306
  const WATCHDOG_INTERVAL_MS = 9e4;
4307
+ const FORWARD_PROBE_WATCHDOG_MS = 7 * 60 * 1e3;
4308
+ const STARTUP_GATE_AFTER_PROBE_FAIL_COOLDOWN_MS = 10 * 60 * 1e3;
4309
+ let alphaStalePhase = 0;
4310
+ const watchdogStartedAt = Date.now();
4311
+ let lastForwardProbeWatchdogMs = watchdogStartedAt;
4312
+ let lastProbeFailureStartupGateMs = watchdogStartedAt;
4288
4313
  const watchdogTimer = setInterval(async () => {
4314
+ const now = Date.now();
4289
4315
  if (!alphaStreamManager.isSubscribed()) {
4316
+ alphaStalePhase = 0;
4290
4317
  api.logger.warn("[watchdog] Alpha stream not subscribed \u2014 resubscribing...");
4291
4318
  try {
4292
4319
  await alphaStreamManager.subscribe();
@@ -4294,6 +4321,32 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
4294
4321
  } catch (err) {
4295
4322
  api.logger.error(`[watchdog] Alpha resubscribe failed: ${err instanceof Error ? err.message : String(err)}`);
4296
4323
  }
4324
+ } else if (alphaStreamManager.isIngestionStale(now)) {
4325
+ if (alphaStalePhase === 0) {
4326
+ api.logger.warn("[watchdog] Alpha ingestion stale (no recent alpha_signal) \u2014 resending alpha_stream_subscribe");
4327
+ if (alphaStreamManager.resendApplicationSubscribe()) {
4328
+ alphaStalePhase = 1;
4329
+ } else {
4330
+ try {
4331
+ await alphaStreamManager.subscribe({ force: true });
4332
+ api.logger.info("[watchdog] Alpha force-resubscribe completed (soft send unavailable).");
4333
+ } catch (err) {
4334
+ api.logger.error(`[watchdog] Alpha force-resubscribe failed: ${err instanceof Error ? err.message : String(err)}`);
4335
+ }
4336
+ alphaStalePhase = 0;
4337
+ }
4338
+ } else {
4339
+ api.logger.warn("[watchdog] Alpha still stale after soft recovery \u2014 forcing WebSocket reconnect");
4340
+ try {
4341
+ await alphaStreamManager.subscribe({ force: true });
4342
+ api.logger.info("[watchdog] Alpha force-resubscribe completed.");
4343
+ } catch (err) {
4344
+ api.logger.error(`[watchdog] Alpha force-resubscribe failed: ${err instanceof Error ? err.message : String(err)}`);
4345
+ }
4346
+ alphaStalePhase = 0;
4347
+ }
4348
+ } else {
4349
+ alphaStalePhase = 0;
4297
4350
  }
4298
4351
  try {
4299
4352
  const creds = await get("/api/agents/gateway-credentials");
@@ -4304,6 +4357,22 @@ Context compaction triggered. STATE.md synced from last persisted state. Decisio
4304
4357
  } catch (err) {
4305
4358
  api.logger.warn(`[watchdog] Gateway credential check failed: ${err instanceof Error ? err.message : String(err)}`);
4306
4359
  }
4360
+ if (now - lastForwardProbeWatchdogMs >= FORWARD_PROBE_WATCHDOG_MS) {
4361
+ lastForwardProbeWatchdogMs = now;
4362
+ try {
4363
+ const pr = await runForwardProbe({ agentId: config.agentId || "main", source: "watchdog" });
4364
+ if (!Boolean(pr.ok)) {
4365
+ api.logger.warn(`[watchdog] Gateway forward probe failed: ${JSON.stringify(pr)}`);
4366
+ if (now - lastProbeFailureStartupGateMs >= STARTUP_GATE_AFTER_PROBE_FAIL_COOLDOWN_MS) {
4367
+ lastProbeFailureStartupGateMs = now;
4368
+ api.logger.warn("[watchdog] Running startup gate after forward probe failure (cooldown elapsed)");
4369
+ await runStartupGate({ autoFixGateway: true, force: true });
4370
+ }
4371
+ }
4372
+ } catch (err) {
4373
+ api.logger.warn(`[watchdog] Forward probe error: ${err instanceof Error ? err.message : String(err)}`);
4374
+ }
4375
+ }
4307
4376
  }, WATCHDOG_INTERVAL_MS);
4308
4377
  if (watchdogTimer && typeof watchdogTimer === "object" && "unref" in watchdogTimer) {
4309
4378
  watchdogTimer.unref();
@@ -1,6 +1,10 @@
1
1
  import {
2
+ ALPHA_INGESTION_STALE_MS,
3
+ ALPHA_STALE_GRACE_AFTER_CONNECT_MS,
2
4
  AlphaStreamManager
3
- } from "../chunk-ATWB7K63.js";
5
+ } from "../chunk-JVQNXWBW.js";
4
6
  export {
7
+ ALPHA_INGESTION_STALE_MS,
8
+ ALPHA_STALE_GRACE_AFTER_CONNECT_MS,
5
9
  AlphaStreamManager
6
10
  };
@@ -0,0 +1,8 @@
1
+ import {
2
+ normalizeGatewayBaseUrl,
3
+ shouldSyncGatewayCredentials
4
+ } from "../chunk-R24UDHQG.js";
5
+ export {
6
+ normalizeGatewayBaseUrl,
7
+ shouldSyncGatewayCredentials
8
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solana-traderclaw",
3
- "version": "1.0.99",
3
+ "version": "1.0.101",
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",
@@ -17,6 +17,7 @@
17
17
  ],
18
18
  "scripts": {
19
19
  "build": "node build.mjs",
20
+ "test": "npm run build && node scripts/gateway-config-sync-selftest.mjs",
20
21
  "prepublishOnly": "npm run build",
21
22
  "sync-cli": "node scripts/sync-cli-version.mjs",
22
23
  "release:patch": "node scripts/release-patch.mjs"
@@ -60,7 +60,7 @@ Call `solana_scan_launches` for new launches and `solana_scan_hot_pairs` for hot
60
60
 
61
61
  ## STEP 1.5: ALPHA SIGNALS
62
62
 
63
- Call `solana_alpha_signals` to poll the buffer. Score and classify each signal by priority. Check `calledAgainCount` — multiple independent callers on same token = high conviction.
63
+ Call `solana_alpha_signals` to poll the buffer. Score and classify each signal by priority. Check `calledAgainCount` — multiple independent callers on same token = high conviction. If buffer stays empty while live, `solana_runtime_status` then `solana_alpha_subscribe({ force: true })` or unsub+subscribe.
64
64
 
65
65
  **Source trust check (mandatory before acting on any signal):**
66
66
  ```
@@ -162,7 +162,7 @@ After startup completes, deliver the welcome ceremony:
162
162
 
163
163
  1. `solana_system_status()` — verify orchestrator reachable
164
164
  2. `solana_gateway_credentials_get()` — verify gateway registered
165
- 3. `solana_alpha_subscribe({ agentId: "main" })` — start signal stream
165
+ 3. `solana_alpha_subscribe({ agentId: "main" })` — start signal stream (`force: true` if the socket looks live but alpha ingestion stalled)
166
166
  4. `solana_capital_status()` + `solana_positions()` + `solana_killswitch_status()` — portfolio health
167
167
  5. `solana_gateway_forward_probe({ agentId: "main" })` — verify wake path
168
168
 
@@ -101,7 +101,7 @@ Every tool has a mandatory trigger — when the trigger condition is met, you MU
101
101
  ### Alpha Signals (5)
102
102
  | Tool | Purpose | When to Call |
103
103
  |---|---|---|
104
- | `solana_alpha_subscribe` | Subscribe to alpha stream | Startup sequence; when stream disconnects |
104
+ | `solana_alpha_subscribe` | Subscribe to alpha stream (`force: true` forces reconnect) | Startup sequence; when stream disconnects or ingestion stalls |
105
105
  | `solana_alpha_signals` | Poll buffered signals | Step 1.5 every heartbeat |
106
106
  | `solana_alpha_history` | Historical signal data | Step 1.5 to check prior calls on a token; Step 7 to check source accuracy after exit |
107
107
  | `solana_alpha_sources` | Per-source performance stats | Step 1.5 to check source win rates; `source_reputation_recalc` cron |