@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
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run.start`: parses the args, finds the engine, records the run, spawns
|
|
3
|
+
* the engine detached, and adds the chat's header row.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes, randomInt } from 'node:crypto'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import type { EngineProcess } from './board-server-engine'
|
|
9
|
+
import type { Resumer } from './board-server-resume'
|
|
10
|
+
import type { RunStore } from './board-server-store'
|
|
11
|
+
import type { BoardServerConfig } from './board-server-types'
|
|
12
|
+
import { errorText } from './board-server-util'
|
|
13
|
+
import { engineArgs, mintRunId, parseRunArgs } from './engine-launch'
|
|
14
|
+
import type { RunEntry } from './run-registry'
|
|
15
|
+
|
|
16
|
+
import type { RunStartInput, RunStartOutput } from '../shared/board-rpc'
|
|
17
|
+
|
|
18
|
+
/** The longest `run.start` waits for the header row before it returns. */
|
|
19
|
+
const HEADER_WAIT_MS = 3000
|
|
20
|
+
|
|
21
|
+
const SUFFIX_ALPHABET = 'abcdefghijklmnopqrstuvwxyz0123456789'
|
|
22
|
+
|
|
23
|
+
const randomSuffix = (): string =>
|
|
24
|
+
Array.from(
|
|
25
|
+
{ length: 4 },
|
|
26
|
+
() => SUFFIX_ALPHABET[randomInt(SUFFIX_ALPHABET.length)] ?? 'x'
|
|
27
|
+
).join('')
|
|
28
|
+
|
|
29
|
+
export const createStarter = ({
|
|
30
|
+
config,
|
|
31
|
+
store,
|
|
32
|
+
engine,
|
|
33
|
+
resumer,
|
|
34
|
+
}: {
|
|
35
|
+
config: BoardServerConfig
|
|
36
|
+
store: RunStore
|
|
37
|
+
engine: EngineProcess
|
|
38
|
+
resumer: Resumer
|
|
39
|
+
}) => {
|
|
40
|
+
const { spawn_engine, log, now, log_dir } = config
|
|
41
|
+
const {
|
|
42
|
+
runs,
|
|
43
|
+
registry,
|
|
44
|
+
queues,
|
|
45
|
+
remember,
|
|
46
|
+
enqueue,
|
|
47
|
+
endRun,
|
|
48
|
+
appendRows,
|
|
49
|
+
header,
|
|
50
|
+
ready,
|
|
51
|
+
} = store
|
|
52
|
+
const { childEnv, findEngine } = engine
|
|
53
|
+
const { resumeRun } = resumer
|
|
54
|
+
|
|
55
|
+
/** Waits for rows being added, but at most `HEADER_WAIT_MS`. */
|
|
56
|
+
const waitForRows = async (adding: Promise<unknown>) => {
|
|
57
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
58
|
+
await Promise.race([
|
|
59
|
+
adding,
|
|
60
|
+
new Promise((resolve) => {
|
|
61
|
+
timer = setTimeout(resolve, HEADER_WAIT_MS)
|
|
62
|
+
}),
|
|
63
|
+
])
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `run.start`: parses the args, finds the engine, records the run, spawns
|
|
69
|
+
* the engine detached, and adds the chat's header row. Returns at once.
|
|
70
|
+
* `resume <run id>` goes on with a run instead (see `resumeRun`).
|
|
71
|
+
*/
|
|
72
|
+
const startRun = async (input: RunStartInput): Promise<RunStartOutput> => {
|
|
73
|
+
await ready
|
|
74
|
+
const parsed = parseRunArgs({ args: input.args })
|
|
75
|
+
if (!parsed.ok)
|
|
76
|
+
return { ok: false, message: parsed.message, run_id: null }
|
|
77
|
+
if (parsed.target.kind === 'resume') {
|
|
78
|
+
const output = await resumeRun({
|
|
79
|
+
run_id: parsed.target.run_id,
|
|
80
|
+
input,
|
|
81
|
+
})
|
|
82
|
+
const rows = queues.get(parsed.target.run_id)
|
|
83
|
+
if (output.ok && rows) await waitForRows(rows)
|
|
84
|
+
return output
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const engine = await findEngine()
|
|
88
|
+
if (!engine.ok)
|
|
89
|
+
return { ok: false, message: engine.message, run_id: null }
|
|
90
|
+
|
|
91
|
+
let run_id = mintRunId({ now: now(), suffix: randomSuffix() })
|
|
92
|
+
while (runs.has(run_id)) {
|
|
93
|
+
run_id = mintRunId({ now: now(), suffix: randomSuffix() })
|
|
94
|
+
}
|
|
95
|
+
const token = randomBytes(24).toString('hex')
|
|
96
|
+
const log_path = join(log_dir, `${run_id}.log`)
|
|
97
|
+
const target = parsed.target
|
|
98
|
+
const entry: RunEntry = {
|
|
99
|
+
run_id,
|
|
100
|
+
token,
|
|
101
|
+
agent_id: input.agent_id,
|
|
102
|
+
workspace_id: input.workspace_id,
|
|
103
|
+
repo: input.cwd,
|
|
104
|
+
spec: target.kind === 'spec' ? target.spec : null,
|
|
105
|
+
demo: target.kind === 'demo',
|
|
106
|
+
started_at: now().toISOString(),
|
|
107
|
+
log_path,
|
|
108
|
+
ended: null,
|
|
109
|
+
restarts: 0,
|
|
110
|
+
}
|
|
111
|
+
registry.add({ entry })
|
|
112
|
+
const memory = remember({ entry, records: [] })
|
|
113
|
+
|
|
114
|
+
const child_env = childEnv({ token })
|
|
115
|
+
|
|
116
|
+
let pid: number | null = null
|
|
117
|
+
try {
|
|
118
|
+
pid = spawn_engine({
|
|
119
|
+
command: engine.command,
|
|
120
|
+
args: [
|
|
121
|
+
...engine.lead_args,
|
|
122
|
+
...engineArgs({ target, repo: input.cwd, run_id }),
|
|
123
|
+
],
|
|
124
|
+
env: child_env,
|
|
125
|
+
cwd: input.cwd,
|
|
126
|
+
log_path,
|
|
127
|
+
on_error: (error) => {
|
|
128
|
+
log(
|
|
129
|
+
`[${run_id}] the engine process failed: ${errorText({ error })}`
|
|
130
|
+
)
|
|
131
|
+
void enqueue({
|
|
132
|
+
run_id,
|
|
133
|
+
work: () =>
|
|
134
|
+
endRun({
|
|
135
|
+
memory,
|
|
136
|
+
ended: {
|
|
137
|
+
ok: false,
|
|
138
|
+
message: `The engine couldn't start: ${errorText({ error })}`,
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
}).pid
|
|
144
|
+
} catch (error) {
|
|
145
|
+
registry.remove({ run_id })
|
|
146
|
+
runs.delete(run_id)
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
message: `Couldn't start the engine: ${errorText({ error })}`,
|
|
150
|
+
run_id: null,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
log(
|
|
154
|
+
`[${run_id}] started ${engine.command} (pid ${pid ?? '?'}), log ${log_path}`
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
await waitForRows(
|
|
158
|
+
enqueue({
|
|
159
|
+
run_id,
|
|
160
|
+
work: () => appendRows({ memory, rows: [header({ memory })] }),
|
|
161
|
+
})
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const what =
|
|
165
|
+
target.kind === 'demo' ? 'a demo run' : `spec #${target.spec}`
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
run_id,
|
|
169
|
+
message: `Started ${what} as ${run_id}. Its log is ${log_path}.`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { startRun }
|
|
174
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board server's runs in memory: each run the plugin started, its board
|
|
3
|
+
* built from its journal records, the queue that keeps a run's work in
|
|
4
|
+
* order, and the rows it adds to the chat.
|
|
5
|
+
*/
|
|
6
|
+
import type { BoardServerConfig, Replay, RunMemory } from './board-server-types'
|
|
7
|
+
import { errorText } from './board-server-util'
|
|
8
|
+
import { readRecord } from './board-vocabulary'
|
|
9
|
+
import { describeRecord, headerRow, rowsForRecord } from './make-rows'
|
|
10
|
+
import { applyEnded, applyRecord, createBoardState } from './reduce-board'
|
|
11
|
+
import { createReplyPoster } from './reply-post'
|
|
12
|
+
import { readJournal } from './run-journals'
|
|
13
|
+
import { createRunRegistry, type RunEntry } from './run-registry'
|
|
14
|
+
|
|
15
|
+
import type { BoardRow } from '../shared/board-rows'
|
|
16
|
+
import type { EngineRecord } from '../shared/board-rpc'
|
|
17
|
+
import type { EngineEnded } from '../shared/board-state'
|
|
18
|
+
|
|
19
|
+
/** After this many failed appends in a row, a run stops sending rows. */
|
|
20
|
+
const APPEND_FAILURES_BEFORE_STOP = 3
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The runs this plugin started, their queues, and the work on them. With a
|
|
24
|
+
* `runs_dir`, every run in the registry is rebuilt from its journal there,
|
|
25
|
+
* and `ready` resolves once that is done.
|
|
26
|
+
*/
|
|
27
|
+
export const createRunStore = ({ config }: { config: BoardServerConfig }) => {
|
|
28
|
+
const {
|
|
29
|
+
registry_path,
|
|
30
|
+
runs_dir = null,
|
|
31
|
+
append_row,
|
|
32
|
+
run_command,
|
|
33
|
+
env,
|
|
34
|
+
home_dir,
|
|
35
|
+
file_exists,
|
|
36
|
+
now,
|
|
37
|
+
log,
|
|
38
|
+
} = config
|
|
39
|
+
const registry = createRunRegistry({ path: registry_path, log })
|
|
40
|
+
const runs = new Map<string, RunMemory>()
|
|
41
|
+
const queues = new Map<string, Promise<unknown>>()
|
|
42
|
+
const poster = createReplyPoster({
|
|
43
|
+
run_command,
|
|
44
|
+
env,
|
|
45
|
+
home_dir,
|
|
46
|
+
file_exists,
|
|
47
|
+
now,
|
|
48
|
+
log,
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
/** Runs `work` after everything already queued for the same run. */
|
|
52
|
+
const enqueue = <Result>({
|
|
53
|
+
run_id,
|
|
54
|
+
work,
|
|
55
|
+
}: {
|
|
56
|
+
run_id: string
|
|
57
|
+
work: () => Promise<Result>
|
|
58
|
+
}): Promise<Result> => {
|
|
59
|
+
const result = (queues.get(run_id) ?? Promise.resolve()).then(work)
|
|
60
|
+
queues.set(
|
|
61
|
+
run_id,
|
|
62
|
+
result.catch(() => undefined)
|
|
63
|
+
)
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const appendRows = async ({
|
|
68
|
+
memory,
|
|
69
|
+
rows,
|
|
70
|
+
}: {
|
|
71
|
+
memory: RunMemory
|
|
72
|
+
rows: BoardRow[]
|
|
73
|
+
}) => {
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
if (memory.rows_stopped) return
|
|
76
|
+
try {
|
|
77
|
+
await append_row({ agent_id: memory.entry.agent_id, row })
|
|
78
|
+
memory.append_failures = 0
|
|
79
|
+
} catch (error) {
|
|
80
|
+
memory.append_failures += 1
|
|
81
|
+
log(
|
|
82
|
+
`[${memory.entry.run_id}] couldn't add row ${row.id} to chat ${memory.entry.agent_id}: ${errorText({ error })}`
|
|
83
|
+
)
|
|
84
|
+
if (memory.append_failures >= APPEND_FAILURES_BEFORE_STOP) {
|
|
85
|
+
memory.rows_stopped = true
|
|
86
|
+
log(
|
|
87
|
+
`[${memory.entry.run_id}] stopped adding rows: chat ${memory.entry.agent_id} seems gone. The side panel still updates.`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const header = ({ memory }: { memory: RunMemory }): BoardRow =>
|
|
95
|
+
headerRow({ state: memory.state, updated_at: now().toISOString() })
|
|
96
|
+
|
|
97
|
+
/** Applies one record to a run's board; returns the rows it makes. */
|
|
98
|
+
const applyOne = ({
|
|
99
|
+
memory,
|
|
100
|
+
record,
|
|
101
|
+
}: {
|
|
102
|
+
memory: Replay
|
|
103
|
+
record: EngineRecord
|
|
104
|
+
}): BoardRow[] => {
|
|
105
|
+
const { run_id } = memory.state.run
|
|
106
|
+
const read = readRecord({ record })
|
|
107
|
+
if (read.status !== 'known') {
|
|
108
|
+
if (read.status === 'bad_content') {
|
|
109
|
+
log(
|
|
110
|
+
`[${run_id}] skipped record ${record.seq} (${record.kind}): its content doesn't fit.\n${read.error}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
memory.state = {
|
|
114
|
+
...memory.state,
|
|
115
|
+
event_count: memory.state.event_count + 1,
|
|
116
|
+
}
|
|
117
|
+
return []
|
|
118
|
+
}
|
|
119
|
+
const before = memory.state
|
|
120
|
+
const after = applyRecord({ state: before, record: read.record })
|
|
121
|
+
if (read.record.kind === 'reply_received') {
|
|
122
|
+
poster.noteTaken({
|
|
123
|
+
run_id,
|
|
124
|
+
seq: read.record.seq,
|
|
125
|
+
time: read.record.time,
|
|
126
|
+
word: read.record.content.word,
|
|
127
|
+
ticket: read.record.content.ticket ?? read.record.ticket,
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
const described = describeRecord({
|
|
131
|
+
before,
|
|
132
|
+
after,
|
|
133
|
+
record: read.record,
|
|
134
|
+
})
|
|
135
|
+
memory.state = described
|
|
136
|
+
? { ...after, latest: described.text, latest_at: read.record.time }
|
|
137
|
+
: after
|
|
138
|
+
return rowsForRecord({ run_id, before, after, record: read.record })
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Applies records in seq order from the run's `next_seq`. Lower seqs are
|
|
143
|
+
* duplicates and skipped; a gap stops there (`gap` is the seq found).
|
|
144
|
+
*/
|
|
145
|
+
const applyInOrder = ({
|
|
146
|
+
memory,
|
|
147
|
+
records,
|
|
148
|
+
}: {
|
|
149
|
+
memory: Replay
|
|
150
|
+
records: EngineRecord[]
|
|
151
|
+
}): {
|
|
152
|
+
rows: Map<number, BoardRow[]>
|
|
153
|
+
applied: number
|
|
154
|
+
gap: number | null
|
|
155
|
+
} => {
|
|
156
|
+
const rows = new Map<number, BoardRow[]>()
|
|
157
|
+
let applied = 0
|
|
158
|
+
for (const record of records.toSorted(
|
|
159
|
+
(left, right) => left.seq - right.seq
|
|
160
|
+
)) {
|
|
161
|
+
if (record.seq < memory.next_seq) continue
|
|
162
|
+
if (record.seq > memory.next_seq) {
|
|
163
|
+
return { rows, applied, gap: record.seq }
|
|
164
|
+
}
|
|
165
|
+
rows.set(record.seq, applyOne({ memory, record }))
|
|
166
|
+
memory.next_seq += 1
|
|
167
|
+
applied += 1
|
|
168
|
+
}
|
|
169
|
+
return { rows, applied, gap: null }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Knows a run this plugin started, rebuilt from its journal `records`.
|
|
174
|
+
* Their rows aren't added now; while the engine may still send, they are
|
|
175
|
+
* kept until it sends those records again (see `handleEngineEvent`).
|
|
176
|
+
*/
|
|
177
|
+
const remember = ({
|
|
178
|
+
entry,
|
|
179
|
+
records,
|
|
180
|
+
}: {
|
|
181
|
+
entry: RunEntry
|
|
182
|
+
records: EngineRecord[]
|
|
183
|
+
}): RunMemory => {
|
|
184
|
+
const memory: RunMemory = {
|
|
185
|
+
entry,
|
|
186
|
+
state: createBoardState({
|
|
187
|
+
run_id: entry.run_id,
|
|
188
|
+
spec_number: entry.spec,
|
|
189
|
+
demo: entry.demo,
|
|
190
|
+
started_at: entry.started_at,
|
|
191
|
+
log_path: entry.log_path,
|
|
192
|
+
}),
|
|
193
|
+
next_seq: 1,
|
|
194
|
+
append_failures: 0,
|
|
195
|
+
rows_stopped: false,
|
|
196
|
+
unsent_rows: new Map(),
|
|
197
|
+
}
|
|
198
|
+
const { rows } = applyInOrder({ memory, records })
|
|
199
|
+
// A run whose engine ended stays ended after a plugin restart.
|
|
200
|
+
if (entry.ended) {
|
|
201
|
+
memory.state = applyEnded({
|
|
202
|
+
state: memory.state,
|
|
203
|
+
ended: entry.ended,
|
|
204
|
+
})
|
|
205
|
+
} else {
|
|
206
|
+
memory.unsent_rows = rows
|
|
207
|
+
}
|
|
208
|
+
runs.set(entry.run_id, memory)
|
|
209
|
+
return memory
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A run's journal records on disk; none without a runs folder. */
|
|
213
|
+
const journalOf = ({ run_id }: { run_id: string }) =>
|
|
214
|
+
runs_dir === null
|
|
215
|
+
? Promise.resolve([])
|
|
216
|
+
: readJournal({ runs_dir, run_id, log })
|
|
217
|
+
|
|
218
|
+
/** Rebuilds every run in the registry from its journal. */
|
|
219
|
+
const load = async () => {
|
|
220
|
+
const entries = registry.list()
|
|
221
|
+
const journals = await Promise.all(
|
|
222
|
+
entries.map(({ run_id }) => journalOf({ run_id }))
|
|
223
|
+
)
|
|
224
|
+
entries.forEach((entry, index) =>
|
|
225
|
+
remember({ entry, records: journals[index] ?? [] })
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Changes a run's registry entry, and its copy in memory. */
|
|
230
|
+
const updateEntry = ({
|
|
231
|
+
memory,
|
|
232
|
+
change,
|
|
233
|
+
}: {
|
|
234
|
+
memory: RunMemory
|
|
235
|
+
change: Partial<Pick<RunEntry, 'ended' | 'restarts' | 'token'>>
|
|
236
|
+
}) => {
|
|
237
|
+
memory.entry = registry.update({
|
|
238
|
+
run_id: memory.entry.run_id,
|
|
239
|
+
change,
|
|
240
|
+
}) ?? { ...memory.entry, ...change }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Marks a run's engine as ended (kept in the registry) and updates its header. */
|
|
244
|
+
const endRun = async ({
|
|
245
|
+
memory,
|
|
246
|
+
ended,
|
|
247
|
+
}: {
|
|
248
|
+
memory: RunMemory
|
|
249
|
+
ended: EngineEnded
|
|
250
|
+
}) => {
|
|
251
|
+
memory.state = applyEnded({ state: memory.state, ended })
|
|
252
|
+
memory.unsent_rows.clear()
|
|
253
|
+
updateEntry({ memory, change: { ended } })
|
|
254
|
+
await appendRows({ memory, rows: [header({ memory })] })
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Every method waits for the registry's runs to be rebuilt first.
|
|
258
|
+
const ready = load().catch((error: unknown) => {
|
|
259
|
+
log(
|
|
260
|
+
`Couldn't rebuild the runs from their journals: ${errorText({ error })}`
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
registry,
|
|
266
|
+
runs,
|
|
267
|
+
queues,
|
|
268
|
+
poster,
|
|
269
|
+
enqueue,
|
|
270
|
+
appendRows,
|
|
271
|
+
header,
|
|
272
|
+
applyInOrder,
|
|
273
|
+
remember,
|
|
274
|
+
journalOf,
|
|
275
|
+
updateEntry,
|
|
276
|
+
endRun,
|
|
277
|
+
ready,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export type RunStore = ReturnType<typeof createRunStore>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board server's types: what it spawns and runs, what it is given, and
|
|
3
|
+
* what it keeps in memory for each run.
|
|
4
|
+
*/
|
|
5
|
+
import type { RunEntry } from './run-registry'
|
|
6
|
+
|
|
7
|
+
import type { BoardRow } from '../shared/board-rows'
|
|
8
|
+
import type { BoardState } from '../shared/board-state'
|
|
9
|
+
import type { EngineSettings } from '../shared/engine-settings'
|
|
10
|
+
|
|
11
|
+
/** What the server asks the host to spawn: detached, output to a log file. */
|
|
12
|
+
export type SpawnRequest = {
|
|
13
|
+
command: string
|
|
14
|
+
args: string[]
|
|
15
|
+
env: Record<string, string>
|
|
16
|
+
cwd: string
|
|
17
|
+
log_path: string
|
|
18
|
+
/** Called if the process fails to start after `spawn_engine` returned. */
|
|
19
|
+
on_error: (error: unknown) => void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SpawnEngine = (request: SpawnRequest) => { pid: number | null }
|
|
23
|
+
|
|
24
|
+
/** A short command the server runs and waits for, such as `luca-run --unfinished`. */
|
|
25
|
+
export type CommandRequest = {
|
|
26
|
+
command: string
|
|
27
|
+
args: string[]
|
|
28
|
+
cwd: string
|
|
29
|
+
env: Record<string, string>
|
|
30
|
+
timeout_ms: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** How a command ended: `exit_code` is `null` when it was killed or timed out. */
|
|
34
|
+
export type CommandResult = {
|
|
35
|
+
exit_code: number | null
|
|
36
|
+
stdout: string
|
|
37
|
+
stderr: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Runs a command to its end. Never throws: a failure is in the result. */
|
|
41
|
+
export type RunCommand = (request: CommandRequest) => Promise<CommandResult>
|
|
42
|
+
|
|
43
|
+
/** Every running process's full command line. */
|
|
44
|
+
export type ListProcesses = () => Promise<string[]>
|
|
45
|
+
|
|
46
|
+
/** What one engine check did, by run id. */
|
|
47
|
+
export type EngineCheck = { restarted: string[]; stopped: string[] }
|
|
48
|
+
|
|
49
|
+
/** Appends (or replaces, by id) one row in a chat's timeline. */
|
|
50
|
+
export type AppendRow = ({
|
|
51
|
+
agent_id,
|
|
52
|
+
row,
|
|
53
|
+
}: {
|
|
54
|
+
agent_id: string
|
|
55
|
+
row: BoardRow
|
|
56
|
+
}) => Promise<void>
|
|
57
|
+
|
|
58
|
+
/** A run's board, built from its journal records so far. */
|
|
59
|
+
export type Replay = {
|
|
60
|
+
state: BoardState
|
|
61
|
+
/** The next journal seq the board wants (starts at 1). */
|
|
62
|
+
next_seq: number
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One run the plugin started: its registry entry and its board in memory. */
|
|
66
|
+
export type RunMemory = Replay & {
|
|
67
|
+
entry: RunEntry
|
|
68
|
+
append_failures: number
|
|
69
|
+
rows_stopped: boolean
|
|
70
|
+
/**
|
|
71
|
+
* The rows of records rebuilt from disk, by seq, that the chat may not
|
|
72
|
+
* have: the engine may have written them while the plugin was down.
|
|
73
|
+
* Added when the engine sends those records again.
|
|
74
|
+
*/
|
|
75
|
+
unsent_rows: Map<number, BoardRow[]>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A run the plugin didn't start (such as one from the command line), read
|
|
80
|
+
* from its journal on disk. Read-only: no chat rows, no engine events, no
|
|
81
|
+
* restarts. `size` and `changed_ms` are its journal's when last read.
|
|
82
|
+
* `repo` is the repo its `run_started` names, `null` when it names none.
|
|
83
|
+
* `engine_gone` is set by the engine check (#635): no process names the
|
|
84
|
+
* run and its live file isn't fresh. It only labels the run.
|
|
85
|
+
*/
|
|
86
|
+
export type OutsideRun = Replay & {
|
|
87
|
+
size: number
|
|
88
|
+
changed_ms: number
|
|
89
|
+
repo: string | null
|
|
90
|
+
engine_gone: boolean
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* What `createBoardServer` is given. Every side effect is injected, so tests
|
|
95
|
+
* drive the server without Paseo.
|
|
96
|
+
*/
|
|
97
|
+
export type BoardServerConfig = {
|
|
98
|
+
registry_path: string
|
|
99
|
+
/** The engine's runs folder (`<runs_dir>/<run_id>/journal.jsonl`). */
|
|
100
|
+
runs_dir?: string | null
|
|
101
|
+
append_row: AppendRow
|
|
102
|
+
spawn_engine: SpawnEngine
|
|
103
|
+
list_processes: ListProcesses
|
|
104
|
+
run_command: RunCommand
|
|
105
|
+
read_settings: () => Promise<EngineSettings>
|
|
106
|
+
file_exists: ({ path }: { path: string }) => boolean
|
|
107
|
+
/** Follows an installed `luca-run`'s symlink; the path as is without it. */
|
|
108
|
+
real_path?: ({ path }: { path: string }) => string
|
|
109
|
+
home_dir: string
|
|
110
|
+
env: Record<string, string | undefined>
|
|
111
|
+
log_dir: string
|
|
112
|
+
now: () => Date
|
|
113
|
+
log: (message: string) => void
|
|
114
|
+
}
|