@alecsibilia/luca 14.0.0-alpha.2 → 14.0.0-alpha.21

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 (321) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +7 -2
  3. package/board/client/board-look.ts +31 -1
  4. package/board/client/board-panel-parts.tsx +150 -0
  5. package/board/client/board-panel-stages.ts +99 -0
  6. package/board/client/board-panel-styles.ts +223 -0
  7. package/board/client/board-panel.tsx +77 -1275
  8. package/board/client/engine-life.ts +48 -0
  9. package/board/client/final-review-stack.tsx +144 -0
  10. package/board/client/heartbeat.tsx +61 -0
  11. package/board/client/helper-lines.tsx +69 -0
  12. package/board/client/live-feed.tsx +86 -0
  13. package/board/client/live-line.ts +73 -0
  14. package/board/client/model-line.tsx +178 -0
  15. package/board/client/needs-you-section.tsx +133 -0
  16. package/board/client/one-line.tsx +193 -0
  17. package/board/client/repo-wait-banner.tsx +69 -0
  18. package/board/client/run-footer.tsx +68 -0
  19. package/board/client/run-header.tsx +189 -0
  20. package/board/client/run-picker.tsx +98 -0
  21. package/board/client/status-bar.tsx +131 -0
  22. package/board/client/step-dots.tsx +127 -0
  23. package/board/client/stuck-actions.tsx +269 -0
  24. package/board/client/ticket-card-view.tsx +238 -0
  25. package/board/client/ticket-detail.tsx +87 -0
  26. package/board/client/ticket-stack.tsx +119 -0
  27. package/board/client/ticket-wait.ts +188 -0
  28. package/board/client/timeline-rows.tsx +41 -14
  29. package/board/client/usage-line.tsx +51 -0
  30. package/board/client/wait-line.tsx +39 -0
  31. package/board/index.client.tsx +1 -1
  32. package/board/index.server.ts +7 -1
  33. package/board/package.json +1 -1
  34. package/board/server/board-panel-state-helpers.ts +67 -0
  35. package/board/server/board-server-check.ts +294 -0
  36. package/board/server/board-server-engine.ts +129 -0
  37. package/board/server/board-server-events.ts +134 -0
  38. package/board/server/board-server-read.ts +190 -0
  39. package/board/server/board-server-resume.ts +164 -0
  40. package/board/server/board-server-start.ts +174 -0
  41. package/board/server/board-server-store.ts +281 -0
  42. package/board/server/board-server-types.ts +114 -0
  43. package/board/server/board-server-util.ts +4 -0
  44. package/board/server/board-server.ts +52 -943
  45. package/board/server/board-vocabulary-engine.ts +144 -0
  46. package/board/server/board-vocabulary-review.ts +115 -0
  47. package/board/server/board-vocabulary-run.ts +145 -0
  48. package/board/server/board-vocabulary-shapes.ts +153 -0
  49. package/board/server/board-vocabulary.ts +20 -375
  50. package/board/server/current-step-panel-helpers.ts +108 -0
  51. package/board/server/describe-record.ts +403 -0
  52. package/board/server/engine-launch.ts +20 -3
  53. package/board/server/engine-restart-helpers.ts +105 -0
  54. package/board/server/engine-watch.ts +45 -5
  55. package/board/server/journal-rebuild-helpers.ts +169 -0
  56. package/board/server/live-file.ts +130 -0
  57. package/board/server/luca-version.ts +1 -1
  58. package/board/server/make-rows.ts +14 -629
  59. package/board/server/record-texts.ts +350 -0
  60. package/board/server/reduce-board-agent-start.ts +224 -0
  61. package/board/server/reduce-board-agents.ts +160 -0
  62. package/board/server/reduce-board-counters.ts +138 -0
  63. package/board/server/reduce-board-create.ts +148 -0
  64. package/board/server/reduce-board-final-review.ts +174 -0
  65. package/board/server/reduce-board-needs-you.ts +283 -0
  66. package/board/server/reduce-board-reasons.ts +59 -0
  67. package/board/server/reduce-board-run.ts +255 -0
  68. package/board/server/reduce-board-settle.ts +88 -0
  69. package/board/server/reduce-board-steps.ts +226 -0
  70. package/board/server/reduce-board-text.ts +139 -0
  71. package/board/server/reduce-board-ticket-checks.ts +168 -0
  72. package/board/server/reduce-board-ticket-join.ts +144 -0
  73. package/board/server/reduce-board-ticket-start.ts +132 -0
  74. package/board/server/reduce-board-update.ts +117 -0
  75. package/board/server/reduce-board-usage.ts +247 -0
  76. package/board/server/reduce-board.ts +100 -1917
  77. package/board/server/reply-post.ts +407 -0
  78. package/board/server/run-registry.ts +8 -4
  79. package/board/server/ticket-stages-panel-helpers.ts +296 -0
  80. package/board/shared/board-rows.ts +9 -0
  81. package/board/shared/board-rpc.ts +42 -0
  82. package/board/shared/board-state-counts.ts +77 -0
  83. package/board/shared/board-state-live.ts +99 -0
  84. package/board/shared/board-state-review.ts +59 -0
  85. package/board/shared/board-state-run.ts +236 -0
  86. package/board/shared/board-state-ticket.ts +257 -0
  87. package/board/shared/board-state-usage.ts +107 -0
  88. package/board/shared/board-state.ts +97 -567
  89. package/board/shared/reply-actions.ts +207 -0
  90. package/engine/agents/agent-launcher.ts +27 -4
  91. package/engine/agents/claude-launcher-helpers.ts +233 -0
  92. package/engine/agents/claude-launcher.ts +28 -669
  93. package/engine/agents/claude-messages.ts +212 -0
  94. package/engine/agents/claude-options.ts +43 -8
  95. package/engine/agents/common-instructions.ts +138 -0
  96. package/engine/agents/input-channel.ts +36 -0
  97. package/engine/agents/launch-paths.ts +29 -0
  98. package/engine/agents/launch-session.ts +227 -0
  99. package/engine/agents/learner-instructions.ts +31 -0
  100. package/engine/agents/open-session.ts +210 -0
  101. package/engine/agents/prompt-sections.ts +95 -0
  102. package/engine/agents/rejoin-prompts.ts +135 -0
  103. package/engine/agents/reviewer-instructions.ts +99 -0
  104. package/engine/agents/role-instructions.ts +31 -244
  105. package/engine/agents/role-prompts.ts +39 -109
  106. package/engine/agents/role-results.ts +106 -7
  107. package/engine/agents/scripted-launcher.ts +21 -6
  108. package/engine/agents/session-pool.ts +124 -0
  109. package/engine/agents/turn-progress.ts +283 -0
  110. package/engine/agents/writer-instructions.ts +151 -0
  111. package/engine/cli/computer-adapters-real.ts +15 -3
  112. package/engine/cli/computer-adapters.ts +2 -0
  113. package/engine/cli/computer-checks.ts +87 -4
  114. package/engine/cli/config-reload-text.ts +39 -0
  115. package/engine/cli/decision-model-setup.ts +70 -0
  116. package/engine/cli/doctor-command.ts +1 -0
  117. package/engine/cli/doctor.ts +21 -3
  118. package/engine/cli/going-runs.ts +192 -36
  119. package/engine/cli/init-command.ts +1 -0
  120. package/engine/cli/init.ts +23 -1
  121. package/engine/cli/live-runs.ts +73 -0
  122. package/engine/cli/luca-run.ts +42 -7
  123. package/engine/cli/luca-skills.ts +183 -0
  124. package/engine/cli/luca.ts +8 -1
  125. package/engine/cli/models-command.ts +119 -0
  126. package/engine/cli/read-run.ts +81 -0
  127. package/engine/cli/run-args.ts +33 -6
  128. package/engine/cli/run-demo.ts +143 -0
  129. package/engine/cli/run-end.ts +184 -0
  130. package/engine/cli/run-modes-helpers.ts +81 -0
  131. package/engine/cli/run-modes.ts +62 -439
  132. package/engine/cli/setup.ts +5 -48
  133. package/engine/cli/unfinished-runs.ts +122 -0
  134. package/engine/cli/upgrade-command.ts +212 -30
  135. package/engine/cli/upgrade-finish.ts +149 -0
  136. package/engine/cli/upgrade.ts +112 -71
  137. package/engine/config/config-fields.ts +127 -0
  138. package/engine/config/config-reload.ts +250 -0
  139. package/engine/config/engine-config-schema.ts +235 -0
  140. package/engine/config/engine-config.ts +72 -142
  141. package/engine/config/test-commands.ts +102 -0
  142. package/engine/core/agent-guards-helpers.ts +84 -0
  143. package/engine/core/agent-turn.ts +395 -0
  144. package/engine/core/already-done.ts +44 -1
  145. package/engine/core/build-action.ts +214 -0
  146. package/engine/core/build-context.ts +135 -0
  147. package/engine/core/build-launch.ts +259 -0
  148. package/engine/core/commit-in.ts +282 -0
  149. package/engine/core/decide-build-early.ts +108 -0
  150. package/engine/core/decide-build-ending.ts +145 -0
  151. package/engine/core/decide-build-helpers.ts +87 -0
  152. package/engine/core/decide-build-tickets.ts +207 -0
  153. package/engine/core/decide-build-view.ts +103 -0
  154. package/engine/core/decide-build.ts +101 -1368
  155. package/engine/core/decide-final-fix.ts +203 -0
  156. package/engine/core/decide-final-review.ts +55 -310
  157. package/engine/core/decide-red-green.ts +398 -0
  158. package/engine/core/decide-rejoin.ts +259 -0
  159. package/engine/core/decide-repo-wait.ts +129 -0
  160. package/engine/core/decide-run-budget.ts +86 -12
  161. package/engine/core/decide-stuck-helpers.ts +127 -0
  162. package/engine/core/decide-stuck.ts +60 -8
  163. package/engine/core/decide-ticket-review.ts +95 -0
  164. package/engine/core/decide-ticket.ts +267 -0
  165. package/engine/core/decide-waits.ts +47 -0
  166. package/engine/core/decide.ts +74 -23
  167. package/engine/core/engine-chores.ts +163 -0
  168. package/engine/core/environment-rerun.ts +83 -0
  169. package/engine/core/evidence-text.ts +84 -0
  170. package/engine/core/execute-action.ts +303 -0
  171. package/engine/core/execute-already-done-check.ts +133 -0
  172. package/engine/core/execute-build.ts +68 -944
  173. package/engine/core/execute-changeset.ts +5 -7
  174. package/engine/core/execute-final-review.ts +125 -13
  175. package/engine/core/execute-gates.ts +182 -0
  176. package/engine/core/execute-intake.ts +130 -0
  177. package/engine/core/execute-join.ts +276 -0
  178. package/engine/core/execute-memory.ts +12 -0
  179. package/engine/core/execute-pull-request.ts +84 -0
  180. package/engine/core/execute-red-check.ts +219 -0
  181. package/engine/core/execute-repo-wait.ts +129 -0
  182. package/engine/core/execute-stuck.ts +25 -10
  183. package/engine/core/execute-waits.ts +158 -0
  184. package/engine/core/execute-with-jev.ts +118 -0
  185. package/engine/core/execute.ts +124 -831
  186. package/engine/core/final-failure-cap.ts +38 -0
  187. package/engine/core/final-review-action.ts +125 -0
  188. package/engine/core/final-review-text.ts +63 -17
  189. package/engine/core/final-stuck-text.ts +86 -0
  190. package/engine/core/fix-loop-text.ts +49 -2
  191. package/engine/core/left-out.ts +120 -0
  192. package/engine/core/live-file.ts +372 -0
  193. package/engine/core/loop-caps.ts +25 -2
  194. package/engine/core/not-in-run.ts +7 -3
  195. package/engine/core/pull-request-text.ts +255 -6
  196. package/engine/core/repo-wait-text.ts +81 -0
  197. package/engine/core/review-text.ts +55 -1
  198. package/engine/core/run-crash-recovery-helpers.ts +16 -0
  199. package/engine/core/run-session-close-helpers.ts +96 -0
  200. package/engine/core/run-start.ts +148 -0
  201. package/engine/core/run-stuck-text.ts +86 -0
  202. package/engine/core/schedule-rules.ts +193 -0
  203. package/engine/core/stuck-detail-text.ts +89 -0
  204. package/engine/core/stuck-format.ts +22 -0
  205. package/engine/core/stuck-text.ts +29 -332
  206. package/engine/core/ticket-stuck-text.ts +192 -0
  207. package/engine/core/worktree-changes.ts +106 -0
  208. package/engine/gates/already-done-check.ts +120 -0
  209. package/engine/gates/feature-map.ts +96 -0
  210. package/engine/gates/gate-runner.ts +108 -108
  211. package/engine/gates/gate-schemas.ts +61 -2
  212. package/engine/gates/leftover-scan.ts +154 -31
  213. package/engine/gates/new-tests.ts +45 -0
  214. package/engine/gates/own-files-check.ts +98 -0
  215. package/engine/gates/prepare-check.ts +60 -0
  216. package/engine/gates/prepare-slots.ts +99 -0
  217. package/engine/gates/red-check.ts +184 -16
  218. package/engine/gates/shell-check.ts +64 -0
  219. package/engine/gates/slot-waits.ts +55 -0
  220. package/engine/gates/slow-test-runner.ts +59 -0
  221. package/engine/gates/test-checks.ts +161 -0
  222. package/engine/gates/test-runner.ts +164 -5
  223. package/engine/gates/test-slots.ts +48 -0
  224. package/engine/git/git-adapter.ts +55 -396
  225. package/engine/git/git-commits.ts +132 -0
  226. package/engine/git/git-join.ts +139 -0
  227. package/engine/git/git-read.ts +123 -0
  228. package/engine/git/git-run.ts +32 -0
  229. package/engine/git/git-worktrees.ts +143 -0
  230. package/engine/git/outside-links.ts +55 -0
  231. package/engine/guards/after-turn-check.ts +31 -5
  232. package/engine/guards/guard-hook.ts +60 -12
  233. package/engine/guards/guard-roles.ts +68 -0
  234. package/engine/guards/helper-rules.ts +92 -0
  235. package/engine/guards/permission-rules.ts +161 -0
  236. package/engine/guards/role-rules-helpers.ts +48 -0
  237. package/engine/guards/role-rules.ts +34 -606
  238. package/engine/guards/shell-rules.ts +344 -0
  239. package/engine/guards/worktree-git.ts +208 -0
  240. package/engine/guards/worktree-snapshot.ts +412 -0
  241. package/engine/guards/worktree-state.ts +28 -825
  242. package/engine/guards/worktree-undo.ts +260 -0
  243. package/engine/guards/write-rules.ts +149 -0
  244. package/engine/index.ts +30 -9
  245. package/engine/intake/criteria-checks.ts +114 -0
  246. package/engine/intake/intake-checks.ts +164 -30
  247. package/engine/intake/intake-schemas.ts +37 -2
  248. package/engine/jev/clef-client.ts +259 -0
  249. package/engine/jev/decision-model-credentials.ts +212 -0
  250. package/engine/jev/jev-jobs.ts +144 -60
  251. package/engine/jev/jev-schemas.ts +86 -5
  252. package/engine/jev/jev-shadow-helpers.ts +218 -0
  253. package/engine/jev/jev-shadow.ts +109 -64
  254. package/engine/jev/model-pick.ts +275 -0
  255. package/engine/journal/agent-entries.ts +167 -0
  256. package/engine/journal/agent-message-entries.ts +72 -0
  257. package/engine/journal/agent-turn-replay.ts +203 -0
  258. package/engine/journal/already-done-entries.ts +55 -0
  259. package/engine/journal/commit-entries.ts +56 -0
  260. package/engine/journal/cross-ticket-replay.ts +222 -0
  261. package/engine/journal/decision-model-entries.ts +86 -0
  262. package/engine/journal/entry-fields.ts +10 -0
  263. package/engine/journal/final-review-entries.ts +128 -0
  264. package/engine/journal/final-review-replay.ts +324 -0
  265. package/engine/journal/final-review-round.ts +320 -0
  266. package/engine/journal/final-review-state.ts +180 -0
  267. package/engine/journal/gate-entries.ts +81 -0
  268. package/engine/journal/intake-entries.ts +87 -0
  269. package/engine/journal/join-entries.ts +126 -0
  270. package/engine/journal/journal-entry-schema.ts +218 -0
  271. package/engine/journal/journal-kinds.ts +97 -0
  272. package/engine/journal/journal-record-schema.ts +218 -0
  273. package/engine/journal/journal-record.ts +43 -1400
  274. package/engine/journal/journal.ts +6 -1
  275. package/engine/journal/limit-entries.ts +90 -0
  276. package/engine/journal/memory-entries.ts +124 -0
  277. package/engine/journal/memory-replay.ts +157 -0
  278. package/engine/journal/plan-replay.ts +139 -0
  279. package/engine/journal/pull-request-entries.ts +41 -0
  280. package/engine/journal/replay.ts +157 -2144
  281. package/engine/journal/reply-replay.ts +144 -0
  282. package/engine/journal/repo-wait-entries.ts +53 -0
  283. package/engine/journal/run-entries.ts +172 -0
  284. package/engine/journal/run-state.ts +264 -0
  285. package/engine/journal/shared-failure-replay.ts +133 -0
  286. package/engine/journal/stuck-entries.ts +193 -0
  287. package/engine/journal/ticket-progress.ts +392 -0
  288. package/engine/journal/ticket-replay.ts +411 -0
  289. package/engine/journal/ticket-result-replay.ts +364 -0
  290. package/engine/journal/ticket-retry-replay.ts +142 -0
  291. package/engine/journal/worktree-entries.ts +122 -0
  292. package/engine/models/model-score.ts +512 -0
  293. package/engine/shell/run-command.ts +187 -18
  294. package/engine/testing/agent-turn-fixtures.ts +287 -0
  295. package/engine/testing/already-done-fixtures.ts +97 -0
  296. package/engine/testing/build-fixtures.ts +68 -973
  297. package/engine/testing/checkouts-fixtures.ts +48 -0
  298. package/engine/testing/final-review-fixtures.ts +62 -1
  299. package/engine/testing/final-review-steps.ts +143 -0
  300. package/engine/testing/limit-fixtures.ts +85 -0
  301. package/engine/testing/live-file-fixtures.ts +57 -0
  302. package/engine/testing/practice-repo.ts +10 -4
  303. package/engine/testing/practice-run.ts +1 -1
  304. package/engine/testing/reply-fixtures.ts +88 -0
  305. package/engine/testing/run-budget-fixtures.ts +165 -0
  306. package/engine/testing/run-fixtures.ts +219 -0
  307. package/engine/testing/sdk-play.ts +177 -0
  308. package/engine/testing/setup-repo.ts +194 -0
  309. package/engine/testing/step-record-fixtures.ts +55 -0
  310. package/engine/testing/ticket-step-fixtures.ts +272 -0
  311. package/engine/testing/wait-scenarios.ts +186 -0
  312. package/engine/tracker/gh-retry.ts +217 -0
  313. package/engine/tracker/github-tracker.ts +310 -90
  314. package/engine/tracker/in-memory-tracker.ts +8 -4
  315. package/engine/tracker/tracker.ts +131 -6
  316. package/package.json +2 -1
  317. package/skills/luca-retro/SKILL.md +74 -0
  318. package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
  319. package/skills/luca-unstick/SKILL.md +153 -0
  320. package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
  321. package/engine/jev/jev-client.ts +0 -173
@@ -1,164 +1,37 @@
1
- import { randomBytes, randomInt, timingSafeEqual } from 'node:crypto'
2
- import { join, resolve } from 'node:path'
3
-
4
- import { z } from 'zod'
5
-
6
- import { readRecord } from './board-vocabulary'
7
- import {
8
- engineArgs,
9
- mintRunId,
10
- parseRunArgs,
11
- resolveEngine,
12
- } from './engine-launch'
13
- import {
14
- engineStoppedText,
15
- liveRunIds,
16
- MAX_AUTO_RESTARTS,
17
- parseUnfinished,
18
- restartRow,
19
- restartText,
20
- resumeArgs,
21
- UNFINISHED_TIMEOUT_MS,
22
- type StopWhy,
23
- type UnfinishedRun,
24
- } from './engine-watch'
25
- import { describeRecord, headerRow, rowsForRecord } from './make-rows'
26
- import { applyEnded, applyRecord, createBoardState } from './reduce-board'
27
- import { listJournals, readJournal } from './run-journals'
28
- import { createRunRegistry, type RunEntry } from './run-registry'
29
-
30
- import type { BoardRow } from '../shared/board-rows'
31
- import type {
32
- BoardReadInput,
33
- BoardReadOutput,
34
- EngineEventInput,
35
- EngineEventOutput,
36
- EngineRecord,
37
- RunStartInput,
38
- RunStartOutput,
39
- } from '../shared/board-rpc'
40
- import type { BoardState, EngineEnded } from '../shared/board-state'
41
- import {
42
- EngineSettingsSchema,
43
- type EngineSettings,
44
- } from '../shared/engine-settings'
45
-
46
- /** What the server asks the host to spawn: detached, output to a log file. */
47
- export type SpawnRequest = {
48
- command: string
49
- args: string[]
50
- env: Record<string, string>
51
- cwd: string
52
- log_path: string
53
- /** Called if the process fails to start after `spawn_engine` returned. */
54
- on_error: (error: unknown) => void
55
- }
56
-
57
- export type SpawnEngine = (request: SpawnRequest) => { pid: number | null }
58
-
59
- /** A short command the server runs and waits for, such as `luca-run --unfinished`. */
60
- export type CommandRequest = {
61
- command: string
62
- args: string[]
63
- cwd: string
64
- env: Record<string, string>
65
- timeout_ms: number
66
- }
67
-
68
- /** How a command ended: `exit_code` is `null` when it was killed or timed out. */
69
- export type CommandResult = {
70
- exit_code: number | null
71
- stdout: string
72
- stderr: string
73
- }
74
-
75
- /** Runs a command to its end. Never throws: a failure is in the result. */
76
- export type RunCommand = (request: CommandRequest) => Promise<CommandResult>
77
-
78
- /** Every running process's full command line. */
79
- export type ListProcesses = () => Promise<string[]>
80
-
81
- /** What one engine check did, by run id. */
82
- export type EngineCheck = { restarted: string[]; stopped: string[] }
83
-
84
- /** The run phases after which no engine is needed. */
85
- const OVER_PHASES = new Set(['done', 'refused', 'nothing_to_do'])
86
-
87
- /** Appends (or replaces, by id) one row in a chat's timeline. */
88
- export type AppendRow = ({
89
- agent_id,
90
- row,
91
- }: {
92
- agent_id: string
93
- row: BoardRow
94
- }) => Promise<void>
95
-
96
- /** After this many failed appends in a row, a run stops sending rows. */
97
- const APPEND_FAILURES_BEFORE_STOP = 3
98
-
99
- /** The longest `run.start` waits for the header row before it returns. */
100
- const HEADER_WAIT_MS = 3000
101
-
102
- const SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789'
103
-
104
- const randomSuffix = (): string =>
105
- Array.from(
106
- { length: 4 },
107
- () => SUFFIX_ALPHABET[randomInt(SUFFIX_ALPHABET.length)] ?? 'x'
108
- ).join('')
109
-
110
- const errorText = ({ error }: { error: unknown }): string =>
111
- error instanceof Error ? error.message : String(error)
112
-
113
- const sameToken = ({ left, right }: { left: string; right: string }) => {
114
- const a = Buffer.from(left)
115
- const b = Buffer.from(right)
116
- return a.length === b.length && timingSafeEqual(a, b)
117
- }
118
-
119
- /** A run's board, built from its journal records so far. */
120
- type Replay = {
121
- state: BoardState
122
- /** The next journal seq the board wants (starts at 1). */
123
- next_seq: number
124
- }
125
-
126
- /** One run the plugin started: its registry entry and its board in memory. */
127
- type RunMemory = Replay & {
128
- entry: RunEntry
129
- append_failures: number
130
- rows_stopped: boolean
131
- /**
132
- * The rows of records rebuilt from disk, by seq, that the chat may not
133
- * have: the engine may have written them while the plugin was down.
134
- * Added when the engine sends those records again.
135
- */
136
- unsent_rows: Map<number, BoardRow[]>
137
- }
138
-
139
1
  /**
140
- * A run the plugin didn't start (such as one from the command line), read
141
- * from its journal on disk. Read-only: no chat rows, no engine events, no
142
- * restarts. `size` and `changed_ms` are its journal's when last read.
143
- * `repo` is the repo its `run_started` names, `null` when it names none.
2
+ * The board plugin's server logic. `createBoardServer` wires the pieces,
3
+ * which are in modules by responsibility:
4
+ * - `board-server-types.ts`: the types, and what the server is given.
5
+ * - `board-server-store.ts`: the runs in memory, their queues, and the rows.
6
+ * - `board-server-engine.ts`: running the engine (find it, spawn it).
7
+ * - `board-server-start.ts` and `board-server-resume.ts`: `run.start` and
8
+ * `/luca-run resume`.
9
+ * - `board-server-events.ts`: `engine.event`.
10
+ * - `board-server-read.ts`: `board.read` and `reply.post`, and the runs the
11
+ * plugin didn't start.
12
+ * - `board-server-check.ts`: the engine check, which also labels the runs
13
+ * the plugin didn't start whose engine is gone.
144
14
  */
145
- type OutsideRun = Replay & {
146
- size: number
147
- changed_ms: number
148
- repo: string | null
149
- }
150
-
151
- const RunRepoSchema = z.object({ repo: z.string().min(1) })
152
-
153
- /** The repo a run's `run_started` record names, if any. */
154
- const repoOf = ({ records }: { records: EngineRecord[] }): string | null => {
155
- const started = records.find((record) => record.kind === 'run_started')
156
- const parsed = RunRepoSchema.safeParse(started?.content)
157
- return parsed.success ? resolve(parsed.data.repo) : null
158
- }
159
-
160
- /** How many runs the plugin didn't start it shows, the latest changed first. */
161
- const OUTSIDE_RUNS_SHOWN = 50
15
+ import { createEngineChecks } from './board-server-check'
16
+ import { createEngineProcess } from './board-server-engine'
17
+ import { createEngineEvents } from './board-server-events'
18
+ import { createBoardReads } from './board-server-read'
19
+ import { createResumer } from './board-server-resume'
20
+ import { createStarter } from './board-server-start'
21
+ import { createRunStore } from './board-server-store'
22
+ import type { BoardServerConfig } from './board-server-types'
23
+
24
+ export type {
25
+ AppendRow,
26
+ BoardServerConfig,
27
+ CommandRequest,
28
+ CommandResult,
29
+ EngineCheck,
30
+ ListProcesses,
31
+ RunCommand,
32
+ SpawnEngine,
33
+ SpawnRequest,
34
+ } from './board-server-types'
162
35
 
163
36
  /**
164
37
  * The board plugin's server logic, with every side effect injected so tests
@@ -194,795 +67,31 @@ const OUTSIDE_RUNS_SHOWN = 50
194
67
  * })
195
68
  * server.handle(runStartRpc, (input) => board.startRun(input))
196
69
  */
197
- export const createBoardServer = ({
198
- registry_path,
199
- runs_dir = null,
200
- append_row,
201
- spawn_engine,
202
- list_processes,
203
- run_command,
204
- read_settings,
205
- file_exists,
206
- real_path,
207
- home_dir,
208
- env,
209
- log_dir,
210
- now,
211
- log,
212
- }: {
213
- registry_path: string
214
- /** The engine's runs folder (`<runs_dir>/<run_id>/journal.jsonl`). */
215
- runs_dir?: string | null
216
- append_row: AppendRow
217
- spawn_engine: SpawnEngine
218
- list_processes: ListProcesses
219
- run_command: RunCommand
220
- read_settings: () => Promise<EngineSettings>
221
- file_exists: ({ path }: { path: string }) => boolean
222
- /** Follows an installed `luca-run`'s symlink; the path as is without it. */
223
- real_path?: ({ path }: { path: string }) => string
224
- home_dir: string
225
- env: Record<string, string | undefined>
226
- log_dir: string
227
- now: () => Date
228
- log: (message: string) => void
229
- }) => {
230
- const registry = createRunRegistry({ path: registry_path, log })
231
- const runs = new Map<string, RunMemory>()
232
- const outside = new Map<string, OutsideRun>()
233
- const queues = new Map<string, Promise<unknown>>()
234
-
235
- /** Runs `work` after everything already queued for the same run. */
236
- const enqueue = <Result>({
237
- run_id,
238
- work,
239
- }: {
240
- run_id: string
241
- work: () => Promise<Result>
242
- }): Promise<Result> => {
243
- const result = (queues.get(run_id) ?? Promise.resolve()).then(work)
244
- queues.set(
245
- run_id,
246
- result.catch(() => undefined)
247
- )
248
- return result
249
- }
250
-
251
- const appendRows = async ({
252
- memory,
253
- rows,
254
- }: {
255
- memory: RunMemory
256
- rows: BoardRow[]
257
- }) => {
258
- for (const row of rows) {
259
- if (memory.rows_stopped) return
260
- try {
261
- await append_row({ agent_id: memory.entry.agent_id, row })
262
- memory.append_failures = 0
263
- } catch (error) {
264
- memory.append_failures += 1
265
- log(
266
- `[${memory.entry.run_id}] couldn't add row ${row.id} to chat ${memory.entry.agent_id}: ${errorText({ error })}`
267
- )
268
- if (memory.append_failures >= APPEND_FAILURES_BEFORE_STOP) {
269
- memory.rows_stopped = true
270
- log(
271
- `[${memory.entry.run_id}] stopped adding rows: chat ${memory.entry.agent_id} seems gone. The side panel still updates.`
272
- )
273
- }
274
- }
275
- }
276
- }
277
-
278
- const header = ({ memory }: { memory: RunMemory }): BoardRow =>
279
- headerRow({ state: memory.state, updated_at: now().toISOString() })
280
-
281
- /** Applies one record to a run's board; returns the rows it makes. */
282
- const applyOne = ({
283
- memory,
284
- record,
285
- }: {
286
- memory: Replay
287
- record: EngineRecord
288
- }): BoardRow[] => {
289
- const { run_id } = memory.state.run
290
- const read = readRecord({ record })
291
- if (read.status !== 'known') {
292
- if (read.status === 'bad_content') {
293
- log(
294
- `[${run_id}] skipped record ${record.seq} (${record.kind}): its content doesn't fit.\n${read.error}`
295
- )
296
- }
297
- memory.state = {
298
- ...memory.state,
299
- event_count: memory.state.event_count + 1,
300
- }
301
- return []
302
- }
303
- const before = memory.state
304
- const after = applyRecord({ state: before, record: read.record })
305
- const described = describeRecord({
306
- before,
307
- after,
308
- record: read.record,
309
- })
310
- memory.state = described ? { ...after, latest: described.text } : after
311
- return rowsForRecord({ run_id, before, after, record: read.record })
312
- }
313
-
314
- /**
315
- * Applies records in seq order from the run's `next_seq`. Lower seqs are
316
- * duplicates and skipped; a gap stops there (`gap` is the seq found).
317
- */
318
- const applyInOrder = ({
319
- memory,
320
- records,
321
- }: {
322
- memory: Replay
323
- records: EngineRecord[]
324
- }): {
325
- rows: Map<number, BoardRow[]>
326
- applied: number
327
- gap: number | null
328
- } => {
329
- const rows = new Map<number, BoardRow[]>()
330
- let applied = 0
331
- for (const record of records.toSorted(
332
- (left, right) => left.seq - right.seq
333
- )) {
334
- if (record.seq < memory.next_seq) continue
335
- if (record.seq > memory.next_seq) {
336
- return { rows, applied, gap: record.seq }
337
- }
338
- rows.set(record.seq, applyOne({ memory, record }))
339
- memory.next_seq += 1
340
- applied += 1
341
- }
342
- return { rows, applied, gap: null }
343
- }
344
-
345
- /**
346
- * Knows a run this plugin started, rebuilt from its journal `records`.
347
- * Their rows aren't added now; while the engine may still send, they are
348
- * kept until it sends those records again (see `handleEngineEvent`).
349
- */
350
- const remember = ({
351
- entry,
352
- records,
353
- }: {
354
- entry: RunEntry
355
- records: EngineRecord[]
356
- }): RunMemory => {
357
- const memory: RunMemory = {
358
- entry,
359
- state: createBoardState({
360
- run_id: entry.run_id,
361
- spec_number: entry.spec,
362
- demo: entry.demo,
363
- started_at: entry.started_at,
364
- log_path: entry.log_path,
365
- }),
366
- next_seq: 1,
367
- append_failures: 0,
368
- rows_stopped: false,
369
- unsent_rows: new Map(),
370
- }
371
- const { rows } = applyInOrder({ memory, records })
372
- // A run whose engine ended stays ended after a plugin restart.
373
- if (entry.ended) {
374
- memory.state = applyEnded({
375
- state: memory.state,
376
- ended: entry.ended,
377
- })
378
- } else {
379
- memory.unsent_rows = rows
380
- }
381
- runs.set(entry.run_id, memory)
382
- return memory
383
- }
384
-
385
- /** A run's journal records on disk; none without a runs folder. */
386
- const journalOf = ({ run_id }: { run_id: string }) =>
387
- runs_dir === null
388
- ? Promise.resolve([])
389
- : readJournal({ runs_dir, run_id, log })
390
-
391
- /** Rebuilds every run in the registry from its journal. */
392
- const load = async () => {
393
- const entries = registry.list()
394
- const journals = await Promise.all(
395
- entries.map(({ run_id }) => journalOf({ run_id }))
396
- )
397
- entries.forEach((entry, index) =>
398
- remember({ entry, records: journals[index] ?? [] })
399
- )
400
- }
401
-
402
- /**
403
- * Reads the journals of runs this plugin didn't start, the latest
404
- * changed first: a new or changed journal is read again and its new
405
- * records applied.
406
- */
407
- const readOutside = async () => {
408
- if (runs_dir === null) return
409
- const journals = (await listJournals({ runs_dir }))
410
- .filter((journal) => !runs.has(journal.run_id))
411
- .slice(0, OUTSIDE_RUNS_SHOWN)
412
- const shown = new Set(journals.map((journal) => journal.run_id))
413
- for (const run_id of outside.keys()) {
414
- if (!shown.has(run_id)) outside.delete(run_id)
415
- }
416
- await Promise.all(
417
- journals.map(async ({ run_id, size, changed_ms }) => {
418
- const known = outside.get(run_id)
419
- if (known?.size === size && known.changed_ms === changed_ms) {
420
- return
421
- }
422
- const records = await journalOf({ run_id })
423
- const first = records[0]
424
- if (!first) return
425
- const run: OutsideRun = known ?? {
426
- state: createBoardState({
427
- run_id,
428
- spec_number: null,
429
- demo: false,
430
- started_at: first.time,
431
- log_path: null,
432
- }),
433
- next_seq: 1,
434
- size,
435
- changed_ms,
436
- repo: null,
437
- }
438
- applyInOrder({ memory: run, records })
439
- run.size = size
440
- run.changed_ms = changed_ms
441
- run.repo ??= repoOf({ records })
442
- outside.set(run_id, run)
443
- })
444
- )
445
- }
446
-
447
- /** Changes a run's registry entry, and its copy in memory. */
448
- const updateEntry = ({
449
- memory,
450
- change,
451
- }: {
452
- memory: RunMemory
453
- change: Partial<Pick<RunEntry, 'ended' | 'restarts'>>
454
- }) => {
455
- memory.entry = registry.update({
456
- run_id: memory.entry.run_id,
457
- change,
458
- }) ?? { ...memory.entry, ...change }
459
- }
460
-
461
- /** Marks a run's engine as ended (kept in the registry) and updates its header. */
462
- const endRun = async ({
463
- memory,
464
- ended,
465
- }: {
466
- memory: RunMemory
467
- ended: EngineEnded
468
- }) => {
469
- memory.state = applyEnded({ state: memory.state, ended })
470
- memory.unsent_rows.clear()
471
- updateEntry({ memory, change: { ended } })
472
- await appendRows({ memory, rows: [header({ memory })] })
473
- }
474
-
475
- /** The daemon's env for the engine, plus the run's token when given. */
476
- const childEnv = ({
477
- token,
478
- }: {
479
- token: string | null
480
- }): Record<string, string> => {
481
- const child_env: Record<string, string> = {}
482
- for (const [key, value] of Object.entries(env)) {
483
- if (value !== undefined) child_env[key] = value
484
- }
485
- if (token !== null) child_env.LUCA_BOARD_TOKEN = token
486
- return child_env
487
- }
488
-
489
- /** Finds the engine from the current settings, as `run.start` does. */
490
- const findEngine = async () => {
491
- let settings: EngineSettings = EngineSettingsSchema.parse({})
492
- try {
493
- settings = await read_settings()
494
- } catch (error) {
495
- log(`Couldn't read the engine settings: ${errorText({ error })}`)
496
- }
497
- return resolveEngine({
498
- settings,
499
- env,
500
- home_dir,
501
- file_exists,
502
- real_path,
503
- })
504
- }
505
-
506
- /**
507
- * `run.start`: parses the args, finds the engine, records the run, spawns
508
- * the engine detached, and adds the chat's header row. Returns at once.
509
- */
510
- const startRun = async (input: RunStartInput): Promise<RunStartOutput> => {
511
- await ready
512
- const parsed = parseRunArgs({ args: input.args })
513
- if (!parsed.ok)
514
- return { ok: false, message: parsed.message, run_id: null }
515
-
516
- const engine = await findEngine()
517
- if (!engine.ok)
518
- return { ok: false, message: engine.message, run_id: null }
519
-
520
- let run_id = mintRunId({ now: now(), suffix: randomSuffix() })
521
- while (runs.has(run_id)) {
522
- run_id = mintRunId({ now: now(), suffix: randomSuffix() })
523
- }
524
- const token = randomBytes(24).toString('hex')
525
- const log_path = join(log_dir, `${run_id}.log`)
526
- const { target } = parsed
527
- const entry: RunEntry = {
528
- run_id,
529
- token,
530
- agent_id: input.agent_id,
531
- workspace_id: input.workspace_id,
532
- repo: input.cwd,
533
- spec: target.kind === 'spec' ? target.spec : null,
534
- demo: target.kind === 'demo',
535
- started_at: now().toISOString(),
536
- log_path,
537
- ended: null,
538
- restarts: 0,
539
- }
540
- registry.add({ entry })
541
- const memory = remember({ entry, records: [] })
542
-
543
- const child_env = childEnv({ token })
544
-
545
- let pid: number | null = null
546
- try {
547
- pid = spawn_engine({
548
- command: engine.command,
549
- args: [
550
- ...engine.lead_args,
551
- ...engineArgs({ target, repo: input.cwd, run_id }),
552
- ],
553
- env: child_env,
554
- cwd: input.cwd,
555
- log_path,
556
- on_error: (error) => {
557
- log(
558
- `[${run_id}] the engine process failed: ${errorText({ error })}`
559
- )
560
- void enqueue({
561
- run_id,
562
- work: () =>
563
- endRun({
564
- memory,
565
- ended: {
566
- ok: false,
567
- message: `The engine couldn't start: ${errorText({ error })}`,
568
- },
569
- }),
570
- })
571
- },
572
- }).pid
573
- } catch (error) {
574
- registry.remove({ run_id })
575
- runs.delete(run_id)
576
- return {
577
- ok: false,
578
- message: `Couldn't start the engine: ${errorText({ error })}`,
579
- run_id: null,
580
- }
581
- }
582
- log(
583
- `[${run_id}] started ${engine.command} (pid ${pid ?? '?'}), log ${log_path}`
584
- )
585
-
586
- const headerAdded = enqueue({
587
- run_id,
588
- work: () => appendRows({ memory, rows: [header({ memory })] }),
589
- })
590
- let timer: ReturnType<typeof setTimeout> | undefined
591
- await Promise.race([
592
- headerAdded,
593
- new Promise((resolve) => {
594
- timer = setTimeout(resolve, HEADER_WAIT_MS)
595
- }),
596
- ])
597
- clearTimeout(timer)
598
-
599
- const what =
600
- target.kind === 'demo' ? 'a demo run' : `spec #${target.spec}`
601
- return {
602
- ok: true,
603
- run_id,
604
- message: `Started ${what} as ${run_id}. Its log is ${log_path}.`,
605
- }
606
- }
607
-
608
- /**
609
- * The kept rows of records rebuilt from disk that the engine sent again.
610
- * The engine sends from the first record it hasn't had an answer for, so
611
- * the chat already has the rows of every lower seq: those are dropped.
612
- */
613
- const takeUnsentRows = ({
614
- memory,
615
- records,
616
- }: {
617
- memory: RunMemory
618
- records: EngineRecord[]
619
- }): { rows: BoardRow[]; taken: number } => {
620
- const rows: BoardRow[] = []
621
- let taken = 0
622
- if (memory.unsent_rows.size === 0 || records.length === 0) {
623
- return { rows, taken }
624
- }
625
- const lowest = Math.min(...records.map((record) => record.seq))
626
- const sent = new Set(records.map((record) => record.seq))
627
- for (const [seq, seq_rows] of memory.unsent_rows) {
628
- if (seq < lowest) {
629
- memory.unsent_rows.delete(seq)
630
- } else if (sent.has(seq)) {
631
- rows.push(...seq_rows)
632
- memory.unsent_rows.delete(seq)
633
- taken += 1
634
- }
635
- }
636
- return { rows, taken }
637
- }
638
-
639
- /**
640
- * `engine.event`: applies a run's journal records in seq order from its
641
- * `next_seq`. Lower seqs are duplicates and skipped (but a record rebuilt
642
- * from disk adds its rows the first time the engine sends it); a gap
643
- * stops there and the reply's `next_seq` asks the engine to resend from it.
644
- */
645
- const handleEngineEvent = async (
646
- input: EngineEventInput
647
- ): Promise<EngineEventOutput> => {
648
- await ready
649
- // A run this plugin didn't start is read-only: it isn't in `runs`.
650
- const memory = runs.get(input.run_id)
651
- if (!memory) {
652
- return {
653
- ok: false,
654
- next_seq: 0,
655
- message: `The board doesn't know run ${input.run_id}.`,
656
- }
657
- }
658
- if (!sameToken({ left: input.token, right: memory.entry.token })) {
659
- return {
660
- ok: false,
661
- next_seq: 0,
662
- message: `Wrong token for run ${input.run_id}.`,
663
- }
664
- }
665
- return enqueue({
666
- run_id: input.run_id,
667
- work: async () => {
668
- const unsent = takeUnsentRows({
669
- memory,
670
- records: input.records,
671
- })
672
- const {
673
- rows: new_rows,
674
- applied,
675
- gap,
676
- } = applyInOrder({
677
- memory,
678
- records: input.records,
679
- })
680
- const rows = [...unsent.rows, ...[...new_rows.values()].flat()]
681
- if (gap === null && input.ended) {
682
- memory.state = applyEnded({
683
- state: memory.state,
684
- ended: input.ended,
685
- })
686
- memory.unsent_rows.clear()
687
- updateEntry({ memory, change: { ended: input.ended } })
688
- }
689
- if (
690
- unsent.taken > 0 ||
691
- applied > 0 ||
692
- (gap === null && input.ended)
693
- ) {
694
- rows.push(header({ memory }))
695
- }
696
- await appendRows({ memory, rows })
697
- const message =
698
- gap === null
699
- ? `Applied ${applied} records.`
700
- : `Applied ${applied} records, then found seq ${gap} while waiting for ${memory.next_seq}: resend from ${memory.next_seq}.`
701
- return { ok: true, next_seq: memory.next_seq, message }
702
- },
703
- })
704
- }
705
-
706
- /**
707
- * `board.read`: the runs started from the workspace's chats and the runs
708
- * this plugin didn't start whose repo is the workspace's folder, newest
709
- * first, and one of them in full.
710
- */
711
- const readBoard = async (
712
- input: BoardReadInput
713
- ): Promise<BoardReadOutput> => {
714
- await ready
715
- await readOutside()
716
- const directory = input.directory ? resolve(input.directory) : null
717
- const list = [
718
- ...[...runs.values()].filter(
719
- (memory) => memory.entry.workspace_id === input.workspace_id
720
- ),
721
- ...[...outside.values()].filter(
722
- (run) => directory !== null && run.repo === directory
723
- ),
724
- ]
725
- .map(({ state }) => state)
726
- .toSorted((left, right) =>
727
- right.run.started_at === left.run.started_at
728
- ? right.run.run_id.localeCompare(left.run.run_id)
729
- : right.run.started_at.localeCompare(left.run.started_at)
730
- )
731
- const selected =
732
- (input.run_id
733
- ? list.find((state) => state.run.run_id === input.run_id)
734
- : undefined) ?? list[0]
735
- return {
736
- runs: list.map((state) => ({
737
- run_id: state.run.run_id,
738
- spec_number: state.run.spec_number,
739
- spec_title: state.run.spec_title,
740
- demo: state.run.demo,
741
- status: state.run.status,
742
- started_at: state.run.started_at,
743
- needs_you: state.needs_you.length,
744
- })),
745
- selected: selected ?? null,
746
- }
747
- }
748
-
749
- /** Whether a run may still need an engine: not ended, not over. */
750
- const needsEngine = ({ memory }: { memory: RunMemory }) =>
751
- memory.state.run.engine_ended === null &&
752
- !OVER_PHASES.has(memory.state.run.phase)
753
-
754
- /**
755
- * Asks the engine which unfinished runs may be restarted
756
- * (`luca-run --unfinished`), with the engine to restart them with.
757
- */
758
- const listUnfinished = async (): Promise<
759
- | {
760
- ok: true
761
- runs: Map<string, UnfinishedRun>
762
- engine: { command: string; lead_args: string[] }
763
- }
764
- | { ok: false; error: string }
765
- > => {
766
- const engine = await findEngine()
767
- if (!engine.ok) return { ok: false, error: engine.message }
768
- const result = await run_command({
769
- command: engine.command,
770
- args: [...engine.lead_args, '--unfinished'],
771
- cwd: home_dir,
772
- env: childEnv({ token: null }),
773
- timeout_ms: UNFINISHED_TIMEOUT_MS,
774
- })
775
- if (result.exit_code === null) {
776
- return {
777
- ok: false,
778
- error: `luca-run --unfinished did not finish in ${UNFINISHED_TIMEOUT_MS / 1000} s (it timed out or was killed)`,
779
- }
780
- }
781
- if (result.exit_code !== 0) {
782
- return {
783
- ok: false,
784
- error: `luca-run --unfinished exited with ${result.exit_code}: ${result.stderr.trim().slice(0, 300)}`,
785
- }
786
- }
787
- const parsed = parseUnfinished({ stdout: result.stdout })
788
- if (!parsed.ok) {
789
- return {
790
- ok: false,
791
- error: `luca-run --unfinished ${parsed.error}`,
792
- }
793
- }
794
- return { ok: true, runs: parsed.runs, engine }
795
- }
796
-
797
- /**
798
- * Restarts a run from its journal (`--resume`) with its token, appending
799
- * to its log, and counts the restart. An error when the spawn threw.
800
- */
801
- const restartRun = async ({
802
- memory,
803
- engine,
804
- }: {
805
- memory: RunMemory
806
- engine: { command: string; lead_args: string[] }
807
- }): Promise<{ ok: true } | { ok: false; error: string }> => {
808
- const { run_id, repo, token, log_path } = memory.entry
809
- let pid: number | null = null
810
- try {
811
- pid = spawn_engine({
812
- command: engine.command,
813
- args: [...engine.lead_args, ...resumeArgs({ run_id, repo })],
814
- env: childEnv({ token }),
815
- cwd: repo,
816
- log_path,
817
- on_error: (error) => {
818
- log(
819
- `[${run_id}] the restarted engine failed: ${errorText({ error })}`
820
- )
821
- void enqueue({
822
- run_id,
823
- work: () =>
824
- endRun({
825
- memory,
826
- ended: {
827
- ok: false,
828
- message: engineStoppedText({
829
- why: {
830
- kind: 'spawn_failed',
831
- error: errorText({ error }),
832
- },
833
- run_id,
834
- log_path,
835
- }),
836
- },
837
- }),
838
- })
839
- },
840
- }).pid
841
- } catch (error) {
842
- return { ok: false, error: errorText({ error }) }
843
- }
844
- const restart = memory.entry.restarts + 1
845
- updateEntry({ memory, change: { restarts: restart } })
846
- const row = restartRow({ run_id, restart, time: now().toISOString() })
847
- memory.state = {
848
- ...memory.state,
849
- run: { ...memory.state.run, engine_ended: null },
850
- latest: restartText({ restart }),
851
- }
852
- log(
853
- `[${run_id}] restarted ${engine.command} with --resume (pid ${pid ?? '?'}, restart ${restart} of ${MAX_AUTO_RESTARTS}), log ${log_path}`
854
- )
855
- await appendRows({ memory, rows: [row, header({ memory })] })
856
- return { ok: true }
857
- }
858
-
859
- /** One check: see `checkEngines`. */
860
- const runCheck = async (): Promise<EngineCheck> => {
861
- await ready
862
- const summary: EngineCheck = { restarted: [], stopped: [] }
863
- const candidates = [...runs.values()].filter((memory) =>
864
- needsEngine({ memory })
865
- )
866
- if (candidates.length === 0) return summary
867
- let command_lines: string[]
868
- try {
869
- command_lines = await list_processes()
870
- } catch (error) {
871
- // Never restart when unsure: two engines on one run is far worse
872
- // than a late restart.
873
- log(
874
- `Couldn't list the processes, so no engine was checked: ${errorText({ error })}`
875
- )
876
- return summary
877
- }
878
- const live = liveRunIds({ command_lines })
879
- const dead = candidates.filter(
880
- (memory) => !live.has(memory.entry.run_id)
881
- )
882
- if (dead.length === 0) return summary
883
- const unfinished = dead.some((memory) => !memory.entry.demo)
884
- ? await listUnfinished()
885
- : null
886
- if (unfinished && !unfinished.ok) {
887
- log(`Couldn't check which runs can go on: ${unfinished.error}`)
888
- }
889
-
890
- const settle = async ({ memory }: { memory: RunMemory }) => {
891
- // The engine may have ended the run while this check ran.
892
- if (!needsEngine({ memory })) return
893
- const { run_id, log_path } = memory.entry
894
- const stop = async ({ why }: { why: StopWhy }) => {
895
- const message = engineStoppedText({ why, run_id, log_path })
896
- log(`[${run_id}] the engine is gone: ${message}`)
897
- await endRun({ memory, ended: { ok: false, message } })
898
- summary.stopped.push(run_id)
899
- }
900
- if (memory.entry.demo) return stop({ why: { kind: 'demo' } })
901
- if (!unfinished?.ok) {
902
- return stop({
903
- why: {
904
- kind: 'check_failed',
905
- error: unfinished?.error ?? 'no answer',
906
- },
907
- })
908
- }
909
- const listed = unfinished.runs.get(run_id)
910
- if (!listed) return stop({ why: { kind: 'not_listed' } })
911
- if (!listed.restart) {
912
- const reason = listed.message ?? 'no reason given'
913
- return stop({
914
- why:
915
- listed.reason === 'billing_stopped'
916
- ? { kind: 'billing_stopped', reason }
917
- : { kind: 'launcher_stopped', reason },
918
- })
919
- }
920
- if (memory.entry.restarts >= MAX_AUTO_RESTARTS) {
921
- return stop({ why: { kind: 'restarts_used_up' } })
922
- }
923
- const restarted = await restartRun({
924
- memory,
925
- engine: unfinished.engine,
926
- })
927
- if (!restarted.ok) {
928
- return stop({
929
- why: { kind: 'spawn_failed', error: restarted.error },
930
- })
931
- }
932
- summary.restarted.push(run_id)
933
- }
934
-
935
- await Promise.all(
936
- dead.map((memory) =>
937
- enqueue({
938
- run_id: memory.entry.run_id,
939
- work: () => settle({ memory }),
940
- })
941
- )
942
- )
943
- return summary
944
- }
945
-
946
- let checking: Promise<EngineCheck> | null = null
947
-
948
- /**
949
- * Finds runs whose engine process is gone although the run hasn't ended,
950
- * and restarts the ones that can go on from their journal
951
- * (`--resume`, at most `MAX_AUTO_RESTARTS` times per run). The rest show
952
- * "engine stopped" with why. When the processes can't be listed, nothing
953
- * is done. Only one check runs at a time: a call while one runs gets the
954
- * same promise. Never rejects.
955
- *
956
- * @example
957
- * const { restarted, stopped } = await board.checkEngines()
958
- */
959
- const checkEngines = (): Promise<EngineCheck> => {
960
- checking ??= runCheck()
961
- .catch((error: unknown) => {
962
- log(`The engine check failed: ${errorText({ error })}`)
963
- return { restarted: [], stopped: [] }
964
- })
965
- .finally(() => {
966
- checking = null
967
- })
968
- return checking
969
- }
70
+ export const createBoardServer = (config: BoardServerConfig) => {
71
+ const store = createRunStore({ config })
72
+ const engine = createEngineProcess({ config, store })
73
+ const resumer = createResumer({ config, store, engine })
74
+ const starter = createStarter({ config, store, engine, resumer })
75
+ const events = createEngineEvents({ store })
76
+ const reads = createBoardReads({ config, store })
77
+ const checks = createEngineChecks({ config, store, engine, reads })
970
78
 
971
79
  /** Resolves when every queued piece of work (and any check) has finished. */
972
80
  const idle = async () => {
973
- await ready
974
- await checking
975
- await Promise.all([...queues.values()])
81
+ await store.ready
82
+ await checks.checking()
83
+ await engine.working()
84
+ await Promise.all([...store.queues.values()])
976
85
  }
977
86
 
978
- // Every method waits for the registry's runs to be rebuilt first.
979
- const ready = load().catch((error: unknown) => {
980
- log(
981
- `Couldn't rebuild the runs from their journals: ${errorText({ error })}`
982
- )
983
- })
984
-
985
- return { startRun, handleEngineEvent, readBoard, checkEngines, idle }
87
+ return {
88
+ startRun: starter.startRun,
89
+ handleEngineEvent: events.handleEngineEvent,
90
+ readBoard: reads.readBoard,
91
+ postReply: reads.postReply,
92
+ checkEngines: checks.checkEngines,
93
+ idle,
94
+ }
986
95
  }
987
96
 
988
97
  export type BoardServer = ReturnType<typeof createBoardServer>