@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.
- package/CHANGELOG.md +76 -0
- package/README.md +7 -2
- package/board/client/board-look.ts +31 -1
- package/board/client/board-panel-parts.tsx +150 -0
- package/board/client/board-panel-stages.ts +99 -0
- package/board/client/board-panel-styles.ts +223 -0
- package/board/client/board-panel.tsx +77 -1275
- package/board/client/engine-life.ts +48 -0
- package/board/client/final-review-stack.tsx +144 -0
- package/board/client/heartbeat.tsx +61 -0
- package/board/client/helper-lines.tsx +69 -0
- package/board/client/live-feed.tsx +86 -0
- package/board/client/live-line.ts +73 -0
- package/board/client/model-line.tsx +178 -0
- package/board/client/needs-you-section.tsx +133 -0
- package/board/client/one-line.tsx +193 -0
- package/board/client/repo-wait-banner.tsx +69 -0
- package/board/client/run-footer.tsx +68 -0
- package/board/client/run-header.tsx +189 -0
- package/board/client/run-picker.tsx +98 -0
- package/board/client/status-bar.tsx +131 -0
- package/board/client/step-dots.tsx +127 -0
- package/board/client/stuck-actions.tsx +269 -0
- package/board/client/ticket-card-view.tsx +238 -0
- package/board/client/ticket-detail.tsx +87 -0
- package/board/client/ticket-stack.tsx +119 -0
- package/board/client/ticket-wait.ts +188 -0
- package/board/client/timeline-rows.tsx +41 -14
- package/board/client/usage-line.tsx +51 -0
- package/board/client/wait-line.tsx +39 -0
- package/board/index.client.tsx +1 -1
- package/board/index.server.ts +7 -1
- package/board/package.json +1 -1
- package/board/server/board-panel-state-helpers.ts +67 -0
- package/board/server/board-server-check.ts +294 -0
- package/board/server/board-server-engine.ts +129 -0
- package/board/server/board-server-events.ts +134 -0
- package/board/server/board-server-read.ts +190 -0
- package/board/server/board-server-resume.ts +164 -0
- package/board/server/board-server-start.ts +174 -0
- package/board/server/board-server-store.ts +281 -0
- package/board/server/board-server-types.ts +114 -0
- package/board/server/board-server-util.ts +4 -0
- package/board/server/board-server.ts +52 -943
- package/board/server/board-vocabulary-engine.ts +144 -0
- package/board/server/board-vocabulary-review.ts +115 -0
- package/board/server/board-vocabulary-run.ts +145 -0
- package/board/server/board-vocabulary-shapes.ts +153 -0
- package/board/server/board-vocabulary.ts +20 -375
- package/board/server/current-step-panel-helpers.ts +108 -0
- package/board/server/describe-record.ts +403 -0
- package/board/server/engine-launch.ts +20 -3
- package/board/server/engine-restart-helpers.ts +105 -0
- package/board/server/engine-watch.ts +45 -5
- package/board/server/journal-rebuild-helpers.ts +169 -0
- package/board/server/live-file.ts +130 -0
- package/board/server/luca-version.ts +1 -1
- package/board/server/make-rows.ts +14 -629
- package/board/server/record-texts.ts +350 -0
- package/board/server/reduce-board-agent-start.ts +224 -0
- package/board/server/reduce-board-agents.ts +160 -0
- package/board/server/reduce-board-counters.ts +138 -0
- package/board/server/reduce-board-create.ts +148 -0
- package/board/server/reduce-board-final-review.ts +174 -0
- package/board/server/reduce-board-needs-you.ts +283 -0
- package/board/server/reduce-board-reasons.ts +59 -0
- package/board/server/reduce-board-run.ts +255 -0
- package/board/server/reduce-board-settle.ts +88 -0
- package/board/server/reduce-board-steps.ts +226 -0
- package/board/server/reduce-board-text.ts +139 -0
- package/board/server/reduce-board-ticket-checks.ts +168 -0
- package/board/server/reduce-board-ticket-join.ts +144 -0
- package/board/server/reduce-board-ticket-start.ts +132 -0
- package/board/server/reduce-board-update.ts +117 -0
- package/board/server/reduce-board-usage.ts +247 -0
- package/board/server/reduce-board.ts +100 -1917
- package/board/server/reply-post.ts +407 -0
- package/board/server/run-registry.ts +8 -4
- package/board/server/ticket-stages-panel-helpers.ts +296 -0
- package/board/shared/board-rows.ts +9 -0
- package/board/shared/board-rpc.ts +42 -0
- package/board/shared/board-state-counts.ts +77 -0
- package/board/shared/board-state-live.ts +99 -0
- package/board/shared/board-state-review.ts +59 -0
- package/board/shared/board-state-run.ts +236 -0
- package/board/shared/board-state-ticket.ts +257 -0
- package/board/shared/board-state-usage.ts +107 -0
- package/board/shared/board-state.ts +97 -567
- package/board/shared/reply-actions.ts +207 -0
- package/engine/agents/agent-launcher.ts +27 -4
- package/engine/agents/claude-launcher-helpers.ts +233 -0
- package/engine/agents/claude-launcher.ts +28 -669
- package/engine/agents/claude-messages.ts +212 -0
- package/engine/agents/claude-options.ts +43 -8
- package/engine/agents/common-instructions.ts +138 -0
- package/engine/agents/input-channel.ts +36 -0
- package/engine/agents/launch-paths.ts +29 -0
- package/engine/agents/launch-session.ts +227 -0
- package/engine/agents/learner-instructions.ts +31 -0
- package/engine/agents/open-session.ts +210 -0
- package/engine/agents/prompt-sections.ts +95 -0
- package/engine/agents/rejoin-prompts.ts +135 -0
- package/engine/agents/reviewer-instructions.ts +99 -0
- package/engine/agents/role-instructions.ts +31 -244
- package/engine/agents/role-prompts.ts +39 -109
- package/engine/agents/role-results.ts +106 -7
- package/engine/agents/scripted-launcher.ts +21 -6
- package/engine/agents/session-pool.ts +124 -0
- package/engine/agents/turn-progress.ts +283 -0
- package/engine/agents/writer-instructions.ts +151 -0
- package/engine/cli/computer-adapters-real.ts +15 -3
- package/engine/cli/computer-adapters.ts +2 -0
- package/engine/cli/computer-checks.ts +87 -4
- package/engine/cli/config-reload-text.ts +39 -0
- package/engine/cli/decision-model-setup.ts +70 -0
- package/engine/cli/doctor-command.ts +1 -0
- package/engine/cli/doctor.ts +21 -3
- package/engine/cli/going-runs.ts +192 -36
- package/engine/cli/init-command.ts +1 -0
- package/engine/cli/init.ts +23 -1
- package/engine/cli/live-runs.ts +73 -0
- package/engine/cli/luca-run.ts +42 -7
- package/engine/cli/luca-skills.ts +183 -0
- package/engine/cli/luca.ts +8 -1
- package/engine/cli/models-command.ts +119 -0
- package/engine/cli/read-run.ts +81 -0
- package/engine/cli/run-args.ts +33 -6
- package/engine/cli/run-demo.ts +143 -0
- package/engine/cli/run-end.ts +184 -0
- package/engine/cli/run-modes-helpers.ts +81 -0
- package/engine/cli/run-modes.ts +62 -439
- package/engine/cli/setup.ts +5 -48
- package/engine/cli/unfinished-runs.ts +122 -0
- package/engine/cli/upgrade-command.ts +212 -30
- package/engine/cli/upgrade-finish.ts +149 -0
- package/engine/cli/upgrade.ts +112 -71
- package/engine/config/config-fields.ts +127 -0
- package/engine/config/config-reload.ts +250 -0
- package/engine/config/engine-config-schema.ts +235 -0
- package/engine/config/engine-config.ts +72 -142
- package/engine/config/test-commands.ts +102 -0
- package/engine/core/agent-guards-helpers.ts +84 -0
- package/engine/core/agent-turn.ts +395 -0
- package/engine/core/already-done.ts +44 -1
- package/engine/core/build-action.ts +214 -0
- package/engine/core/build-context.ts +135 -0
- package/engine/core/build-launch.ts +259 -0
- package/engine/core/commit-in.ts +282 -0
- package/engine/core/decide-build-early.ts +108 -0
- package/engine/core/decide-build-ending.ts +145 -0
- package/engine/core/decide-build-helpers.ts +87 -0
- package/engine/core/decide-build-tickets.ts +207 -0
- package/engine/core/decide-build-view.ts +103 -0
- package/engine/core/decide-build.ts +101 -1368
- package/engine/core/decide-final-fix.ts +203 -0
- package/engine/core/decide-final-review.ts +55 -310
- package/engine/core/decide-red-green.ts +398 -0
- package/engine/core/decide-rejoin.ts +259 -0
- package/engine/core/decide-repo-wait.ts +129 -0
- package/engine/core/decide-run-budget.ts +86 -12
- package/engine/core/decide-stuck-helpers.ts +127 -0
- package/engine/core/decide-stuck.ts +60 -8
- package/engine/core/decide-ticket-review.ts +95 -0
- package/engine/core/decide-ticket.ts +267 -0
- package/engine/core/decide-waits.ts +47 -0
- package/engine/core/decide.ts +74 -23
- package/engine/core/engine-chores.ts +163 -0
- package/engine/core/environment-rerun.ts +83 -0
- package/engine/core/evidence-text.ts +84 -0
- package/engine/core/execute-action.ts +303 -0
- package/engine/core/execute-already-done-check.ts +133 -0
- package/engine/core/execute-build.ts +68 -944
- package/engine/core/execute-changeset.ts +5 -7
- package/engine/core/execute-final-review.ts +125 -13
- package/engine/core/execute-gates.ts +182 -0
- package/engine/core/execute-intake.ts +130 -0
- package/engine/core/execute-join.ts +276 -0
- package/engine/core/execute-memory.ts +12 -0
- package/engine/core/execute-pull-request.ts +84 -0
- package/engine/core/execute-red-check.ts +219 -0
- package/engine/core/execute-repo-wait.ts +129 -0
- package/engine/core/execute-stuck.ts +25 -10
- package/engine/core/execute-waits.ts +158 -0
- package/engine/core/execute-with-jev.ts +118 -0
- package/engine/core/execute.ts +124 -831
- package/engine/core/final-failure-cap.ts +38 -0
- package/engine/core/final-review-action.ts +125 -0
- package/engine/core/final-review-text.ts +63 -17
- package/engine/core/final-stuck-text.ts +86 -0
- package/engine/core/fix-loop-text.ts +49 -2
- package/engine/core/left-out.ts +120 -0
- package/engine/core/live-file.ts +372 -0
- package/engine/core/loop-caps.ts +25 -2
- package/engine/core/not-in-run.ts +7 -3
- package/engine/core/pull-request-text.ts +255 -6
- package/engine/core/repo-wait-text.ts +81 -0
- package/engine/core/review-text.ts +55 -1
- package/engine/core/run-crash-recovery-helpers.ts +16 -0
- package/engine/core/run-session-close-helpers.ts +96 -0
- package/engine/core/run-start.ts +148 -0
- package/engine/core/run-stuck-text.ts +86 -0
- package/engine/core/schedule-rules.ts +193 -0
- package/engine/core/stuck-detail-text.ts +89 -0
- package/engine/core/stuck-format.ts +22 -0
- package/engine/core/stuck-text.ts +29 -332
- package/engine/core/ticket-stuck-text.ts +192 -0
- package/engine/core/worktree-changes.ts +106 -0
- package/engine/gates/already-done-check.ts +120 -0
- package/engine/gates/feature-map.ts +96 -0
- package/engine/gates/gate-runner.ts +108 -108
- package/engine/gates/gate-schemas.ts +61 -2
- package/engine/gates/leftover-scan.ts +154 -31
- package/engine/gates/new-tests.ts +45 -0
- package/engine/gates/own-files-check.ts +98 -0
- package/engine/gates/prepare-check.ts +60 -0
- package/engine/gates/prepare-slots.ts +99 -0
- package/engine/gates/red-check.ts +184 -16
- package/engine/gates/shell-check.ts +64 -0
- package/engine/gates/slot-waits.ts +55 -0
- package/engine/gates/slow-test-runner.ts +59 -0
- package/engine/gates/test-checks.ts +161 -0
- package/engine/gates/test-runner.ts +164 -5
- package/engine/gates/test-slots.ts +48 -0
- package/engine/git/git-adapter.ts +55 -396
- package/engine/git/git-commits.ts +132 -0
- package/engine/git/git-join.ts +139 -0
- package/engine/git/git-read.ts +123 -0
- package/engine/git/git-run.ts +32 -0
- package/engine/git/git-worktrees.ts +143 -0
- package/engine/git/outside-links.ts +55 -0
- package/engine/guards/after-turn-check.ts +31 -5
- package/engine/guards/guard-hook.ts +60 -12
- package/engine/guards/guard-roles.ts +68 -0
- package/engine/guards/helper-rules.ts +92 -0
- package/engine/guards/permission-rules.ts +161 -0
- package/engine/guards/role-rules-helpers.ts +48 -0
- package/engine/guards/role-rules.ts +34 -606
- package/engine/guards/shell-rules.ts +344 -0
- package/engine/guards/worktree-git.ts +208 -0
- package/engine/guards/worktree-snapshot.ts +412 -0
- package/engine/guards/worktree-state.ts +28 -825
- package/engine/guards/worktree-undo.ts +260 -0
- package/engine/guards/write-rules.ts +149 -0
- package/engine/index.ts +30 -9
- package/engine/intake/criteria-checks.ts +114 -0
- package/engine/intake/intake-checks.ts +164 -30
- package/engine/intake/intake-schemas.ts +37 -2
- package/engine/jev/clef-client.ts +259 -0
- package/engine/jev/decision-model-credentials.ts +212 -0
- package/engine/jev/jev-jobs.ts +144 -60
- package/engine/jev/jev-schemas.ts +86 -5
- package/engine/jev/jev-shadow-helpers.ts +218 -0
- package/engine/jev/jev-shadow.ts +109 -64
- package/engine/jev/model-pick.ts +275 -0
- package/engine/journal/agent-entries.ts +167 -0
- package/engine/journal/agent-message-entries.ts +72 -0
- package/engine/journal/agent-turn-replay.ts +203 -0
- package/engine/journal/already-done-entries.ts +55 -0
- package/engine/journal/commit-entries.ts +56 -0
- package/engine/journal/cross-ticket-replay.ts +222 -0
- package/engine/journal/decision-model-entries.ts +86 -0
- package/engine/journal/entry-fields.ts +10 -0
- package/engine/journal/final-review-entries.ts +128 -0
- package/engine/journal/final-review-replay.ts +324 -0
- package/engine/journal/final-review-round.ts +320 -0
- package/engine/journal/final-review-state.ts +180 -0
- package/engine/journal/gate-entries.ts +81 -0
- package/engine/journal/intake-entries.ts +87 -0
- package/engine/journal/join-entries.ts +126 -0
- package/engine/journal/journal-entry-schema.ts +218 -0
- package/engine/journal/journal-kinds.ts +97 -0
- package/engine/journal/journal-record-schema.ts +218 -0
- package/engine/journal/journal-record.ts +43 -1400
- package/engine/journal/journal.ts +6 -1
- package/engine/journal/limit-entries.ts +90 -0
- package/engine/journal/memory-entries.ts +124 -0
- package/engine/journal/memory-replay.ts +157 -0
- package/engine/journal/plan-replay.ts +139 -0
- package/engine/journal/pull-request-entries.ts +41 -0
- package/engine/journal/replay.ts +157 -2144
- package/engine/journal/reply-replay.ts +144 -0
- package/engine/journal/repo-wait-entries.ts +53 -0
- package/engine/journal/run-entries.ts +172 -0
- package/engine/journal/run-state.ts +264 -0
- package/engine/journal/shared-failure-replay.ts +133 -0
- package/engine/journal/stuck-entries.ts +193 -0
- package/engine/journal/ticket-progress.ts +392 -0
- package/engine/journal/ticket-replay.ts +411 -0
- package/engine/journal/ticket-result-replay.ts +364 -0
- package/engine/journal/ticket-retry-replay.ts +142 -0
- package/engine/journal/worktree-entries.ts +122 -0
- package/engine/models/model-score.ts +512 -0
- package/engine/shell/run-command.ts +187 -18
- package/engine/testing/agent-turn-fixtures.ts +287 -0
- package/engine/testing/already-done-fixtures.ts +97 -0
- package/engine/testing/build-fixtures.ts +68 -973
- package/engine/testing/checkouts-fixtures.ts +48 -0
- package/engine/testing/final-review-fixtures.ts +62 -1
- package/engine/testing/final-review-steps.ts +143 -0
- package/engine/testing/limit-fixtures.ts +85 -0
- package/engine/testing/live-file-fixtures.ts +57 -0
- package/engine/testing/practice-repo.ts +10 -4
- package/engine/testing/practice-run.ts +1 -1
- package/engine/testing/reply-fixtures.ts +88 -0
- package/engine/testing/run-budget-fixtures.ts +165 -0
- package/engine/testing/run-fixtures.ts +219 -0
- package/engine/testing/sdk-play.ts +177 -0
- package/engine/testing/setup-repo.ts +194 -0
- package/engine/testing/step-record-fixtures.ts +55 -0
- package/engine/testing/ticket-step-fixtures.ts +272 -0
- package/engine/testing/wait-scenarios.ts +186 -0
- package/engine/tracker/gh-retry.ts +217 -0
- package/engine/tracker/github-tracker.ts +310 -90
- package/engine/tracker/in-memory-tracker.ts +8 -4
- package/engine/tracker/tracker.ts +131 -6
- package/package.json +2 -1
- package/skills/luca-retro/SKILL.md +74 -0
- package/skills/luca-retro/scripts/retro-summary.ts +1356 -0
- package/skills/luca-unstick/SKILL.md +153 -0
- package/skills/luca-unstick/scripts/stuck-summary.ts +1063 -0
- 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
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* `
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
81
|
+
await store.ready
|
|
82
|
+
await checks.checking()
|
|
83
|
+
await engine.working()
|
|
84
|
+
await Promise.all([...store.queues.values()])
|
|
976
85
|
}
|
|
977
86
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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>
|