@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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Running the engine for a run: the environment it gets, finding it from
|
|
3
|
+
* the settings, the one-at-a-time lock on engine work, and spawning it with
|
|
4
|
+
* `--resume`.
|
|
5
|
+
*/
|
|
6
|
+
import type { RunStore } from './board-server-store'
|
|
7
|
+
import type { BoardServerConfig, RunMemory } from './board-server-types'
|
|
8
|
+
import { errorText } from './board-server-util'
|
|
9
|
+
import { resolveEngine } from './engine-launch'
|
|
10
|
+
import { engineStoppedText, resumeArgs } from './engine-watch'
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
EngineSettingsSchema,
|
|
14
|
+
type EngineSettings,
|
|
15
|
+
} from '../shared/engine-settings'
|
|
16
|
+
|
|
17
|
+
export const createEngineProcess = ({
|
|
18
|
+
config,
|
|
19
|
+
store,
|
|
20
|
+
}: {
|
|
21
|
+
config: BoardServerConfig
|
|
22
|
+
store: RunStore
|
|
23
|
+
}) => {
|
|
24
|
+
const {
|
|
25
|
+
env,
|
|
26
|
+
read_settings,
|
|
27
|
+
log,
|
|
28
|
+
home_dir,
|
|
29
|
+
file_exists,
|
|
30
|
+
real_path,
|
|
31
|
+
spawn_engine,
|
|
32
|
+
} = config
|
|
33
|
+
const { enqueue, endRun } = store
|
|
34
|
+
|
|
35
|
+
/** The daemon's env for the engine, plus the run's token when given. */
|
|
36
|
+
const childEnv = ({
|
|
37
|
+
token,
|
|
38
|
+
}: {
|
|
39
|
+
token: string | null
|
|
40
|
+
}): Record<string, string> => {
|
|
41
|
+
const child_env: Record<string, string> = {}
|
|
42
|
+
for (const [key, value] of Object.entries(env)) {
|
|
43
|
+
if (value !== undefined) child_env[key] = value
|
|
44
|
+
}
|
|
45
|
+
if (token !== null) child_env.LUCA_BOARD_TOKEN = token
|
|
46
|
+
return child_env
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Finds the engine from the current settings, as `run.start` does. */
|
|
50
|
+
const findEngine = async () => {
|
|
51
|
+
let settings: EngineSettings = EngineSettingsSchema.parse({})
|
|
52
|
+
try {
|
|
53
|
+
settings = await read_settings()
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(`Couldn't read the engine settings: ${errorText({ error })}`)
|
|
56
|
+
}
|
|
57
|
+
return resolveEngine({
|
|
58
|
+
settings,
|
|
59
|
+
env,
|
|
60
|
+
home_dir,
|
|
61
|
+
file_exists,
|
|
62
|
+
real_path,
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Engine checks and resumes run one at a time, so a resume and a check
|
|
68
|
+
* never both start an engine for the same run.
|
|
69
|
+
*/
|
|
70
|
+
let engine_work: Promise<unknown> = Promise.resolve()
|
|
71
|
+
const exclusive = <Result>(work: () => Promise<Result>) => {
|
|
72
|
+
const result = engine_work.then(work)
|
|
73
|
+
engine_work = result.catch(() => undefined)
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Spawns a run's engine with `--resume` and its token; returns its pid. */
|
|
78
|
+
const spawnResume = ({
|
|
79
|
+
memory,
|
|
80
|
+
engine,
|
|
81
|
+
on_failed,
|
|
82
|
+
}: {
|
|
83
|
+
memory: RunMemory
|
|
84
|
+
engine: { command: string; lead_args: string[] }
|
|
85
|
+
/** What the log says when the process fails after the spawn. */
|
|
86
|
+
on_failed: string
|
|
87
|
+
}): number | null => {
|
|
88
|
+
const { run_id, repo, token, log_path } = memory.entry
|
|
89
|
+
return spawn_engine({
|
|
90
|
+
command: engine.command,
|
|
91
|
+
args: [...engine.lead_args, ...resumeArgs({ run_id, repo })],
|
|
92
|
+
env: childEnv({ token }),
|
|
93
|
+
cwd: repo,
|
|
94
|
+
log_path,
|
|
95
|
+
on_error: (error) => {
|
|
96
|
+
log(`[${run_id}] ${on_failed}: ${errorText({ error })}`)
|
|
97
|
+
void enqueue({
|
|
98
|
+
run_id,
|
|
99
|
+
work: () =>
|
|
100
|
+
endRun({
|
|
101
|
+
memory,
|
|
102
|
+
ended: {
|
|
103
|
+
ok: false,
|
|
104
|
+
message: engineStoppedText({
|
|
105
|
+
why: {
|
|
106
|
+
kind: 'spawn_failed',
|
|
107
|
+
error: errorText({ error }),
|
|
108
|
+
},
|
|
109
|
+
run_id,
|
|
110
|
+
log_path,
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
}).pid
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
childEnv,
|
|
121
|
+
findEngine,
|
|
122
|
+
exclusive,
|
|
123
|
+
spawnResume,
|
|
124
|
+
/** The engine work in flight (or done), to wait for. */
|
|
125
|
+
working: () => engine_work,
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export type EngineProcess = ReturnType<typeof createEngineProcess>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `engine.event`: applies a run's journal records, sent by the engine, in
|
|
3
|
+
* seq order, and adds the rows they make.
|
|
4
|
+
*/
|
|
5
|
+
import { timingSafeEqual } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
import type { RunStore } from './board-server-store'
|
|
8
|
+
import type { RunMemory } from './board-server-types'
|
|
9
|
+
import { applyEnded } from './reduce-board'
|
|
10
|
+
|
|
11
|
+
import type { BoardRow } from '../shared/board-rows'
|
|
12
|
+
import type {
|
|
13
|
+
EngineEventInput,
|
|
14
|
+
EngineEventOutput,
|
|
15
|
+
EngineRecord,
|
|
16
|
+
} from '../shared/board-rpc'
|
|
17
|
+
|
|
18
|
+
const sameToken = ({ left, right }: { left: string; right: string }) => {
|
|
19
|
+
const a = Buffer.from(left)
|
|
20
|
+
const b = Buffer.from(right)
|
|
21
|
+
return a.length === b.length && timingSafeEqual(a, b)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const createEngineEvents = ({ store }: { store: RunStore }) => {
|
|
25
|
+
const {
|
|
26
|
+
runs,
|
|
27
|
+
enqueue,
|
|
28
|
+
applyInOrder,
|
|
29
|
+
appendRows,
|
|
30
|
+
header,
|
|
31
|
+
updateEntry,
|
|
32
|
+
ready,
|
|
33
|
+
} = store
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The kept rows of records rebuilt from disk that the engine sent again.
|
|
37
|
+
* The engine sends from the first record it hasn't had an answer for, so
|
|
38
|
+
* the chat already has the rows of every lower seq: those are dropped.
|
|
39
|
+
*/
|
|
40
|
+
const takeUnsentRows = ({
|
|
41
|
+
memory,
|
|
42
|
+
records,
|
|
43
|
+
}: {
|
|
44
|
+
memory: RunMemory
|
|
45
|
+
records: EngineRecord[]
|
|
46
|
+
}): { rows: BoardRow[]; taken: number } => {
|
|
47
|
+
const rows: BoardRow[] = []
|
|
48
|
+
let taken = 0
|
|
49
|
+
if (memory.unsent_rows.size === 0 || records.length === 0) {
|
|
50
|
+
return { rows, taken }
|
|
51
|
+
}
|
|
52
|
+
const lowest = Math.min(...records.map((record) => record.seq))
|
|
53
|
+
const sent = new Set(records.map((record) => record.seq))
|
|
54
|
+
for (const [seq, seq_rows] of memory.unsent_rows) {
|
|
55
|
+
if (seq < lowest) {
|
|
56
|
+
memory.unsent_rows.delete(seq)
|
|
57
|
+
} else if (sent.has(seq)) {
|
|
58
|
+
rows.push(...seq_rows)
|
|
59
|
+
memory.unsent_rows.delete(seq)
|
|
60
|
+
taken += 1
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { rows, taken }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* `engine.event`: applies a run's journal records in seq order from its
|
|
68
|
+
* `next_seq`. Lower seqs are duplicates and skipped (but a record rebuilt
|
|
69
|
+
* from disk adds its rows the first time the engine sends it); a gap
|
|
70
|
+
* stops there and the reply's `next_seq` asks the engine to resend from it.
|
|
71
|
+
*/
|
|
72
|
+
const handleEngineEvent = async (
|
|
73
|
+
input: EngineEventInput
|
|
74
|
+
): Promise<EngineEventOutput> => {
|
|
75
|
+
await ready
|
|
76
|
+
// A run this plugin didn't start is read-only: it isn't in `runs`.
|
|
77
|
+
const memory = runs.get(input.run_id)
|
|
78
|
+
if (!memory) {
|
|
79
|
+
return {
|
|
80
|
+
ok: false,
|
|
81
|
+
next_seq: 0,
|
|
82
|
+
message: `The board doesn't know run ${input.run_id}.`,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!sameToken({ left: input.token, right: memory.entry.token })) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
next_seq: 0,
|
|
89
|
+
message: `Wrong token for run ${input.run_id}.`,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return enqueue({
|
|
93
|
+
run_id: input.run_id,
|
|
94
|
+
work: async () => {
|
|
95
|
+
const unsent = takeUnsentRows({
|
|
96
|
+
memory,
|
|
97
|
+
records: input.records,
|
|
98
|
+
})
|
|
99
|
+
const {
|
|
100
|
+
rows: new_rows,
|
|
101
|
+
applied,
|
|
102
|
+
gap,
|
|
103
|
+
} = applyInOrder({
|
|
104
|
+
memory,
|
|
105
|
+
records: input.records,
|
|
106
|
+
})
|
|
107
|
+
const rows = [...unsent.rows, ...[...new_rows.values()].flat()]
|
|
108
|
+
if (gap === null && input.ended) {
|
|
109
|
+
memory.state = applyEnded({
|
|
110
|
+
state: memory.state,
|
|
111
|
+
ended: input.ended,
|
|
112
|
+
})
|
|
113
|
+
memory.unsent_rows.clear()
|
|
114
|
+
updateEntry({ memory, change: { ended: input.ended } })
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
unsent.taken > 0 ||
|
|
118
|
+
applied > 0 ||
|
|
119
|
+
(gap === null && input.ended)
|
|
120
|
+
) {
|
|
121
|
+
rows.push(header({ memory }))
|
|
122
|
+
}
|
|
123
|
+
await appendRows({ memory, rows })
|
|
124
|
+
const message =
|
|
125
|
+
gap === null
|
|
126
|
+
? `Applied ${applied} records.`
|
|
127
|
+
: `Applied ${applied} records, then found seq ${gap} while waiting for ${memory.next_seq}: resend from ${memory.next_seq}.`
|
|
128
|
+
return { ok: true, next_seq: memory.next_seq, message }
|
|
129
|
+
},
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { handleEngineEvent }
|
|
134
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the side panel reads and posts: `board.read` (the runs started from
|
|
3
|
+
* a workspace's chats and the runs the plugin didn't start, read from their
|
|
4
|
+
* journals), and `reply.post`.
|
|
5
|
+
*/
|
|
6
|
+
import { resolve } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { z } from 'zod'
|
|
9
|
+
|
|
10
|
+
import type { RunStore } from './board-server-store'
|
|
11
|
+
import type { BoardServerConfig, OutsideRun } from './board-server-types'
|
|
12
|
+
import { readLiveFile, withLive } from './live-file'
|
|
13
|
+
import { createBoardState } from './reduce-board'
|
|
14
|
+
import type { ReplyRun } from './reply-post'
|
|
15
|
+
import { listJournals } from './run-journals'
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
BoardReadInput,
|
|
19
|
+
BoardReadOutput,
|
|
20
|
+
EngineRecord,
|
|
21
|
+
ReplyPostInput,
|
|
22
|
+
ReplyPostOutput,
|
|
23
|
+
} from '../shared/board-rpc'
|
|
24
|
+
import { needsEngine } from '../shared/board-state'
|
|
25
|
+
|
|
26
|
+
const RunRepoSchema = z.object({ repo: z.string().min(1) })
|
|
27
|
+
|
|
28
|
+
/** The repo a run's `run_started` record names, if any. */
|
|
29
|
+
const repoOf = ({ records }: { records: EngineRecord[] }): string | null => {
|
|
30
|
+
const started = records.find((record) => record.kind === 'run_started')
|
|
31
|
+
const parsed = RunRepoSchema.safeParse(started?.content)
|
|
32
|
+
return parsed.success ? resolve(parsed.data.repo) : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** How many runs the plugin didn't start it shows, the latest changed first. */
|
|
36
|
+
const OUTSIDE_RUNS_SHOWN = 50
|
|
37
|
+
|
|
38
|
+
export const createBoardReads = ({
|
|
39
|
+
config,
|
|
40
|
+
store,
|
|
41
|
+
}: {
|
|
42
|
+
config: BoardServerConfig
|
|
43
|
+
store: RunStore
|
|
44
|
+
}) => {
|
|
45
|
+
const { runs_dir = null } = config
|
|
46
|
+
const { runs, poster, applyInOrder, journalOf, ready } = store
|
|
47
|
+
const outside = new Map<string, OutsideRun>()
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reads the journals of runs this plugin didn't start, the latest
|
|
51
|
+
* changed first: a new or changed journal is read again and its new
|
|
52
|
+
* records applied.
|
|
53
|
+
*/
|
|
54
|
+
const readOutside = async () => {
|
|
55
|
+
if (runs_dir === null) return
|
|
56
|
+
const journals = (await listJournals({ runs_dir }))
|
|
57
|
+
.filter((journal) => !runs.has(journal.run_id))
|
|
58
|
+
.slice(0, OUTSIDE_RUNS_SHOWN)
|
|
59
|
+
const shown = new Set(journals.map((journal) => journal.run_id))
|
|
60
|
+
for (const run_id of outside.keys()) {
|
|
61
|
+
if (!shown.has(run_id)) outside.delete(run_id)
|
|
62
|
+
}
|
|
63
|
+
await Promise.all(
|
|
64
|
+
journals.map(async ({ run_id, size, changed_ms }) => {
|
|
65
|
+
const known = outside.get(run_id)
|
|
66
|
+
if (known?.size === size && known.changed_ms === changed_ms) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
const records = await journalOf({ run_id })
|
|
70
|
+
const first = records[0]
|
|
71
|
+
if (!first) return
|
|
72
|
+
const run: OutsideRun = known ?? {
|
|
73
|
+
state: createBoardState({
|
|
74
|
+
run_id,
|
|
75
|
+
spec_number: null,
|
|
76
|
+
demo: false,
|
|
77
|
+
started_at: first.time,
|
|
78
|
+
log_path: null,
|
|
79
|
+
}),
|
|
80
|
+
next_seq: 1,
|
|
81
|
+
size,
|
|
82
|
+
changed_ms,
|
|
83
|
+
repo: null,
|
|
84
|
+
engine_gone: false,
|
|
85
|
+
}
|
|
86
|
+
applyInOrder({ memory: run, records })
|
|
87
|
+
run.size = size
|
|
88
|
+
run.changed_ms = changed_ms
|
|
89
|
+
run.repo ??= repoOf({ records })
|
|
90
|
+
outside.set(run_id, run)
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `board.read`: the runs started from the workspace's chats and the runs
|
|
97
|
+
* this plugin didn't start whose repo is the workspace's folder, newest
|
|
98
|
+
* first, and one of them in full, with its live file laid over it.
|
|
99
|
+
*/
|
|
100
|
+
const readBoard = async (
|
|
101
|
+
input: BoardReadInput
|
|
102
|
+
): Promise<BoardReadOutput> => {
|
|
103
|
+
await ready
|
|
104
|
+
await readOutside()
|
|
105
|
+
const directory = input.directory ? resolve(input.directory) : null
|
|
106
|
+
const list = [
|
|
107
|
+
...[...runs.values()].filter(
|
|
108
|
+
(memory) => memory.entry.workspace_id === input.workspace_id
|
|
109
|
+
),
|
|
110
|
+
...[...outside.values()]
|
|
111
|
+
.filter((run) => directory !== null && run.repo === directory)
|
|
112
|
+
.map((run) => ({
|
|
113
|
+
state: {
|
|
114
|
+
...run.state,
|
|
115
|
+
engine_gone:
|
|
116
|
+
run.engine_gone &&
|
|
117
|
+
needsEngine({ state: run.state }),
|
|
118
|
+
},
|
|
119
|
+
})),
|
|
120
|
+
]
|
|
121
|
+
.map(({ state }) => state)
|
|
122
|
+
.toSorted((left, right) =>
|
|
123
|
+
right.run.started_at === left.run.started_at
|
|
124
|
+
? right.run.run_id.localeCompare(left.run.run_id)
|
|
125
|
+
: right.run.started_at.localeCompare(left.run.started_at)
|
|
126
|
+
)
|
|
127
|
+
const selected =
|
|
128
|
+
(input.run_id
|
|
129
|
+
? list.find((state) => state.run.run_id === input.run_id)
|
|
130
|
+
: undefined) ?? list[0]
|
|
131
|
+
const selected_id = selected?.run.run_id ?? null
|
|
132
|
+
const live =
|
|
133
|
+
runs_dir === null || selected_id === null
|
|
134
|
+
? null
|
|
135
|
+
: await readLiveFile({ runs_dir, run_id: selected_id })
|
|
136
|
+
return {
|
|
137
|
+
runs: list.map((state) => ({
|
|
138
|
+
run_id: state.run.run_id,
|
|
139
|
+
spec_number: state.run.spec_number,
|
|
140
|
+
spec_title: state.run.spec_title,
|
|
141
|
+
demo: state.run.demo,
|
|
142
|
+
status: state.run.status,
|
|
143
|
+
started_at: state.run.started_at,
|
|
144
|
+
needs_you: state.needs_you.length,
|
|
145
|
+
})),
|
|
146
|
+
selected:
|
|
147
|
+
selected === undefined
|
|
148
|
+
? null
|
|
149
|
+
: withLive({ state: selected, live }),
|
|
150
|
+
chat_agent_id:
|
|
151
|
+
selected_id === null
|
|
152
|
+
? null
|
|
153
|
+
: (runs.get(selected_id)?.entry.agent_id ?? null),
|
|
154
|
+
posted:
|
|
155
|
+
selected_id === null
|
|
156
|
+
? []
|
|
157
|
+
: poster.postedOf({ run_id: selected_id }),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* `reply.post` (#503): posts a reply button's word on the run's spec
|
|
163
|
+
* issue. Works for runs this plugin started and for runs it only reads
|
|
164
|
+
* (their journal is read again first); see `createReplyPoster`.
|
|
165
|
+
*/
|
|
166
|
+
const postReply = async (
|
|
167
|
+
input: ReplyPostInput
|
|
168
|
+
): Promise<ReplyPostOutput> => {
|
|
169
|
+
await ready
|
|
170
|
+
await readOutside()
|
|
171
|
+
const memory = runs.get(input.run_id)
|
|
172
|
+
const other = outside.get(input.run_id)
|
|
173
|
+
const run: ReplyRun | null = memory
|
|
174
|
+
? {
|
|
175
|
+
state: memory.state,
|
|
176
|
+
repo_dir: memory.entry.repo,
|
|
177
|
+
next_seq: memory.next_seq,
|
|
178
|
+
}
|
|
179
|
+
: other
|
|
180
|
+
? {
|
|
181
|
+
state: other.state,
|
|
182
|
+
repo_dir: other.repo,
|
|
183
|
+
next_seq: other.next_seq,
|
|
184
|
+
}
|
|
185
|
+
: null
|
|
186
|
+
return poster.postReply({ run, input })
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { outside, readOutside, readBoard, postReply }
|
|
190
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/luca-run resume <run id>` (#493): going on with a run this plugin
|
|
3
|
+
* started, from its journal.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes } from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
import type { EngineProcess } from './board-server-engine'
|
|
8
|
+
import type { RunStore } from './board-server-store'
|
|
9
|
+
import type { BoardServerConfig } from './board-server-types'
|
|
10
|
+
import { errorText } from './board-server-util'
|
|
11
|
+
import { liveRunIds, RESUMED_TEXT, resumeRow } from './engine-watch'
|
|
12
|
+
|
|
13
|
+
import type { RunStartInput, RunStartOutput } from '../shared/board-rpc'
|
|
14
|
+
import { OVER_PHASES } from '../shared/board-state'
|
|
15
|
+
|
|
16
|
+
export const createResumer = ({
|
|
17
|
+
config,
|
|
18
|
+
store,
|
|
19
|
+
engine,
|
|
20
|
+
}: {
|
|
21
|
+
config: BoardServerConfig
|
|
22
|
+
store: RunStore
|
|
23
|
+
engine: EngineProcess
|
|
24
|
+
}) => {
|
|
25
|
+
const { list_processes, log, now } = config
|
|
26
|
+
const { runs, enqueue, appendRows, header, updateEntry } = store
|
|
27
|
+
const { findEngine, exclusive, spawnResume } = engine
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `/luca-run resume <run id>` (#493): goes on with a run this plugin
|
|
31
|
+
* started, from its journal, with the board attached again. Refused for
|
|
32
|
+
* an unknown run, a demo, a run that is over, a run whose engine is
|
|
33
|
+
* still running, or when `ps` fails. Otherwise the run gets a new token,
|
|
34
|
+
* loses its `ended`, and starts its restart count again at 0; it keeps
|
|
35
|
+
* its chat, workspace, repo, and log. The engine is spawned with
|
|
36
|
+
* `--resume`, and the run's chat gets a row saying so.
|
|
37
|
+
*/
|
|
38
|
+
const resumeRun = async ({
|
|
39
|
+
run_id,
|
|
40
|
+
input,
|
|
41
|
+
}: {
|
|
42
|
+
run_id: string
|
|
43
|
+
input: RunStartInput
|
|
44
|
+
}): Promise<RunStartOutput> => {
|
|
45
|
+
const refuse = (message: string): RunStartOutput => ({
|
|
46
|
+
ok: false,
|
|
47
|
+
message,
|
|
48
|
+
run_id: null,
|
|
49
|
+
})
|
|
50
|
+
const memory = runs.get(run_id)
|
|
51
|
+
if (!memory) {
|
|
52
|
+
return refuse(
|
|
53
|
+
`The board doesn't know run ${run_id}, so it can't resume it. Check the id: the run's header row and the board panel show it. A run the board didn't start goes on from a terminal with luca-run --resume ${run_id}, without the board.`
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
if (memory.entry.demo) {
|
|
57
|
+
return refuse(
|
|
58
|
+
`Run ${run_id} is a demo, and a demo can't be picked up again. Start a new one with /luca-run demo.`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
if (OVER_PHASES.has(memory.state.run.phase)) {
|
|
62
|
+
return refuse(
|
|
63
|
+
`Run ${run_id} is over, so there is nothing to resume.`
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
const engine = await findEngine()
|
|
67
|
+
if (!engine.ok) return refuse(engine.message)
|
|
68
|
+
|
|
69
|
+
return exclusive(async () => {
|
|
70
|
+
let command_lines: string[]
|
|
71
|
+
try {
|
|
72
|
+
command_lines = await list_processes()
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return refuse(
|
|
75
|
+
`Couldn't check whether run ${run_id}'s engine is still running (${errorText({ error })}), so it wasn't resumed: two engines on one run would be far worse. Try again.`
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
if (liveRunIds({ command_lines }).has(run_id)) {
|
|
79
|
+
return refuse(
|
|
80
|
+
`Run ${run_id} is still running (its engine is live), so there is nothing to resume.`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
return enqueue({
|
|
84
|
+
run_id,
|
|
85
|
+
work: async (): Promise<RunStartOutput> => {
|
|
86
|
+
const before = memory.entry
|
|
87
|
+
// The new token is kept first: the engine may send
|
|
88
|
+
// before the spawn returns.
|
|
89
|
+
updateEntry({
|
|
90
|
+
memory,
|
|
91
|
+
change: {
|
|
92
|
+
token: randomBytes(24).toString('hex'),
|
|
93
|
+
ended: null,
|
|
94
|
+
restarts: 0,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
let pid: number | null = null
|
|
98
|
+
try {
|
|
99
|
+
pid = spawnResume({
|
|
100
|
+
memory,
|
|
101
|
+
engine,
|
|
102
|
+
on_failed: 'the resumed engine failed',
|
|
103
|
+
})
|
|
104
|
+
} catch (error) {
|
|
105
|
+
updateEntry({
|
|
106
|
+
memory,
|
|
107
|
+
change: {
|
|
108
|
+
token: before.token,
|
|
109
|
+
ended: before.ended,
|
|
110
|
+
restarts: before.restarts,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
return refuse(
|
|
114
|
+
`Couldn't start the engine: ${errorText({ error })}`
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
const { log_path, spec, agent_id } = memory.entry
|
|
118
|
+
log(
|
|
119
|
+
`[${run_id}] resumed with /luca-run resume: ${engine.command} with --resume (pid ${pid ?? '?'}), log ${log_path}`
|
|
120
|
+
)
|
|
121
|
+
memory.rows_stopped = false
|
|
122
|
+
memory.append_failures = 0
|
|
123
|
+
memory.state = {
|
|
124
|
+
...memory.state,
|
|
125
|
+
run: { ...memory.state.run, engine_ended: null },
|
|
126
|
+
latest: RESUMED_TEXT,
|
|
127
|
+
latest_at: now().toISOString(),
|
|
128
|
+
}
|
|
129
|
+
void enqueue({
|
|
130
|
+
run_id,
|
|
131
|
+
work: () =>
|
|
132
|
+
appendRows({
|
|
133
|
+
memory,
|
|
134
|
+
rows: [
|
|
135
|
+
resumeRow({
|
|
136
|
+
run_id,
|
|
137
|
+
time: now().toISOString(),
|
|
138
|
+
}),
|
|
139
|
+
header({ memory }),
|
|
140
|
+
],
|
|
141
|
+
}),
|
|
142
|
+
})
|
|
143
|
+
const what =
|
|
144
|
+
spec === null
|
|
145
|
+
? `run ${run_id}`
|
|
146
|
+
: `spec #${spec} (run ${run_id})`
|
|
147
|
+
const where =
|
|
148
|
+
agent_id === input.agent_id
|
|
149
|
+
? ''
|
|
150
|
+
: ' Its rows go on in the chat it started in.'
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
run_id,
|
|
154
|
+
message: `Resumed ${what} from its journal.${where} Its log is ${log_path}.`,
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { resumeRun }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export type Resumer = ReturnType<typeof createResumer>
|