@reefclaw/openclaw-plugin 0.1.0
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/.gitignore +8 -0
- package/audit/mode-transition-audit.d.ts +11 -0
- package/audit/mode-transition-audit.js +29 -0
- package/balance-utils.d.ts +36 -0
- package/balance-utils.js +98 -0
- package/bridge/bridge.d.ts +109 -0
- package/bridge/bridge.js +1036 -0
- package/bridge/config.d.ts +43 -0
- package/bridge/config.js +176 -0
- package/bridge/connector.d.ts +52 -0
- package/bridge/connector.js +293 -0
- package/bridge/event-replay-buffer.d.ts +43 -0
- package/bridge/event-replay-buffer.js +109 -0
- package/bridge/gateway/event-parser.d.ts +209 -0
- package/bridge/gateway/event-parser.js +793 -0
- package/bridge/gateway/gateway-config.d.ts +39 -0
- package/bridge/gateway/gateway-config.js +86 -0
- package/bridge/gateway/gateway-http-client.d.ts +50 -0
- package/bridge/gateway/gateway-http-client.js +165 -0
- package/bridge/gateway/gateway-ws-client.d.ts +116 -0
- package/bridge/gateway/gateway-ws-client.js +417 -0
- package/bridge/gateway/poller.d.ts +146 -0
- package/bridge/gateway/poller.js +505 -0
- package/bridge/gateway/tool-discovery.d.ts +25 -0
- package/bridge/gateway/tool-discovery.js +198 -0
- package/bridge/index.d.ts +2 -0
- package/bridge/index.js +253 -0
- package/bridge/logger.d.ts +2 -0
- package/bridge/logger.js +2 -0
- package/bridge/provider.d.ts +156 -0
- package/bridge/provider.js +2 -0
- package/bridge/providers/emergency-commands.d.ts +54 -0
- package/bridge/providers/emergency-commands.js +235 -0
- package/bridge/providers/gateway.d.ts +322 -0
- package/bridge/providers/gateway.js +2302 -0
- package/bridge/providers/mock.d.ts +37 -0
- package/bridge/providers/mock.js +385 -0
- package/bridge/providers/onboarding-commands.d.ts +83 -0
- package/bridge/providers/onboarding-commands.js +213 -0
- package/bridge/providers/risk-calculator.d.ts +96 -0
- package/bridge/providers/risk-calculator.js +369 -0
- package/bridge/setup.d.ts +32 -0
- package/bridge/setup.js +226 -0
- package/bridge/types.d.ts +584 -0
- package/bridge/types.js +50 -0
- package/bridge/utils/reconnect.d.ts +6 -0
- package/bridge/utils/reconnect.js +6 -0
- package/bridge/utils/skill-signing.d.ts +51 -0
- package/bridge/utils/skill-signing.js +138 -0
- package/bridge/utils/skill-version.d.ts +17 -0
- package/bridge/utils/skill-version.js +74 -0
- package/ccxt/binance-ban-gate.d.ts +47 -0
- package/ccxt/binance-ban-gate.js +409 -0
- package/ccxt/binance-private.d.ts +325 -0
- package/ccxt/binance-private.js +1415 -0
- package/ccxt/binance-public.d.ts +18 -0
- package/ccxt/binance-public.js +147 -0
- package/config/agent-config-client.d.ts +55 -0
- package/config/agent-config-client.js +145 -0
- package/config/agent-config-poller.d.ts +25 -0
- package/config/agent-config-poller.js +100 -0
- package/config/brackets-config.d.ts +22 -0
- package/config/brackets-config.js +58 -0
- package/config/gate-store.d.ts +18 -0
- package/config/gate-store.js +61 -0
- package/config/plugin-config-io.d.ts +181 -0
- package/config/plugin-config-io.js +84 -0
- package/config/position-review-config.d.ts +35 -0
- package/config/position-review-config.js +105 -0
- package/config/tool-gate.d.ts +53 -0
- package/config/tool-gate.js +125 -0
- package/config/user-data-stream-config.d.ts +85 -0
- package/config/user-data-stream-config.js +224 -0
- package/connector-supervisor.d.ts +36 -0
- package/connector-supervisor.js +149 -0
- package/exchange-adapter.d.ts +49 -0
- package/exchange-adapter.js +4 -0
- package/index.d.ts +30 -0
- package/index.js +2030 -0
- package/ingest/pending-entry-metadata.d.ts +52 -0
- package/ingest/pending-entry-metadata.js +182 -0
- package/ingest/position-auto-capture.d.ts +98 -0
- package/ingest/position-auto-capture.js +394 -0
- package/ingest/position-decisions-client.d.ts +318 -0
- package/ingest/position-decisions-client.js +296 -0
- package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
- package/ingest/reconcile-db-vs-exchange.js +114 -0
- package/ingest/reconciler-cleanup.d.ts +37 -0
- package/ingest/reconciler-cleanup.js +147 -0
- package/ingest/rest-gap-filler.d.ts +191 -0
- package/ingest/rest-gap-filler.js +565 -0
- package/ingest/touched-symbols-store.d.ts +25 -0
- package/ingest/touched-symbols-store.js +96 -0
- package/ingest/trade-store-client.d.ts +40 -0
- package/ingest/trade-store-client.js +125 -0
- package/ingest/ws-ingest.d.ts +43 -0
- package/ingest/ws-ingest.js +126 -0
- package/learning/setup-family.d.ts +21 -0
- package/learning/setup-family.js +103 -0
- package/lifecycle/install-signal-handlers.d.ts +33 -0
- package/lifecycle/install-signal-handlers.js +112 -0
- package/lifecycle/shutdown-coordinator.d.ts +43 -0
- package/lifecycle/shutdown-coordinator.js +131 -0
- package/live/bracket-id.d.ts +18 -0
- package/live/bracket-id.js +81 -0
- package/live/bracket-ledger.d.ts +54 -0
- package/live/bracket-ledger.js +267 -0
- package/live/bracket-manager.d.ts +82 -0
- package/live/bracket-manager.js +478 -0
- package/live/bracket-params.d.ts +22 -0
- package/live/bracket-params.js +124 -0
- package/live/bracket-reconciler.d.ts +95 -0
- package/live/bracket-reconciler.js +573 -0
- package/live/bracket-types.d.ts +102 -0
- package/live/bracket-types.js +8 -0
- package/live/deposit-tracker.d.ts +62 -0
- package/live/deposit-tracker.js +97 -0
- package/live/emergency-controls.d.ts +32 -0
- package/live/emergency-controls.js +226 -0
- package/live/exchange-errors.d.ts +12 -0
- package/live/exchange-errors.js +130 -0
- package/live/exchange-info-cache.d.ts +35 -0
- package/live/exchange-info-cache.js +119 -0
- package/live/fact-subscriber.d.ts +78 -0
- package/live/fact-subscriber.js +182 -0
- package/live/intent-journal.d.ts +42 -0
- package/live/intent-journal.js +122 -0
- package/live/listen-key-manager.d.ts +70 -0
- package/live/listen-key-manager.js +169 -0
- package/live/live-adapter.d.ts +264 -0
- package/live/live-adapter.js +1665 -0
- package/live/live-balance-enricher.d.ts +32 -0
- package/live/live-balance-enricher.js +104 -0
- package/live/live-bracket-api.d.ts +13 -0
- package/live/live-bracket-api.js +20 -0
- package/live/live-state-store.d.ts +194 -0
- package/live/live-state-store.js +450 -0
- package/live/local-signal-service.d.ts +57 -0
- package/live/local-signal-service.js +146 -0
- package/live/local-strategy-evaluator.d.ts +62 -0
- package/live/local-strategy-evaluator.js +127 -0
- package/live/microstructure-assembler.d.ts +54 -0
- package/live/microstructure-assembler.js +148 -0
- package/live/order-poller.d.ts +29 -0
- package/live/order-poller.js +125 -0
- package/live/position-state-store.d.ts +83 -0
- package/live/position-state-store.js +237 -0
- package/live/proposal-decision-listener.d.ts +64 -0
- package/live/proposal-decision-listener.js +288 -0
- package/live/proposal-manager.d.ts +76 -0
- package/live/proposal-manager.js +140 -0
- package/live/rate-limiter.d.ts +47 -0
- package/live/rate-limiter.js +159 -0
- package/live/reconciler.d.ts +39 -0
- package/live/reconciler.js +175 -0
- package/live/setup-buckets.d.ts +7 -0
- package/live/setup-buckets.js +33 -0
- package/live/slippage-tracker.d.ts +45 -0
- package/live/slippage-tracker.js +78 -0
- package/live/stop-watcher.d.ts +34 -0
- package/live/stop-watcher.js +158 -0
- package/live/user-data-active-probe.d.ts +54 -0
- package/live/user-data-active-probe.js +180 -0
- package/live/user-data-stream-controller.d.ts +200 -0
- package/live/user-data-stream-controller.js +579 -0
- package/live/user-data-stream-ws.d.ts +22 -0
- package/live/user-data-stream-ws.js +63 -0
- package/live/user-data-stream.d.ts +243 -0
- package/live/user-data-stream.js +704 -0
- package/logger.d.ts +2 -0
- package/logger.js +2 -0
- package/mfe.d.ts +21 -0
- package/mfe.js +68 -0
- package/onboarding/mode-ladder.d.ts +1 -0
- package/onboarding/mode-ladder.js +3 -0
- package/onboarding/runtime.d.ts +71 -0
- package/onboarding/runtime.js +153 -0
- package/openclaw.plugin.json +94 -0
- package/package.json +27 -0
- package/paper-adapter.d.ts +24 -0
- package/paper-adapter.js +91 -0
- package/persistence/state-manager.d.ts +42 -0
- package/persistence/state-manager.js +164 -0
- package/pinned-plan.d.ts +9 -0
- package/pinned-plan.js +23 -0
- package/risk/pre-trade-check.d.ts +38 -0
- package/risk/pre-trade-check.js +345 -0
- package/risk/pre-trade-types.d.ts +60 -0
- package/risk/pre-trade-types.js +3 -0
- package/scripts/assemble.mjs +114 -0
- package/shadow/shadow-tracker.d.ts +36 -0
- package/shadow/shadow-tracker.js +151 -0
- package/shadow/types.d.ts +42 -0
- package/shadow/types.js +20 -0
- package/shared/indicators-extended.d.ts +52 -0
- package/shared/indicators-extended.js +291 -0
- package/shared/indicators.d.ts +15 -0
- package/shared/indicators.js +114 -0
- package/signals/conditions/registry.d.ts +16 -0
- package/signals/conditions/registry.js +1274 -0
- package/signals/conditions/types.d.ts +1 -0
- package/signals/conditions/types.js +4 -0
- package/signals/direction-rules.d.ts +3 -0
- package/signals/direction-rules.js +24 -0
- package/signals/entry-rules.d.ts +6 -0
- package/signals/entry-rules.js +33 -0
- package/signals/serialize-context.d.ts +4 -0
- package/signals/serialize-context.js +39 -0
- package/signals/stop-rules.d.ts +3 -0
- package/signals/stop-rules.js +48 -0
- package/signals/strategy-adapter.d.ts +14 -0
- package/signals/strategy-adapter.js +122 -0
- package/signals/types.d.ts +1 -0
- package/signals/types.js +8 -0
- package/simulator/exchange-simulator.d.ts +93 -0
- package/simulator/exchange-simulator.js +684 -0
- package/simulator/fill-engine.d.ts +53 -0
- package/simulator/fill-engine.js +276 -0
- package/simulator/paper-market-feed.d.ts +26 -0
- package/simulator/paper-market-feed.js +104 -0
- package/simulator/realistic-fills.d.ts +59 -0
- package/simulator/realistic-fills.js +175 -0
- package/simulator/types.d.ts +219 -0
- package/simulator/types.js +43 -0
- package/skills/reefclaw/SKILL.md +97 -0
- package/strategy/builtin-strategies.d.ts +2 -0
- package/strategy/builtin-strategies.js +109 -0
- package/strategy/condition-registry.d.ts +3 -0
- package/strategy/condition-registry.js +153 -0
- package/strategy/evaluator.d.ts +67 -0
- package/strategy/evaluator.js +93 -0
- package/tools/assessment-validation.d.ts +118 -0
- package/tools/assessment-validation.js +415 -0
- package/tools/attach-brackets.d.ts +34 -0
- package/tools/attach-brackets.js +363 -0
- package/tools/audit-bracket-protection.d.ts +49 -0
- package/tools/audit-bracket-protection.js +527 -0
- package/tools/cancel-all-orders.d.ts +7 -0
- package/tools/cancel-all-orders.js +5 -0
- package/tools/cancel-order.d.ts +10 -0
- package/tools/cancel-order.js +14 -0
- package/tools/check-position-health.d.ts +46 -0
- package/tools/check-position-health.js +194 -0
- package/tools/clear-exchange-credentials.d.ts +24 -0
- package/tools/clear-exchange-credentials.js +70 -0
- package/tools/close-position.d.ts +22 -0
- package/tools/close-position.js +449 -0
- package/tools/create-order.d.ts +54 -0
- package/tools/create-order.js +338 -0
- package/tools/exit-gate.d.ts +58 -0
- package/tools/exit-gate.js +162 -0
- package/tools/fetch-balance.d.ts +5 -0
- package/tools/fetch-balance.js +4 -0
- package/tools/fetch-ohlcv.d.ts +11 -0
- package/tools/fetch-ohlcv.js +8 -0
- package/tools/fetch-open-orders.d.ts +7 -0
- package/tools/fetch-open-orders.js +4 -0
- package/tools/fetch-positions.d.ts +7 -0
- package/tools/fetch-positions.js +4 -0
- package/tools/fetch-ticker.d.ts +11 -0
- package/tools/fetch-ticker.js +5 -0
- package/tools/get-agent-profile.d.ts +4 -0
- package/tools/get-agent-profile.js +6 -0
- package/tools/get-analytics.d.ts +6 -0
- package/tools/get-analytics.js +7 -0
- package/tools/get-backtest.d.ts +12 -0
- package/tools/get-backtest.js +91 -0
- package/tools/get-basis.d.ts +7 -0
- package/tools/get-basis.js +7 -0
- package/tools/get-bracket-config.d.ts +11 -0
- package/tools/get-bracket-config.js +24 -0
- package/tools/get-cascade-risk.d.ts +7 -0
- package/tools/get-cascade-risk.js +8 -0
- package/tools/get-crypto-metrics.d.ts +18 -0
- package/tools/get-crypto-metrics.js +45 -0
- package/tools/get-cvd.d.ts +6 -0
- package/tools/get-cvd.js +6 -0
- package/tools/get-divergences.d.ts +6 -0
- package/tools/get-divergences.js +6 -0
- package/tools/get-funding-context.d.ts +6 -0
- package/tools/get-funding-context.js +16 -0
- package/tools/get-liquidation-levels.d.ts +7 -0
- package/tools/get-liquidation-levels.js +7 -0
- package/tools/get-liquidation-pulse.d.ts +9 -0
- package/tools/get-liquidation-pulse.js +22 -0
- package/tools/get-market-breadth.d.ts +6 -0
- package/tools/get-market-breadth.js +8 -0
- package/tools/get-market-intel.d.ts +19 -0
- package/tools/get-market-intel.js +116 -0
- package/tools/get-market-structure.d.ts +47 -0
- package/tools/get-market-structure.js +198 -0
- package/tools/get-my-mined-patterns.d.ts +20 -0
- package/tools/get-my-mined-patterns.js +61 -0
- package/tools/get-my-proposed-learnings.d.ts +20 -0
- package/tools/get-my-proposed-learnings.js +55 -0
- package/tools/get-my-recent-reviews.d.ts +22 -0
- package/tools/get-my-recent-reviews.js +66 -0
- package/tools/get-orderbook.d.ts +21 -0
- package/tools/get-orderbook.js +32 -0
- package/tools/get-pattern-scan.d.ts +7 -0
- package/tools/get-pattern-scan.js +8 -0
- package/tools/get-regime.d.ts +6 -0
- package/tools/get-regime.js +7 -0
- package/tools/get-relevant-learnings.d.ts +21 -0
- package/tools/get-relevant-learnings.js +65 -0
- package/tools/get-resting-liquidity.d.ts +6 -0
- package/tools/get-resting-liquidity.js +11 -0
- package/tools/get-risk-scenario.d.ts +29 -0
- package/tools/get-risk-scenario.js +47 -0
- package/tools/get-risk-summary.d.ts +51 -0
- package/tools/get-risk-summary.js +118 -0
- package/tools/get-sentiment.d.ts +4 -0
- package/tools/get-sentiment.js +6 -0
- package/tools/get-session-review.d.ts +7 -0
- package/tools/get-session-review.js +8 -0
- package/tools/get-setup-detail.d.ts +7 -0
- package/tools/get-setup-detail.js +303 -0
- package/tools/get-signals.d.ts +15 -0
- package/tools/get-signals.js +54 -0
- package/tools/get-sizing.d.ts +6 -0
- package/tools/get-sizing.js +6 -0
- package/tools/get-trade-feedback.d.ts +7 -0
- package/tools/get-trade-feedback.js +8 -0
- package/tools/get-trade-flow.d.ts +7 -0
- package/tools/get-trade-flow.js +7 -0
- package/tools/get-volume-analysis.d.ts +21 -0
- package/tools/get-volume-analysis.js +74 -0
- package/tools/get-volume-profile.d.ts +7 -0
- package/tools/get-volume-profile.js +7 -0
- package/tools/helpers.d.ts +25 -0
- package/tools/helpers.js +38 -0
- package/tools/intel-api.d.ts +14 -0
- package/tools/intel-api.js +52 -0
- package/tools/intel-cache.d.ts +25 -0
- package/tools/intel-cache.js +133 -0
- package/tools/list-strategies.d.ts +7 -0
- package/tools/list-strategies.js +6 -0
- package/tools/modify-stop.d.ts +17 -0
- package/tools/modify-stop.js +81 -0
- package/tools/modify-target.d.ts +17 -0
- package/tools/modify-target.js +71 -0
- package/tools/propose-learning.d.ts +22 -0
- package/tools/propose-learning.js +65 -0
- package/tools/query-review-outcomes.d.ts +30 -0
- package/tools/query-review-outcomes.js +64 -0
- package/tools/query-trades.d.ts +21 -0
- package/tools/query-trades.js +37 -0
- package/tools/record-position-reviews.d.ts +38 -0
- package/tools/record-position-reviews.js +147 -0
- package/tools/save-strategy.d.ts +16 -0
- package/tools/save-strategy.js +46 -0
- package/tools/scan-pairs.d.ts +18 -0
- package/tools/scan-pairs.js +220 -0
- package/tools/score-setup.d.ts +31 -0
- package/tools/score-setup.js +268 -0
- package/tools/set-bracket-requirement.d.ts +18 -0
- package/tools/set-bracket-requirement.js +81 -0
- package/tools/set-exchange-credentials.d.ts +25 -0
- package/tools/set-exchange-credentials.js +80 -0
- package/tools/set-trading-mode.d.ts +26 -0
- package/tools/set-trading-mode.js +135 -0
- package/tools/test-exchange-credentials.d.ts +16 -0
- package/tools/test-exchange-credentials.js +100 -0
- package/tools/toggle-strategy.d.ts +8 -0
- package/tools/toggle-strategy.js +8 -0
- package/trading-params-cache.d.ts +26 -0
- package/trading-params-cache.js +52 -0
- package/types.d.ts +110 -0
- package/types.js +7 -0
- package/util/plugin-paths.d.ts +3 -0
- package/util/plugin-paths.js +15 -0
|
@@ -0,0 +1,793 @@
|
|
|
1
|
+
// Agent event parser for the OpenClaw gateway.
|
|
2
|
+
// Consumes raw agent events (routed by GatewayWsClient) and transforms them
|
|
3
|
+
// into ProviderEvent[] that the GatewayProvider can fire to the bridge.
|
|
4
|
+
import { logger } from '../logger.js';
|
|
5
|
+
const TAG = 'event-parser';
|
|
6
|
+
// ---- CCXT → ReefClaw mapping functions ----
|
|
7
|
+
const CCXT_STATUS_MAP = {
|
|
8
|
+
open: 'SUBMITTED',
|
|
9
|
+
closed: 'FILLED',
|
|
10
|
+
canceled: 'CANCELLED',
|
|
11
|
+
cancelled: 'CANCELLED', // CCXT has both spellings across exchanges
|
|
12
|
+
expired: 'EXPIRED',
|
|
13
|
+
rejected: 'REJECTED',
|
|
14
|
+
pending: 'PENDING',
|
|
15
|
+
};
|
|
16
|
+
export function mapCcxtStatus(ccxtStatus) {
|
|
17
|
+
const mapped = CCXT_STATUS_MAP[ccxtStatus];
|
|
18
|
+
if (!mapped) {
|
|
19
|
+
logger.warn(TAG, `Unmapped CCXT status: "${ccxtStatus}", using uppercase`);
|
|
20
|
+
}
|
|
21
|
+
return mapped ?? ccxtStatus.toUpperCase();
|
|
22
|
+
}
|
|
23
|
+
export function mapCcxtSide(ccxtSide) {
|
|
24
|
+
return ccxtSide.toLowerCase() === 'sell' ? 'SELL' : 'BUY';
|
|
25
|
+
}
|
|
26
|
+
export function mapCcxtType(ccxtType) {
|
|
27
|
+
return ccxtType.toLowerCase() === 'limit' ? 'LIMIT' : 'MARKET';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Map a CCXT order to a ReefClaw OrderData.
|
|
31
|
+
* Returns null if the order has missing critical fields.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* True when a CCXT order can only REDUCE/CLOSE a position (a protective bracket),
|
|
35
|
+
* never open/add. Detects ALL forms a Binance Futures stop/TP can take:
|
|
36
|
+
* - `reduceOnly` (unified or raw info)
|
|
37
|
+
* - `closePosition` (the position-bound form ReefClaw brackets actually use —
|
|
38
|
+
* see plugin bracket-params.ts; a `reduceOnly`-only check would MISS these
|
|
39
|
+
* and the kill switch would cancel them, leaving a naked position)
|
|
40
|
+
* - a STOP / TAKE_PROFIT order type (protective by nature)
|
|
41
|
+
* The kill switch uses this to preserve protection. Errs toward `true` only on
|
|
42
|
+
* real protective signals; a plain LIMIT/MARKET entry has none → false.
|
|
43
|
+
*/
|
|
44
|
+
export function isProtectiveOrder(ccxt) {
|
|
45
|
+
const o = ccxt;
|
|
46
|
+
if (o.reduceOnly === true || o.info?.reduceOnly === true || o.info?.reduceOnly === 'true')
|
|
47
|
+
return true;
|
|
48
|
+
if (o.info?.closePosition === true || o.info?.closePosition === 'true')
|
|
49
|
+
return true;
|
|
50
|
+
const rawType = String(o.info?.origType ?? o.info?.type ?? o.type ?? '').toUpperCase();
|
|
51
|
+
if (rawType.includes('STOP') || rawType.includes('TAKE_PROFIT'))
|
|
52
|
+
return true;
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
export function mapCcxtOrder(ccxt) {
|
|
56
|
+
if (!ccxt.id || typeof ccxt.id !== 'string') {
|
|
57
|
+
logger.warn(TAG, `CCXT order missing valid ID: ${JSON.stringify(ccxt.id)}`);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
if (!ccxt.symbol || typeof ccxt.symbol !== 'string') {
|
|
61
|
+
logger.warn(TAG, `CCXT order ${ccxt.id} missing symbol`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (ccxt.amount === null || ccxt.amount === undefined) {
|
|
65
|
+
logger.warn(TAG, `CCXT order ${ccxt.id} missing amount`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const now = new Date().toISOString();
|
|
69
|
+
const ts = ccxt.datetime ?? (ccxt.timestamp ? new Date(ccxt.timestamp).toISOString() : now);
|
|
70
|
+
const protective = isProtectiveOrder(ccxt);
|
|
71
|
+
return {
|
|
72
|
+
id: ccxt.id,
|
|
73
|
+
symbol: ccxt.symbol,
|
|
74
|
+
side: mapCcxtSide(ccxt.side),
|
|
75
|
+
type: mapCcxtType(ccxt.type),
|
|
76
|
+
status: mapCcxtStatus(ccxt.status),
|
|
77
|
+
quantity: ccxt.amount,
|
|
78
|
+
filledQuantity: ccxt.filled ?? 0,
|
|
79
|
+
averageFillPrice: ccxt.average ?? 0,
|
|
80
|
+
timeInForce: (ccxt.timeInForce ?? 'GTC').toUpperCase(),
|
|
81
|
+
createdAt: ts,
|
|
82
|
+
updatedAt: ts,
|
|
83
|
+
...(protective && { protective: true }),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build a FillData from a CCXT order (when the order has fills).
|
|
88
|
+
* expectedPrice is the limit price (or average if market order).
|
|
89
|
+
*/
|
|
90
|
+
export function buildFillFromCcxtOrder(ccxt, expectedPrice) {
|
|
91
|
+
if (!ccxt.filled || ccxt.filled <= 0)
|
|
92
|
+
return null;
|
|
93
|
+
const fillPrice = ccxt.average ?? 0;
|
|
94
|
+
if (fillPrice <= 0) {
|
|
95
|
+
logger.warn(TAG, `Order ${ccxt.id} has fill but no average price, skipping fill data`);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const expected = expectedPrice ?? ccxt.price ?? fillPrice;
|
|
99
|
+
const slippage = fillPrice - expected;
|
|
100
|
+
const slippageBps = expected > 0 ? (slippage / expected) * 10_000 : 0;
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
const ts = ccxt.datetime ?? (ccxt.timestamp ? new Date(ccxt.timestamp).toISOString() : now);
|
|
103
|
+
// CCXT exposes `reduceOnly` on the unified order shape for futures; Binance
|
|
104
|
+
// Futures also surfaces it on `info.reduceOnly`. Take the unified value
|
|
105
|
+
// when present, fall back to the raw Binance field, leave undefined when
|
|
106
|
+
// neither is set (legacy paths / non-futures venues).
|
|
107
|
+
const ccxtReduceOnly = ccxt.reduceOnly;
|
|
108
|
+
const rawInfo = ccxt.info;
|
|
109
|
+
const reduceOnly = typeof ccxtReduceOnly === 'boolean'
|
|
110
|
+
? ccxtReduceOnly
|
|
111
|
+
: typeof rawInfo?.reduceOnly === 'boolean'
|
|
112
|
+
? rawInfo.reduceOnly
|
|
113
|
+
: undefined;
|
|
114
|
+
return {
|
|
115
|
+
id: `fill-${ccxt.id}-${ccxt.filled}-${ccxt.timestamp ?? Date.now()}`,
|
|
116
|
+
orderId: ccxt.id,
|
|
117
|
+
symbol: ccxt.symbol,
|
|
118
|
+
side: mapCcxtSide(ccxt.side),
|
|
119
|
+
quantity: ccxt.filled,
|
|
120
|
+
price: fillPrice,
|
|
121
|
+
expectedPrice: expected,
|
|
122
|
+
slippage: +slippage.toFixed(8),
|
|
123
|
+
slippageBps: +slippageBps.toFixed(2),
|
|
124
|
+
fee: ccxt.fee?.cost ?? 0,
|
|
125
|
+
feeCurrency: ccxt.fee?.currency ?? 'USDT',
|
|
126
|
+
timestamp: ts,
|
|
127
|
+
...(reduceOnly !== undefined && { reduceOnly }),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Map a CCXT order into a full OrderUpdatePayload.
|
|
132
|
+
* Returns null if the order has invalid critical fields.
|
|
133
|
+
*/
|
|
134
|
+
export function mapCcxtOrderToUpdate(ccxt, expectedPrice) {
|
|
135
|
+
const order = mapCcxtOrder(ccxt);
|
|
136
|
+
if (!order)
|
|
137
|
+
return null;
|
|
138
|
+
const fill = buildFillFromCcxtOrder(ccxt, expectedPrice);
|
|
139
|
+
return {
|
|
140
|
+
orderId: order.id,
|
|
141
|
+
status: order.status,
|
|
142
|
+
order,
|
|
143
|
+
...(fill && { fill }),
|
|
144
|
+
timestamp: order.updatedAt,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Map a CCXT ticker to a ReefClaw TickerData.
|
|
149
|
+
* Returns null if the ticker has no valid price data.
|
|
150
|
+
*/
|
|
151
|
+
export function mapCcxtTicker(ccxt) {
|
|
152
|
+
if (!ccxt.symbol || typeof ccxt.symbol !== 'string') {
|
|
153
|
+
logger.warn(TAG, `CCXT ticker missing symbol`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (ccxt.last === null && ccxt.bid === null && ccxt.ask === null) {
|
|
157
|
+
logger.warn(TAG, `Ticker ${ccxt.symbol} has no valid prices, skipping`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
symbol: ccxt.symbol,
|
|
162
|
+
lastPrice: ccxt.last ?? 0,
|
|
163
|
+
bidPrice: ccxt.bid ?? 0,
|
|
164
|
+
askPrice: ccxt.ask ?? 0,
|
|
165
|
+
volume24h: ccxt.baseVolume ?? 0,
|
|
166
|
+
change24h: ccxt.change ?? 0,
|
|
167
|
+
change24hPercent: ccxt.percentage ?? 0,
|
|
168
|
+
timestamp: ccxt.datetime ?? new Date().toISOString(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Map a CCXT balance response to ReefClaw balance shape.
|
|
173
|
+
* Extracts the balance for a given currency (default: USDT).
|
|
174
|
+
*
|
|
175
|
+
* On Binance Futures, prefer `info.assets[]` over CCXT's per-asset
|
|
176
|
+
* `free`/`used` fields. Two reasons:
|
|
177
|
+
*
|
|
178
|
+
* 1. Under cross-margin, per-asset `used` from CCXT is structurally 0 —
|
|
179
|
+
* margin is shared across the account, not booked against any one asset.
|
|
180
|
+
* The plugin backfills `free=total` when `used==0`, which sets
|
|
181
|
+
* `available` to the full wallet and hides the actual locked margin.
|
|
182
|
+
*
|
|
183
|
+
* 2. Multi-asset accounts settle realized PnL into a separate `BNFCR` line —
|
|
184
|
+
* negative when net-loss, positive when net-gain. It's independent of the
|
|
185
|
+
* funded stablecoin (USDT/USDC) and must be folded into both wallet and
|
|
186
|
+
* available reads so the dashboard's `Available + Locked = NAV`-style
|
|
187
|
+
* invariant holds rather than the figures reading off the USDC line alone.
|
|
188
|
+
*
|
|
189
|
+
* `info.assets[i].availableBalance` is Binance's authoritative
|
|
190
|
+
* available-after-margin number; `walletBalance` is the wallet-only line
|
|
191
|
+
* (excludes uPnl). We sum these across {funded stablecoin, BNFCR}; the
|
|
192
|
+
* gateway derives `Locked = NAV − Available` so the invariant holds.
|
|
193
|
+
*
|
|
194
|
+
* Falls back to the legacy per-currency / top-level shape when `info.assets`
|
|
195
|
+
* is absent (paper, non-Binance exchanges, mocks).
|
|
196
|
+
*/
|
|
197
|
+
export function mapCcxtBalance(ccxt, currency = 'USDT') {
|
|
198
|
+
const fromInfo = mapBinanceFromInfoAssets(ccxt, currency);
|
|
199
|
+
if (fromInfo)
|
|
200
|
+
return fromInfo;
|
|
201
|
+
// Binance USDⓈ-M Futures settles in USDT or USDC — check both and sum.
|
|
202
|
+
const currencies = currency === 'USDT' ? ['USDT', 'USDC'] : [currency];
|
|
203
|
+
// CCXT balances can be in two shapes:
|
|
204
|
+
// 1. { USDT: { free: 1000, used: 200, total: 1200 } }
|
|
205
|
+
// 2. { free: { USDT: 1000 }, used: { USDT: 200 }, total: { USDT: 1200 } }
|
|
206
|
+
// Try shape 1 first: per-currency objects
|
|
207
|
+
let totalVal = 0, availableVal = 0, lockedVal = 0;
|
|
208
|
+
let foundShape1 = false;
|
|
209
|
+
for (const cur of currencies) {
|
|
210
|
+
const perCurrency = ccxt[cur];
|
|
211
|
+
if (perCurrency && typeof perCurrency === 'object' && 'free' in perCurrency) {
|
|
212
|
+
availableVal += perCurrency.free ?? 0;
|
|
213
|
+
lockedVal += perCurrency.used ?? 0;
|
|
214
|
+
totalVal += perCurrency.total ?? 0;
|
|
215
|
+
foundShape1 = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!foundShape1) {
|
|
219
|
+
// Fallback: top-level free/used/total maps
|
|
220
|
+
const free = ccxt.free;
|
|
221
|
+
const used = ccxt.used;
|
|
222
|
+
const total = ccxt.total;
|
|
223
|
+
for (const cur of currencies) {
|
|
224
|
+
availableVal += free?.[cur] ?? 0;
|
|
225
|
+
lockedVal += used?.[cur] ?? 0;
|
|
226
|
+
totalVal += total?.[cur] ?? 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return { currency, available: availableVal, locked: lockedVal, total: totalVal };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Extract liquidation-risk fields from a CCXT position. Reads the CCXT
|
|
233
|
+
* unified shape first (`position.liquidationPrice`, `position.leverage`,
|
|
234
|
+
* `position.maintenanceMargin`, `position.marginRatio`) and falls back to
|
|
235
|
+
* Binance's raw `info.*` shape — `info.liquidationPrice`, `info.leverage`,
|
|
236
|
+
* `info.maintMargin`, `info.marginRatio` — which arrives as stringy decimals
|
|
237
|
+
* on the futures REST endpoint. Returns an object of optional numbers so
|
|
238
|
+
* callers can spread it directly into the outgoing position payload.
|
|
239
|
+
*
|
|
240
|
+
* Verified against:
|
|
241
|
+
* - CCXT unified position fields (ccxt docs → "position structure")
|
|
242
|
+
* - Binance Futures `/fapi/v2/positionRisk` response
|
|
243
|
+
* (developers.binance.com → derivatives → positionRisk)
|
|
244
|
+
*/
|
|
245
|
+
export function extractLiquidationFields(pos) {
|
|
246
|
+
const info = (pos.info ?? {});
|
|
247
|
+
const num = (v) => {
|
|
248
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0)
|
|
249
|
+
return v;
|
|
250
|
+
if (typeof v === 'string' && v.length > 0) {
|
|
251
|
+
const parsed = parseFloat(v);
|
|
252
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
253
|
+
return parsed;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
};
|
|
257
|
+
const liquidationPrice = num(pos.liquidationPrice) ?? num(info.liquidationPrice);
|
|
258
|
+
const leverage = num(pos.leverage) ?? num(info.leverage);
|
|
259
|
+
const maintenanceMargin = num(pos.maintenanceMargin) ?? num(info.maintMargin) ?? num(info.maintenanceMargin);
|
|
260
|
+
const marginRatio = num(pos.marginRatio) ?? num(info.marginRatio);
|
|
261
|
+
const out = {};
|
|
262
|
+
if (liquidationPrice !== undefined)
|
|
263
|
+
out.liquidationPrice = liquidationPrice;
|
|
264
|
+
if (leverage !== undefined)
|
|
265
|
+
out.leverage = leverage;
|
|
266
|
+
if (maintenanceMargin !== undefined)
|
|
267
|
+
out.maintenanceMargin = maintenanceMargin;
|
|
268
|
+
if (marginRatio !== undefined)
|
|
269
|
+
out.marginRatio = marginRatio;
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Narrow plugin-side `pos.bracket` at the skill/snapshot boundary.
|
|
274
|
+
* The plugin attaches it via LiveAdapter.decoratePositionsWithMetadata when
|
|
275
|
+
* BracketLedger has an active/partial/attaching row for the symbol. The
|
|
276
|
+
* CcxtPosition index signature is `unknown`, so we re-validate shape here
|
|
277
|
+
* before forwarding into ReconciliationSnapshot.
|
|
278
|
+
*
|
|
279
|
+
* Returns undefined when the field is absent (paper mode, no bracket attached,
|
|
280
|
+
* terminal-state bracket) or malformed. The webapp hides bracket lines when
|
|
281
|
+
* the field is undefined.
|
|
282
|
+
*/
|
|
283
|
+
export function extractBracketField(pos) {
|
|
284
|
+
const raw = pos.bracket;
|
|
285
|
+
if (!raw || typeof raw !== 'object')
|
|
286
|
+
return undefined;
|
|
287
|
+
const r = raw;
|
|
288
|
+
if (typeof r.state !== 'string' || r.state.length === 0)
|
|
289
|
+
return undefined;
|
|
290
|
+
const num = (v) => {
|
|
291
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0)
|
|
292
|
+
return v;
|
|
293
|
+
return undefined;
|
|
294
|
+
};
|
|
295
|
+
const slPrice = num(r.slPrice);
|
|
296
|
+
const tpPrice = num(r.tpPrice);
|
|
297
|
+
// A bracket row with neither leg is useless to the UI — treat as no bracket.
|
|
298
|
+
if (slPrice === undefined && tpPrice === undefined)
|
|
299
|
+
return undefined;
|
|
300
|
+
const out = { state: r.state };
|
|
301
|
+
if (slPrice !== undefined)
|
|
302
|
+
out.slPrice = slPrice;
|
|
303
|
+
if (tpPrice !== undefined)
|
|
304
|
+
out.tpPrice = tpPrice;
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Build the wallet/available/locked tuple from Binance Futures `info.assets[]`
|
|
309
|
+
* when present. Returns null when the field is absent or unusable so the
|
|
310
|
+
* caller falls back to legacy per-currency reads.
|
|
311
|
+
*/
|
|
312
|
+
function mapBinanceFromInfoAssets(ccxt, currency) {
|
|
313
|
+
const info = ccxt.info;
|
|
314
|
+
// Prefer Binance's own account-level aggregates when present. From
|
|
315
|
+
// `/fapi/v3/account` (passed through verbatim at `info`): in Multi-Assets
|
|
316
|
+
// Mode `totalWalletBalance` is the USD-denominated wallet across all margin
|
|
317
|
+
// assets, each non-USD collateral valued at Binance's live index — the
|
|
318
|
+
// exact number the app's Futures "Wallet Balance"/"Available" show
|
|
319
|
+
// (verified vs developers.binance.com → Account Information V3,
|
|
320
|
+
// 2026-05-16). The per-asset reconstruction below sums USDC at FACE 1.0,
|
|
321
|
+
// but Binance values it at the live USDC/USD index (~0.9998), leaving the
|
|
322
|
+
// dashboard ~$0.2 permanently high ("figures slightly off always"). Keep
|
|
323
|
+
// `locked = total - available` — identical relationship to the per-asset
|
|
324
|
+
// branch, so the Available/Locked invariant is unchanged.
|
|
325
|
+
if (info && info.totalWalletBalance != null && info.availableBalance != null) {
|
|
326
|
+
const total = parseFloat(String(info.totalWalletBalance));
|
|
327
|
+
const available = parseFloat(String(info.availableBalance));
|
|
328
|
+
if (Number.isFinite(total) && Number.isFinite(available)) {
|
|
329
|
+
return { currency, total, available, locked: Math.max(0, total - available) };
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const assets = info?.assets;
|
|
333
|
+
if (!Array.isArray(assets) || assets.length === 0)
|
|
334
|
+
return null;
|
|
335
|
+
let usdtWallet = 0, usdtAvail = 0;
|
|
336
|
+
let usdcWallet = 0, usdcAvail = 0;
|
|
337
|
+
let bnfcrWallet = 0, bnfcrAvail = 0;
|
|
338
|
+
let sawBnfcrAvail = false;
|
|
339
|
+
let sawAny = false;
|
|
340
|
+
const pickFloat = (v) => {
|
|
341
|
+
const n = parseFloat(String(v ?? 0));
|
|
342
|
+
return Number.isFinite(n) ? n : 0;
|
|
343
|
+
};
|
|
344
|
+
for (const a of assets) {
|
|
345
|
+
if (typeof a?.asset !== 'string')
|
|
346
|
+
continue;
|
|
347
|
+
if (a.asset === 'USDT') {
|
|
348
|
+
usdtWallet = pickFloat(a.walletBalance);
|
|
349
|
+
usdtAvail = pickFloat(a.availableBalance);
|
|
350
|
+
sawAny = true;
|
|
351
|
+
}
|
|
352
|
+
else if (a.asset === 'USDC') {
|
|
353
|
+
usdcWallet = pickFloat(a.walletBalance);
|
|
354
|
+
usdcAvail = pickFloat(a.availableBalance);
|
|
355
|
+
sawAny = true;
|
|
356
|
+
}
|
|
357
|
+
else if (a.asset === 'BNFCR') {
|
|
358
|
+
bnfcrWallet = pickFloat(a.walletBalance);
|
|
359
|
+
bnfcrAvail = pickFloat(a.availableBalance);
|
|
360
|
+
sawBnfcrAvail = a.availableBalance !== undefined && Number.isFinite(parseFloat(String(a.availableBalance)));
|
|
361
|
+
sawAny = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!sawAny)
|
|
365
|
+
return null;
|
|
366
|
+
// Funded stablecoin = whichever line has the larger wallet balance.
|
|
367
|
+
// Avoids summing USDT + USDC (cross-margin can mirror a position's notional
|
|
368
|
+
// into the non-funded line as a phantom).
|
|
369
|
+
const useUsdt = usdtWallet >= usdcWallet;
|
|
370
|
+
const wallet = (useUsdt ? usdtWallet : usdcWallet) + bnfcrWallet;
|
|
371
|
+
const bnfcrAvailableContribution = sawBnfcrAvail && bnfcrAvail !== 0 ? bnfcrAvail : bnfcrWallet;
|
|
372
|
+
const available = (useUsdt ? usdtAvail : usdcAvail) + bnfcrAvailableContribution;
|
|
373
|
+
const locked = Math.max(0, wallet - available);
|
|
374
|
+
return { currency, total: wallet, available, locked };
|
|
375
|
+
}
|
|
376
|
+
// ---- Tool names that produce order-related results ----
|
|
377
|
+
const ORDER_TOOL_NAMES = new Set([
|
|
378
|
+
'create_order', 'ccxt_create_order',
|
|
379
|
+
'cancel_order', 'ccxt_cancel_order',
|
|
380
|
+
'cancel_all_orders', 'ccxt_cancel_all_orders',
|
|
381
|
+
'close_position', 'ccxt_close_position', 'close_all_positions',
|
|
382
|
+
'edit_order', 'ccxt_edit_order',
|
|
383
|
+
]);
|
|
384
|
+
const TICKER_TOOL_NAMES = new Set([
|
|
385
|
+
'fetch_ticker', 'ccxt_fetch_ticker', 'binance_fetch_ticker', 'get_ticker',
|
|
386
|
+
]);
|
|
387
|
+
const BALANCE_TOOL_NAMES = new Set([
|
|
388
|
+
'fetch_balance', 'ccxt_fetch_balance', 'binance_fetch_balance', 'get_balance',
|
|
389
|
+
]);
|
|
390
|
+
/**
|
|
391
|
+
* Check if a tool name matches a known set, handling namespaced names like
|
|
392
|
+
* "reefclaw-paper-trading:create_order" by stripping the prefix.
|
|
393
|
+
*/
|
|
394
|
+
function matchesToolSet(name, knownNames) {
|
|
395
|
+
if (knownNames.has(name))
|
|
396
|
+
return true;
|
|
397
|
+
const colonIdx = name.lastIndexOf(':');
|
|
398
|
+
return colonIdx >= 0 ? knownNames.has(name.substring(colonIdx + 1)) : false;
|
|
399
|
+
}
|
|
400
|
+
// ---- Error rate tracking ----
|
|
401
|
+
const ERROR_WINDOW_MS = 3_600_000; // 1 hour rolling window
|
|
402
|
+
// ---- Assistant buffer expiry ----
|
|
403
|
+
const BUFFER_EXPIRY_MS = 300_000; // 5 minutes — prune abandoned runs
|
|
404
|
+
/** Inactivity timeout — if no new chunks arrive for this long, auto-flush the buffer.
|
|
405
|
+
* This handles the case where OpenClaw's lifecycle 'end' event never arrives
|
|
406
|
+
* (e.g., the run ends without a lifecycle event).
|
|
407
|
+
*
|
|
408
|
+
* NOTE: Set high because agents routinely interleave tool calls with text AND
|
|
409
|
+
* models can pause for extended periods during complex reasoning (30-60s+ between
|
|
410
|
+
* text chunks is normal for long responses). A premature flush splits the message,
|
|
411
|
+
* potentially causing content loss when the webapp merges or replaces partial content.
|
|
412
|
+
* The lifecycle 'end' event is the normal flush trigger — this timer is a LAST RESORT
|
|
413
|
+
* safety net for when 'end' never fires, and should almost never trigger during
|
|
414
|
+
* normal operation. */
|
|
415
|
+
const BUFFER_INACTIVITY_MS = 120_000; // 120 seconds — matches webapp streaming timeout
|
|
416
|
+
// ---- EventParser class ----
|
|
417
|
+
export class EventParser {
|
|
418
|
+
/** Buffered assistant text per runId, with timestamps for expiry */
|
|
419
|
+
assistantBuffers = new Map();
|
|
420
|
+
/** Last decision timestamp (set on lifecycle 'start') */
|
|
421
|
+
lastDecisionTs = null;
|
|
422
|
+
/** Error timestamps within the rolling window */
|
|
423
|
+
errorTimestamps = [];
|
|
424
|
+
/** Symbol for balance currency extraction */
|
|
425
|
+
quoteCurrency;
|
|
426
|
+
/** Callback to emit events from inactivity timer (outside parseAgentEvent flow) */
|
|
427
|
+
onAsyncEvents;
|
|
428
|
+
constructor(quoteCurrency = 'USDT', onAsyncEvents) {
|
|
429
|
+
this.quoteCurrency = quoteCurrency;
|
|
430
|
+
this.onAsyncEvents = onAsyncEvents ?? null;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Parse a raw agent event into zero or more ProviderEvents.
|
|
434
|
+
* GatewayProvider calls this on every 'agent' event from the WS client.
|
|
435
|
+
*/
|
|
436
|
+
parseAgentEvent(payload) {
|
|
437
|
+
const { stream, runId, data } = payload;
|
|
438
|
+
switch (stream) {
|
|
439
|
+
case 'lifecycle':
|
|
440
|
+
return this.handleLifecycle(runId, data);
|
|
441
|
+
case 'assistant':
|
|
442
|
+
return this.handleAssistant(runId, data);
|
|
443
|
+
case 'tool':
|
|
444
|
+
return this.handleTool(data);
|
|
445
|
+
default:
|
|
446
|
+
logger.debug(TAG, `Unknown stream type: ${stream}`);
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
/** Get the last decision timestamp */
|
|
451
|
+
getLastDecision() {
|
|
452
|
+
return this.lastDecisionTs;
|
|
453
|
+
}
|
|
454
|
+
/** Get the rolling error rate (errors per hour) */
|
|
455
|
+
getErrorRate() {
|
|
456
|
+
this.pruneErrorTimestamps();
|
|
457
|
+
return this.errorTimestamps.length;
|
|
458
|
+
}
|
|
459
|
+
// ---- Stream handlers ----
|
|
460
|
+
handleLifecycle(runId, data) {
|
|
461
|
+
const phase = data?.phase;
|
|
462
|
+
if (!phase) {
|
|
463
|
+
logger.debug(TAG, `Lifecycle event missing phase for run ${runId}`);
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
switch (phase) {
|
|
467
|
+
case 'start':
|
|
468
|
+
this.lastDecisionTs = new Date().toISOString();
|
|
469
|
+
logger.debug(TAG, `Agent turn started: ${runId}`);
|
|
470
|
+
return [];
|
|
471
|
+
case 'end': {
|
|
472
|
+
logger.debug(TAG, `Agent turn ended: ${runId}`);
|
|
473
|
+
return this.flushAssistantBuffer(runId);
|
|
474
|
+
}
|
|
475
|
+
case 'error': {
|
|
476
|
+
const errMsg = data.error ?? 'Unknown agent error';
|
|
477
|
+
logger.warn(TAG, `Agent error (run ${runId}): ${errMsg}`);
|
|
478
|
+
this.errorTimestamps.push(Date.now());
|
|
479
|
+
// Still flush the buffer — partial responses are better than nothing
|
|
480
|
+
return this.flushAssistantBuffer(runId);
|
|
481
|
+
}
|
|
482
|
+
default:
|
|
483
|
+
logger.debug(TAG, `Unknown lifecycle phase: ${phase}`);
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
handleAssistant(runId, data) {
|
|
488
|
+
const chunk = data?.delta ?? data?.text ?? '';
|
|
489
|
+
if (!chunk)
|
|
490
|
+
return [];
|
|
491
|
+
// Prune expired buffers on each update to prevent unbounded growth
|
|
492
|
+
this.pruneExpiredBuffers();
|
|
493
|
+
const existing = this.assistantBuffers.get(runId);
|
|
494
|
+
const isFirst = !existing;
|
|
495
|
+
// Clear any existing inactivity timer (we got a new chunk, so reset the clock)
|
|
496
|
+
if (existing?.inactivityTimer) {
|
|
497
|
+
clearTimeout(existing.inactivityTimer);
|
|
498
|
+
}
|
|
499
|
+
const newText = (existing?.text ?? '') + chunk;
|
|
500
|
+
// Set up inactivity timer — if no more chunks arrive within BUFFER_INACTIVITY_MS,
|
|
501
|
+
// auto-flush the buffer and emit chatMessageEnd. This handles cases where the
|
|
502
|
+
// OpenClaw lifecycle 'end' event never fires.
|
|
503
|
+
// If a tool call is active, skip the flush — the tool may take a while and
|
|
504
|
+
// more assistant text will likely follow when it completes.
|
|
505
|
+
const inactivityTimer = setTimeout(() => {
|
|
506
|
+
const entry = this.assistantBuffers.get(runId);
|
|
507
|
+
if (entry) {
|
|
508
|
+
if (entry.toolActive) {
|
|
509
|
+
logger.debug(TAG, `Inactivity timeout for run ${runId} — suppressed (tool in-flight)`);
|
|
510
|
+
// Re-arm the timer: check again after another interval
|
|
511
|
+
entry.inactivityTimer = setTimeout(() => {
|
|
512
|
+
const e2 = this.assistantBuffers.get(runId);
|
|
513
|
+
if (e2 && !e2.toolActive) {
|
|
514
|
+
logger.info(TAG, `Inactivity timeout (re-armed) for run ${runId} — flushing (${e2.text.length} chars)`);
|
|
515
|
+
const events = this.flushAssistantBuffer(runId);
|
|
516
|
+
if (events.length > 0 && this.onAsyncEvents) {
|
|
517
|
+
this.onAsyncEvents(events);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}, BUFFER_INACTIVITY_MS);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
logger.info(TAG, `Inactivity timeout for run ${runId} — auto-flushing assistant buffer (${entry.text.length} chars)`);
|
|
524
|
+
const events = this.flushAssistantBuffer(runId);
|
|
525
|
+
if (events.length > 0 && this.onAsyncEvents) {
|
|
526
|
+
this.onAsyncEvents(events);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}, BUFFER_INACTIVITY_MS);
|
|
530
|
+
this.assistantBuffers.set(runId, { text: newText, lastUpdate: Date.now(), inactivityTimer, toolActive: existing?.toolActive ?? false });
|
|
531
|
+
logger.debug(TAG, `Buffered ${chunk.length} chars for run ${runId} (total: ${newText.length})`);
|
|
532
|
+
const events = [];
|
|
533
|
+
const msgId = `agent-msg-${runId}`;
|
|
534
|
+
// Emit start event on first chunk so UI can create the placeholder message
|
|
535
|
+
if (isFirst) {
|
|
536
|
+
events.push({
|
|
537
|
+
event: 'chatMessageStart',
|
|
538
|
+
data: { id: msgId, runId, timestamp: new Date().toISOString() },
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
// Emit delta chunk for streaming display
|
|
542
|
+
events.push({
|
|
543
|
+
event: 'chatMessageChunk',
|
|
544
|
+
data: { id: msgId, delta: chunk },
|
|
545
|
+
});
|
|
546
|
+
return events;
|
|
547
|
+
}
|
|
548
|
+
handleTool(data) {
|
|
549
|
+
if (!data)
|
|
550
|
+
return [];
|
|
551
|
+
const toolName = data.tool ?? data.name ?? '';
|
|
552
|
+
// Track tool start — mark all active buffers as having a tool in-flight.
|
|
553
|
+
// This prevents the inactivity timer from prematurely flushing the assistant
|
|
554
|
+
// buffer while a tool call is executing (which can take 10-20s).
|
|
555
|
+
if (data.phase === 'start') {
|
|
556
|
+
const events = [];
|
|
557
|
+
for (const [runId, entry] of this.assistantBuffers) {
|
|
558
|
+
if (!entry.toolActive) {
|
|
559
|
+
entry.toolActive = true;
|
|
560
|
+
logger.debug(TAG, `Tool started (${toolName}) — pausing inactivity timer for run ${runId}`);
|
|
561
|
+
}
|
|
562
|
+
// Emit keep-alive so the webapp resets its streaming timeout.
|
|
563
|
+
// Without this, long tool chains (60-120s+) cause the webapp's
|
|
564
|
+
// 120s watchdog to fire and truncate the response mid-stream.
|
|
565
|
+
const msgId = `agent-msg-${runId}`;
|
|
566
|
+
events.push({
|
|
567
|
+
event: 'chatStreamingKeepAlive',
|
|
568
|
+
data: { id: msgId, toolName },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return events;
|
|
572
|
+
}
|
|
573
|
+
// Track tool end — unmark tool-active so the inactivity timer can resume.
|
|
574
|
+
// Reset the inactivity timer for each buffer so it starts fresh after the tool completes.
|
|
575
|
+
if (data.phase === 'end') {
|
|
576
|
+
this.resumeBuffersAfterTool();
|
|
577
|
+
}
|
|
578
|
+
// Only care about tool end events (with results) for data parsing
|
|
579
|
+
if (data.phase !== 'end')
|
|
580
|
+
return [];
|
|
581
|
+
if (!toolName)
|
|
582
|
+
return [];
|
|
583
|
+
const result = data.result;
|
|
584
|
+
if (result === undefined || result === null)
|
|
585
|
+
return [];
|
|
586
|
+
logger.debug(TAG, `Tool end: name="${toolName}", resultType=${typeof result}, keys=${typeof result === 'object' && result !== null ? Object.keys(result).join(',') : 'N/A'}`);
|
|
587
|
+
// Parse the result — it may be a string (double-wrapped JSON) or an object
|
|
588
|
+
let parsed = result;
|
|
589
|
+
if (typeof result === 'string') {
|
|
590
|
+
try {
|
|
591
|
+
parsed = JSON.parse(result);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
logger.debug(TAG, `Tool ${toolName} result is non-JSON string, skipping`);
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// Also unwrap the nested envelope if present (result.content[0].text)
|
|
599
|
+
parsed = unwrapToolEnvelope(parsed);
|
|
600
|
+
// Route based on tool name (supports namespaced names like "plugin:create_order")
|
|
601
|
+
if (matchesToolSet(toolName, ORDER_TOOL_NAMES)) {
|
|
602
|
+
return this.parseOrderResult(toolName, parsed);
|
|
603
|
+
}
|
|
604
|
+
if (matchesToolSet(toolName, TICKER_TOOL_NAMES)) {
|
|
605
|
+
return this.parseTickerResult(parsed);
|
|
606
|
+
}
|
|
607
|
+
if (matchesToolSet(toolName, BALANCE_TOOL_NAMES)) {
|
|
608
|
+
return this.parseBalanceResult(parsed);
|
|
609
|
+
}
|
|
610
|
+
// Unrecognized tool — ignore
|
|
611
|
+
logger.debug(TAG, `Ignoring tool result: ${toolName}`);
|
|
612
|
+
return [];
|
|
613
|
+
}
|
|
614
|
+
// ---- Tool result parsers ----
|
|
615
|
+
parseOrderResult(toolName, result) {
|
|
616
|
+
if (!result || typeof result !== 'object')
|
|
617
|
+
return [];
|
|
618
|
+
// cancel_all_orders may return an array of cancelled orders
|
|
619
|
+
if (Array.isArray(result)) {
|
|
620
|
+
const events = [];
|
|
621
|
+
for (const item of result) {
|
|
622
|
+
if (item && typeof item === 'object' && 'id' in item) {
|
|
623
|
+
const parsed = this.parseSingleOrder(item);
|
|
624
|
+
if (parsed.length > 0)
|
|
625
|
+
events.push(...parsed);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return events;
|
|
629
|
+
}
|
|
630
|
+
// Single order result
|
|
631
|
+
if ('id' in result) {
|
|
632
|
+
return this.parseSingleOrder(result);
|
|
633
|
+
}
|
|
634
|
+
// Defense in depth: some envelopes carry the order in a `details` field
|
|
635
|
+
if ('details' in result) {
|
|
636
|
+
const details = result.details;
|
|
637
|
+
if (details && typeof details === 'object' && 'id' in details) {
|
|
638
|
+
return this.parseSingleOrder(details);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
logger.debug(TAG, `Unexpected order result shape from ${toolName}`);
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
parseSingleOrder(ccxtOrder) {
|
|
645
|
+
try {
|
|
646
|
+
const update = mapCcxtOrderToUpdate(ccxtOrder);
|
|
647
|
+
if (!update)
|
|
648
|
+
return []; // Invalid order, already logged by mapper
|
|
649
|
+
logger.debug(TAG, `Order update: ${update.orderId} → ${update.status}`);
|
|
650
|
+
return [{ event: 'orderUpdate', data: update }];
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
logger.warn(TAG, `Failed to map CCXT order: ${err instanceof Error ? err.message : String(err)}`);
|
|
654
|
+
return [];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
parseTickerResult(result) {
|
|
658
|
+
if (!result || typeof result !== 'object')
|
|
659
|
+
return [];
|
|
660
|
+
try {
|
|
661
|
+
const ticker = mapCcxtTicker(result);
|
|
662
|
+
if (!ticker)
|
|
663
|
+
return []; // Invalid ticker, already logged by mapper
|
|
664
|
+
return [{ event: 'ticker', data: ticker }];
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
logger.warn(TAG, `Failed to map CCXT ticker: ${err instanceof Error ? err.message : String(err)}`);
|
|
668
|
+
return [];
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
parseBalanceResult(_result) {
|
|
672
|
+
// Balance results are handled by the poller (Phase 7f), not the event parser.
|
|
673
|
+
// Tool invocation results for balance pass through here but we don't emit
|
|
674
|
+
// a provider event — the poller computes risk from balance + positions together.
|
|
675
|
+
logger.debug(TAG, 'Balance tool result received (handled by poller)');
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
// ---- Assistant buffer management ----
|
|
679
|
+
/**
|
|
680
|
+
* Called when a tool ends — resume inactivity timers on all active buffers.
|
|
681
|
+
* If more assistant text follows the tool call, the timer will be cleared
|
|
682
|
+
* again by handleAssistant. If not, the buffer flushes after BUFFER_INACTIVITY_MS.
|
|
683
|
+
*/
|
|
684
|
+
resumeBuffersAfterTool() {
|
|
685
|
+
for (const [runId, entry] of this.assistantBuffers) {
|
|
686
|
+
if (!entry.toolActive)
|
|
687
|
+
continue;
|
|
688
|
+
entry.toolActive = false;
|
|
689
|
+
// Reset the inactivity timer to start fresh after tool completion
|
|
690
|
+
if (entry.inactivityTimer) {
|
|
691
|
+
clearTimeout(entry.inactivityTimer);
|
|
692
|
+
}
|
|
693
|
+
entry.inactivityTimer = setTimeout(() => {
|
|
694
|
+
const current = this.assistantBuffers.get(runId);
|
|
695
|
+
if (current && !current.toolActive) {
|
|
696
|
+
logger.info(TAG, `Post-tool inactivity timeout for run ${runId} — flushing (${current.text.length} chars)`);
|
|
697
|
+
const events = this.flushAssistantBuffer(runId);
|
|
698
|
+
if (events.length > 0 && this.onAsyncEvents) {
|
|
699
|
+
this.onAsyncEvents(events);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}, BUFFER_INACTIVITY_MS);
|
|
703
|
+
logger.debug(TAG, `Tool ended — resumed inactivity timer for run ${runId}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
flushAssistantBuffer(runId) {
|
|
707
|
+
const entry = this.assistantBuffers.get(runId);
|
|
708
|
+
if (entry?.inactivityTimer) {
|
|
709
|
+
clearTimeout(entry.inactivityTimer);
|
|
710
|
+
}
|
|
711
|
+
this.assistantBuffers.delete(runId);
|
|
712
|
+
if (!entry || entry.text.trim().length === 0)
|
|
713
|
+
return [];
|
|
714
|
+
const msgId = `agent-msg-${runId}`;
|
|
715
|
+
logger.debug(TAG, `Flushing assistant buffer for run ${runId} (${entry.text.length} chars)`);
|
|
716
|
+
// Emit chatMessageEnd so the UI finalizes the streaming message.
|
|
717
|
+
// Also emit legacy chatMessage for backward compatibility (DB persistence, etc.).
|
|
718
|
+
const message = {
|
|
719
|
+
id: msgId,
|
|
720
|
+
role: 'agent',
|
|
721
|
+
content: entry.text,
|
|
722
|
+
timestamp: new Date().toISOString(),
|
|
723
|
+
};
|
|
724
|
+
return [
|
|
725
|
+
{ event: 'chatMessageEnd', data: { id: msgId, content: entry.text } },
|
|
726
|
+
{ event: 'chatMessage', data: { message } },
|
|
727
|
+
];
|
|
728
|
+
}
|
|
729
|
+
/** Prune buffers older than BUFFER_EXPIRY_MS to prevent memory leaks from abandoned runs */
|
|
730
|
+
pruneExpiredBuffers() {
|
|
731
|
+
const cutoff = Date.now() - BUFFER_EXPIRY_MS;
|
|
732
|
+
for (const [runId, entry] of this.assistantBuffers) {
|
|
733
|
+
if (entry.lastUpdate < cutoff) {
|
|
734
|
+
logger.warn(TAG, `Pruning expired assistant buffer for run ${runId} (${entry.text.length} chars)`);
|
|
735
|
+
if (entry.inactivityTimer)
|
|
736
|
+
clearTimeout(entry.inactivityTimer);
|
|
737
|
+
this.assistantBuffers.delete(runId);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
// ---- Error rate tracking ----
|
|
742
|
+
/** In-place prune of error timestamps outside the rolling window */
|
|
743
|
+
pruneErrorTimestamps() {
|
|
744
|
+
const cutoff = Date.now() - ERROR_WINDOW_MS;
|
|
745
|
+
// Timestamps are appended chronologically, so find first valid index
|
|
746
|
+
let firstValid = 0;
|
|
747
|
+
while (firstValid < this.errorTimestamps.length && this.errorTimestamps[firstValid] <= cutoff) {
|
|
748
|
+
firstValid++;
|
|
749
|
+
}
|
|
750
|
+
if (firstValid > 0) {
|
|
751
|
+
this.errorTimestamps.splice(0, firstValid);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
// ---- Helpers ----
|
|
756
|
+
/**
|
|
757
|
+
* Unwrap the OpenClaw tool envelope if present.
|
|
758
|
+
* Tool results may be double-wrapped: { result: { content: [{ text: "..." }] } }
|
|
759
|
+
*/
|
|
760
|
+
function unwrapToolEnvelope(data) {
|
|
761
|
+
if (!data || typeof data !== 'object')
|
|
762
|
+
return data;
|
|
763
|
+
const obj = data;
|
|
764
|
+
// Check for envelope shape: { result: { content: [{ type: "text", text: "..." }] } }
|
|
765
|
+
if (obj.result && typeof obj.result === 'object') {
|
|
766
|
+
const result = obj.result;
|
|
767
|
+
if (Array.isArray(result.content) && result.content.length > 0) {
|
|
768
|
+
const first = result.content[0];
|
|
769
|
+
if (typeof first.text === 'string') {
|
|
770
|
+
try {
|
|
771
|
+
return JSON.parse(first.text);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return first.text;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// Single-level envelope: { content: [{ type: "text", text: "..." }], details?: ... }
|
|
780
|
+
// (WS agent event stream — ToolResult from plugin)
|
|
781
|
+
if (Array.isArray(obj.content) && obj.content.length > 0) {
|
|
782
|
+
const first = obj.content[0];
|
|
783
|
+
if (typeof first.text === 'string') {
|
|
784
|
+
try {
|
|
785
|
+
return JSON.parse(first.text);
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
return first.text;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return data;
|
|
793
|
+
}
|