@reefclaw/openclaw-plugin 0.1.19 → 0.1.20
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.
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { type ReconnectConfig as BaseReconnectConfig } from '../utils/reconnect.js';
|
|
2
2
|
import type { GatewayConfig } from './gateway-config.js';
|
|
3
3
|
interface ReconnectConfig extends BaseReconnectConfig {
|
|
4
|
-
|
|
4
|
+
/** Fast-backoff attempts before falling back to the slow retry loop */
|
|
5
|
+
maxFastAttempts: number;
|
|
6
|
+
/** Slow retry interval once the fast attempts are exhausted (ms) */
|
|
7
|
+
slowRetryMs: number;
|
|
5
8
|
}
|
|
6
9
|
/** hello-ok response payload from gateway */
|
|
7
10
|
export interface HelloOkPayload {
|
|
@@ -56,6 +59,9 @@ export declare class GatewayWsClient {
|
|
|
56
59
|
private ws;
|
|
57
60
|
private state;
|
|
58
61
|
private attempt;
|
|
62
|
+
/** True once the fast burst is spent and we are in the slow retry loop
|
|
63
|
+
* (kept so the "switching to slow retry" warning logs once, not per attempt). */
|
|
64
|
+
private slowRetryActive;
|
|
59
65
|
private reconnectTimer;
|
|
60
66
|
private staleWarnTimer;
|
|
61
67
|
private staleReconnectTimer;
|
|
@@ -30,11 +30,23 @@ const MIN_PROTOCOL_VERSION = 3;
|
|
|
30
30
|
const MAX_PROTOCOL_VERSION = 4;
|
|
31
31
|
const CLIENT_ID = 'openclaw-tui';
|
|
32
32
|
const CLIENT_VERSION = '0.1.0';
|
|
33
|
+
// Mirrors the relay connector's proven shape (see connector.ts DEFAULT_RECONNECT):
|
|
34
|
+
// a bounded fast-backoff burst, then a slow retry that NEVER gives up.
|
|
35
|
+
//
|
|
36
|
+
// This client used to have `maxAttempts: 20` and go terminal ('failed') when it
|
|
37
|
+
// ran out — roughly 9 minutes of budget (2+4+8+16s, then 16 x 30s). On
|
|
38
|
+
// 2026-07-31 an OpenClaw upgrade left the gateway crash-looping for 56 minutes;
|
|
39
|
+
// the bridge burned its 20 attempts in the first 9, gave up permanently, and
|
|
40
|
+
// stayed dark even AFTER the gateway recovered. The dashboard — the operator's
|
|
41
|
+
// window and one of the emergency-control paths — required a manual bridge
|
|
42
|
+
// restart to come back. Any gateway downtime over ~9 min (upgrade, reboot,
|
|
43
|
+
// crash-loop) reproduced it, on every deployment.
|
|
33
44
|
const DEFAULT_RECONNECT = {
|
|
34
45
|
baseDelayMs: 2_000,
|
|
35
46
|
maxDelayMs: 30_000,
|
|
36
|
-
|
|
47
|
+
maxFastAttempts: 20,
|
|
37
48
|
jitterFactor: 0.3,
|
|
49
|
+
slowRetryMs: 60_000,
|
|
38
50
|
};
|
|
39
51
|
// ---- Fatal close codes ----
|
|
40
52
|
/** Auth errors — do not reconnect */
|
|
@@ -45,6 +57,9 @@ export class GatewayWsClient {
|
|
|
45
57
|
ws = null;
|
|
46
58
|
state = 'disconnected';
|
|
47
59
|
attempt = 0;
|
|
60
|
+
/** True once the fast burst is spent and we are in the slow retry loop
|
|
61
|
+
* (kept so the "switching to slow retry" warning logs once, not per attempt). */
|
|
62
|
+
slowRetryActive = false;
|
|
48
63
|
reconnectTimer = null;
|
|
49
64
|
staleWarnTimer = null;
|
|
50
65
|
staleReconnectTimer = null;
|
|
@@ -351,6 +366,7 @@ export class GatewayWsClient {
|
|
|
351
366
|
logger.info(TAG, `Connected to gateway (protocol=${payload.protocol})`);
|
|
352
367
|
this.lastHelloOk = payload;
|
|
353
368
|
this.attempt = 0; // Reset reconnect counter
|
|
369
|
+
this.slowRetryActive = false; // Back on the fast budget for the next drop
|
|
354
370
|
// Parse policy
|
|
355
371
|
if (payload.policy?.tickIntervalMs) {
|
|
356
372
|
this.tickIntervalMs = payload.policy.tickIntervalMs;
|
|
@@ -403,14 +419,21 @@ export class GatewayWsClient {
|
|
|
403
419
|
// ---- Reconnection ----
|
|
404
420
|
scheduleReconnect() {
|
|
405
421
|
this.attempt++;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
422
|
+
const { maxFastAttempts, slowRetryMs } = this.reconnectConfig;
|
|
423
|
+
// Exhausting the fast burst is NOT terminal — a gateway that is down for an
|
|
424
|
+
// hour (upgrade, reboot, crash-loop) must still be picked up when it
|
|
425
|
+
// returns. Only a fatal close code (4001 auth) ends the client, because a
|
|
426
|
+
// rejected token will never start working and must not be retried forever.
|
|
427
|
+
const exhausted = this.attempt > maxFastAttempts;
|
|
428
|
+
if (exhausted && !this.slowRetryActive) {
|
|
429
|
+
this.slowRetryActive = true;
|
|
430
|
+
logger.warn(TAG, `Fast reconnect budget (${maxFastAttempts} attempts) exhausted — switching to slow retry every ` +
|
|
431
|
+
`${Math.round(slowRetryMs / 1000)}s. Still trying; will reconnect on its own when the gateway returns.`);
|
|
411
432
|
}
|
|
412
|
-
const delay = computeDelay(this.attempt - 1, this.reconnectConfig);
|
|
413
|
-
logger.info(TAG,
|
|
433
|
+
const delay = exhausted ? slowRetryMs : computeDelay(this.attempt - 1, this.reconnectConfig);
|
|
434
|
+
logger.info(TAG, exhausted
|
|
435
|
+
? `Reconnecting in ${Math.round(delay)}ms (slow retry, attempt ${this.attempt})`
|
|
436
|
+
: `Reconnecting in ${Math.round(delay)}ms (attempt ${this.attempt}/${maxFastAttempts})`);
|
|
414
437
|
this.setState('reconnecting');
|
|
415
438
|
this.emit('reconnecting', { attempt: this.attempt, delayMs: Math.round(delay) });
|
|
416
439
|
this.reconnectTimer = setTimeout(() => {
|
|
@@ -124,6 +124,25 @@ export async function onCreateOrderFilled(ctx, inputs, order) {
|
|
|
124
124
|
// scale-ins with WS-exact data; capturing here would double-post).
|
|
125
125
|
const existing = ctx.stateStore.get(inputs.symbol);
|
|
126
126
|
if (existing) {
|
|
127
|
+
// ★ SAME-ORDER GUARD — the twin of the dedup in onWsFillObserved (which
|
|
128
|
+
// returns when `stateEntry.openedFromExchangeTradeId === fill.exchangeOrderId`).
|
|
129
|
+
// That one covers create_order-then-WS; this covers WS-then-create_order,
|
|
130
|
+
// which is the ORDINARY ordering for a market order: the WS fill routinely
|
|
131
|
+
// beats the REST response (measured ~270-415ms on prod).
|
|
132
|
+
//
|
|
133
|
+
// Without it the stale probe below decides this case, and it CANNOT: its
|
|
134
|
+
// test is "does the exchange position equal this fill?", which is equally
|
|
135
|
+
// true of a genuinely stale mapping AND of the mapping the WS path just
|
|
136
|
+
// wrote for THIS VERY FILL. It chose 'stale', dropped the fresh mapping and
|
|
137
|
+
// journaled a second position — 38 duplicate pairs in 9 days (~11/day), each
|
|
138
|
+
// orphaning the WS row at 0 reviews while the exchange held ONE position.
|
|
139
|
+
// The twins were undedupable downstream because they key on different ids
|
|
140
|
+
// (WS = trade id, here = order id).
|
|
141
|
+
const orderId = typeof order.id === 'string' ? order.id : undefined;
|
|
142
|
+
if (orderId && existing.openedFromExchangeTradeId === orderId) {
|
|
143
|
+
logger.info(TAG, `onCreateOrderFilled ${inputs.symbol}: WS fill path already captured this order (${orderId}) — skipping duplicate capture`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
127
146
|
const probe = await probeStaleStateEntry(ctx, inputs.symbol, filledQty);
|
|
128
147
|
if (probe === 'stale') {
|
|
129
148
|
logger.warn(TAG, `onCreateOrderFilled ${inputs.symbol}: state-store entry (openedAt=${existing.openedAt}) is STALE — ` +
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "reefclaw-paper-trading",
|
|
3
3
|
"name": "ReefClaw Trading",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.20",
|
|
5
5
|
"description": "Supervised trading plugin for the ReefClaw dashboard. It runs on YOUR machine and starts in PAPER mode with no API keys. It cannot trade real funds until you supply exchange credentials and step PAPER→MICRO_LIVE→LIVE yourself from the dashboard — the agent cannot make that change (the tool is refused without operator provenance). Exchange keys stay local, are used only to sign requests to the exchange, and are never transmitted to ReefClaw (asserted by a test in this package). Trading telemetry — positions, fills, decision journal — is sent to ReefClaw to render the dashboard. Every live position carries exchange-native protective stops. Remote updates to the agent's trading instructions are applied only after an Ed25519 signature is verified against a public key pinned in this build.",
|
|
6
6
|
"author": "ReefClaw",
|
|
7
7
|
"activation": {
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "ReefClaw supervised trading plugin for OpenClaw. Runs entirely on YOUR machine and starts in PAPER mode
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "index.js",
|
|
7
|
-
"openclaw": {
|
|
8
|
-
"extensions": [
|
|
9
|
-
"./index.js"
|
|
10
|
-
],
|
|
11
|
-
"compat": {
|
|
12
|
-
"pluginApi": ">=2026.6.0"
|
|
13
|
-
},
|
|
14
|
-
"build": {
|
|
15
|
-
"openclawVersion": "2026.6.11"
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"files": [
|
|
19
|
-
"**/*",
|
|
20
|
-
"!scripts/**"
|
|
21
|
-
],
|
|
22
|
-
"engines": {
|
|
23
|
-
"node": ">=20"
|
|
24
|
-
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@reefclaw/shared": "0.1.4",
|
|
27
|
-
"ccxt": "4.5.37",
|
|
28
|
-
"json5": "2.2.3",
|
|
29
|
-
"ws": "8.21.1"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "node scripts/assemble.mjs",
|
|
33
|
-
"verify": "node scripts/verify-shared-contract.mjs",
|
|
34
|
-
"prepublishOnly": "node scripts/verify-shared-contract.mjs"
|
|
35
|
-
},
|
|
36
|
-
"license": "MIT",
|
|
37
|
-
"homepage": "https://reefclaw.com"
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@reefclaw/openclaw-plugin",
|
|
3
|
+
"version": "0.1.20",
|
|
4
|
+
"description": "ReefClaw supervised trading plugin for OpenClaw. Runs entirely on YOUR machine and starts in PAPER mode \u00e2\u20ac\u201d it cannot trade real funds until you supply exchange credentials and walk the PAPER\u00e2\u2020\u2019MICRO_LIVE\u00e2\u2020\u2019LIVE ladder yourself from the ReefClaw dashboard (the agent cannot make that change; it is refused without operator provenance). Your exchange API keys stay on your machine to sign requests to the exchange and are NEVER sent to ReefClaw \u00e2\u20ac\u201d a test in the package asserts this. What does reach ReefClaw is trading telemetry for the dashboard (positions, fills, decision journal). Live trading always carries exchange-native protective stops. Trading instructions can be updated remotely, and every update must carry a valid Ed25519 signature verified against a key pinned in this build before it is applied. Install: /plugins install clawhub:@reefclaw/openclaw-plugin",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"openclaw": {
|
|
8
|
+
"extensions": [
|
|
9
|
+
"./index.js"
|
|
10
|
+
],
|
|
11
|
+
"compat": {
|
|
12
|
+
"pluginApi": ">=2026.6.0"
|
|
13
|
+
},
|
|
14
|
+
"build": {
|
|
15
|
+
"openclawVersion": "2026.6.11"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"**/*",
|
|
20
|
+
"!scripts/**"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@reefclaw/shared": "0.1.4",
|
|
27
|
+
"ccxt": "4.5.37",
|
|
28
|
+
"json5": "2.2.3",
|
|
29
|
+
"ws": "8.21.1"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "node scripts/assemble.mjs",
|
|
33
|
+
"verify": "node scripts/verify-shared-contract.mjs",
|
|
34
|
+
"prepublishOnly": "node scripts/verify-shared-contract.mjs"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://reefclaw.com"
|
|
38
|
+
}
|