@reefclaw/openclaw-plugin 0.1.23 → 0.1.25
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/bridge/bridge.js +72 -5
- package/bridge/connector.d.ts +3 -1
- package/bridge/connector.js +51 -4
- package/bridge/gateway/heartbeat-cron.js +31 -7
- package/bridge/gateway/poller.d.ts +5 -0
- package/bridge/gateway/poller.js +9 -0
- package/bridge/index.js +21 -0
- package/bridge/provider.d.ts +15 -0
- package/bridge/providers/connector-update.d.ts +89 -0
- package/bridge/providers/connector-update.js +212 -0
- package/bridge/providers/emergency-commands.d.ts +36 -0
- package/bridge/providers/emergency-commands.js +91 -0
- package/bridge/providers/gateway.d.ts +26 -1
- package/bridge/providers/gateway.js +159 -8
- package/bridge/providers/mock.js +1 -0
- package/bridge/shock-wake.d.ts +80 -0
- package/bridge/shock-wake.js +291 -0
- package/bridge/types.d.ts +5 -1
- package/bridge/types.js +5 -0
- package/bridge/utils/instance-id.d.ts +3 -0
- package/bridge/utils/instance-id.js +48 -0
- package/ccxt/binance-private.js +2 -1
- package/ccxt/binance-public.js +6 -1
- package/config/agent-config-client.d.ts +7 -2
- package/config/agent-config-client.js +17 -0
- package/config/agent-config-poller.js +5 -1
- package/config/brackets-config.d.ts +2 -1
- package/config/brackets-config.js +25 -3
- package/config/gate-store.d.ts +12 -0
- package/config/gate-store.js +26 -2
- package/config/loss-streak-config.d.ts +2 -0
- package/config/loss-streak-config.js +33 -0
- package/config/plugin-config-io.d.ts +19 -0
- package/config/plugin-config-io.js +24 -2
- package/config/reentry-cooldown-config.d.ts +7 -0
- package/config/reentry-cooldown-config.js +59 -0
- package/http/keepalive-fetch.d.ts +5 -0
- package/http/keepalive-fetch.js +50 -0
- package/index.js +77 -8
- package/ingest/position-auto-capture.js +49 -4
- package/ingest/position-decisions-client.d.ts +6 -0
- package/ingest/position-decisions-client.js +27 -9
- package/ingest/readiness-reporter.d.ts +23 -2
- package/ingest/readiness-reporter.js +56 -1
- package/live/approval-lifecycle.d.ts +10 -0
- package/live/approval-lifecycle.js +16 -2
- package/live/microstructure-assembler.js +11 -2
- package/live/proposal-decision-listener.d.ts +21 -0
- package/live/proposal-decision-listener.js +39 -0
- package/live/proposal-manager.d.ts +12 -0
- package/live/proposal-manager.js +47 -0
- package/live/stop-watcher.d.ts +16 -1
- package/live/stop-watcher.js +48 -8
- package/onboarding/runtime.js +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +38 -38
- package/persistence/state-manager.d.ts +7 -0
- package/persistence/state-manager.js +28 -1
- package/portfolio/directional-scoreboard.d.ts +17 -0
- package/portfolio/directional-scoreboard.js +71 -0
- package/portfolio/reentry-tracker.d.ts +38 -1
- package/portfolio/reentry-tracker.js +49 -0
- package/signals/change-of-character.d.ts +38 -0
- package/signals/change-of-character.js +93 -0
- package/simulator/exchange-simulator.d.ts +27 -1
- package/simulator/exchange-simulator.js +98 -38
- package/simulator/types.d.ts +11 -0
- package/skills/reefclaw/SKILL.md +2 -2
- package/strategy/evaluator.d.ts +4 -0
- package/tools/audit-bracket-protection.js +11 -7
- package/tools/close-position.js +10 -1
- package/tools/create-order.js +121 -9
- package/tools/get-funding-context.js +6 -1
- package/tools/get-liquidation-levels.js +5 -1
- package/tools/get-liquidation-pulse.js +7 -1
- package/tools/get-market-intel.js +2 -1
- package/tools/get-relevant-learnings.js +20 -1
- package/tools/get-resting-liquidity.js +6 -1
- package/tools/get-wave9-status.js +17 -0
- package/tools/hl-provision-agent-wallet.js +29 -11
- package/tools/intel-api.d.ts +9 -0
- package/tools/intel-api.js +32 -1
- package/tools/record-position-reviews.js +2 -2
- package/tools/reentry-cooldown.d.ts +33 -0
- package/tools/reentry-cooldown.js +74 -0
- package/tools/scan-pairs.d.ts +7 -0
- package/tools/scan-pairs.js +67 -11
- package/tools/set-exchange-credentials.js +19 -0
- package/tools/set-trading-mode.d.ts +6 -0
- package/tools/set-trading-mode.js +48 -1
- package/types.d.ts +7 -0
- package/venues/hyperliquid/hl-agent-wallet.d.ts +26 -0
- package/venues/hyperliquid/hl-agent-wallet.js +32 -0
- package/venues/hyperliquid/hl-live-adapter.d.ts +27 -2
- package/venues/hyperliquid/hl-live-adapter.js +101 -13
package/bridge/bridge.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { writeFileSync, renameSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
|
-
import { logger } from './logger.js';
|
|
14
|
+
import { logger, formatError } from './logger.js';
|
|
15
15
|
import { Connector } from './connector.js';
|
|
16
16
|
import { readLocalSkillVersion, readAgentVisibleSkillVersions, validateSkillContent, compareSemver } from './utils/skill-version.js';
|
|
17
17
|
import { invalidateSkillsSnapshot } from './utils/skills-snapshot-invalidation.js';
|
|
@@ -53,6 +53,8 @@ const CRITICAL_EVENTS = new Set([
|
|
|
53
53
|
'state_update', // Agent mode changes (ACTIVE/PAUSED/STOPPED)
|
|
54
54
|
'trading_mode', // Paper/live mode changes
|
|
55
55
|
'skill_update_applied', // OTA SKILL.md confirmation
|
|
56
|
+
'connector_update', // Operator-triggered update progress — the frame right
|
|
57
|
+
// before the gateway restart must not be coalesced away.
|
|
56
58
|
// All chat events are on the 'chat' channel and always sent immediately
|
|
57
59
|
]);
|
|
58
60
|
/** How often to re-attempt the webapp SKILL.md pull while still on a bootstrap
|
|
@@ -249,6 +251,14 @@ export class Bridge {
|
|
|
249
251
|
this.addListener('decisionTraceUpdate', (data) => {
|
|
250
252
|
this.emit('agent_state', 'decision_trace', data);
|
|
251
253
|
});
|
|
254
|
+
// Operator-triggered connector update: PTY output + status. CRITICAL
|
|
255
|
+
// priority so the operator watching an update in progress sees it live
|
|
256
|
+
// instead of behind the 500ms low-priority coalescer — and, more to the
|
|
257
|
+
// point, so the final frame before the gateway restart is not the one that
|
|
258
|
+
// gets dropped.
|
|
259
|
+
this.addListener('connectorUpdate', (data) => {
|
|
260
|
+
this.emit('agent_state', 'connector_update', data);
|
|
261
|
+
});
|
|
252
262
|
}
|
|
253
263
|
addListener(event, fn) {
|
|
254
264
|
this.provider.on(event, fn);
|
|
@@ -313,10 +323,20 @@ export class Bridge {
|
|
|
313
323
|
flushThrottleBuffer() {
|
|
314
324
|
if (this.throttleBuffer.size === 0)
|
|
315
325
|
return;
|
|
316
|
-
|
|
317
|
-
|
|
326
|
+
// Per-entry guard + delete-as-sent: this runs bare inside setInterval, so
|
|
327
|
+
// an uncaught throw (e.g. JSON.stringify on a payload with a circular ref
|
|
328
|
+
// or BigInt leaking out of a tool result) would be a process-killing
|
|
329
|
+
// uncaught exception — and skipping clear() would re-flush the survivors
|
|
330
|
+
// next tick under NEW seq numbers (duplicate frames on the wire).
|
|
331
|
+
for (const [key, { channel, event, payload }] of this.throttleBuffer) {
|
|
332
|
+
this.throttleBuffer.delete(key);
|
|
333
|
+
try {
|
|
334
|
+
this.sendFrame(channel, event, payload);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
logger.warn(TAG, `flush dropped unserializable ${channel}:${event}: ${formatError(err)}`);
|
|
338
|
+
}
|
|
318
339
|
}
|
|
319
|
-
this.throttleBuffer.clear();
|
|
320
340
|
}
|
|
321
341
|
startThrottleTimer() {
|
|
322
342
|
this.stopThrottleTimer();
|
|
@@ -361,7 +381,7 @@ export class Bridge {
|
|
|
361
381
|
logger.info(TAG, 'Reconciliation requested');
|
|
362
382
|
const snapshot = await this.provider.getSnapshot();
|
|
363
383
|
const lastSequences = parseLastSequences(params);
|
|
364
|
-
|
|
384
|
+
let gapFill = this.replayBuffer
|
|
365
385
|
.sinceMany(lastSequences)
|
|
366
386
|
.map((e) => ({
|
|
367
387
|
type: 'event',
|
|
@@ -370,7 +390,32 @@ export class Bridge {
|
|
|
370
390
|
payload: e.payload,
|
|
371
391
|
seq: e.seq,
|
|
372
392
|
}));
|
|
393
|
+
// The whole reconcile response is ONE WS message; an unbounded replay
|
|
394
|
+
// blows the relay's frame limit and the browser then gets NO snapshot
|
|
395
|
+
// at all (observed live 2026-08-04: 369 replayed events → relay
|
|
396
|
+
// "Message too large"). Keep the NEWEST events under a byte budget —
|
|
397
|
+
// the snapshot already carries current state, so dropping the oldest
|
|
398
|
+
// stream events is strictly better than losing the entire response.
|
|
373
399
|
if (gapFill.length > 0) {
|
|
400
|
+
const GAP_FILL_BYTE_BUDGET = 256 * 1024;
|
|
401
|
+
let bytes = 0;
|
|
402
|
+
let start = gapFill.length;
|
|
403
|
+
for (let i = gapFill.length - 1; i >= 0; i--) {
|
|
404
|
+
try {
|
|
405
|
+
bytes += JSON.stringify(gapFill[i]).length;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
start = i + 1; // unserializable event — cut it and everything older
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
if (bytes > GAP_FILL_BYTE_BUDGET)
|
|
412
|
+
break;
|
|
413
|
+
start = i;
|
|
414
|
+
}
|
|
415
|
+
if (start > 0) {
|
|
416
|
+
logger.warn(TAG, `gapFill capped: dropping ${start} oldest of ${gapFill.length} events to fit the relay frame limit`);
|
|
417
|
+
gapFill = gapFill.slice(start);
|
|
418
|
+
}
|
|
374
419
|
logger.info(TAG, `gapFill: ${gapFill.length} events replayed (lastSeqs=${JSON.stringify(lastSequences)})`);
|
|
375
420
|
}
|
|
376
421
|
// Provider hardcodes gapFill: []; bridge owns the wire-side seq
|
|
@@ -557,6 +602,28 @@ export class Bridge {
|
|
|
557
602
|
this.connector.sendResponse(id, true, result);
|
|
558
603
|
return;
|
|
559
604
|
}
|
|
605
|
+
if (method === 'connector.update') {
|
|
606
|
+
const provider = this.provider;
|
|
607
|
+
if (!provider.updateConnector) {
|
|
608
|
+
this.connector.sendResponse(id, true, {
|
|
609
|
+
ok: false,
|
|
610
|
+
status: 'failed',
|
|
611
|
+
message: 'Provider does not support connector.update (likely mock provider)',
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// ★ Deliberately reads ONE field. There is no command/version/args
|
|
616
|
+
// parameter to forward — the command is a constant inside the driver
|
|
617
|
+
// (providers/connector-update.ts). If a future change starts forwarding
|
|
618
|
+
// caller-supplied strings into that command, this stops being an update
|
|
619
|
+
// button and becomes a remote shell on the trader's machine.
|
|
620
|
+
const acknowledgeOpenPositions = params?.acknowledgeOpenPositions === true;
|
|
621
|
+
audit('connector_update.start', { id, acknowledgeOpenPositions });
|
|
622
|
+
const outcome = await provider.updateConnector({ acknowledgeOpenPositions });
|
|
623
|
+
audit('connector_update.dispatched', { id, ok: outcome.ok, status: outcome.status });
|
|
624
|
+
this.connector.sendResponse(id, true, outcome);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
560
627
|
if (method === 'get_bracket_config') {
|
|
561
628
|
if (!this.provider.getBracketConfig) {
|
|
562
629
|
this.connector.sendResponse(id, false, undefined, {
|
package/bridge/connector.d.ts
CHANGED
|
@@ -22,7 +22,9 @@ export declare class Connector {
|
|
|
22
22
|
private lastMessageAt;
|
|
23
23
|
private destroyed;
|
|
24
24
|
constructor(config: ConnectorConfig, callbacks: ConnectorCallbacks);
|
|
25
|
-
/** Build the relay WebSocket URL (token is sent via Authorization header, not
|
|
25
|
+
/** Build the relay WebSocket URL (token is sent via Authorization header, not
|
|
26
|
+
* URL; the instance id is non-secret and rides the query string so the
|
|
27
|
+
* relay's skill-slot admission can recognise a same-box restart). */
|
|
26
28
|
private buildUrl;
|
|
27
29
|
/** Start connecting to the relay */
|
|
28
30
|
connect(): void;
|
package/bridge/connector.js
CHANGED
|
@@ -37,10 +37,15 @@ export class Connector {
|
|
|
37
37
|
this.reconnectConfig = { ...DEFAULT_RECONNECT, ...config.reconnect };
|
|
38
38
|
this.heartbeatConfig = { ...DEFAULT_HEARTBEAT };
|
|
39
39
|
}
|
|
40
|
-
/** Build the relay WebSocket URL (token is sent via Authorization header, not
|
|
40
|
+
/** Build the relay WebSocket URL (token is sent via Authorization header, not
|
|
41
|
+
* URL; the instance id is non-secret and rides the query string so the
|
|
42
|
+
* relay's skill-slot admission can recognise a same-box restart). */
|
|
41
43
|
buildUrl() {
|
|
42
44
|
const base = this.config.relayUrl.replace(/\/$/, '');
|
|
43
|
-
|
|
45
|
+
const path = `${base}/parties/reefclaw/${this.config.userId}`;
|
|
46
|
+
return this.config.instanceId
|
|
47
|
+
? `${path}?instance=${encodeURIComponent(this.config.instanceId)}`
|
|
48
|
+
: path;
|
|
44
49
|
}
|
|
45
50
|
/** Start connecting to the relay */
|
|
46
51
|
connect() {
|
|
@@ -78,10 +83,40 @@ export class Connector {
|
|
|
78
83
|
this.rejectAllPending('Connection closed');
|
|
79
84
|
if (this.destroyed)
|
|
80
85
|
return;
|
|
81
|
-
// Fatal codes:
|
|
86
|
+
// Fatal codes (4001 invalid/mismatched token): no fast reconnect — but
|
|
87
|
+
// not terminal-forever either. A 4001 can be transient (relay/DB blip,
|
|
88
|
+
// token-cache miss on a just-created token, revoke-then-restore), and
|
|
89
|
+
// the old return-and-never-retry left the dashboard dark until a manual
|
|
90
|
+
// bridge restart (residual half of the #312 gave-up-forever class). One
|
|
91
|
+
// probe per 15 min is harmless and self-heals the transient cases.
|
|
82
92
|
if (FATAL_CLOSE_CODES.includes(code)) {
|
|
83
|
-
logger.error(TAG, `Fatal close code ${code}
|
|
93
|
+
logger.error(TAG, `Fatal close code ${code} (invalid/mismatched token). If the token was rotated, ` +
|
|
94
|
+
`update the bridge .env (REEFCLAW_TOKEN) and restart. Probing again in 15 min ` +
|
|
95
|
+
`in case the rejection was transient.`);
|
|
84
96
|
this.setState('failed');
|
|
97
|
+
this.attempt = 0;
|
|
98
|
+
this.reconnectTimer = setTimeout(() => {
|
|
99
|
+
this.reconnectTimer = null;
|
|
100
|
+
this.connect();
|
|
101
|
+
}, 15 * 60_000);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// 4011 = another agent holds this account's skill slot and is alive
|
|
105
|
+
// (relay skill-slot admission, 2026-08-25 — the fix for the 4010
|
|
106
|
+
// ping-pong two bridges used to fight). Retrying fast is pointless and
|
|
107
|
+
// noisy: the seated agent keeps the slot until it stops. Slow-probe
|
|
108
|
+
// every 5 min so a deliberate box switch (stop the old one) is picked
|
|
109
|
+
// up without a manual restart here.
|
|
110
|
+
if (code === 4011) {
|
|
111
|
+
logger.error(TAG, `Relay refused this connection (4011): another agent is already connected for this ` +
|
|
112
|
+
`account. ReefClaw runs ONE agent per account — stop the other agent (or revoke its ` +
|
|
113
|
+
`token in the dashboard) to move this box in. Probing again in 5 min.`);
|
|
114
|
+
this.setState('failed');
|
|
115
|
+
this.attempt = 0;
|
|
116
|
+
this.reconnectTimer = setTimeout(() => {
|
|
117
|
+
this.reconnectTimer = null;
|
|
118
|
+
this.connect();
|
|
119
|
+
}, 5 * 60_000);
|
|
85
120
|
return;
|
|
86
121
|
}
|
|
87
122
|
// 4002 = stale skill connection on relay. Wait for it to time out, then retry.
|
|
@@ -263,6 +298,18 @@ export class Connector {
|
|
|
263
298
|
// Send a native WebSocket ping (protocol-level, handled by PartyKit automatically).
|
|
264
299
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
265
300
|
this.ws.ping();
|
|
301
|
+
// App-level keepalive: native pings are absorbed by the PartyKit
|
|
302
|
+
// runtime and never reach the relay DO, so an idle-but-healthy agent
|
|
303
|
+
// would look dead to the skill-slot admission's liveness window and
|
|
304
|
+
// could be evicted by a second box's probe. One tiny frame per
|
|
305
|
+
// heartbeat tick keeps the seat provably occupied; the relay swallows
|
|
306
|
+
// it (never forwarded to browsers, never audited).
|
|
307
|
+
this.ws.send(JSON.stringify({
|
|
308
|
+
type: 'event',
|
|
309
|
+
event: 'skill_keepalive',
|
|
310
|
+
channel: 'agent_state',
|
|
311
|
+
payload: { ts: Date.now() },
|
|
312
|
+
}));
|
|
266
313
|
}
|
|
267
314
|
}, this.heartbeatConfig.intervalMs);
|
|
268
315
|
logger.debug(TAG, `Heartbeat started: interval=${this.heartbeatConfig.intervalMs}ms timeout=${this.heartbeatConfig.timeoutMs}ms`);
|
|
@@ -28,11 +28,35 @@ export const HEARTBEAT_EVERY_MS = 15 * 60 * 1000;
|
|
|
28
28
|
// (verified empirically on 2026.7.1-2: a cron.add without it stores none) —
|
|
29
29
|
// 780s is the field-proven value from the 2026-07-22 small-box mitigations.
|
|
30
30
|
export const HEARTBEAT_TIMEOUT_SECONDS = 780;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
// The routine beat is HEARTBEAT.md (injected into every isolated cron session
|
|
32
|
+
// as workspace bootstrap context) — NOT the SKILL.md Decision Loop. The old
|
|
33
|
+
// message ("run the full Decision Loop (Steps 0-9) from SKILL.md") had two
|
|
34
|
+
// measured costs on prod (2026-08-04): the agent paged the whole ~110KB
|
|
35
|
+
// SKILL.md through sed/rg every beat (~100KB of tool output before any
|
|
36
|
+
// trading work), and Steps 0-9 predate the pinned-plan/exit-gate discipline —
|
|
37
|
+
// Step 8's exit rules are the scratch-the-winners instructions. SKILL.md is
|
|
38
|
+
// consulted only when preparing a NEW entry.
|
|
39
|
+
// NOTE: once-per-install marker semantics mean this message only reaches NEW
|
|
40
|
+
// installs; existing boxes need a one-time `openclaw cron edit <id> --message`
|
|
41
|
+
// (done on prod at deploy).
|
|
42
|
+
// ★ The mandatory items are ENUMERATED here on purpose. The first (softer)
|
|
43
|
+
// version of this message — "Run the HEARTBEAT.md checklist" — produced
|
|
44
|
+
// 85-second beats on gpt-5.4-mini that skipped record_position_reviews,
|
|
45
|
+
// get_wave9_status, and the liquidity batch entirely (observed on prod
|
|
46
|
+
// 2026-08-04 12:25/12:40 UTC): the checklist being in context is not enough,
|
|
47
|
+
// the beat prompt itself must name what cannot be skipped.
|
|
48
|
+
export const HEARTBEAT_MESSAGE = 'Heartbeat. Execute EVERY checkbox in the HEARTBEAT.md checklist (already injected in your context; ' +
|
|
49
|
+
'do NOT re-read SKILL.md from disk unless preparing a NEW entry). ' +
|
|
50
|
+
'NON-SKIPPABLE every beat: (1) query_trades({hours:1}) stop-watcher reconcile; ' +
|
|
51
|
+
'(2) get_wave9_status() once, unconditionally; ' +
|
|
52
|
+
'(3) if ANY position is open and the Position Decision Journal is enabled (record_position_reviews reports off-mode when it is not): ' +
|
|
53
|
+
'get_my_recent_reviews() + get_relevant_learnings({applies_at: heartbeat}) in one batch, ' +
|
|
54
|
+
'then record_position_reviews with ONE review per open position, ' +
|
|
55
|
+
'plus get_resting_liquidity + get_liquidation_levels + get_liquidation_pulse for ALL positions in one parallel batch; ' +
|
|
56
|
+
'(4) the Market Assessment reads. ' +
|
|
57
|
+
'Manage open positions by their pinned plan (invalidation_price / realization_rule) — do not re-argue the entry thesis each beat. ' +
|
|
58
|
+
'Check tradingMode from fetch_balance — the source of truth for paper vs live. ' +
|
|
59
|
+
'Batch independent tool calls in parallel. Trading tools only: no subagents, no web browsing.';
|
|
36
60
|
/**
|
|
37
61
|
* Whether an existing cron job counts as "the heartbeat already exists".
|
|
38
62
|
* Union of the two matchers already in the codebase: the old setup path
|
|
@@ -89,8 +113,8 @@ export async function ensureHeartbeatCron(opts) {
|
|
|
89
113
|
// cron.add on 2026.7.1-2). No toolsAllow: absent = all-tools-allowed,
|
|
90
114
|
// which is future-proof — an explicit allowlist freezes at creation and
|
|
91
115
|
// silently excludes every plugin tool shipped later. Beat tool
|
|
92
|
-
// discipline (no subagents/web
|
|
93
|
-
//
|
|
116
|
+
// discipline (no subagents/web during heartbeats) is stated in
|
|
117
|
+
// HEARTBEAT_MESSAGE itself.
|
|
94
118
|
payload: {
|
|
95
119
|
kind: 'agentTurn',
|
|
96
120
|
message: HEARTBEAT_MESSAGE,
|
|
@@ -114,6 +114,11 @@ export declare class Poller {
|
|
|
114
114
|
* - Errors are logged and swallowed (never crash the provider)
|
|
115
115
|
*/
|
|
116
116
|
private guardedExec;
|
|
117
|
+
/** Arm the circuit-breaker for a Binance-bound call made OUTSIDE
|
|
118
|
+
* guardedExec (e.g. the gateway's paper mark-to-market ticker fan-out).
|
|
119
|
+
* Those calls used to swallow 418/429s entirely — exactly the
|
|
120
|
+
* ban-extending failure mode the gate exists to prevent. */
|
|
121
|
+
noteExternalRateLimit(label: string, msg: string): void;
|
|
117
122
|
/** Inspect a failed poll's error for Binance rate-limit signals and arm
|
|
118
123
|
* the process-wide circuit-breaker accordingly.
|
|
119
124
|
*
|
package/bridge/gateway/poller.js
CHANGED
|
@@ -266,6 +266,15 @@ export class Poller {
|
|
|
266
266
|
this.inflight.delete(label);
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
|
+
/** Arm the circuit-breaker for a Binance-bound call made OUTSIDE
|
|
270
|
+
* guardedExec (e.g. the gateway's paper mark-to-market ticker fan-out).
|
|
271
|
+
* Those calls used to swallow 418/429s entirely — exactly the
|
|
272
|
+
* ban-extending failure mode the gate exists to prevent. */
|
|
273
|
+
noteExternalRateLimit(label, msg) {
|
|
274
|
+
if (isRateLimitShapedError(msg)) {
|
|
275
|
+
this.applyRateLimitBackoff(label, msg);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
269
278
|
/** Inspect a failed poll's error for Binance rate-limit signals and arm
|
|
270
279
|
* the process-wide circuit-breaker accordingly.
|
|
271
280
|
*
|
package/bridge/index.js
CHANGED
|
@@ -16,7 +16,9 @@ import { MockProvider } from './providers/mock.js';
|
|
|
16
16
|
import { GatewayProvider } from './providers/gateway.js';
|
|
17
17
|
import { resolveConfig } from './config.js';
|
|
18
18
|
import { resolveGatewayConfig, validateGatewayConfig } from './gateway/gateway-config.js';
|
|
19
|
+
import { ShockWakePoller } from './shock-wake.js';
|
|
19
20
|
import { runSetup } from './setup.js';
|
|
21
|
+
import { resolveRelayInstanceId } from './utils/instance-id.js';
|
|
20
22
|
const TAG = 'main';
|
|
21
23
|
// ---- Load .env file (skill/.env only) ----
|
|
22
24
|
function loadEnvFile() {
|
|
@@ -191,6 +193,7 @@ async function main() {
|
|
|
191
193
|
}
|
|
192
194
|
// Create provider
|
|
193
195
|
let provider;
|
|
196
|
+
let intelligenceUrlForWakes;
|
|
194
197
|
if (args.provider === 'mock') {
|
|
195
198
|
logger.info(TAG, 'Using MockProvider');
|
|
196
199
|
provider = new MockProvider();
|
|
@@ -217,14 +220,30 @@ async function main() {
|
|
|
217
220
|
gwConfig.connectionToken = token;
|
|
218
221
|
logger.info(TAG, `Using GatewayProvider (url=${gwConfig.gatewayUrl}, symbol=${gwConfig.symbol})`);
|
|
219
222
|
provider = new GatewayProvider(gwConfig);
|
|
223
|
+
intelligenceUrlForWakes = gwConfig.intelligenceUrl;
|
|
220
224
|
}
|
|
221
225
|
// Create bridge
|
|
222
226
|
const connectorConfig = {
|
|
223
227
|
relayUrl,
|
|
224
228
|
userId,
|
|
225
229
|
token,
|
|
230
|
+
// Stable per-install id → the relay's skill-slot admission recognises a
|
|
231
|
+
// same-box restart (instant takeover) vs a second box (rejected 4011).
|
|
232
|
+
instanceId: resolveRelayInstanceId(),
|
|
226
233
|
};
|
|
227
234
|
const bridge = new Bridge(provider, connectorConfig);
|
|
235
|
+
// Shock-wake poller (WS3, docs/MARKET_ADAPTIVITY_PLAN.md) — watches intel's
|
|
236
|
+
// /api/shocks (the WS2 change-of-character flags) and delivers an
|
|
237
|
+
// out-of-band re-evaluation turn to the agent on a genuine market shift.
|
|
238
|
+
// Inert until the central gate agent_config.gates.shockWake leaves 'off'.
|
|
239
|
+
const shockWake = intelligenceUrlForWakes
|
|
240
|
+
? new ShockWakePoller({ provider, token, intelligenceUrl: intelligenceUrlForWakes })
|
|
241
|
+
: null;
|
|
242
|
+
if (shockWake) {
|
|
243
|
+
// Delivery confirmation: any agent turn STARTING proves the main-session
|
|
244
|
+
// lane is alive — the gateway ack alone cannot (swallowed-turn class).
|
|
245
|
+
provider.on('chatMessageStart', () => shockWake.noteAssistantActivity());
|
|
246
|
+
}
|
|
228
247
|
// Handle graceful shutdown
|
|
229
248
|
let shuttingDown = false;
|
|
230
249
|
async function shutdown(signal) {
|
|
@@ -239,6 +258,7 @@ async function main() {
|
|
|
239
258
|
}
|
|
240
259
|
catch { /* best-effort */ }
|
|
241
260
|
}
|
|
261
|
+
shockWake?.stop();
|
|
242
262
|
bridge.stop();
|
|
243
263
|
// Allow 1s for final cleanup before force exit
|
|
244
264
|
setTimeout(() => process.exit(0), 1000);
|
|
@@ -248,5 +268,6 @@ async function main() {
|
|
|
248
268
|
// Start
|
|
249
269
|
logger.info(TAG, `Starting ReefClaw skill (provider=${args.provider}, relay=${relayUrl})`);
|
|
250
270
|
bridge.start();
|
|
271
|
+
shockWake?.start();
|
|
251
272
|
}
|
|
252
273
|
main();
|
package/bridge/provider.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { EmergencyAction, ReconciliationSnapshot, TickerData, CandleData, OrderUpdatePayload, AgentStateData, RiskUpdatePayload, ChatMessage, MarketStructureData, CryptoMetricsData, VolumeAnalysisData, TradeJournalEntry, RegimeData, SignalData, MissionData, AnalyticsData, ShadowComparisonData, TradingModeData, DecisionTraceData } from './types.js';
|
|
2
2
|
import type { TradingMode } from '@reefclaw/shared';
|
|
3
|
+
import type { ConnectorUpdateOutcome } from './providers/connector-update.js';
|
|
3
4
|
/** Per-venue credential shapes for the operator set/test RPCs. Binance is an
|
|
4
5
|
* HMAC key pair; Hyperliquid is a MASTER account address (public — queries)
|
|
5
6
|
* plus an approved AGENT wallet private key (signs only, cannot withdraw).
|
|
@@ -48,6 +49,9 @@ export interface HlSubmitApprovalOutcome {
|
|
|
48
49
|
chain?: string;
|
|
49
50
|
agentAddress?: string;
|
|
50
51
|
}
|
|
52
|
+
/** Progress of an operator-triggered connector update. Re-exported from the
|
|
53
|
+
* driver so callers get the status union without importing the PTY module. */
|
|
54
|
+
export type { ConnectorUpdateOutcome, ConnectorUpdateStatus, } from './providers/connector-update.js';
|
|
51
55
|
/** Result of hl_agent_wallet_status — approval polling for the guided flow. */
|
|
52
56
|
export interface HlAgentWalletStatusOutcome {
|
|
53
57
|
ok: boolean;
|
|
@@ -100,6 +104,8 @@ export interface ProviderEvents {
|
|
|
100
104
|
id: string;
|
|
101
105
|
toolName: string;
|
|
102
106
|
}) => void;
|
|
107
|
+
/** Progress of an operator-triggered connector update (PTY output + status). */
|
|
108
|
+
connectorUpdate: (data: ConnectorUpdateOutcome) => void;
|
|
103
109
|
marketStructure: (data: MarketStructureData) => void;
|
|
104
110
|
cryptoMetrics: (data: CryptoMetricsData) => void;
|
|
105
111
|
volumeAnalysis: (data: VolumeAnalysisData) => void;
|
|
@@ -217,6 +223,15 @@ export interface OpenClawProvider {
|
|
|
217
223
|
v: number;
|
|
218
224
|
};
|
|
219
225
|
}): Promise<HlSubmitApprovalOutcome>;
|
|
226
|
+
/** Operator-only: update the ReefClaw connector on this box to the latest
|
|
227
|
+
* published version, driven over the gateway's terminal.* PTY methods.
|
|
228
|
+
*
|
|
229
|
+
* There is no command parameter, by design — see the security note at the
|
|
230
|
+
* top of providers/connector-update.ts. The only input is the operator's
|
|
231
|
+
* acknowledgement that the resulting restart may interrupt an open book. */
|
|
232
|
+
updateConnector?(args: {
|
|
233
|
+
acknowledgeOpenPositions?: boolean;
|
|
234
|
+
}): Promise<ConnectorUpdateOutcome>;
|
|
220
235
|
/** Operator-only: remove stored credentials from the plugin config and
|
|
221
236
|
* de-escalate to PAPER mode if currently running in a live mode. */
|
|
222
237
|
clearExchangeCredentials?(): Promise<{
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE command this feature can ever run.
|
|
3
|
+
*
|
|
4
|
+
* `-y` so npx never blocks on its install prompt; `@latest` because this is an
|
|
5
|
+
* update. No user input, no interpolation, no exceptions.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONNECTOR_UPDATE_COMMAND = "npx -y @reefclaw/connect@latest";
|
|
8
|
+
/** Marker printed after the command so we can detect completion + exit code. */
|
|
9
|
+
export declare const DONE_SENTINEL = "__RC_CONNECTOR_UPDATE_DONE__";
|
|
10
|
+
/**
|
|
11
|
+
* The exact line written to the PTY. Built once, from constants only.
|
|
12
|
+
* `$?` is expanded by the remote shell, not by us.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildUpdateCommandLine(): string;
|
|
15
|
+
/** Minimal RPC surface this module needs (injected — keeps it unit-testable). */
|
|
16
|
+
export type TerminalRpc = (method: string, params: Record<string, unknown>) => Promise<unknown>;
|
|
17
|
+
export type ConnectorUpdateStatus =
|
|
18
|
+
/** Command written to the PTY; output is streaming. */
|
|
19
|
+
'started'
|
|
20
|
+
/** DONE sentinel observed — we have a real exit code. */
|
|
21
|
+
| 'completed'
|
|
22
|
+
/** Transport died after the command started = the gateway restart landed. */
|
|
23
|
+
| 'restarting'
|
|
24
|
+
/** Refused before anything ran (terminal disabled / sandboxed / open positions). */
|
|
25
|
+
| 'blocked'
|
|
26
|
+
/** Something genuinely went wrong. */
|
|
27
|
+
| 'failed';
|
|
28
|
+
export interface ConnectorUpdateOutcome {
|
|
29
|
+
ok: boolean;
|
|
30
|
+
status: ConnectorUpdateStatus;
|
|
31
|
+
message: string;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
exitCode?: number | null;
|
|
34
|
+
/** Accumulated screen text, for the dashboard's output pane. */
|
|
35
|
+
output?: string;
|
|
36
|
+
/** True when the caller must re-submit with acknowledgeOpenPositions. */
|
|
37
|
+
requiresPositionAck?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Map a gateway error from `terminal.open` onto an operator-readable outcome.
|
|
41
|
+
*
|
|
42
|
+
* The two refusals we know the gateway can produce are worth distinct copy —
|
|
43
|
+
* both are configuration facts on the trader's box that no retry will fix:
|
|
44
|
+
* - "terminal is not available" / "terminal is disabled"
|
|
45
|
+
* - agent runs in a sandbox → in-sandbox terminals are unsupported
|
|
46
|
+
*/
|
|
47
|
+
export declare function describeOpenFailure(err: unknown): ConnectorUpdateOutcome;
|
|
48
|
+
/**
|
|
49
|
+
* Once the command is running, losing the transport is EXPECTED — the installer
|
|
50
|
+
* restarts the gateway, which is the process hosting both the PTY and our own
|
|
51
|
+
* connection. Treating that as a failure would report a successful update as
|
|
52
|
+
* broken, so the distinction is explicit and tested.
|
|
53
|
+
*/
|
|
54
|
+
export declare function classifyTransportLoss(started: boolean): ConnectorUpdateStatus;
|
|
55
|
+
/** Extract the exit code that follows the DONE sentinel, if it has appeared. */
|
|
56
|
+
export declare function parseDoneSentinel(screen: string): {
|
|
57
|
+
done: boolean;
|
|
58
|
+
exitCode: number | null;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The sentinel is echoed by the shell as part of the command line BEFORE the
|
|
62
|
+
* command runs, so a naive `includes()` reports "done" immediately. Strip the
|
|
63
|
+
* echoed command line first: completion is only credible after the printf has
|
|
64
|
+
* actually executed, which is the LAST occurrence and is followed by a digit.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isCredibleCompletion(screen: string): boolean;
|
|
67
|
+
export interface StartOptions {
|
|
68
|
+
/** Open positions the operator has been warned about and accepted. */
|
|
69
|
+
acknowledgeOpenPositions?: boolean;
|
|
70
|
+
/** Count of currently-open positions, as the skill sees them. */
|
|
71
|
+
openPositionCount: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Guard the restart hazard. The installer restarts the gateway; with positions
|
|
75
|
+
* open that means the agent stops managing them for the duration (exchange-side
|
|
76
|
+
* brackets still protect the book — they are enforced by the venue, not by us —
|
|
77
|
+
* but there is a known startup window where protective closes are refused).
|
|
78
|
+
* So: never silently update a live book. Demand an explicit acknowledgement.
|
|
79
|
+
*/
|
|
80
|
+
export declare function checkPositionGuard(opts: StartOptions): ConnectorUpdateOutcome | null;
|
|
81
|
+
/**
|
|
82
|
+
* Open a PTY and write the update command to it. Returns as soon as the command
|
|
83
|
+
* is running — the caller polls `readTerminalText` for output.
|
|
84
|
+
*/
|
|
85
|
+
export declare function startConnectorUpdate(rpc: TerminalRpc, opts: StartOptions): Promise<ConnectorUpdateOutcome>;
|
|
86
|
+
/** Read the current screen contents. Returns null when the session is gone. */
|
|
87
|
+
export declare function readTerminalText(rpc: TerminalRpc, sessionId: string): Promise<string | null>;
|
|
88
|
+
/** Best-effort PTY cleanup. Never throws — cleanup failure must not mask an outcome. */
|
|
89
|
+
export declare function closeTerminal(rpc: TerminalRpc, sessionId: string): Promise<void>;
|