@senpi/runtime 3.0.171 → 3.0.180

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 (192) 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/open-position/open-position.action.d.ts.map +1 -1
  5. package/dist/actions/open-position/open-position.action.js +42 -6
  6. package/dist/actions/open-position/open-position.action.js.map +1 -1
  7. package/dist/actions/open-position/resolve-margin.d.ts +1 -1
  8. package/dist/actions/open-position/resolve-margin.d.ts.map +1 -1
  9. package/dist/actions/open-position/resolve-margin.js +7 -0
  10. package/dist/actions/open-position/resolve-margin.js.map +1 -1
  11. package/dist/approvals/manifest.d.ts.map +1 -1
  12. package/dist/approvals/manifest.js +0 -5
  13. package/dist/approvals/manifest.js.map +1 -1
  14. package/dist/approvals/tool-approval-gate.d.ts.map +1 -1
  15. package/dist/approvals/tool-approval-gate.js +0 -6
  16. package/dist/approvals/tool-approval-gate.js.map +1 -1
  17. package/dist/approvals/tool-approvals-manifest.json +0 -2
  18. package/dist/cli/guide-commands.d.ts.map +1 -1
  19. package/dist/cli/guide-commands.js +26 -5
  20. package/dist/cli/guide-commands.js.map +1 -1
  21. package/dist/cli/senpi-commands.d.ts.map +1 -1
  22. package/dist/cli/senpi-commands.js +20 -8
  23. package/dist/cli/senpi-commands.js.map +1 -1
  24. package/dist/constants/dsl/index.d.ts +2 -0
  25. package/dist/constants/dsl/index.d.ts.map +1 -1
  26. package/dist/constants/dsl/index.js +2 -0
  27. package/dist/constants/dsl/index.js.map +1 -1
  28. package/dist/constants/order-types.d.ts.map +1 -1
  29. package/dist/constants/order-types.js +4 -1
  30. package/dist/constants/order-types.js.map +1 -1
  31. package/dist/deploy/messages.d.ts +15 -1
  32. package/dist/deploy/messages.d.ts.map +1 -1
  33. package/dist/deploy/messages.js +78 -0
  34. package/dist/deploy/messages.js.map +1 -1
  35. package/dist/deploy/orchestrator.d.ts +11 -1
  36. package/dist/deploy/orchestrator.d.ts.map +1 -1
  37. package/dist/deploy/orchestrator.js +111 -15
  38. package/dist/deploy/orchestrator.js.map +1 -1
  39. package/dist/deploy/register.d.ts +7 -0
  40. package/dist/deploy/register.d.ts.map +1 -1
  41. package/dist/deploy/register.js +9 -0
  42. package/dist/deploy/register.js.map +1 -1
  43. package/dist/dsl/engine/exchange-stop.d.ts +21 -1
  44. package/dist/dsl/engine/exchange-stop.d.ts.map +1 -1
  45. package/dist/dsl/engine/exchange-stop.js +27 -1
  46. package/dist/dsl/engine/exchange-stop.js.map +1 -1
  47. package/dist/dsl/engine/floors.d.ts +2 -1
  48. package/dist/dsl/engine/floors.d.ts.map +1 -1
  49. package/dist/dsl/engine/floors.js +2 -1
  50. package/dist/dsl/engine/floors.js.map +1 -1
  51. package/dist/dsl/engine/index.d.ts +1 -1
  52. package/dist/dsl/engine/index.d.ts.map +1 -1
  53. package/dist/dsl/engine/index.js +1 -1
  54. package/dist/dsl/engine/index.js.map +1 -1
  55. package/dist/dsl/engine/tick.d.ts.map +1 -1
  56. package/dist/dsl/engine/tick.js +4 -3
  57. package/dist/dsl/engine/tick.js.map +1 -1
  58. package/dist/dsl/engine-client/config-mapping.d.ts +18 -0
  59. package/dist/dsl/engine-client/config-mapping.d.ts.map +1 -1
  60. package/dist/dsl/engine-client/config-mapping.js +29 -0
  61. package/dist/dsl/engine-client/config-mapping.js.map +1 -1
  62. package/dist/dsl/events/handlers.d.ts +7 -3
  63. package/dist/dsl/events/handlers.d.ts.map +1 -1
  64. package/dist/dsl/events/handlers.js +10 -4
  65. package/dist/dsl/events/handlers.js.map +1 -1
  66. package/dist/dsl/events/initial-state.d.ts.map +1 -1
  67. package/dist/dsl/events/initial-state.js +2 -0
  68. package/dist/dsl/events/initial-state.js.map +1 -1
  69. package/dist/dsl/monitor/exchange-sync.d.ts +1 -0
  70. package/dist/dsl/monitor/exchange-sync.d.ts.map +1 -1
  71. package/dist/dsl/monitor/exchange-sync.js +7 -14
  72. package/dist/dsl/monitor/exchange-sync.js.map +1 -1
  73. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  74. package/dist/dsl/monitor/process-one-position.js +108 -17
  75. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  76. package/dist/dsl/notifications/formatter.d.ts +2 -1
  77. package/dist/dsl/notifications/formatter.d.ts.map +1 -1
  78. package/dist/dsl/notifications/formatter.js +18 -1
  79. package/dist/dsl/notifications/formatter.js.map +1 -1
  80. package/dist/health/types.d.ts +6 -1
  81. package/dist/health/types.d.ts.map +1 -1
  82. package/dist/health/types.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +98 -26
  85. package/dist/index.js.map +1 -1
  86. package/dist/leverage/max-leverage-cache.d.ts +25 -0
  87. package/dist/leverage/max-leverage-cache.d.ts.map +1 -1
  88. package/dist/leverage/max-leverage-cache.js +77 -0
  89. package/dist/leverage/max-leverage-cache.js.map +1 -1
  90. package/dist/runtime/create-scanner-module.d.ts +1 -1
  91. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  92. package/dist/runtime/create-scanner-module.js +2 -1
  93. package/dist/runtime/create-scanner-module.js.map +1 -1
  94. package/dist/runtime/lifecycle-events.d.ts +1 -1
  95. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  96. package/dist/runtime/lifecycle-events.js +10 -1
  97. package/dist/runtime/lifecycle-events.js.map +1 -1
  98. package/dist/runtime/runtime-descriptor.d.ts +11 -2
  99. package/dist/runtime/runtime-descriptor.d.ts.map +1 -1
  100. package/dist/runtime/runtime-descriptor.js +11 -2
  101. package/dist/runtime/runtime-descriptor.js.map +1 -1
  102. package/dist/runtime/runtime.d.ts.map +1 -1
  103. package/dist/runtime/runtime.js +4 -3
  104. package/dist/runtime/runtime.js.map +1 -1
  105. package/dist/runtime/strategy-lifecycle.js +1 -1
  106. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  107. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  108. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  109. package/dist/runtime-api/intake/acceptance-record.d.ts +17 -3
  110. package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
  111. package/dist/runtime-api/intake/acceptance-record.js +57 -32
  112. package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
  113. package/dist/runtime-api/intake/intake-events.d.ts +6 -5
  114. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  115. package/dist/runtime-api/intake/intake-events.js +9 -3
  116. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  117. package/dist/runtime-api/intake/process-signal-post.d.ts +1 -1
  118. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  119. package/dist/runtime-api/intake/process-signal-post.js +34 -31
  120. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  121. package/dist/runtime-api/intake/scaffold-error-router.d.ts +8 -0
  122. package/dist/runtime-api/intake/scaffold-error-router.d.ts.map +1 -1
  123. package/dist/runtime-api/intake/scaffold-error-router.js +9 -0
  124. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  125. package/dist/runtime-api/intake/types.d.ts +9 -0
  126. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  127. package/dist/runtime-api/intake/types.js +5 -1
  128. package/dist/runtime-api/intake/types.js.map +1 -1
  129. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  130. package/dist/runtime-api/routes/signals.js +25 -1
  131. package/dist/runtime-api/routes/signals.js.map +1 -1
  132. package/dist/runtime-api/routes/signals.schema.d.ts +2 -0
  133. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  134. package/dist/runtime-api/routes/signals.schema.js +4 -0
  135. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  136. package/dist/scanners/engine/data-providers.d.ts +6 -0
  137. package/dist/scanners/engine/data-providers.d.ts.map +1 -1
  138. package/dist/scanners/engine/data-providers.js.map +1 -1
  139. package/dist/scanners/engine/engine.d.ts +9 -0
  140. package/dist/scanners/engine/engine.d.ts.map +1 -1
  141. package/dist/scanners/engine/engine.js +29 -6
  142. package/dist/scanners/engine/engine.js.map +1 -1
  143. package/dist/scanners/external/scaffold/ctx.py +55 -1
  144. package/dist/scanners/external/scaffold/delivery.py +92 -28
  145. package/dist/scanners/external/scaffold/preflight.py +1 -1
  146. package/dist/scanners/external/scaffold/scaffold.py +75 -2
  147. package/dist/scanners/runtime-module.d.ts +2 -0
  148. package/dist/scanners/runtime-module.d.ts.map +1 -1
  149. package/dist/scanners/runtime-module.js +14 -0
  150. package/dist/scanners/runtime-module.js.map +1 -1
  151. package/dist/senpi/client.d.ts +11 -1
  152. package/dist/senpi/client.d.ts.map +1 -1
  153. package/dist/senpi/client.js +44 -8
  154. package/dist/senpi/client.js.map +1 -1
  155. package/dist/senpi/types.d.ts +54 -2
  156. package/dist/senpi/types.d.ts.map +1 -1
  157. package/dist/telemetry/metrics.d.ts +1 -1
  158. package/dist/telemetry/metrics.d.ts.map +1 -1
  159. package/dist/telemetry/metrics.js +1 -1
  160. package/dist/telemetry/metrics.js.map +1 -1
  161. package/dist/types/dsl/index.d.ts +4 -0
  162. package/dist/types/dsl/index.d.ts.map +1 -1
  163. package/dist/utils/event-catalog.d.ts +24 -5
  164. package/dist/utils/event-catalog.d.ts.map +1 -1
  165. package/dist/utils/event-catalog.js +4 -1
  166. package/dist/utils/event-catalog.js.map +1 -1
  167. package/dist/utils/perp-coin.d.ts.map +1 -1
  168. package/dist/utils/perp-coin.js +2 -1
  169. package/dist/utils/perp-coin.js.map +1 -1
  170. package/dist/utils/resolve-order-type.d.ts.map +1 -1
  171. package/dist/utils/resolve-order-type.js +5 -1
  172. package/dist/utils/resolve-order-type.js.map +1 -1
  173. package/dist/validate/codes.d.ts +2 -0
  174. package/dist/validate/codes.d.ts.map +1 -1
  175. package/dist/validate/codes.js +6 -0
  176. package/dist/validate/codes.js.map +1 -1
  177. package/dist/validate/index.d.ts +1 -1
  178. package/dist/validate/index.d.ts.map +1 -1
  179. package/dist/validate/index.js +1 -1
  180. package/dist/validate/index.js.map +1 -1
  181. package/dist/validate/recipe-checks.d.ts +7 -0
  182. package/dist/validate/recipe-checks.d.ts.map +1 -1
  183. package/dist/validate/recipe-checks.js +18 -0
  184. package/dist/validate/recipe-checks.js.map +1 -1
  185. package/dist/validate/recipe.d.ts.map +1 -1
  186. package/dist/validate/recipe.js +2 -1
  187. package/dist/validate/recipe.js.map +1 -1
  188. package/examples/strategies/README.md +2 -1
  189. package/examples/strategies/iguana/README.md +3 -3
  190. package/examples/strategies/raptor/README.md +3 -3
  191. package/openclaw.plugin.json +1 -1
  192. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -28,6 +28,7 @@ import { checkProofCoversRecipe, makeApplyToRuntime, makeWriteRegistry, runtimeC
28
28
  import { INSTALL_DEADLINE_MS, WALLET_INSTALL_IN_FLIGHT_CODE } from "./deploy/deadlines.js";
29
29
  import { AuditQueryError } from "./runtime/audit-query.types.js";
30
30
  import { buildRuntimeStateDir, archiveStateDir } from "./runtime/state-layout.js";
31
+ import { KeyedMutex } from "./utils/keyed-mutex.js";
31
32
  import { describeRuntimeEntry } from "./runtime/runtime-descriptor.js";
32
33
  import { registerSenpiCli } from "./cli/senpi-commands.js";
33
34
  import { readSenpiCliStore, resolveSenpiApiKeyFromStore, resolveSenpiBaseStateDir, } from "./cli/senpi-cli-config.js";
@@ -39,6 +40,7 @@ import { resolveAutoUpdateConfig } from "./runtime/auto-update/types.js";
39
40
  import { SkillsCoordinatorUnavailableError, SkillsUpdateCoordinator, resolveSkillsManagerConfig, } from "./skills-manager/index.js";
40
41
  import { createScannerProvidersForRun } from "./runtime/create-scanner-providers-for-run.js";
41
42
  import { initDsl } from "./dsl/index.js";
43
+ import { exchangeStopClass } from "./dsl/engine/index.js";
42
44
  import { initHealth } from "./health/index.js";
43
45
  import { initRisk } from "./risk/index.js";
44
46
  import { readRiskAuditEntries } from "./risk/risk-audit-log.js";
@@ -184,6 +186,12 @@ const WALLET_INSTALL_IN_FLIGHT = `[${WALLET_INSTALL_IN_FLIGHT_CODE}]`;
184
186
  * anyone's money is being claimed, which is precisely why it must not answer with the wallet code.
185
187
  */
186
188
  const RUNTIME_ID_TAKEN = "[E_RUNTIME_ID_TAKEN]";
189
+ /**
190
+ * One StrategyFacts send per runtime at a time, keyed on runtime id, across the whole process: a
191
+ * second `register()` in the same process shares it. Separate from `runtimeChangeLock`. Delete does
192
+ * not take it; an apply queues its send here and does not wait.
193
+ */
194
+ const factsSendLock = new KeyedMutex();
187
195
  /**
188
196
  * OpenClaw plugin entry.
189
197
  * Boots from internal registry (installed_runtimes.json) in stateDir; does not read runtime list from config.
@@ -487,7 +495,7 @@ const senpiRuntimePlugin = {
487
495
  // PluginLogger surface (info/warn/error) lacks `debug`, while the
488
496
  // Logger contract used by `runtime-api/` includes it.
489
497
  const apiLogger = createLogger("[senpi-core][api]");
490
- /** One line per instance for the boot StrategyFacts send (ticket 125); see `sendBootFacts`. */
498
+ /** One line per instance for each StrategyFacts send; see `queueFactsSend`. */
491
499
  const factsLogger = createLogger("[senpi-core][strategy-facts]");
492
500
  // A gateway seam failed to wire/launch a runtime's external scanner stack: the runtime is up but
493
501
  // runs blind on internal scanners only. Two records, by design: a first-class `runtime.scanners_unwired`
@@ -759,26 +767,16 @@ const senpiRuntimePlugin = {
759
767
  emitEvent(apiLogger, () => buildRuntimeBootSkippedEvent({ runtimeId: entry.id, address: entry.wallet ?? "", status }));
760
768
  }
761
769
  const started = [];
762
- const factsPlans = [];
770
+ // Started runtimes with a facts plan, in boot order; their send runs detached after boot, below.
771
+ const factsRuntimeIds = [];
763
772
  try {
764
773
  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)
774
+ const inputs = factsPlanInputs(entry);
775
+ if (!inputs)
768
776
  continue;
769
- if (!hasPath && !hasContent)
770
- 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
- });
777
+ const { pathOrContent, fromPath: hasPath, stateDir: entryStateDir } = inputs;
778
+ // Local only (files, no network). Read here for the `factsSent` stamp on this start.
779
+ const factsPlan = await planInstanceFacts(inputs);
782
780
  const handle = await run(pathOrContent, {
783
781
  fromPath: hasPath,
784
782
  stateDir: entryStateDir,
@@ -792,7 +790,7 @@ const senpiRuntimePlugin = {
792
790
  runtimeHandles.set(entry.id, handle);
793
791
  started.push(entry.id);
794
792
  if (factsPlan)
795
- factsPlans.push(factsPlan);
793
+ factsRuntimeIds.push(entry.id);
796
794
  // ── supervised scanner mount (additive; guarded) ──────────────────
797
795
  // Boot path: used for runtimes already present in the registry at
798
796
  // startup. A runtime installed live, after the API is listening,
@@ -885,8 +883,12 @@ const senpiRuntimePlugin = {
885
883
  throw err;
886
884
  }
887
885
  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) => {
886
+ // Detached: the facts send never holds boot or trading. One line per instance, one instance at
887
+ // a time, each planned again when its turn comes under the per-runtime facts lock.
888
+ void (async () => {
889
+ for (const runtimeId of factsRuntimeIds)
890
+ await queueFactsSend(runtimeId, bootStrategyIds);
891
+ })().catch((err) => {
890
892
  factsLogger.warn("strategy facts send crashed", err);
891
893
  });
892
894
  // ── HTTP API ──────────────────────────────────────────────────────
@@ -1628,8 +1630,27 @@ const senpiRuntimePlugin = {
1628
1630
  else
1629
1631
  respond(false, undefined, result.error);
1630
1632
  }, { 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. */
1633
+ // ── StrategyFacts send ─────────────────────────────────────────────────
1634
+ /**
1635
+ * The facts plan inputs for one registry entry, which are also the recipe and state dir boot
1636
+ * starts it with. Null when the entry holds both or neither of a recipe path and recipe content.
1637
+ */
1638
+ function factsPlanInputs(entry) {
1639
+ const hasPath = typeof entry.runtimeYamlPath === "string" && entry.runtimeYamlPath.length > 0;
1640
+ const hasContent = typeof entry.runtimeYamlContent === "string";
1641
+ if (hasPath === hasContent)
1642
+ return null;
1643
+ const pathOrContent = entry.runtimeYamlPath ?? entry.runtimeYamlContent;
1644
+ return {
1645
+ runtimeId: entry.id,
1646
+ wallet: entry.wallet,
1647
+ stateDir: buildRuntimeStateDir(baseStateDir, entry.id, entry.wallet ?? ""),
1648
+ pathOrContent,
1649
+ fromPath: hasPath,
1650
+ runtimeYamlDir: hasPath ? dirname(pathOrContent) : entry.runtimeYamlDir,
1651
+ };
1652
+ }
1653
+ /** The facts plan for one instance, or null (logged once as `failed`) when it cannot be made. */
1633
1654
  async function planInstanceFacts(input) {
1634
1655
  try {
1635
1656
  const config = await loadRuntimeConfig(input.pathOrContent, { fromPath: input.fromPath });
@@ -1696,6 +1717,43 @@ const senpiRuntimePlugin = {
1696
1717
  await session.client.disconnect().catch(() => { });
1697
1718
  }
1698
1719
  }
1720
+ /**
1721
+ * Send one runtime's facts under `factsSendLock`. The plan is made from the registry entry and
1722
+ * the state file as they are when the send runs, so the last send for a runtime carries its
1723
+ * current recipe. A runtime no longer in the registry, or whose entry cannot be planned, gets
1724
+ * no send. An unchanged plan makes no call.
1725
+ *
1726
+ * `strategyIds` is the boot's one batched status read (null when that read failed). Without it,
1727
+ * the strategy id is read for this one wallet; a failed read logs its reason and sends nothing.
1728
+ */
1729
+ function queueFactsSend(runtimeId, strategyIds) {
1730
+ return factsSendLock.withKeyLock(runtimeId, async () => {
1731
+ const { readRegistry } = await import("./runtime/strategy-registry.js");
1732
+ const entry = (await readRegistry(baseStateDir)).runtimes.find((e) => e.id === runtimeId);
1733
+ const inputs = entry ? factsPlanInputs(entry) : null;
1734
+ if (!inputs)
1735
+ return;
1736
+ const plan = await planInstanceFacts(inputs);
1737
+ if (!plan)
1738
+ return;
1739
+ let ids = strategyIds ?? null;
1740
+ if (strategyIds === undefined && !plan.unchanged) {
1741
+ const read = await readBootWalletStatuses([plan.wallet.trim().toLowerCase()], (wallets, signal) => withSenpiClient((client) => client.listStrategies({ strategyAddresses: wallets }, { signal })));
1742
+ if (!read.ok) {
1743
+ factsLogger.warn("strategy facts failed: could not read the strategy id; retrying next boot", undefined, {
1744
+ runtimeId: plan.runtimeId,
1745
+ wallet: plan.wallet,
1746
+ hash: plan.hash.slice(0, 12),
1747
+ reason: read.reason,
1748
+ detail: read.message,
1749
+ });
1750
+ return;
1751
+ }
1752
+ ids = read.strategyIds;
1753
+ }
1754
+ await sendBootFacts([plan], ids);
1755
+ });
1756
+ }
1699
1757
  /** One connected SenpiClient per job; always disconnected, even on failure. */
1700
1758
  async function withSenpiClient(fn) {
1701
1759
  const client = mcpTransport ? new SenpiClient(mcpTransport) : new SenpiClient({ apiKey });
@@ -1908,6 +1966,13 @@ const senpiRuntimePlugin = {
1908
1966
  ...(info.slots !== undefined ? { slots: info.slots } : {}),
1909
1967
  });
1910
1968
  });
1969
+ // The apply swaps the recipe with no boot, so this send moves the backend's facts (what the
1970
+ // app shows) to the new recipe before the next boot. Detached: the apply has landed and must
1971
+ // not wait on, or fail over, the send. The send plans from the registry entry the apply has
1972
+ // written, and takes the wallet from that entry or its recipe.
1973
+ void queueFactsSend(info.runtimeId).catch((err) => {
1974
+ factsLogger.warn("strategy facts send crashed", err);
1975
+ });
1911
1976
  },
1912
1977
  });
1913
1978
  registerDeployGatewayMethods(api, {
@@ -2055,6 +2120,7 @@ const senpiRuntimePlugin = {
2055
2120
  const runtimeId = typeof ctx.params?.runtimeId === "string" ? ctx.params.runtimeId : undefined;
2056
2121
  const address = typeof ctx.params?.address === "string" ? ctx.params.address : undefined;
2057
2122
  pluginLogger.info("[senpi-runtime] senpi.getDslPositionState", { asset, runtimeId, address });
2123
+ // `exchangeStop` is derived from the state on each response and is not persisted.
2058
2124
  const matches = [];
2059
2125
  for (const [id, handle] of runtimeHandles) {
2060
2126
  if (runtimeId && id !== runtimeId)
@@ -2065,12 +2131,18 @@ const senpiRuntimePlugin = {
2065
2131
  if (address) {
2066
2132
  const state = handle.dsl.getPositionState(address, asset);
2067
2133
  if (state)
2068
- matches.push({ runtimeId: id, address, state });
2134
+ matches.push({ runtimeId: id, address, state, exchangeStop: exchangeStopClass(state) });
2069
2135
  }
2070
2136
  else {
2071
2137
  const found = handle.dsl.findPositionByAsset(asset);
2072
- if (found)
2073
- matches.push({ runtimeId: id, address: found.address, state: found.state });
2138
+ if (found) {
2139
+ matches.push({
2140
+ runtimeId: id,
2141
+ address: found.address,
2142
+ state: found.state,
2143
+ exchangeStop: exchangeStopClass(found.state),
2144
+ });
2145
+ }
2074
2146
  }
2075
2147
  }
2076
2148
  catch (err) {