@reefclaw/connect 0.1.30 → 0.1.32
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.js +72 -5
- package/assets/bridge/connector.d.ts +3 -1
- package/assets/bridge/connector.js +51 -4
- package/assets/bridge/gateway/heartbeat-cron.js +31 -7
- package/assets/bridge/gateway/poller.d.ts +5 -0
- package/assets/bridge/gateway/poller.js +9 -0
- package/assets/bridge/index.js +4 -0
- package/assets/bridge/provider.d.ts +15 -0
- package/assets/bridge/providers/connector-update.d.ts +89 -0
- package/assets/bridge/providers/connector-update.js +212 -0
- package/assets/bridge/providers/emergency-commands.d.ts +36 -0
- package/assets/bridge/providers/emergency-commands.js +91 -0
- package/assets/bridge/providers/gateway.d.ts +26 -1
- package/assets/bridge/providers/gateway.js +159 -8
- package/assets/bridge/providers/mock.js +1 -0
- package/assets/bridge/types.d.ts +5 -1
- package/assets/bridge/types.js +5 -0
- package/assets/bridge/utils/instance-id.d.ts +3 -0
- package/assets/bridge/utils/instance-id.js +48 -0
- package/assets/plugin/ccxt/binance-private.js +2 -1
- package/assets/plugin/ccxt/binance-public.js +6 -1
- package/assets/plugin/config/agent-config-client.d.ts +5 -2
- package/assets/plugin/config/agent-config-client.js +13 -0
- package/assets/plugin/config/agent-config-poller.js +5 -1
- package/assets/plugin/config/gate-store.d.ts +9 -0
- package/assets/plugin/config/gate-store.js +17 -2
- package/assets/plugin/config/plugin-config-io.js +24 -2
- package/assets/plugin/http/keepalive-fetch.d.ts +5 -0
- package/assets/plugin/http/keepalive-fetch.js +50 -0
- package/assets/plugin/index.js +53 -6
- package/assets/plugin/ingest/position-auto-capture.js +14 -2
- package/assets/plugin/ingest/position-decisions-client.d.ts +6 -0
- package/assets/plugin/ingest/position-decisions-client.js +27 -9
- package/assets/plugin/live/approval-lifecycle.d.ts +10 -0
- package/assets/plugin/live/approval-lifecycle.js +16 -2
- package/assets/plugin/live/microstructure-assembler.js +11 -2
- package/assets/plugin/live/proposal-decision-listener.d.ts +21 -0
- package/assets/plugin/live/proposal-decision-listener.js +39 -0
- package/assets/plugin/live/proposal-manager.d.ts +12 -0
- package/assets/plugin/live/proposal-manager.js +47 -0
- package/assets/plugin/live/stop-watcher.d.ts +16 -1
- package/assets/plugin/live/stop-watcher.js +48 -8
- package/assets/plugin/onboarding/runtime.js +4 -0
- package/assets/plugin/openclaw.plugin.json +1 -1
- package/assets/plugin/persistence/state-manager.d.ts +7 -0
- package/assets/plugin/persistence/state-manager.js +28 -1
- package/assets/plugin/simulator/exchange-simulator.d.ts +27 -1
- package/assets/plugin/simulator/exchange-simulator.js +98 -38
- package/assets/plugin/tools/audit-bracket-protection.js +11 -7
- package/assets/plugin/tools/close-position.js +10 -1
- package/assets/plugin/tools/create-order.js +49 -7
- package/assets/plugin/tools/get-funding-context.js +6 -1
- package/assets/plugin/tools/get-liquidation-levels.js +5 -1
- package/assets/plugin/tools/get-liquidation-pulse.js +7 -1
- package/assets/plugin/tools/get-market-intel.js +2 -1
- package/assets/plugin/tools/get-relevant-learnings.js +20 -1
- package/assets/plugin/tools/get-resting-liquidity.js +6 -1
- package/assets/plugin/tools/get-wave9-status.js +17 -0
- package/assets/plugin/tools/intel-api.d.ts +9 -0
- package/assets/plugin/tools/intel-api.js +32 -1
- package/assets/plugin/tools/record-position-reviews.js +2 -2
- package/assets/plugin/tools/scan-pairs.js +20 -11
- package/assets/plugin/types.d.ts +7 -0
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +27 -2
- package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +101 -13
- package/assets/shared/readiness.js +5 -1
- package/dist/cli.js +27 -6
- package/dist/openclaw.js +1 -1
- package/dist/validate.js +46 -9
- package/package.json +33 -32
|
@@ -12,6 +12,34 @@ import { fillMarketOrder, fillLimitOrder, parseSymbol } from './fill-engine.js';
|
|
|
12
12
|
import { updateMfe } from '../mfe.js';
|
|
13
13
|
import { computeInvalidationHit } from '../pinned-plan.js';
|
|
14
14
|
const TAG = 'simulator';
|
|
15
|
+
/** Canonical key for the per-symbol quote/orderbook caches and for matching a
|
|
16
|
+
* ticker against stored positions/orders.
|
|
17
|
+
*
|
|
18
|
+
* CCXT echoes the VENUE-UNIFIED symbol back from fetchTicker: ask Binance USDM
|
|
19
|
+
* for 'ETH/USDT' and the ticker returns as 'ETH/USDT:USDT'. Positions and
|
|
20
|
+
* orders, however, are stored under whatever form the caller opened them with,
|
|
21
|
+
* so `lastTicker.get(position.symbol)` silently missed for every position held
|
|
22
|
+
* in the un-suffixed form. The consequences were all silent:
|
|
23
|
+
* - getPositions()/computeEquity() fell back to entryPrice, so the mark was
|
|
24
|
+
* FROZEN at entry — the stop-watcher compared a frozen mark, never saw a
|
|
25
|
+
* breach, and the position ran unprotected past its stop indefinitely;
|
|
26
|
+
* - createOrder() threw 'No ticker data for X' on the market leg, so the
|
|
27
|
+
* agent could not close by hand either (both automatic and manual exits
|
|
28
|
+
* were dead at once);
|
|
29
|
+
* - MFE, take-profit legs and resting-limit fills never advanced.
|
|
30
|
+
* Keying both sides through this normalizer makes the caches format-agnostic,
|
|
31
|
+
* which also repairs books already persisted with a mix of both forms.
|
|
32
|
+
*
|
|
33
|
+
* Same regex as webapp/src/lib/symbols.ts normalizeSymbol and
|
|
34
|
+
* plugin/src/venues/symbols.ts stripSettleSuffix — kept inline so the
|
|
35
|
+
* simulator keeps its zero-import-for-hot-path shape. */
|
|
36
|
+
export function tickerKey(symbol) {
|
|
37
|
+
return symbol.replace(/:[A-Z]+$/, '');
|
|
38
|
+
}
|
|
39
|
+
/** True when two symbols denote the same market regardless of settle suffix. */
|
|
40
|
+
function sameMarket(a, b) {
|
|
41
|
+
return tickerKey(a) === tickerKey(b);
|
|
42
|
+
}
|
|
15
43
|
export class ExchangeSimulator extends EventEmitter {
|
|
16
44
|
state;
|
|
17
45
|
lastTicker = new Map();
|
|
@@ -112,7 +140,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
112
140
|
const walletTotal = this.state.wallet[quote]?.total ?? 0;
|
|
113
141
|
let positionEquity = 0;
|
|
114
142
|
for (const pos of this.state.positions) {
|
|
115
|
-
const ticker = this.lastTicker.get(pos.symbol);
|
|
143
|
+
const ticker = this.lastTicker.get(tickerKey(pos.symbol));
|
|
116
144
|
const mark = ticker?.last ?? pos.entryPrice;
|
|
117
145
|
const entryNotional = pos.entryPrice * pos.quantity;
|
|
118
146
|
const unrealized = pos.side === 'long'
|
|
@@ -182,10 +210,16 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
182
210
|
}
|
|
183
211
|
getPositions(symbol) {
|
|
184
212
|
const positions = symbol
|
|
185
|
-
? this.state.positions.filter(p => p.symbol
|
|
213
|
+
? this.state.positions.filter(p => sameMarket(p.symbol, symbol))
|
|
186
214
|
: this.state.positions;
|
|
187
215
|
return positions.map(p => {
|
|
188
|
-
const ticker = this.lastTicker.get(p.symbol);
|
|
216
|
+
const ticker = this.lastTicker.get(tickerKey(p.symbol));
|
|
217
|
+
// No quote for this symbol means we CANNOT mark this position. Falling
|
|
218
|
+
// back to entryPrice fabricates "flat since entry", which reads as a
|
|
219
|
+
// perfectly healthy position to every consumer — that is how a stop
|
|
220
|
+
// breach stayed invisible for 35h. Keep the fallback (callers need a
|
|
221
|
+
// number) but flag it so the stop-watcher can alarm instead of skipping.
|
|
222
|
+
const markPriceStale = ticker === undefined;
|
|
189
223
|
const markPrice = ticker?.last ?? p.entryPrice;
|
|
190
224
|
const notional = p.quantity * markPrice;
|
|
191
225
|
const pnlMultiplier = p.side === 'long' ? 1 : -1;
|
|
@@ -200,6 +234,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
200
234
|
contractSize: 1,
|
|
201
235
|
entryPrice: p.entryPrice,
|
|
202
236
|
markPrice,
|
|
237
|
+
markPriceStale,
|
|
203
238
|
notional,
|
|
204
239
|
unrealizedPnl,
|
|
205
240
|
percentage,
|
|
@@ -233,7 +268,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
233
268
|
}
|
|
234
269
|
getOpenOrders(symbol) {
|
|
235
270
|
const orders = symbol
|
|
236
|
-
? this.state.openOrders.filter(o => o.symbol
|
|
271
|
+
? this.state.openOrders.filter(o => sameMarket(o.symbol, symbol))
|
|
237
272
|
: this.state.openOrders;
|
|
238
273
|
return orders.map(o => this.toCcxtOrder(o));
|
|
239
274
|
}
|
|
@@ -244,10 +279,10 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
244
279
|
// ---- Order book ----
|
|
245
280
|
/** Cache the latest order book snapshot for a symbol. */
|
|
246
281
|
updateOrderBook(symbol, orderbook) {
|
|
247
|
-
this.lastOrderBook.set(symbol, orderbook);
|
|
282
|
+
this.lastOrderBook.set(tickerKey(symbol), orderbook);
|
|
248
283
|
}
|
|
249
284
|
getLastOrderBook(symbol) {
|
|
250
|
-
return this.lastOrderBook.get(symbol);
|
|
285
|
+
return this.lastOrderBook.get(tickerKey(symbol));
|
|
251
286
|
}
|
|
252
287
|
// ---- Write operations (for tools) ----
|
|
253
288
|
createOrder(symbol, side, type, amount, price, metadata,
|
|
@@ -255,17 +290,29 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
255
290
|
* market branch prices off THIS instead of the last tick, and skips the
|
|
256
291
|
* stale-quote guard — the caller supplied the price, so quote age is
|
|
257
292
|
* irrelevant, and a protective exit must never be blocked (issue #202). */
|
|
258
|
-
referencePrice
|
|
293
|
+
referencePrice,
|
|
294
|
+
/** Set ONLY by closePosition for mechanical/operator-initiated exits —
|
|
295
|
+
* exempts them from the startup lockout. Never reachable from the
|
|
296
|
+
* agent-facing create_order path. */
|
|
297
|
+
protectiveExit) {
|
|
259
298
|
// ---- Startup trade lockout ----
|
|
260
299
|
// Block trades during the first 15s after gateway restart IF there were
|
|
261
300
|
// existing positions at startup. This prevents stale agent sessions from
|
|
262
301
|
// selling positions before the session is cleared and the agent re-reads SKILL.md.
|
|
263
302
|
// Only activates when positions exist (nothing to protect if starting empty).
|
|
303
|
+
// Protective exits pass through: a stop breached 3s after a restart must
|
|
304
|
+
// close NOW — blocking the watcher here left positions unprotected for
|
|
305
|
+
// the whole window (open item since 2026-07-27).
|
|
264
306
|
const elapsed = Date.now() - this.startupTime;
|
|
265
307
|
if (this.hadPositionsAtStartup && elapsed < ExchangeSimulator.STARTUP_LOCKOUT_MS) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
308
|
+
if (protectiveExit) {
|
|
309
|
+
logger.info(TAG, `Startup lockout bypassed for protective exit: ${side} ${amount} ${symbol}`);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const remaining = Math.ceil((ExchangeSimulator.STARTUP_LOCKOUT_MS - elapsed) / 1000);
|
|
313
|
+
logger.warn(TAG, `STARTUP LOCKOUT: Blocked ${side} ${amount} ${symbol} — ${remaining}s remaining. This prevents stale session trades during restart.`);
|
|
314
|
+
throw new Error(`Trade blocked: startup lockout (${remaining}s remaining). The gateway just restarted — wait for the agent to re-read its instructions and check positions before trading.`);
|
|
315
|
+
}
|
|
269
316
|
}
|
|
270
317
|
if (amount <= 0) {
|
|
271
318
|
throw new Error('Order amount must be positive');
|
|
@@ -294,7 +341,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
294
341
|
return this.executeMarketFill(order, referencePrice, metadata);
|
|
295
342
|
}
|
|
296
343
|
// Market orders fill immediately at current price
|
|
297
|
-
const ticker = this.lastTicker.get(symbol);
|
|
344
|
+
const ticker = this.lastTicker.get(tickerKey(symbol));
|
|
298
345
|
if (!ticker) {
|
|
299
346
|
throw new Error(`No ticker data for ${symbol}. Call updateTicker() first.`);
|
|
300
347
|
}
|
|
@@ -308,7 +355,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
308
355
|
// Limit order — check if it crosses the current price. A stale quote must
|
|
309
356
|
// not price an immediate cross-fill (same hazard as market fills); the
|
|
310
357
|
// order RESTS instead and fills on the next fresh tick via updateTicker.
|
|
311
|
-
const ticker = this.lastTicker.get(symbol);
|
|
358
|
+
const ticker = this.lastTicker.get(tickerKey(symbol));
|
|
312
359
|
if (ticker && this.quoteAgeMs(ticker) <= this.maxQuoteAgeMs() && this.shouldFillLimit(order, ticker.last)) {
|
|
313
360
|
return this.executeLimitFill(order, ticker.last, metadata);
|
|
314
361
|
}
|
|
@@ -341,7 +388,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
341
388
|
const cancelled = [];
|
|
342
389
|
const remaining = [];
|
|
343
390
|
for (const order of this.state.openOrders) {
|
|
344
|
-
if (!symbol || order.symbol
|
|
391
|
+
if (!symbol || sameMarket(order.symbol, symbol)) {
|
|
345
392
|
order.status = 'canceled';
|
|
346
393
|
cancelled.push(order);
|
|
347
394
|
this.pendingOrderMetadata.delete(order.id);
|
|
@@ -364,7 +411,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
364
411
|
* else, which keeps the normal market-close path byte-identical.
|
|
365
412
|
*/
|
|
366
413
|
closePosition(symbol, closeReason, referencePrice) {
|
|
367
|
-
const position = this.state.positions.find(p => p.symbol
|
|
414
|
+
const position = this.state.positions.find(p => sameMarket(p.symbol, symbol));
|
|
368
415
|
if (!position) {
|
|
369
416
|
throw new Error(`No open position for ${symbol}`);
|
|
370
417
|
}
|
|
@@ -375,9 +422,15 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
375
422
|
if (closeReason) {
|
|
376
423
|
position.metadata = { ...(position.metadata ?? {}), closeReason };
|
|
377
424
|
}
|
|
378
|
-
// Create opposing market order to close the position
|
|
425
|
+
// Create opposing market order to close the position.
|
|
426
|
+
// Mechanical / operator-initiated exits (stop_watcher, exchange_target,
|
|
427
|
+
// emergency, operator, bracket_attach_failed, …) must never wait out the
|
|
428
|
+
// startup lockout — a stop breached seconds after a restart has to close
|
|
429
|
+
// immediately. Only discretionary closes ('agent' or reason-less) keep
|
|
430
|
+
// the stale-session guard.
|
|
431
|
+
const protectiveExit = closeReason !== undefined && closeReason !== 'agent';
|
|
379
432
|
const closeSide = position.side === 'long' ? 'sell' : 'buy';
|
|
380
|
-
return this.createOrder(symbol, closeSide, 'market', position.quantity, undefined, undefined, referencePrice);
|
|
433
|
+
return this.createOrder(symbol, closeSide, 'market', position.quantity, undefined, undefined, referencePrice, protectiveExit);
|
|
381
434
|
}
|
|
382
435
|
/** Paper-only: move an open position's MUTABLE protective levels (stopPrice /
|
|
383
436
|
* targetPrice) in place and persist, WITHOUT the close+reopen round-trip
|
|
@@ -387,7 +440,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
387
440
|
* and getPositions both read metadata.stopPrice, so a moved stop takes
|
|
388
441
|
* effect on the next watcher tick. Throws if there is no open position. (M9) */
|
|
389
442
|
updatePositionMetadata(symbol, patch) {
|
|
390
|
-
const pos = this.state.positions.find(p => p.symbol
|
|
443
|
+
const pos = this.state.positions.find(p => sameMarket(p.symbol, symbol));
|
|
391
444
|
if (!pos) {
|
|
392
445
|
throw new Error(`No open position for ${symbol}`);
|
|
393
446
|
}
|
|
@@ -401,7 +454,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
401
454
|
}
|
|
402
455
|
// ---- Ticker updates (checks pending limit fills) ----
|
|
403
456
|
updateTicker(ticker) {
|
|
404
|
-
this.lastTicker.set(ticker.symbol, ticker);
|
|
457
|
+
this.lastTicker.set(tickerKey(ticker.symbol), ticker);
|
|
405
458
|
this.refreshMfeForSymbol(ticker.symbol, ticker.last);
|
|
406
459
|
// MFE is refreshed FIRST so the peak this tick reached is recorded before a
|
|
407
460
|
// target close reads it — otherwise every TP exit would understate its own
|
|
@@ -411,7 +464,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
411
464
|
const toFill = [];
|
|
412
465
|
const remaining = [];
|
|
413
466
|
for (const order of this.state.openOrders) {
|
|
414
|
-
if (order.symbol
|
|
467
|
+
if (sameMarket(order.symbol, ticker.symbol) && this.shouldFillLimit(order, ticker.last)) {
|
|
415
468
|
toFill.push(order);
|
|
416
469
|
}
|
|
417
470
|
else {
|
|
@@ -449,7 +502,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
449
502
|
}
|
|
450
503
|
}
|
|
451
504
|
getLastTicker(symbol) {
|
|
452
|
-
return this.lastTicker.get(symbol);
|
|
505
|
+
return this.lastTicker.get(tickerKey(symbol));
|
|
453
506
|
}
|
|
454
507
|
/**
|
|
455
508
|
* Take-profit legs — the paper analog of the exchange-native
|
|
@@ -483,7 +536,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
483
536
|
if (!Number.isFinite(price) || price <= 0)
|
|
484
537
|
return;
|
|
485
538
|
// Snapshot: closing mutates state.positions mid-iteration.
|
|
486
|
-
const candidates = this.state.positions.filter((p) => p.symbol
|
|
539
|
+
const candidates = this.state.positions.filter((p) => sameMarket(p.symbol, symbol));
|
|
487
540
|
for (const position of candidates) {
|
|
488
541
|
const target = position.metadata?.targetPrice;
|
|
489
542
|
if (target === undefined || !Number.isFinite(target) || target <= 0)
|
|
@@ -492,17 +545,20 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
492
545
|
if (!breached)
|
|
493
546
|
continue;
|
|
494
547
|
// Guard against a re-entrant tick firing a second close on the same
|
|
495
|
-
// symbol while the first is still settling.
|
|
496
|
-
|
|
548
|
+
// symbol while the first is still settling. Keyed canonically so the
|
|
549
|
+
// guard still holds when the tick and the position disagree on suffix.
|
|
550
|
+
const pendingKey = tickerKey(position.symbol);
|
|
551
|
+
if (this.takeProfitPending.has(pendingKey))
|
|
497
552
|
continue;
|
|
498
|
-
this.takeProfitPending.add(
|
|
553
|
+
this.takeProfitPending.add(pendingKey);
|
|
499
554
|
try {
|
|
500
|
-
logger.info(TAG, `TARGET REACHED: ${symbol} ${position.side} price=${price} target=${target} — closing (exchange_target)`);
|
|
555
|
+
logger.info(TAG, `TARGET REACHED: ${position.symbol} ${position.side} price=${price} target=${target} — closing (exchange_target)`);
|
|
501
556
|
// Target = decision price; the fill engine adds realistic adverse
|
|
502
|
-
// slippage on top (see the fill-convention note above).
|
|
503
|
-
|
|
557
|
+
// slippage on top (see the fill-convention note above). Address the
|
|
558
|
+
// close with the POSITION's own symbol, never the ticker's form.
|
|
559
|
+
const order = this.closePosition(position.symbol, 'exchange_target', target);
|
|
504
560
|
this.emit('target_closed', {
|
|
505
|
-
symbol,
|
|
561
|
+
symbol: position.symbol,
|
|
506
562
|
side: position.side,
|
|
507
563
|
targetPrice: target,
|
|
508
564
|
markPrice: price,
|
|
@@ -514,10 +570,10 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
514
570
|
catch (err) {
|
|
515
571
|
// Never let a failed protective close kill the tick loop — the next
|
|
516
572
|
// tick retries, and the position is still visible to the agent.
|
|
517
|
-
logger.error(TAG, `Target close failed for ${symbol}: ${formatError(err)}`);
|
|
573
|
+
logger.error(TAG, `Target close failed for ${position.symbol}: ${formatError(err)}`);
|
|
518
574
|
}
|
|
519
575
|
finally {
|
|
520
|
-
this.takeProfitPending.delete(
|
|
576
|
+
this.takeProfitPending.delete(pendingKey);
|
|
521
577
|
}
|
|
522
578
|
}
|
|
523
579
|
}
|
|
@@ -530,7 +586,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
530
586
|
return;
|
|
531
587
|
let peakAdvanced = false;
|
|
532
588
|
for (const p of this.state.positions) {
|
|
533
|
-
if (p.symbol
|
|
589
|
+
if (!sameMarket(p.symbol, symbol))
|
|
534
590
|
continue;
|
|
535
591
|
const meta = { ...(p.metadata ?? {}) };
|
|
536
592
|
const priorPeak = meta.mfePeakPrice;
|
|
@@ -602,7 +658,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
602
658
|
const keptPeak = diskPeak === undefined
|
|
603
659
|
? priorPeak
|
|
604
660
|
: (p.side === 'long' ? Math.max(priorPeak, diskPeak) : Math.min(priorPeak, diskPeak));
|
|
605
|
-
const mark = this.lastTicker.get(p.symbol)?.last ?? meta.originalEntryPrice ?? p.entryPrice;
|
|
661
|
+
const mark = this.lastTicker.get(tickerKey(p.symbol))?.last ?? meta.originalEntryPrice ?? p.entryPrice;
|
|
606
662
|
const out = updateMfe({
|
|
607
663
|
side: p.side,
|
|
608
664
|
entryPrice: meta.originalEntryPrice ?? p.entryPrice,
|
|
@@ -671,7 +727,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
671
727
|
const max = this.maxQuoteAgeMs();
|
|
672
728
|
if (age <= max)
|
|
673
729
|
return;
|
|
674
|
-
const pos = this.state.positions.find(p => p.symbol
|
|
730
|
+
const pos = this.state.positions.find(p => sameMarket(p.symbol, symbol));
|
|
675
731
|
// The order side reaching here is the one being filled — derive reduce vs
|
|
676
732
|
// grow from the position side at the call site instead? The market path
|
|
677
733
|
// calls this before fill with the order side unavailable; use position
|
|
@@ -696,8 +752,10 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
696
752
|
: currentPrice >= order.price;
|
|
697
753
|
}
|
|
698
754
|
executeMarketFill(order, currentPrice, metadata) {
|
|
699
|
-
|
|
700
|
-
|
|
755
|
+
// Match canonically: a miss here would hand fillMarketOrder position=null,
|
|
756
|
+
// turning a CLOSE into a brand-new opposing position.
|
|
757
|
+
const position = this.state.positions.find(p => sameMarket(p.symbol, order.symbol)) ?? null;
|
|
758
|
+
const orderbook = this.lastOrderBook.get(tickerKey(order.symbol)) ?? null;
|
|
701
759
|
const realistic = {
|
|
702
760
|
orderbook,
|
|
703
761
|
config: this.simulationConfig,
|
|
@@ -707,7 +765,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
707
765
|
const result = fillMarketOrder(order, currentPrice, this.state.wallet, position, realistic);
|
|
708
766
|
// Observability for issue #202: stamp the quote's age onto the fill's
|
|
709
767
|
// execution-quality record so staleness is visible in trade history.
|
|
710
|
-
const tickerAtFill = this.lastTicker.get(order.symbol);
|
|
768
|
+
const tickerAtFill = this.lastTicker.get(tickerKey(order.symbol));
|
|
711
769
|
if (result.executionQuality && tickerAtFill) {
|
|
712
770
|
result.executionQuality.quoteAgeMs = this.quoteAgeMs(tickerAtFill);
|
|
713
771
|
}
|
|
@@ -725,7 +783,7 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
725
783
|
return ccxtOrder;
|
|
726
784
|
}
|
|
727
785
|
executeLimitFill(order, decisionPrice, metadata) {
|
|
728
|
-
const position = this.state.positions.find(p => p.symbol
|
|
786
|
+
const position = this.state.positions.find(p => sameMarket(p.symbol, order.symbol)) ?? null;
|
|
729
787
|
const result = fillLimitOrder(order, this.state.wallet, position, this.simulationConfig, decisionPrice, metadata);
|
|
730
788
|
const ccxtOrder = this.applyFillResult(result);
|
|
731
789
|
const eq = result.executionQuality;
|
|
@@ -778,7 +836,9 @@ export class ExchangeSimulator extends EventEmitter {
|
|
|
778
836
|
return ccxtOrder;
|
|
779
837
|
}
|
|
780
838
|
updatePosition(symbol, newPosition) {
|
|
781
|
-
|
|
839
|
+
// Canonical match: an exact-compare miss here would append a DUPLICATE
|
|
840
|
+
// position row instead of replacing/removing the existing one.
|
|
841
|
+
const idx = this.state.positions.findIndex(p => sameMarket(p.symbol, symbol));
|
|
782
842
|
if (newPosition) {
|
|
783
843
|
if (idx >= 0) {
|
|
784
844
|
this.state.positions[idx] = newPosition;
|
|
@@ -566,13 +566,16 @@ async function auditHlLive(adapter) {
|
|
|
566
566
|
};
|
|
567
567
|
}
|
|
568
568
|
const entries = [];
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
569
|
+
// Fan the per-symbol open-orders reads out (HL has no batched read; the
|
|
570
|
+
// serial loop cost N round-trips per heartbeat), then walk the results in
|
|
571
|
+
// the ORIGINAL position order — the first failing symbol still produces
|
|
572
|
+
// the identical error return, and no verdict logic changes.
|
|
573
|
+
const orderFetches = await Promise.allSettled(activePositions.map((pos) => adapter.getOpenOrders(pos.symbol)));
|
|
574
|
+
for (let i = 0; i < activePositions.length; i++) {
|
|
575
|
+
const p = activePositions[i];
|
|
576
|
+
const fetched = orderFetches[i];
|
|
577
|
+
if (fetched.status === 'rejected') {
|
|
578
|
+
const m = formatError(fetched.reason);
|
|
576
579
|
logger.warn(TAG, `HL audit skipped — open orders unavailable for ${p.symbol}: ${m}`);
|
|
577
580
|
return {
|
|
578
581
|
error: `Exchange data unavailable for ${p.symbol} (Hyperliquid open-orders fetch failed). ` +
|
|
@@ -580,6 +583,7 @@ async function auditHlLive(adapter) {
|
|
|
580
583
|
`close_position(reason='bracket_integrity') on the basis of this call. Underlying: ${m}`,
|
|
581
584
|
};
|
|
582
585
|
}
|
|
586
|
+
const symbolOrders = fetched.value;
|
|
583
587
|
const ledgerRow = ledger.getBySymbol(p.symbol) ?? null;
|
|
584
588
|
const rowNonTerminalWithCids = !!ledgerRow && !isTerminalBracketState(ledgerRow.state) && Boolean(ledgerRow.slCid || ledgerRow.tpCid);
|
|
585
589
|
if (symbolOrders.length === 0 && rowNonTerminalWithCids) {
|
|
@@ -317,9 +317,18 @@ export async function closePositionTool(args, deps) {
|
|
|
317
317
|
'Use get_wave9_status for a reversal authorization or operator_command for an emergency close.');
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
+
// Thread the validated reason to the adapter as its CloseReason:
|
|
321
|
+
// 'operator_command' maps to 'operator' so an operator-driven close is
|
|
322
|
+
// never caught by the paper startup lockout (whose protective-exit
|
|
323
|
+
// carve-out keys on the reason — before this, the tool dropped the
|
|
324
|
+
// reason entirely and EVERY generic close arrived as discretionary).
|
|
325
|
+
// All other reasons are agent-discretionary by design and stay subject
|
|
326
|
+
// to the lockout; the rich reason still reaches the journal via
|
|
327
|
+
// close_reason, this only fixes the adapter-level class + metadata.
|
|
328
|
+
const adapterCloseReason = args.reason === 'operator_command' ? 'operator' : 'agent';
|
|
320
329
|
return wave9ExitClaimed
|
|
321
330
|
? deps.adapter.closePosition(args.symbol, 'wave9_signal_reversal')
|
|
322
|
-
: deps.adapter.closePosition(args.symbol);
|
|
331
|
+
: deps.adapter.closePosition(args.symbol, adapterCloseReason);
|
|
323
332
|
};
|
|
324
333
|
const closeWave9 = async () => {
|
|
325
334
|
if (!wave9Lease)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Tool: create_order — order execution with real price data + pre-trade risk gate
|
|
2
2
|
// Readiness gate: BLOCKED unless adapter.readiness === 'READY'.
|
|
3
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
4
|
-
import { formatError } from '../logger.js';
|
|
4
|
+
import { formatError, logger } from '../logger.js';
|
|
5
5
|
import { getQuoteBalance, getQuoteWalletBalance } from '../balance-utils.js';
|
|
6
6
|
import { fetchCurrentPrice, fetchOrderBook, isError } from './helpers.js';
|
|
7
7
|
import { validateCreateOrder, sanitizeRealizationRule, validateProtectiveGeometry } from './assessment-validation.js';
|
|
@@ -875,6 +875,7 @@ export async function createOrderTool(args, deps) {
|
|
|
875
875
|
}
|
|
876
876
|
// In paper mode, fetch latest ticker + order book for realistic fills
|
|
877
877
|
let ticker = null;
|
|
878
|
+
let livePortfolioPromise;
|
|
878
879
|
if (!deps.adapter.isLive) {
|
|
879
880
|
const paperDeps = { binanceApi: deps.binanceApi, simulator: deps.adapter.getSimulator() };
|
|
880
881
|
const [priceResult] = await Promise.all([
|
|
@@ -886,7 +887,14 @@ export async function createOrderTool(args, deps) {
|
|
|
886
887
|
ticker = priceResult;
|
|
887
888
|
}
|
|
888
889
|
else {
|
|
889
|
-
// Live mode:
|
|
890
|
+
// Live mode: the risk-gate portfolio snapshot (balance + positions) has
|
|
891
|
+
// no dependency on the ticker — start it here so the two exchange
|
|
892
|
+
// round-trips overlap; awaited at the risk check below. The catch-keeper
|
|
893
|
+
// only silences the unhandled rejection an early return would otherwise
|
|
894
|
+
// leave floating; the later await still surfaces the real error.
|
|
895
|
+
livePortfolioPromise = buildPortfolioSnapshotFromAdapter(deps.adapter);
|
|
896
|
+
livePortfolioPromise.catch(() => { });
|
|
897
|
+
// Fetch ticker for risk check reference price
|
|
890
898
|
const lastPrice = await deps.adapter.getLastPrice(args.symbol);
|
|
891
899
|
if (lastPrice != null) {
|
|
892
900
|
ticker = {
|
|
@@ -1003,7 +1011,7 @@ export async function createOrderTool(args, deps) {
|
|
|
1003
1011
|
consecutiveLosses = computeConsecutiveLosses(simulator.getState().tradeHistory);
|
|
1004
1012
|
}
|
|
1005
1013
|
else {
|
|
1006
|
-
portfolio = await buildPortfolioSnapshotFromAdapter(deps.adapter);
|
|
1014
|
+
portfolio = await (livePortfolioPromise ?? buildPortfolioSnapshotFromAdapter(deps.adapter));
|
|
1007
1015
|
// In live mode, consecutive losses would come from intelligence DB — use 0 for now
|
|
1008
1016
|
}
|
|
1009
1017
|
// Bracket enforcement only applies in live mode when the feature is enabled.
|
|
@@ -1083,14 +1091,48 @@ export async function createOrderTool(args, deps) {
|
|
|
1083
1091
|
typeof args.confluence_score === 'number');
|
|
1084
1092
|
const proposalPathWired = !!(deps.proposalManager && deps.userId);
|
|
1085
1093
|
const approvalMode = deps.approvalMode ?? 'off';
|
|
1094
|
+
// per_trade + proposal path unwired: FAIL CLOSED.
|
|
1095
|
+
//
|
|
1096
|
+
// The operator asked for a human gate on every entry. If we can't deliver
|
|
1097
|
+
// the proposal (no ingest token / no REEFCLAW_USER_ID), the honest failure
|
|
1098
|
+
// is "no trade", not "trade without the gate". This branch previously fell
|
|
1099
|
+
// through to the normal fire path so the agent was never trapped in an
|
|
1100
|
+
// unfireable state — but that turned a misconfiguration into a live order
|
|
1101
|
+
// the operator never approved, silently, with only a boot-time warning as
|
|
1102
|
+
// the signal. A silent bypass of an explicitly-requested safety gate is the
|
|
1103
|
+
// one outcome this feature exists to prevent (Locked Decision #2: never
|
|
1104
|
+
// silent fire). Credentials drift for ordinary reasons — token rotation
|
|
1105
|
+
// touches four files (docs/CLAUDE/token-rotation.md), and a freshly
|
|
1106
|
+
// provisioned box can boot before they land — so this is a reachable state,
|
|
1107
|
+
// not a theoretical one.
|
|
1108
|
+
//
|
|
1109
|
+
// Halting entries is the safe direction: exits, closes, stops and every
|
|
1110
|
+
// operator emergency control are unaffected (they don't route through the
|
|
1111
|
+
// approval branch), so a mis-wired box can still protect and unwind an open
|
|
1112
|
+
// book — it just can't open new risk without the gate the operator asked
|
|
1113
|
+
// for. Logged at ERROR on EVERY occurrence, not once at boot, so the signal
|
|
1114
|
+
// is present at the time the trade is refused.
|
|
1115
|
+
if (approvalMode === 'per_trade' && !proposalPathWired) {
|
|
1116
|
+
logger.error('create-order', `approval.mode=per_trade but the proposal path is not wired — REFUSING to fire ` +
|
|
1117
|
+
`${args.symbol} ${side} (fail-closed). Missing plugin connectionToken/WEBAPP_INGEST_TOKEN ` +
|
|
1118
|
+
`and/or REEFCLAW_USER_ID. Restore the credentials, or set approval.mode=off to trade ` +
|
|
1119
|
+
`autonomously again.`);
|
|
1120
|
+
return {
|
|
1121
|
+
error: 'Order refused: approval mode is per_trade but this box cannot deliver the proposal ' +
|
|
1122
|
+
'to the operator (missing connection token and/or REEFCLAW_USER_ID), so there is no ' +
|
|
1123
|
+
'way for the operator to approve it. Refusing to fire un-approved — this is fail-closed ' +
|
|
1124
|
+
'by design, not a transient error. Tell the operator: either restore the plugin ' +
|
|
1125
|
+
'connection credentials, or set approval.mode=off to resume autonomous trading. ' +
|
|
1126
|
+
'Do not retry until they confirm one of those. Closing positions, stops and brackets ' +
|
|
1127
|
+
'are unaffected.',
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1086
1130
|
// per_trade mode: propose and EARLY-RETURN. The agent's call doesn't fire;
|
|
1087
1131
|
// the operator approves and ProposalDecisionListener fires via this same
|
|
1088
1132
|
// tool with proposalManager omitted (which takes the real path below).
|
|
1089
1133
|
//
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
// fire path — the startup warning is the operator's signal. We never trap
|
|
1093
|
-
// the agent inside an unfireable per_trade branch.
|
|
1134
|
+
// NOTE the listener's own fire passes NO approvalMode (defaults to 'off')
|
|
1135
|
+
// and no proposalManager, so it never re-enters this branch.
|
|
1094
1136
|
if (approvalMode === 'per_trade' && proposalPathWired) {
|
|
1095
1137
|
if (!argsMetadataComplete) {
|
|
1096
1138
|
return {
|
|
@@ -12,5 +12,10 @@
|
|
|
12
12
|
// FUNDING_OVERLAY flag is off or the symbol has < 100 30d samples.
|
|
13
13
|
import { fetchIntelApi, enc, resolveIntelSymbol } from './intel-api.js';
|
|
14
14
|
export async function getFundingContextTool(args, deps) {
|
|
15
|
-
|
|
15
|
+
// Server-side percentile context already has a 15-min TTL — 60s here just
|
|
16
|
+
// collapses same-heartbeat repeats.
|
|
17
|
+
return fetchIntelApi(`/api/funding/${enc(resolveIntelSymbol(deps, args.symbol))}`, deps, {
|
|
18
|
+
cacheTtlMs: 60_000,
|
|
19
|
+
timeoutMs: 10_000,
|
|
20
|
+
});
|
|
16
21
|
}
|
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
import { fetchIntelApi, enc, resolveIntelSymbol } from './intel-api.js';
|
|
4
4
|
export async function getLiquidationLevelsTool(args, deps) {
|
|
5
5
|
const hours = args.hours ?? 24;
|
|
6
|
-
|
|
6
|
+
// Historical clusters move slowly — 30s TTL collapses per-position repeats.
|
|
7
|
+
return fetchIntelApi(`/api/liquidation-levels/${enc(resolveIntelSymbol(deps, args.symbol))}?hours=${hours}`, deps, {
|
|
8
|
+
cacheTtlMs: 30_000,
|
|
9
|
+
timeoutMs: 10_000,
|
|
10
|
+
});
|
|
7
11
|
}
|
|
@@ -13,7 +13,13 @@ export async function getLiquidationPulseTool(args, deps) {
|
|
|
13
13
|
params.set('symbol', resolveIntelSymbol(deps, args.symbol));
|
|
14
14
|
const windowSeconds = clamp(args.window_seconds ?? 60, 5, 300);
|
|
15
15
|
params.set('window_seconds', String(windowSeconds));
|
|
16
|
-
|
|
16
|
+
// 10s TTL: short enough that an active_cascade classification is never
|
|
17
|
+
// stale-read into the override decision (cascades persist 60-180s), long
|
|
18
|
+
// enough to collapse the per-position repeats within one heartbeat.
|
|
19
|
+
return fetchIntelApi(`/api/liquidation-pulse?${params.toString()}`, deps, {
|
|
20
|
+
cacheTtlMs: 10_000,
|
|
21
|
+
timeoutMs: 10_000,
|
|
22
|
+
});
|
|
17
23
|
}
|
|
18
24
|
function clamp(n, lo, hi) {
|
|
19
25
|
if (!Number.isFinite(n))
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// unchanged. This bounds the threat — it can't make the agent place a naked
|
|
13
13
|
// order (the create_order gate still applies) — and makes injected directives
|
|
14
14
|
// far less likely to be followed.
|
|
15
|
+
import { keepAliveFetch } from '../http/keepalive-fetch.js';
|
|
15
16
|
/** Categories whose payloads contain attacker-influenceable free text. */
|
|
16
17
|
const UNTRUSTED_TEXT_CATEGORIES = new Set(['news', 'social', 'calendar']);
|
|
17
18
|
const MAX_TEXT_LEN = 2000;
|
|
@@ -88,7 +89,7 @@ export async function getMarketIntelTool(args, deps) {
|
|
|
88
89
|
url.searchParams.set('symbols', symbols.join(','));
|
|
89
90
|
}
|
|
90
91
|
try {
|
|
91
|
-
const res = await
|
|
92
|
+
const res = await keepAliveFetch(url.toString(), {
|
|
92
93
|
headers: { Authorization: `Bearer ${connectionToken}` },
|
|
93
94
|
signal: AbortSignal.timeout(10_000),
|
|
94
95
|
});
|
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
import { logger, formatError } from '../logger.js';
|
|
16
16
|
const TAG = 'get-relevant-learnings';
|
|
17
17
|
const VALID_APPLIES_AT = new Set(['entry', 'heartbeat', 'close']);
|
|
18
|
+
// Curated learnings change on operator-curation timescales, but the agent is
|
|
19
|
+
// instructed to read them at several points per heartbeat — and a looping
|
|
20
|
+
// model can spam identical calls (observed live 2026-08-04: ~230 identical
|
|
21
|
+
// calls in one beat drove a 272k-token context overflow). A short TTL keyed
|
|
22
|
+
// on the exact query bounds both.
|
|
23
|
+
const CACHE_TTL_MS = 30_000;
|
|
24
|
+
const cache = new Map();
|
|
18
25
|
export async function getRelevantLearningsTool(args, deps) {
|
|
19
26
|
if (!deps.decisionsClient || !deps.userId) {
|
|
20
27
|
// Ingest not wired — return empty, callable. Same shape as
|
|
@@ -32,6 +39,11 @@ export async function getRelevantLearningsTool(args, deps) {
|
|
|
32
39
|
error: "applies_at is required and must be one of: 'entry', 'heartbeat', 'close'.",
|
|
33
40
|
};
|
|
34
41
|
}
|
|
42
|
+
const cacheKey = `${deps.userId}:${args.applies_at}:${args.setup_type ?? ''}:${args.regime ?? ''}:${args.verdict ?? ''}`;
|
|
43
|
+
const hit = cache.get(cacheKey);
|
|
44
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) {
|
|
45
|
+
return structuredClone(hit.result);
|
|
46
|
+
}
|
|
35
47
|
let response;
|
|
36
48
|
try {
|
|
37
49
|
response = await deps.decisionsClient.getRelevantLearnings(deps.userId, {
|
|
@@ -56,10 +68,17 @@ export async function getRelevantLearningsTool(args, deps) {
|
|
|
56
68
|
note: 'Webapp /api/internal/learnings returned no result; proceeding without curated learnings.',
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
|
-
|
|
71
|
+
const result = {
|
|
60
72
|
ok: true,
|
|
61
73
|
learnings: response.learnings,
|
|
62
74
|
total_candidates: response.totalCandidates,
|
|
63
75
|
returned_count: response.returnedCount,
|
|
64
76
|
};
|
|
77
|
+
cache.set(cacheKey, { at: Date.now(), result });
|
|
78
|
+
if (cache.size > 64) {
|
|
79
|
+
const oldest = cache.keys().next().value;
|
|
80
|
+
if (oldest !== undefined)
|
|
81
|
+
cache.delete(oldest);
|
|
82
|
+
}
|
|
83
|
+
return structuredClone(result);
|
|
65
84
|
}
|
|
@@ -7,5 +7,10 @@
|
|
|
7
7
|
// `microstructure.bandedLiquidity` flag is off (= columns are NULL).
|
|
8
8
|
import { fetchIntelApi, enc, resolveIntelSymbol } from './intel-api.js';
|
|
9
9
|
export async function getRestingLiquidityTool(args, deps) {
|
|
10
|
-
|
|
10
|
+
// Cached: the microstructure assembler hits the same endpoint on the review
|
|
11
|
+
// path each heartbeat; advisory read, so a short TTL + tight timeout.
|
|
12
|
+
return fetchIntelApi(`/api/resting-liquidity/${enc(resolveIntelSymbol(deps, args.symbol))}`, deps, {
|
|
13
|
+
cacheTtlMs: 15_000,
|
|
14
|
+
timeoutMs: 10_000,
|
|
15
|
+
});
|
|
11
16
|
}
|
|
@@ -695,6 +695,23 @@ export async function getWave9StatusTool(deps) {
|
|
|
695
695
|
const reversalDue = position
|
|
696
696
|
? (position.side === 'long' ? daily.exitLong : daily.exitShort)
|
|
697
697
|
: false;
|
|
698
|
+
// Quiet-row compaction: no candidate and no held position — the evidence
|
|
699
|
+
// numerics (returns / ATR / reference close) are ~740B per symbol the
|
|
700
|
+
// agent never acts on; × 8 symbols × every heartbeat this was ~68% of
|
|
701
|
+
// the whole status payload. Rows with entries or a position keep the
|
|
702
|
+
// full shape unchanged. Deliberately NOT gated on daily.exitLong/
|
|
703
|
+
// exitShort: those flags are set for most symbols on any
|
|
704
|
+
// negative-momentum day, and with no held position there is nothing to
|
|
705
|
+
// exit — the first shipped version kept them and saved nothing
|
|
706
|
+
// (verified live 2026-08-04: 72 currentReturn mentions per beat).
|
|
707
|
+
if (entries.length === 0 && !position) {
|
|
708
|
+
return {
|
|
709
|
+
symbol,
|
|
710
|
+
capDecision: { status: 'not_candidate', reason: 'no_completed_daily_zero_cross' },
|
|
711
|
+
entries: [],
|
|
712
|
+
reversal: { due: false, timing: 'not_due' },
|
|
713
|
+
};
|
|
714
|
+
}
|
|
698
715
|
return {
|
|
699
716
|
symbol,
|
|
700
717
|
currentReturn: daily.currentReturn,
|
|
@@ -26,7 +26,16 @@ export interface FetchOptions {
|
|
|
26
26
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
27
27
|
body?: unknown;
|
|
28
28
|
timeoutMs?: number;
|
|
29
|
+
/** Opt-in TTL cache for GET reads. The same intel endpoints get hit several
|
|
30
|
+
* times per heartbeat by different callers (agent tool + microstructure
|
|
31
|
+
* assembler + repeated agent calls) — a short shared TTL collapses those
|
|
32
|
+
* into one round-trip. Never set on polling reads that must observe fresh
|
|
33
|
+
* server state (e.g. backtest status). */
|
|
34
|
+
cacheTtlMs?: number;
|
|
29
35
|
}
|
|
36
|
+
/** Test support: drop every cached GET response (the cache is module-level,
|
|
37
|
+
* so suites that stub fetch with per-test responses must clear it). */
|
|
38
|
+
export declare function clearIntelGetCache(): void;
|
|
30
39
|
/** Encode a value for safe use in a URL path segment */
|
|
31
40
|
export declare const enc: typeof encodeURIComponent;
|
|
32
41
|
export declare function fetchIntelApi(path: string, deps: IntelApiDeps, options?: FetchOptions): Promise<Record<string, unknown> | {
|