dexterm-cli 0.1.7 → 0.2.1

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 (233) hide show
  1. package/dist/packages/agents/src/spectator.js +31 -3
  2. package/dist/packages/agents/src/tiers.js +12 -0
  3. package/dist/packages/agents/src/tools.js +13 -5
  4. package/dist/packages/autotrade/assets/limits.schema.json +569 -0
  5. package/dist/packages/autotrade/examples/executor/README.md +70 -0
  6. package/dist/packages/autotrade/examples/executor/jsonl-tail-executor.mjs +57 -0
  7. package/dist/packages/autotrade/examples/executor/kit.mjs +144 -0
  8. package/dist/packages/autotrade/examples/executor/signal-cmd-executor.mjs +25 -0
  9. package/dist/packages/autotrade/examples/executor/telegram-bot-recipe.md +47 -0
  10. package/dist/packages/autotrade/examples/executor/webhook-executor.mjs +47 -0
  11. package/dist/packages/autotrade/src/backtest.js +7 -2
  12. package/dist/packages/autotrade/src/chat/config.js +4 -2
  13. package/dist/packages/autotrade/src/chat/control.js +1 -0
  14. package/dist/packages/autotrade/src/chat/notify.js +16 -6
  15. package/dist/packages/autotrade/src/chat/secrets.js +25 -18
  16. package/dist/packages/autotrade/src/chat/service.js +3 -0
  17. package/dist/packages/autotrade/src/engine.js +16 -2
  18. package/dist/packages/autotrade/src/executor-health.js +217 -0
  19. package/dist/packages/autotrade/src/fleet.js +5 -2
  20. package/dist/packages/autotrade/src/forward.js +355 -0
  21. package/dist/packages/autotrade/src/index.js +21 -0
  22. package/dist/packages/autotrade/src/journey.js +477 -0
  23. package/dist/packages/autotrade/src/limits.js +682 -0
  24. package/dist/packages/autotrade/src/outbox.js +394 -0
  25. package/dist/packages/autotrade/src/paper-limits.js +87 -0
  26. package/dist/packages/autotrade/src/real-mode.js +688 -0
  27. package/dist/packages/autotrade/src/receipts.js +330 -0
  28. package/dist/packages/autotrade/src/report.js +168 -0
  29. package/dist/packages/autotrade/src/rugproof-probe.js +415 -0
  30. package/dist/packages/autotrade/src/rugproof.js +231 -0
  31. package/dist/packages/autotrade/src/runner.js +194 -28
  32. package/dist/packages/autotrade/src/service-guard.js +125 -0
  33. package/dist/packages/autotrade/src/service-install.js +215 -0
  34. package/dist/packages/autotrade/src/service-recover.js +232 -0
  35. package/dist/packages/autotrade/src/service.js +289 -0
  36. package/dist/packages/autotrade/src/sessions.js +16 -7
  37. package/dist/packages/autotrade/src/signal-channels.js +312 -0
  38. package/dist/packages/autotrade/src/signal.js +196 -0
  39. package/dist/packages/autotrade/src/strategy-stats.js +344 -0
  40. package/dist/packages/autotrade/src/strategy.js +15 -2
  41. package/dist/packages/autotrade/src/trade-ledger.js +2 -2
  42. package/dist/packages/classifier/src/calibrate.js +138 -0
  43. package/dist/packages/classifier/src/classify.js +107 -0
  44. package/dist/packages/classifier/src/config.js +220 -0
  45. package/dist/packages/classifier/src/dossier.js +95 -0
  46. package/dist/packages/classifier/src/hardware.js +63 -0
  47. package/dist/packages/classifier/src/index.js +18 -0
  48. package/dist/packages/classifier/src/models.js +111 -0
  49. package/dist/packages/classifier/src/providers/heuristic.js +77 -0
  50. package/dist/packages/classifier/src/providers/http.js +63 -0
  51. package/dist/packages/classifier/src/providers/jev-open.js +86 -0
  52. package/dist/packages/classifier/src/providers/llm.js +89 -0
  53. package/dist/packages/classifier/src/providers/systemone-http.js +95 -0
  54. package/dist/packages/classifier/src/questions.js +97 -0
  55. package/dist/packages/classifier/src/registry.js +42 -0
  56. package/dist/packages/classifier/src/snapshot.js +31 -0
  57. package/dist/packages/classifier/src/systemone.js +88 -0
  58. package/dist/packages/classifier/src/types.js +18 -0
  59. package/dist/packages/cli/assets/dash/dash.css +139 -0
  60. package/dist/packages/cli/assets/dash/dash.js +200 -0
  61. package/dist/packages/cli/assets/dash/icon.svg +1 -0
  62. package/dist/packages/cli/assets/dash/index.html +36 -0
  63. package/dist/packages/cli/src/analyst/backend.js +3 -2
  64. package/dist/packages/cli/src/analyst/codex.js +3 -2
  65. package/dist/packages/cli/src/app-help-gen.js +26 -2
  66. package/dist/packages/cli/src/autotrade-cmd/audit.js +19 -2
  67. package/dist/packages/cli/src/autotrade-cmd/backtest.js +21 -4
  68. package/dist/packages/cli/src/autotrade-cmd/executor.js +91 -0
  69. package/dist/packages/cli/src/autotrade-cmd/fleet.js +16 -5
  70. package/dist/packages/cli/src/autotrade-cmd/forward.js +283 -0
  71. package/dist/packages/cli/src/autotrade-cmd/journey.js +208 -0
  72. package/dist/packages/cli/src/autotrade-cmd/limits.js +393 -0
  73. package/dist/packages/cli/src/autotrade-cmd/receipt.js +63 -0
  74. package/dist/packages/cli/src/autotrade-cmd/report.js +79 -0
  75. package/dist/packages/cli/src/autotrade-cmd/run.js +268 -14
  76. package/dist/packages/cli/src/autotrade-cmd/service.js +873 -0
  77. package/dist/packages/cli/src/autotrade-cmd/signal.js +824 -0
  78. package/dist/packages/cli/src/autotrade-cmd.js +50 -4
  79. package/dist/packages/cli/src/classifier-cmd.js +353 -0
  80. package/dist/packages/cli/src/classifier-live.js +81 -0
  81. package/dist/packages/cli/src/dash/data.js +217 -0
  82. package/dist/packages/cli/src/dash/server.js +92 -0
  83. package/dist/packages/cli/src/dash-cmd.js +113 -0
  84. package/dist/packages/cli/src/doctor.js +34 -0
  85. package/dist/packages/cli/src/ledger-cmd.js +4 -2
  86. package/dist/packages/cli/src/live/screens/token-live.js +4 -0
  87. package/dist/packages/cli/src/live/signals.js +8 -0
  88. package/dist/packages/cli/src/main.js +98 -6
  89. package/dist/packages/cli/src/mcp-follow.js +21 -1
  90. package/dist/packages/cli/src/narratives-cmd.js +147 -0
  91. package/dist/packages/cli/src/narratives.js +114 -0
  92. package/dist/packages/cli/src/overlay/model.js +147 -0
  93. package/dist/packages/cli/src/overlay/page.js +146 -0
  94. package/dist/packages/cli/src/overlay/pnl.js +75 -0
  95. package/dist/packages/cli/src/overlay/server.js +170 -0
  96. package/dist/packages/cli/src/overlay-cmd.js +367 -0
  97. package/dist/packages/cli/src/pnl-card.js +4 -0
  98. package/dist/packages/cli/src/record-cmd.js +82 -11
  99. package/dist/packages/cli/src/rules-cmd.js +2 -1
  100. package/dist/packages/cli/src/rules.js +24 -4
  101. package/dist/packages/cli/src/screens/autotrade-journey.js +134 -0
  102. package/dist/packages/cli/src/screens/autotrade-keys.js +13 -0
  103. package/dist/packages/cli/src/screens/autotrade-screen.js +57 -6
  104. package/dist/packages/cli/src/screens/autotrade-ui.js +17 -7
  105. package/dist/packages/cli/src/screens/autotrade-worker.js +4 -2
  106. package/dist/packages/cli/src/screens/narrative.js +154 -2
  107. package/dist/packages/cli/src/state.js +1 -0
  108. package/dist/packages/cli/src/telegram-cmd.js +4 -1
  109. package/dist/packages/cli/src/version.js +1 -1
  110. package/dist/packages/custom/src/rules.js +5 -1
  111. package/dist/packages/data/src/trade/ledger/schema.js +1 -1
  112. package/dist/packages/mcp/assets/kb/app-help/autotrade-executor-kit.md +44 -0
  113. package/dist/packages/mcp/assets/kb/app-help/autotrade-journey.md +46 -0
  114. package/dist/packages/mcp/assets/kb/app-help/autotrade-limits.md +48 -0
  115. package/dist/packages/mcp/assets/kb/app-help/autotrade-real-signal.md +45 -0
  116. package/dist/packages/mcp/assets/kb/app-help/classification-models.md +41 -0
  117. package/dist/packages/mcp/assets/kb/app-help/live-trading.md +1 -1
  118. package/dist/packages/mcp/assets/kb/app-help/narratives-tracker.md +45 -0
  119. package/dist/packages/mcp/assets/kb/app-help/screen-autotrade.md +12 -0
  120. package/dist/packages/mcp/assets/kb/app-help/strategy-dashboard.md +38 -0
  121. package/dist/packages/mcp/assets/kb/app-help/streamer-overlay.md +36 -0
  122. package/dist/packages/mcp/assets/kb/app-help/trade-ledger.md +1 -1
  123. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-change-a-strategy-by-chat.md +3 -1
  124. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-on-the-q-screen.md +1 -0
  125. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-rug-proof-entries-experimental.md +52 -0
  126. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-running-it-all-day.md +2 -2
  127. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-stopping-it.md +2 -0
  128. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-always-on-service.md +59 -0
  129. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-the-strategy-dashboard-dexterm-dash.md +37 -0
  130. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-is-paper-self-directed.md +9 -2
  131. package/dist/packages/mcp/assets/kb/docs/docs-autotrade-what-it-will-not-do.md +1 -1
  132. package/dist/packages/mcp/assets/kb/docs/docs-autotrade.md +3 -1
  133. package/dist/packages/mcp/assets/kb/docs/docs-beginners-before-any-experimental-trading.md +2 -2
  134. package/dist/packages/mcp/assets/kb/docs/docs-beginners-copy-and-paste-without-surprises.md +0 -1
  135. package/dist/packages/mcp/assets/kb/docs/docs-beginners-if-dexterm-says-command-not-found.md +2 -2
  136. package/dist/packages/mcp/assets/kb/docs/docs-beginners-reopen-dexterm-later.md +1 -1
  137. package/dist/packages/mcp/assets/kb/docs/docs-beginners-terminal-shell-prompt-command-output-and-path.md +2 -2
  138. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-expectations.md +1 -1
  139. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows.md +1 -1
  140. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-1-ask-for-a-strategy.md +27 -0
  141. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-2-backtest-it-held-out-split.md +31 -0
  142. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-3-forward-test-it-on-live-data.md +44 -0
  143. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-4-promote-it-to-always-on-paper.md +36 -0
  144. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-5-real-only-if-you-choose.md +36 -0
  145. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-ladder.md +41 -0
  146. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-the-report.md +31 -0
  147. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat-what-the-analyst-knows.md +30 -0
  148. package/dist/packages/mcp/assets/kb/docs/docs-build-a-strategy-by-chat.md +26 -0
  149. package/dist/packages/mcp/assets/kb/docs/docs-classifier-hosted-jev-with-your-own-key.md +34 -0
  150. package/dist/packages/mcp/assets/kb/docs/docs-classifier-how-they-measured-on-dexterm-s-own-history.md +52 -0
  151. package/dist/packages/mcp/assets/kb/docs/docs-classifier-set-up-kev-on-your-own-machine.md +39 -0
  152. package/dist/packages/mcp/assets/kb/docs/docs-classifier-settings.md +35 -0
  153. package/dist/packages/mcp/assets/kb/docs/docs-classifier-the-choices.md +36 -0
  154. package/dist/packages/mcp/assets/kb/docs/docs-classifier-where-it-plugs-in.md +29 -0
  155. package/dist/packages/mcp/assets/kb/docs/docs-classifier-which-model-for-which-machine.md +42 -0
  156. package/dist/packages/mcp/assets/kb/docs/docs-classifier.md +32 -0
  157. package/dist/packages/mcp/assets/kb/docs/docs-cli-invocations.md +1 -1
  158. package/dist/packages/mcp/assets/kb/docs/docs-configuration-headless-recorder-dexterm-record.md +3 -0
  159. package/dist/packages/mcp/assets/kb/docs/docs-copy-trading.md +1 -1
  160. package/dist/packages/mcp/assets/kb/docs/docs-data-sources-recorded-history-and-retention.md +1 -0
  161. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-install.md +3 -3
  162. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-requirements.md +2 -2
  163. package/dist/packages/mcp/assets/kb/docs/docs-getting-started-supported-platforms.md +2 -11
  164. package/dist/packages/mcp/assets/kb/docs/docs-getting-started.md +2 -2
  165. package/dist/packages/mcp/assets/kb/docs/docs-going-real-executor-health.md +38 -0
  166. package/dist/packages/mcp/assets/kb/docs/docs-going-real-paper-vs-real.md +33 -0
  167. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-digest.md +36 -0
  168. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-executor-kit.md +48 -0
  169. package/dist/packages/mcp/assets/kb/docs/docs-going-real-the-whole-path.md +37 -0
  170. package/dist/packages/mcp/assets/kb/docs/docs-going-real-what-dexterm-does-and-never-does.md +44 -0
  171. package/dist/packages/mcp/assets/kb/docs/docs-going-real-your-limits.md +77 -0
  172. package/dist/packages/mcp/assets/kb/docs/docs-going-real.md +24 -0
  173. package/dist/packages/mcp/assets/kb/docs/docs-larp-measured-accuracy.md +2 -2
  174. package/dist/packages/mcp/assets/kb/docs/docs-larp-paste-a-coin-on-the-web.md +1 -1
  175. package/dist/packages/mcp/assets/kb/docs/docs-larp-the-larp-score.md +1 -1
  176. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-changed-in-0-1-7.md +1 -1
  177. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-real-mode-your-strategy-s-trades-to-your.md +31 -0
  178. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-the-short-version.md +1 -1
  179. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-using-a-trading-bot-with-dexterm.md +0 -2
  180. package/dist/packages/mcp/assets/kb/docs/docs-live-trading-what-dexterm-does-not-do.md +1 -1
  181. package/dist/packages/mcp/assets/kb/docs/docs-narratives-how-coins-are-grouped.md +39 -0
  182. package/dist/packages/mcp/assets/kb/docs/docs-narratives-limits.md +29 -0
  183. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-is-measured.md +44 -0
  184. package/dist/packages/mcp/assets/kb/docs/docs-narratives-what-testing-showed.md +43 -0
  185. package/dist/packages/mcp/assets/kb/docs/docs-narratives-where-to-find-it.md +37 -0
  186. package/dist/packages/mcp/assets/kb/docs/docs-narratives.md +29 -0
  187. package/dist/packages/mcp/assets/kb/docs/docs-overlay-before-you-start.md +33 -0
  188. package/dist/packages/mcp/assets/kb/docs/docs-overlay-limits.md +30 -0
  189. package/dist/packages/mcp/assets/kb/docs/docs-overlay-options.md +36 -0
  190. package/dist/packages/mcp/assets/kb/docs/docs-overlay-security-model.md +36 -0
  191. package/dist/packages/mcp/assets/kb/docs/docs-overlay-set-it-up-in-obs-step-by-step.md +49 -0
  192. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-pnl-scene-paper-and-live.md +34 -0
  193. package/dist/packages/mcp/assets/kb/docs/docs-overlay-the-three-scenes.md +35 -0
  194. package/dist/packages/mcp/assets/kb/docs/docs-overlay-what-is-redacted.md +31 -0
  195. package/dist/packages/mcp/assets/kb/docs/docs-overlay.md +25 -0
  196. package/dist/packages/mcp/assets/kb/docs/docs-paper-trading.md +1 -1
  197. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-receipts-what-your-executor-did.md +42 -0
  198. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-safety.md +34 -0
  199. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-set-it-up.md +47 -0
  200. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-channels.md +42 -0
  201. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-the-signal.md +48 -0
  202. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-is.md +37 -0
  203. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-what-it-will-not-do.md +34 -0
  204. package/dist/packages/mcp/assets/kb/docs/docs-real-mode-where-you-see-it.md +34 -0
  205. package/dist/packages/mcp/assets/kb/docs/docs-real-mode.md +27 -0
  206. package/dist/packages/mcp/assets/kb/docs/docs-screens-bubble-map.md +1 -1
  207. package/dist/packages/mcp/assets/kb/docs/docs-spectator-streaming-it.md +1 -0
  208. package/dist/packages/mcp/assets/kb/docs/docs-telegram-what-it-is.md +1 -1
  209. package/dist/packages/mcp/assets/kb/docs/docs-voice-linux-and-windows.md +2 -2
  210. package/dist/packages/mcp/assets/kb/entries/autotrade.md +4 -2
  211. package/dist/packages/mcp/assets/kb/entries/narratives-tracker.md +45 -0
  212. package/dist/packages/mcp/src/autotrade-tools.js +168 -5
  213. package/dist/packages/mcp/src/engine/restrictions.js +3 -3
  214. package/dist/packages/mcp/src/index.js +1 -1
  215. package/dist/packages/mcp/src/ledger-tools.js +155 -7
  216. package/dist/packages/mcp/src/server.js +125 -10
  217. package/dist/packages/mcp/src/spectator-sink.js +27 -0
  218. package/dist/packages/narratives/src/cluster.js +193 -0
  219. package/dist/packages/narratives/src/embed.js +184 -0
  220. package/dist/packages/narratives/src/export.js +60 -0
  221. package/dist/packages/narratives/src/index.js +10 -0
  222. package/dist/packages/narratives/src/label.js +48 -0
  223. package/dist/packages/narratives/src/normalize.js +179 -0
  224. package/dist/packages/narratives/src/recorder.js +93 -0
  225. package/dist/packages/narratives/src/tracker.js +328 -0
  226. package/dist/packages/recorder/src/exit-probes.js +215 -0
  227. package/dist/packages/recorder/src/index.js +1 -0
  228. package/dist/packages/recorder/src/recorder.js +11 -1
  229. package/dist/packages/recorder/src/retention.js +4 -0
  230. package/dist/packages/recorder/src/schema.js +20 -1
  231. package/dist/packages/tui/src/keymap.js +12 -0
  232. package/package.json +3 -1
  233. package/dist/packages/mcp/assets/kb/docs/docs-beginners-windows-powershell-experimental.md +0 -39
@@ -0,0 +1,394 @@
1
+ // dexterm autotrade — the trade-fire outbox (task-1645, 0.2.0 card A).
2
+ //
3
+ // Every real-mode signal is written here (fsync) BEFORE the runner books its fill, then delivered to each channel
4
+ // the user enabled: at-least-once, in nonce order per channel, retried with backoff (1 s doubling to 30 s) until
5
+ // the channel acks, rejects it for good, or the signal's TTL passes (then it is marked expired and never sent late).
6
+ //
7
+ // $DEXTERM_HOME/autotrade/real/outbox.jsonl append-only records, replayed on open (a restart resumes delivery)
8
+ // {k:"enq", signal, channels} a signal and the channels it was addressed to
9
+ // {k:"ack"|"rej"|"exp"|"drop", nonce, ch} a channel's terminal outcome (drop: the channel is no longer set up)
10
+ // {k:"try", nonce, ch, why, attempt} a failed attempt (retried)
11
+ // {k:"run", runId, state:"open"|"closed"} run markers: a run left "open" died without its halted signal
12
+ // {k:"base", lastNonce} the head of an outbox started by compaction (r2): the nonce floor
13
+ // $DEXTERM_HOME/autotrade/real/nonce.json the last nonce handed out (written before the signal exists)
14
+ // $DEXTERM_HOME/autotrade/real/outbox.lock one writer at a time (a stale lock of a dead pid is reclaimed)
15
+ //
16
+ // Nonces only grow: the next one is max(nonce.json, the outbox's base record, the newest in the outbox) + 1, so
17
+ // neither a restart, a compaction nor a lost nonce file can hand out a number twice. A record that cannot be parsed is skipped and counted (`corrupt`).
18
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+ import { writeJsonAtomic } from "./ledger.js";
21
+ import { validateSignal } from "./signal.js";
22
+ export const CHANNEL_NAMES = ['webhook', 'cmd', 'jsonl', 'mcp', 'telegram'];
23
+ /** Channels an executor can read. The Telegram copy is for the human: a bot cannot read another bot's messages. */
24
+ export const EXECUTOR_CHANNELS = ['webhook', 'cmd', 'jsonl', 'mcp'];
25
+ export const realDir = (home) => join(home, 'autotrade', 'real');
26
+ export const outboxPath = (home) => join(realDir(home), 'outbox.jsonl');
27
+ export const noncePath = (home) => join(realDir(home), 'nonce.json');
28
+ const lockPath = (home) => join(realDir(home), 'outbox.lock');
29
+ export const BACKOFF_MIN_MS = 1000;
30
+ export const BACKOFF_MAX_MS = 30_000;
31
+ const COMPACT_ABOVE_BYTES = 1 << 20;
32
+ const MAX_PER_PUMP = 50;
33
+ const alive = (pid) => { try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch (e) {
38
+ return e.code === 'EPERM';
39
+ } };
40
+ function appendSync(path, line) {
41
+ const fd = openSync(path, 'a', 0o600);
42
+ try {
43
+ writeSync(fd, line.endsWith('\n') ? line : `${line}\n`);
44
+ fsyncSync(fd);
45
+ }
46
+ finally {
47
+ closeSync(fd);
48
+ }
49
+ }
50
+ export class Outbox {
51
+ home;
52
+ path;
53
+ channels = new Map();
54
+ defer;
55
+ entries = [];
56
+ byNonce = new Map();
57
+ runs = new Map();
58
+ health = new Map();
59
+ now;
60
+ lastNonce = 0;
61
+ running = null;
62
+ queued = null;
63
+ locked = false;
64
+ corrupt = 0;
65
+ constructor(home, o) {
66
+ this.home = home;
67
+ this.path = outboxPath(home);
68
+ this.now = o.now ?? Date.now;
69
+ for (const c of o.channels)
70
+ this.channels.set(c.name, c);
71
+ this.defer = new Set(o.defer ?? []);
72
+ }
73
+ /** Open (and replay) the outbox of a dexterm home. Throws when another live process holds it. */
74
+ static open(home, o) {
75
+ mkdirSync(realDir(home), { recursive: true, mode: 0o700 });
76
+ const b = new Outbox(home, o);
77
+ if (!o.noLock)
78
+ b.lock();
79
+ b.replay();
80
+ // Only the lock holder compacts (it writes): a read-only view never moves the file under a running writer.
81
+ if (!o.noLock)
82
+ b.compact();
83
+ return b;
84
+ }
85
+ lock() {
86
+ const p = lockPath(this.home);
87
+ for (let i = 0; i < 2; i++) {
88
+ try {
89
+ const fd = openSync(p, 'wx', 0o600);
90
+ writeSync(fd, `${JSON.stringify({ pid: process.pid, at: this.now() })}\n`);
91
+ closeSync(fd);
92
+ this.locked = true;
93
+ return;
94
+ }
95
+ catch (e) {
96
+ if (e.code !== 'EEXIST')
97
+ throw e;
98
+ }
99
+ let pid = 0;
100
+ try {
101
+ pid = Number(JSON.parse(readFileSync(p, 'utf8')).pid) || 0;
102
+ }
103
+ catch {
104
+ pid = 0;
105
+ }
106
+ if (pid && pid !== process.pid && alive(pid))
107
+ throw new Error(`the trade-fire outbox is in use by pid ${pid} (a running real-mode runner); stop it first`);
108
+ rmSync(p, { force: true });
109
+ }
110
+ throw new Error('could not lock the trade-fire outbox');
111
+ }
112
+ close() {
113
+ if (!this.locked)
114
+ return;
115
+ try {
116
+ const d = JSON.parse(readFileSync(lockPath(this.home), 'utf8'));
117
+ if (d.pid === process.pid)
118
+ rmSync(lockPath(this.home), { force: true });
119
+ }
120
+ catch { /* gone */ }
121
+ this.locked = false;
122
+ }
123
+ /** Swap the channel set (config changed). Pending deliveries to a channel that is gone are dropped at the next pump. */
124
+ setChannels(cs) { this.channels = new Map(cs.map((c) => [c.name, c])); }
125
+ channelNames() { return [...this.channels.keys()]; }
126
+ replay() {
127
+ if (existsSync(noncePath(this.home))) {
128
+ try {
129
+ const n = Number(JSON.parse(readFileSync(noncePath(this.home), 'utf8')).last);
130
+ if (Number.isSafeInteger(n) && n > 0)
131
+ this.lastNonce = n;
132
+ }
133
+ catch {
134
+ this.corrupt++;
135
+ }
136
+ }
137
+ if (!existsSync(this.path))
138
+ return;
139
+ for (const raw of readFileSync(this.path, 'utf8').split('\n')) {
140
+ if (!raw.trim())
141
+ continue;
142
+ let r;
143
+ try {
144
+ r = JSON.parse(raw);
145
+ }
146
+ catch {
147
+ this.corrupt++;
148
+ continue;
149
+ }
150
+ this.apply(r, false);
151
+ }
152
+ }
153
+ /** Apply one record to the in-memory state (replay and live writes share this). */
154
+ apply(r, live) {
155
+ const at = typeof r.at === 'number' ? r.at : 0;
156
+ if (r.k === 'enq') {
157
+ const v = validateSignal(r.signal);
158
+ if (!v.ok || this.byNonce.has(v.signal.nonce)) {
159
+ this.corrupt++;
160
+ return;
161
+ }
162
+ const chs = Array.isArray(r.channels) ? r.channels.filter((c) => CHANNEL_NAMES.includes(c)) : [];
163
+ const e = { signal: v.signal, line: JSON.stringify(v.signal), ch: new Map(chs.map((c) => [c, { done: null, attempts: 0, nextAt: 0, lastWhy: null }])) };
164
+ this.entries.push(e);
165
+ this.byNonce.set(e.signal.nonce, e);
166
+ this.lastNonce = Math.max(this.lastNonce, e.signal.nonce);
167
+ return;
168
+ }
169
+ if (r.k === 'base') {
170
+ const n = Number(r.lastNonce);
171
+ if (Number.isSafeInteger(n) && n > 0)
172
+ this.lastNonce = Math.max(this.lastNonce, n);
173
+ else
174
+ this.corrupt++;
175
+ return;
176
+ }
177
+ if (r.k === 'run' && typeof r.runId === 'string') {
178
+ this.runs.set(r.runId, { state: r.state === 'closed' ? 'closed' : 'open', at, strategy: r.strategy, accountId: typeof r.accountId === 'string' ? r.accountId : undefined, reason: typeof r.reason === 'string' ? r.reason : undefined });
179
+ return;
180
+ }
181
+ const e = this.byNonce.get(Number(r.nonce));
182
+ const name = r.ch;
183
+ const st = e?.ch.get(name);
184
+ if (!e || !st) {
185
+ if (!live && !['try', 'ack', 'rej', 'exp', 'drop'].includes(String(r.k)))
186
+ this.corrupt++;
187
+ return;
188
+ }
189
+ if (r.k === 'try') {
190
+ st.attempts = Number(r.attempt) || st.attempts + 1;
191
+ st.lastWhy = String(r.why ?? '').slice(0, 200);
192
+ st.nextAt = at + backoff(st.attempts);
193
+ return;
194
+ }
195
+ if (r.k === 'ack' || r.k === 'rej' || r.k === 'exp' || r.k === 'drop') {
196
+ st.done = r.k;
197
+ st.dup = r.dup === true;
198
+ st.lastWhy = typeof r.why === 'string' ? r.why.slice(0, 200) : st.lastWhy;
199
+ }
200
+ }
201
+ write(r) {
202
+ appendSync(this.path, JSON.stringify(r));
203
+ this.apply(r, true);
204
+ }
205
+ /** Move a fully settled outbox aside once it is large (the run ledgers keep every signal for the audit). */
206
+ compact() {
207
+ if (!existsSync(this.path) || statSync(this.path).size < COMPACT_ABOVE_BYTES)
208
+ return;
209
+ if (this.entries.some((e) => [...e.ch.values()].some((c) => !c.done)) || [...this.runs.values()].some((r) => r.state === 'open'))
210
+ return;
211
+ renameSync(this.path, join(realDir(this.home), `outbox.${this.now()}.done.jsonl`));
212
+ this.entries.length = 0;
213
+ this.byNonce.clear();
214
+ this.runs.clear();
215
+ // The fresh outbox starts with the nonce floor, so a lost nonce.json can never restart the count at 1.
216
+ if (this.lastNonce > 0)
217
+ this.write({ k: 'base', at: this.now(), lastNonce: this.lastNonce });
218
+ }
219
+ /** The next nonce, persisted before it is used (so a crash after this line can only skip a number, never reuse one). */
220
+ allocateNonce() {
221
+ const n = this.lastNonce + 1;
222
+ writeJsonAtomic(noncePath(this.home), { last: n, at: this.now() });
223
+ this.lastNonce = n;
224
+ return n;
225
+ }
226
+ get lastNonceIssued() { return this.lastNonce; }
227
+ /** Durably queue a signal for every channel set up now. Throws when the disk refuses (the caller then fires nothing). */
228
+ enqueue(signal) {
229
+ if (this.byNonce.has(signal.nonce))
230
+ throw new Error(`nonce ${signal.nonce} is already in the outbox`);
231
+ if (signal.nonce > this.lastNonce) {
232
+ writeJsonAtomic(noncePath(this.home), { last: signal.nonce, at: this.now() });
233
+ this.lastNonce = signal.nonce;
234
+ }
235
+ this.write({ k: 'enq', at: this.now(), channels: [...this.channels.keys()], signal });
236
+ }
237
+ markRun(runId, state, extra = {}) {
238
+ this.write({ k: 'run', at: this.now(), runId, state, ...extra });
239
+ }
240
+ /** Runs that were opened and never closed (their runner died before sending `halted`). */
241
+ unclosedRuns() {
242
+ return [...this.runs.entries()].filter(([, r]) => r.state === 'open').map(([runId, r]) => ({ runId, at: r.at, strategy: r.strategy, accountId: r.accountId }));
243
+ }
244
+ /**
245
+ * Deliver what is due. Serialized: a call while a pump runs queues ONE follow-up pump (signals queued meanwhile are
246
+ * covered) and resolves with its report.
247
+ */
248
+ pump() {
249
+ if (this.running) {
250
+ this.queued ??= this.running.then(() => { this.queued = null; return this.pump(); });
251
+ return this.queued;
252
+ }
253
+ this.running = this.pumpOnce().finally(() => { this.running = null; });
254
+ return this.running;
255
+ }
256
+ async pumpOnce() {
257
+ const rep = { acked: 0, retried: 0, rejected: 0, expired: 0, dropped: 0 };
258
+ const names = new Set();
259
+ for (const e of this.entries)
260
+ for (const [n, st] of e.ch)
261
+ if (!st.done)
262
+ names.add(n);
263
+ // Channels deliver side by side (a slow hook never holds up the stream); each one strictly in nonce order.
264
+ await Promise.all([...names].map((name) => this.pumpChannel(name, rep)));
265
+ return rep;
266
+ }
267
+ async pumpChannel(name, rep) {
268
+ const chan = this.channels.get(name);
269
+ if (!chan && this.defer.has(name))
270
+ return;
271
+ let sent = 0;
272
+ for (const e of this.entries) {
273
+ const st = e.ch.get(name);
274
+ if (!st || st.done)
275
+ continue;
276
+ const now = this.now();
277
+ if (!chan) {
278
+ this.write({ k: 'drop', at: now, nonce: e.signal.nonce, ch: name, why: 'channel-not-configured' });
279
+ rep.dropped++;
280
+ continue;
281
+ }
282
+ if (now > e.signal.expiresAt) {
283
+ this.write({ k: 'exp', at: now, nonce: e.signal.nonce, ch: name });
284
+ rep.expired++;
285
+ continue;
286
+ }
287
+ if (st.nextAt > now)
288
+ return; // in order: a channel waits on its oldest undelivered signal
289
+ if (sent++ >= MAX_PER_PUMP)
290
+ return;
291
+ let r;
292
+ try {
293
+ r = await chan.deliver(e.signal, e.line);
294
+ }
295
+ catch (err) {
296
+ r = { ok: false, retry: true, why: err.message.slice(0, 160) };
297
+ }
298
+ const at = this.now();
299
+ const h = this.healthOf(name);
300
+ if (r.ok) {
301
+ this.write({ k: 'ack', at, nonce: e.signal.nonce, ch: name, ...(r.duplicate ? { dup: true } : {}) });
302
+ h.lastOkAt = at;
303
+ h.failing = 0;
304
+ rep.acked++;
305
+ continue;
306
+ }
307
+ h.lastErrAt = at;
308
+ h.lastErr = r.why.slice(0, 160);
309
+ h.failing++;
310
+ if (!r.retry) {
311
+ this.write({ k: 'rej', at, nonce: e.signal.nonce, ch: name, why: r.why.slice(0, 200) });
312
+ rep.rejected++;
313
+ continue;
314
+ }
315
+ this.write({ k: 'try', at, nonce: e.signal.nonce, ch: name, why: r.why.slice(0, 200), attempt: st.attempts + 1 });
316
+ rep.retried++;
317
+ return;
318
+ }
319
+ }
320
+ healthOf(name) {
321
+ let h = this.health.get(name);
322
+ if (!h) {
323
+ h = { name, pending: 0, lastOkAt: null, lastErrAt: null, lastErr: null, failing: 0 };
324
+ this.health.set(name, h);
325
+ }
326
+ return h;
327
+ }
328
+ /** Per channel: pending deliveries, the last success and failure (this process), consecutive failures. */
329
+ channelHealth() {
330
+ const out = [];
331
+ for (const name of new Set([...this.channels.keys(), ...this.health.keys()])) {
332
+ const h = { ...this.healthOf(name) };
333
+ h.pending = this.entries.filter((e) => { const s = e.ch.get(name); return s && !s.done; }).length;
334
+ if (h.lastErr === null) {
335
+ const last = [...this.entries].reverse().map((e) => e.ch.get(name)).find((s) => s?.lastWhy);
336
+ h.lastErr = last?.lastWhy ?? null;
337
+ }
338
+ out.push(h);
339
+ }
340
+ return out;
341
+ }
342
+ /** Signals with at least one channel not settled yet. */
343
+ depth() { return this.entries.filter((e) => [...e.ch.values()].some((s) => !s.done)).length; }
344
+ stateOf(nonce) {
345
+ const e = this.byNonce.get(nonce);
346
+ if (!e)
347
+ return null;
348
+ const ss = [...e.ch.values()];
349
+ if (!ss.length)
350
+ return this.now() > e.signal.expiresAt ? 'expired' : 'pending';
351
+ if (ss.some((s) => !s.done))
352
+ return ss.some((s) => s.done === 'ack') ? 'partial' : 'pending';
353
+ if (ss.length && ss.every((s) => s.done === 'ack'))
354
+ return 'delivered';
355
+ if (ss.some((s) => s.done === 'ack'))
356
+ return 'partial';
357
+ return ss.some((s) => s.done === 'rej') ? 'rejected' : 'expired';
358
+ }
359
+ /** Trade signals of a run that were queued or delivered and have not expired yet (the halted signal lists them). */
360
+ outstanding(runId) {
361
+ const now = this.now();
362
+ return this.entries.filter((e) => e.signal.type === 'trade' && (!runId || e.signal.runId === runId) && now <= e.signal.expiresAt).map((e) => e.signal.nonce);
363
+ }
364
+ signals() { return this.entries.map((e) => e.signal); }
365
+ get(nonce) { return this.byNonce.get(nonce)?.signal; }
366
+ findById(id) { return this.entries.find((e) => e.signal.id === id)?.signal; }
367
+ }
368
+ /**
369
+ * Every valid signal ever queued on this home, oldest nonce first: the live outbox plus the archives compaction moved
370
+ * aside. Read-only; used to rebuild the real holdings when held.json is missing.
371
+ */
372
+ export function allOutboxSignals(home) {
373
+ const dir = realDir(home);
374
+ if (!existsSync(dir))
375
+ return [];
376
+ const byNonce = new Map();
377
+ for (const f of readdirSync(dir).filter((n) => /^outbox(\.\d+\.done)?\.jsonl$/.test(n))) {
378
+ for (const raw of readFileSync(join(dir, f), 'utf8').split('\n')) {
379
+ if (!raw.trim())
380
+ continue;
381
+ try {
382
+ const r = JSON.parse(raw);
383
+ if (r.k !== 'enq')
384
+ continue;
385
+ const v = validateSignal(r.signal);
386
+ if (v.ok && !byNonce.has(v.signal.nonce))
387
+ byNonce.set(v.signal.nonce, v.signal);
388
+ }
389
+ catch { /* torn line */ }
390
+ }
391
+ }
392
+ return [...byNonce.values()].sort((a, b) => a.nonce - b.nonce);
393
+ }
394
+ export const backoff = (attempt) => Math.min(BACKOFF_MAX_MS, BACKOFF_MIN_MS * 2 ** Math.max(0, attempt - 1));
@@ -0,0 +1,87 @@
1
+ // dexterm autotrade — the limits on a PAPER run (task-1654, 0.2.0 card E): the `paper` scope of limits.json.
2
+ //
3
+ // Paper ignores the limits unless the user adds a `modes.paper` scope; then a paper run (a forward test) obeys the
4
+ // same guard a real run does (limits.ts checkBuy / checkSell / recordSell), so it shows what real mode would have
5
+ // refused or clamped. The state is the run's own (in memory): paper never touches real mode's risk.json, held.json
6
+ // or counters. No signal, no executor, no network: it only wraps the paper fill model.
7
+ import { checkBuy, checkSell, freshRiskState, recordBuy, recordSell } from "./limits.js";
8
+ import { localDay } from "./chat/audit.js";
9
+ export class PaperLimitsExecutor {
10
+ name;
11
+ inner;
12
+ L;
13
+ solUsd;
14
+ bookUsd;
15
+ onTrip;
16
+ risk = null;
17
+ held = new Map();
18
+ constructor(o) {
19
+ this.inner = o.inner;
20
+ this.L = o.limits;
21
+ this.solUsd = o.solUsd;
22
+ this.bookUsd = o.bookUsd;
23
+ this.onTrip = o.onTrip ?? (() => { });
24
+ this.name = `paper-limits(${o.inner.name})`;
25
+ }
26
+ /** The run's own state, rolled at local midnight of the tick clock (a replay keeps its days). */
27
+ state(t) {
28
+ const day = localDay(t);
29
+ if (!this.risk)
30
+ this.risk = freshRiskState(day);
31
+ else if (this.risk.day !== day)
32
+ this.risk = { ...this.risk, day, buys: 0, buySol: 0, perCoin: {}, dayPnlSol: 0 };
33
+ this.risk.recentBuys = this.risk.recentBuys.filter((x) => t - x < 60_000);
34
+ this.risk.recentSells = this.risk.recentSells.filter((x) => t - x < 60_000);
35
+ return this.risk;
36
+ }
37
+ /** The maxHoldMinutes limit for the runner (RunOpts.maxHoldMinutes), or null. */
38
+ maxHoldMinutes() { return this.L.maxHoldMinutes ?? null; }
39
+ async buy(k, usd, ctx) {
40
+ const solUsd = this.solUsd();
41
+ if (!solUsd || !(solUsd > 0))
42
+ return { refused: 'paper-limits-sol-price-unknown' };
43
+ const s = this.state(k.t);
44
+ const prior = this.held.get(k.mint);
45
+ if (!prior && this.L.maxOpenPositions !== undefined && this.held.size >= this.L.maxOpenPositions)
46
+ return { refused: 'paper-limit-max-open' };
47
+ const openCostUsd = [...this.held.values()].reduce((a, h) => a + Math.max(0, h.costUsd - h.costOutUsd), 0);
48
+ const v = checkBuy(this.L, s, { now: k.t, solUsd, usd, bookUsd: this.bookUsd(), mint: k.mint, liqUsd: k.liq, mcapUsd: k.mcapResolved ?? k.mcap, positionCostUsd: prior ? Math.max(0, prior.costUsd - prior.costOutUsd) : 0, openCostUsd });
49
+ if (!v.ok)
50
+ return { refused: v.refused.replace(/^real-/, 'paper-limit-') };
51
+ const f = await this.inner.buy(k, v.usd, ctx);
52
+ if ('refused' in f)
53
+ return f;
54
+ recordBuy(s, k.mint, f.usd / solUsd, k.t);
55
+ const h = this.held.get(k.mint) ?? { tokens: 0, costUsd: 0, costOutUsd: 0, realizedUsd: 0 };
56
+ h.tokens += f.tokens;
57
+ h.costUsd += f.usd;
58
+ this.held.set(k.mint, h);
59
+ return v.clamps.length ? { ...f, limitClamps: v.clamps } : f;
60
+ }
61
+ async sell(k, tokens, reason) {
62
+ const s = this.state(k.t);
63
+ const cs = checkSell(this.L, s, k.t);
64
+ if (!cs.ok)
65
+ return { refused: cs.refused.replace(/^real-/, 'paper-limit-') };
66
+ const f = await this.inner.sell(k, tokens, reason);
67
+ if ('refused' in f)
68
+ return f;
69
+ const solUsd = this.solUsd() ?? 0;
70
+ const h = this.held.get(k.mint);
71
+ if (h && solUsd > 0) {
72
+ const frac = h.tokens > 0 ? Math.min(1, f.tokens / h.tokens) : 1;
73
+ const portion = Math.max(0, h.costUsd - h.costOutUsd) * frac;
74
+ h.tokens -= f.tokens;
75
+ h.costOutUsd += portion;
76
+ h.realizedUsd += Math.max(0, f.usd);
77
+ const closed = h.tokens <= 1e-12 || frac >= 0.999;
78
+ for (const line of recordSell(this.L, s, { pnlSol: (Math.max(0, f.usd) - portion) / solUsd, closedPnlSol: closed ? (h.realizedUsd - h.costUsd) / solUsd : null, now: k.t }))
79
+ this.onTrip(`PAPER LIMIT: ${line}`);
80
+ if (closed)
81
+ this.held.delete(k.mint);
82
+ }
83
+ else if (h && (h.tokens - f.tokens <= 1e-12))
84
+ this.held.delete(k.mint);
85
+ return f;
86
+ }
87
+ }