@reefclaw/connect 0.1.21 → 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.
Files changed (42) hide show
  1. package/assets/bridge/bridge.d.ts +20 -5
  2. package/assets/bridge/bridge.js +29 -14
  3. package/assets/bridge/config.js +6 -0
  4. package/assets/bridge/gateway/gateway-ws-client.d.ts +4 -1
  5. package/assets/bridge/gateway/gateway-ws-client.js +41 -11
  6. package/assets/bridge/providers/gateway.d.ts +28 -0
  7. package/assets/bridge/providers/gateway.js +130 -5
  8. package/assets/bridge/providers/onboarding-commands.d.ts +8 -5
  9. package/assets/bridge/providers/onboarding-commands.js +1 -1
  10. package/assets/bridge/providers/risk-calculator.d.ts +61 -2
  11. package/assets/bridge/providers/risk-calculator.js +92 -20
  12. package/assets/bridge/utils/skill-signing.js +8 -3
  13. package/assets/plugin/config/plugin-config-io.js +5 -0
  14. package/assets/plugin/exchange-adapter.d.ts +13 -0
  15. package/assets/plugin/index.js +15 -4
  16. package/assets/plugin/ingest/event-loop-monitor.d.ts +11 -0
  17. package/assets/plugin/ingest/event-loop-monitor.js +77 -0
  18. package/assets/plugin/ingest/readiness-reporter.d.ts +9 -0
  19. package/assets/plugin/ingest/readiness-reporter.js +54 -5
  20. package/assets/plugin/live/user-data-stream.js +10 -2
  21. package/assets/plugin/openclaw.plugin.json +1 -1
  22. package/assets/plugin/risk/pre-trade-check.js +18 -5
  23. package/assets/plugin/strategy/condition-registry.js +9 -2
  24. package/assets/plugin/strategy/evaluator.d.ts +5 -0
  25. package/assets/plugin/tools/cancel-all-orders.js +9 -1
  26. package/assets/plugin/tools/create-order.js +18 -1
  27. package/assets/plugin/tools/get-bracket-config.d.ts +21 -2
  28. package/assets/plugin/tools/get-bracket-config.js +18 -2
  29. package/assets/plugin/tools/set-trading-mode.js +6 -3
  30. package/assets/plugin/venues/hyperliquid/hl-fill-ingest.js +20 -1
  31. package/assets/plugin/venues/hyperliquid/hl-live-adapter.d.ts +4 -0
  32. package/assets/plugin/venues/hyperliquid/hl-live-adapter.js +4 -0
  33. package/assets/plugin/venues/registry.js +8 -7
  34. package/assets/plugin/wave9/paper-admission-guard.d.ts +12 -1
  35. package/assets/plugin/wave9/paper-admission-guard.js +12 -1
  36. package/assets/shared/index.d.ts +1 -1
  37. package/assets/shared/index.js +5 -1
  38. package/assets/shared/readiness.d.ts +7 -0
  39. package/assets/shared/readiness.js +23 -3
  40. package/assets/skill/SKILL.md +6 -11
  41. package/dist/cli.js +3 -0
  42. package/package.json +1 -1
@@ -36,20 +36,27 @@ register('oi_slope', (fact) => ({
36
36
  name: 'oi_slope',
37
37
  met: fact.conditions.oi_slope_up ?? false,
38
38
  }));
39
- // 5. no_liquidation_cluster — always passes in scan context (no live data)
39
+ // 5. no_liquidation_cluster — FAIL-OPEN PLACEHOLDER in scan context: the
40
+ // fact-computer ships no liquidation data, so this preview cannot evaluate
41
+ // it. `note` marks the gap explicitly (never a silent pass); the
42
+ // authoritative evaluation runs in the central signal engine, which reads the
43
+ // live liquidation-levels feed.
40
44
  register('no_liquidation_cluster', () => ({
41
45
  name: 'no_liquidation_cluster',
42
46
  met: true,
47
+ note: 'not evaluated in scan preview (no liquidation data in facts) — authoritative check runs in the signal engine',
43
48
  }));
44
49
  // 6. price_sweep
45
50
  register('price_sweep', (fact) => ({
46
51
  name: 'price_sweep',
47
52
  met: (fact.conditions.price_sweep_high ?? false) || (fact.conditions.price_sweep_low ?? false),
48
53
  }));
49
- // 7. liquidations_at_sweep — always passes in scan context
54
+ // 7. liquidations_at_sweep — FAIL-OPEN PLACEHOLDER in scan context, same gap
55
+ // and same note contract as no_liquidation_cluster above.
50
56
  register('liquidations_at_sweep', () => ({
51
57
  name: 'liquidations_at_sweep',
52
58
  met: true,
59
+ note: 'not evaluated in scan preview (no liquidation data in facts) — authoritative check runs in the signal engine',
53
60
  }));
54
61
  // 8. order_flow_absorption
55
62
  register('order_flow_absorption', (fact) => ({
@@ -45,6 +45,11 @@ export interface ConditionEvalResult {
45
45
  name: string;
46
46
  met: boolean;
47
47
  value?: number;
48
+ /** Set when `met` is a fail-open placeholder rather than a real evaluation
49
+ * (e.g. the scan facts carry no data for this condition). Surfaces the gap
50
+ * to any consumer instead of letting a pass silently impersonate a check —
51
+ * the authoritative evaluation runs in the central signal engine. */
52
+ note?: string;
48
53
  }
49
54
  export interface StrategyEvalResult {
50
55
  strategy: string;
@@ -1,5 +1,13 @@
1
1
  // Tool: cancel_all_orders — cancel all open orders (paper or live)
2
- // NO readiness gate — emergency control (kill switch), must always work.
2
+ //
3
+ // NO readiness gate and NO confirmation prompt — BY DESIGN, not an oversight:
4
+ // this is the safety floor's kill-switch primitive ("Hard controls NEVER
5
+ // disabled or hidden"). Any gate added here becomes a failure mode of the
6
+ // emergency path itself — a wedged confirmation would strand live orders
7
+ // during the exact incident the kill switch exists for. Risk-reducing only:
8
+ // it cancels WORKING orders; the adapter layer preserves protective bracket
9
+ // legs (parseBracketCid skip in cancelAllOrders) so positions are never left
10
+ // naked, and it never opens or increases exposure.
3
11
  export async function cancelAllOrdersTool(args, deps) {
4
12
  const cancel = () => deps.adapter.cancelAllOrders(args.symbol);
5
13
  return deps.adapter.isLive && deps.operationLock
@@ -1008,11 +1008,28 @@ export async function createOrderTool(args, deps) {
1008
1008
  }
1009
1009
  // Bracket enforcement only applies in live mode when the feature is enabled.
1010
1010
  // Paper mode uses the stop-watcher and doesn't care about these flags.
1011
+ //
1012
+ // ★ `brackets.mode` is a BINANCE-only knob — it is only ever passed to
1013
+ // LiveAdapter, and it defaults to 'off'. Gating solely on it meant an HL
1014
+ // live rig skipped the mandatory-stop check entirely: a stopless entry
1015
+ // passed the gate, and HL only attaches legs when stop/target metadata is
1016
+ // present (`wireBracketsAfterSubmit`), so the position went on the book
1017
+ // NAKED. Venues that always enforce brackets declare it on the adapter.
1011
1018
  let bracketEnforcement;
1012
- if (deps.adapter.isLive && bracketsEnabled(loadBracketMode())) {
1019
+ if (deps.adapter.isLive && (deps.adapter.bracketsAlwaysEnforced || bracketsEnabled(loadBracketMode()))) {
1013
1020
  bracketEnforcement = wave9Claimed
1014
1021
  ? { requireStopLoss: true, requireTakeProfit: false }
1015
1022
  : loadBracketRequirements();
1023
+ // On a venue-enforced adapter the stop requirement is NOT operator-
1024
+ // waivable: `requireStopLoss: false` in plugin-config is a Binance-era
1025
+ // toggle whose documented risk assumed a watcher fallback existed. HL has
1026
+ // none — a waved-through stopless entry would sit naked. This also keeps
1027
+ // the gate consistent with get_bracket_config, which reports
1028
+ // requireStopLoss=true for venue-enforced adapters. The TP flag stays
1029
+ // operator-controlled (stop-only "let winners run" is legitimate).
1030
+ if (deps.adapter.bracketsAlwaysEnforced) {
1031
+ bracketEnforcement = { ...bracketEnforcement, requireStopLoss: true };
1032
+ }
1016
1033
  }
1017
1034
  const riskCheck = preTradeRiskCheck(proposed, portfolio, getDefaultPreTradeLimits(), {
1018
1035
  volFactor: !deps.adapter.isLive ? deps.adapter.getSimulator().getVolFactor() : 1.0,
@@ -3,9 +3,28 @@ export interface GetBracketConfigResult {
3
3
  mode: BracketMode;
4
4
  requireStopLoss: boolean;
5
5
  requireTakeProfit: boolean;
6
+ /** True when the mode reported above is the VENUE's unconditional
7
+ * enforcement rather than the `brackets.mode` config value. Lets the
8
+ * dashboard explain why the toggles are inert. */
9
+ venueEnforced?: boolean;
6
10
  }
7
- export declare function getBracketConfigTool(_args: Record<string, never>, deps?: {
11
+ export interface GetBracketConfigDeps {
8
12
  configPath?: string;
9
- }): GetBracketConfigResult | {
13
+ /** Active adapter. Its `bracketsAlwaysEnforced` capability overrides the
14
+ * config-file mode — see below. */
15
+ adapter?: {
16
+ readonly bracketsAlwaysEnforced?: boolean;
17
+ };
18
+ }
19
+ /**
20
+ * ★ `brackets.mode` in plugin-config.json is a BINANCE-only lifecycle knob: it
21
+ * is passed to `LiveAdapter` and nowhere else. Reporting it verbatim made a
22
+ * Hyperliquid rig — where `HlBracketCoordinator` attaches legs unconditionally
23
+ * and there is no watcher fallback — render "Brackets off" on a live dashboard
24
+ * whose every position was in fact bracketed. A protection indicator that
25
+ * under-reports is exactly as dangerous as one that over-reports, so the
26
+ * effective venue behaviour wins over the stale config value.
27
+ */
28
+ export declare function getBracketConfigTool(_args: Record<string, never>, deps?: GetBracketConfigDeps): GetBracketConfigResult | {
10
29
  error: string;
11
30
  };
@@ -6,13 +6,29 @@
6
6
  import { readPluginConfig } from '../config/plugin-config-io.js';
7
7
  import { getBracketMode, getBracketRequirements } from '../config/brackets-config.js';
8
8
  import { formatError } from '../logger.js';
9
+ /**
10
+ * ★ `brackets.mode` in plugin-config.json is a BINANCE-only lifecycle knob: it
11
+ * is passed to `LiveAdapter` and nowhere else. Reporting it verbatim made a
12
+ * Hyperliquid rig — where `HlBracketCoordinator` attaches legs unconditionally
13
+ * and there is no watcher fallback — render "Brackets off" on a live dashboard
14
+ * whose every position was in fact bracketed. A protection indicator that
15
+ * under-reports is exactly as dangerous as one that over-reports, so the
16
+ * effective venue behaviour wins over the stale config value.
17
+ */
9
18
  export function getBracketConfigTool(_args, deps) {
10
19
  try {
11
20
  const cfg = readPluginConfig(deps?.configPath);
12
- const mode = getBracketMode(cfg);
13
21
  const req = getBracketRequirements(cfg);
22
+ if (deps?.adapter?.bracketsAlwaysEnforced) {
23
+ return {
24
+ mode: 'enforce',
25
+ requireStopLoss: true,
26
+ requireTakeProfit: req.requireTakeProfit,
27
+ venueEnforced: true,
28
+ };
29
+ }
14
30
  return {
15
- mode,
31
+ mode: getBracketMode(cfg),
16
32
  requireStopLoss: req.requireStopLoss,
17
33
  requireTakeProfit: req.requireTakeProfit,
18
34
  };
@@ -4,9 +4,12 @@
4
4
  // the one-rung-at-a-time ladder. Requires valid exchange credentials for
5
5
  // any non-PAPER target. Rebuilds the adapter via runtime.reconnect().
6
6
  //
7
- // The agent should NOT call this tool; PR2 will enforce operator-only scope
8
- // in the skill. PR1 leaves the plugin-side handler functional but unguarded
9
- // because the caller chain is currently controlled end-to-end by the skill.
7
+ // GUARDED at the dispatch site (plugin/src/index.ts registration): every call
8
+ // must carry the `operator_token` provenance proof and is refused by
9
+ // verifyOperatorProvenance without it (audit 2026-07-26 F12). The agent cannot
10
+ // supply that token — chat redaction strips rc_* tokens — so only the
11
+ // dashboard operator path reaches this handler. This module stays guard-free
12
+ // by design: the check lives once, at registration, for all operator tools.
10
13
  import { readPluginConfig } from '../config/plugin-config-io.js';
11
14
  import { validateModeTransition, modeRequiresCredentials } from '../onboarding/mode-ladder.js';
12
15
  import { parseVenue } from '../venues/registry.js';
@@ -40,7 +40,26 @@ export function hlFillToFillEvent(fill, wiring, source) {
40
40
  price === undefined || quantity === undefined || quantity <= 0 ||
41
41
  time === undefined ||
42
42
  (fill.side !== 'A' && fill.side !== 'B')) {
43
- logger.warn(TAG, `Dropping unmappable HL fill (source=${source}): ${JSON.stringify(fill).slice(0, 200)}`);
43
+ // Log the DIAGNOSIS (which identity fields failed + what shape arrived),
44
+ // never the raw payload — fills carry account trading data that doesn't
45
+ // belong in journals.
46
+ const bad = [];
47
+ if (typeof fill.tid !== 'number' || !Number.isFinite(fill.tid))
48
+ bad.push('tid');
49
+ if (typeof fill.oid !== 'number' || !Number.isFinite(fill.oid))
50
+ bad.push('oid');
51
+ if (typeof fill.coin !== 'string' || fill.coin.length === 0)
52
+ bad.push('coin');
53
+ if (price === undefined)
54
+ bad.push('px');
55
+ if (quantity === undefined || quantity <= 0)
56
+ bad.push('sz');
57
+ if (time === undefined)
58
+ bad.push('time');
59
+ if (fill.side !== 'A' && fill.side !== 'B')
60
+ bad.push('side');
61
+ logger.warn(TAG, `Dropping unmappable HL fill (source=${source}): invalid=[${bad.join(',')}] ` +
62
+ `keys=[${Object.keys(fill).join(',')}]`);
44
63
  return null;
45
64
  }
46
65
  return {
@@ -38,6 +38,10 @@ export declare class HyperliquidLiveAdapter extends EventEmitter implements IExc
38
38
  * the exchange-side legs ARE the safety floor. Lazily constructed so that
39
39
  * merely constructing the adapter (registry tests) writes no ledger file. */
40
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;
41
45
  private userStream;
42
46
  private truthCheckTimer;
43
47
  private truthCheckRunning;
@@ -66,6 +66,10 @@ export class HyperliquidLiveAdapter extends EventEmitter {
66
66
  * the exchange-side legs ARE the safety floor. Lazily constructed so that
67
67
  * merely constructing the adapter (registry tests) writes no ledger file. */
68
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;
69
73
  userStream = null;
70
74
  truthCheckTimer = null;
71
75
  truthCheckRunning = false;
@@ -1,12 +1,13 @@
1
1
  // Venue registry — the single seam where a trading venue's LIVE adapter is
2
- // constructed (Phase 0 of docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.1/§7.1).
2
+ // constructed (docs/HYPERLIQUID_INTEGRATION_PLAN.md §5.1/§7.1).
3
3
  //
4
- // Phase 0 scope: Binance is the ONLY venue this build can trade live;
5
- // 'hyperliquid' is a recognised-but-unsupported config value that boot handles
6
- // by falling back to PAPER (never by crashing register() — OpenClaw treats a
7
- // throwing register as "ignored" and the agent silently loses every tool).
8
- // Phase 3 adds the HyperliquidLiveAdapter arm HERE and nowhere else, so the
9
- // boot path never grows a second venue branch.
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: 'signal_reversal';
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 !== 'signal_reversal')
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}`);
@@ -11,4 +11,4 @@ export type { ConditionResult, ConditionContext, ConditionFn, ConditionConfig, E
11
11
  export type { VenueId } from './venues/symbols.js';
12
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
13
  export type { ReadinessStatus, ReadinessPhase, ReadinessCheckId, ReadinessCheck, ReadinessReport, VenueReachabilityOutcome, VenueReachabilityResult, } from './readiness.js';
14
- export { READINESS_CHECK_COPY, GEO_BLOCK_FIX_HINT, REACHABILITY_STALL_FACTOR, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
14
+ export { READINESS_CHECK_COPY, GEO_BLOCK_FIX_HINT, HOST_STALL_FIX_HINT, REACHABILITY_STALL_FACTOR, makeReadinessCheck, deriveOverallReadiness, } from './readiness.js';
@@ -4,4 +4,8 @@ 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
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
- export { READINESS_CHECK_COPY, GEO_BLOCK_FIX_HINT, REACHABILITY_STALL_FACTOR, makeReadinessCheck, deriveOverallReadiness, } from './readiness.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';
@@ -65,6 +65,13 @@ interface CheckCopy {
65
65
  /** The canonical connect-phase checks + their plain-English fixes. Single source
66
66
  * of copy; the plugin inlines these into each report so the webapp stays dumb. */
67
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>;
68
75
  /** Assertive copy for a CONFIRMED geo-block, passed explicitly as the `fixHint`
69
76
  * override. This is the one case where naming the cause is honest: the server
70
77
  * answered 451/403, so we are reporting what it told us — not an inference from
@@ -47,14 +47,34 @@ export const READINESS_CHECK_COPY = {
47
47
  },
48
48
  // The signal that actually matters on a trading rig, and the one that was
49
49
  // invisible while a starved host masqueraded as an unreachable venue
50
- // (issue #265): the agent process was runnable but not getting scheduled for
51
- // minutes at a time. Fed by the event-loop delay the plugin samples locally.
50
+ // (issue #265): the agent process stopped running for seconds at a time. Fed
51
+ // by the event-loop delay the plugin samples locally.
52
+ //
53
+ // ★ This DEFAULT must not name a cause. Event-loop delay is a symptom of two
54
+ // opposite problems — the host starving the process, or the process blocking
55
+ // its own loop — and it alone cannot distinguish them. Asserting "your host is
56
+ // starved, give it more CPU/RAM" sent an operator toward a resize on a box
57
+ // measured at 0.05 load with 1.95s of lifetime runqueue wait (2026-07-29);
58
+ // the freeze was an in-process LLM turn. When the plugin CAN discriminate (it
59
+ // reads /proc/self/schedstat) it passes one of HOST_STALL_FIX_HINT below;
60
+ // this cautious copy is what an unattributable freeze gets. Same rule the
61
+ // reachability hints already follow.
52
62
  host_responsive: {
53
63
  label: 'Agent host responsive',
54
64
  phase: 'connect',
55
- fixHint: 'Your agent host is starved of CPU/memory — the agent process froze for seconds at a time, so order handling, stop attachment and bracket resync can be delayed by minutes. Give the host more CPU/RAM, or reduce what else runs on it (other containers, agents or a leaking process competing for the same box).',
65
+ fixHint: 'The agent process froze for seconds at a time, so order handling, stop attachment and bracket resync can be delayed by that much. Two things cause this and we could not tell them apart on this host: the host being short of CPU/memory (other containers, agents or a leaking process competing for the box), or something inside the agent process blocking it — note the AI agent itself runs in this same process. Check host load first; if it is idle, the freeze came from inside.',
56
66
  },
57
67
  };
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 const HOST_STALL_FIX_HINT = {
75
+ host_starved: 'Your agent host is starved of CPU/memory — the agent process was ready to run but the host kept it waiting, so order handling, stop attachment and bracket resync can be delayed by that much. Give the host more CPU/RAM, or reduce what else runs on it (other containers, agents or a leaking process competing for the same box).',
76
+ self_inflicted: 'The agent process froze for seconds at a time, but the host was NOT short of CPU — it barely waited for one. Something inside the agent process blocked it, and the usual cause is an AI turn: the agent runs in the same process as order handling, so assembling a large turn (long session transcript, big memory database, large injected files) stops trading work while it runs. More CPU/RAM will not fix this. Trim what each turn loads, or reduce turn frequency.',
77
+ };
58
78
  /** Assertive copy for a CONFIRMED geo-block, passed explicitly as the `fixHint`
59
79
  * override. This is the one case where naming the cause is honest: the server
60
80
  * answered 451/403, so we are reporting what it told us — not an inference from
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: reefclaw
3
- version: 0.0.6
3
+ version: 0.0.7
4
4
  description: ReefClaw trading control room — bootstrap (connects your agent; full trading instructions arrive automatically after first connect)
5
5
  author: ReefClaw
6
6
  homepage: https://reefclaw.com
@@ -24,18 +24,13 @@ config:
24
24
  description: ReefClaw relay WebSocket URL. Defaults to the production relay when omitted.
25
25
  ---
26
26
 
27
- <!-- BOOTSTRAP SKILL.md — this is the thin installer copy shipped in the
28
- @reefclaw/connect npm package. It carries ONLY the config schema and the
29
- connect instructions. The FULL trading instructions (versioned 2.x.y) are
30
- delivered automatically by the ReefClaw connector minutes after the first
31
- successful connect, over the authenticated update channel — they replace
32
- this file in place (a 0.0.x version always loses the semver comparison).
33
- The "Connecting (first run)" section below MUST stay in sync with the
34
- same section in skill/SKILL.md. -->
35
-
36
27
  # ReefClaw — Connect Your Agent (Bootstrap)
37
28
 
38
- You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard. This bootstrap file only covers **connecting**; your full trading instructions arrive automatically a few minutes after the connection succeeds.
29
+ ## About this file
30
+
31
+ This is the thin BOOTSTRAP copy shipped in the `@reefclaw/connect` npm package. It covers **installing the ReefClaw plugin and connecting to the user's ReefClaw account** — nothing else. The FULL trading instructions (versioned 2.x.y) are delivered automatically by the ReefClaw connector minutes after the first successful connect, over the authenticated update channel — they replace this file in place (a 0.0.x version always loses the semver comparison). Maintainers: the "Connecting (first run)" section below MUST stay in sync with the same section in `skill/SKILL.md`.
32
+
33
+ You are being connected to **ReefClaw**, a supervised trading control room. Your operator watches everything you do on the ReefClaw dashboard. Some steps below change host configuration or install software; they exist solely to complete the setup **the user initiated by pasting their connect message**. Tell the user what each step changes before you do it, do it only for that user, and revert any temporary permission (like elevated exec) as soon as the install completes.
39
34
 
40
35
  **Do not attempt to trade before the full instructions arrive.** If trading tools are visible but this file is still a bootstrap version (0.0.x), the connection has not completed — finish connecting first.
41
36
 
package/dist/cli.js CHANGED
@@ -89,6 +89,9 @@ function wireConfig(pre) {
89
89
  info('This is required for OpenClaw v2026.4.2+ to keep operator scopes on the WS connection; keep the gateway bound to localhost.');
90
90
  }
91
91
  const merged = mergeReefClawConfig(base);
92
+ // Explicit disclosure: the connect message's rc_ token is persisted locally
93
+ // so the connector can authenticate to the user's relay room across restarts.
94
+ info('Saving your ReefClaw connection settings (including the rc_ token) to ~/.openclaw/openclaw.json — owner-only file permissions (0600).');
92
95
  writeConfig(merged);
93
96
  ok('sandbox allowlist (group:plugins) + gateway scope + reefclaw entry ensured');
94
97
  return merged;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reefclaw/connect",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "One-command installer that connects your OpenClaw agent to ReefClaw (paper trading, no exchange keys).",
5
5
  "type": "module",
6
6
  "bin": {