@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.
Files changed (371) hide show
  1. package/.gitignore +8 -0
  2. package/audit/mode-transition-audit.d.ts +11 -0
  3. package/audit/mode-transition-audit.js +29 -0
  4. package/balance-utils.d.ts +36 -0
  5. package/balance-utils.js +98 -0
  6. package/bridge/bridge.d.ts +109 -0
  7. package/bridge/bridge.js +1036 -0
  8. package/bridge/config.d.ts +43 -0
  9. package/bridge/config.js +176 -0
  10. package/bridge/connector.d.ts +52 -0
  11. package/bridge/connector.js +293 -0
  12. package/bridge/event-replay-buffer.d.ts +43 -0
  13. package/bridge/event-replay-buffer.js +109 -0
  14. package/bridge/gateway/event-parser.d.ts +209 -0
  15. package/bridge/gateway/event-parser.js +793 -0
  16. package/bridge/gateway/gateway-config.d.ts +39 -0
  17. package/bridge/gateway/gateway-config.js +86 -0
  18. package/bridge/gateway/gateway-http-client.d.ts +50 -0
  19. package/bridge/gateway/gateway-http-client.js +165 -0
  20. package/bridge/gateway/gateway-ws-client.d.ts +116 -0
  21. package/bridge/gateway/gateway-ws-client.js +417 -0
  22. package/bridge/gateway/poller.d.ts +146 -0
  23. package/bridge/gateway/poller.js +505 -0
  24. package/bridge/gateway/tool-discovery.d.ts +25 -0
  25. package/bridge/gateway/tool-discovery.js +198 -0
  26. package/bridge/index.d.ts +2 -0
  27. package/bridge/index.js +253 -0
  28. package/bridge/logger.d.ts +2 -0
  29. package/bridge/logger.js +2 -0
  30. package/bridge/provider.d.ts +156 -0
  31. package/bridge/provider.js +2 -0
  32. package/bridge/providers/emergency-commands.d.ts +54 -0
  33. package/bridge/providers/emergency-commands.js +235 -0
  34. package/bridge/providers/gateway.d.ts +322 -0
  35. package/bridge/providers/gateway.js +2302 -0
  36. package/bridge/providers/mock.d.ts +37 -0
  37. package/bridge/providers/mock.js +385 -0
  38. package/bridge/providers/onboarding-commands.d.ts +83 -0
  39. package/bridge/providers/onboarding-commands.js +213 -0
  40. package/bridge/providers/risk-calculator.d.ts +96 -0
  41. package/bridge/providers/risk-calculator.js +369 -0
  42. package/bridge/setup.d.ts +32 -0
  43. package/bridge/setup.js +226 -0
  44. package/bridge/types.d.ts +584 -0
  45. package/bridge/types.js +50 -0
  46. package/bridge/utils/reconnect.d.ts +6 -0
  47. package/bridge/utils/reconnect.js +6 -0
  48. package/bridge/utils/skill-signing.d.ts +51 -0
  49. package/bridge/utils/skill-signing.js +138 -0
  50. package/bridge/utils/skill-version.d.ts +17 -0
  51. package/bridge/utils/skill-version.js +74 -0
  52. package/ccxt/binance-ban-gate.d.ts +47 -0
  53. package/ccxt/binance-ban-gate.js +409 -0
  54. package/ccxt/binance-private.d.ts +325 -0
  55. package/ccxt/binance-private.js +1415 -0
  56. package/ccxt/binance-public.d.ts +18 -0
  57. package/ccxt/binance-public.js +147 -0
  58. package/config/agent-config-client.d.ts +55 -0
  59. package/config/agent-config-client.js +145 -0
  60. package/config/agent-config-poller.d.ts +25 -0
  61. package/config/agent-config-poller.js +100 -0
  62. package/config/brackets-config.d.ts +22 -0
  63. package/config/brackets-config.js +58 -0
  64. package/config/gate-store.d.ts +18 -0
  65. package/config/gate-store.js +61 -0
  66. package/config/plugin-config-io.d.ts +181 -0
  67. package/config/plugin-config-io.js +84 -0
  68. package/config/position-review-config.d.ts +35 -0
  69. package/config/position-review-config.js +105 -0
  70. package/config/tool-gate.d.ts +53 -0
  71. package/config/tool-gate.js +125 -0
  72. package/config/user-data-stream-config.d.ts +85 -0
  73. package/config/user-data-stream-config.js +224 -0
  74. package/connector-supervisor.d.ts +36 -0
  75. package/connector-supervisor.js +149 -0
  76. package/exchange-adapter.d.ts +49 -0
  77. package/exchange-adapter.js +4 -0
  78. package/index.d.ts +30 -0
  79. package/index.js +2030 -0
  80. package/ingest/pending-entry-metadata.d.ts +52 -0
  81. package/ingest/pending-entry-metadata.js +182 -0
  82. package/ingest/position-auto-capture.d.ts +98 -0
  83. package/ingest/position-auto-capture.js +394 -0
  84. package/ingest/position-decisions-client.d.ts +318 -0
  85. package/ingest/position-decisions-client.js +296 -0
  86. package/ingest/reconcile-db-vs-exchange.d.ts +13 -0
  87. package/ingest/reconcile-db-vs-exchange.js +114 -0
  88. package/ingest/reconciler-cleanup.d.ts +37 -0
  89. package/ingest/reconciler-cleanup.js +147 -0
  90. package/ingest/rest-gap-filler.d.ts +191 -0
  91. package/ingest/rest-gap-filler.js +565 -0
  92. package/ingest/touched-symbols-store.d.ts +25 -0
  93. package/ingest/touched-symbols-store.js +96 -0
  94. package/ingest/trade-store-client.d.ts +40 -0
  95. package/ingest/trade-store-client.js +125 -0
  96. package/ingest/ws-ingest.d.ts +43 -0
  97. package/ingest/ws-ingest.js +126 -0
  98. package/learning/setup-family.d.ts +21 -0
  99. package/learning/setup-family.js +103 -0
  100. package/lifecycle/install-signal-handlers.d.ts +33 -0
  101. package/lifecycle/install-signal-handlers.js +112 -0
  102. package/lifecycle/shutdown-coordinator.d.ts +43 -0
  103. package/lifecycle/shutdown-coordinator.js +131 -0
  104. package/live/bracket-id.d.ts +18 -0
  105. package/live/bracket-id.js +81 -0
  106. package/live/bracket-ledger.d.ts +54 -0
  107. package/live/bracket-ledger.js +267 -0
  108. package/live/bracket-manager.d.ts +82 -0
  109. package/live/bracket-manager.js +478 -0
  110. package/live/bracket-params.d.ts +22 -0
  111. package/live/bracket-params.js +124 -0
  112. package/live/bracket-reconciler.d.ts +95 -0
  113. package/live/bracket-reconciler.js +573 -0
  114. package/live/bracket-types.d.ts +102 -0
  115. package/live/bracket-types.js +8 -0
  116. package/live/deposit-tracker.d.ts +62 -0
  117. package/live/deposit-tracker.js +97 -0
  118. package/live/emergency-controls.d.ts +32 -0
  119. package/live/emergency-controls.js +226 -0
  120. package/live/exchange-errors.d.ts +12 -0
  121. package/live/exchange-errors.js +130 -0
  122. package/live/exchange-info-cache.d.ts +35 -0
  123. package/live/exchange-info-cache.js +119 -0
  124. package/live/fact-subscriber.d.ts +78 -0
  125. package/live/fact-subscriber.js +182 -0
  126. package/live/intent-journal.d.ts +42 -0
  127. package/live/intent-journal.js +122 -0
  128. package/live/listen-key-manager.d.ts +70 -0
  129. package/live/listen-key-manager.js +169 -0
  130. package/live/live-adapter.d.ts +264 -0
  131. package/live/live-adapter.js +1665 -0
  132. package/live/live-balance-enricher.d.ts +32 -0
  133. package/live/live-balance-enricher.js +104 -0
  134. package/live/live-bracket-api.d.ts +13 -0
  135. package/live/live-bracket-api.js +20 -0
  136. package/live/live-state-store.d.ts +194 -0
  137. package/live/live-state-store.js +450 -0
  138. package/live/local-signal-service.d.ts +57 -0
  139. package/live/local-signal-service.js +146 -0
  140. package/live/local-strategy-evaluator.d.ts +62 -0
  141. package/live/local-strategy-evaluator.js +127 -0
  142. package/live/microstructure-assembler.d.ts +54 -0
  143. package/live/microstructure-assembler.js +148 -0
  144. package/live/order-poller.d.ts +29 -0
  145. package/live/order-poller.js +125 -0
  146. package/live/position-state-store.d.ts +83 -0
  147. package/live/position-state-store.js +237 -0
  148. package/live/proposal-decision-listener.d.ts +64 -0
  149. package/live/proposal-decision-listener.js +288 -0
  150. package/live/proposal-manager.d.ts +76 -0
  151. package/live/proposal-manager.js +140 -0
  152. package/live/rate-limiter.d.ts +47 -0
  153. package/live/rate-limiter.js +159 -0
  154. package/live/reconciler.d.ts +39 -0
  155. package/live/reconciler.js +175 -0
  156. package/live/setup-buckets.d.ts +7 -0
  157. package/live/setup-buckets.js +33 -0
  158. package/live/slippage-tracker.d.ts +45 -0
  159. package/live/slippage-tracker.js +78 -0
  160. package/live/stop-watcher.d.ts +34 -0
  161. package/live/stop-watcher.js +158 -0
  162. package/live/user-data-active-probe.d.ts +54 -0
  163. package/live/user-data-active-probe.js +180 -0
  164. package/live/user-data-stream-controller.d.ts +200 -0
  165. package/live/user-data-stream-controller.js +579 -0
  166. package/live/user-data-stream-ws.d.ts +22 -0
  167. package/live/user-data-stream-ws.js +63 -0
  168. package/live/user-data-stream.d.ts +243 -0
  169. package/live/user-data-stream.js +704 -0
  170. package/logger.d.ts +2 -0
  171. package/logger.js +2 -0
  172. package/mfe.d.ts +21 -0
  173. package/mfe.js +68 -0
  174. package/onboarding/mode-ladder.d.ts +1 -0
  175. package/onboarding/mode-ladder.js +3 -0
  176. package/onboarding/runtime.d.ts +71 -0
  177. package/onboarding/runtime.js +153 -0
  178. package/openclaw.plugin.json +94 -0
  179. package/package.json +27 -0
  180. package/paper-adapter.d.ts +24 -0
  181. package/paper-adapter.js +91 -0
  182. package/persistence/state-manager.d.ts +42 -0
  183. package/persistence/state-manager.js +164 -0
  184. package/pinned-plan.d.ts +9 -0
  185. package/pinned-plan.js +23 -0
  186. package/risk/pre-trade-check.d.ts +38 -0
  187. package/risk/pre-trade-check.js +345 -0
  188. package/risk/pre-trade-types.d.ts +60 -0
  189. package/risk/pre-trade-types.js +3 -0
  190. package/scripts/assemble.mjs +114 -0
  191. package/shadow/shadow-tracker.d.ts +36 -0
  192. package/shadow/shadow-tracker.js +151 -0
  193. package/shadow/types.d.ts +42 -0
  194. package/shadow/types.js +20 -0
  195. package/shared/indicators-extended.d.ts +52 -0
  196. package/shared/indicators-extended.js +291 -0
  197. package/shared/indicators.d.ts +15 -0
  198. package/shared/indicators.js +114 -0
  199. package/signals/conditions/registry.d.ts +16 -0
  200. package/signals/conditions/registry.js +1274 -0
  201. package/signals/conditions/types.d.ts +1 -0
  202. package/signals/conditions/types.js +4 -0
  203. package/signals/direction-rules.d.ts +3 -0
  204. package/signals/direction-rules.js +24 -0
  205. package/signals/entry-rules.d.ts +6 -0
  206. package/signals/entry-rules.js +33 -0
  207. package/signals/serialize-context.d.ts +4 -0
  208. package/signals/serialize-context.js +39 -0
  209. package/signals/stop-rules.d.ts +3 -0
  210. package/signals/stop-rules.js +48 -0
  211. package/signals/strategy-adapter.d.ts +14 -0
  212. package/signals/strategy-adapter.js +122 -0
  213. package/signals/types.d.ts +1 -0
  214. package/signals/types.js +8 -0
  215. package/simulator/exchange-simulator.d.ts +93 -0
  216. package/simulator/exchange-simulator.js +684 -0
  217. package/simulator/fill-engine.d.ts +53 -0
  218. package/simulator/fill-engine.js +276 -0
  219. package/simulator/paper-market-feed.d.ts +26 -0
  220. package/simulator/paper-market-feed.js +104 -0
  221. package/simulator/realistic-fills.d.ts +59 -0
  222. package/simulator/realistic-fills.js +175 -0
  223. package/simulator/types.d.ts +219 -0
  224. package/simulator/types.js +43 -0
  225. package/skills/reefclaw/SKILL.md +97 -0
  226. package/strategy/builtin-strategies.d.ts +2 -0
  227. package/strategy/builtin-strategies.js +109 -0
  228. package/strategy/condition-registry.d.ts +3 -0
  229. package/strategy/condition-registry.js +153 -0
  230. package/strategy/evaluator.d.ts +67 -0
  231. package/strategy/evaluator.js +93 -0
  232. package/tools/assessment-validation.d.ts +118 -0
  233. package/tools/assessment-validation.js +415 -0
  234. package/tools/attach-brackets.d.ts +34 -0
  235. package/tools/attach-brackets.js +363 -0
  236. package/tools/audit-bracket-protection.d.ts +49 -0
  237. package/tools/audit-bracket-protection.js +527 -0
  238. package/tools/cancel-all-orders.d.ts +7 -0
  239. package/tools/cancel-all-orders.js +5 -0
  240. package/tools/cancel-order.d.ts +10 -0
  241. package/tools/cancel-order.js +14 -0
  242. package/tools/check-position-health.d.ts +46 -0
  243. package/tools/check-position-health.js +194 -0
  244. package/tools/clear-exchange-credentials.d.ts +24 -0
  245. package/tools/clear-exchange-credentials.js +70 -0
  246. package/tools/close-position.d.ts +22 -0
  247. package/tools/close-position.js +449 -0
  248. package/tools/create-order.d.ts +54 -0
  249. package/tools/create-order.js +338 -0
  250. package/tools/exit-gate.d.ts +58 -0
  251. package/tools/exit-gate.js +162 -0
  252. package/tools/fetch-balance.d.ts +5 -0
  253. package/tools/fetch-balance.js +4 -0
  254. package/tools/fetch-ohlcv.d.ts +11 -0
  255. package/tools/fetch-ohlcv.js +8 -0
  256. package/tools/fetch-open-orders.d.ts +7 -0
  257. package/tools/fetch-open-orders.js +4 -0
  258. package/tools/fetch-positions.d.ts +7 -0
  259. package/tools/fetch-positions.js +4 -0
  260. package/tools/fetch-ticker.d.ts +11 -0
  261. package/tools/fetch-ticker.js +5 -0
  262. package/tools/get-agent-profile.d.ts +4 -0
  263. package/tools/get-agent-profile.js +6 -0
  264. package/tools/get-analytics.d.ts +6 -0
  265. package/tools/get-analytics.js +7 -0
  266. package/tools/get-backtest.d.ts +12 -0
  267. package/tools/get-backtest.js +91 -0
  268. package/tools/get-basis.d.ts +7 -0
  269. package/tools/get-basis.js +7 -0
  270. package/tools/get-bracket-config.d.ts +11 -0
  271. package/tools/get-bracket-config.js +24 -0
  272. package/tools/get-cascade-risk.d.ts +7 -0
  273. package/tools/get-cascade-risk.js +8 -0
  274. package/tools/get-crypto-metrics.d.ts +18 -0
  275. package/tools/get-crypto-metrics.js +45 -0
  276. package/tools/get-cvd.d.ts +6 -0
  277. package/tools/get-cvd.js +6 -0
  278. package/tools/get-divergences.d.ts +6 -0
  279. package/tools/get-divergences.js +6 -0
  280. package/tools/get-funding-context.d.ts +6 -0
  281. package/tools/get-funding-context.js +16 -0
  282. package/tools/get-liquidation-levels.d.ts +7 -0
  283. package/tools/get-liquidation-levels.js +7 -0
  284. package/tools/get-liquidation-pulse.d.ts +9 -0
  285. package/tools/get-liquidation-pulse.js +22 -0
  286. package/tools/get-market-breadth.d.ts +6 -0
  287. package/tools/get-market-breadth.js +8 -0
  288. package/tools/get-market-intel.d.ts +19 -0
  289. package/tools/get-market-intel.js +116 -0
  290. package/tools/get-market-structure.d.ts +47 -0
  291. package/tools/get-market-structure.js +198 -0
  292. package/tools/get-my-mined-patterns.d.ts +20 -0
  293. package/tools/get-my-mined-patterns.js +61 -0
  294. package/tools/get-my-proposed-learnings.d.ts +20 -0
  295. package/tools/get-my-proposed-learnings.js +55 -0
  296. package/tools/get-my-recent-reviews.d.ts +22 -0
  297. package/tools/get-my-recent-reviews.js +66 -0
  298. package/tools/get-orderbook.d.ts +21 -0
  299. package/tools/get-orderbook.js +32 -0
  300. package/tools/get-pattern-scan.d.ts +7 -0
  301. package/tools/get-pattern-scan.js +8 -0
  302. package/tools/get-regime.d.ts +6 -0
  303. package/tools/get-regime.js +7 -0
  304. package/tools/get-relevant-learnings.d.ts +21 -0
  305. package/tools/get-relevant-learnings.js +65 -0
  306. package/tools/get-resting-liquidity.d.ts +6 -0
  307. package/tools/get-resting-liquidity.js +11 -0
  308. package/tools/get-risk-scenario.d.ts +29 -0
  309. package/tools/get-risk-scenario.js +47 -0
  310. package/tools/get-risk-summary.d.ts +51 -0
  311. package/tools/get-risk-summary.js +118 -0
  312. package/tools/get-sentiment.d.ts +4 -0
  313. package/tools/get-sentiment.js +6 -0
  314. package/tools/get-session-review.d.ts +7 -0
  315. package/tools/get-session-review.js +8 -0
  316. package/tools/get-setup-detail.d.ts +7 -0
  317. package/tools/get-setup-detail.js +303 -0
  318. package/tools/get-signals.d.ts +15 -0
  319. package/tools/get-signals.js +54 -0
  320. package/tools/get-sizing.d.ts +6 -0
  321. package/tools/get-sizing.js +6 -0
  322. package/tools/get-trade-feedback.d.ts +7 -0
  323. package/tools/get-trade-feedback.js +8 -0
  324. package/tools/get-trade-flow.d.ts +7 -0
  325. package/tools/get-trade-flow.js +7 -0
  326. package/tools/get-volume-analysis.d.ts +21 -0
  327. package/tools/get-volume-analysis.js +74 -0
  328. package/tools/get-volume-profile.d.ts +7 -0
  329. package/tools/get-volume-profile.js +7 -0
  330. package/tools/helpers.d.ts +25 -0
  331. package/tools/helpers.js +38 -0
  332. package/tools/intel-api.d.ts +14 -0
  333. package/tools/intel-api.js +52 -0
  334. package/tools/intel-cache.d.ts +25 -0
  335. package/tools/intel-cache.js +133 -0
  336. package/tools/list-strategies.d.ts +7 -0
  337. package/tools/list-strategies.js +6 -0
  338. package/tools/modify-stop.d.ts +17 -0
  339. package/tools/modify-stop.js +81 -0
  340. package/tools/modify-target.d.ts +17 -0
  341. package/tools/modify-target.js +71 -0
  342. package/tools/propose-learning.d.ts +22 -0
  343. package/tools/propose-learning.js +65 -0
  344. package/tools/query-review-outcomes.d.ts +30 -0
  345. package/tools/query-review-outcomes.js +64 -0
  346. package/tools/query-trades.d.ts +21 -0
  347. package/tools/query-trades.js +37 -0
  348. package/tools/record-position-reviews.d.ts +38 -0
  349. package/tools/record-position-reviews.js +147 -0
  350. package/tools/save-strategy.d.ts +16 -0
  351. package/tools/save-strategy.js +46 -0
  352. package/tools/scan-pairs.d.ts +18 -0
  353. package/tools/scan-pairs.js +220 -0
  354. package/tools/score-setup.d.ts +31 -0
  355. package/tools/score-setup.js +268 -0
  356. package/tools/set-bracket-requirement.d.ts +18 -0
  357. package/tools/set-bracket-requirement.js +81 -0
  358. package/tools/set-exchange-credentials.d.ts +25 -0
  359. package/tools/set-exchange-credentials.js +80 -0
  360. package/tools/set-trading-mode.d.ts +26 -0
  361. package/tools/set-trading-mode.js +135 -0
  362. package/tools/test-exchange-credentials.d.ts +16 -0
  363. package/tools/test-exchange-credentials.js +100 -0
  364. package/tools/toggle-strategy.d.ts +8 -0
  365. package/tools/toggle-strategy.js +8 -0
  366. package/trading-params-cache.d.ts +26 -0
  367. package/trading-params-cache.js +52 -0
  368. package/types.d.ts +110 -0
  369. package/types.js +7 -0
  370. package/util/plugin-paths.d.ts +3 -0
  371. package/util/plugin-paths.js +15 -0
@@ -0,0 +1,237 @@
1
+ // Position state store — restart-survival for the Position Decision Journal.
2
+ //
3
+ // Tracks the minimal per-open-position state the auto-capture pipeline needs
4
+ // to keep the (symbol, positionOpenAt) join key stable across plugin restarts:
5
+ //
6
+ // - openedAt: authoritative timestamp of the first opening fill, used as the
7
+ // webapp's natural-key column. Without persistence, restart would
8
+ // reset every position's openedAt to "now" and break the join.
9
+ // - webappPositionId: the UUID returned by POST /api/internal/positions; needed
10
+ // so subsequent /position-{entries,reviews,closes} POSTs can
11
+ // reference the correct parent row.
12
+ // - lastReviewAt + lastVerdict: drives the stale-review gate (per
13
+ // POSITION_REVIEW_PLAN.md §7.4). Cheap to recompute from the
14
+ // webapp on boot, but persisting locally avoids a round-trip
15
+ // during the gate-check hot path.
16
+ //
17
+ // Storage: full JSON rewrite + atomic rename, same pattern as bracket-ledger.
18
+ // Path: ~/.openclaw/plugins/<pluginId>/position-state.json
19
+ // Symbols stored in canonical (un-suffixed) form via normalizeBracketSymbol.
20
+ //
21
+ // Recovery on boot:
22
+ // - If the file exists and is valid, load it.
23
+ // - If not, start empty. On the next position observation (live: getPositions
24
+ // tick; paper: state.json load) the auto-capture pipeline seeds new entries.
25
+ //
26
+ // See docs/POSITION_DECISION_JOURNAL_PLAN.md §4.5.
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { logger, formatError } from '../logger.js';
30
+ import { normalizeBracketSymbol } from './bracket-ledger.js';
31
+ import { resolvePluginsBaseDir } from '../util/plugin-paths.js';
32
+ const TAG = 'position-state-store';
33
+ const DEFAULT_PLUGIN_ID = 'reefclaw-paper-trading';
34
+ const STATE_FILENAME = 'position-state.json';
35
+ const SCHEMA_VERSION = 1;
36
+ const defaultNow = () => Date.now();
37
+ export class PositionStateStore {
38
+ filePath;
39
+ dir;
40
+ now;
41
+ entries = new Map(); // key = canonical symbol
42
+ constructor(pluginId, opts) {
43
+ const base = resolvePluginsBaseDir(opts?.basePath);
44
+ this.dir = join(base, pluginId ?? DEFAULT_PLUGIN_ID);
45
+ this.filePath = join(this.dir, STATE_FILENAME);
46
+ this.now = opts?.now ?? defaultNow;
47
+ if (!existsSync(this.dir))
48
+ mkdirSync(this.dir, { recursive: true });
49
+ this.loadOrInit();
50
+ }
51
+ /** Insert or update a position. Caller is responsible for deciding when
52
+ * to call this — typically on first observation of a new position. */
53
+ upsert(entry) {
54
+ const ts = this.now();
55
+ const symbol = normalizeBracketSymbol(entry.symbol);
56
+ const existing = this.entries.get(symbol);
57
+ const full = {
58
+ ...entry,
59
+ symbol,
60
+ createdAt: existing?.createdAt ?? entry.createdAt ?? ts,
61
+ updatedAt: ts,
62
+ };
63
+ this.entries.set(symbol, full);
64
+ this.persist();
65
+ return full;
66
+ }
67
+ /** Set the webapp positionId after the upsert returns. Idempotent. */
68
+ setWebappId(symbol, webappPositionId) {
69
+ const key = normalizeBracketSymbol(symbol);
70
+ const current = this.entries.get(key);
71
+ if (!current) {
72
+ logger.warn(TAG, `setWebappId: no entry for ${key} — dropping ${webappPositionId}`);
73
+ return;
74
+ }
75
+ if (current.webappPositionId === webappPositionId)
76
+ return;
77
+ this.entries.set(key, { ...current, webappPositionId, updatedAt: this.now() });
78
+ this.persist();
79
+ }
80
+ /** Update review tracking after a successful record_position_reviews call.
81
+ * thesisStatus is optional for backwards compatibility — older callers that
82
+ * predate the exit-gate work pass only verdict; the gate falls back to
83
+ * PASS('inputs_unavailable_thesis_status') when the field isn't cached. */
84
+ recordReview(symbol, verdict, reviewAt, thesisStatus) {
85
+ const key = normalizeBracketSymbol(symbol);
86
+ const current = this.entries.get(key);
87
+ if (!current)
88
+ return;
89
+ this.entries.set(key, {
90
+ ...current,
91
+ lastReviewAt: reviewAt ?? this.now(),
92
+ lastVerdict: verdict,
93
+ lastThesisStatus: thesisStatus ?? current.lastThesisStatus,
94
+ updatedAt: this.now(),
95
+ });
96
+ this.persist();
97
+ }
98
+ /** Add opening contracts to the running size (first opening fill or a
99
+ * scale-in). Initialises the realised-PnL accumulator on first open. No-op
100
+ * if the entry doesn't exist yet (caller upserts the entry first). */
101
+ addOpenContracts(symbol, qty) {
102
+ const key = normalizeBracketSymbol(symbol);
103
+ const current = this.entries.get(key);
104
+ if (!current || !(qty > 0))
105
+ return;
106
+ this.entries.set(key, {
107
+ ...current,
108
+ remainingContracts: (current.remainingContracts ?? 0) + qty,
109
+ realizedPnlAccum: current.realizedPnlAccum ?? 0,
110
+ updatedAt: this.now(),
111
+ });
112
+ this.persist();
113
+ }
114
+ /** Seed remaining contracts from exchange truth at startup. Only sets when
115
+ * the size is currently unknown — never clobbers live fill-tracked state. */
116
+ seedRemainingContracts(symbol, qty) {
117
+ const key = normalizeBracketSymbol(symbol);
118
+ const current = this.entries.get(key);
119
+ if (!current || current.remainingContracts !== undefined || !(qty > 0))
120
+ return;
121
+ this.entries.set(key, {
122
+ ...current,
123
+ remainingContracts: qty,
124
+ realizedPnlAccum: current.realizedPnlAccum ?? 0,
125
+ updatedAt: this.now(),
126
+ });
127
+ this.persist();
128
+ }
129
+ /** Apply a reduce-only exit fill: decrement remaining contracts and sum the
130
+ * fill's realised PnL. Returns the updated entry so the caller can journal a
131
+ * close when the position is flat, or null when there's no tracked entry or
132
+ * the size is unknown (can't decide flatness — defer to the backstop). */
133
+ applyExitContracts(symbol, qty, realizedPnl) {
134
+ const key = normalizeBracketSymbol(symbol);
135
+ const current = this.entries.get(key);
136
+ if (!current || current.remainingContracts === undefined || !(qty > 0))
137
+ return null;
138
+ const updated = {
139
+ ...current,
140
+ remainingContracts: current.remainingContracts - qty,
141
+ realizedPnlAccum: (current.realizedPnlAccum ?? 0) + (Number.isFinite(realizedPnl) ? realizedPnl : 0),
142
+ updatedAt: this.now(),
143
+ };
144
+ this.entries.set(key, updated);
145
+ this.persist();
146
+ return updated;
147
+ }
148
+ get(symbol) {
149
+ return this.entries.get(normalizeBracketSymbol(symbol));
150
+ }
151
+ /** Returns all entries — used by the stale-gate to enumerate open positions. */
152
+ getAll() {
153
+ return Array.from(this.entries.values());
154
+ }
155
+ /** Drop an entry — called when the auto-capture pipeline observes the
156
+ * position has closed (size dropped to zero on the exchange). */
157
+ remove(symbol) {
158
+ const existed = this.entries.delete(normalizeBracketSymbol(symbol));
159
+ if (existed)
160
+ this.persist();
161
+ return existed;
162
+ }
163
+ size() {
164
+ return this.entries.size;
165
+ }
166
+ getFilePath() {
167
+ return this.filePath;
168
+ }
169
+ // ---- Internals ----
170
+ loadOrInit() {
171
+ if (!existsSync(this.filePath)) {
172
+ logger.info(TAG, `No state file at ${this.filePath}; starting empty`);
173
+ return;
174
+ }
175
+ let raw;
176
+ try {
177
+ raw = readFileSync(this.filePath, 'utf8');
178
+ }
179
+ catch (err) {
180
+ throw new Error(`position-state-store: failed to read ${this.filePath}: ${formatError(err)}`);
181
+ }
182
+ let parsed;
183
+ try {
184
+ parsed = JSON.parse(raw);
185
+ }
186
+ catch (err) {
187
+ throw new Error(`position-state-store: ${this.filePath} is not valid JSON: ${formatError(err)}`);
188
+ }
189
+ const file = this.validateFile(parsed);
190
+ for (const entry of file.entries) {
191
+ this.entries.set(entry.symbol, entry);
192
+ }
193
+ logger.info(TAG, `Loaded ${this.entries.size} entries from ${this.filePath}`);
194
+ }
195
+ persist() {
196
+ const file = {
197
+ schemaVersion: SCHEMA_VERSION,
198
+ entries: Array.from(this.entries.values()),
199
+ };
200
+ const tempPath = `${this.filePath}.tmp`;
201
+ writeFileSync(tempPath, JSON.stringify(file, null, 2));
202
+ renameSync(tempPath, this.filePath);
203
+ }
204
+ validateFile(raw) {
205
+ if (!isRecord(raw))
206
+ throw new Error('position-state-store: file root is not an object');
207
+ if (raw.schemaVersion !== SCHEMA_VERSION) {
208
+ throw new Error(`position-state-store: unsupported schemaVersion ${String(raw.schemaVersion)} (expected ${SCHEMA_VERSION})`);
209
+ }
210
+ if (!Array.isArray(raw.entries))
211
+ throw new Error('position-state-store: entries is not an array');
212
+ const entries = [];
213
+ for (const e of raw.entries)
214
+ entries.push(this.validateEntry(e));
215
+ return { schemaVersion: SCHEMA_VERSION, entries };
216
+ }
217
+ validateEntry(raw) {
218
+ if (!isRecord(raw))
219
+ throw new Error('position-state-store: entry is not an object');
220
+ if (typeof raw.symbol !== 'string' || raw.symbol.length === 0) {
221
+ throw new Error('position-state-store: entry missing symbol');
222
+ }
223
+ if (typeof raw.openedAt !== 'number' || !Number.isFinite(raw.openedAt)) {
224
+ throw new Error('position-state-store: entry missing/invalid openedAt');
225
+ }
226
+ if (raw.side !== 'long' && raw.side !== 'short') {
227
+ throw new Error(`position-state-store: bad side ${String(raw.side)}`);
228
+ }
229
+ if (typeof raw.createdAt !== 'number' || typeof raw.updatedAt !== 'number') {
230
+ throw new Error('position-state-store: missing createdAt/updatedAt');
231
+ }
232
+ return raw;
233
+ }
234
+ }
235
+ function isRecord(v) {
236
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
237
+ }
@@ -0,0 +1,64 @@
1
+ import type { BinancePublicApi } from '../ccxt/binance-public.js';
2
+ import type { IExchangeAdapter } from '../exchange-adapter.js';
3
+ import type { AutoCaptureContext } from '../ingest/position-auto-capture.js';
4
+ export interface ProposalDecisionListenerOptions {
5
+ baseUrl: string;
6
+ ingestToken: string;
7
+ userId: string;
8
+ adapter: IExchangeAdapter;
9
+ binanceApi: BinancePublicApi;
10
+ autoCapture?: AutoCaptureContext;
11
+ /** Active poll interval — used while there are pending proposals (or the
12
+ * previous tick fetched any). Default 3 s. Keeps approval-to-fire latency
13
+ * low when the operator is actively working through proposals. */
14
+ pollIntervalMs?: number;
15
+ /** Idle poll interval — used after a tick returns zero pending proposals.
16
+ * Default 15 s. Cuts the constant-warm pressure on the webapp DB by ~5x
17
+ * during idle stretches (no approvals queued). Reset to active on the
18
+ * next non-empty tick. 15 s is well within the shortest hard-expiry
19
+ * bucket (90 s momentum) even allowing for operator decision time. */
20
+ pollIntervalIdleMs?: number;
21
+ /** Maximum |markPrice − proposedEntry| / |proposedEntry − stopPrice|.
22
+ * Default 0.3 per Locked Decision #6. */
23
+ driftAbandonR?: number;
24
+ /** Per-attempt request timeout for HTTP calls to the webapp. */
25
+ requestTimeoutMs?: number;
26
+ fetchImpl?: typeof fetch;
27
+ }
28
+ export interface ProposalDecisionListenerHealth {
29
+ running: boolean;
30
+ ticksTotal: number;
31
+ ticksWithFires: number;
32
+ firesAttempted: number;
33
+ firesSucceeded: number;
34
+ firesAbandonedDrift: number;
35
+ firesFailedTrading: number;
36
+ patchFailures: number;
37
+ pollFailures: number;
38
+ lastTickAt: string | null;
39
+ }
40
+ export declare class ProposalDecisionListener {
41
+ private readonly opts;
42
+ private timer;
43
+ private inFlight;
44
+ /** Current scheduling interval. Starts at active (so the first tick after
45
+ * start() fires promptly even if the queue is empty); switches to idle
46
+ * after any tick that finds zero pending; resets to active on any tick
47
+ * that finds ≥1 pending. */
48
+ private currentIntervalMs;
49
+ private health;
50
+ constructor(options: ProposalDecisionListenerOptions);
51
+ /** Begin the poll loop. Idempotent — subsequent calls are no-ops while
52
+ * the listener is already running. */
53
+ start(): void;
54
+ /** Stop the poll loop. If a tick is in flight, awaits its completion so
55
+ * the PATCH back to the webapp lands before the process exits. */
56
+ stop(): Promise<void>;
57
+ getHealth(): ProposalDecisionListenerHealth;
58
+ private scheduleNextTick;
59
+ private runTick;
60
+ private tick;
61
+ private fetchPending;
62
+ private firePending;
63
+ private patchResult;
64
+ }
@@ -0,0 +1,288 @@
1
+ // ProposalDecisionListener — background poller that fires operator-approved
2
+ // proposals via the existing create_order backend path.
3
+ //
4
+ // Phase B implementation: polls GET /api/internal/proposed_orders/pending-decisions
5
+ // at config.pollIntervalMs (default 3 sec). For each approved-but-unfired row:
6
+ // 1. Drift abandon — if mark price has moved more than ±0.3 R from the
7
+ // proposed entry since the proposal was created, fail with a transparent
8
+ // `fireError` rather than firing into a different setup.
9
+ // 2. Fire via createOrderTool — same backend function the toggle-off path
10
+ // calls directly. Reuses the pre-trade risk gate, bracket attach, and
11
+ // auto-capture entry-row write.
12
+ // 3. PATCH /fire-result with either firedOrderId or fireError. Fired-once
13
+ // idempotency lives in the DB constraint (status='approved' AND
14
+ // fired_order_id IS NULL filter).
15
+ //
16
+ // Started by index.ts only when config.approval.mode === 'per_trade' AND
17
+ // ingest credentials are present. Drained at shutdown so an in-flight fire
18
+ // completes its PATCH before the process exits.
19
+ //
20
+ // Why polling and not relay events: simpler — no skill <-> plugin event-bus
21
+ // changes needed. 3-sec poll is well under the latency a human approval flow
22
+ // tolerates. Refactor to SSE/relay-push only if measurement shows the poll
23
+ // load is meaningful (very unlikely — ~3 req/s per user is negligible).
24
+ //
25
+ // See docs/APPROVAL_MODE_DESIGN.md §2 (architecture) + §7.3 (re-validation) +
26
+ // §10 failure matrix.
27
+ import { logger, formatError } from '../logger.js';
28
+ import { createOrderTool } from '../tools/create-order.js';
29
+ const TAG = 'proposal-decision-listener';
30
+ export class ProposalDecisionListener {
31
+ opts;
32
+ timer = null;
33
+ inFlight = null;
34
+ /** Current scheduling interval. Starts at active (so the first tick after
35
+ * start() fires promptly even if the queue is empty); switches to idle
36
+ * after any tick that finds zero pending; resets to active on any tick
37
+ * that finds ≥1 pending. */
38
+ currentIntervalMs;
39
+ health = {
40
+ running: false,
41
+ ticksTotal: 0,
42
+ ticksWithFires: 0,
43
+ firesAttempted: 0,
44
+ firesSucceeded: 0,
45
+ firesAbandonedDrift: 0,
46
+ firesFailedTrading: 0,
47
+ patchFailures: 0,
48
+ pollFailures: 0,
49
+ lastTickAt: null,
50
+ };
51
+ constructor(options) {
52
+ this.opts = {
53
+ baseUrl: options.baseUrl.replace(/\/+$/, ''),
54
+ ingestToken: options.ingestToken,
55
+ userId: options.userId,
56
+ adapter: options.adapter,
57
+ binanceApi: options.binanceApi,
58
+ autoCapture: options.autoCapture,
59
+ pollIntervalMs: options.pollIntervalMs ?? 3_000,
60
+ pollIntervalIdleMs: options.pollIntervalIdleMs ?? 15_000,
61
+ driftAbandonR: options.driftAbandonR ?? 0.3,
62
+ requestTimeoutMs: options.requestTimeoutMs ?? 10_000,
63
+ fetchImpl: options.fetchImpl ?? fetch,
64
+ };
65
+ this.currentIntervalMs = this.opts.pollIntervalMs;
66
+ }
67
+ /** Begin the poll loop. Idempotent — subsequent calls are no-ops while
68
+ * the listener is already running. */
69
+ start() {
70
+ if (this.health.running)
71
+ return;
72
+ this.health.running = true;
73
+ logger.info(TAG, `Listener started — polling ${this.opts.baseUrl}/api/internal/proposed_orders/pending-decisions (active ${this.opts.pollIntervalMs} ms / idle ${this.opts.pollIntervalIdleMs} ms; userId=${this.opts.userId.slice(0, 8)}…)`);
74
+ this.scheduleNextTick();
75
+ }
76
+ /** Stop the poll loop. If a tick is in flight, awaits its completion so
77
+ * the PATCH back to the webapp lands before the process exits. */
78
+ async stop() {
79
+ this.health.running = false;
80
+ if (this.timer) {
81
+ clearTimeout(this.timer);
82
+ this.timer = null;
83
+ }
84
+ if (this.inFlight) {
85
+ try {
86
+ await this.inFlight;
87
+ }
88
+ catch { /* tick errors are logged inside tick() */ }
89
+ }
90
+ logger.info(TAG, 'Listener stopped');
91
+ }
92
+ getHealth() {
93
+ return { ...this.health };
94
+ }
95
+ // ---- Internals ----
96
+ scheduleNextTick() {
97
+ if (!this.health.running)
98
+ return;
99
+ this.timer = setTimeout(() => {
100
+ void this.runTick();
101
+ }, this.currentIntervalMs);
102
+ }
103
+ async runTick() {
104
+ if (!this.health.running)
105
+ return;
106
+ this.inFlight = this.tick().catch((err) => {
107
+ logger.warn(TAG, `Tick threw (caught): ${formatError(err)}`);
108
+ });
109
+ try {
110
+ await this.inFlight;
111
+ }
112
+ finally {
113
+ this.inFlight = null;
114
+ this.scheduleNextTick();
115
+ }
116
+ }
117
+ async tick() {
118
+ this.health.ticksTotal++;
119
+ this.health.lastTickAt = new Date().toISOString();
120
+ const pending = await this.fetchPending();
121
+ if (pending.length === 0) {
122
+ // Idle backoff — schedule next tick at the longer idle interval until
123
+ // the queue becomes non-empty. This is the dominant cost-reduction lever
124
+ // when Approval Mode is active but the operator is not currently
125
+ // working through proposals. The next non-empty tick resets to active.
126
+ this.currentIntervalMs = this.opts.pollIntervalIdleMs;
127
+ return;
128
+ }
129
+ // Reset to fast polling so subsequent approvals fire with minimal latency.
130
+ this.currentIntervalMs = this.opts.pollIntervalMs;
131
+ this.health.ticksWithFires++;
132
+ logger.info(TAG, `${pending.length} approved proposal(s) pending fire`);
133
+ for (const p of pending) {
134
+ try {
135
+ await this.firePending(p);
136
+ }
137
+ catch (err) {
138
+ // Defensive — firePending should already write a fireError PATCH on
139
+ // any failure path. If something escapes, surface it loudly so the
140
+ // operator can diagnose; the proposal will remain in approved state
141
+ // and be retried on the next tick (idempotent because firedOrderId
142
+ // is still NULL until a successful PATCH writes it).
143
+ logger.error(TAG, `Unhandled exception firing proposal ${p.id} (will retry next tick): ${formatError(err)}`);
144
+ }
145
+ }
146
+ }
147
+ async fetchPending() {
148
+ const url = `${this.opts.baseUrl}/api/internal/proposed_orders/pending-decisions`;
149
+ try {
150
+ const ac = new AbortController();
151
+ const tid = setTimeout(() => ac.abort(), this.opts.requestTimeoutMs);
152
+ let res;
153
+ try {
154
+ res = await this.opts.fetchImpl(url, {
155
+ method: 'GET',
156
+ headers: {
157
+ authorization: `Bearer ${this.opts.ingestToken}`,
158
+ 'x-user-id': this.opts.userId,
159
+ },
160
+ signal: ac.signal,
161
+ });
162
+ }
163
+ finally {
164
+ clearTimeout(tid);
165
+ }
166
+ if (!res.ok) {
167
+ this.health.pollFailures++;
168
+ logger.warn(TAG, `GET ${url} → ${res.status}`);
169
+ return [];
170
+ }
171
+ const body = await res.json();
172
+ return Array.isArray(body.proposals) ? body.proposals : [];
173
+ }
174
+ catch (err) {
175
+ this.health.pollFailures++;
176
+ logger.warn(TAG, `GET ${url} threw: ${formatError(err)}`);
177
+ return [];
178
+ }
179
+ }
180
+ async firePending(p) {
181
+ this.health.firesAttempted++;
182
+ // ---- 1. Drift abandon (Locked Decision #6) ----
183
+ // Skip if we can't read a current price — better to let the trading path
184
+ // surface the error than to silently abandon on a transient ticker miss.
185
+ const lastPrice = await this.opts.adapter.getLastPrice(p.symbol).catch(() => null);
186
+ if (lastPrice != null && Number.isFinite(lastPrice) && lastPrice > 0) {
187
+ const rDistance = Math.abs(p.proposedEntry - p.stopPrice);
188
+ if (rDistance > 0) {
189
+ const driftR = Math.abs(lastPrice - p.proposedEntry) / rDistance;
190
+ if (driftR > this.opts.driftAbandonR) {
191
+ const msg = `price_drift: mark ${lastPrice} is ${driftR.toFixed(2)}R from proposed entry ${p.proposedEntry} (limit ${this.opts.driftAbandonR}R)`;
192
+ logger.warn(TAG, `Abandoning proposal ${p.id} — ${msg}`);
193
+ await this.patchResult(p.id, { fireError: msg });
194
+ this.health.firesAbandonedDrift++;
195
+ return;
196
+ }
197
+ }
198
+ }
199
+ // ---- 2. Fire via createOrderTool (same path toggle-off mode uses) ----
200
+ // CRITICAL: do NOT pass proposalManager + userId here. The listener IS the
201
+ // consumer of approved proposals; passing proposalManager would recursively
202
+ // dual-write another proposal row for the same intent. The agent's path
203
+ // dual-writes; the listener's path fires the underlying order.
204
+ const result = await createOrderTool({
205
+ symbol: p.symbol,
206
+ side: p.side,
207
+ type: p.orderType,
208
+ amount: p.size,
209
+ // For limit orders we honour the original proposed entry. For market,
210
+ // the create_order tool reads the live ticker (per existing semantics).
211
+ price: p.orderType === 'limit' ? p.proposedEntry : undefined,
212
+ stopPrice: p.stopPrice,
213
+ target_price: p.targetPrice,
214
+ setup_type: p.setupType,
215
+ thesis: p.thesis,
216
+ regime: p.regime,
217
+ regime_confidence: p.regimeConfidence,
218
+ scorecard_verdict: p.scorecardVerdict,
219
+ confluence_score: p.confluenceScore,
220
+ }, {
221
+ binanceApi: this.opts.binanceApi,
222
+ adapter: this.opts.adapter,
223
+ autoCapture: this.opts.autoCapture,
224
+ // proposalManager + userId deliberately omitted — see above.
225
+ });
226
+ if ('error' in result) {
227
+ // Trading-path rejection (risk gate, bracket attach failure, exchange
228
+ // rejection, etc.). Surface to the operator via the proposal row.
229
+ logger.warn(TAG, `Proposal ${p.id} fire rejected: ${result.error}`);
230
+ await this.patchResult(p.id, { fireError: result.error });
231
+ this.health.firesFailedTrading++;
232
+ return;
233
+ }
234
+ if ('status' in result && result.status === 'pending_approval') {
235
+ // Should be unreachable: the listener calls createOrderTool without
236
+ // proposalManager, so the per_trade branch can't trigger. If it does
237
+ // (config race or future refactor), surface as a fireError so we don't
238
+ // silently lose the proposal.
239
+ const msg = `unexpected pending_approval from listener-side fire (proposal_id=${result.proposal_id})`;
240
+ logger.error(TAG, msg);
241
+ await this.patchResult(p.id, { fireError: msg });
242
+ this.health.firesFailedTrading++;
243
+ return;
244
+ }
245
+ const firedOrderId = typeof result.id === 'string' ? result.id : `unknown-${Date.now()}`;
246
+ logger.info(TAG, `Proposal ${p.id} fired → order ${firedOrderId}`);
247
+ await this.patchResult(p.id, { firedOrderId });
248
+ this.health.firesSucceeded++;
249
+ }
250
+ async patchResult(proposalId, body) {
251
+ const url = `${this.opts.baseUrl}/api/internal/proposed_orders/${proposalId}/fire-result`;
252
+ try {
253
+ const ac = new AbortController();
254
+ const tid = setTimeout(() => ac.abort(), this.opts.requestTimeoutMs);
255
+ let res;
256
+ try {
257
+ res = await this.opts.fetchImpl(url, {
258
+ method: 'PATCH',
259
+ headers: {
260
+ 'content-type': 'application/json',
261
+ authorization: `Bearer ${this.opts.ingestToken}`,
262
+ 'x-user-id': this.opts.userId,
263
+ },
264
+ body: JSON.stringify(body),
265
+ signal: ac.signal,
266
+ });
267
+ }
268
+ finally {
269
+ clearTimeout(tid);
270
+ }
271
+ if (res.status === 409) {
272
+ // Already-recorded race — listener double-fired but webapp's idempotency
273
+ // guard caught the duplicate. Not an error.
274
+ logger.info(TAG, `Proposal ${proposalId} fire-result already recorded (409, expected on retry)`);
275
+ return;
276
+ }
277
+ if (!res.ok) {
278
+ this.health.patchFailures++;
279
+ logger.warn(TAG, `PATCH ${url} → ${res.status}`);
280
+ return;
281
+ }
282
+ }
283
+ catch (err) {
284
+ this.health.patchFailures++;
285
+ logger.warn(TAG, `PATCH ${url} threw: ${formatError(err)}`);
286
+ }
287
+ }
288
+ }
@@ -0,0 +1,76 @@
1
+ import { type SetupBucket } from './setup-buckets.js';
2
+ /** Trade params + agent metadata composing a single proposal. Mirrors the
3
+ * /api/internal/proposed_orders payload shape. */
4
+ export interface ProposalRequest {
5
+ symbol: string;
6
+ side: 'buy' | 'sell';
7
+ orderType: 'market' | 'limit';
8
+ size: number;
9
+ proposedEntry: number;
10
+ stopPrice: number;
11
+ targetPrice: number;
12
+ riskUsd: number;
13
+ thesis: string;
14
+ setupType: string;
15
+ regime: string;
16
+ regimeConfidence: number;
17
+ scorecardVerdict: string;
18
+ confluenceScore: number;
19
+ /** Phase C — when set, the named proposal is atomically marked `superseded`
20
+ * by the webapp and the new row links back via supersedes_id. Use this when
21
+ * the agent is modifying a still-pending proposal (e.g. after operator
22
+ * discussion). Must reference a pending/discussing proposal owned by the
23
+ * same user, or the supersede half is a no-op. */
24
+ supersedesId?: string;
25
+ /** 'shadow' = Phase A telemetry-only row (agent's create_order also fires
26
+ * the real order synchronously). The webapp filters these out of the
27
+ * operator UI so they don't appear as actionable cards. 'real' = Phase B/C
28
+ * actionable proposal the operator decides on. Defaults to 'real' on the
29
+ * webapp side if omitted. */
30
+ origin?: 'shadow' | 'real';
31
+ }
32
+ export interface ProposalManagerOptions {
33
+ baseUrl: string;
34
+ ingestToken: string;
35
+ fetchImpl?: typeof fetch;
36
+ requestTimeoutMs?: number;
37
+ maxAttempts?: number;
38
+ baseBackoffMs?: number;
39
+ backoffCapMs?: number;
40
+ /** Override clock for tests. */
41
+ now?: () => Date;
42
+ /** Override UUID generator for tests. */
43
+ uuid?: () => string;
44
+ }
45
+ export interface ProposalManagerHealth {
46
+ inFlight: number;
47
+ totalPosted: number;
48
+ totalSucceeded: number;
49
+ totalDroppedTerminal: number;
50
+ totalDroppedRetriesExhausted: number;
51
+ }
52
+ /** Result of a propose() call. The proposal_uuid is generated synchronously
53
+ * before any network I/O so the caller can correlate logs immediately. */
54
+ export interface ProposeResult {
55
+ proposalUuid: string;
56
+ setupBucket: SetupBucket;
57
+ hardExpiresAt: Date;
58
+ }
59
+ export declare class ProposalManager {
60
+ private readonly opts;
61
+ private inFlight;
62
+ private totalPosted;
63
+ private totalSucceeded;
64
+ private totalDroppedTerminal;
65
+ private totalDroppedRetriesExhausted;
66
+ constructor(options: ProposalManagerOptions);
67
+ /** Submit a proposal. Generates the proposal_uuid + computes the bucket
68
+ * + hard expiry synchronously; the actual POST runs in the background.
69
+ * Caller may correlate via the returned proposalUuid before the post lands. */
70
+ propose(userId: string, req: ProposalRequest): ProposeResult;
71
+ /** Await all in-flight POSTs. Used at shutdown so we don't lose proposals. */
72
+ drain(): Promise<void>;
73
+ getHealth(): ProposalManagerHealth;
74
+ private run;
75
+ private sleepBackoff;
76
+ }