@reefclaw/connect 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.
Files changed (415) hide show
  1. package/assets/bridge/bridge.d.ts +96 -0
  2. package/assets/bridge/bridge.js +969 -0
  3. package/assets/bridge/config.d.ts +43 -0
  4. package/assets/bridge/config.js +176 -0
  5. package/assets/bridge/connector.d.ts +52 -0
  6. package/assets/bridge/connector.js +293 -0
  7. package/assets/bridge/event-replay-buffer.d.ts +43 -0
  8. package/assets/bridge/event-replay-buffer.js +109 -0
  9. package/assets/bridge/gateway/event-parser.d.ts +209 -0
  10. package/assets/bridge/gateway/event-parser.js +793 -0
  11. package/assets/bridge/gateway/gateway-config.d.ts +39 -0
  12. package/assets/bridge/gateway/gateway-config.js +86 -0
  13. package/assets/bridge/gateway/gateway-http-client.d.ts +50 -0
  14. package/assets/bridge/gateway/gateway-http-client.js +165 -0
  15. package/assets/bridge/gateway/gateway-ws-client.d.ts +116 -0
  16. package/assets/bridge/gateway/gateway-ws-client.js +417 -0
  17. package/assets/bridge/gateway/poller.d.ts +146 -0
  18. package/assets/bridge/gateway/poller.js +505 -0
  19. package/assets/bridge/gateway/tool-discovery.d.ts +25 -0
  20. package/assets/bridge/gateway/tool-discovery.js +198 -0
  21. package/assets/bridge/index.d.ts +2 -0
  22. package/assets/bridge/index.js +253 -0
  23. package/assets/bridge/logger.d.ts +2 -0
  24. package/assets/bridge/logger.js +2 -0
  25. package/assets/bridge/provider.d.ts +156 -0
  26. package/assets/bridge/provider.js +2 -0
  27. package/assets/bridge/providers/emergency-commands.d.ts +54 -0
  28. package/assets/bridge/providers/emergency-commands.js +235 -0
  29. package/assets/bridge/providers/gateway.d.ts +322 -0
  30. package/assets/bridge/providers/gateway.js +2302 -0
  31. package/assets/bridge/providers/mock.d.ts +37 -0
  32. package/assets/bridge/providers/mock.js +385 -0
  33. package/assets/bridge/providers/onboarding-commands.d.ts +83 -0
  34. package/assets/bridge/providers/onboarding-commands.js +213 -0
  35. package/assets/bridge/providers/risk-calculator.d.ts +96 -0
  36. package/assets/bridge/providers/risk-calculator.js +369 -0
  37. package/assets/bridge/setup.d.ts +32 -0
  38. package/assets/bridge/setup.js +226 -0
  39. package/assets/bridge/types.d.ts +584 -0
  40. package/assets/bridge/types.js +50 -0
  41. package/assets/bridge/utils/reconnect.d.ts +6 -0
  42. package/assets/bridge/utils/reconnect.js +6 -0
  43. package/assets/bridge/utils/skill-signing.d.ts +51 -0
  44. package/assets/bridge/utils/skill-signing.js +138 -0
  45. package/assets/bridge/utils/skill-version.d.ts +17 -0
  46. package/assets/bridge/utils/skill-version.js +71 -0
  47. package/assets/plugin/audit/mode-transition-audit.d.ts +11 -0
  48. package/assets/plugin/audit/mode-transition-audit.js +29 -0
  49. package/assets/plugin/balance-utils.d.ts +36 -0
  50. package/assets/plugin/balance-utils.js +98 -0
  51. package/assets/plugin/ccxt/binance-ban-gate.d.ts +47 -0
  52. package/assets/plugin/ccxt/binance-ban-gate.js +409 -0
  53. package/assets/plugin/ccxt/binance-private.d.ts +325 -0
  54. package/assets/plugin/ccxt/binance-private.js +1415 -0
  55. package/assets/plugin/ccxt/binance-public.d.ts +18 -0
  56. package/assets/plugin/ccxt/binance-public.js +147 -0
  57. package/assets/plugin/config/agent-config-client.d.ts +55 -0
  58. package/assets/plugin/config/agent-config-client.js +145 -0
  59. package/assets/plugin/config/agent-config-poller.d.ts +25 -0
  60. package/assets/plugin/config/agent-config-poller.js +100 -0
  61. package/assets/plugin/config/brackets-config.d.ts +22 -0
  62. package/assets/plugin/config/brackets-config.js +58 -0
  63. package/assets/plugin/config/gate-store.d.ts +18 -0
  64. package/assets/plugin/config/gate-store.js +61 -0
  65. package/assets/plugin/config/plugin-config-io.d.ts +175 -0
  66. package/assets/plugin/config/plugin-config-io.js +84 -0
  67. package/assets/plugin/config/position-review-config.d.ts +35 -0
  68. package/assets/plugin/config/position-review-config.js +105 -0
  69. package/assets/plugin/config/tool-gate.d.ts +53 -0
  70. package/assets/plugin/config/tool-gate.js +125 -0
  71. package/assets/plugin/config/user-data-stream-config.d.ts +85 -0
  72. package/assets/plugin/config/user-data-stream-config.js +224 -0
  73. package/assets/plugin/exchange-adapter.d.ts +49 -0
  74. package/assets/plugin/exchange-adapter.js +4 -0
  75. package/assets/plugin/index.d.ts +30 -0
  76. package/assets/plugin/index.js +1989 -0
  77. package/assets/plugin/ingest/pending-entry-metadata.d.ts +52 -0
  78. package/assets/plugin/ingest/pending-entry-metadata.js +182 -0
  79. package/assets/plugin/ingest/position-auto-capture.d.ts +98 -0
  80. package/assets/plugin/ingest/position-auto-capture.js +394 -0
  81. package/assets/plugin/ingest/position-decisions-client.d.ts +318 -0
  82. package/assets/plugin/ingest/position-decisions-client.js +296 -0
  83. package/assets/plugin/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  84. package/assets/plugin/ingest/reconcile-db-vs-exchange.js +114 -0
  85. package/assets/plugin/ingest/reconciler-cleanup.d.ts +37 -0
  86. package/assets/plugin/ingest/reconciler-cleanup.js +147 -0
  87. package/assets/plugin/ingest/rest-gap-filler.d.ts +191 -0
  88. package/assets/plugin/ingest/rest-gap-filler.js +565 -0
  89. package/assets/plugin/ingest/touched-symbols-store.d.ts +25 -0
  90. package/assets/plugin/ingest/touched-symbols-store.js +96 -0
  91. package/assets/plugin/ingest/trade-store-client.d.ts +40 -0
  92. package/assets/plugin/ingest/trade-store-client.js +125 -0
  93. package/assets/plugin/ingest/ws-ingest.d.ts +43 -0
  94. package/assets/plugin/ingest/ws-ingest.js +126 -0
  95. package/assets/plugin/learning/setup-family.d.ts +21 -0
  96. package/assets/plugin/learning/setup-family.js +103 -0
  97. package/assets/plugin/lifecycle/install-signal-handlers.d.ts +33 -0
  98. package/assets/plugin/lifecycle/install-signal-handlers.js +112 -0
  99. package/assets/plugin/lifecycle/shutdown-coordinator.d.ts +43 -0
  100. package/assets/plugin/lifecycle/shutdown-coordinator.js +131 -0
  101. package/assets/plugin/live/bracket-id.d.ts +18 -0
  102. package/assets/plugin/live/bracket-id.js +81 -0
  103. package/assets/plugin/live/bracket-ledger.d.ts +54 -0
  104. package/assets/plugin/live/bracket-ledger.js +267 -0
  105. package/assets/plugin/live/bracket-manager.d.ts +82 -0
  106. package/assets/plugin/live/bracket-manager.js +478 -0
  107. package/assets/plugin/live/bracket-params.d.ts +22 -0
  108. package/assets/plugin/live/bracket-params.js +124 -0
  109. package/assets/plugin/live/bracket-reconciler.d.ts +95 -0
  110. package/assets/plugin/live/bracket-reconciler.js +573 -0
  111. package/assets/plugin/live/bracket-types.d.ts +102 -0
  112. package/assets/plugin/live/bracket-types.js +8 -0
  113. package/assets/plugin/live/deposit-tracker.d.ts +62 -0
  114. package/assets/plugin/live/deposit-tracker.js +97 -0
  115. package/assets/plugin/live/emergency-controls.d.ts +32 -0
  116. package/assets/plugin/live/emergency-controls.js +226 -0
  117. package/assets/plugin/live/exchange-errors.d.ts +12 -0
  118. package/assets/plugin/live/exchange-errors.js +130 -0
  119. package/assets/plugin/live/exchange-info-cache.d.ts +35 -0
  120. package/assets/plugin/live/exchange-info-cache.js +119 -0
  121. package/assets/plugin/live/fact-subscriber.d.ts +78 -0
  122. package/assets/plugin/live/fact-subscriber.js +182 -0
  123. package/assets/plugin/live/intent-journal.d.ts +42 -0
  124. package/assets/plugin/live/intent-journal.js +122 -0
  125. package/assets/plugin/live/listen-key-manager.d.ts +70 -0
  126. package/assets/plugin/live/listen-key-manager.js +169 -0
  127. package/assets/plugin/live/live-adapter.d.ts +264 -0
  128. package/assets/plugin/live/live-adapter.js +1665 -0
  129. package/assets/plugin/live/live-balance-enricher.d.ts +32 -0
  130. package/assets/plugin/live/live-balance-enricher.js +104 -0
  131. package/assets/plugin/live/live-bracket-api.d.ts +13 -0
  132. package/assets/plugin/live/live-bracket-api.js +20 -0
  133. package/assets/plugin/live/live-state-store.d.ts +194 -0
  134. package/assets/plugin/live/live-state-store.js +450 -0
  135. package/assets/plugin/live/local-signal-service.d.ts +57 -0
  136. package/assets/plugin/live/local-signal-service.js +146 -0
  137. package/assets/plugin/live/local-strategy-evaluator.d.ts +62 -0
  138. package/assets/plugin/live/local-strategy-evaluator.js +127 -0
  139. package/assets/plugin/live/microstructure-assembler.d.ts +54 -0
  140. package/assets/plugin/live/microstructure-assembler.js +148 -0
  141. package/assets/plugin/live/order-poller.d.ts +29 -0
  142. package/assets/plugin/live/order-poller.js +125 -0
  143. package/assets/plugin/live/position-state-store.d.ts +83 -0
  144. package/assets/plugin/live/position-state-store.js +237 -0
  145. package/assets/plugin/live/proposal-decision-listener.d.ts +64 -0
  146. package/assets/plugin/live/proposal-decision-listener.js +288 -0
  147. package/assets/plugin/live/proposal-manager.d.ts +76 -0
  148. package/assets/plugin/live/proposal-manager.js +140 -0
  149. package/assets/plugin/live/rate-limiter.d.ts +47 -0
  150. package/assets/plugin/live/rate-limiter.js +159 -0
  151. package/assets/plugin/live/reconciler.d.ts +39 -0
  152. package/assets/plugin/live/reconciler.js +175 -0
  153. package/assets/plugin/live/setup-buckets.d.ts +7 -0
  154. package/assets/plugin/live/setup-buckets.js +33 -0
  155. package/assets/plugin/live/slippage-tracker.d.ts +45 -0
  156. package/assets/plugin/live/slippage-tracker.js +78 -0
  157. package/assets/plugin/live/stop-watcher.d.ts +34 -0
  158. package/assets/plugin/live/stop-watcher.js +158 -0
  159. package/assets/plugin/live/user-data-active-probe.d.ts +54 -0
  160. package/assets/plugin/live/user-data-active-probe.js +180 -0
  161. package/assets/plugin/live/user-data-stream-controller.d.ts +200 -0
  162. package/assets/plugin/live/user-data-stream-controller.js +579 -0
  163. package/assets/plugin/live/user-data-stream-ws.d.ts +22 -0
  164. package/assets/plugin/live/user-data-stream-ws.js +63 -0
  165. package/assets/plugin/live/user-data-stream.d.ts +243 -0
  166. package/assets/plugin/live/user-data-stream.js +704 -0
  167. package/assets/plugin/logger.d.ts +2 -0
  168. package/assets/plugin/logger.js +2 -0
  169. package/assets/plugin/mfe.d.ts +21 -0
  170. package/assets/plugin/mfe.js +68 -0
  171. package/assets/plugin/onboarding/mode-ladder.d.ts +1 -0
  172. package/assets/plugin/onboarding/mode-ladder.js +3 -0
  173. package/assets/plugin/onboarding/runtime.d.ts +71 -0
  174. package/assets/plugin/onboarding/runtime.js +153 -0
  175. package/assets/plugin/openclaw.plugin.json +91 -0
  176. package/assets/plugin/paper-adapter.d.ts +24 -0
  177. package/assets/plugin/paper-adapter.js +91 -0
  178. package/assets/plugin/persistence/state-manager.d.ts +42 -0
  179. package/assets/plugin/persistence/state-manager.js +164 -0
  180. package/assets/plugin/pinned-plan.d.ts +9 -0
  181. package/assets/plugin/pinned-plan.js +23 -0
  182. package/assets/plugin/risk/pre-trade-check.d.ts +38 -0
  183. package/assets/plugin/risk/pre-trade-check.js +345 -0
  184. package/assets/plugin/risk/pre-trade-types.d.ts +60 -0
  185. package/assets/plugin/risk/pre-trade-types.js +3 -0
  186. package/assets/plugin/shadow/shadow-tracker.d.ts +36 -0
  187. package/assets/plugin/shadow/shadow-tracker.js +151 -0
  188. package/assets/plugin/shadow/types.d.ts +42 -0
  189. package/assets/plugin/shadow/types.js +20 -0
  190. package/assets/plugin/shared/indicators-extended.d.ts +52 -0
  191. package/assets/plugin/shared/indicators-extended.js +291 -0
  192. package/assets/plugin/shared/indicators.d.ts +15 -0
  193. package/assets/plugin/shared/indicators.js +114 -0
  194. package/assets/plugin/signals/conditions/registry.d.ts +16 -0
  195. package/assets/plugin/signals/conditions/registry.js +1274 -0
  196. package/assets/plugin/signals/conditions/types.d.ts +1 -0
  197. package/assets/plugin/signals/conditions/types.js +4 -0
  198. package/assets/plugin/signals/direction-rules.d.ts +3 -0
  199. package/assets/plugin/signals/direction-rules.js +24 -0
  200. package/assets/plugin/signals/entry-rules.d.ts +6 -0
  201. package/assets/plugin/signals/entry-rules.js +33 -0
  202. package/assets/plugin/signals/serialize-context.d.ts +4 -0
  203. package/assets/plugin/signals/serialize-context.js +39 -0
  204. package/assets/plugin/signals/stop-rules.d.ts +3 -0
  205. package/assets/plugin/signals/stop-rules.js +48 -0
  206. package/assets/plugin/signals/strategy-adapter.d.ts +14 -0
  207. package/assets/plugin/signals/strategy-adapter.js +122 -0
  208. package/assets/plugin/signals/types.d.ts +1 -0
  209. package/assets/plugin/signals/types.js +8 -0
  210. package/assets/plugin/simulator/exchange-simulator.d.ts +93 -0
  211. package/assets/plugin/simulator/exchange-simulator.js +684 -0
  212. package/assets/plugin/simulator/fill-engine.d.ts +53 -0
  213. package/assets/plugin/simulator/fill-engine.js +276 -0
  214. package/assets/plugin/simulator/paper-market-feed.d.ts +26 -0
  215. package/assets/plugin/simulator/paper-market-feed.js +104 -0
  216. package/assets/plugin/simulator/realistic-fills.d.ts +59 -0
  217. package/assets/plugin/simulator/realistic-fills.js +175 -0
  218. package/assets/plugin/simulator/types.d.ts +219 -0
  219. package/assets/plugin/simulator/types.js +43 -0
  220. package/assets/plugin/strategy/builtin-strategies.d.ts +2 -0
  221. package/assets/plugin/strategy/builtin-strategies.js +109 -0
  222. package/assets/plugin/strategy/condition-registry.d.ts +3 -0
  223. package/assets/plugin/strategy/condition-registry.js +153 -0
  224. package/assets/plugin/strategy/evaluator.d.ts +67 -0
  225. package/assets/plugin/strategy/evaluator.js +93 -0
  226. package/assets/plugin/tools/assessment-validation.d.ts +118 -0
  227. package/assets/plugin/tools/assessment-validation.js +415 -0
  228. package/assets/plugin/tools/attach-brackets.d.ts +34 -0
  229. package/assets/plugin/tools/attach-brackets.js +363 -0
  230. package/assets/plugin/tools/audit-bracket-protection.d.ts +49 -0
  231. package/assets/plugin/tools/audit-bracket-protection.js +527 -0
  232. package/assets/plugin/tools/cancel-all-orders.d.ts +7 -0
  233. package/assets/plugin/tools/cancel-all-orders.js +5 -0
  234. package/assets/plugin/tools/cancel-order.d.ts +10 -0
  235. package/assets/plugin/tools/cancel-order.js +14 -0
  236. package/assets/plugin/tools/check-position-health.d.ts +46 -0
  237. package/assets/plugin/tools/check-position-health.js +194 -0
  238. package/assets/plugin/tools/clear-exchange-credentials.d.ts +24 -0
  239. package/assets/plugin/tools/clear-exchange-credentials.js +70 -0
  240. package/assets/plugin/tools/close-position.d.ts +22 -0
  241. package/assets/plugin/tools/close-position.js +449 -0
  242. package/assets/plugin/tools/create-order.d.ts +54 -0
  243. package/assets/plugin/tools/create-order.js +338 -0
  244. package/assets/plugin/tools/exit-gate.d.ts +58 -0
  245. package/assets/plugin/tools/exit-gate.js +162 -0
  246. package/assets/plugin/tools/fetch-balance.d.ts +5 -0
  247. package/assets/plugin/tools/fetch-balance.js +4 -0
  248. package/assets/plugin/tools/fetch-ohlcv.d.ts +11 -0
  249. package/assets/plugin/tools/fetch-ohlcv.js +8 -0
  250. package/assets/plugin/tools/fetch-open-orders.d.ts +7 -0
  251. package/assets/plugin/tools/fetch-open-orders.js +4 -0
  252. package/assets/plugin/tools/fetch-positions.d.ts +7 -0
  253. package/assets/plugin/tools/fetch-positions.js +4 -0
  254. package/assets/plugin/tools/fetch-ticker.d.ts +11 -0
  255. package/assets/plugin/tools/fetch-ticker.js +5 -0
  256. package/assets/plugin/tools/get-agent-profile.d.ts +4 -0
  257. package/assets/plugin/tools/get-agent-profile.js +6 -0
  258. package/assets/plugin/tools/get-analytics.d.ts +6 -0
  259. package/assets/plugin/tools/get-analytics.js +7 -0
  260. package/assets/plugin/tools/get-backtest.d.ts +12 -0
  261. package/assets/plugin/tools/get-backtest.js +91 -0
  262. package/assets/plugin/tools/get-basis.d.ts +7 -0
  263. package/assets/plugin/tools/get-basis.js +7 -0
  264. package/assets/plugin/tools/get-bracket-config.d.ts +11 -0
  265. package/assets/plugin/tools/get-bracket-config.js +24 -0
  266. package/assets/plugin/tools/get-cascade-risk.d.ts +7 -0
  267. package/assets/plugin/tools/get-cascade-risk.js +8 -0
  268. package/assets/plugin/tools/get-crypto-metrics.d.ts +18 -0
  269. package/assets/plugin/tools/get-crypto-metrics.js +45 -0
  270. package/assets/plugin/tools/get-cvd.d.ts +6 -0
  271. package/assets/plugin/tools/get-cvd.js +6 -0
  272. package/assets/plugin/tools/get-divergences.d.ts +6 -0
  273. package/assets/plugin/tools/get-divergences.js +6 -0
  274. package/assets/plugin/tools/get-funding-context.d.ts +6 -0
  275. package/assets/plugin/tools/get-funding-context.js +16 -0
  276. package/assets/plugin/tools/get-liquidation-levels.d.ts +7 -0
  277. package/assets/plugin/tools/get-liquidation-levels.js +7 -0
  278. package/assets/plugin/tools/get-liquidation-pulse.d.ts +9 -0
  279. package/assets/plugin/tools/get-liquidation-pulse.js +22 -0
  280. package/assets/plugin/tools/get-market-breadth.d.ts +6 -0
  281. package/assets/plugin/tools/get-market-breadth.js +8 -0
  282. package/assets/plugin/tools/get-market-intel.d.ts +19 -0
  283. package/assets/plugin/tools/get-market-intel.js +116 -0
  284. package/assets/plugin/tools/get-market-structure.d.ts +47 -0
  285. package/assets/plugin/tools/get-market-structure.js +198 -0
  286. package/assets/plugin/tools/get-my-mined-patterns.d.ts +20 -0
  287. package/assets/plugin/tools/get-my-mined-patterns.js +61 -0
  288. package/assets/plugin/tools/get-my-proposed-learnings.d.ts +20 -0
  289. package/assets/plugin/tools/get-my-proposed-learnings.js +55 -0
  290. package/assets/plugin/tools/get-my-recent-reviews.d.ts +22 -0
  291. package/assets/plugin/tools/get-my-recent-reviews.js +66 -0
  292. package/assets/plugin/tools/get-orderbook.d.ts +21 -0
  293. package/assets/plugin/tools/get-orderbook.js +32 -0
  294. package/assets/plugin/tools/get-pattern-scan.d.ts +7 -0
  295. package/assets/plugin/tools/get-pattern-scan.js +8 -0
  296. package/assets/plugin/tools/get-regime.d.ts +6 -0
  297. package/assets/plugin/tools/get-regime.js +7 -0
  298. package/assets/plugin/tools/get-relevant-learnings.d.ts +21 -0
  299. package/assets/plugin/tools/get-relevant-learnings.js +65 -0
  300. package/assets/plugin/tools/get-resting-liquidity.d.ts +6 -0
  301. package/assets/plugin/tools/get-resting-liquidity.js +11 -0
  302. package/assets/plugin/tools/get-risk-scenario.d.ts +29 -0
  303. package/assets/plugin/tools/get-risk-scenario.js +47 -0
  304. package/assets/plugin/tools/get-risk-summary.d.ts +51 -0
  305. package/assets/plugin/tools/get-risk-summary.js +118 -0
  306. package/assets/plugin/tools/get-sentiment.d.ts +4 -0
  307. package/assets/plugin/tools/get-sentiment.js +6 -0
  308. package/assets/plugin/tools/get-session-review.d.ts +7 -0
  309. package/assets/plugin/tools/get-session-review.js +8 -0
  310. package/assets/plugin/tools/get-setup-detail.d.ts +7 -0
  311. package/assets/plugin/tools/get-setup-detail.js +303 -0
  312. package/assets/plugin/tools/get-signals.d.ts +15 -0
  313. package/assets/plugin/tools/get-signals.js +54 -0
  314. package/assets/plugin/tools/get-sizing.d.ts +6 -0
  315. package/assets/plugin/tools/get-sizing.js +6 -0
  316. package/assets/plugin/tools/get-trade-feedback.d.ts +7 -0
  317. package/assets/plugin/tools/get-trade-feedback.js +8 -0
  318. package/assets/plugin/tools/get-trade-flow.d.ts +7 -0
  319. package/assets/plugin/tools/get-trade-flow.js +7 -0
  320. package/assets/plugin/tools/get-volume-analysis.d.ts +21 -0
  321. package/assets/plugin/tools/get-volume-analysis.js +74 -0
  322. package/assets/plugin/tools/get-volume-profile.d.ts +7 -0
  323. package/assets/plugin/tools/get-volume-profile.js +7 -0
  324. package/assets/plugin/tools/helpers.d.ts +25 -0
  325. package/assets/plugin/tools/helpers.js +38 -0
  326. package/assets/plugin/tools/intel-api.d.ts +14 -0
  327. package/assets/plugin/tools/intel-api.js +52 -0
  328. package/assets/plugin/tools/intel-cache.d.ts +25 -0
  329. package/assets/plugin/tools/intel-cache.js +133 -0
  330. package/assets/plugin/tools/list-strategies.d.ts +7 -0
  331. package/assets/plugin/tools/list-strategies.js +6 -0
  332. package/assets/plugin/tools/modify-stop.d.ts +17 -0
  333. package/assets/plugin/tools/modify-stop.js +81 -0
  334. package/assets/plugin/tools/modify-target.d.ts +17 -0
  335. package/assets/plugin/tools/modify-target.js +71 -0
  336. package/assets/plugin/tools/propose-learning.d.ts +22 -0
  337. package/assets/plugin/tools/propose-learning.js +65 -0
  338. package/assets/plugin/tools/query-review-outcomes.d.ts +30 -0
  339. package/assets/plugin/tools/query-review-outcomes.js +64 -0
  340. package/assets/plugin/tools/query-trades.d.ts +21 -0
  341. package/assets/plugin/tools/query-trades.js +37 -0
  342. package/assets/plugin/tools/record-position-reviews.d.ts +38 -0
  343. package/assets/plugin/tools/record-position-reviews.js +147 -0
  344. package/assets/plugin/tools/save-strategy.d.ts +16 -0
  345. package/assets/plugin/tools/save-strategy.js +46 -0
  346. package/assets/plugin/tools/scan-pairs.d.ts +18 -0
  347. package/assets/plugin/tools/scan-pairs.js +220 -0
  348. package/assets/plugin/tools/score-setup.d.ts +31 -0
  349. package/assets/plugin/tools/score-setup.js +268 -0
  350. package/assets/plugin/tools/set-bracket-requirement.d.ts +18 -0
  351. package/assets/plugin/tools/set-bracket-requirement.js +81 -0
  352. package/assets/plugin/tools/set-exchange-credentials.d.ts +25 -0
  353. package/assets/plugin/tools/set-exchange-credentials.js +80 -0
  354. package/assets/plugin/tools/set-trading-mode.d.ts +26 -0
  355. package/assets/plugin/tools/set-trading-mode.js +135 -0
  356. package/assets/plugin/tools/test-exchange-credentials.d.ts +16 -0
  357. package/assets/plugin/tools/test-exchange-credentials.js +100 -0
  358. package/assets/plugin/tools/toggle-strategy.d.ts +8 -0
  359. package/assets/plugin/tools/toggle-strategy.js +8 -0
  360. package/assets/plugin/trading-params-cache.d.ts +26 -0
  361. package/assets/plugin/trading-params-cache.js +52 -0
  362. package/assets/plugin/types.d.ts +110 -0
  363. package/assets/plugin/types.js +7 -0
  364. package/assets/plugin/util/plugin-paths.d.ts +3 -0
  365. package/assets/plugin/util/plugin-paths.js +15 -0
  366. package/assets/shared/fills.d.ts +36 -0
  367. package/assets/shared/fills.js +20 -0
  368. package/assets/shared/index.d.ts +10 -0
  369. package/assets/shared/index.js +5 -0
  370. package/assets/shared/logger.d.ts +10 -0
  371. package/assets/shared/logger.js +34 -0
  372. package/assets/shared/protocol.d.ts +29 -0
  373. package/assets/shared/protocol.js +14 -0
  374. package/assets/shared/redact.d.ts +6 -0
  375. package/assets/shared/redact.js +31 -0
  376. package/assets/shared/shared/indicators-extended.d.ts +52 -0
  377. package/assets/shared/shared/indicators-extended.js +284 -0
  378. package/assets/shared/shared/indicators.d.ts +15 -0
  379. package/assets/shared/shared/indicators.js +107 -0
  380. package/assets/shared/signals/conditions/registry.d.ts +16 -0
  381. package/assets/shared/signals/conditions/registry.js +1267 -0
  382. package/assets/shared/signals/conditions/types.d.ts +65 -0
  383. package/assets/shared/signals/conditions/types.js +8 -0
  384. package/assets/shared/signals/direction-rules.d.ts +3 -0
  385. package/assets/shared/signals/direction-rules.js +18 -0
  386. package/assets/shared/signals/entry-rules.d.ts +6 -0
  387. package/assets/shared/signals/entry-rules.js +26 -0
  388. package/assets/shared/signals/indicators-extended.d.ts +52 -0
  389. package/assets/shared/signals/indicators-extended.js +284 -0
  390. package/assets/shared/signals/indicators.d.ts +15 -0
  391. package/assets/shared/signals/indicators.js +107 -0
  392. package/assets/shared/signals/serialize-context.d.ts +4 -0
  393. package/assets/shared/signals/serialize-context.js +41 -0
  394. package/assets/shared/signals/stop-rules.d.ts +3 -0
  395. package/assets/shared/signals/stop-rules.js +41 -0
  396. package/assets/shared/signals/strategy-adapter.d.ts +14 -0
  397. package/assets/shared/signals/strategy-adapter.js +117 -0
  398. package/assets/shared/signals/types.d.ts +187 -0
  399. package/assets/shared/signals/types.js +9 -0
  400. package/assets/shared/trading-mode.d.ts +16 -0
  401. package/assets/shared/trading-mode.js +49 -0
  402. package/assets/skill/SKILL.md +1357 -0
  403. package/dist/bridge.js +25 -0
  404. package/dist/bundle.js +33 -0
  405. package/dist/cli.js +120 -0
  406. package/dist/daemon.js +104 -0
  407. package/dist/deps.js +41 -0
  408. package/dist/exec.js +28 -0
  409. package/dist/openclaw.js +153 -0
  410. package/dist/paths.js +23 -0
  411. package/dist/plugin.js +48 -0
  412. package/dist/skill.js +35 -0
  413. package/dist/ui.js +29 -0
  414. package/dist/validate.js +28 -0
  415. package/package.json +32 -0
@@ -0,0 +1,969 @@
1
+ // Bridge: wires an OpenClawProvider to a Connector.
2
+ // Provider events → EventFrames with seq tracking → Connector → Relay → Browser.
3
+ // Incoming requests from Connector → routed to Provider methods.
4
+ import { writeFileSync, readFileSync, renameSync, existsSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
5
+ import { join } from 'path';
6
+ import { homedir } from 'os';
7
+ import { logger } from './logger.js';
8
+ import { Connector } from './connector.js';
9
+ import { readLocalSkillVersion, validateSkillContent, compareSemver } from './utils/skill-version.js';
10
+ import { verifySkillSignature, signatureRequired, readLastAppliedSignedAtMs, recordAppliedSignedAt, } from './utils/skill-signing.js';
11
+ import { isTradingMode, validateModeTransition, redactTokens } from '@reefclaw/shared';
12
+ import { OPERATOR_WRITE_METHODS } from './types.js';
13
+ import { EventReplayBuffer } from './event-replay-buffer.js';
14
+ const TAG = 'bridge';
15
+ const AUDIT_TAG = 'bridge-audit';
16
+ /** Write a structured audit log line. Goes through the normal logger today —
17
+ * a future PR may tee this to a dedicated audit sink. Must NEVER include raw
18
+ * secrets; use the `fingerprint` field in payload instead of `secret`. */
19
+ function audit(event, payload) {
20
+ // JSON-encoded payload keeps fields greppable and machine-parseable without
21
+ // a dedicated audit schema. Secrets must be redacted BEFORE calling this.
22
+ logger.info(AUDIT_TAG, `${event} ${JSON.stringify(payload)}`);
23
+ }
24
+ /** Best-effort redaction helper — first 4 + last 2 chars, or `***` for short
25
+ * strings. Mirrors plugin/src/config/plugin-config-io.ts:redactCredential. */
26
+ function fingerprint(value) {
27
+ if (!value)
28
+ return '(empty)';
29
+ if (value.length <= 8)
30
+ return '***';
31
+ return `${value.slice(0, 4)}…${value.slice(-2)}`;
32
+ }
33
+ // ---- Event priority throttler ----
34
+ // Critical events (trades, risk, chat, fill errors) are sent immediately.
35
+ // Low-priority events (ticker, candles, market structure, crypto metrics, volume, regime,
36
+ // signals, missions, analytics, decision traces, shadows) are coalesced: only the latest
37
+ // value per event type is kept, flushed every THROTTLE_INTERVAL_MS.
38
+ const THROTTLE_INTERVAL_MS = 500; // Flush low-priority events every 500ms
39
+ /** Events that must NEVER be dropped or delayed */
40
+ const CRITICAL_EVENTS = new Set([
41
+ 'order_update', // Trade fills, submissions, cancellations
42
+ 'fill_error', // Execution failures
43
+ 'risk_update', // Risk breach alerts
44
+ 'trade_journal', // Trade journal entries
45
+ 'state_update', // Agent mode changes (ACTIVE/PAUSED/STOPPED)
46
+ 'trading_mode', // Paper/live mode changes
47
+ 'skill_update_applied', // OTA SKILL.md confirmation
48
+ // All chat events are on the 'chat' channel and always sent immediately
49
+ ]);
50
+ export class Bridge {
51
+ provider;
52
+ connector;
53
+ seqs = {
54
+ market_data: 0,
55
+ trade_events: 0,
56
+ agent_state: 0,
57
+ chat: 0,
58
+ };
59
+ // Throttle buffer: keyed by "channel:event", holds latest payload
60
+ throttleBuffer = new Map();
61
+ throttleTimer = null;
62
+ /**
63
+ * Bounded ring buffer of every frame this bridge has emitted to the relay.
64
+ * On a `reconcile` request the bridge populates `snapshot.gapFill` with
65
+ * everything the browser hasn't seen yet (per-channel `seq > lastSeq`).
66
+ * Defaults: 500 events × 4 channels × 5 min age cap. See
67
+ * event-replay-buffer.ts for the rationale.
68
+ */
69
+ replayBuffer = new EventReplayBuffer();
70
+ /** Current local SKILL.md version (from frontmatter) */
71
+ currentSkillVersion;
72
+ /** Last trading mode observed from either a tradingMode event or a
73
+ * successful set_trading_mode outcome. Used by the skill-side ladder
74
+ * check so the dashboard can be rejected quickly without a plugin
75
+ * round-trip. `null` means "unknown yet" — the plugin is the authority. */
76
+ lastKnownTradingMode = null;
77
+ // Bound listeners for cleanup
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ listeners = [];
80
+ constructor(provider, connectorConfig) {
81
+ this.provider = provider;
82
+ this.currentSkillVersion = readLocalSkillVersion();
83
+ logger.info(TAG, `Local SKILL.md version: ${this.currentSkillVersion ?? 'unknown'}`);
84
+ this.connector = new Connector(connectorConfig, {
85
+ onRequest: (frame) => this.handleRequest(frame),
86
+ onStateChange: (state) => this.handleStateChange(state),
87
+ onConnectAck: (payload) => this.handleConnectAck(payload),
88
+ });
89
+ }
90
+ /** Start the bridge: connect to relay + start provider */
91
+ start() {
92
+ logger.info(TAG, 'Starting bridge');
93
+ this.wireProviderEvents();
94
+ this.startThrottleTimer();
95
+ this.provider.start();
96
+ this.connector.connect();
97
+ }
98
+ /** Stop the bridge: disconnect + stop provider */
99
+ stop() {
100
+ logger.info(TAG, 'Stopping bridge');
101
+ this.stopThrottleTimer();
102
+ this.unwireProviderEvents();
103
+ this.connector.destroy();
104
+ this.provider.stop();
105
+ }
106
+ // ---- Wire provider events to connector ----
107
+ wireProviderEvents() {
108
+ this.addListener('ticker', (data) => {
109
+ // Wrap in MarketDataEvent format expected by webapp
110
+ this.emit('market_data', 'ticker', {
111
+ symbol: data.symbol,
112
+ ticker: data,
113
+ timestamp: data.timestamp,
114
+ });
115
+ });
116
+ this.addListener('candle', (data) => {
117
+ this.emit('market_data', 'candle', data);
118
+ });
119
+ this.addListener('orderUpdate', (data) => {
120
+ this.emit('trade_events', 'order_update', data);
121
+ });
122
+ this.addListener('agentState', (data) => {
123
+ this.emit('agent_state', 'state_update', data);
124
+ });
125
+ this.addListener('riskUpdate', (data) => {
126
+ this.emit('agent_state', 'risk_update', data);
127
+ });
128
+ this.addListener('chatMessage', (data) => {
129
+ this.emit('chat', 'message', data);
130
+ });
131
+ // Streaming chat events
132
+ this.addListener('chatMessageStart', (data) => {
133
+ this.emit('chat', 'message_start', data);
134
+ });
135
+ this.addListener('chatMessageChunk', (data) => {
136
+ this.emit('chat', 'message_chunk', data);
137
+ });
138
+ this.addListener('chatMessageEnd', (data) => {
139
+ this.emit('chat', 'message_end', data);
140
+ });
141
+ this.addListener('chatStreamingKeepAlive', (data) => {
142
+ this.emit('chat', 'streaming_keepalive', data);
143
+ });
144
+ this.addListener('marketStructure', (data) => {
145
+ this.emit('agent_state', 'market_structure', {
146
+ event: 'market_structure',
147
+ ...data,
148
+ });
149
+ });
150
+ this.addListener('cryptoMetrics', (data) => {
151
+ this.emit('agent_state', 'crypto_metrics', {
152
+ event: 'crypto_metrics',
153
+ ...data,
154
+ });
155
+ });
156
+ this.addListener('volumeAnalysis', (data) => {
157
+ this.emit('agent_state', 'volume_analysis', {
158
+ event: 'volume_analysis',
159
+ ...data,
160
+ });
161
+ });
162
+ this.addListener('tradeJournal', (data) => {
163
+ this.emit('trade_events', 'trade_journal', {
164
+ event: 'trade_journal',
165
+ ...data,
166
+ });
167
+ });
168
+ this.addListener('regimeUpdate', (data) => {
169
+ this.emit('agent_state', 'regime_update', data);
170
+ });
171
+ this.addListener('signalUpdate', (data) => {
172
+ this.emit('agent_state', 'signal_update', data);
173
+ });
174
+ this.addListener('missionUpdate', (data) => {
175
+ this.emit('agent_state', 'mission_update', data);
176
+ });
177
+ this.addListener('analyticsUpdate', (data) => {
178
+ this.emit('agent_state', 'analytics_update', data);
179
+ });
180
+ this.addListener('fillError', (data) => {
181
+ this.emit('agent_state', 'fill_error', {
182
+ event: 'fill_error',
183
+ orderId: data.orderId,
184
+ symbol: data.symbol,
185
+ error: data.error,
186
+ timestamp: data.timestamp,
187
+ });
188
+ });
189
+ // Phase 9b: Shadow mode events
190
+ // Normalize side/type from CCXT lowercase to protocol uppercase
191
+ this.addListener('shadowComparison', (data) => {
192
+ this.emit('agent_state', 'shadow_comparison', {
193
+ ...data,
194
+ side: data.side.toUpperCase(),
195
+ type: data.type.toUpperCase(),
196
+ });
197
+ });
198
+ this.addListener('tradingModeUpdate', (data) => {
199
+ // Keep the skill-side ladder cache in sync with the plugin's view.
200
+ this.lastKnownTradingMode = data.mode;
201
+ this.emit('agent_state', 'trading_mode', data);
202
+ });
203
+ // Phase 9c: Decision trace events
204
+ this.addListener('decisionTraceUpdate', (data) => {
205
+ this.emit('agent_state', 'decision_trace', data);
206
+ });
207
+ }
208
+ addListener(event, fn) {
209
+ this.provider.on(event, fn);
210
+ this.listeners.push({ event, fn });
211
+ }
212
+ unwireProviderEvents() {
213
+ for (const { event, fn } of this.listeners) {
214
+ this.provider.off(event, fn);
215
+ }
216
+ this.listeners.length = 0;
217
+ }
218
+ // ---- Emit event frames with seq tracking + priority throttling ----
219
+ emit(channel, event, payload) {
220
+ // Deliberately NO `isConnected()` short-circuit. Every emitted event must
221
+ // reach `sendFrame` so it lands in the replay buffer — that buffer is
222
+ // what we replay via `gapFill` when a browser reconciles after the
223
+ // skill→relay link recovers from a transient drop. The wire-side send
224
+ // inside sendFrame is gated by the connector and no-ops cleanly when the
225
+ // socket is closed, so emitting while disconnected is safe.
226
+ // Critical events and all chat events: send immediately, never throttle
227
+ if (channel === 'chat' || CRITICAL_EVENTS.has(event)) {
228
+ this.sendFrame(channel, event, payload);
229
+ return;
230
+ }
231
+ // Low-priority events: coalesce in buffer, flush periodically.
232
+ // Key by symbol when present so multi-symbol data doesn't overwrite each other.
233
+ let key = `${channel}:${event}`;
234
+ if (payload && typeof payload === 'object' && 'symbol' in payload) {
235
+ key = `${channel}:${event}:${payload.symbol}`;
236
+ }
237
+ this.throttleBuffer.set(key, { channel, event, payload });
238
+ }
239
+ /** Send a frame to the relay AND record it in the replay buffer.
240
+ *
241
+ * Both writes happen unconditionally:
242
+ * - replayBuffer.record() is the canonical wire-history; gapFill draws
243
+ * from it on the next reconcile, even if this frame's wire send was
244
+ * a no-op because the connector was disconnected.
245
+ * - connector.sendEvent() internally checks readyState and silently
246
+ * no-ops when the socket is closed (connector.ts:164), so calling it
247
+ * during a disconnect is safe and we don't have to mirror the gate
248
+ * here. */
249
+ sendFrame(channel, event, payload) {
250
+ const seq = this.seqs[channel]++;
251
+ const frame = {
252
+ type: 'event',
253
+ event,
254
+ channel,
255
+ payload,
256
+ seq,
257
+ };
258
+ this.replayBuffer.record(channel, event, payload, seq);
259
+ this.connector.sendEvent(frame);
260
+ }
261
+ /** Flush all buffered low-priority events (latest value per type).
262
+ *
263
+ * Drains through sendFrame regardless of connector state — the replay
264
+ * buffer is the authoritative record and must capture coalesced events
265
+ * even when the link is down. The wire send inside sendFrame is a no-op
266
+ * until the connector reconnects, at which point new events flow live and
267
+ * the offline coalesced ones are available via gapFill on reconcile. */
268
+ flushThrottleBuffer() {
269
+ if (this.throttleBuffer.size === 0)
270
+ return;
271
+ for (const [, { channel, event, payload }] of this.throttleBuffer) {
272
+ this.sendFrame(channel, event, payload);
273
+ }
274
+ this.throttleBuffer.clear();
275
+ }
276
+ startThrottleTimer() {
277
+ this.stopThrottleTimer();
278
+ this.throttleTimer = setInterval(() => this.flushThrottleBuffer(), THROTTLE_INTERVAL_MS);
279
+ }
280
+ stopThrottleTimer() {
281
+ if (this.throttleTimer) {
282
+ clearInterval(this.throttleTimer);
283
+ this.throttleTimer = null;
284
+ }
285
+ // Flush remaining on stop
286
+ this.flushThrottleBuffer();
287
+ }
288
+ // ---- Handle incoming requests ----
289
+ async handleRequest(frame) {
290
+ const { id, method, params } = frame;
291
+ try {
292
+ // ---- Operator-write scope gate ----
293
+ // Methods in OPERATOR_WRITE_METHODS require an elevated scope. Today,
294
+ // all connected sessions inherit operator.write from the relay-level
295
+ // authentication (Clerk-issued token), so this check is effectively a
296
+ // whitelist — it exists so a future PR can attach per-session roles
297
+ // without restructuring the dispatcher. Emergency methods are on the
298
+ // list for the same reason (they are already covered today, but the
299
+ // list is the forward-compatible gate).
300
+ if (OPERATOR_WRITE_METHODS.has(method) && !this.hasOperatorWriteScope()) {
301
+ audit('operator_write.denied', { method, id });
302
+ this.connector.sendResponse(id, false, undefined, {
303
+ code: 403,
304
+ message: 'operator.write scope required',
305
+ });
306
+ return;
307
+ }
308
+ if (method.startsWith('emergency.')) {
309
+ const action = method.split('.')[1];
310
+ logger.info(TAG, `Emergency command: ${action}`);
311
+ const result = await this.provider.executeEmergency(action);
312
+ this.connector.sendResponse(id, true, result);
313
+ return;
314
+ }
315
+ if (method === 'reconcile') {
316
+ logger.info(TAG, 'Reconciliation requested');
317
+ const snapshot = await this.provider.getSnapshot();
318
+ const lastSequences = parseLastSequences(params);
319
+ const gapFill = this.replayBuffer
320
+ .sinceMany(lastSequences)
321
+ .map((e) => ({
322
+ type: 'event',
323
+ channel: e.channel,
324
+ event: e.event,
325
+ payload: e.payload,
326
+ seq: e.seq,
327
+ }));
328
+ if (gapFill.length > 0) {
329
+ logger.info(TAG, `gapFill: ${gapFill.length} events replayed (lastSeqs=${JSON.stringify(lastSequences)})`);
330
+ }
331
+ // Provider hardcodes gapFill: []; bridge owns the wire-side seq
332
+ // counter so it is the canonical source. Overwrite unconditionally.
333
+ const enriched = { ...snapshot, gapFill };
334
+ this.connector.sendResponse(id, true, enriched);
335
+ return;
336
+ }
337
+ if (method === 'chat.send') {
338
+ const rawContent = params?.content ?? '';
339
+ // Redact rc_* tokens BEFORE we touch the message anywhere downstream:
340
+ // - the log line below
341
+ // - the provider (which may forward to the OpenClaw agent's session
342
+ // JSONL, the mock provider's stdout, etc.)
343
+ // Onboarding directs users to paste their token into the local
344
+ // OpenClaw terminal; a confused operator pasting into the dashboard
345
+ // chat lands here, and the webapp + relay have already auto-revoked
346
+ // the token by now. Forwarding the raw bytes any further has no
347
+ // legitimate purpose — the agent can react to "rc_***" the same way
348
+ // it would react to the original message.
349
+ const safeContent = redactTokens(rawContent);
350
+ if (safeContent !== rawContent) {
351
+ logger.warn(TAG, 'Chat contained a connection token — webapp/relay should have auto-revoked. ' +
352
+ 'If the agent is still connected after this, generate a new token in Settings.');
353
+ }
354
+ // Approval Mode Phase C — when the operator is discussing a pending
355
+ // proposal in the chat panel, the webapp attaches a compact blob so
356
+ // we can prefix a context note. The agent then knows which trade
357
+ // the operator is asking about for THIS turn, and may emit a new
358
+ // create_order with `supersedes_id` to modify the proposal.
359
+ // Fail-open: if the context is malformed, we just pass through the
360
+ // raw chat content without crashing the bridge.
361
+ let augmentedContent = safeContent;
362
+ const proposalContext = params?.proposalContext;
363
+ if (proposalContext && typeof proposalContext === 'object') {
364
+ try {
365
+ const c = proposalContext;
366
+ const header = `[Operator is discussing your pending proposal — ` +
367
+ `id=${c.id} ${c.symbol} ${c.side} size=${c.size} ` +
368
+ `entry=${c.proposedEntry} stop=${c.stopPrice} target=${c.targetPrice} ` +
369
+ `risk=$${c.riskUsd} setup=${c.setupType} regime=${c.regime} ` +
370
+ `conf=${c.confluenceScore}. Thesis: ${String(c.thesis ?? '').slice(0, 200)}. ` +
371
+ `If they want a modification, use create_order with supersedes_id=${c.id}.]\n\n`;
372
+ augmentedContent = header + safeContent;
373
+ }
374
+ catch (err) {
375
+ logger.warn(TAG, `Failed to render proposalContext header: ${String(err)}`);
376
+ }
377
+ }
378
+ // Phase 4 learning curation — operator may be discussing a learning
379
+ // rule (confirmed / hypothesis / retired). The webapp pins the
380
+ // learning via the LearningsTab 💬 button and attaches a compact
381
+ // blob with the learning's identity, directive, trigger, and
382
+ // mined-pattern evidence stats. The agent's job in that turn is to
383
+ // help the operator refine the rule — soften/tighten the trigger,
384
+ // re-word the directive, point out gaps in evidence, or argue for
385
+ // retiring/re-confirming. The agent does NOT mutate the learning
386
+ // directly; concrete edits flow through the operator's Edit dialog.
387
+ // Independent from proposalContext — both may fire on the same turn.
388
+ const learningContext = params?.learningContext;
389
+ if (learningContext && typeof learningContext === 'object') {
390
+ try {
391
+ const l = learningContext;
392
+ const trig = l.triggerCondition && typeof l.triggerCondition === 'object'
393
+ ? Object.entries(l.triggerCondition)
394
+ .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join('|') : String(v)}`)
395
+ .join(' ')
396
+ : '∅';
397
+ const evidence = (typeof l.sampleSize === 'number' ? `n=${l.sampleSize}` : '') +
398
+ (typeof l.effect === 'number' ? ` Δ=${l.effect.toFixed(2)}R` : '') +
399
+ (typeof l.pValue === 'number' ? ` p=${l.pValue.toFixed(3)}` : '');
400
+ const header = `[Operator is discussing learning id=${l.id} (${l.confidence} · ${l.appliesAt}) ` +
401
+ `"${String(l.title ?? '').slice(0, 120)}". ` +
402
+ `Directive: ${String(l.directive ?? '').slice(0, 400)} | ` +
403
+ `Trigger: ${trig} | Evidence: ${evidence || 'none'}. ` +
404
+ `Help them refine: argue for softer/tighter triggers, more specific override ` +
405
+ `conditions, or that the pattern no longer holds. You do NOT mutate the ` +
406
+ `learning directly — concrete edits flow through the operator's UI.]\n\n`;
407
+ augmentedContent = header + augmentedContent;
408
+ }
409
+ catch (err) {
410
+ logger.warn(TAG, `Failed to render learningContext header: ${String(err)}`);
411
+ }
412
+ }
413
+ logger.info(TAG, `Chat received${proposalContext ? ' (with proposal context)' : ''}${learningContext ? ' (with learning context)' : ''}: "${safeContent.slice(0, 50)}${safeContent.length > 50 ? '...' : ''}"`);
414
+ const result = await this.provider.handleChat(augmentedContent);
415
+ this.connector.sendResponse(id, true, result);
416
+ return;
417
+ }
418
+ if (method === 'close_position') {
419
+ const symbol = params?.symbol ?? '';
420
+ if (!symbol) {
421
+ this.connector.sendResponse(id, false, undefined, { code: 400, message: 'Missing symbol' });
422
+ return;
423
+ }
424
+ logger.info(TAG, `Close position requested: ${symbol}`);
425
+ const result = await this.provider.closePosition(symbol);
426
+ this.connector.sendResponse(id, true, result);
427
+ return;
428
+ }
429
+ if (method === 'set_trading_mode') {
430
+ const result = await this.handleSetTradingMode(id, params);
431
+ if (result !== 'responded') {
432
+ // Handler returned a structured outcome — forward as success frame.
433
+ this.connector.sendResponse(id, true, result);
434
+ }
435
+ return;
436
+ }
437
+ if (method === 'set_exchange_credentials') {
438
+ const result = await this.handleSetExchangeCredentials(id, params);
439
+ if (result !== 'responded') {
440
+ this.connector.sendResponse(id, true, result);
441
+ }
442
+ return;
443
+ }
444
+ if (method === 'test_exchange_credentials') {
445
+ const result = await this.handleTestExchangeCredentials(id, params);
446
+ if (result !== 'responded') {
447
+ this.connector.sendResponse(id, true, result);
448
+ }
449
+ return;
450
+ }
451
+ if (method === 'clear_exchange_credentials') {
452
+ const result = await this.handleClearExchangeCredentials(id);
453
+ this.connector.sendResponse(id, true, result);
454
+ return;
455
+ }
456
+ if (method === 'get_bracket_config') {
457
+ if (!this.provider.getBracketConfig) {
458
+ this.connector.sendResponse(id, false, undefined, {
459
+ code: 501,
460
+ message: 'Provider does not support getBracketConfig',
461
+ });
462
+ return;
463
+ }
464
+ const result = await this.provider.getBracketConfig();
465
+ this.connector.sendResponse(id, true, result);
466
+ return;
467
+ }
468
+ if (method === 'set_bracket_requirement') {
469
+ const flag = params?.flag;
470
+ const value = params?.value;
471
+ if ((flag !== 'requireStopLoss' && flag !== 'requireTakeProfit') || typeof value !== 'boolean') {
472
+ this.connector.sendResponse(id, false, undefined, {
473
+ code: 400,
474
+ message: 'set_bracket_requirement requires { flag: "requireStopLoss"|"requireTakeProfit", value: boolean }',
475
+ });
476
+ return;
477
+ }
478
+ if (!this.provider.setBracketRequirement) {
479
+ this.connector.sendResponse(id, false, undefined, {
480
+ code: 501,
481
+ message: 'Provider does not support setBracketRequirement',
482
+ });
483
+ return;
484
+ }
485
+ logger.info(TAG, `set_bracket_requirement ${flag}=${value}`);
486
+ const result = await this.provider.setBracketRequirement(flag, value);
487
+ this.connector.sendResponse(id, true, result);
488
+ return;
489
+ }
490
+ if (method === 'skill.update') {
491
+ // SECURITY: the frame's content is UNTRUSTED. The relay forwards
492
+ // browser request frames to the skill, so (on a relay without the
493
+ // matching browser-side block) any authenticated dashboard session
494
+ // could forge a `skill.update` req carrying attacker instructions.
495
+ // Treat the frame as a DOORBELL only: ignore its payload and pull the
496
+ // authoritative envelope from the relay's Durable-Object store, which
497
+ // is writable exclusively through the ADMIN_SECRET-gated HTTP push
498
+ // (party.ts handleSkillPush). A forged frame can therefore at most
499
+ // trigger (re)application of the legitimately-stored SKILL.md — and
500
+ // the C1 signature gate inside applySkillUpdate still applies when
501
+ // enforcement is on.
502
+ const pushedVersion = typeof params?.version === 'string' ? params.version : 'unknown';
503
+ logger.info(TAG, `Received SKILL.md OTA update notification (v${pushedVersion}) — pulling authoritative copy from relay store`);
504
+ let result;
505
+ try {
506
+ result = await this.pullAndApplySkillUpdate();
507
+ }
508
+ catch (pullErr) {
509
+ this.connector.sendResponse(id, false, undefined, {
510
+ code: 502,
511
+ message: `Failed to fetch SKILL.md from relay store: ${pullErr instanceof Error ? pullErr.message : String(pullErr)}`,
512
+ });
513
+ return;
514
+ }
515
+ if (!result) {
516
+ this.connector.sendResponse(id, false, undefined, {
517
+ code: 404,
518
+ message: 'No stored SKILL.md on relay — push via the admin HTTP path first',
519
+ });
520
+ return;
521
+ }
522
+ this.connector.sendResponse(id, true, result);
523
+ return;
524
+ }
525
+ // Should not reach here — connector whitelist blocks unknown methods
526
+ this.connector.sendResponse(id, false, undefined, {
527
+ code: 400,
528
+ message: `Unhandled method: ${method}`,
529
+ });
530
+ }
531
+ catch (err) {
532
+ const message = err instanceof Error ? err.message : String(err);
533
+ logger.error(TAG, `Error handling ${method}: ${message}`);
534
+ this.connector.sendResponse(id, false, undefined, {
535
+ code: 500,
536
+ message,
537
+ });
538
+ }
539
+ }
540
+ handleStateChange(state) {
541
+ logger.info(TAG, `Connector state: ${state}`);
542
+ }
543
+ // ---- Operator onboarding (PR2) ----
544
+ /** Today every authenticated session carries operator.write via the
545
+ * relay-level Clerk token. This method exists as the enforcement seam —
546
+ * a future PR can drop per-session roles in here without restructuring
547
+ * the dispatcher. Returning true keeps current behavior identical. */
548
+ hasOperatorWriteScope() {
549
+ return true;
550
+ }
551
+ /** Handle a set_trading_mode request. Returns 'responded' if the handler
552
+ * already wrote the response frame (e.g. on validation failure), otherwise
553
+ * returns the structured outcome for the caller to wrap in a success
554
+ * frame. Ladder validation runs here for fast dashboard feedback and
555
+ * again in the plugin (defense in depth). */
556
+ async handleSetTradingMode(id, params) {
557
+ const rawMode = params?.mode;
558
+ const acknowledged = params?.acknowledged === true;
559
+ if (!isTradingMode(rawMode)) {
560
+ this.connector.sendResponse(id, false, undefined, {
561
+ code: 400,
562
+ message: 'mode must be one of PAPER, MICRO_LIVE, LIVE',
563
+ });
564
+ return 'responded';
565
+ }
566
+ // Fast skill-side ladder check so the dashboard gets an error without
567
+ // round-tripping to the plugin. We use the current tradingMode from the
568
+ // latest snapshot — if we don't have one yet, skip the check and let the
569
+ // plugin enforce.
570
+ const currentMode = this.lastKnownTradingMode;
571
+ if (currentMode) {
572
+ const ladder = validateModeTransition(currentMode, rawMode);
573
+ if (!ladder.allowed) {
574
+ audit('set_trading_mode.rejected', {
575
+ id,
576
+ from: currentMode,
577
+ to: rawMode,
578
+ reason: 'ladder',
579
+ });
580
+ return {
581
+ ok: false,
582
+ message: ladder.reason ?? 'Mode transition not allowed',
583
+ previousMode: currentMode,
584
+ mode: currentMode,
585
+ readiness: 'UNKNOWN',
586
+ reason: 'ladder',
587
+ };
588
+ }
589
+ }
590
+ const provider = this.provider;
591
+ if (!provider.setTradingMode) {
592
+ return {
593
+ ok: false,
594
+ message: 'Provider does not support set_trading_mode (likely mock provider)',
595
+ previousMode: currentMode ?? 'PAPER',
596
+ mode: currentMode ?? 'PAPER',
597
+ readiness: 'UNKNOWN',
598
+ };
599
+ }
600
+ audit('set_trading_mode.start', { id, from: currentMode ?? 'unknown', to: rawMode, acknowledged });
601
+ const outcome = await provider.setTradingMode(rawMode, acknowledged);
602
+ audit('set_trading_mode.complete', {
603
+ id,
604
+ ok: outcome.ok,
605
+ previousMode: outcome.previousMode,
606
+ mode: outcome.mode,
607
+ reason: outcome.reason,
608
+ });
609
+ if (outcome.ok) {
610
+ this.lastKnownTradingMode = outcome.mode;
611
+ }
612
+ return outcome;
613
+ }
614
+ /** Handle a set_exchange_credentials request. The secret value passes
615
+ * through once and is never cached or logged; only a redacted fingerprint
616
+ * appears in the audit trail. */
617
+ async handleSetExchangeCredentials(id, params) {
618
+ const apiKey = typeof params?.apiKey === 'string' ? params.apiKey : '';
619
+ const secret = typeof params?.secret === 'string' ? params.secret : '';
620
+ const testnet = params?.testnet === true;
621
+ if (apiKey.length < 8 || secret.length < 8) {
622
+ this.connector.sendResponse(id, false, undefined, {
623
+ code: 400,
624
+ message: 'apiKey and secret must be at least 8 characters each',
625
+ });
626
+ return 'responded';
627
+ }
628
+ const fp = fingerprint(apiKey);
629
+ const provider = this.provider;
630
+ if (!provider.setExchangeCredentials) {
631
+ return {
632
+ ok: false,
633
+ message: 'Provider does not support set_exchange_credentials (likely mock provider)',
634
+ fingerprint: fp,
635
+ reconnected: false,
636
+ mode: this.lastKnownTradingMode ?? 'PAPER',
637
+ readiness: 'UNKNOWN',
638
+ };
639
+ }
640
+ audit('set_exchange_credentials.start', { id, fingerprint: fp, testnet });
641
+ const outcome = await provider.setExchangeCredentials(apiKey, secret, testnet);
642
+ audit('set_exchange_credentials.complete', {
643
+ id,
644
+ ok: outcome.ok,
645
+ fingerprint: outcome.fingerprint,
646
+ reconnected: outcome.reconnected,
647
+ mode: outcome.mode,
648
+ });
649
+ return outcome;
650
+ }
651
+ /** Handle a test_exchange_credentials request — transient read-only
652
+ * verification, nothing persisted. Same input validation + secret
653
+ * redaction rules as set_exchange_credentials. */
654
+ async handleTestExchangeCredentials(id, params) {
655
+ const apiKey = typeof params?.apiKey === 'string' ? params.apiKey : '';
656
+ const secret = typeof params?.secret === 'string' ? params.secret : '';
657
+ const testnet = params?.testnet === true;
658
+ if (apiKey.length < 8 || secret.length < 8) {
659
+ this.connector.sendResponse(id, false, undefined, {
660
+ code: 400,
661
+ message: 'apiKey and secret must be at least 8 characters each',
662
+ });
663
+ return 'responded';
664
+ }
665
+ const fp = fingerprint(apiKey);
666
+ const provider = this.provider;
667
+ if (!provider.testExchangeCredentials) {
668
+ return {
669
+ ok: false,
670
+ message: 'Provider does not support test_exchange_credentials (likely mock provider)',
671
+ fingerprint: fp,
672
+ canReadBalance: false,
673
+ canReadPositions: false,
674
+ balanceUSDT: null,
675
+ testnet,
676
+ errors: ['provider-unsupported'],
677
+ };
678
+ }
679
+ audit('test_exchange_credentials.start', { id, fingerprint: fp, testnet });
680
+ const outcome = await provider.testExchangeCredentials(apiKey, secret, testnet);
681
+ audit('test_exchange_credentials.complete', {
682
+ id,
683
+ ok: outcome.ok,
684
+ fingerprint: outcome.fingerprint,
685
+ canReadBalance: outcome.canReadBalance,
686
+ canReadPositions: outcome.canReadPositions,
687
+ balanceUSDT: outcome.balanceUSDT,
688
+ });
689
+ return outcome;
690
+ }
691
+ /** Handle a clear_exchange_credentials request. No input — the method
692
+ * itself is the "yes I want this" signal; the webapp is expected to
693
+ * gate this behind a confirmation dialog before calling. */
694
+ async handleClearExchangeCredentials(id) {
695
+ const provider = this.provider;
696
+ if (!provider.clearExchangeCredentials) {
697
+ return {
698
+ ok: false,
699
+ message: 'Provider does not support clear_exchange_credentials (likely mock provider)',
700
+ modeBefore: this.lastKnownTradingMode ?? 'PAPER',
701
+ mode: this.lastKnownTradingMode ?? 'PAPER',
702
+ rolledBackToPaper: false,
703
+ readiness: 'UNKNOWN',
704
+ };
705
+ }
706
+ audit('clear_exchange_credentials.start', { id });
707
+ const outcome = await provider.clearExchangeCredentials();
708
+ audit('clear_exchange_credentials.complete', {
709
+ id,
710
+ ok: outcome.ok,
711
+ modeBefore: outcome.modeBefore,
712
+ mode: outcome.mode,
713
+ rolledBackToPaper: outcome.rolledBackToPaper,
714
+ });
715
+ if (outcome.ok && outcome.rolledBackToPaper) {
716
+ this.lastKnownTradingMode = 'PAPER';
717
+ }
718
+ return outcome;
719
+ }
720
+ // ---- OTA SKILL.md update ----
721
+ /** Apply a SKILL.md update: verify signature (C1), write to workspace +
722
+ * installed skill dir, clear session cache, notify agent. */
723
+ async applySkillUpdate(env) {
724
+ const { version, content } = env;
725
+ const workspacePath = join(homedir(), '.openclaw', 'workspace');
726
+ const skillMdPath = join(workspacePath, 'SKILL.md');
727
+ const tempPath = join(workspacePath, 'SKILL.md.tmp');
728
+ const sessionsJsonPath = join(homedir(), '.openclaw', 'agents', 'main', 'sessions', 'sessions.json');
729
+ // True only when the Ed25519 signature was actually checked AND passed —
730
+ // gates the replay-clock write below. An update applied UNVERIFIED
731
+ // (enforcement off) must never advance the clock: an unsigned push with a
732
+ // far-future signedAt would otherwise brick every future legitimately-
733
+ // signed update as "replay".
734
+ let signatureVerified = false;
735
+ try {
736
+ // 0. SIGNATURE GATE (C1) — verify BEFORE touching disk. The relay is a
737
+ // dumb transport: only a payload signed by the operator's offline key
738
+ // is applied. Fail closed when enforced.
739
+ if (signatureRequired()) {
740
+ const verdict = verifySkillSignature(env);
741
+ if (!verdict.ok) {
742
+ logger.error(TAG, `OTA SKILL.md REJECTED (signature): ${verdict.reason}`);
743
+ return { success: false, version, message: `signature rejected: ${verdict.reason}` };
744
+ }
745
+ // Replay/rollback guard: only accept a strictly newer signed artifact.
746
+ const sigMs = new Date(env.signedAt).getTime();
747
+ if (!(sigMs > readLastAppliedSignedAtMs())) {
748
+ logger.error(TAG, `OTA SKILL.md REJECTED (replay): signedAt ${env.signedAt} not newer than last applied`);
749
+ return { success: false, version, message: 'signature rejected: replay (stale signedAt)' };
750
+ }
751
+ signatureVerified = true;
752
+ }
753
+ else if (!env.signature) {
754
+ // C1 (signed OTA) is ON HOLD → verification off by default, so OTA works
755
+ // unsigned as before. To enforce: pin a key + SKILL_OTA_REQUIRE_SIGNATURE=on.
756
+ logger.warn(TAG, 'OTA SKILL.md applied WITHOUT signature verification — signed-OTA (C1) is disabled');
757
+ }
758
+ // 1. Validate content
759
+ const validation = validateSkillContent(content);
760
+ if (!validation.valid) {
761
+ return { success: false, version, message: `Invalid SKILL.md: ${validation.error}` };
762
+ }
763
+ // 2. Atomic write: temp file → rename (workspace copy)
764
+ mkdirSync(workspacePath, { recursive: true });
765
+ writeFileSync(tempPath, content, 'utf-8');
766
+ renameSync(tempPath, skillMdPath);
767
+ // 2b. Also write to the installed skill directory — this is what OpenClaw's
768
+ // skill system actually reads for agent instructions (not the workspace copy).
769
+ // Scan ~/.openclaw/skills/ for any reefclaw-related skill directories.
770
+ const skillsDir = join(homedir(), '.openclaw', 'skills');
771
+ if (existsSync(skillsDir)) {
772
+ try {
773
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
774
+ for (const entry of entries) {
775
+ if (entry.isDirectory() && entry.name.includes('reefclaw')) {
776
+ const installedSkillMd = join(skillsDir, entry.name, 'SKILL.md');
777
+ writeFileSync(installedSkillMd, content, 'utf-8');
778
+ logger.info(TAG, `Updated installed skill SKILL.md: ${installedSkillMd}`);
779
+ }
780
+ }
781
+ }
782
+ catch (scanErr) {
783
+ logger.warn(TAG, `Failed to update installed skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
784
+ }
785
+ }
786
+ // 2c. Also write to workspace/skills/ subdirectory if it exists
787
+ const wsSkillsDir = join(workspacePath, 'skills');
788
+ if (existsSync(wsSkillsDir)) {
789
+ try {
790
+ const entries = readdirSync(wsSkillsDir, { withFileTypes: true });
791
+ for (const entry of entries) {
792
+ if (entry.isDirectory() && entry.name.includes('reefclaw')) {
793
+ const wsSkillMd = join(wsSkillsDir, entry.name, 'SKILL.md');
794
+ writeFileSync(wsSkillMd, content, 'utf-8');
795
+ logger.info(TAG, `Updated workspace skill SKILL.md: ${wsSkillMd}`);
796
+ }
797
+ }
798
+ }
799
+ catch (scanErr) {
800
+ logger.warn(TAG, `Failed to update workspace skill SKILL.md: ${scanErr instanceof Error ? scanErr.message : String(scanErr)}`);
801
+ }
802
+ }
803
+ // 3. Invalidate skillsSnapshot cache WITHOUT wiping sessions.json
804
+ // Previously we wrote '{}' to sessions.json which destroyed OpenClaw chat history.
805
+ // Now we surgically remove only the skillsSnapshot key from each session entry,
806
+ // preserving the session pointer and chat history.
807
+ if (existsSync(sessionsJsonPath)) {
808
+ try {
809
+ const raw = readFileSync(sessionsJsonPath, 'utf-8');
810
+ const sessions = JSON.parse(raw);
811
+ let patched = false;
812
+ for (const key of Object.keys(sessions)) {
813
+ const entry = sessions[key];
814
+ if (entry && typeof entry === 'object' && 'skillsSnapshot' in entry) {
815
+ delete entry.skillsSnapshot;
816
+ patched = true;
817
+ }
818
+ }
819
+ if (patched) {
820
+ writeFileSync(sessionsJsonPath, JSON.stringify(sessions, null, 2), 'utf-8');
821
+ logger.info(TAG, 'Removed skillsSnapshot from sessions.json — agent will re-read SKILL.md (chat history preserved)');
822
+ }
823
+ else {
824
+ logger.info(TAG, 'No skillsSnapshot found in sessions.json — nothing to invalidate');
825
+ }
826
+ }
827
+ catch (parseErr) {
828
+ // If sessions.json is corrupted or unparseable, fall back to resetting it
829
+ logger.warn(TAG, `Failed to patch sessions.json, resetting: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
830
+ writeFileSync(sessionsJsonPath, '{}', 'utf-8');
831
+ }
832
+ }
833
+ // 4. Send chat message to agent to trigger immediate re-read
834
+ try {
835
+ await this.provider.handleChat(`Your trading instructions (SKILL.md) have been updated to v${version}. ` +
836
+ `Check for any open positions with fetch_positions() and fetch_open_orders(), then re-read SKILL.md to confirm you've processed the changes.`);
837
+ }
838
+ catch (chatErr) {
839
+ logger.warn(TAG, `Chat notification failed (non-fatal): ${chatErr instanceof Error ? chatErr.message : String(chatErr)}`);
840
+ }
841
+ // 5. Update local version tracker + advance the replay clock. The clock
842
+ // advances ONLY on a VERIFIED signature: recording an unverified
843
+ // signedAt (enforcement off) would let an unsigned push with a
844
+ // far-future timestamp poison the clock and reject all future
845
+ // legitimately-signed updates as replays.
846
+ this.currentSkillVersion = version;
847
+ if (signatureVerified && env.signedAt)
848
+ recordAppliedSignedAt(env.signedAt);
849
+ logger.info(TAG, `SKILL.md updated to v${version} successfully`);
850
+ return { success: true, version, message: `Updated to v${version}` };
851
+ }
852
+ catch (err) {
853
+ const message = err instanceof Error ? err.message : String(err);
854
+ logger.error(TAG, `Failed to apply SKILL.md update: ${message}`);
855
+ try {
856
+ unlinkSync(tempPath);
857
+ }
858
+ catch { /* ignore cleanup failure */ }
859
+ return { success: false, version, message };
860
+ }
861
+ }
862
+ /**
863
+ * Fetch the stored SKILL.md envelope from the relay's Durable-Object store
864
+ * and apply it. The DO store is writable exclusively through the
865
+ * ADMIN_SECRET-gated HTTP push (party.ts handleSkillPush) — pulling makes
866
+ * the STORE, not any WS frame, the trust anchor for OTA content. Refuses to
867
+ * downgrade below the local version (a stale relay copy must never clobber
868
+ * a newer local SKILL.md — prod incident 2026-05-13); an equal version
869
+ * re-applies (idempotent repair for a re-push). Returns null when the relay
870
+ * has nothing stored; throws on transport error.
871
+ */
872
+ async pullAndApplySkillUpdate() {
873
+ const result = await this.connector.sendRequest('skill.request_update', {});
874
+ if (!result?.content)
875
+ return null;
876
+ // Re-read local version (may have changed since startup) + downgrade guard.
877
+ this.currentSkillVersion = readLocalSkillVersion();
878
+ if (compareSemver(result.version, this.currentSkillVersion) < 0) {
879
+ const message = `refusing downgrade: relay v${result.version} is older than local v${this.currentSkillVersion ?? 'unknown'}`;
880
+ logger.warn(TAG, `SKILL.md pull: ${message}`);
881
+ return { success: false, version: result.version, message };
882
+ }
883
+ const updateResult = await this.applySkillUpdate({
884
+ version: result.version,
885
+ content: result.content,
886
+ contentSha256: result.contentSha256,
887
+ signedAt: result.signedAt,
888
+ signature: result.signature,
889
+ });
890
+ // Confirmation event to the dashboard — the operator pushed something and
891
+ // should see the outcome either way (relay audits skill_update_applied).
892
+ this.emit('agent_state', 'skill_update_applied', {
893
+ event: 'skill_update_applied',
894
+ version: result.version,
895
+ success: updateResult.success,
896
+ timestamp: new Date().toISOString(),
897
+ });
898
+ return updateResult;
899
+ }
900
+ /** Check relay's latest SKILL.md version against local and auto-update if stale */
901
+ async handleConnectAck(payload) {
902
+ const latestVersion = payload.latestSkillVersion;
903
+ if (!latestVersion)
904
+ return;
905
+ // Re-read local version (may have changed since startup)
906
+ this.currentSkillVersion = readLocalSkillVersion();
907
+ const cmp = compareSemver(latestVersion, this.currentSkillVersion);
908
+ if (cmp === 0) {
909
+ logger.debug(TAG, `SKILL.md up to date (v${latestVersion})`);
910
+ return;
911
+ }
912
+ if (cmp < 0) {
913
+ // Local is AHEAD of relay. Never downgrade: a stale relay copy must not
914
+ // clobber a freshly SFTP'd or hand-edited SKILL.md. (Prod incident
915
+ // 2026-05-13: relay was at v2.14 but local was v2.15; the old `===`
916
+ // check treated "not equal" as "behind" and silently downgraded to
917
+ // whatever the relay delivered — which was v2.11.) Log loudly so it's
918
+ // obvious the relay needs a push.
919
+ logger.warn(TAG, `Relay SKILL.md v${latestVersion} is OLDER than local v${this.currentSkillVersion ?? 'unknown'} — refusing to downgrade. Push current SKILL.md to relay to clear this warning.`);
920
+ return;
921
+ }
922
+ logger.info(TAG, `Local SKILL.md v${this.currentSkillVersion ?? 'unknown'} behind relay v${latestVersion} — requesting update`);
923
+ try {
924
+ await this.pullAndApplySkillUpdate();
925
+ }
926
+ catch (err) {
927
+ logger.warn(TAG, `Failed to fetch SKILL.md update from relay: ${err instanceof Error ? err.message : String(err)}`);
928
+ }
929
+ }
930
+ }
931
+ // ---------------------------------------------------------------------------
932
+ // Helpers
933
+ // ---------------------------------------------------------------------------
934
+ const VALID_GAPFILL_CHANNELS = [
935
+ 'market_data',
936
+ 'trade_events',
937
+ 'agent_state',
938
+ 'chat',
939
+ ];
940
+ /**
941
+ * Parse the `lastSequences` array from a reconcile request into a per-channel
942
+ * map. Defaults to `-1` (i.e. "send everything you have") for any channel the
943
+ * client didn't include or that has a malformed entry. Hostile / malformed
944
+ * input is silently ignored — the gapFill replay is read-only and bounded by
945
+ * the buffer's age cap, so worst case is a slightly larger response.
946
+ */
947
+ function parseLastSequences(params) {
948
+ const empty = {};
949
+ for (const ch of VALID_GAPFILL_CHANNELS)
950
+ empty[ch] = -1;
951
+ if (!params || typeof params !== 'object')
952
+ return empty;
953
+ const ls = params.lastSequences;
954
+ if (!Array.isArray(ls))
955
+ return empty;
956
+ const out = { ...empty };
957
+ for (const item of ls) {
958
+ if (!item || typeof item !== 'object')
959
+ continue;
960
+ const obj = item;
961
+ if (typeof obj.channel === 'string' &&
962
+ VALID_GAPFILL_CHANNELS.includes(obj.channel) &&
963
+ typeof obj.lastSeq === 'number' &&
964
+ Number.isFinite(obj.lastSeq)) {
965
+ out[obj.channel] = obj.lastSeq;
966
+ }
967
+ }
968
+ return out;
969
+ }