@reefclaw/connect 0.1.20 → 0.1.21
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/assets/bridge/gateway/gateway-config.d.ts +16 -5
- package/assets/bridge/gateway/gateway-config.js +68 -12
- package/assets/bridge/gateway/poller.js +18 -8
- package/assets/bridge/providers/emergency-commands.d.ts +9 -1
- package/assets/bridge/providers/emergency-commands.js +38 -1
- package/assets/bridge/providers/gateway.d.ts +23 -1
- package/assets/bridge/providers/gateway.js +79 -17
- package/assets/bridge/providers/onboarding-commands.d.ts +8 -0
- package/assets/bridge/providers/onboarding-commands.js +4 -4
- package/assets/plugin/ccxt/binance-public.d.ts +17 -5
- package/assets/plugin/ccxt/binance-public.js +31 -3
- package/assets/plugin/config/operator-provenance.d.ts +6 -0
- package/assets/plugin/config/operator-provenance.js +50 -0
- package/assets/plugin/config/plugin-config-io.d.ts +15 -1
- package/assets/plugin/config/plugin-config-io.js +24 -0
- package/assets/plugin/index.js +216 -173
- package/assets/plugin/ingest/event-loop-monitor.d.ts +11 -0
- package/assets/plugin/ingest/event-loop-monitor.js +113 -0
- package/assets/plugin/ingest/position-auto-capture.d.ts +5 -0
- package/assets/plugin/ingest/position-auto-capture.js +14 -5
- package/assets/plugin/ingest/readiness-reporter.d.ts +17 -6
- package/assets/plugin/ingest/readiness-reporter.js +88 -9
- package/assets/plugin/ingest/skill-version-reader.d.ts +16 -0
- package/assets/plugin/ingest/skill-version-reader.js +64 -0
- package/assets/plugin/live/approval-lifecycle.d.ts +30 -0
- package/assets/plugin/live/approval-lifecycle.js +80 -0
- package/assets/plugin/live/bracket-types.d.ts +9 -0
- package/assets/plugin/live/live-adapter.d.ts +0 -1
- package/assets/plugin/onboarding/runtime.d.ts +34 -1
- package/assets/plugin/onboarding/runtime.js +56 -5
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/simulator/exchange-simulator.d.ts +45 -2
- package/assets/plugin/simulator/exchange-simulator.js +96 -4
- package/assets/plugin/simulator/types.d.ts +17 -0
- package/assets/plugin/tools/attach-brackets.js +50 -1
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.d.ts +25 -1
- package/assets/plugin/venues/hyperliquid/hl-bracket-coordinator.js +57 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.d.ts +10 -0
- package/assets/plugin/venues/hyperliquid/hl-brackets.js +45 -13
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.d.ts +18 -0
- package/assets/plugin/venues/hyperliquid/hl-fill-ingest.js +69 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +32 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +112 -7
- package/assets/plugin/venues/hyperliquid/hl-public.d.ts +12 -5
- package/assets/plugin/venues/hyperliquid/hl-public.js +24 -3
- package/assets/plugin/venues/hyperliquid/hl-user-stream.d.ts +13 -1
- package/assets/plugin/venues/hyperliquid/hl-user-stream.js +4 -1
- package/assets/shared/index.d.ts +3 -3
- package/assets/shared/index.js +2 -2
- package/assets/shared/readiness.d.ts +38 -2
- package/assets/shared/readiness.js +40 -8
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VenueId } from '@reefclaw/shared';
|
|
1
2
|
export interface GatewayConfig {
|
|
2
3
|
/** Full gateway URL, e.g. "http://localhost:18789" */
|
|
3
4
|
gatewayUrl: string;
|
|
@@ -5,10 +6,16 @@ export interface GatewayConfig {
|
|
|
5
6
|
gatewayToken: string;
|
|
6
7
|
/** Trading symbol, e.g. "BTC/USDT" */
|
|
7
8
|
symbol: string;
|
|
8
|
-
/** Intelligence service URL (
|
|
9
|
+
/** Intelligence service URL (regime classification + trade-result reporting) */
|
|
9
10
|
intelligenceUrl?: string;
|
|
10
11
|
/** Connection token for intelligence service auth */
|
|
11
12
|
connectionToken?: string;
|
|
13
|
+
/** Trading venue this box is configured for. Decides how a canonical symbol
|
|
14
|
+
* is spelled in the intel DB ('BTC/USDT' → 'BTCUSDT' vs 'BTC/USDC' →
|
|
15
|
+
* 'HL_BTC'), so every skill↔intel symbol crossing depends on it.
|
|
16
|
+
* Optional so a hand-built config (tests, embedders) stays valid; every
|
|
17
|
+
* consumer treats absent as 'binance', the historical behaviour. */
|
|
18
|
+
venue?: VenueId;
|
|
12
19
|
}
|
|
13
20
|
/** CLI args specific to gateway provider */
|
|
14
21
|
export interface GatewayCliArgs {
|
|
@@ -20,17 +27,21 @@ export interface GatewayCliArgs {
|
|
|
20
27
|
/**
|
|
21
28
|
* Resolve gateway connection config with priority:
|
|
22
29
|
* 1. CLI args (--gateway-url, --gateway-token, --symbol)
|
|
23
|
-
* 2. Env vars (OPENCLAW_GATEWAY_URL, OPENCLAW_GATEWAY_TOKEN, REEFCLAW_SYMBOL
|
|
24
|
-
*
|
|
25
|
-
*
|
|
30
|
+
* 2. Env vars (OPENCLAW_GATEWAY_URL, OPENCLAW_GATEWAY_TOKEN, REEFCLAW_SYMBOL,
|
|
31
|
+
* REEFCLAW_INTELLIGENCE_URL, REEFCLAW_VENUE)
|
|
32
|
+
* 3. Config files (~/.openclaw/openclaw.json, ~/.reefclaw/plugin-config.json)
|
|
33
|
+
* 4. Defaults (localhost:18789, the venue's default symbol, intel.reefclaw.com)
|
|
26
34
|
*
|
|
27
35
|
* Returns null for gatewayToken if it can't be resolved (caller must handle).
|
|
36
|
+
* `intelligenceUrl` is never null — see DEFAULT_INTELLIGENCE_URL for why that
|
|
37
|
+
* matters.
|
|
28
38
|
*/
|
|
29
39
|
export declare function resolveGatewayConfig(args: GatewayCliArgs): {
|
|
30
40
|
gatewayUrl: string;
|
|
31
41
|
gatewayToken: string | null;
|
|
32
42
|
symbol: string;
|
|
33
|
-
intelligenceUrl: string
|
|
43
|
+
intelligenceUrl: string;
|
|
44
|
+
venue: VenueId;
|
|
34
45
|
};
|
|
35
46
|
/**
|
|
36
47
|
* Validate that the resolved config has all required fields.
|
|
@@ -5,11 +5,27 @@ import { readFileSync } from 'fs';
|
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import JSON5 from 'json5';
|
|
8
|
+
import { parseVenue, VENUE_DEFAULT_SYMBOL } from '@reefclaw/shared';
|
|
8
9
|
import { logger } from '../logger.js';
|
|
9
10
|
const TAG = 'gateway-config';
|
|
10
11
|
const DEFAULT_GATEWAY_PORT = 18789;
|
|
11
12
|
const DEFAULT_GATEWAY_HOST = 'localhost';
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Same default the plugin hardcodes (plugin/src/index.ts) so the two processes
|
|
15
|
+
* agree on the intel service without any operator configuration.
|
|
16
|
+
*
|
|
17
|
+
* ★ Before 2026-07-27 the skill had NO default and NO config-file fallback:
|
|
18
|
+
* `intelligenceUrl` came only from `--intelligence-url` or
|
|
19
|
+
* `REEFCLAW_INTELLIGENCE_URL`, and NOTHING in the repo ever set either one —
|
|
20
|
+
* not the installer, not a systemd unit, not a deploy script. The skill is the
|
|
21
|
+
* ONLY writer of intel's `trade_results` table, so on every default install
|
|
22
|
+
* every closed trade was dropped at debug level inside `reportTradeResult`.
|
|
23
|
+
* Downstream that meant `tradeCount` never reached `minTradesForKelly`, Kelly
|
|
24
|
+
* sizing was pinned at `cappedReason: 'insufficient_history'` forever, and
|
|
25
|
+
* `get_agent_profile` reported tier `novice` / 0 trades no matter how much the
|
|
26
|
+
* agent traded. Reads had a default; the write did not.
|
|
27
|
+
*/
|
|
28
|
+
const DEFAULT_INTELLIGENCE_URL = 'https://intel.reefclaw.com';
|
|
13
29
|
// ---- Read OpenClaw config file ----
|
|
14
30
|
function getConfigPath() {
|
|
15
31
|
return process.env.OPENCLAW_CONFIG_PATH
|
|
@@ -28,18 +44,48 @@ function readOpenClawJson() {
|
|
|
28
44
|
return null;
|
|
29
45
|
}
|
|
30
46
|
}
|
|
47
|
+
// ---- Read the plugin's own config file ----
|
|
48
|
+
function getPluginConfigPath() {
|
|
49
|
+
return process.env.REEFCLAW_PLUGIN_CONFIG_PATH
|
|
50
|
+
|| join(homedir(), '.reefclaw', 'plugin-config.json');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Best-effort read of ~/.reefclaw/plugin-config.json — the file that already
|
|
54
|
+
* carries `intelligenceUrl` and `exchange.venue` for the plugin. Reading the
|
|
55
|
+
* same file here makes the two processes agree by construction rather than by
|
|
56
|
+
* convention (the skill previously had no way at all to learn either value).
|
|
57
|
+
* Fail-soft: null on a missing/unreadable/malformed file, which is the common
|
|
58
|
+
* case on a chat-install box that never writes this file.
|
|
59
|
+
*/
|
|
60
|
+
function readPluginConfig() {
|
|
61
|
+
const path = getPluginConfigPath();
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
64
|
+
if (parsed == null || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
65
|
+
return null;
|
|
66
|
+
return parsed;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
logger.debug(TAG, `Could not read plugin config from ${path}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
31
73
|
// ---- Resolve gateway config ----
|
|
32
74
|
/**
|
|
33
75
|
* Resolve gateway connection config with priority:
|
|
34
76
|
* 1. CLI args (--gateway-url, --gateway-token, --symbol)
|
|
35
|
-
* 2. Env vars (OPENCLAW_GATEWAY_URL, OPENCLAW_GATEWAY_TOKEN, REEFCLAW_SYMBOL
|
|
36
|
-
*
|
|
37
|
-
*
|
|
77
|
+
* 2. Env vars (OPENCLAW_GATEWAY_URL, OPENCLAW_GATEWAY_TOKEN, REEFCLAW_SYMBOL,
|
|
78
|
+
* REEFCLAW_INTELLIGENCE_URL, REEFCLAW_VENUE)
|
|
79
|
+
* 3. Config files (~/.openclaw/openclaw.json, ~/.reefclaw/plugin-config.json)
|
|
80
|
+
* 4. Defaults (localhost:18789, the venue's default symbol, intel.reefclaw.com)
|
|
38
81
|
*
|
|
39
82
|
* Returns null for gatewayToken if it can't be resolved (caller must handle).
|
|
83
|
+
* `intelligenceUrl` is never null — see DEFAULT_INTELLIGENCE_URL for why that
|
|
84
|
+
* matters.
|
|
40
85
|
*/
|
|
41
86
|
export function resolveGatewayConfig(args) {
|
|
42
87
|
const ocConfig = readOpenClawJson();
|
|
88
|
+
const pluginConfig = readPluginConfig();
|
|
43
89
|
// --- Gateway URL ---
|
|
44
90
|
// CLI > env > construct from openclaw.json port > default
|
|
45
91
|
const port = ocConfig?.gateway?.port ?? DEFAULT_GATEWAY_PORT;
|
|
@@ -56,24 +102,34 @@ export function resolveGatewayConfig(args) {
|
|
|
56
102
|
if (fileToken && !args.gatewayToken && !process.env.OPENCLAW_GATEWAY_TOKEN) {
|
|
57
103
|
logger.info(TAG, 'Using gateway token from OpenClaw config');
|
|
58
104
|
}
|
|
105
|
+
// --- Venue ---
|
|
106
|
+
// env > plugin-config.json exchange.venue > binance. Resolved BEFORE the
|
|
107
|
+
// symbol because it decides both the symbol default and the intel spelling.
|
|
108
|
+
// parseVenue surfaces a mis-spelled value instead of silently coercing it.
|
|
109
|
+
const { venue, unrecognized } = parseVenue(process.env.REEFCLAW_VENUE ?? pluginConfig?.exchange?.venue);
|
|
110
|
+
if (unrecognized) {
|
|
111
|
+
logger.warn(TAG, `Unrecognized venue '${unrecognized}' — falling back to binance`);
|
|
112
|
+
}
|
|
59
113
|
// --- Symbol ---
|
|
60
|
-
// CLI > env > default
|
|
114
|
+
// CLI > env > the VENUE's default. A venue-blind 'BTC/USDT' default on a
|
|
115
|
+
// hyperliquid box is a symbol that venue rejects everywhere it's consumed
|
|
116
|
+
// (the issue-#174 class of bug).
|
|
61
117
|
const symbol = args.symbol
|
|
62
118
|
|| process.env.REEFCLAW_SYMBOL
|
|
63
|
-
||
|
|
119
|
+
|| VENUE_DEFAULT_SYMBOL[venue];
|
|
64
120
|
if (!gatewayToken) {
|
|
65
121
|
logger.warn(TAG, 'Gateway token not found in CLI args, env vars, or OpenClaw config');
|
|
66
122
|
}
|
|
67
123
|
// --- Intelligence URL ---
|
|
68
|
-
// CLI > env > default
|
|
124
|
+
// CLI > env > plugin-config.json > default. A default is mandatory: this URL
|
|
125
|
+
// gates the ONLY writer of intel's trade_results (see the constant).
|
|
69
126
|
const intelligenceUrl = args.intelligenceUrl
|
|
70
127
|
|| process.env.REEFCLAW_INTELLIGENCE_URL
|
|
71
|
-
||
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
128
|
+
|| pluginConfig?.intelligenceUrl
|
|
129
|
+
|| DEFAULT_INTELLIGENCE_URL;
|
|
130
|
+
logger.info(TAG, `Intelligence service: ${intelligenceUrl} (venue=${venue})`);
|
|
75
131
|
logger.debug(TAG, `Resolved: url=${gatewayUrl}, token=${gatewayToken ? '***' : '(none)'}, symbol=${symbol}`);
|
|
76
|
-
return { gatewayUrl, gatewayToken, symbol, intelligenceUrl };
|
|
132
|
+
return { gatewayUrl, gatewayToken, symbol, intelligenceUrl, venue };
|
|
77
133
|
}
|
|
78
134
|
/**
|
|
79
135
|
* Validate that the resolved config has all required fields.
|
|
@@ -356,8 +356,14 @@ export class Poller {
|
|
|
356
356
|
const tool = this.toolMap.fetch_positions;
|
|
357
357
|
// Fetch ALL positions (no symbol filter) so multi-symbol trades are visible
|
|
358
358
|
const result = await this.http.invoke(tool, {});
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
if (!Array.isArray(result.data)) {
|
|
360
|
+
// null ≠ empty: this cache is the emergency-Flatten fallback. Collapsing
|
|
361
|
+
// a garbage response to [] would let a later failed fresh read report
|
|
362
|
+
// "No positions to close" off poisoned data (audit 2026-07-26 F6).
|
|
363
|
+
logger.warn(TAG, `fetch_positions returned non-array (${typeof result.data}) — keeping last known positions`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
this.callbacks.onPositions(result.data);
|
|
361
367
|
}
|
|
362
368
|
async pollBalance() {
|
|
363
369
|
const tool = this.toolMap.fetch_balance;
|
|
@@ -370,13 +376,17 @@ export class Poller {
|
|
|
370
376
|
// Fetch ALL open orders (no symbol filter) — this cache is the fallback
|
|
371
377
|
// for the portfolio-wide Kill + reconcile snapshot (see interval comment).
|
|
372
378
|
const result = await this.http.invoke(tool, {});
|
|
379
|
+
if (!Array.isArray(result.data)) {
|
|
380
|
+
// null ≠ empty: same reasoning as pollPositions — a garbage response
|
|
381
|
+
// must not overwrite the Kill fallback cache with a confirmed-empty [].
|
|
382
|
+
logger.warn(TAG, `fetch_open_orders returned non-array (${typeof result.data}) — keeping last known orders`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
373
385
|
const orders = [];
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
orders.push(mapped);
|
|
379
|
-
}
|
|
386
|
+
for (const ccxt of result.data) {
|
|
387
|
+
const mapped = mapCcxtOrder(ccxt);
|
|
388
|
+
if (mapped)
|
|
389
|
+
orders.push(mapped);
|
|
380
390
|
}
|
|
381
391
|
this.callbacks.onOpenOrders(orders);
|
|
382
392
|
}
|
|
@@ -29,8 +29,16 @@ export interface EmergencyContext {
|
|
|
29
29
|
*
|
|
30
30
|
* Caller must set agentMode = 'STOPPED' after this returns, and should pass a
|
|
31
31
|
* FRESH all-symbols order snapshot (see the gateway wrapper).
|
|
32
|
+
*
|
|
33
|
+
* ★ null ≠ empty (audit 2026-07-26 F6): `freshOrders === null` means the fresh
|
|
34
|
+
* fetch FAILED — order state is unknown. A stale (≤60s) cache that happens to
|
|
35
|
+
* be empty is NOT proof of zero working orders, and reporting executed:true
|
|
36
|
+
* from it is exactly the false-green the 2026-06-19 fix exists to prevent. On
|
|
37
|
+
* a failed fresh read Kill still halts the agent and best-effort cancels the
|
|
38
|
+
* cached-known working set, but ALWAYS reports executed:false with the
|
|
39
|
+
* unverified-coverage reason.
|
|
32
40
|
*/
|
|
33
|
-
export declare function executeKill(ctx: EmergencyContext,
|
|
41
|
+
export declare function executeKill(ctx: EmergencyContext, freshOrders: OrderData[] | null, cachedOrders?: OrderData[]): Promise<EmergencyResult>;
|
|
34
42
|
/**
|
|
35
43
|
* Flatten: close all open positions at market.
|
|
36
44
|
* Caller must set agentMode = 'STOPPED' after this returns.
|
|
@@ -30,11 +30,21 @@ function rejectionReasons(results, max = 3) {
|
|
|
30
30
|
*
|
|
31
31
|
* Caller must set agentMode = 'STOPPED' after this returns, and should pass a
|
|
32
32
|
* FRESH all-symbols order snapshot (see the gateway wrapper).
|
|
33
|
+
*
|
|
34
|
+
* ★ null ≠ empty (audit 2026-07-26 F6): `freshOrders === null` means the fresh
|
|
35
|
+
* fetch FAILED — order state is unknown. A stale (≤60s) cache that happens to
|
|
36
|
+
* be empty is NOT proof of zero working orders, and reporting executed:true
|
|
37
|
+
* from it is exactly the false-green the 2026-06-19 fix exists to prevent. On
|
|
38
|
+
* a failed fresh read Kill still halts the agent and best-effort cancels the
|
|
39
|
+
* cached-known working set, but ALWAYS reports executed:false with the
|
|
40
|
+
* unverified-coverage reason.
|
|
33
41
|
*/
|
|
34
|
-
export async function executeKill(ctx,
|
|
42
|
+
export async function executeKill(ctx, freshOrders, cachedOrders = []) {
|
|
35
43
|
if (!ctx.http) {
|
|
36
44
|
return { action: 'kill', executed: false, timestamp: new Date().toISOString(), details: 'HTTP client not initialized' };
|
|
37
45
|
}
|
|
46
|
+
const fresh = freshOrders !== null;
|
|
47
|
+
const openOrders = freshOrders ?? cachedOrders;
|
|
38
48
|
// Protective = a stop/TP bracket (reduceOnly OR closePosition OR stop-type;
|
|
39
49
|
// see mapCcxtOrder/isProtectiveOrder). Working = everything else (entries /
|
|
40
50
|
// adds). Preserve protective so an open position is never left naked.
|
|
@@ -95,6 +105,15 @@ export async function executeKill(ctx, openOrders) {
|
|
|
95
105
|
logger.warn(TAG, details);
|
|
96
106
|
}
|
|
97
107
|
}
|
|
108
|
+
if (!fresh) {
|
|
109
|
+
// Coverage is unverifiable: whatever the cached list said, orders placed
|
|
110
|
+
// in the last ≤60s are invisible to it. Never render a green "Killed".
|
|
111
|
+
executed = false;
|
|
112
|
+
details = working.length === 0
|
|
113
|
+
? `Agent halted, but order state is UNVERIFIED — fresh order fetch failed and the cached snapshot shows no working orders; verify on the exchange${preservedNote}`
|
|
114
|
+
: `${details} — STALE snapshot (fresh order fetch failed); full coverage unverified`;
|
|
115
|
+
logger.error(TAG, details);
|
|
116
|
+
}
|
|
98
117
|
return { action: 'kill', executed, timestamp: new Date().toISOString(), details };
|
|
99
118
|
}
|
|
100
119
|
/**
|
|
@@ -110,13 +129,19 @@ export async function executeFlatten(ctx, cachedPositions) {
|
|
|
110
129
|
// ctx.symbol here narrowed the result to a single symbol, so positions on
|
|
111
130
|
// every other symbol were silently skipped and never closed. The cached
|
|
112
131
|
// fallback (this.positions) is already all-symbols.
|
|
132
|
+
// ★ null ≠ empty (audit 2026-07-26 F6): `fresh` records whether the list is
|
|
133
|
+
// exchange-confirmed. A stale cache that happens to be empty must NEVER
|
|
134
|
+
// produce a green "Flattened" — a position opened in the last ≤60s would be
|
|
135
|
+
// invisible to it and left running behind a STOPPED agent.
|
|
113
136
|
let positions = cachedPositions;
|
|
137
|
+
let fresh = false;
|
|
114
138
|
const fetchTool = ctx.toolMap.fetch_positions;
|
|
115
139
|
if (fetchTool) {
|
|
116
140
|
try {
|
|
117
141
|
const result = await ctx.http.invokeEmergency(fetchTool, {});
|
|
118
142
|
if (Array.isArray(result.data)) {
|
|
119
143
|
positions = result.data;
|
|
144
|
+
fresh = true;
|
|
120
145
|
}
|
|
121
146
|
else {
|
|
122
147
|
// Mirror gateway.ts fetchFreshPositions: a non-array response must
|
|
@@ -132,6 +157,11 @@ export async function executeFlatten(ctx, cachedPositions) {
|
|
|
132
157
|
// Filter to non-zero positions
|
|
133
158
|
const openPositions = positions.filter((p) => (p.contracts ?? 0) !== 0);
|
|
134
159
|
if (openPositions.length === 0) {
|
|
160
|
+
if (!fresh) {
|
|
161
|
+
const details = 'Position state UNVERIFIED — fresh position fetch failed and the cached snapshot shows none; nothing was closed, verify on the exchange';
|
|
162
|
+
logger.error(TAG, details);
|
|
163
|
+
return { action: 'flatten', executed: false, timestamp: new Date().toISOString(), details };
|
|
164
|
+
}
|
|
135
165
|
return { action: 'flatten', executed: true, timestamp: new Date().toISOString(), details: 'No positions to close' };
|
|
136
166
|
}
|
|
137
167
|
// Close each position
|
|
@@ -179,6 +209,13 @@ export async function executeFlatten(ctx, cachedPositions) {
|
|
|
179
209
|
logger.error(TAG, details);
|
|
180
210
|
return { action: 'flatten', executed: false, timestamp: new Date().toISOString(), details };
|
|
181
211
|
}
|
|
212
|
+
if (!fresh) {
|
|
213
|
+
// Every cached-known position closed, but positions opened in the last
|
|
214
|
+
// ≤60s are invisible to a stale snapshot — coverage is unverifiable.
|
|
215
|
+
const details = `Closed ${succeeded}/${openPositions.length} positions from a STALE snapshot (fresh position fetch failed) — full coverage unverified, verify on the exchange`;
|
|
216
|
+
logger.error(TAG, details);
|
|
217
|
+
return { action: 'flatten', executed: false, timestamp: new Date().toISOString(), details };
|
|
218
|
+
}
|
|
182
219
|
const details = `Closed ${succeeded}/${openPositions.length} positions`;
|
|
183
220
|
return { action: 'flatten', executed: true, timestamp: new Date().toISOString(), details };
|
|
184
221
|
}
|
|
@@ -243,6 +243,20 @@ export declare class GatewayProvider implements OpenClawProvider {
|
|
|
243
243
|
* the WS store; under weight pressure isStoreTrusted() flickers between
|
|
244
244
|
* the two, so the raw symbol alternates poll-to-poll. */
|
|
245
245
|
private canonicalSymbol;
|
|
246
|
+
/** Canonical symbol → the intel DB symbol for this box's venue.
|
|
247
|
+
* binance: 'BTC/USDT' → 'BTCUSDT'; hyperliquid: 'BTC/USDC' → 'HL_BTC'.
|
|
248
|
+
*
|
|
249
|
+
* ★ The old venue-blind `.replace('/','')` produced 'BTCUSDC' on a
|
|
250
|
+
* hyperliquid box — a symbol no intel row has ever carried (intel
|
|
251
|
+
* namespaces every HL row under the 'HL_' prefix). That silently broke the
|
|
252
|
+
* regime/signal/mission/analytics pollers AND wrote every trade result to a
|
|
253
|
+
* symbol Kelly sizing would never read back, so a hyperliquid box stayed at
|
|
254
|
+
* `insufficient_history` even once the reporting path itself worked.
|
|
255
|
+
*
|
|
256
|
+
* FAILS OPEN: an unmappable symbol falls back to the legacy concatenation
|
|
257
|
+
* so intel answers "no data for <echo>" — honest and debuggable — rather
|
|
258
|
+
* than the caller throwing and dropping the report entirely. */
|
|
259
|
+
private toIntelSymbol;
|
|
246
260
|
private onPollerPositions;
|
|
247
261
|
/**
|
|
248
262
|
* Compare previous and current positions from polling.
|
|
@@ -279,6 +293,14 @@ export declare class GatewayProvider implements OpenClawProvider {
|
|
|
279
293
|
private lastMarketStructure;
|
|
280
294
|
private fetchFreshPositions;
|
|
281
295
|
private fetchFreshBalance;
|
|
296
|
+
/** Strict fresh read — null on ANY failure (missing tool, thrown fetch,
|
|
297
|
+
* non-array garbage). Callers whose verdict claims order-state coverage
|
|
298
|
+
* (Kill) MUST distinguish "confirmed list" from "unknown"; collapsing a
|
|
299
|
+
* failed read into the ≤60s cache here was how a stale-empty cache became
|
|
300
|
+
* a green "No open orders to cancel" (audit 2026-07-26 F6). */
|
|
301
|
+
private fetchFreshOpenOrdersOrNull;
|
|
302
|
+
/** Cached-fallback wrapper for DISPLAY surfaces (the reconcile snapshot):
|
|
303
|
+
* stale data beats a blank panel there. Never use for emergency verdicts. */
|
|
282
304
|
private fetchFreshOpenOrders;
|
|
283
305
|
private fetchAgentHealth;
|
|
284
306
|
/**
|
|
@@ -387,7 +409,7 @@ export declare class GatewayProvider implements OpenClawProvider {
|
|
|
387
409
|
* truthful-unknown beats confidently-wrong.
|
|
388
410
|
*/
|
|
389
411
|
private seedLastTradeTs;
|
|
390
|
-
/**
|
|
412
|
+
/** The configured symbol as the intel DB spells it. */
|
|
391
413
|
private get intelligenceSymbol();
|
|
392
414
|
/** Shared intelligence poller with circuit-breaker (exponential backoff on errors). */
|
|
393
415
|
private startIntelligencePoller;
|
|
@@ -6,6 +6,7 @@ import { join } from 'path';
|
|
|
6
6
|
import { homedir } from 'os';
|
|
7
7
|
import { logger, formatError } from '../logger.js';
|
|
8
8
|
import { isTradingMode } from '../types.js';
|
|
9
|
+
import { toIntelSymbol } from '@reefclaw/shared';
|
|
9
10
|
import { GatewayHttpClient } from '../gateway/gateway-http-client.js';
|
|
10
11
|
import { GatewayWsClient } from '../gateway/gateway-ws-client.js';
|
|
11
12
|
import { discoverTools } from '../gateway/tool-discovery.js';
|
|
@@ -572,10 +573,11 @@ export class GatewayProvider {
|
|
|
572
573
|
const ctx = { http: this.http, toolMap: this.toolMap, symbol: this.config.symbol };
|
|
573
574
|
// Pass a FRESH, all-symbols order snapshot so Kill cancels every working
|
|
574
575
|
// order portfolio-wide and correctly identifies (and preserves) protective
|
|
575
|
-
// reduceOnly brackets.
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
const
|
|
576
|
+
// reduceOnly brackets. On a failed fresh read Kill still runs against the
|
|
577
|
+
// ≤60s cache (never blocked) but reports executed:false — a stale cache
|
|
578
|
+
// cannot verify coverage (audit 2026-07-26 F6).
|
|
579
|
+
const freshOrders = await this.fetchFreshOpenOrdersOrNull();
|
|
580
|
+
const result = await _executeKill(ctx, freshOrders, this.openOrders);
|
|
579
581
|
this.agentMode = 'STOPPED';
|
|
580
582
|
this.emitAgentState();
|
|
581
583
|
return result;
|
|
@@ -660,16 +662,23 @@ export class GatewayProvider {
|
|
|
660
662
|
// Thin wrappers — real logic lives in providers/onboarding-commands.ts
|
|
661
663
|
// so unit tests can drive pure functions with a mocked HTTP client.
|
|
662
664
|
async setTradingMode(mode, acknowledged) {
|
|
663
|
-
return executeSetTradingMode(
|
|
665
|
+
return executeSetTradingMode(
|
|
666
|
+
// operatorToken = the rc_* connection token — the plugin refuses the
|
|
667
|
+
// four state-mutating operator tools without it (audit F12).
|
|
668
|
+
{ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, mode, acknowledged);
|
|
664
669
|
}
|
|
665
670
|
async setExchangeCredentials(apiKey, secret, testnet) {
|
|
666
|
-
return executeSetExchangeCredentials({ http: this.http, toolMap: this.toolMap }, apiKey, secret, testnet);
|
|
671
|
+
return executeSetExchangeCredentials({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, apiKey, secret, testnet);
|
|
667
672
|
}
|
|
668
673
|
async testExchangeCredentials(apiKey, secret, testnet) {
|
|
669
674
|
return executeTestExchangeCredentials({ http: this.http, toolMap: this.toolMap }, apiKey, secret, testnet);
|
|
670
675
|
}
|
|
671
676
|
async clearExchangeCredentials() {
|
|
672
|
-
return executeClearExchangeCredentials({
|
|
677
|
+
return executeClearExchangeCredentials({
|
|
678
|
+
http: this.http,
|
|
679
|
+
toolMap: this.toolMap,
|
|
680
|
+
operatorToken: this.config.connectionToken,
|
|
681
|
+
});
|
|
673
682
|
}
|
|
674
683
|
/** Operator-only. Read the current bracket-orders config from the plugin. */
|
|
675
684
|
async getBracketConfig() {
|
|
@@ -677,7 +686,7 @@ export class GatewayProvider {
|
|
|
677
686
|
}
|
|
678
687
|
/** Operator-only. Flip requireStopLoss or requireTakeProfit. */
|
|
679
688
|
async setBracketRequirement(flag, value) {
|
|
680
|
-
return executeSetBracketRequirement({ http: this.http, toolMap: this.toolMap }, flag, value);
|
|
689
|
+
return executeSetBracketRequirement({ http: this.http, toolMap: this.toolMap, operatorToken: this.config.connectionToken }, flag, value);
|
|
681
690
|
}
|
|
682
691
|
async getSnapshot() {
|
|
683
692
|
logger.info(TAG, 'Reconciliation snapshot requested');
|
|
@@ -1195,6 +1204,28 @@ export class GatewayProvider {
|
|
|
1195
1204
|
canonicalSymbol(s) {
|
|
1196
1205
|
return typeof s === 'string' ? s.replace(/:[A-Z]+$/, '') : s;
|
|
1197
1206
|
}
|
|
1207
|
+
/** Canonical symbol → the intel DB symbol for this box's venue.
|
|
1208
|
+
* binance: 'BTC/USDT' → 'BTCUSDT'; hyperliquid: 'BTC/USDC' → 'HL_BTC'.
|
|
1209
|
+
*
|
|
1210
|
+
* ★ The old venue-blind `.replace('/','')` produced 'BTCUSDC' on a
|
|
1211
|
+
* hyperliquid box — a symbol no intel row has ever carried (intel
|
|
1212
|
+
* namespaces every HL row under the 'HL_' prefix). That silently broke the
|
|
1213
|
+
* regime/signal/mission/analytics pollers AND wrote every trade result to a
|
|
1214
|
+
* symbol Kelly sizing would never read back, so a hyperliquid box stayed at
|
|
1215
|
+
* `insufficient_history` even once the reporting path itself worked.
|
|
1216
|
+
*
|
|
1217
|
+
* FAILS OPEN: an unmappable symbol falls back to the legacy concatenation
|
|
1218
|
+
* so intel answers "no data for <echo>" — honest and debuggable — rather
|
|
1219
|
+
* than the caller throwing and dropping the report entirely. */
|
|
1220
|
+
toIntelSymbol(symbol) {
|
|
1221
|
+
const canonical = this.canonicalSymbol(symbol);
|
|
1222
|
+
try {
|
|
1223
|
+
return toIntelSymbol(this.config.venue ?? 'binance', canonical);
|
|
1224
|
+
}
|
|
1225
|
+
catch {
|
|
1226
|
+
return canonical.replace('/', '');
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1198
1229
|
async onPollerPositions(positions) {
|
|
1199
1230
|
// Canonicalize symbols at this single position-ingress chokepoint so
|
|
1200
1231
|
// the ENTIRE skill (position-diff, openTradeEntries keys, snapshots,
|
|
@@ -1737,10 +1768,15 @@ export class GatewayProvider {
|
|
|
1737
1768
|
return this.balance;
|
|
1738
1769
|
}
|
|
1739
1770
|
}
|
|
1740
|
-
|
|
1771
|
+
/** Strict fresh read — null on ANY failure (missing tool, thrown fetch,
|
|
1772
|
+
* non-array garbage). Callers whose verdict claims order-state coverage
|
|
1773
|
+
* (Kill) MUST distinguish "confirmed list" from "unknown"; collapsing a
|
|
1774
|
+
* failed read into the ≤60s cache here was how a stale-empty cache became
|
|
1775
|
+
* a green "No open orders to cancel" (audit 2026-07-26 F6). */
|
|
1776
|
+
async fetchFreshOpenOrdersOrNull() {
|
|
1741
1777
|
const tool = this.toolMap.fetch_open_orders;
|
|
1742
1778
|
if (!tool || !this.http)
|
|
1743
|
-
return
|
|
1779
|
+
return null;
|
|
1744
1780
|
try {
|
|
1745
1781
|
// Fetch ALL open orders (no symbol filter). Multi-symbol orphan orders
|
|
1746
1782
|
// submitted while the browser was disconnected must show up in the
|
|
@@ -1749,7 +1785,7 @@ export class GatewayProvider {
|
|
|
1749
1785
|
const result = await this.http.invoke(tool, {});
|
|
1750
1786
|
if (!Array.isArray(result.data)) {
|
|
1751
1787
|
logger.warn(TAG, `fetch_open_orders returned non-array: ${typeof result.data}`);
|
|
1752
|
-
return
|
|
1788
|
+
return null;
|
|
1753
1789
|
}
|
|
1754
1790
|
const mapped = result.data
|
|
1755
1791
|
.map((o) => mapCcxtOrder(o))
|
|
@@ -1761,10 +1797,15 @@ export class GatewayProvider {
|
|
|
1761
1797
|
return mapped;
|
|
1762
1798
|
}
|
|
1763
1799
|
catch (err) {
|
|
1764
|
-
logger.warn(TAG, `Fresh open orders fetch failed
|
|
1765
|
-
return
|
|
1800
|
+
logger.warn(TAG, `Fresh open orders fetch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1801
|
+
return null;
|
|
1766
1802
|
}
|
|
1767
1803
|
}
|
|
1804
|
+
/** Cached-fallback wrapper for DISPLAY surfaces (the reconcile snapshot):
|
|
1805
|
+
* stale data beats a blank panel there. Never use for emergency verdicts. */
|
|
1806
|
+
async fetchFreshOpenOrders() {
|
|
1807
|
+
return (await this.fetchFreshOpenOrdersOrNull()) ?? this.openOrders;
|
|
1808
|
+
}
|
|
1768
1809
|
async fetchAgentHealth() {
|
|
1769
1810
|
const ws = this.wsClient;
|
|
1770
1811
|
if (!ws)
|
|
@@ -2309,7 +2350,13 @@ export class GatewayProvider {
|
|
|
2309
2350
|
const url = this.config.intelligenceUrl;
|
|
2310
2351
|
const token = this.config.connectionToken;
|
|
2311
2352
|
if (!url || !token) {
|
|
2312
|
-
|
|
2353
|
+
// NEVER debug-level. This is the only writer of intel's `trade_results`,
|
|
2354
|
+
// and those rows are what `minTradesForKelly` counts and what
|
|
2355
|
+
// `get_agent_profile` derives its tier from. Silently skipping here is
|
|
2356
|
+
// indistinguishable from "the agent has never traded" — which is exactly
|
|
2357
|
+
// how it read for months. See DEFAULT_INTELLIGENCE_URL in gateway-config.
|
|
2358
|
+
logger.warn(TAG, `Intelligence not configured (url=${url ? 'set' : 'MISSING'}, token=${token ? 'set' : 'MISSING'}) — DROPPING trade result for ${symbol}. `
|
|
2359
|
+
+ 'Kelly sizing and the agent profile are computed from these rows and will stay pinned at their no-history defaults.');
|
|
2313
2360
|
return;
|
|
2314
2361
|
}
|
|
2315
2362
|
const direction = entry.side === 'long' ? 'LONG' : 'SHORT';
|
|
@@ -2321,7 +2368,7 @@ export class GatewayProvider {
|
|
|
2321
2368
|
const durationSeconds = Math.floor((Date.now() - new Date(entry.entryTime).getTime()) / 1000);
|
|
2322
2369
|
const tradeResult = {
|
|
2323
2370
|
missionId: entry.missionId ?? `trade-${Date.now()}`,
|
|
2324
|
-
symbol:
|
|
2371
|
+
symbol: this.toIntelSymbol(symbol), // venue-aware: BTC/USDT → BTCUSDT, BTC/USDC → HL_BTC
|
|
2325
2372
|
direction,
|
|
2326
2373
|
strategy: this.lastKnownStrategy.name,
|
|
2327
2374
|
regime: entry.regime ?? this.currentRegime,
|
|
@@ -2406,9 +2453,9 @@ export class GatewayProvider {
|
|
|
2406
2453
|
}
|
|
2407
2454
|
}
|
|
2408
2455
|
// ---- Intelligence service polling ----
|
|
2409
|
-
/**
|
|
2456
|
+
/** The configured symbol as the intel DB spells it. */
|
|
2410
2457
|
get intelligenceSymbol() {
|
|
2411
|
-
return this.config.symbol
|
|
2458
|
+
return this.toIntelSymbol(this.config.symbol);
|
|
2412
2459
|
}
|
|
2413
2460
|
/** Shared intelligence poller with circuit-breaker (exponential backoff on errors). */
|
|
2414
2461
|
startIntelligencePoller(name, endpoint, intervalMs, onData, intervalRef, delayFirstMs = 0) {
|
|
@@ -2420,6 +2467,21 @@ export class GatewayProvider {
|
|
|
2420
2467
|
}
|
|
2421
2468
|
return;
|
|
2422
2469
|
}
|
|
2470
|
+
// These five read-pollers (regime 60s, signals 10s, missions 10s,
|
|
2471
|
+
// analytics 30s, decision-trace 30s ≈ 22 req/min per box) were dormant for
|
|
2472
|
+
// as long as `intelligenceUrl` had no default. Giving it one turns them on
|
|
2473
|
+
// everywhere, which is the intended behaviour — they feed the dashboard's
|
|
2474
|
+
// regime/signals/missions panels — but it IS new load on an intel API with
|
|
2475
|
+
// a known heap leak. Kill-switch so that load can be shed in one restart
|
|
2476
|
+
// WITHOUT taking the trade_results writer down with it: the writer is what
|
|
2477
|
+
// Kelly sizing and the agent tier are computed from, and it does not go
|
|
2478
|
+
// through this function.
|
|
2479
|
+
if (process.env.RC_SKILL_INTEL_POLL === 'off') {
|
|
2480
|
+
if (name === 'regime') {
|
|
2481
|
+
logger.warn(TAG, 'RC_SKILL_INTEL_POLL=off — intelligence read-polling disabled (trade-result reporting is unaffected)');
|
|
2482
|
+
}
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2423
2485
|
const symbol = this.intelligenceSymbol;
|
|
2424
2486
|
const fullUrl = `${url}/api/${endpoint}/${symbol}`;
|
|
2425
2487
|
logger.info(TAG, `Starting ${name} poller: ${fullUrl}`);
|
|
@@ -38,6 +38,14 @@ export interface ClearExchangeCredentialsOutcome {
|
|
|
38
38
|
export interface OnboardingContext {
|
|
39
39
|
http: GatewayHttpClient | null;
|
|
40
40
|
toolMap: ToolMap;
|
|
41
|
+
/** rc_* connection token, injected as `operator_token` on the four
|
|
42
|
+
* state-MUTATING operator tools (set_trading_mode /
|
|
43
|
+
* set_exchange_credentials / clear_exchange_credentials /
|
|
44
|
+
* set_bracket_requirement). The plugin refuses those calls without it
|
|
45
|
+
* (audit 2026-07-26 F12) — this is the operator-provenance proof that the
|
|
46
|
+
* request came through the dashboard path, which the agent cannot forge
|
|
47
|
+
* conversationally (chat redaction strips rc_* tokens). */
|
|
48
|
+
operatorToken?: string;
|
|
41
49
|
}
|
|
42
50
|
export type BracketMode = 'off' | 'observe' | 'enforce';
|
|
43
51
|
export type BracketRequirementFlag = 'requireStopLoss' | 'requireTakeProfit';
|
|
@@ -23,7 +23,7 @@ export async function executeSetTradingMode(ctx, mode, acknowledged) {
|
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
|
-
const result = await ctx.http.invoke(tool, { mode, acknowledged });
|
|
26
|
+
const result = await ctx.http.invoke(tool, { mode, acknowledged, operator_token: ctx.operatorToken });
|
|
27
27
|
return result.data;
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
@@ -93,7 +93,7 @@ export async function executeClearExchangeCredentials(ctx) {
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
95
|
try {
|
|
96
|
-
const result = await ctx.http.invoke(tool, { confirm: true });
|
|
96
|
+
const result = await ctx.http.invoke(tool, { confirm: true, operator_token: ctx.operatorToken });
|
|
97
97
|
return result.data;
|
|
98
98
|
}
|
|
99
99
|
catch (err) {
|
|
@@ -162,7 +162,7 @@ export async function executeSetBracketRequirement(ctx, flag, value) {
|
|
|
162
162
|
};
|
|
163
163
|
}
|
|
164
164
|
try {
|
|
165
|
-
const result = await ctx.http.invoke(tool, { flag, value });
|
|
165
|
+
const result = await ctx.http.invoke(tool, { flag, value, operator_token: ctx.operatorToken });
|
|
166
166
|
return result.data;
|
|
167
167
|
}
|
|
168
168
|
catch (err) {
|
|
@@ -194,7 +194,7 @@ export async function executeSetExchangeCredentials(ctx, apiKey, secret, testnet
|
|
|
194
194
|
};
|
|
195
195
|
}
|
|
196
196
|
try {
|
|
197
|
-
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet });
|
|
197
|
+
const result = await ctx.http.invoke(tool, { apiKey, secret, testnet, operator_token: ctx.operatorToken });
|
|
198
198
|
return result.data;
|
|
199
199
|
}
|
|
200
200
|
catch (err) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type VenueReachabilityResult } from '@reefclaw/shared';
|
|
1
2
|
import type { CcxtTicker, CcxtOHLCV } from '../types.js';
|
|
2
3
|
import type { OrderBookDepth } from '../simulator/types.js';
|
|
3
4
|
import type { PublicMarketDataApi } from './public-market-data-api.js';
|
|
@@ -28,9 +29,20 @@ export declare class BinancePublicApi implements PublicMarketDataApi {
|
|
|
28
29
|
* - 'reachable' clean response (driftMs = serverTime − localTime)
|
|
29
30
|
* - 'geo_blocked' HTTP 451 — host is in a restricted region (actionable)
|
|
30
31
|
* - 'unknown' the ban/weight gate paused us — NOT a host problem
|
|
31
|
-
* - '
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
* - 'stalled' THIS process was starved — we learned nothing (issue #265)
|
|
33
|
+
* - 'unreachable' network / DNS / timeout / other error
|
|
34
|
+
*
|
|
35
|
+
* ★ Self-stall detection: an error is evidence about BINANCE only if it
|
|
36
|
+
* arrived on schedule. When the host starves the process the loop stops
|
|
37
|
+
* running, ccxt's own timeout lands minutes late, and the old code blamed the
|
|
38
|
+
* network — producing a confident "you are geo-blocked / check your firewall"
|
|
39
|
+
* banner while the agent was in fact trading normally. Past
|
|
40
|
+
* REACHABILITY_STALL_FACTOR× the request budget we report `stalled` and the
|
|
41
|
+
* reporter renders it `unknown`. A 451 still wins: the server ANSWERED, so
|
|
42
|
+
* that classification stands however late we noticed it. */
|
|
43
|
+
probeReachability(): Promise<VenueReachabilityResult>;
|
|
44
|
+
/** The request budget the stall yardstick is measured against. ccxt owns the
|
|
45
|
+
* actual timeout, so read it from the instance rather than hardcoding a
|
|
46
|
+
* second copy that could silently drift from it. */
|
|
47
|
+
private probeTimeoutMs;
|
|
36
48
|
}
|