@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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export type ShockWakeMode = 'off' | 'shadow' | 'on';
|
|
2
|
+
export interface ShockEntry {
|
|
3
|
+
symbol: string;
|
|
4
|
+
time: string;
|
|
5
|
+
flags: string[];
|
|
6
|
+
shock30mAtr: number;
|
|
7
|
+
range30mAtr: number;
|
|
8
|
+
return1hPct: number;
|
|
9
|
+
return4hPct: number;
|
|
10
|
+
tape4hAtr: number;
|
|
11
|
+
regime: string;
|
|
12
|
+
summary?: string;
|
|
13
|
+
}
|
|
14
|
+
/** 'BTCUSDT' / 'HL_BTC' / 'BTC/USDC' → 'BTC'. Best-effort market vocabulary
|
|
15
|
+
* for the wake message — never used to address an order. */
|
|
16
|
+
export declare function coinOf(intelSymbol: string): string;
|
|
17
|
+
/** Parse gates.shockWake off the raw /api/internal/config payload. Central-only
|
|
18
|
+
* gate — absent/garbage → 'off'. */
|
|
19
|
+
export declare function parseShockWakeGate(raw: unknown): ShockWakeMode;
|
|
20
|
+
export interface WakeAssessment {
|
|
21
|
+
wake: boolean;
|
|
22
|
+
reason?: 'leader_shock' | 'broad_shock';
|
|
23
|
+
/** Fresh 'shock'-flagged entries, extreme-first (as served). */
|
|
24
|
+
shocked: ShockEntry[];
|
|
25
|
+
}
|
|
26
|
+
/** Pure wake-worthiness rule. `nowMs` gates staleness. Broad rule (2026-09-06):
|
|
27
|
+
* ≥`minBroadCount` distinct coins shocked, OR ≥3 when a major participates. */
|
|
28
|
+
export declare function assessWakeWorthiness(shocks: ShockEntry[], nowMs: number, minBroadCount?: number): WakeAssessment;
|
|
29
|
+
/** The out-of-band agent turn. Clearly machine-labeled (never impersonates the
|
|
30
|
+
* operator), mandates re-evaluation, orders nothing. */
|
|
31
|
+
export declare function buildWakeMessage(a: WakeAssessment): string;
|
|
32
|
+
export interface ShockWakeOpts {
|
|
33
|
+
provider: {
|
|
34
|
+
handleChat(content: string): Promise<{
|
|
35
|
+
received: boolean;
|
|
36
|
+
error?: string;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
token: string;
|
|
40
|
+
intelligenceUrl: string;
|
|
41
|
+
webappUrl?: string;
|
|
42
|
+
fetchImpl?: typeof fetch;
|
|
43
|
+
now?: () => number;
|
|
44
|
+
}
|
|
45
|
+
export declare class ShockWakePoller {
|
|
46
|
+
private readonly opts;
|
|
47
|
+
private mode;
|
|
48
|
+
private pollTimer;
|
|
49
|
+
private gateTimer;
|
|
50
|
+
private lastWakeAtMs;
|
|
51
|
+
private wakesToday;
|
|
52
|
+
private wakeDayUtc;
|
|
53
|
+
private polling;
|
|
54
|
+
private readonly pollMs;
|
|
55
|
+
private readonly gatePollMs;
|
|
56
|
+
private readonly cooldownMs;
|
|
57
|
+
private readonly dailyCap;
|
|
58
|
+
private readonly minBroad;
|
|
59
|
+
private readonly replyTimeoutMs;
|
|
60
|
+
private lastAssistantAtMs;
|
|
61
|
+
private awaitingSinceMs;
|
|
62
|
+
private awaitingDeadlineMs;
|
|
63
|
+
private suspectedSwallows;
|
|
64
|
+
constructor(opts: ShockWakeOpts);
|
|
65
|
+
start(): void;
|
|
66
|
+
stop(): void;
|
|
67
|
+
getMode(): ShockWakeMode;
|
|
68
|
+
/** Fed by the bridge from provider `chatMessageStart` — any agent turn
|
|
69
|
+
* beginning counts (the wake's reply, or an operator chat's; either proves
|
|
70
|
+
* the lane is alive, which is what the swallow check needs). */
|
|
71
|
+
noteAssistantActivity(atMs?: number): void;
|
|
72
|
+
/** Suspected-swallow count this process (test/telemetry seam). */
|
|
73
|
+
getSuspectedSwallows(): number;
|
|
74
|
+
private checkPendingReply;
|
|
75
|
+
private refreshGate;
|
|
76
|
+
/** One poll cycle. Exposed for tests. */
|
|
77
|
+
tick(): Promise<void>;
|
|
78
|
+
/** Test seam. */
|
|
79
|
+
__setModeForTest(mode: ShockWakeMode): void;
|
|
80
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// Shock-wake poller — WS3 of docs/MARKET_ADAPTIVITY_PLAN.md.
|
|
2
|
+
//
|
|
3
|
+
// The agent thinks on a 15–30 min heartbeat; an announcement lands mid-cycle
|
|
4
|
+
// and isn't reconsidered until the next one — the adaptivity investigation's
|
|
5
|
+
// core latency gap. This poller watches intel's /api/shocks (the WS2
|
|
6
|
+
// change-of-character flags, computed each fact pass) and, when the market
|
|
7
|
+
// genuinely changes character, delivers ONE out-of-band agent turn through
|
|
8
|
+
// the same gateway path operator chat uses (provider.handleChat — the
|
|
9
|
+
// precedent is the trade-mission presenter).
|
|
10
|
+
//
|
|
11
|
+
// Guardrails (every one deliberately boring):
|
|
12
|
+
// - Central gate `agent_config.gates.shockWake`: off (default) → shadow
|
|
13
|
+
// (logs WOULD WAKE, delivers nothing) → on. Polled from the webapp
|
|
14
|
+
// every ~5 min; absent/garbage/outage → off. No local file fallback —
|
|
15
|
+
// this gate is central-only.
|
|
16
|
+
// - Wake-worthy = a market LEADER (BTC/ETH) flagged 'shock', OR a broad
|
|
17
|
+
// correlated move: ≥5 distinct coins shocked (env RC_SHOCK_WAKE_MIN_BROAD),
|
|
18
|
+
// or ≥3 when a MAJOR (BTC/ETH/BNB/SOL/XRP) is among them. Disagreement-only
|
|
19
|
+
// flags never wake — they are slow-moving and already ride scan_pairs
|
|
20
|
+
// indication. (Broad bar raised from a flat ≥3 on 2026-09-06: the first
|
|
21
|
+
// live night delivered 7 wakes on rotating 3-coin mid-cap alt clusters —
|
|
22
|
+
// routine alt volatility, not announcements. Replayed against the new
|
|
23
|
+
// rule, those 7 become 1.)
|
|
24
|
+
// - Stale shocks (fact older than 10 min) are ignored.
|
|
25
|
+
// - Global cooldown 60 min between wakes + daily cap 8 (UTC), armed in
|
|
26
|
+
// shadow too so the shadow soak counts exactly what 'on' would deliver.
|
|
27
|
+
// A FAILED delivery does not arm the cooldown (retry next tick).
|
|
28
|
+
// (Cooldown default raised 30→60 min 2026-09-05: the shadow soak showed a
|
|
29
|
+
// single sustained 09-03 event re-firing 4 would-wakes in 95 min — one
|
|
30
|
+
// re-ping per hour during a long move is the intended cadence.)
|
|
31
|
+
// - The wake is INDICATION: it mandates re-evaluation, never an action.
|
|
32
|
+
// - DELIVERY CONFIRMATION (2026-09-06): the gateway acks the RPC even when
|
|
33
|
+
// the main-session lane silently swallows the turn (the 2026-07-22
|
|
34
|
+
// OpenClaw class — 7 of the first night's 9 wakes died this way). The
|
|
35
|
+
// bridge wires provider `chatMessageStart` into noteAssistantActivity();
|
|
36
|
+
// if no agent turn STARTS within RC_SHOCK_WAKE_REPLY_TIMEOUT_MS (10 min)
|
|
37
|
+
// of a delivered wake, a WARN names the swallow suspicion (dashboard chat
|
|
38
|
+
// is likely dead too) and a counter increments. Observability only — no
|
|
39
|
+
// auto-retry into a wedged lane.
|
|
40
|
+
//
|
|
41
|
+
// Env overrides: RC_SHOCK_WAKE_POLL_MS (60s), RC_SHOCK_WAKE_GATE_POLL_MS
|
|
42
|
+
// (5 min), RC_SHOCK_WAKE_COOLDOWN_MS (30 min), RC_SHOCK_WAKE_DAILY_CAP (8).
|
|
43
|
+
import { logger, formatError } from './logger.js';
|
|
44
|
+
const TAG = 'shock-wake';
|
|
45
|
+
/** Leaders whose lone shock is wake-worthy, as base coins (venue-stripped). */
|
|
46
|
+
const LEADER_COINS = new Set(['BTC', 'ETH']);
|
|
47
|
+
/** Majors (the platform's liquidation-pulse majors group): their participation
|
|
48
|
+
* lowers the broad-shock coin bar from 5 to 3 — a SOL-led cluster is market
|
|
49
|
+
* information; three rotating meme-alts are Tuesday. */
|
|
50
|
+
const MAJOR_COINS = new Set(['BTC', 'ETH', 'BNB', 'SOL', 'XRP']);
|
|
51
|
+
const STALE_SHOCK_MS = 10 * 60_000;
|
|
52
|
+
function envInt(name, fallback) {
|
|
53
|
+
const v = Number(process.env[name]);
|
|
54
|
+
return Number.isFinite(v) && v > 0 ? v : fallback;
|
|
55
|
+
}
|
|
56
|
+
/** 'BTCUSDT' / 'HL_BTC' / 'BTC/USDC' → 'BTC'. Best-effort market vocabulary
|
|
57
|
+
* for the wake message — never used to address an order. */
|
|
58
|
+
export function coinOf(intelSymbol) {
|
|
59
|
+
return intelSymbol
|
|
60
|
+
.replace(/^HL_/, '')
|
|
61
|
+
.replace(/\/.*$/, '')
|
|
62
|
+
.replace(/(USDT|USDC|BUSD)$/i, '')
|
|
63
|
+
.toUpperCase();
|
|
64
|
+
}
|
|
65
|
+
/** Parse gates.shockWake off the raw /api/internal/config payload. Central-only
|
|
66
|
+
* gate — absent/garbage → 'off'. */
|
|
67
|
+
export function parseShockWakeGate(raw) {
|
|
68
|
+
if (!raw || typeof raw !== 'object')
|
|
69
|
+
return 'off';
|
|
70
|
+
const gates = raw.gates;
|
|
71
|
+
if (!gates || typeof gates !== 'object')
|
|
72
|
+
return 'off';
|
|
73
|
+
const v = gates.shockWake;
|
|
74
|
+
return v === 'shadow' || v === 'on' ? v : 'off';
|
|
75
|
+
}
|
|
76
|
+
/** Pure wake-worthiness rule. `nowMs` gates staleness. Broad rule (2026-09-06):
|
|
77
|
+
* ≥`minBroadCount` distinct coins shocked, OR ≥3 when a major participates. */
|
|
78
|
+
export function assessWakeWorthiness(shocks, nowMs, minBroadCount = 5) {
|
|
79
|
+
const fresh = shocks.filter((s) => {
|
|
80
|
+
const t = Date.parse(s.time);
|
|
81
|
+
return Number.isFinite(t) && nowMs - t <= STALE_SHOCK_MS;
|
|
82
|
+
});
|
|
83
|
+
const shocked = fresh.filter((s) => s.flags.includes('shock'));
|
|
84
|
+
// One coin can appear per-venue (BTCUSDT + HL_BTC) — count coins, not rows.
|
|
85
|
+
const coins = new Set(shocked.map((s) => coinOf(s.symbol)));
|
|
86
|
+
const leader = shocked.find((s) => LEADER_COINS.has(coinOf(s.symbol)));
|
|
87
|
+
if (leader)
|
|
88
|
+
return { wake: true, reason: 'leader_shock', shocked };
|
|
89
|
+
const hasMajor = [...coins].some((c) => MAJOR_COINS.has(c));
|
|
90
|
+
if (coins.size >= minBroadCount || (hasMajor && coins.size >= 3)) {
|
|
91
|
+
return { wake: true, reason: 'broad_shock', shocked };
|
|
92
|
+
}
|
|
93
|
+
return { wake: false, shocked };
|
|
94
|
+
}
|
|
95
|
+
/** The out-of-band agent turn. Clearly machine-labeled (never impersonates the
|
|
96
|
+
* operator), mandates re-evaluation, orders nothing. */
|
|
97
|
+
export function buildWakeMessage(a) {
|
|
98
|
+
const seen = new Set();
|
|
99
|
+
const lines = [];
|
|
100
|
+
for (const s of a.shocked) {
|
|
101
|
+
const coin = coinOf(s.symbol);
|
|
102
|
+
if (seen.has(coin))
|
|
103
|
+
continue;
|
|
104
|
+
seen.add(coin);
|
|
105
|
+
const sign = (v) => (v >= 0 ? '+' : '');
|
|
106
|
+
lines.push(`- ${coin}: 30m ${Math.max(s.shock30mAtr, s.range30mAtr).toFixed(1)}×ATR, ` +
|
|
107
|
+
`1h ${sign(s.return1hPct)}${s.return1hPct}%, 4h ${sign(s.return4hPct)}${s.return4hPct}% ` +
|
|
108
|
+
`(regime label: ${s.regime})`);
|
|
109
|
+
if (lines.length >= 6)
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
return [
|
|
113
|
+
'[AUTOMATED MARKET-SHIFT ALERT — from the ReefClaw system, NOT the operator]',
|
|
114
|
+
'',
|
|
115
|
+
a.reason === 'leader_shock'
|
|
116
|
+
? 'A market leader just moved sharply — the market may have changed character:'
|
|
117
|
+
: 'Multiple symbols moved sharply together — the market may have changed character:',
|
|
118
|
+
...lines,
|
|
119
|
+
'',
|
|
120
|
+
'Do this NOW, ahead of your scheduled heartbeat:',
|
|
121
|
+
'1. Re-derive your directional bias from FRESH data (scan_pairs, get_regime) — do NOT lean on theses formed before this move; regime labels may LAG it (see market_shift_caution in scan results).',
|
|
122
|
+
'2. For EVERY open position: check the pinned invalidation FIRST. If price is through it, act on your own plan.',
|
|
123
|
+
'3. Hold off on new entries until the re-derived bias and the tape agree.',
|
|
124
|
+
'',
|
|
125
|
+
'This alert is indication only — it does not order you to close anything. Reply with a brief assessment so the operator sees your read.',
|
|
126
|
+
].join('\n');
|
|
127
|
+
}
|
|
128
|
+
export class ShockWakePoller {
|
|
129
|
+
opts;
|
|
130
|
+
mode = 'off';
|
|
131
|
+
pollTimer = null;
|
|
132
|
+
gateTimer = null;
|
|
133
|
+
lastWakeAtMs = 0;
|
|
134
|
+
wakesToday = 0;
|
|
135
|
+
wakeDayUtc = '';
|
|
136
|
+
polling = false;
|
|
137
|
+
pollMs = envInt('RC_SHOCK_WAKE_POLL_MS', 60_000);
|
|
138
|
+
gatePollMs = envInt('RC_SHOCK_WAKE_GATE_POLL_MS', 5 * 60_000);
|
|
139
|
+
cooldownMs = envInt('RC_SHOCK_WAKE_COOLDOWN_MS', 60 * 60_000);
|
|
140
|
+
dailyCap = envInt('RC_SHOCK_WAKE_DAILY_CAP', 8);
|
|
141
|
+
minBroad = envInt('RC_SHOCK_WAKE_MIN_BROAD', 5);
|
|
142
|
+
replyTimeoutMs = envInt('RC_SHOCK_WAKE_REPLY_TIMEOUT_MS', 10 * 60_000);
|
|
143
|
+
// Delivery confirmation (see header): a wake is only proven delivered when
|
|
144
|
+
// an agent turn STARTS after it. lastAssistantAtMs is fed by the bridge's
|
|
145
|
+
// chatMessageStart wiring; awaitingSince/deadline track one in-flight wake
|
|
146
|
+
// (the 60-min cooldown guarantees no overlap with the 10-min timeout).
|
|
147
|
+
lastAssistantAtMs = 0;
|
|
148
|
+
awaitingSinceMs = null;
|
|
149
|
+
awaitingDeadlineMs = 0;
|
|
150
|
+
suspectedSwallows = 0;
|
|
151
|
+
constructor(opts) {
|
|
152
|
+
this.opts = opts;
|
|
153
|
+
}
|
|
154
|
+
start() {
|
|
155
|
+
if (this.pollTimer)
|
|
156
|
+
return;
|
|
157
|
+
const refresh = () => { void this.refreshGate(); };
|
|
158
|
+
refresh();
|
|
159
|
+
this.gateTimer = setInterval(refresh, this.gatePollMs);
|
|
160
|
+
this.gateTimer.unref?.();
|
|
161
|
+
this.pollTimer = setInterval(() => { void this.tick(); }, this.pollMs);
|
|
162
|
+
this.pollTimer.unref?.();
|
|
163
|
+
logger.info(TAG, `started (poll ${this.pollMs / 1000}s, cooldown ${this.cooldownMs / 60000}m, cap ${this.dailyCap}/day, gate polled ${this.gatePollMs / 60000}m)`);
|
|
164
|
+
}
|
|
165
|
+
stop() {
|
|
166
|
+
if (this.pollTimer)
|
|
167
|
+
clearInterval(this.pollTimer);
|
|
168
|
+
if (this.gateTimer)
|
|
169
|
+
clearInterval(this.gateTimer);
|
|
170
|
+
this.pollTimer = null;
|
|
171
|
+
this.gateTimer = null;
|
|
172
|
+
}
|
|
173
|
+
getMode() {
|
|
174
|
+
return this.mode;
|
|
175
|
+
}
|
|
176
|
+
/** Fed by the bridge from provider `chatMessageStart` — any agent turn
|
|
177
|
+
* beginning counts (the wake's reply, or an operator chat's; either proves
|
|
178
|
+
* the lane is alive, which is what the swallow check needs). */
|
|
179
|
+
noteAssistantActivity(atMs) {
|
|
180
|
+
const t = atMs ?? this.opts.now?.() ?? Date.now();
|
|
181
|
+
this.lastAssistantAtMs = t;
|
|
182
|
+
if (this.awaitingSinceMs !== null && t >= this.awaitingSinceMs) {
|
|
183
|
+
logger.info(TAG, `wake reply confirmed — agent turn started ${Math.round((t - this.awaitingSinceMs) / 1000)}s after delivery`);
|
|
184
|
+
this.awaitingSinceMs = null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Suspected-swallow count this process (test/telemetry seam). */
|
|
188
|
+
getSuspectedSwallows() {
|
|
189
|
+
return this.suspectedSwallows;
|
|
190
|
+
}
|
|
191
|
+
checkPendingReply(nowMs) {
|
|
192
|
+
if (this.awaitingSinceMs === null || nowMs <= this.awaitingDeadlineMs)
|
|
193
|
+
return;
|
|
194
|
+
this.suspectedSwallows++;
|
|
195
|
+
logger.warn(TAG, `wake delivered at ${new Date(this.awaitingSinceMs).toISOString()} but NO agent turn started ` +
|
|
196
|
+
`within ${Math.round(this.replyTimeoutMs / 60_000)}m — the main-session lane may be silently ` +
|
|
197
|
+
`swallowing turns (known OpenClaw class, 2026-07-22; dashboard chat is likely affected too). ` +
|
|
198
|
+
`Suspected swallows this process: ${this.suspectedSwallows}`);
|
|
199
|
+
this.awaitingSinceMs = null;
|
|
200
|
+
}
|
|
201
|
+
async refreshGate() {
|
|
202
|
+
const base = (this.opts.webappUrl ?? process.env.REEFCLAW_API_URL ?? 'https://www.reefclaw.com').replace(/\/$/, '');
|
|
203
|
+
try {
|
|
204
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
205
|
+
const res = await doFetch(`${base}/api/internal/config`, {
|
|
206
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
207
|
+
});
|
|
208
|
+
if (!res.ok)
|
|
209
|
+
return; // keep last-known mode on outage (fail toward inert)
|
|
210
|
+
const next = parseShockWakeGate(await res.json());
|
|
211
|
+
if (next !== this.mode) {
|
|
212
|
+
logger.info(TAG, `gate: shockWake ${this.mode} → ${next}`);
|
|
213
|
+
this.mode = next;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
logger.debug(TAG, `gate poll failed (keeping ${this.mode}): ${formatError(err)}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** One poll cycle. Exposed for tests. */
|
|
221
|
+
async tick() {
|
|
222
|
+
const tickNowMs = this.opts.now?.() ?? Date.now();
|
|
223
|
+
// Runs even when the gate is off — a wake delivered just before an
|
|
224
|
+
// operator rollback still deserves its confirmation verdict.
|
|
225
|
+
this.checkPendingReply(tickNowMs);
|
|
226
|
+
if (this.mode === 'off' || this.polling)
|
|
227
|
+
return;
|
|
228
|
+
this.polling = true;
|
|
229
|
+
try {
|
|
230
|
+
const nowMs = tickNowMs;
|
|
231
|
+
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
232
|
+
const base = this.opts.intelligenceUrl.replace(/\/$/, '');
|
|
233
|
+
const res = await doFetch(`${base}/api/shocks`, {
|
|
234
|
+
headers: { Authorization: `Bearer ${this.opts.token}` },
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok)
|
|
237
|
+
return;
|
|
238
|
+
const body = (await res.json());
|
|
239
|
+
if (!Array.isArray(body?.shocks))
|
|
240
|
+
return;
|
|
241
|
+
const a = assessWakeWorthiness(body.shocks, nowMs, this.minBroad);
|
|
242
|
+
if (!a.wake)
|
|
243
|
+
return;
|
|
244
|
+
// Rate limits — checked only for wake-worthy ticks so quiet markets
|
|
245
|
+
// never touch the counters.
|
|
246
|
+
const day = new Date(nowMs).toISOString().slice(0, 10);
|
|
247
|
+
if (day !== this.wakeDayUtc) {
|
|
248
|
+
this.wakeDayUtc = day;
|
|
249
|
+
this.wakesToday = 0;
|
|
250
|
+
}
|
|
251
|
+
if (nowMs - this.lastWakeAtMs < this.cooldownMs)
|
|
252
|
+
return;
|
|
253
|
+
if (this.wakesToday >= this.dailyCap) {
|
|
254
|
+
logger.warn(TAG, `daily cap ${this.dailyCap} reached — suppressing further wakes today`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const coins = [...new Set(a.shocked.map((s) => coinOf(s.symbol)))].join(', ');
|
|
258
|
+
if (this.mode === 'shadow') {
|
|
259
|
+
this.lastWakeAtMs = nowMs;
|
|
260
|
+
this.wakesToday++;
|
|
261
|
+
logger.warn(TAG, `WOULD WAKE (shadow): ${a.reason} — ${coins} (${this.wakesToday}/${this.dailyCap} today)`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const message = buildWakeMessage(a);
|
|
265
|
+
const out = await this.opts.provider.handleChat(message);
|
|
266
|
+
if (out.received) {
|
|
267
|
+
this.lastWakeAtMs = nowMs;
|
|
268
|
+
this.wakesToday++;
|
|
269
|
+
// Arm the delivery-confirmation window (see checkPendingReply).
|
|
270
|
+
this.awaitingSinceMs = nowMs;
|
|
271
|
+
this.awaitingDeadlineMs = nowMs + this.replyTimeoutMs;
|
|
272
|
+
logger.warn(TAG, `WAKE delivered: ${a.reason} — ${coins} (${this.wakesToday}/${this.dailyCap} today)`);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Delivery failed (gateway not connected etc.) — no cooldown, retry
|
|
276
|
+
// on the next tick while the shock is still fresh.
|
|
277
|
+
logger.warn(TAG, `wake delivery FAILED (${out.error ?? 'unknown'}) — will retry next tick`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
logger.debug(TAG, `tick failed: ${formatError(err)}`);
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
this.polling = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/** Test seam. */
|
|
288
|
+
__setModeForTest(mode) {
|
|
289
|
+
this.mode = mode;
|
|
290
|
+
}
|
|
291
|
+
}
|
package/bridge/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { Channel, RequestFrame, ResponseFrame, EventFrame, Frame, Emergency
|
|
|
3
3
|
export { VALID_CHANNELS, VALID_EMERGENCY_ACTIONS } from '@reefclaw/shared';
|
|
4
4
|
type EventFrame = _EventFrame;
|
|
5
5
|
/** Methods the skill accepts from the relay (browser → skill) */
|
|
6
|
-
export declare const ALLOWED_METHODS: readonly ["emergency.kill", "emergency.flatten", "emergency.pause", "emergency.resume", "reconcile", "chat.send", "skill.update", "close_position", "set_trading_mode", "set_exchange_credentials", "test_exchange_credentials", "clear_exchange_credentials", "hl_provision_agent_wallet", "hl_agent_wallet_status", "hl_submit_agent_approval", "get_bracket_config", "set_bracket_requirement"];
|
|
6
|
+
export declare const ALLOWED_METHODS: readonly ["emergency.kill", "emergency.flatten", "emergency.pause", "emergency.resume", "reconcile", "chat.send", "skill.update", "close_position", "set_trading_mode", "set_exchange_credentials", "test_exchange_credentials", "clear_exchange_credentials", "hl_provision_agent_wallet", "hl_agent_wallet_status", "hl_submit_agent_approval", "get_bracket_config", "set_bracket_requirement", "connector.update"];
|
|
7
7
|
/** Subset of ALLOWED_METHODS that require operator.write scope. The bridge
|
|
8
8
|
* enforces this before dispatching — a session without the scope gets a
|
|
9
9
|
* 403 error. PR2 ships the scope as session-wide (inherited from the Clerk
|
|
@@ -602,6 +602,10 @@ export interface ConnectorConfig {
|
|
|
602
602
|
relayUrl: string;
|
|
603
603
|
userId: string;
|
|
604
604
|
token: string;
|
|
605
|
+
/** Stable per-install id (non-secret), sent as `?instance=` so the relay's
|
|
606
|
+
* skill-slot admission can tell "same box restarting" (instant takeover)
|
|
607
|
+
* from "second box" (rejected 4011). See utils/instance-id.ts. */
|
|
608
|
+
instanceId?: string;
|
|
605
609
|
reconnect?: {
|
|
606
610
|
baseDelayMs?: number;
|
|
607
611
|
maxDelayMs?: number;
|
package/bridge/types.js
CHANGED
|
@@ -25,6 +25,10 @@ export const ALLOWED_METHODS = [
|
|
|
25
25
|
// Bracket-orders config (Phase 3.5b) — operator.write-gated.
|
|
26
26
|
'get_bracket_config',
|
|
27
27
|
'set_bracket_requirement',
|
|
28
|
+
// Operator-triggered connector update. Runs a FIXED command on the box over
|
|
29
|
+
// the gateway PTY — carries no command/version/args field by design (see the
|
|
30
|
+
// security note in providers/connector-update.ts). operator.write-gated.
|
|
31
|
+
'connector.update',
|
|
28
32
|
];
|
|
29
33
|
/** Subset of ALLOWED_METHODS that require operator.write scope. The bridge
|
|
30
34
|
* enforces this before dispatching — a session without the scope gets a
|
|
@@ -42,6 +46,7 @@ export const OPERATOR_WRITE_METHODS = new Set([
|
|
|
42
46
|
'hl_submit_agent_approval',
|
|
43
47
|
'get_bracket_config',
|
|
44
48
|
'set_bracket_requirement',
|
|
49
|
+
'connector.update',
|
|
45
50
|
'emergency.kill',
|
|
46
51
|
'emergency.flatten',
|
|
47
52
|
'emergency.pause',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Stable per-install relay instance id (2026-08-25, skill-slot admission).
|
|
2
|
+
//
|
|
3
|
+
// The relay seats ONE skill connection per account and refuses newcomers —
|
|
4
|
+
// EXCEPT a newcomer proving it is the same box restarting, which takes over
|
|
5
|
+
// instantly (the graceful-restart property). "Same box" = this id, sent as a
|
|
6
|
+
// non-secret `?instance=` query param on the relay URL. It must therefore
|
|
7
|
+
// survive process restarts: persisted once per install at
|
|
8
|
+
// `~/.reefclaw/relay-instance-id` and reused forever.
|
|
9
|
+
//
|
|
10
|
+
// Fail-open: when the filesystem refuses (read-only home, exotic container),
|
|
11
|
+
// fall back to a per-process id. Takeover-after-crash then degrades to the
|
|
12
|
+
// relay's liveness timeout instead of being instant — worse, never wrong.
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { homedir } from 'node:os';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { logger } from '../logger.js';
|
|
18
|
+
const TAG = 'instance-id';
|
|
19
|
+
const ID_SHAPE = /^[A-Za-z0-9-]{8,64}$/;
|
|
20
|
+
/** Read-or-create the stable instance id. `baseDir` overrides the storage
|
|
21
|
+
* directory (tests; defaults to ~/.reefclaw). Never throws. */
|
|
22
|
+
export function resolveRelayInstanceId(baseDir) {
|
|
23
|
+
// Operator escape hatch (e.g. two deliberate installs sharing one home dir).
|
|
24
|
+
const fromEnv = process.env.RC_RELAY_INSTANCE_ID?.trim();
|
|
25
|
+
if (fromEnv && ID_SHAPE.test(fromEnv))
|
|
26
|
+
return fromEnv;
|
|
27
|
+
const dir = baseDir ?? join(homedir(), '.reefclaw');
|
|
28
|
+
const file = join(dir, 'relay-instance-id');
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(file)) {
|
|
31
|
+
const existing = readFileSync(file, 'utf8').trim();
|
|
32
|
+
if (ID_SHAPE.test(existing))
|
|
33
|
+
return existing;
|
|
34
|
+
// Garbled file: fall through and rewrite — a fresh id only costs one
|
|
35
|
+
// liveness-timeout takeover, a garbled param corrupts the admission key.
|
|
36
|
+
}
|
|
37
|
+
const fresh = randomUUID();
|
|
38
|
+
mkdirSync(dir, { recursive: true });
|
|
39
|
+
writeFileSync(file, fresh + '\n', 'utf8');
|
|
40
|
+
return fresh;
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const perProcess = randomUUID();
|
|
44
|
+
logger.warn(TAG, `Could not persist relay instance id (${err.message}) — using per-process id; ` +
|
|
45
|
+
`crash takeover degrades to the relay liveness timeout`);
|
|
46
|
+
return perProcess;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/ccxt/binance-private.js
CHANGED
|
@@ -813,7 +813,8 @@ export class BinancePrivateApi {
|
|
|
813
813
|
const raw = await this.exchange.fetchTicker(symbol);
|
|
814
814
|
noteSuccess();
|
|
815
815
|
return {
|
|
816
|
-
symbol
|
|
816
|
+
// Echo the REQUESTED symbol — see BinancePublicApi.fetchTicker.
|
|
817
|
+
symbol,
|
|
817
818
|
last: raw.last ?? 0,
|
|
818
819
|
bid: raw.bid ?? 0,
|
|
819
820
|
ask: raw.ask ?? 0,
|
package/ccxt/binance-public.js
CHANGED
|
@@ -86,7 +86,12 @@ export class BinancePublicApi {
|
|
|
86
86
|
const raw = await this.exchange.fetchTicker(symbol);
|
|
87
87
|
noteSuccess();
|
|
88
88
|
return {
|
|
89
|
-
symbol
|
|
89
|
+
// Echo the REQUESTED symbol, not ccxt's unified form. ccxt rewrites
|
|
90
|
+
// 'ETH/USDT' → 'ETH/USDT:USDT' on USDM futures; returning that made
|
|
91
|
+
// callers key per-symbol caches under a name the caller never asked
|
|
92
|
+
// for (see the simulator's tickerKey note). Same invariant that
|
|
93
|
+
// intel-public.ts fetchTicker already documents.
|
|
94
|
+
symbol,
|
|
90
95
|
last: raw.last ?? 0,
|
|
91
96
|
bid: raw.bid ?? 0,
|
|
92
97
|
ask: raw.ask ?? 0,
|
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
* never whole-payload rejection. Gates are added one at a time
|
|
5
5
|
* (TOOL_DISTRIBUTION_ARCHITECTURE.md §11 step 3): `exitGate` (slice 2) →
|
|
6
6
|
* `positionReviewMode` (slice 3, the Position Decision Journal
|
|
7
|
-
* heartbeat-mandate + superset gate, file key `positionReview.mode`)
|
|
8
|
-
*
|
|
7
|
+
* heartbeat-mandate + superset gate, file key `positionReview.mode`) →
|
|
8
|
+
* `approvalMode` (slice 4, per-trade operator approval, file key
|
|
9
|
+
* `approval.mode`) → `reentryCooldown` (create_order re-entry cooldown, file
|
|
10
|
+
* key `reentryCooldown.mode`). All but approvalMode ride the same four-stage
|
|
11
|
+
* `off → shadow → observe → enforce` ladder; approvalMode has its own. */
|
|
9
12
|
export interface AgentGates {
|
|
10
13
|
exitGate?: 'off' | 'shadow' | 'observe' | 'enforce';
|
|
11
14
|
positionReviewMode?: 'off' | 'shadow' | 'observe' | 'enforce';
|
|
15
|
+
approvalMode?: 'off' | 'per_trade';
|
|
16
|
+
reentryCooldown?: 'off' | 'shadow' | 'observe' | 'enforce';
|
|
12
17
|
}
|
|
13
18
|
/** Server-resolved entitlement verdict (webapp lib/entitlements.ts, computed
|
|
14
19
|
* from the users row and delivered on the config channel). The plugin NEVER
|
|
@@ -23,6 +23,15 @@ const TAG = 'agent-config';
|
|
|
23
23
|
* because exitGate + positionReviewMode use identical values; a future gate
|
|
24
24
|
* with a different enum gets its own constant. */
|
|
25
25
|
const MODE_LADDER_VALUES = new Set(['off', 'shadow', 'observe', 'enforce']);
|
|
26
|
+
/** approvalMode's own enum — deliberately NOT the four-stage ladder.
|
|
27
|
+
*
|
|
28
|
+
* ★ `shadow` is absent on purpose. Shadow proposal telemetry is driven by the
|
|
29
|
+
* APPROVAL_SHADOW_MODE systemd env flag, which is boot-snapshotted and local
|
|
30
|
+
* to the box; it is not a central value. Accepting 'shadow' here would let a
|
|
31
|
+
* central push silently enable telemetry writes the operator never configured,
|
|
32
|
+
* and would collide with the env flag's precedence. Central can express
|
|
33
|
+
* exactly the two states that change trading behaviour. */
|
|
34
|
+
const APPROVAL_MODE_VALUES = new Set(['off', 'per_trade']);
|
|
26
35
|
const ENTITLEMENT_STATES = new Set([
|
|
27
36
|
'active',
|
|
28
37
|
'trialing',
|
|
@@ -112,6 +121,14 @@ function validateGates(raw) {
|
|
|
112
121
|
if (typeof positionReviewMode === 'string' && MODE_LADDER_VALUES.has(positionReviewMode)) {
|
|
113
122
|
gates.positionReviewMode = positionReviewMode;
|
|
114
123
|
}
|
|
124
|
+
const approvalMode = obj.approvalMode;
|
|
125
|
+
if (typeof approvalMode === 'string' && APPROVAL_MODE_VALUES.has(approvalMode)) {
|
|
126
|
+
gates.approvalMode = approvalMode;
|
|
127
|
+
}
|
|
128
|
+
const reentryCooldown = obj.reentryCooldown;
|
|
129
|
+
if (typeof reentryCooldown === 'string' && MODE_LADDER_VALUES.has(reentryCooldown)) {
|
|
130
|
+
gates.reentryCooldown = reentryCooldown;
|
|
131
|
+
}
|
|
115
132
|
return gates;
|
|
116
133
|
}
|
|
117
134
|
/** Version-monotonic acceptance (basic rollback/replay protection): a fetched
|
|
@@ -65,7 +65,11 @@ export function startAgentConfigPoller(opts) {
|
|
|
65
65
|
});
|
|
66
66
|
if (fetched) {
|
|
67
67
|
loggedFetchFailure = false;
|
|
68
|
-
|
|
68
|
+
// Only touch the cache file when the config actually changed —
|
|
69
|
+
// isAcceptableVersion accepts equal versions, so this used to do a
|
|
70
|
+
// blocking write+rename every 60s poll forever.
|
|
71
|
+
const prevJson = JSON.stringify(current);
|
|
72
|
+
if (applyIfAcceptable(fetched, 'network') && JSON.stringify(fetched) !== prevJson) {
|
|
69
73
|
writeCachedConfig(fetched, opts.cachePath);
|
|
70
74
|
}
|
|
71
75
|
return;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type PluginConfigFile } from './plugin-config-io.js';
|
|
2
2
|
export type BracketMode = 'off' | 'observe' | 'enforce';
|
|
3
|
-
/** Read bracket mode from a config object. Invalid values fall back to
|
|
3
|
+
/** Read bracket mode from a config object. Invalid values fall back to the
|
|
4
|
+
* context default ('enforce' on a live-Binance box, else 'off'). */
|
|
4
5
|
export declare function getBracketMode(config?: PluginConfigFile, remoteOverride?: BracketMode): BracketMode;
|
|
5
6
|
/** Convenience: load from disk and return the effective mode. */
|
|
6
7
|
export declare function loadBracketMode(): BracketMode;
|
|
@@ -5,10 +5,32 @@
|
|
|
5
5
|
// in memory on the plugin after push; not persisted to plugin-config.json.
|
|
6
6
|
// Wired in Phase 2; for Phase 1 this is always undefined.
|
|
7
7
|
// 2. Local plugin-config.json `brackets.mode` field.
|
|
8
|
-
// 3. Default:
|
|
8
|
+
// 3. Default: CONTEXT-AWARE since 2026-08-25 (E2E audit #3, the last
|
|
9
|
+
// real-money item on the fresh-box list): a box whose persisted
|
|
10
|
+
// tradingMode is MICRO_LIVE/LIVE on the BINANCE venue defaults to
|
|
11
|
+
// 'enforce' — the historical 'off' default meant a fresh npx install
|
|
12
|
+
// flipped to live traded with NO exchange-side stops AND no mandatory-
|
|
13
|
+
// stop pre-trade gate (the gate is wired behind bracketsEnabled), i.e.
|
|
14
|
+
// genuinely naked, with no in-product way to notice. Everything else
|
|
15
|
+
// (paper, shadow, hyperliquid — whose adapter enforces brackets
|
|
16
|
+
// unconditionally and ignores this Binance-only knob) keeps 'off'.
|
|
17
|
+
// An EXPLICIT 'off' in config is still honored (operator break-glass,
|
|
18
|
+
// behind the Settings type-to-confirm guard) — with the new default it
|
|
19
|
+
// can only ever be a deliberate choice.
|
|
9
20
|
import { readPluginConfig } from './plugin-config-io.js';
|
|
10
21
|
const VALID = new Set(['off', 'observe', 'enforce']);
|
|
11
|
-
/**
|
|
22
|
+
/** Does this config describe a live-Binance box — the one context where an
|
|
23
|
+
* unset bracket mode must NOT mean "no protection"? Reads the PERSISTED
|
|
24
|
+
* trading mode (set_trading_mode persists BEFORE the adapter swap, so
|
|
25
|
+
* construction-time and per-call reads agree). */
|
|
26
|
+
function isLiveBinanceContext(config) {
|
|
27
|
+
const mode = config?.tradingMode;
|
|
28
|
+
if (mode !== 'MICRO_LIVE' && mode !== 'LIVE')
|
|
29
|
+
return false;
|
|
30
|
+
return config?.exchange?.venue !== 'hyperliquid';
|
|
31
|
+
}
|
|
32
|
+
/** Read bracket mode from a config object. Invalid values fall back to the
|
|
33
|
+
* context default ('enforce' on a live-Binance box, else 'off'). */
|
|
12
34
|
export function getBracketMode(config, remoteOverride) {
|
|
13
35
|
if (remoteOverride && VALID.has(remoteOverride))
|
|
14
36
|
return remoteOverride;
|
|
@@ -16,7 +38,7 @@ export function getBracketMode(config, remoteOverride) {
|
|
|
16
38
|
if (typeof raw === 'string' && VALID.has(raw)) {
|
|
17
39
|
return raw;
|
|
18
40
|
}
|
|
19
|
-
return 'off';
|
|
41
|
+
return isLiveBinanceContext(config) ? 'enforce' : 'off';
|
|
20
42
|
}
|
|
21
43
|
/** Convenience: load from disk and return the effective mode. */
|
|
22
44
|
export function loadBracketMode() {
|
package/config/gate-store.d.ts
CHANGED
|
@@ -9,6 +9,18 @@ declare class GateStore {
|
|
|
9
9
|
/** The central positionReview.mode, or null when central has no value (or the
|
|
10
10
|
* kill-switch is on) — null tells the reader to fall back to the file. */
|
|
11
11
|
getPositionReviewMode(): AgentGates['positionReviewMode'] | null;
|
|
12
|
+
/** The central approval.mode, or null when central has no value (or the
|
|
13
|
+
* kill-switch is on) — null tells the reader to fall back to the file.
|
|
14
|
+
*
|
|
15
|
+
* ★ Central can only ever say 'off' or 'per_trade'. It cannot enable shadow
|
|
16
|
+
* telemetry (that's the local APPROVAL_SHADOW_MODE env flag) and it cannot
|
|
17
|
+
* weaken the hardcoded safety floor — per_trade only ADDS a gate, and 'off'
|
|
18
|
+
* is the pre-existing autonomous behaviour, so neither value can leave a
|
|
19
|
+
* position unprotected. */
|
|
20
|
+
getApprovalMode(): AgentGates['approvalMode'] | null;
|
|
21
|
+
/** The central reentryCooldown mode, or null when central has no value (or
|
|
22
|
+
* the kill-switch is on) — null tells the reader to fall back to the file. */
|
|
23
|
+
getReentryCooldown(): AgentGates['reentryCooldown'] | null;
|
|
12
24
|
/** Test-only. */
|
|
13
25
|
__reset(): void;
|
|
14
26
|
}
|