@senpi/runtime 3.0.173 → 3.0.181

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 (293) hide show
  1. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  2. package/dist/actions/close-position/close-position.action.js +48 -3
  3. package/dist/actions/close-position/close-position.action.js.map +1 -1
  4. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +8 -0
  5. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
  6. package/dist/actions/llm-decision/openclaw-llm-decision.js +56 -1
  7. package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
  8. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  9. package/dist/actions/open-position/open-position.action.js +42 -6
  10. package/dist/actions/open-position/open-position.action.js.map +1 -1
  11. package/dist/actions/open-position/resolve-margin.d.ts +1 -1
  12. package/dist/actions/open-position/resolve-margin.d.ts.map +1 -1
  13. package/dist/actions/open-position/resolve-margin.js +7 -0
  14. package/dist/actions/open-position/resolve-margin.js.map +1 -1
  15. package/dist/approvals/approval-store.d.ts +37 -7
  16. package/dist/approvals/approval-store.d.ts.map +1 -1
  17. package/dist/approvals/approval-store.js +127 -23
  18. package/dist/approvals/approval-store.js.map +1 -1
  19. package/dist/approvals/manifest.d.ts.map +1 -1
  20. package/dist/approvals/manifest.js +0 -5
  21. package/dist/approvals/manifest.js.map +1 -1
  22. package/dist/approvals/telemetry.d.ts +27 -3
  23. package/dist/approvals/telemetry.d.ts.map +1 -1
  24. package/dist/approvals/telemetry.js +8 -4
  25. package/dist/approvals/telemetry.js.map +1 -1
  26. package/dist/approvals/tool-approval-gate.d.ts +79 -0
  27. package/dist/approvals/tool-approval-gate.d.ts.map +1 -1
  28. package/dist/approvals/tool-approval-gate.js +184 -21
  29. package/dist/approvals/tool-approval-gate.js.map +1 -1
  30. package/dist/approvals/tool-approvals-manifest.json +0 -2
  31. package/dist/approvals/turn-context.d.ts +271 -0
  32. package/dist/approvals/turn-context.d.ts.map +1 -0
  33. package/dist/approvals/turn-context.js +551 -0
  34. package/dist/approvals/turn-context.js.map +1 -0
  35. package/dist/cli/guide-commands.d.ts.map +1 -1
  36. package/dist/cli/guide-commands.js +43 -8
  37. package/dist/cli/guide-commands.js.map +1 -1
  38. package/dist/cli/senpi-commands.d.ts.map +1 -1
  39. package/dist/cli/senpi-commands.js +91 -12
  40. package/dist/cli/senpi-commands.js.map +1 -1
  41. package/dist/constants/dsl/index.d.ts +2 -0
  42. package/dist/constants/dsl/index.d.ts.map +1 -1
  43. package/dist/constants/dsl/index.js +2 -0
  44. package/dist/constants/dsl/index.js.map +1 -1
  45. package/dist/constants/order-types.d.ts.map +1 -1
  46. package/dist/constants/order-types.js +4 -1
  47. package/dist/constants/order-types.js.map +1 -1
  48. package/dist/deploy/job.d.ts +32 -0
  49. package/dist/deploy/job.d.ts.map +1 -1
  50. package/dist/deploy/job.js +72 -7
  51. package/dist/deploy/job.js.map +1 -1
  52. package/dist/deploy/messages.d.ts +15 -1
  53. package/dist/deploy/messages.d.ts.map +1 -1
  54. package/dist/deploy/messages.js +78 -0
  55. package/dist/deploy/messages.js.map +1 -1
  56. package/dist/deploy/orchestrator.d.ts +11 -1
  57. package/dist/deploy/orchestrator.d.ts.map +1 -1
  58. package/dist/deploy/orchestrator.js +111 -15
  59. package/dist/deploy/orchestrator.js.map +1 -1
  60. package/dist/deploy/register.d.ts +18 -0
  61. package/dist/deploy/register.d.ts.map +1 -1
  62. package/dist/deploy/register.js +93 -2
  63. package/dist/deploy/register.js.map +1 -1
  64. package/dist/dsl/engine/exchange-stop.d.ts +21 -1
  65. package/dist/dsl/engine/exchange-stop.d.ts.map +1 -1
  66. package/dist/dsl/engine/exchange-stop.js +27 -1
  67. package/dist/dsl/engine/exchange-stop.js.map +1 -1
  68. package/dist/dsl/engine/floors.d.ts +2 -1
  69. package/dist/dsl/engine/floors.d.ts.map +1 -1
  70. package/dist/dsl/engine/floors.js +2 -1
  71. package/dist/dsl/engine/floors.js.map +1 -1
  72. package/dist/dsl/engine/index.d.ts +1 -1
  73. package/dist/dsl/engine/index.d.ts.map +1 -1
  74. package/dist/dsl/engine/index.js +1 -1
  75. package/dist/dsl/engine/index.js.map +1 -1
  76. package/dist/dsl/engine/tick.d.ts.map +1 -1
  77. package/dist/dsl/engine/tick.js +4 -3
  78. package/dist/dsl/engine/tick.js.map +1 -1
  79. package/dist/dsl/events/handlers.d.ts +7 -3
  80. package/dist/dsl/events/handlers.d.ts.map +1 -1
  81. package/dist/dsl/events/handlers.js +10 -4
  82. package/dist/dsl/events/handlers.js.map +1 -1
  83. package/dist/dsl/events/initial-state.d.ts.map +1 -1
  84. package/dist/dsl/events/initial-state.js +2 -0
  85. package/dist/dsl/events/initial-state.js.map +1 -1
  86. package/dist/dsl/monitor/exchange-sync.d.ts +1 -0
  87. package/dist/dsl/monitor/exchange-sync.d.ts.map +1 -1
  88. package/dist/dsl/monitor/exchange-sync.js +7 -14
  89. package/dist/dsl/monitor/exchange-sync.js.map +1 -1
  90. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  91. package/dist/dsl/monitor/process-one-position.js +108 -17
  92. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  93. package/dist/dsl/notifications/formatter.d.ts +2 -1
  94. package/dist/dsl/notifications/formatter.d.ts.map +1 -1
  95. package/dist/dsl/notifications/formatter.js +18 -1
  96. package/dist/dsl/notifications/formatter.js.map +1 -1
  97. package/dist/health/types.d.ts +6 -1
  98. package/dist/health/types.d.ts.map +1 -1
  99. package/dist/health/types.js.map +1 -1
  100. package/dist/host/openclaw-host.d.ts +138 -0
  101. package/dist/host/openclaw-host.d.ts.map +1 -0
  102. package/dist/host/openclaw-host.js +177 -0
  103. package/dist/host/openclaw-host.js.map +1 -0
  104. package/dist/index.d.ts +46 -2
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +318 -38
  107. package/dist/index.js.map +1 -1
  108. package/dist/leverage/max-leverage-cache.d.ts +25 -0
  109. package/dist/leverage/max-leverage-cache.d.ts.map +1 -1
  110. package/dist/leverage/max-leverage-cache.js +77 -0
  111. package/dist/leverage/max-leverage-cache.js.map +1 -1
  112. package/dist/runtime/auto-update/coordinator.d.ts +203 -0
  113. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  114. package/dist/runtime/auto-update/coordinator.js +621 -12
  115. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  116. package/dist/runtime/auto-update/gateway-restart.d.ts +60 -1
  117. package/dist/runtime/auto-update/gateway-restart.d.ts.map +1 -1
  118. package/dist/runtime/auto-update/gateway-restart.js +95 -21
  119. package/dist/runtime/auto-update/gateway-restart.js.map +1 -1
  120. package/dist/runtime/auto-update/idle-gate.d.ts +139 -0
  121. package/dist/runtime/auto-update/idle-gate.d.ts.map +1 -0
  122. package/dist/runtime/auto-update/idle-gate.js +217 -0
  123. package/dist/runtime/auto-update/idle-gate.js.map +1 -0
  124. package/dist/runtime/auto-update/install-results.d.ts +130 -0
  125. package/dist/runtime/auto-update/install-results.d.ts.map +1 -0
  126. package/dist/runtime/auto-update/install-results.js +264 -0
  127. package/dist/runtime/auto-update/install-results.js.map +1 -0
  128. package/dist/runtime/auto-update/money-call-tracker.d.ts +36 -0
  129. package/dist/runtime/auto-update/money-call-tracker.d.ts.map +1 -0
  130. package/dist/runtime/auto-update/money-call-tracker.js +129 -0
  131. package/dist/runtime/auto-update/money-call-tracker.js.map +1 -0
  132. package/dist/runtime/auto-update/register-idle-gate.d.ts +70 -0
  133. package/dist/runtime/auto-update/register-idle-gate.d.ts.map +1 -0
  134. package/dist/runtime/auto-update/register-idle-gate.js +123 -0
  135. package/dist/runtime/auto-update/register-idle-gate.js.map +1 -0
  136. package/dist/runtime/auto-update/run-update-command.d.ts +13 -0
  137. package/dist/runtime/auto-update/run-update-command.d.ts.map +1 -1
  138. package/dist/runtime/auto-update/run-update-command.js +18 -0
  139. package/dist/runtime/auto-update/run-update-command.js.map +1 -1
  140. package/dist/runtime/auto-update/state-store.d.ts +14 -1
  141. package/dist/runtime/auto-update/state-store.d.ts.map +1 -1
  142. package/dist/runtime/auto-update/state-store.js +37 -3
  143. package/dist/runtime/auto-update/state-store.js.map +1 -1
  144. package/dist/runtime/auto-update/types.d.ts +51 -0
  145. package/dist/runtime/auto-update/types.d.ts.map +1 -1
  146. package/dist/runtime/auto-update/types.js.map +1 -1
  147. package/dist/runtime/auto-update/update-marker.d.ts +95 -0
  148. package/dist/runtime/auto-update/update-marker.d.ts.map +1 -0
  149. package/dist/runtime/auto-update/update-marker.js +185 -0
  150. package/dist/runtime/auto-update/update-marker.js.map +1 -0
  151. package/dist/runtime/auto-update/update-schedule.d.ts +57 -0
  152. package/dist/runtime/auto-update/update-schedule.d.ts.map +1 -0
  153. package/dist/runtime/auto-update/update-schedule.js +75 -0
  154. package/dist/runtime/auto-update/update-schedule.js.map +1 -0
  155. package/dist/runtime/create-scanner-module.d.ts +1 -1
  156. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  157. package/dist/runtime/create-scanner-module.js +2 -1
  158. package/dist/runtime/create-scanner-module.js.map +1 -1
  159. package/dist/runtime/decision-errors.d.ts +7 -0
  160. package/dist/runtime/decision-errors.d.ts.map +1 -1
  161. package/dist/runtime/decision-errors.js +15 -0
  162. package/dist/runtime/decision-errors.js.map +1 -1
  163. package/dist/runtime/lifecycle-events.d.ts +1 -1
  164. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  165. package/dist/runtime/lifecycle-events.js +10 -1
  166. package/dist/runtime/lifecycle-events.js.map +1 -1
  167. package/dist/runtime/runtime-descriptor.d.ts +11 -2
  168. package/dist/runtime/runtime-descriptor.d.ts.map +1 -1
  169. package/dist/runtime/runtime-descriptor.js +11 -2
  170. package/dist/runtime/runtime-descriptor.js.map +1 -1
  171. package/dist/runtime/runtime.d.ts.map +1 -1
  172. package/dist/runtime/runtime.js +4 -3
  173. package/dist/runtime/runtime.js.map +1 -1
  174. package/dist/runtime/strategy-lifecycle.js +1 -1
  175. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  176. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  177. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  178. package/dist/runtime-api/intake/acceptance-record.d.ts +17 -3
  179. package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
  180. package/dist/runtime-api/intake/acceptance-record.js +57 -32
  181. package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
  182. package/dist/runtime-api/intake/intake-events.d.ts +6 -5
  183. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  184. package/dist/runtime-api/intake/intake-events.js +9 -3
  185. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  186. package/dist/runtime-api/intake/process-signal-post.d.ts +1 -1
  187. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  188. package/dist/runtime-api/intake/process-signal-post.js +34 -31
  189. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  190. package/dist/runtime-api/intake/scaffold-error-router.d.ts +8 -0
  191. package/dist/runtime-api/intake/scaffold-error-router.d.ts.map +1 -1
  192. package/dist/runtime-api/intake/scaffold-error-router.js +9 -0
  193. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  194. package/dist/runtime-api/intake/types.d.ts +9 -0
  195. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  196. package/dist/runtime-api/intake/types.js +5 -1
  197. package/dist/runtime-api/intake/types.js.map +1 -1
  198. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  199. package/dist/runtime-api/routes/signals.js +25 -1
  200. package/dist/runtime-api/routes/signals.js.map +1 -1
  201. package/dist/runtime-api/routes/signals.schema.d.ts +2 -0
  202. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  203. package/dist/runtime-api/routes/signals.schema.js +4 -0
  204. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  205. package/dist/scanners/engine/data-providers.d.ts +6 -0
  206. package/dist/scanners/engine/data-providers.d.ts.map +1 -1
  207. package/dist/scanners/engine/data-providers.js.map +1 -1
  208. package/dist/scanners/engine/engine.d.ts +9 -0
  209. package/dist/scanners/engine/engine.d.ts.map +1 -1
  210. package/dist/scanners/engine/engine.js +29 -6
  211. package/dist/scanners/engine/engine.js.map +1 -1
  212. package/dist/scanners/external/scaffold/ctx.py +55 -1
  213. package/dist/scanners/external/scaffold/delivery.py +92 -28
  214. package/dist/scanners/external/scaffold/preflight.py +1 -1
  215. package/dist/scanners/external/scaffold/scaffold.py +75 -2
  216. package/dist/scanners/runtime-module.d.ts +2 -0
  217. package/dist/scanners/runtime-module.d.ts.map +1 -1
  218. package/dist/scanners/runtime-module.js +14 -0
  219. package/dist/scanners/runtime-module.js.map +1 -1
  220. package/dist/senpi/client.d.ts +36 -1
  221. package/dist/senpi/client.d.ts.map +1 -1
  222. package/dist/senpi/client.js +100 -8
  223. package/dist/senpi/client.js.map +1 -1
  224. package/dist/senpi/types.d.ts +54 -2
  225. package/dist/senpi/types.d.ts.map +1 -1
  226. package/dist/telemetry/metrics.d.ts +15 -1
  227. package/dist/telemetry/metrics.d.ts.map +1 -1
  228. package/dist/telemetry/metrics.js +25 -1
  229. package/dist/telemetry/metrics.js.map +1 -1
  230. package/dist/telemetry/otel-sdk.d.ts +12 -0
  231. package/dist/telemetry/otel-sdk.d.ts.map +1 -1
  232. package/dist/telemetry/otel-sdk.js +47 -1
  233. package/dist/telemetry/otel-sdk.js.map +1 -1
  234. package/dist/telemetry/spans/span-hooks.d.ts +18 -1
  235. package/dist/telemetry/spans/span-hooks.d.ts.map +1 -1
  236. package/dist/telemetry/spans/span-hooks.js +306 -96
  237. package/dist/telemetry/spans/span-hooks.js.map +1 -1
  238. package/dist/telemetry/tool-plane.d.ts +2 -0
  239. package/dist/telemetry/tool-plane.d.ts.map +1 -1
  240. package/dist/telemetry/tool-plane.js +1 -0
  241. package/dist/telemetry/tool-plane.js.map +1 -1
  242. package/dist/tool-surface/crib-cache.d.ts +21 -0
  243. package/dist/tool-surface/crib-cache.d.ts.map +1 -0
  244. package/dist/tool-surface/crib-cache.js +54 -0
  245. package/dist/tool-surface/crib-cache.js.map +1 -0
  246. package/dist/tool-surface/crib-hook.d.ts +78 -0
  247. package/dist/tool-surface/crib-hook.d.ts.map +1 -0
  248. package/dist/tool-surface/crib-hook.js +217 -0
  249. package/dist/tool-surface/crib-hook.js.map +1 -0
  250. package/dist/tool-surface/crib-sheet.d.ts +56 -0
  251. package/dist/tool-surface/crib-sheet.d.ts.map +1 -0
  252. package/dist/tool-surface/crib-sheet.js +129 -0
  253. package/dist/tool-surface/crib-sheet.js.map +1 -0
  254. package/dist/tool-surface/mcp-tool-source.d.ts +64 -0
  255. package/dist/tool-surface/mcp-tool-source.d.ts.map +1 -0
  256. package/dist/tool-surface/mcp-tool-source.js +189 -0
  257. package/dist/tool-surface/mcp-tool-source.js.map +1 -0
  258. package/dist/types/dsl/index.d.ts +4 -0
  259. package/dist/types/dsl/index.d.ts.map +1 -1
  260. package/dist/utils/event-catalog.d.ts +141 -10
  261. package/dist/utils/event-catalog.d.ts.map +1 -1
  262. package/dist/utils/event-catalog.js +23 -1
  263. package/dist/utils/event-catalog.js.map +1 -1
  264. package/dist/utils/perp-coin.d.ts.map +1 -1
  265. package/dist/utils/perp-coin.js +2 -1
  266. package/dist/utils/perp-coin.js.map +1 -1
  267. package/dist/utils/resolve-order-type.d.ts.map +1 -1
  268. package/dist/utils/resolve-order-type.js +5 -1
  269. package/dist/utils/resolve-order-type.js.map +1 -1
  270. package/dist/validate/codes.d.ts +2 -0
  271. package/dist/validate/codes.d.ts.map +1 -1
  272. package/dist/validate/codes.js +6 -0
  273. package/dist/validate/codes.js.map +1 -1
  274. package/dist/validate/index.d.ts +1 -1
  275. package/dist/validate/index.d.ts.map +1 -1
  276. package/dist/validate/index.js +1 -1
  277. package/dist/validate/index.js.map +1 -1
  278. package/dist/validate/recipe-checks.d.ts +7 -0
  279. package/dist/validate/recipe-checks.d.ts.map +1 -1
  280. package/dist/validate/recipe-checks.js +18 -0
  281. package/dist/validate/recipe-checks.js.map +1 -1
  282. package/dist/validate/recipe.d.ts.map +1 -1
  283. package/dist/validate/recipe.js +2 -1
  284. package/dist/validate/recipe.js.map +1 -1
  285. package/dist/widgets/tool.d.ts +9 -0
  286. package/dist/widgets/tool.d.ts.map +1 -1
  287. package/dist/widgets/tool.js +1 -0
  288. package/dist/widgets/tool.js.map +1 -1
  289. package/examples/strategies/README.md +2 -1
  290. package/examples/strategies/iguana/README.md +3 -3
  291. package/examples/strategies/raptor/README.md +3 -3
  292. package/openclaw.plugin.json +11 -1
  293. package/package.json +5 -2
package/dist/index.js CHANGED
@@ -14,11 +14,12 @@
14
14
  import { readFileSync } from "node:fs";
15
15
  import { parse as parseYaml } from "yaml";
16
16
  import { join, dirname } from "node:path";
17
- import { SenpiClient } from "./senpi/client.js";
17
+ import { isMoneyCallTool, SenpiClient, senpiOrderCallsInFlight } from "./senpi/client.js";
18
+ import { MoneyCallTracker, registerMoneyCallHooks } from "./runtime/auto-update/money-call-tracker.js";
18
19
  import { buildWalletStatusHint } from "./senpi/strategy-status.js";
19
20
  import { bootSkippedStatusLabel, partitionByWalletStatus, readBootWalletStatuses, trackableWalletKeys, } from "./runtime/boot-skip.js";
20
21
  import { DeployJobController } from "./deploy/job.js";
21
- import { registerDeployGatewayMethods } from "./deploy/register.js";
22
+ import { DEPLOY_CAPABILITIES, registerDeployGatewayMethods } from "./deploy/register.js";
22
23
  import { createQuantDesk } from "./quant/register.js";
23
24
  import { registerUpdateGatewayMethod } from "./runtime/update-register.js";
24
25
  import { createStrategiesPublisher } from "./runtime/strategies-telemetry.js";
@@ -28,6 +29,7 @@ import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, runtimeC
28
29
  import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
29
30
  import { AuditQueryError } from "./runtime/audit-query.types.js";
30
31
  import { buildRuntimeStateDir, archiveStateDir } from "./runtime/state-layout.js";
32
+ import { KeyedMutex } from "./utils/keyed-mutex.js";
31
33
  import { describeRuntimeEntry } from "./runtime/runtime-descriptor.js";
32
34
  import { registerSenpiCli } from "./cli/senpi-commands.js";
33
35
  import { readSenpiCliStore, resolveSenpiApiKeyFromStore, resolveSenpiBaseStateDir, } from "./cli/senpi-cli-config.js";
@@ -39,6 +41,7 @@ import { resolveAutoUpdateConfig } from "./runtime/auto-update/types.js";
39
41
  import { SkillsCoordinatorUnavailableError, SkillsUpdateCoordinator, resolveSkillsManagerConfig, } from "./skills-manager/index.js";
40
42
  import { createScannerProvidersForRun } from "./runtime/create-scanner-providers-for-run.js";
41
43
  import { initDsl } from "./dsl/index.js";
44
+ import { exchangeStopClass } from "./dsl/engine/index.js";
42
45
  import { initHealth } from "./health/index.js";
43
46
  import { initRisk } from "./risk/index.js";
44
47
  import { readRiskAuditEntries } from "./risk/risk-audit-log.js";
@@ -46,15 +49,21 @@ import { readEvents } from "./telemetry/event-store.js";
46
49
  import { VERSION } from "./version.js";
47
50
  import { resolvePackageName } from "./runtime/auto-update/resolve-package-name.js";
48
51
  import { runOpenClawPluginsUpdate } from "./runtime/auto-update/run-update-command.js";
52
+ import { UpdateIdleGate } from "./runtime/auto-update/idle-gate.js";
53
+ import { registerIdleGateHooks, resolveUpdateMid } from "./runtime/auto-update/register-idle-gate.js";
54
+ import { resolveUpdateMarkerPath } from "./runtime/auto-update/update-marker.js";
55
+ import { INSTALL_RESULTS_OTEL_FLUSH_MS, INSTALL_RESULTS_OTEL_READY_MS, resolveInstallResultsPath, } from "./runtime/auto-update/install-results.js";
49
56
  import { registerSpanHooks } from "./telemetry/spans/index.js";
50
57
  import { registerSessionContextSeedHook } from "./prompt/session-context-seed-hook.js";
51
58
  import { registerSkillRouterHook } from "./prompt/skill-router-hook.js";
59
+ import { registerCribSheet } from "./tool-surface/crib-hook.js";
52
60
  import { getDefaultPiiRedactor, initDefaultPiiRedactor } from "./telemetry/spans/redact-pii.js";
53
- import { initOtelSdk, shutdownOtelSdk } from "./telemetry/otel-sdk.js";
61
+ import { flushOtelLogs, initOtelSdk, shutdownOtelSdk, waitForOtelInit } from "./telemetry/otel-sdk.js";
54
62
  import { getLogLevel, getLogLevelSource, parseLogLevel, setLogLevel, } from "./utils/logger.js";
55
63
  import { ExternalScannerIngestError, } from "./scanners/external-scanner-receiver.js";
56
64
  import { createLogger } from "./utils/logger.js";
57
- import { registerToolApprovalGate, resolveToolApprovalsMode, } from "./approvals/tool-approval-gate.js";
65
+ import { PendingApprovalTracker, registerToolApprovalGate, resolveToolApprovalsMode, } from "./approvals/tool-approval-gate.js";
66
+ import { registerTurnContextHooks, turnClassificationAvailable, TurnContextTracker, } from "./approvals/turn-context.js";
58
67
  import { registerShowWidgetTool } from "./widgets/index.js";
59
68
  import { registerQuantWidgetTurnCap } from "./widgets/turn-cap.js";
60
69
  import { EVENT_NAME, SENPI_ERROR_CODE, eventAttrs, scannerUnwiredErrorCode } from "./utils/event-catalog.js";
@@ -72,6 +81,8 @@ import { ingestDispatchMutex, ingestDispatchKey } from "./runtime-api/intake/ing
72
81
  import { buildScaffoldErrorRouter } from "./runtime-api/intake/scaffold-error-router.js";
73
82
  import { loadRuntimeConfig } from "./runtime/load-runtime-config.js";
74
83
  import { errorText } from "./deploy/error-text.js";
84
+ import { initOpenClawHost } from "./host/openclaw-host.js";
85
+ import { hostname } from "node:os";
75
86
  import { BOOT_FACTS_CALL_TIMEOUT_MS, planBootFacts, syncBootFacts, } from "./strategy/facts-sync.js";
76
87
  /** Initialize all core features (DSL, risk, health) on the given event bus. */
77
88
  export function initCore(bus) {
@@ -184,6 +195,45 @@ const WALLET_INSTALL_IN_FLIGHT = `[${WALLET_INSTALL_IN_FLIGHT_CODE}]`;
184
195
  * anyone's money is being claimed, which is precisely why it must not answer with the wallet code.
185
196
  */
186
197
  const RUNTIME_ID_TAKEN = "[E_RUNTIME_ID_TAKEN]";
198
+ /**
199
+ * One StrategyFacts send per runtime at a time, keyed on runtime id, across the whole process: a
200
+ * second `register()` in the same process shares it. Separate from `runtimeChangeLock`. Delete does
201
+ * not take it; an apply queues its send here and does not wait.
202
+ */
203
+ const factsSendLock = new KeyedMutex();
204
+ /**
205
+ * Whether this process has already logged the 5.x "cannot classify runs" notice.
206
+ *
207
+ * Module-scoped so a gateway reload (which re-registers the plugin and restarts its service in the
208
+ * same process) does not repeat it. The 5.x case is a host-version limit with no config fix, so
209
+ * one info line per process is the whole message; the fixable 10.x case stays an ERROR at every
210
+ * registration and does not read this latch.
211
+ */
212
+ let legacyTurnClassificationNoticeLogged = false;
213
+ /** Clears {@link legacyTurnClassificationNoticeLogged}. Tests only. */
214
+ export function resetLegacyTurnClassificationNoticeForTests() {
215
+ legacyTurnClassificationNoticeLogged = false;
216
+ }
217
+ /**
218
+ * The whole of `register()` in the `"cli-metadata"` pass: the `senpi` CLI root, under the same
219
+ * preconditions and with the same metadata as the full pass, and nothing else.
220
+ *
221
+ * Reads no `api.runtime`: on 10.x that pass's runtime throws on any read, and the host prints the
222
+ * throw as `runtime failed during register` on every `openclaw senpi …` call (RT566-04). Logs
223
+ * nothing: a skipped precondition is reported by the full pass, which follows in the same process.
224
+ */
225
+ function registerSenpiCliMetadata(api) {
226
+ const config = api.pluginConfig;
227
+ if (!config)
228
+ return;
229
+ const mcpTransport = api.mcpTransport;
230
+ const apiKey = resolveSenpiApiKeyFromStore(readSenpiCliStore(), typeof config.apiKey === "string" && config.apiKey.length > 0 ? config.apiKey : undefined);
231
+ if (!mcpTransport && !apiKey)
232
+ return;
233
+ api.registerCli((ctx) => {
234
+ registerSenpiCli(ctx.program);
235
+ }, { commands: ["senpi"] });
236
+ }
187
237
  /**
188
238
  * OpenClaw plugin entry.
189
239
  * Boots from internal registry (installed_runtimes.json) in stateDir; does not read runtime list from config.
@@ -223,7 +273,16 @@ const senpiRuntimePlugin = {
223
273
  warn: (...args) => api.logger.warn(...args),
224
274
  error: (message, meta) => api.logger.error(message, meta),
225
275
  };
276
+ // The cli-metadata pass only collects CLI command roots. 10.x hands it a runtime that throws on
277
+ // any read (RT566-04), so register the `senpi` root and nothing else — and read no api.runtime.
278
+ if (api.registrationMode === "cli-metadata") {
279
+ registerSenpiCliMetadata(api);
280
+ return;
281
+ }
226
282
  pluginLogger.info("[senpi-runtime] register(api) invoked");
283
+ // Host facts first: every version-gated registration below reads them.
284
+ const openClawHost = initOpenClawHost(api);
285
+ pluginLogger.info(`[senpi-runtime] host OpenClaw ${openClawHost.version ?? "unknown"} (${openClawHost.modern ? "modern" : "legacy"})`);
227
286
  const config = api.pluginConfig;
228
287
  if (!config) {
229
288
  pluginLogger.warn("[senpi-runtime] No plugin config found — skipping registration");
@@ -286,8 +345,8 @@ const senpiRuntimePlugin = {
286
345
  // Agent telemetry — gateway hooks emit gen_ai spans (llm/tool/subagent) through
287
346
  // the shared tracer; on-site MCP calls emit their own spans from SenpiClient.
288
347
  try {
289
- registerSpanHooks({ api });
290
- pluginLogger.info("[senpi-runtime] gen_ai span hooks registered (9 hooks)");
348
+ const spanHookCount = registerSpanHooks({ api, modernHost: openClawHost.modern });
349
+ pluginLogger.info(`[senpi-runtime] gen_ai span hooks registered (${spanHookCount} hooks)`);
291
350
  }
292
351
  catch (err) {
293
352
  pluginLogger.warn("[senpi-runtime] failed to register span hooks", {
@@ -326,14 +385,93 @@ const senpiRuntimePlugin = {
326
385
  err: err instanceof Error ? err.message : String(err),
327
386
  });
328
387
  }
388
+ // senpi__ crib sheet (OpenClaw 10.x tool-search directory mode only): the
389
+ // directory folds every MCP tool into one sentence, so the runtime puts
390
+ // the names back as prependSystemContext. Registers nothing on 5.x.
391
+ // Its `before_prompt_build` hook needs the same host grant as the approval
392
+ // turn classification below; without it 10.x drops the hook, so the line
393
+ // says the crib is inactive and why instead of "registered".
394
+ try {
395
+ if (registerCribSheet({
396
+ api: api,
397
+ baseStateDir,
398
+ pluginConfig: config,
399
+ logger: pluginLogger,
400
+ })) {
401
+ // Registered, so the host is modern; only the hook grant is left to check.
402
+ const cribHook = turnClassificationAvailable({ modernHost: true, config: api.config, pluginId: api.id });
403
+ pluginLogger.info(cribHook.available
404
+ ? "[senpi-runtime] senpi__ crib sheet registered (directory mode only)"
405
+ : `[senpi-runtime] senpi__ crib inactive: ${cribHook.reason}` +
406
+ (cribHook.configFix ? `; fix: ${cribHook.configFix}` : ""));
407
+ }
408
+ }
409
+ catch (err) {
410
+ pluginLogger.warn("[senpi-runtime] failed to register the senpi__ crib sheet", {
411
+ err: err instanceof Error ? err.message : String(err),
412
+ });
413
+ }
329
414
  // MCP tool approval gate — state-changing senpi MCP tools require an
330
415
  // interactive user approval before executing. Prompts reach senpi-web via
331
416
  // the bridge's operator.approvals-scoped gateway connection; allow-always
332
417
  // decisions persist in <stateDir>/tool-approvals.json.
333
418
  // See docs/mcp-tool-approvals-design.md.
419
+ // Open trade-approval prompts, read by the self-update idle gate (R2).
420
+ const pendingApprovalTracker = new PendingApprovalTracker();
421
+ // The turn kind (chat / cron / heartbeat) and session key of each gated
422
+ // call, for approval telemetry and the 10.x scheduled-turn block (R6, D13).
423
+ const turnContextTracker = new TurnContextTracker();
424
+ // One boot-time fact (D15): can this host classify runs at all? Only then
425
+ // does the gate stop prompts outside a direct chat turn. Without it every
426
+ // run reads unknown and keeps the pre-10.x prompt, so a box missing the
427
+ // setting never blocks chat trades; it is reported loudly instead.
428
+ let turnClassification = turnClassificationAvailable({
429
+ modernHost: openClawHost.modern,
430
+ config: api.config,
431
+ pluginId: api.id,
432
+ });
433
+ try {
434
+ registerTurnContextHooks({ api, tracker: turnContextTracker });
435
+ }
436
+ catch (err) {
437
+ turnClassification = { available: false, reason: "the turn-context hooks failed to register" };
438
+ pluginLogger.warn("[senpi-runtime] failed to register the approval turn-context hooks", {
439
+ err: err instanceof Error ? err.message : String(err),
440
+ });
441
+ }
442
+ // Only the logging and the status flag differ by host; the gate below reads the same boolean
443
+ // either way. On 10.x it is an ERROR at every registration (a box config to fix, or hooks that
444
+ // failed to register). On 5.x it is a host limit no config can fix, so it is one info line per
445
+ // process, logged from the service start below: that runs in the gateway only, so the
446
+ // `openclaw senpi …` CLI children (deploy.py, close.py, an agent exec) register without
447
+ // printing it.
448
+ const turnClassificationHostLimit = !turnClassification.available && !openClawHost.modern;
449
+ const turnClassificationNotice = turnClassification.available
450
+ ? undefined
451
+ : `[senpi-tool-approvals] cannot tell chat turns from scheduled or background runs: ${turnClassification.reason}. ` +
452
+ "Trade approvals are requested in every run, scheduled ones included, " +
453
+ (turnClassification.configFix
454
+ ? `until the box config is fixed (${turnClassification.configFix}).`
455
+ : turnClassificationHostLimit
456
+ ? "on this OpenClaw version; a 10.x host lifts this."
457
+ : "until the runtime registers its turn-context hooks.");
458
+ const legacyTurnClassificationNotice = turnClassificationHostLimit ? turnClassificationNotice : undefined;
459
+ if (turnClassificationNotice !== undefined && !turnClassificationHostLimit) {
460
+ pluginLogger.error(turnClassificationNotice);
461
+ }
462
+ const approvalsHostStatus = turnClassification.available
463
+ ? { turnClassification: "available" }
464
+ : {
465
+ turnClassification: "unavailable",
466
+ reason: turnClassification.reason,
467
+ ...(turnClassificationHostLimit ? { hostLimit: true } : {}),
468
+ };
334
469
  try {
335
470
  registerToolApprovalGate({
336
471
  api,
472
+ pendingApprovals: pendingApprovalTracker,
473
+ turnContext: turnContextTracker,
474
+ turnClassification: turnClassification.available,
337
475
  mode: resolveToolApprovalsMode({
338
476
  pluginConfig: config,
339
477
  env: process.env,
@@ -487,7 +625,7 @@ const senpiRuntimePlugin = {
487
625
  // PluginLogger surface (info/warn/error) lacks `debug`, while the
488
626
  // Logger contract used by `runtime-api/` includes it.
489
627
  const apiLogger = createLogger("[senpi-core][api]");
490
- /** One line per instance for the boot StrategyFacts send (ticket 125); see `sendBootFacts`. */
628
+ /** One line per instance for each StrategyFacts send; see `queueFactsSend`. */
491
629
  const factsLogger = createLogger("[senpi-core][strategy-facts]");
492
630
  // A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
493
631
  // runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
@@ -525,24 +663,92 @@ const senpiRuntimePlugin = {
525
663
  });
526
664
  const autoUpdateLogger = withEventLogger(pluginLogger, "[senpi-core][auto-update]");
527
665
  const autoUpdateConfig = resolveAutoUpdateConfig(config?.autoUpdate);
666
+ // OpenClaw 10.x (>= 2026.8.1): restart-to-apply (R2). The runtime cannot
667
+ // install itself there, so it waits for its spread slot and an idle box,
668
+ // writes the install marker, and restarts through mutateConfigFile; the
669
+ // senpi-agent wrapper installs before the next gateway spawn. On 5.7 and
670
+ // 5.12 the wiring is the in-process install + config-bump restart.
671
+ const modernHost = openClawHost.modern;
672
+ let idleGate;
673
+ if (modernHost) {
674
+ // Money calls on every route: the runtime's own in-process MCP calls, plus
675
+ // the agent's senpi__ calls (direct or through the dispatcher), seen at the
676
+ // tool hooks. A deploy job is read from the controller (this process) and
677
+ // its journals (any process).
678
+ const moneyCalls = new MoneyCallTracker();
679
+ try {
680
+ registerMoneyCallHooks({
681
+ api: api,
682
+ tracker: moneyCalls,
683
+ isMoneyTool: isMoneyCallTool,
684
+ });
685
+ }
686
+ catch (err) {
687
+ pluginLogger.warn("[senpi-runtime] failed to register the self-update money-call hooks", {
688
+ err: err instanceof Error ? err.message : String(err),
689
+ });
690
+ }
691
+ idleGate = new UpdateIdleGate({
692
+ pendingApprovals: () => pendingApprovalTracker.count(),
693
+ ordersInFlight: () => senpiOrderCallsInFlight() + moneyCalls.count(Date.now()),
694
+ // Read only from ticks, after register() has constructed the controller below.
695
+ deploysInFlight: () => deployController.inFlight(Date.now()),
696
+ logger: pluginLogger,
697
+ });
698
+ try {
699
+ registerIdleGateHooks({ api: api, gate: idleGate, logger: pluginLogger });
700
+ }
701
+ catch (err) {
702
+ pluginLogger.warn("[senpi-runtime] failed to register the self-update idle gate hooks", {
703
+ err: err instanceof Error ? err.message : String(err),
704
+ });
705
+ }
706
+ }
707
+ const openclawStateDir = typeof api.runtime?.state?.resolveStateDir === "function" ? api.runtime.state.resolveStateDir() : undefined;
528
708
  const autoUpdateCoordinator = new AutoUpdateCoordinator({
529
709
  enabled: true,
530
710
  config: autoUpdateConfig,
531
711
  packageName: PLUGIN_PACKAGE_NAME,
532
712
  currentVersion: VERSION,
713
+ // The version this process loaded; "applied" is decided from it alone.
714
+ loadedVersion: VERSION,
533
715
  stateDir: join(baseStateDir, "auto-update"),
534
716
  logger: autoUpdateLogger,
535
717
  // Use the resolved npm package name (not a hardcoded plugin id) so
536
718
  // the update follows the same npm channel as the version check —
537
- // see runtime/auto-update/run-update-command.ts.
538
- runUpdateCommand: runOpenClawPluginsUpdate,
719
+ // see runtime/auto-update/run-update-command.ts. Not on 10.x, which
720
+ // refuses plugins update while the gateway runs.
721
+ runUpdateCommand: modernHost ? undefined : runOpenClawPluginsUpdate,
539
722
  configFromApi: api.config,
540
- loadConfig: api.runtime?.config
723
+ loadConfig: !modernHost && api.runtime?.config
541
724
  ? () => api.runtime.config.loadConfig()
542
725
  : undefined,
543
- writeConfigFile: api.runtime?.config
726
+ writeConfigFile: !modernHost && api.runtime?.config
544
727
  ? (c) => api.runtime.config.writeConfigFile(c)
545
728
  : undefined,
729
+ modernHost,
730
+ mutateConfigFile: modernHost && typeof api.runtime?.config?.mutateConfigFile === "function"
731
+ ? (p) => api.runtime.config.mutateConfigFile(p)
732
+ : undefined,
733
+ readLiveConfig: modernHost ? openClawHost.currentConfig : undefined,
734
+ idleGate,
735
+ mid: modernHost
736
+ ? resolveUpdateMid({
737
+ token: apiKey ?? process.env.SENPI_AUTH_TOKEN ?? process.env.SENPI_API_KEY,
738
+ env: process.env,
739
+ hostname,
740
+ })
741
+ : undefined,
742
+ updateMarkerPath: modernHost && openclawStateDir ? resolveUpdateMarkerPath(openclawStateDir) : undefined,
743
+ // The wrapper's install outcomes, reported once per coordinator start (gateway only),
744
+ // with auto-update on or off. The report runs off the start path: it waits for the OTel
745
+ // init register() started (else the events go to the no-op logger and the file is gone)
746
+ // and flushes the logs before deleting the file.
747
+ installResultsPath: modernHost && openclawStateDir ? resolveInstallResultsPath(openclawStateDir) : undefined,
748
+ installResultsTelemetry: {
749
+ ready: () => waitForOtelInit(INSTALL_RESULTS_OTEL_READY_MS),
750
+ flush: () => flushOtelLogs(INSTALL_RESULTS_OTEL_FLUSH_MS),
751
+ },
546
752
  pluginId: api.id,
547
753
  // Drive skills-manager off the auto-update tick. Both success and
548
754
  // failure branches of auto-update's tick fire this hook so skills
@@ -622,6 +828,11 @@ const senpiRuntimePlugin = {
622
828
  api.registerService({
623
829
  id: "senpi-runtime",
624
830
  async start() {
831
+ // The 5.x classification notice (see the approval gate above), once per process.
832
+ if (legacyTurnClassificationNotice !== undefined && !legacyTurnClassificationNoticeLogged) {
833
+ legacyTurnClassificationNoticeLogged = true;
834
+ pluginLogger.info(legacyTurnClassificationNotice);
835
+ }
625
836
  // Deploy-journal janitor: stamp anything a restart left unterminated as `interrupted`.
626
837
  // It never re-enters a deploy state machine — discovery is pull-based via
627
838
  // `senpi deploy status`.
@@ -759,26 +970,16 @@ const senpiRuntimePlugin = {
759
970
  emitEvent(apiLogger, () => buildRuntimeBootSkippedEvent({ runtimeId: entry.id, address: entry.wallet ?? "", status }));
760
971
  }
761
972
  const started = [];
762
- const factsPlans = [];
973
+ // Started runtimes with a facts plan, in boot order; their send runs detached after boot, below.
974
+ const factsRuntimeIds = [];
763
975
  try {
764
976
  for (const entry of entriesToBoot) {
765
- const hasPath = typeof entry.runtimeYamlPath === "string" && entry.runtimeYamlPath.length > 0;
766
- const hasContent = typeof entry.runtimeYamlContent === "string";
767
- if (hasPath && hasContent)
768
- continue;
769
- if (!hasPath && !hasContent)
977
+ const inputs = factsPlanInputs(entry);
978
+ if (!inputs)
770
979
  continue;
771
- const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
772
- const entryStateDir = buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? "");
773
- // Local only (files, no network); the send itself runs detached after boot, below.
774
- const factsPlan = await planInstanceFacts({
775
- runtimeId: entry.id,
776
- wallet: entry.wallet,
777
- stateDir: entryStateDir,
778
- pathOrContent,
779
- fromPath: hasPath,
780
- runtimeYamlDir: hasPath ? dirname(pathOrContent) : entry.runtimeYamlDir,
781
- });
980
+ const { pathOrContent, fromPath: hasPath, stateDir: entryStateDir } = inputs;
981
+ // Local only (files, no network). Read here for the `factsSent` stamp on this start.
982
+ const factsPlan = await planInstanceFacts(inputs);
782
983
  const handle = await run(pathOrContent, {
783
984
  fromPath: hasPath,
784
985
  stateDir: entryStateDir,
@@ -792,7 +993,7 @@ const senpiRuntimePlugin = {
792
993
  runtimeHandles.set(entry.id, handle);
793
994
  started.push(entry.id);
794
995
  if (factsPlan)
795
- factsPlans.push(factsPlan);
996
+ factsRuntimeIds.push(entry.id);
796
997
  // ── supervised scanner mount (additive; guarded) ──────────────────
797
998
  // Boot path: used for runtimes already present in the registry at
798
999
  // startup. A runtime installed live, after the API is listening,
@@ -885,8 +1086,12 @@ const senpiRuntimePlugin = {
885
1086
  throw err;
886
1087
  }
887
1088
  pluginLogger.info(`[senpi-runtime] Started ${started.length} runtime${started.length === 1 ? "" : "s"}`);
888
- // Detached: the facts send never holds boot or trading. One line per instance.
889
- void sendBootFacts(factsPlans, bootStrategyIds).catch((err) => {
1089
+ // Detached: the facts send never holds boot or trading. One line per instance, one instance at
1090
+ // a time, each planned again when its turn comes under the per-runtime facts lock.
1091
+ void (async () => {
1092
+ for (const runtimeId of factsRuntimeIds)
1093
+ await queueFactsSend(runtimeId, bootStrategyIds);
1094
+ })().catch((err) => {
890
1095
  factsLogger.warn("strategy facts send crashed", err);
891
1096
  });
892
1097
  // ── HTTP API ──────────────────────────────────────────────────────
@@ -1628,8 +1833,27 @@ const senpiRuntimePlugin = {
1628
1833
  else
1629
1834
  respond(false, undefined, result.error);
1630
1835
  }, { scope: "operator.write" });
1631
- // ── StrategyFacts at boot (ticket 125) ─────────────────────────────────
1632
- /** The boot plan for one instance, or null (logged once as `failed`) when it cannot be made. */
1836
+ // ── StrategyFacts send ─────────────────────────────────────────────────
1837
+ /**
1838
+ * The facts plan inputs for one registry entry, which are also the recipe and state dir boot
1839
+ * starts it with. Null when the entry holds both or neither of a recipe path and recipe content.
1840
+ */
1841
+ function factsPlanInputs(entry) {
1842
+ const hasPath = typeof entry.runtimeYamlPath === "string" && entry.runtimeYamlPath.length > 0;
1843
+ const hasContent = typeof entry.runtimeYamlContent === "string";
1844
+ if (hasPath === hasContent)
1845
+ return null;
1846
+ const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
1847
+ return {
1848
+ runtimeId: entry.id,
1849
+ wallet: entry.wallet,
1850
+ stateDir: buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? ""),
1851
+ pathOrContent,
1852
+ fromPath: hasPath,
1853
+ runtimeYamlDir: hasPath ? dirname(pathOrContent) : entry.runtimeYamlDir,
1854
+ };
1855
+ }
1856
+ /** The facts plan for one instance, or null (logged once as `failed`) when it cannot be made. */
1633
1857
  async function planInstanceFacts(input) {
1634
1858
  try {
1635
1859
  const config = await loadRuntimeConfig(input.pathOrContent, { fromPath: input.fromPath });
@@ -1696,6 +1920,43 @@ const senpiRuntimePlugin = {
1696
1920
  await session.client.disconnect().catch(() => { });
1697
1921
  }
1698
1922
  }
1923
+ /**
1924
+ * Send one runtime's facts under `factsSendLock`. The plan is made from the registry entry and
1925
+ * the state file as they are when the send runs, so the last send for a runtime carries its
1926
+ * current recipe. A runtime no longer in the registry, or whose entry cannot be planned, gets
1927
+ * no send. An unchanged plan makes no call.
1928
+ *
1929
+ * `strategyIds` is the boot's one batched status read (null when that read failed). Without it,
1930
+ * the strategy id is read for this one wallet; a failed read logs its reason and sends nothing.
1931
+ */
1932
+ function queueFactsSend(runtimeId, strategyIds) {
1933
+ return factsSendLock.withKeyLock(runtimeId, async () => {
1934
+ const { readRegistry } = await import("./runtime/strategy-registry.js");
1935
+ const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.id === runtimeId);
1936
+ const inputs = entry ? factsPlanInputs(entry) : null;
1937
+ if (!inputs)
1938
+ return;
1939
+ const plan = await planInstanceFacts(inputs);
1940
+ if (!plan)
1941
+ return;
1942
+ let ids = strategyIds ?? null;
1943
+ if (strategyIds === undefined && !plan.unchanged) {
1944
+ const read = await readBootWalletStatuses([plan.wallet.trim().toLowerCase()], (wallets, signal) => withSenpiClient((client) => client.listStrategies({ strategyAddresses: wallets }, { signal })));
1945
+ if (!read.ok) {
1946
+ factsLogger.warn("strategy facts failed: could not read the strategy id; retrying next boot", undefined, {
1947
+ runtimeId: plan.runtimeId,
1948
+ wallet: plan.wallet,
1949
+ hash: plan.hash.slice(0, 12),
1950
+ reason: read.reason,
1951
+ detail: read.message,
1952
+ });
1953
+ return;
1954
+ }
1955
+ ids = read.strategyIds;
1956
+ }
1957
+ await sendBootFacts([plan], ids);
1958
+ });
1959
+ }
1699
1960
  /** One connected SenpiClient per job; always disconnected, even on failure. */
1700
1961
  async function withSenpiClient(fn) {
1701
1962
  const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
@@ -1908,6 +2169,13 @@ const senpiRuntimePlugin = {
1908
2169
  ...(info.slots !== undefined ? { slots: info.slots } : {}),
1909
2170
  });
1910
2171
  });
2172
+ // The apply swaps the recipe with no boot, so this send moves the backend's facts (what the
2173
+ // app shows) to the new recipe before the next boot. Detached: the apply has landed and must
2174
+ // not wait on, or fail over, the send. The send plans from the registry entry the apply has
2175
+ // written, and takes the wallet from that entry or its recipe.
2176
+ void queueFactsSend(info.runtimeId).catch((err) => {
2177
+ factsLogger.warn("strategy facts send crashed", err);
2178
+ });
1911
2179
  },
1912
2180
  });
1913
2181
  registerDeployGatewayMethods(api, {
@@ -1930,7 +2198,10 @@ const senpiRuntimePlugin = {
1930
2198
  // same clean line over a runtime that cannot tick.
1931
2199
  runtimeIsLive: async (id) => runtimeHandles.has(id) && !unwiredRuntimes.has(id),
1932
2200
  installPendingOnWallet: async (wallet) => installsInFlight.has(wallet.trim().toLowerCase()),
1933
- logger: { info: (line) => api.logger.info(`[senpi-runtime] ${line}`) },
2201
+ logger: {
2202
+ info: (line) => api.logger.info(`[senpi-runtime] ${line}`),
2203
+ warn: (line) => api.logger.warn(`[senpi-runtime] ${line}`),
2204
+ },
1934
2205
  });
1935
2206
  /**
1936
2207
  * Gateway shim for `senpi.ingestExternalScannerData`.
@@ -2055,6 +2326,7 @@ const senpiRuntimePlugin = {
2055
2326
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2056
2327
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
2057
2328
  pluginLogger.info("[senpi-runtime] senpi.getDslPositionState", { asset, runtimeId, address });
2329
+ // `exchangeStop` is derived from the state on each response and is not persisted.
2058
2330
  const matches = [];
2059
2331
  for (const [id, handle] of runtimeHandles) {
2060
2332
  if (runtimeId && id !== runtimeId)
@@ -2065,12 +2337,18 @@ const senpiRuntimePlugin = {
2065
2337
  if (address) {
2066
2338
  const state = handle.dsl.getPositionState(address, asset);
2067
2339
  if (state)
2068
- matches.push({ runtimeId: id, address, state });
2340
+ matches.push({ runtimeId: id, address, state, exchangeStop: exchangeStopClass(state) });
2069
2341
  }
2070
2342
  else {
2071
2343
  const found = handle.dsl.findPositionByAsset(asset);
2072
- if (found)
2073
- matches.push({ runtimeId: id, address: found.address, state: found.state });
2344
+ if (found) {
2345
+ matches.push({
2346
+ runtimeId: id,
2347
+ address: found.address,
2348
+ state: found.state,
2349
+ exchangeStop: exchangeStopClass(found.state),
2350
+ });
2351
+ }
2074
2352
  }
2075
2353
  }
2076
2354
  catch (err) {
@@ -2126,6 +2404,8 @@ const senpiRuntimePlugin = {
2126
2404
  respond(true, {
2127
2405
  status: status ? { ...status, runtimeId } : null,
2128
2406
  updater,
2407
+ approvals: approvalsHostStatus,
2408
+ deploy: { capabilities: DEPLOY_CAPABILITIES },
2129
2409
  });
2130
2410
  }
2131
2411
  catch (err) {
@@ -2138,7 +2418,7 @@ const senpiRuntimePlugin = {
2138
2418
  return;
2139
2419
  }
2140
2420
  const statuses = await collectRuntimeHealthStatuses(runtimeHandles, api.logger);
2141
- respond(true, { statuses, updater });
2421
+ respond(true, { statuses, updater, approvals: approvalsHostStatus, deploy: { capabilities: DEPLOY_CAPABILITIES } });
2142
2422
  }, { scope: "operator.read" });
2143
2423
  api.registerGatewayMethod("senpi.getSystemState", async (ctx) => {
2144
2424
  const respond = ctx.respond;