@reefclaw/connect 0.1.20 → 0.1.22
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/bridge.d.ts +20 -5
- package/assets/bridge/bridge.js +29 -14
- package/assets/bridge/config.js +6 -0
- package/assets/bridge/gateway/gateway-config.d.ts +16 -5
- package/assets/bridge/gateway/gateway-config.js +68 -12
- package/assets/bridge/gateway/gateway-ws-client.d.ts +4 -1
- package/assets/bridge/gateway/gateway-ws-client.js +41 -11
- 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 +51 -1
- package/assets/bridge/providers/gateway.js +209 -22
- package/assets/bridge/providers/onboarding-commands.d.ts +11 -0
- package/assets/bridge/providers/onboarding-commands.js +5 -5
- package/assets/bridge/providers/risk-calculator.d.ts +61 -2
- package/assets/bridge/providers/risk-calculator.js +92 -20
- package/assets/bridge/utils/skill-signing.js +8 -3
- 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 +29 -0
- package/assets/plugin/exchange-adapter.d.ts +13 -0
- package/assets/plugin/index.js +230 -176
- package/assets/plugin/ingest/event-loop-monitor.d.ts +22 -0
- package/assets/plugin/ingest/event-loop-monitor.js +190 -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 +26 -6
- package/assets/plugin/ingest/readiness-reporter.js +137 -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/live/user-data-stream.js +10 -2
- 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/risk/pre-trade-check.js +18 -5
- 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/strategy/condition-registry.js +9 -2
- package/assets/plugin/strategy/evaluator.d.ts +5 -0
- package/assets/plugin/tools/attach-brackets.js +50 -1
- package/assets/plugin/tools/cancel-all-orders.js +9 -1
- package/assets/plugin/tools/create-order.js +18 -1
- package/assets/plugin/tools/get-bracket-config.d.ts +21 -2
- package/assets/plugin/tools/get-bracket-config.js +18 -2
- package/assets/plugin/tools/set-trading-mode.js +6 -3
- 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 +88 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +36 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +116 -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/plugin/venues/registry.js +8 -7
- package/assets/plugin/wave9/paper-admission-guard.d.ts +12 -1
- package/assets/plugin/wave9/paper-admission-guard.js +12 -1
- package/assets/shared/index.d.ts +3 -3
- package/assets/shared/index.js +6 -2
- package/assets/shared/readiness.d.ts +45 -2
- package/assets/shared/readiness.js +60 -8
- package/assets/skill/SKILL.md +6 -11
- package/dist/cli.js +3 -0
- package/package.json +1 -1
|
@@ -6,10 +6,23 @@ import { type HlCredentials } from './hl-private.js';
|
|
|
6
6
|
import type { BracketId } from '../../live/bracket-types.js';
|
|
7
7
|
import { BracketLedger } from '../../live/bracket-ledger.js';
|
|
8
8
|
import { HlBracketCoordinator } from './hl-bracket-coordinator.js';
|
|
9
|
+
import type { TradeIngestWiring } from '../../live/live-adapter.js';
|
|
9
10
|
export interface HlLiveAdapterOptions {
|
|
10
11
|
credentials: HlCredentials;
|
|
11
12
|
mode: TradingMode;
|
|
12
13
|
marketSlippagePct?: number;
|
|
14
|
+
/** MICRO_LIVE per-order notional cap in quote-USD (USDC here). Defaults to
|
|
15
|
+
* $50 in MICRO_LIVE — the same default Binance's LiveAdapter has enforced
|
|
16
|
+
* since the mode existed. HL shipped Phase 3 without any cap, making
|
|
17
|
+
* "micro" a label (audit 2026-07-26 F8). Ignored in LIVE. */
|
|
18
|
+
microLive?: {
|
|
19
|
+
maxPositionUSDT?: number;
|
|
20
|
+
};
|
|
21
|
+
/** Audit-trail wiring (audit 2026-07-26 F26): when present, every user-stream
|
|
22
|
+
* fill is POSTed to /api/internal/trades and reconnect gaps are backfilled
|
|
23
|
+
* via userFillsByTime. Same object boot passes the Binance adapter —
|
|
24
|
+
* `exchange` MUST be fillExchangeId('hyperliquid'). */
|
|
25
|
+
tradeIngest?: TradeIngestWiring;
|
|
13
26
|
/** Test seams. Production omits both. */
|
|
14
27
|
bracketLedger?: BracketLedger;
|
|
15
28
|
disableUserStream?: boolean;
|
|
@@ -25,11 +38,22 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
25
38
|
* the exchange-side legs ARE the safety floor. Lazily constructed so that
|
|
26
39
|
* merely constructing the adapter (registry tests) writes no ledger file. */
|
|
27
40
|
private _coordinator;
|
|
41
|
+
/** Venue capability (see IExchangeAdapter): brackets are unconditional here,
|
|
42
|
+
* so every consumer of `brackets.mode` must read this instead of the
|
|
43
|
+
* Binance-only flag. */
|
|
44
|
+
readonly bracketsAlwaysEnforced = true;
|
|
28
45
|
private userStream;
|
|
29
46
|
private truthCheckTimer;
|
|
30
47
|
private truthCheckRunning;
|
|
31
48
|
private _readiness;
|
|
32
49
|
private openOrdersUnavailableUntil;
|
|
50
|
+
/** MICRO_LIVE per-order notional cap (USD). null = LIVE, uncapped. */
|
|
51
|
+
private readonly maxPositionUsd;
|
|
52
|
+
/** Resolved audit-trail wiring (F26). undefined = ingest not configured. */
|
|
53
|
+
private readonly fillIngest?;
|
|
54
|
+
/** exchangeTime of the newest fill ingested (WS or backfill) — the overlap
|
|
55
|
+
* low-water mark the reconnect gap backfill widens from. */
|
|
56
|
+
private lastFillIngestMs;
|
|
33
57
|
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
34
58
|
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
35
59
|
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
@@ -132,6 +156,10 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
132
156
|
symbol: string;
|
|
133
157
|
positionSide: 'long' | 'short';
|
|
134
158
|
positionSize: number;
|
|
159
|
+
registeredPrices?: {
|
|
160
|
+
stop?: number;
|
|
161
|
+
target?: number;
|
|
162
|
+
};
|
|
135
163
|
}): Promise<{
|
|
136
164
|
resized: boolean;
|
|
137
165
|
slCid?: string;
|
|
@@ -147,6 +175,14 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
|
|
|
147
175
|
* cleanup before a fresh attach). THROWS when order state is unknown —
|
|
148
176
|
* the caller logs and still attaches (protection beats hygiene). */
|
|
149
177
|
cancelSymbolBracketLegs(symbol: string): Promise<number>;
|
|
178
|
+
/** Audit-trail ingest (F26): fire-and-forget POST of one fill. The client
|
|
179
|
+
* never blocks the WS hot path; the server-side (exchange, trade id) upsert
|
|
180
|
+
* makes WS/backfill double-delivery a no-op. */
|
|
181
|
+
private ingestFill;
|
|
182
|
+
/** Reconnect gap backfill (F26): the WS replays NOTHING (T-5), so fills that
|
|
183
|
+
* landed while the socket was down exist ONLY via REST. Over-fetch with a
|
|
184
|
+
* 60s overlap is harmless (idempotent upsert); under-fetch loses audit rows. */
|
|
185
|
+
private backfillFillGap;
|
|
150
186
|
/** Entry fills drive attach (resting limits) / resize (partial-fill growth).
|
|
151
187
|
* `startPosition` is the position BEFORE this fill — the WS-authoritative
|
|
152
188
|
* way to know the after-fill total without an extra REST read. */
|
|
@@ -38,6 +38,7 @@ import { generateBracketId } from '../../live/bracket-id.js';
|
|
|
38
38
|
import { validateStopDirection, validateTargetDirection } from '../../live/bracket-params.js';
|
|
39
39
|
import { HlBracketCoordinator, isRejectedOrder, isTerminalBracketState, } from './hl-bracket-coordinator.js';
|
|
40
40
|
import { HyperliquidUserStream } from './hl-user-stream.js';
|
|
41
|
+
import { hlFillToFillEvent } from './hl-fill-ingest.js';
|
|
41
42
|
import { formatError } from '../../logger.js';
|
|
42
43
|
const TAG = 'hl-live-adapter';
|
|
43
44
|
/** Venue-distinct ledger storage — a venue switch on the same box must never
|
|
@@ -65,11 +66,22 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
65
66
|
* the exchange-side legs ARE the safety floor. Lazily constructed so that
|
|
66
67
|
* merely constructing the adapter (registry tests) writes no ledger file. */
|
|
67
68
|
_coordinator = null;
|
|
69
|
+
/** Venue capability (see IExchangeAdapter): brackets are unconditional here,
|
|
70
|
+
* so every consumer of `brackets.mode` must read this instead of the
|
|
71
|
+
* Binance-only flag. */
|
|
72
|
+
bracketsAlwaysEnforced = true;
|
|
68
73
|
userStream = null;
|
|
69
74
|
truthCheckTimer = null;
|
|
70
75
|
truthCheckRunning = false;
|
|
71
76
|
_readiness = 'INIT_PENDING';
|
|
72
77
|
openOrdersUnavailableUntil = 0;
|
|
78
|
+
/** MICRO_LIVE per-order notional cap (USD). null = LIVE, uncapped. */
|
|
79
|
+
maxPositionUsd;
|
|
80
|
+
/** Resolved audit-trail wiring (F26). undefined = ingest not configured. */
|
|
81
|
+
fillIngest;
|
|
82
|
+
/** exchangeTime of the newest fill ingested (WS or backfill) — the overlap
|
|
83
|
+
* low-water mark the reconnect gap backfill widens from. */
|
|
84
|
+
lastFillIngestMs = 0;
|
|
73
85
|
/** UTC-midnight Day-P&L anchor (KPI-must-equal-the-HL-app, §5.8). HL has no
|
|
74
86
|
* income endpoint, so the anchor is rebuilt from userFillsByTime + userFunding
|
|
75
87
|
* each balance fetch. Without this the skill self-computes a bogus anchor and
|
|
@@ -81,6 +93,23 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
81
93
|
this.api = new HyperliquidPrivateApi(opts.credentials);
|
|
82
94
|
this.publicApi = new HyperliquidPublicApi({ testnet: opts.credentials.testnet });
|
|
83
95
|
this.slippagePct = clampSlippage(opts.marketSlippagePct ?? DEFAULT_MARKET_SLIPPAGE);
|
|
96
|
+
this.maxPositionUsd =
|
|
97
|
+
opts.mode === 'MICRO_LIVE' ? opts.microLive?.maxPositionUSDT ?? 50 : null;
|
|
98
|
+
if (opts.tradeIngest) {
|
|
99
|
+
if (opts.tradeIngest.exchange) {
|
|
100
|
+
this.fillIngest = {
|
|
101
|
+
client: opts.tradeIngest.client,
|
|
102
|
+
userId: opts.tradeIngest.userId,
|
|
103
|
+
exchange: opts.tradeIngest.exchange,
|
|
104
|
+
};
|
|
105
|
+
logger.info(TAG, 'Per-fill audit-trail ingest wired (WS + reconnect gap backfill)');
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// FILL_EXCHANGE_ID strings are frozen once rows exist — refusing beats
|
|
109
|
+
// minting rows under a guessed exchange id.
|
|
110
|
+
logger.warn(TAG, 'tradeIngest provided WITHOUT an exchange id — fill ingest disabled');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
84
113
|
this.infoCache = new HyperliquidInfoCache(async () => {
|
|
85
114
|
// `meta` is a keyless info read — the public client owns it.
|
|
86
115
|
return this.publicApi.fetchMeta();
|
|
@@ -109,7 +138,8 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
109
138
|
const rules = await this.infoCache.load();
|
|
110
139
|
if (markets && rules && this.infoCache.size > 0) {
|
|
111
140
|
this._readiness = 'READY';
|
|
112
|
-
logger.info(TAG, `HL live adapter READY (${this.infoCache.size} assets, mode=${this.opts.mode}
|
|
141
|
+
logger.info(TAG, `HL live adapter READY (${this.infoCache.size} assets, mode=${this.opts.mode}` +
|
|
142
|
+
`${this.maxPositionUsd != null ? `, micro cap $${this.maxPositionUsd}/order` : ''})`);
|
|
113
143
|
}
|
|
114
144
|
else {
|
|
115
145
|
// DEGRADED, not BLOCKED: reads/exits still work; entries are refused
|
|
@@ -135,13 +165,17 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
135
165
|
walletAddress: this.opts.credentials.walletAddress,
|
|
136
166
|
testnet: this.opts.credentials.testnet,
|
|
137
167
|
callbacks: {
|
|
138
|
-
onFill: (fill) => this.onUserFill(fill),
|
|
168
|
+
onFill: (fill, meta) => this.onUserFill(fill, meta),
|
|
139
169
|
onOrderUpdate: (update) => this.onUserOrderUpdate(update),
|
|
140
170
|
onUserEvent: () => {
|
|
141
171
|
/* liquidation/funding — liquidation fills also arrive via onFill */
|
|
142
172
|
},
|
|
143
173
|
onResyncNeeded: (window) => {
|
|
144
174
|
void this.runTruthCheck(`ws_resync_blind_${Math.round(window.wasDisconnectedMs / 1000)}s`);
|
|
175
|
+
// Fills that landed inside the blind window exist only via REST
|
|
176
|
+
// (T-5: the WS replays nothing) — recover them for the audit
|
|
177
|
+
// trail (F26). Bracket state is healed by the truth-check above.
|
|
178
|
+
void this.backfillFillGap(window.sinceMs).catch((err) => logger.warn(TAG, `Fill gap backfill threw: ${msg(err)}`));
|
|
145
179
|
},
|
|
146
180
|
},
|
|
147
181
|
});
|
|
@@ -183,6 +217,21 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
183
217
|
}
|
|
184
218
|
referencePrice = mark;
|
|
185
219
|
}
|
|
220
|
+
// ---- Micro-live notional cap (audit 2026-07-26 F8) ----
|
|
221
|
+
// Same semantics as Binance's LiveAdapter: clamp NEW exposure to the cap;
|
|
222
|
+
// never touch risk-reducing orders (blocking a close is worse than an
|
|
223
|
+
// uncapped close, and reduce-only cannot increase the position). A
|
|
224
|
+
// reference price is always in hand by this point — market orders fetched
|
|
225
|
+
// the mark above, limit orders carry their own price.
|
|
226
|
+
let effectiveAmount = amount;
|
|
227
|
+
if (this.maxPositionUsd != null && !options?.reduceOnly) {
|
|
228
|
+
const maxAmount = this.maxPositionUsd / referencePrice;
|
|
229
|
+
if (effectiveAmount > maxAmount) {
|
|
230
|
+
logger.info(TAG, `Micro-live cap: reducing ${symbol} amount ${effectiveAmount} → ${maxAmount} ` +
|
|
231
|
+
`(max $${this.maxPositionUsd} notional at ref ${referencePrice})`);
|
|
232
|
+
effectiveAmount = maxAmount;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
186
235
|
const submitPrice = type === 'market'
|
|
187
236
|
? this.infoCache.marketPrice({
|
|
188
237
|
symbol,
|
|
@@ -193,7 +242,7 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
193
242
|
: this.infoCache.roundPrice(symbol, referencePrice, side === 'buy' ? 'down' : 'up');
|
|
194
243
|
const check = this.infoCache.validate({
|
|
195
244
|
symbol,
|
|
196
|
-
amount,
|
|
245
|
+
amount: effectiveAmount,
|
|
197
246
|
side,
|
|
198
247
|
price: type === 'limit' ? submitPrice : undefined,
|
|
199
248
|
referencePrice,
|
|
@@ -303,6 +352,13 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
303
352
|
const ledger = this.getHlBracketCoordinator().getLedger();
|
|
304
353
|
const existing = ledger.getBySymbol(symbol);
|
|
305
354
|
if (existing && !isTerminalBracketState(existing.state) && existing.state !== 'pending_entry') {
|
|
355
|
+
// ★ F5: record THIS order's cloid against the live row FIRST. A scale-in
|
|
356
|
+
// submitted as a resting limit fills later, and `onUserFill` matches
|
|
357
|
+
// fills to rows by cid — without this its fill matched nothing, so no
|
|
358
|
+
// resize ever ran and the added contracts stayed NAKED (T-2: legs are
|
|
359
|
+
// fixed size) until the 60s truth-check sweep. Registered before the
|
|
360
|
+
// resize below so a fast fill can never race ahead of the bookkeeping.
|
|
361
|
+
this.getHlBracketCoordinator().registerAdditionalEntryCid(symbol, entryCloid);
|
|
306
362
|
if (isFilled) {
|
|
307
363
|
void this.resizeAfterScaleInAsync(symbol);
|
|
308
364
|
}
|
|
@@ -709,14 +765,67 @@ export class HyperliquidLiveAdapter extends EventEmitter {
|
|
|
709
765
|
return cancelled;
|
|
710
766
|
}
|
|
711
767
|
// ---- User-stream handlers (issue #209 wiring) ----
|
|
768
|
+
/** Audit-trail ingest (F26): fire-and-forget POST of one fill. The client
|
|
769
|
+
* never blocks the WS hot path; the server-side (exchange, trade id) upsert
|
|
770
|
+
* makes WS/backfill double-delivery a no-op. */
|
|
771
|
+
ingestFill(fill, source) {
|
|
772
|
+
if (!this.fillIngest)
|
|
773
|
+
return false;
|
|
774
|
+
const event = hlFillToFillEvent(fill, this.fillIngest, source);
|
|
775
|
+
if (!event)
|
|
776
|
+
return false;
|
|
777
|
+
this.fillIngest.client.post(this.fillIngest.userId, event);
|
|
778
|
+
if (event.exchangeTime > this.lastFillIngestMs)
|
|
779
|
+
this.lastFillIngestMs = event.exchangeTime;
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
/** Reconnect gap backfill (F26): the WS replays NOTHING (T-5), so fills that
|
|
783
|
+
* landed while the socket was down exist ONLY via REST. Over-fetch with a
|
|
784
|
+
* 60s overlap is harmless (idempotent upsert); under-fetch loses audit rows. */
|
|
785
|
+
async backfillFillGap(sinceMs) {
|
|
786
|
+
if (!this.fillIngest)
|
|
787
|
+
return;
|
|
788
|
+
const floor = Date.now() - 24 * 3600_000; // never sweep more than a day
|
|
789
|
+
const from = Math.max(floor, Math.min(sinceMs, this.lastFillIngestMs > 0 ? this.lastFillIngestMs : Number.POSITIVE_INFINITY) - 60_000);
|
|
790
|
+
const fills = await this.api.fetchFillsSince(from);
|
|
791
|
+
if (fills === null) {
|
|
792
|
+
// null ≠ empty: the fetch FAILED — the gap stays open, the next resync
|
|
793
|
+
// (or the periodic truth-check path) retries. Never treat as "no fills".
|
|
794
|
+
logger.warn(TAG, `Fill gap backfill fetch FAILED (since=${new Date(from).toISOString()}) — retried on next resync`);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
let posted = 0;
|
|
798
|
+
for (const f of fills) {
|
|
799
|
+
if (this.ingestFill(f, 'rest_reconcile'))
|
|
800
|
+
posted++;
|
|
801
|
+
}
|
|
802
|
+
if (fills.length > 0) {
|
|
803
|
+
logger.info(TAG, `Fill gap backfill: posted ${posted}/${fills.length} fill(s) since ${new Date(from).toISOString()}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
712
806
|
/** Entry fills drive attach (resting limits) / resize (partial-fill growth).
|
|
713
807
|
* `startPosition` is the position BEFORE this fill — the WS-authoritative
|
|
714
808
|
* way to know the after-fill total without an extra REST read. */
|
|
715
|
-
onUserFill(fill) {
|
|
809
|
+
onUserFill(fill, meta) {
|
|
810
|
+
// Audit trail first — bracket bookkeeping below must not gate the record.
|
|
811
|
+
// Snapshot fills ARE ingested on purpose: the ledger is idempotent on
|
|
812
|
+
// (exchange, exchange_trade_id), so replayed history upserts harmlessly and
|
|
813
|
+
// backfills rows we'd otherwise miss (F26).
|
|
814
|
+
this.ingestFill(fill, 'ws');
|
|
815
|
+
// ★ F44: but bracket bookkeeping is a STATE MUTATION, not an idempotent
|
|
816
|
+
// write. Every (re)subscribe ships a snapshot of recent fills, and acting on
|
|
817
|
+
// one recomputes the position as that old fill's `startPosition + sz` — a
|
|
818
|
+
// stale total — then resizes the LIVE protective legs down to it. Observed
|
|
819
|
+
// on the rig 2026-07-27: ETH legs cut 0.027 → 0.0231 thirteen seconds after
|
|
820
|
+
// a user-stream connect, leaving 0.0039 unprotected until the 60s sweep
|
|
821
|
+
// healed it, in a repeating flap. Exchange truth is the resync's job.
|
|
822
|
+
if (meta?.isSnapshot)
|
|
823
|
+
return;
|
|
716
824
|
try {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
825
|
+
// Matches the primary entry cid OR any additional cid recorded for a
|
|
826
|
+
// scale-in / second resting entry (F5) — matching on `entryCid` alone
|
|
827
|
+
// silently dropped those fills.
|
|
828
|
+
const row = this.getHlBracketCoordinator().findRowByEntryCid(fill.cloid);
|
|
720
829
|
if (!row)
|
|
721
830
|
return;
|
|
722
831
|
const sz = Math.abs(Number(fill.sz ?? 0));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CcxtTicker, CcxtOHLCV } from '../../types.js';
|
|
2
2
|
import type { OrderBookDepth } from '../../simulator/types.js';
|
|
3
3
|
import type { PublicMarketDataApi } from '../../ccxt/public-market-data-api.js';
|
|
4
|
+
import { type VenueReachabilityResult } from '@reefclaw/shared';
|
|
4
5
|
export interface HyperliquidPublicApiOptions {
|
|
5
6
|
testnet?: boolean;
|
|
6
7
|
/** Test seam — injected ccxt exchange instance. */
|
|
@@ -60,11 +61,17 @@ export declare class HyperliquidPublicApi implements PublicMarketDataApi {
|
|
|
60
61
|
* (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
|
|
61
62
|
* as the clock-drift source. Geo classification is best-effort — HL's
|
|
62
63
|
* API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
|
|
63
|
-
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
65
|
+
*
|
|
66
|
+
* ★ Self-stall detection (issue #265): the failure path is wall-clock timed.
|
|
67
|
+
* A throw is only evidence about the VENUE if our own abort timer fired
|
|
68
|
+
* roughly when it was set for. On a starved host the loop stops running —
|
|
69
|
+
* observed live on the HL rig: this 10s timer landed 86s late while the
|
|
70
|
+
* agent was trading on HL perfectly well — and blaming the network then
|
|
71
|
+
* produces a confident, wrong "check DNS/firewall/region" banner. Past
|
|
72
|
+
* STALL_FACTOR× the budget we report `stalled`, which the reporter renders
|
|
73
|
+
* as `unknown`: we genuinely did not learn whether HL is reachable. */
|
|
74
|
+
probeReachability(): Promise<VenueReachabilityResult>;
|
|
68
75
|
/** `meta` — the asset universe (szDecimals, maxLeverage, positional assetIndex).
|
|
69
76
|
* Keyless info read, weight 20. Feeds HyperliquidInfoCache. Returns null on any
|
|
70
77
|
* failure (never a partial universe — a half-loaded rules table would silently
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// on this venue — logged clearly, null returned (never silently translated).
|
|
33
33
|
import { createRequire } from 'node:module';
|
|
34
34
|
import { logger } from '../../logger.js';
|
|
35
|
+
import { REACHABILITY_STALL_FACTOR } from '@reefclaw/shared';
|
|
35
36
|
import { toCcxtSymbol, toHyperliquidCoin } from '../symbols.js';
|
|
36
37
|
const TAG = 'hl-public';
|
|
37
38
|
// Load ccxt via CJS require — OpenClaw's ESM loader gives wrong module shape
|
|
@@ -55,6 +56,10 @@ const DEFAULT_TICKER_TTL_MS = 4_000;
|
|
|
55
56
|
* snapshot's ccxt call re-runs the whole fetchMarkets pipeline (weight ~60,
|
|
56
57
|
* ~13s on a slow host) and nothing price-critical reads these fields. */
|
|
57
58
|
const FULL_SNAPSHOT_REFRESH_MS = 300_000;
|
|
59
|
+
/** Reachability-probe request budget. Also the yardstick for self-stall
|
|
60
|
+
* detection — see probeReachability. */
|
|
61
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
62
|
+
const STALL_FACTOR = REACHABILITY_STALL_FACTOR;
|
|
58
63
|
function resolveTickerTtlMs() {
|
|
59
64
|
const raw = Number(process.env.RC_HL_TICKER_TTL_MS);
|
|
60
65
|
if (!Number.isFinite(raw) || raw < 500)
|
|
@@ -343,10 +348,20 @@ export class HyperliquidPublicApi {
|
|
|
343
348
|
* (verified live 2026-07-11: `{"specialStatuses":null,"time":…}`) doubles
|
|
344
349
|
* as the clock-drift source. Geo classification is best-effort — HL's
|
|
345
350
|
* API-level geo behavior is UNVERIFIED (plan §3.9); a 403/451 maps to
|
|
346
|
-
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
351
|
+
* geo_blocked, anything else non-2xx/network maps to unreachable.
|
|
352
|
+
*
|
|
353
|
+
* ★ Self-stall detection (issue #265): the failure path is wall-clock timed.
|
|
354
|
+
* A throw is only evidence about the VENUE if our own abort timer fired
|
|
355
|
+
* roughly when it was set for. On a starved host the loop stops running —
|
|
356
|
+
* observed live on the HL rig: this 10s timer landed 86s late while the
|
|
357
|
+
* agent was trading on HL perfectly well — and blaming the network then
|
|
358
|
+
* produces a confident, wrong "check DNS/firewall/region" banner. Past
|
|
359
|
+
* STALL_FACTOR× the budget we report `stalled`, which the reporter renders
|
|
360
|
+
* as `unknown`: we genuinely did not learn whether HL is reachable. */
|
|
347
361
|
async probeReachability() {
|
|
348
362
|
const ac = new AbortController();
|
|
349
|
-
const tid = setTimeout(() => ac.abort(),
|
|
363
|
+
const tid = setTimeout(() => ac.abort(), PROBE_TIMEOUT_MS);
|
|
364
|
+
const startedAt = Date.now();
|
|
350
365
|
try {
|
|
351
366
|
const res = await this.fetchImpl(`${this.baseUrl()}/info`, {
|
|
352
367
|
method: 'POST',
|
|
@@ -368,7 +383,13 @@ export class HyperliquidPublicApi {
|
|
|
368
383
|
return { outcome: 'reachable', driftMs };
|
|
369
384
|
}
|
|
370
385
|
catch (err) {
|
|
371
|
-
|
|
386
|
+
const elapsedMs = Date.now() - startedAt;
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
if (elapsedMs > PROBE_TIMEOUT_MS * STALL_FACTOR) {
|
|
389
|
+
logger.warn(TAG, `probeReachability inconclusive: this process was starved — a ${PROBE_TIMEOUT_MS}ms probe took ${Math.round(elapsedMs / 1000)}s (${msg}). Reporting reachability as unknown, NOT as a Hyperliquid failure.`);
|
|
390
|
+
return { outcome: 'stalled', driftMs: null, stallMs: elapsedMs };
|
|
391
|
+
}
|
|
392
|
+
logger.warn(TAG, `probeReachability failed: ${msg}`);
|
|
372
393
|
return { outcome: 'unreachable', driftMs: null };
|
|
373
394
|
}
|
|
374
395
|
finally {
|
|
@@ -41,7 +41,19 @@ export interface HlOrderUpdateEvent {
|
|
|
41
41
|
statusTimestamp: number;
|
|
42
42
|
}
|
|
43
43
|
export interface HlUserStreamCallbacks {
|
|
44
|
-
|
|
44
|
+
/** ★ `meta.isSnapshot` marks a fill from the subscription SNAPSHOT — the batch
|
|
45
|
+
* of recent history HL sends on every (re)subscribe — not a live execution.
|
|
46
|
+
*
|
|
47
|
+
* Do NOT confuse this with the T-5 gap rule below: HL not replaying the fills
|
|
48
|
+
* you MISSED while disconnected is true and unrelated. It still ships a
|
|
49
|
+
* snapshot of recent history on subscribe, and consumers must tell them apart.
|
|
50
|
+
* Idempotent consumers (the audit-trail ingest, keyed on exchange trade id)
|
|
51
|
+
* should take snapshot fills; state MUTATORS (bracket resize/attach) must not
|
|
52
|
+
* — replaying an old fill's `startPosition + sz` resizes live protective legs
|
|
53
|
+
* to a stale historical size (audit 2026-07-27 F44). */
|
|
54
|
+
onFill: (fill: HlFillEvent, meta: {
|
|
55
|
+
isSnapshot: boolean;
|
|
56
|
+
}) => void;
|
|
45
57
|
onOrderUpdate: (update: HlOrderUpdateEvent) => void;
|
|
46
58
|
/** userEvents: liquidation / funding / non-user-cancel — the close-bypass feed. */
|
|
47
59
|
onUserEvent: (event: Record<string, unknown>) => void;
|
|
@@ -137,9 +137,12 @@ export class HyperliquidUserStream {
|
|
|
137
137
|
switch (channel) {
|
|
138
138
|
case 'userFills': {
|
|
139
139
|
const payload = data;
|
|
140
|
+
// `isSnapshot` was typed here from the start but never read — so every
|
|
141
|
+
// (re)connect replayed recent history as if it were live (F44).
|
|
142
|
+
const isSnapshot = payload?.isSnapshot === true;
|
|
140
143
|
for (const fill of payload?.fills ?? []) {
|
|
141
144
|
this.lastEventAt = Math.max(this.lastEventAt, fill.time ?? Date.now());
|
|
142
|
-
this.opts.callbacks.onFill(fill);
|
|
145
|
+
this.opts.callbacks.onFill(fill, { isSnapshot });
|
|
143
146
|
}
|
|
144
147
|
break;
|
|
145
148
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Venue registry — the single seam where a trading venue's LIVE adapter is
|
|
2
|
-
// constructed (
|
|
2
|
+
// constructed (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.1/§7.1).
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// boot path never grows a
|
|
4
|
+
// This build trades live on BOTH supported venues: Binance (LiveAdapter) and
|
|
5
|
+
// Hyperliquid (HyperliquidLiveAdapter, Phase 3 — brackets always enforced by
|
|
6
|
+
// HlBracketCoordinator). A venue with missing/invalid credentials falls back
|
|
7
|
+
// to PAPER at boot (never by crashing register() — OpenClaw treats a throwing
|
|
8
|
+
// register as "ignored" and the agent silently loses every tool). Any future
|
|
9
|
+
// venue adds its arm HERE and nowhere else, so the boot path never grows a
|
|
10
|
+
// second venue branch.
|
|
10
11
|
//
|
|
11
12
|
// Venue is LOCAL mechanism (TOOL_DISTRIBUTION_ARCHITECTURE.md §2 decision
|
|
12
13
|
// rule: it holds keys + is part of the safety floor) — it is read from
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import type { IExchangeAdapter } from '../exchange-adapter.js';
|
|
2
2
|
/** One catalog toggle represents both frozen Wave 9 strategy legs. */
|
|
3
3
|
export declare const WAVE9_BUNDLE_SETUP_TYPE = "wave9_28d_momentum_reversal";
|
|
4
|
+
/** TOOL-level close reason: what close_position args + beginExit redemption
|
|
5
|
+
* requests must carry. The `wave9_` namespace keeps it from colliding with
|
|
6
|
+
* generic CloseReason values. */
|
|
4
7
|
export declare const WAVE9_MECHANICAL_EXIT_REASON = "wave9_signal_reversal";
|
|
8
|
+
/** DECISION-level exit cause: what the authorization decision
|
|
9
|
+
* (Wave9PaperExitTokenDecision.reason) records at issuance. Deliberately a
|
|
10
|
+
* DIFFERENT string from WAVE9_MECHANICAL_EXIT_REASON — the decision names
|
|
11
|
+
* WHY the strategy exits (its only exit cause is a signal reversal), the
|
|
12
|
+
* tool reason names WHICH namespaced close path redeems it. issueExitBatch
|
|
13
|
+
* validates this constant; beginExit validates the tool constant. Two fields
|
|
14
|
+
* on two layers, each checked against its own value — not an asymmetry. */
|
|
15
|
+
export declare const WAVE9_EXIT_DECISION_REASON = "signal_reversal";
|
|
5
16
|
export type Wave9ExecutionMode = 'PAPER' | 'LIVE';
|
|
6
17
|
/** Conservative shared identity check for current, legacy, or Wave 9-mission positions. */
|
|
7
18
|
export declare function isWave9ManagedPosition(position: {
|
|
@@ -93,7 +104,7 @@ export interface Wave9PaperExitTokenDecision {
|
|
|
93
104
|
missionId: string;
|
|
94
105
|
symbol: string;
|
|
95
106
|
positionSide: 'long' | 'short';
|
|
96
|
-
reason:
|
|
107
|
+
reason: typeof WAVE9_EXIT_DECISION_REASON;
|
|
97
108
|
notBeforeMs: number;
|
|
98
109
|
deadlineMs: number;
|
|
99
110
|
positionFingerprint: string;
|
|
@@ -2,7 +2,18 @@ import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { WAVE9_SYMBOL_PRIORITY } from '../portfolio/wave9-policy.js';
|
|
3
3
|
/** One catalog toggle represents both frozen Wave 9 strategy legs. */
|
|
4
4
|
export const WAVE9_BUNDLE_SETUP_TYPE = 'wave9_28d_momentum_reversal';
|
|
5
|
+
/** TOOL-level close reason: what close_position args + beginExit redemption
|
|
6
|
+
* requests must carry. The `wave9_` namespace keeps it from colliding with
|
|
7
|
+
* generic CloseReason values. */
|
|
5
8
|
export const WAVE9_MECHANICAL_EXIT_REASON = 'wave9_signal_reversal';
|
|
9
|
+
/** DECISION-level exit cause: what the authorization decision
|
|
10
|
+
* (Wave9PaperExitTokenDecision.reason) records at issuance. Deliberately a
|
|
11
|
+
* DIFFERENT string from WAVE9_MECHANICAL_EXIT_REASON — the decision names
|
|
12
|
+
* WHY the strategy exits (its only exit cause is a signal reversal), the
|
|
13
|
+
* tool reason names WHICH namespaced close path redeems it. issueExitBatch
|
|
14
|
+
* validates this constant; beginExit validates the tool constant. Two fields
|
|
15
|
+
* on two layers, each checked against its own value — not an asymmetry. */
|
|
16
|
+
export const WAVE9_EXIT_DECISION_REASON = 'signal_reversal';
|
|
6
17
|
/** Conservative shared identity check for current, legacy, or Wave 9-mission positions. */
|
|
7
18
|
export function isWave9ManagedPosition(position) {
|
|
8
19
|
return position.setupType === WAVE9_BUNDLE_SETUP_TYPE
|
|
@@ -615,7 +626,7 @@ export class Wave9PaperAdmissionGuard {
|
|
|
615
626
|
exactText(decision.candidateId, 'exit candidateId');
|
|
616
627
|
exactText(decision.missionId, 'missionId');
|
|
617
628
|
requireExecutionMode(decision.tradingMode);
|
|
618
|
-
if (decision.reason !==
|
|
629
|
+
if (decision.reason !== WAVE9_EXIT_DECISION_REASON)
|
|
619
630
|
throw new Wave9PaperAdmissionGuardError('Wave 9 exit reason must be signal_reversal');
|
|
620
631
|
if (!WAVE9_SYMBOL_PRIORITY.includes(decision.symbol)) {
|
|
621
632
|
throw new Wave9PaperAdmissionGuardError(`unsupported Wave 9 symbol ${decision.symbol}`);
|
package/assets/shared/index.d.ts
CHANGED
|
@@ -9,6 +9,6 @@ export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js
|
|
|
9
9
|
export type { Direction, OhlcvBar, TradeFlowBucket, GlobalMarketContext, MarketContext, SignalCondition, StrategyEvaluation, StrategyDefinition, SignalEvent, StrategyState, SignalSnapshot, } from './signals/types.js';
|
|
10
10
|
export type { ConditionResult, ConditionContext, ConditionFn, ConditionConfig, EntryRuleConfig, StopRuleConfig, DirectionRule, PrimaryTimeframe, StrategyConfig, } from './signals/conditions/types.js';
|
|
11
11
|
export type { VenueId } from './venues/symbols.js';
|
|
12
|
-
export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
|
|
13
|
-
export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, } from './readiness.js';
|
|
14
|
-
export { READINESS_CHECK_COPY, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
|
12
|
+
export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, VENUE_DEFAULT_SYMBOL, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
|
|
13
|
+
export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, VenueReachabilityOutcome, VenueReachabilityResult, } from './readiness.js';
|
|
14
|
+
export { READINESS_CHECK_COPY, GEO_BLOCK_FIX_HINT, HOST_STALL_FIX_HINT, REACHABILITY_STALL_FACTOR, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
package/assets/shared/index.js
CHANGED
|
@@ -3,5 +3,9 @@ export { VALID_CHANNELS, VALID_EMERGENCY_ACTIONS } from './protocol.js';
|
|
|
3
3
|
export { logger, setLogLevel, formatError } from './logger.js';
|
|
4
4
|
export { VALID_TRADING_MODES, isTradingMode, validateModeTransition, modeRequiresCredentials, } from './trading-mode.js';
|
|
5
5
|
export { redactTokens, redactTokensInPayload, REDACTED_TOKEN } from './redact.js';
|
|
6
|
-
export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
|
|
7
|
-
|
|
6
|
+
export { VENUE_IDS, isVenueId, parseVenue, FILL_EXCHANGE_ID, fillExchangeId, VENUE_QUOTE_ASSET, VENUE_DEFAULT_SYMBOL, HL_INTEL_PREFIX, toCcxtSymbol, toIntelSymbol, fromIntelSymbol, toHyperliquidCoin, } from './venues/symbols.js';
|
|
7
|
+
// ★ VALUES, not types — a symbol missing from this list type-checks fine at the
|
|
8
|
+
// import site and is `undefined` at runtime (the shared-registry drift class).
|
|
9
|
+
// Adding a const to readiness.ts is only half the job; it must be re-exported
|
|
10
|
+
// here AND the shared dist rebuilt before the plugin can see it.
|
|
11
|
+
export { READINESS_CHECK_COPY, GEO_BLOCK_FIX_HINT, HOST_STALL_FIX_HINT, REACHABILITY_STALL_FACTOR, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
|
|
@@ -2,7 +2,30 @@ export type ReadinessStatus = 'pass' | 'warn' | 'fail' | 'unknown';
|
|
|
2
2
|
/** Which lifecycle phase a check belongs to. `connect` runs with NO Binance API
|
|
3
3
|
* keys (covers paper trading too); `golive` needs keys (Phase 2). */
|
|
4
4
|
export type ReadinessPhase = 'connect' | 'golive';
|
|
5
|
-
export type ReadinessCheckId = 'binance_reachable' | 'hyperliquid_reachable' | 'clock_in_sync' | 'plugin_loaded' | 'tools_registered';
|
|
5
|
+
export type ReadinessCheckId = 'binance_reachable' | 'hyperliquid_reachable' | 'clock_in_sync' | 'plugin_loaded' | 'tools_registered' | 'host_responsive';
|
|
6
|
+
/** What a venue reachability probe concluded.
|
|
7
|
+
*
|
|
8
|
+
* ★ `stalled` is the one that is NOT about the venue: it means the probe's own
|
|
9
|
+
* abort timer fired far later than it was set for, which can only happen when
|
|
10
|
+
* this process was starved of CPU — so the request never got a fair chance and
|
|
11
|
+
* we learned NOTHING about the venue. Before it existed, a frozen host fell
|
|
12
|
+
* into the same catch as a real network failure and the dashboard confidently
|
|
13
|
+
* told the operator to check their DNS/firewall/region (issue #265).
|
|
14
|
+
* `unknown` = we deliberately didn't probe (ban/weight gate paused us). */
|
|
15
|
+
export type VenueReachabilityOutcome = 'reachable' | 'geo_blocked' | 'unreachable' | 'stalled' | 'unknown';
|
|
16
|
+
export interface VenueReachabilityResult {
|
|
17
|
+
outcome: VenueReachabilityOutcome;
|
|
18
|
+
driftMs: number | null;
|
|
19
|
+
/** Wall-clock the probe actually took, set only when `outcome === 'stalled'`
|
|
20
|
+
* — it IS the evidence (a 10s timer that returned after 86s). */
|
|
21
|
+
stallMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/** A failed probe is read as a venue failure only while it finished within
|
|
24
|
+
* this multiple of its own timeout budget; past it, the clock says the
|
|
25
|
+
* process — not the network — is what broke. 2× leaves generous room for
|
|
26
|
+
* ordinary scheduling jitter and TLS/DNS slack while sitting far below the
|
|
27
|
+
* 8×-and-worse overshoots a real freeze produces. */
|
|
28
|
+
export declare const REACHABILITY_STALL_FACTOR = 2;
|
|
6
29
|
export interface ReadinessCheck {
|
|
7
30
|
/** Stable machine id. Widened to string so the webapp can render checks from a
|
|
8
31
|
* newer plugin it doesn't have the union for. */
|
|
@@ -42,11 +65,31 @@ interface CheckCopy {
|
|
|
42
65
|
/** The canonical connect-phase checks + their plain-English fixes. Single source
|
|
43
66
|
* of copy; the plugin inlines these into each report so the webapp stays dumb. */
|
|
44
67
|
export declare const READINESS_CHECK_COPY: Record<ReadinessCheckId, CheckCopy>;
|
|
68
|
+
/** Assertive copy for a stall we COULD attribute, passed explicitly as the
|
|
69
|
+
* `fixHint` override — the same "only name a cause you measured" rule that
|
|
70
|
+
* governs GEO_BLOCK_FIX_HINT. The discriminator is the kernel's own runqueue
|
|
71
|
+
* wait for this process (/proc/self/schedstat): time spent runnable but denied
|
|
72
|
+
* a CPU. Large ⇒ the host really is the problem; ~0 across a multi-second
|
|
73
|
+
* freeze ⇒ the process blocked itself and no amount of extra hardware helps. */
|
|
74
|
+
export declare const HOST_STALL_FIX_HINT: Record<'host_starved' | 'self_inflicted', string>;
|
|
75
|
+
/** Assertive copy for a CONFIRMED geo-block, passed explicitly as the `fixHint`
|
|
76
|
+
* override. This is the one case where naming the cause is honest: the server
|
|
77
|
+
* answered 451/403, so we are reporting what it told us — not an inference from
|
|
78
|
+
* a request that never completed. */
|
|
79
|
+
export declare const GEO_BLOCK_FIX_HINT: Record<'binance' | 'hyperliquid', string>;
|
|
45
80
|
/** Build a self-describing check, pulling label/phase/fixHint from the copy map.
|
|
46
|
-
* fixHint is attached only when there is something to fix (warn/fail).
|
|
81
|
+
* fixHint is attached only when there is something to fix (warn/fail).
|
|
82
|
+
*
|
|
83
|
+
* `fixHint` in opts OVERRIDES the copy-map default. The static copy asserts one
|
|
84
|
+
* cause per check id, which is right for a definitive signal (a 451 IS a geo
|
|
85
|
+
* block) but wrong when the same id fires on an inference: a reachability warn
|
|
86
|
+
* sent the operator hunting a firewall that was never the problem (issue #265).
|
|
87
|
+
* Callers that know the status was inferred pass a hint that describes the
|
|
88
|
+
* possibilities instead of asserting one. */
|
|
47
89
|
export declare function makeReadinessCheck(id: ReadinessCheckId, status: ReadinessStatus, opts?: {
|
|
48
90
|
detail?: string;
|
|
49
91
|
checkedAt?: number;
|
|
92
|
+
fixHint?: string;
|
|
50
93
|
}): ReadinessCheck;
|
|
51
94
|
/** Worst status across the checks: fail > warn > unknown > pass. An all-unknown
|
|
52
95
|
* (or empty) set is 'unknown', so a report that couldn't run anything doesn't
|