@alecsibilia/luca 14.0.0-alpha.28 → 14.0.0-alpha.29
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 +4 -0
- package/board/client/board-look.ts +3 -0
- package/board/package.json +1 -1
- package/board/server/board-server-check.ts +24 -5
- package/board/server/board-server-follow.ts +143 -0
- package/board/server/board-server-read.ts +50 -23
- package/board/server/board-server-start.ts +1 -0
- package/board/server/board-server-store.ts +32 -8
- package/board/server/board-server-types.ts +11 -3
- package/board/server/board-server.ts +11 -6
- package/board/server/board-vocabulary-run.ts +4 -0
- package/board/server/board-vocabulary.ts +2 -0
- package/board/server/describe-record.ts +4 -1
- package/board/server/engine-watch.ts +33 -2
- package/board/server/luca-version.ts +1 -1
- package/board/server/reduce-board-create.ts +1 -0
- package/board/server/reduce-board-run.ts +23 -1
- package/board/server/reduce-board-settle.ts +3 -0
- package/board/server/repo-match.ts +74 -0
- package/board/server/run-registry.ts +12 -4
- package/board/shared/board-state-run.ts +1 -0
- package/board/shared/board-state.ts +6 -0
- package/engine/board/board-sync.ts +7 -3
- package/engine/cli/live-runs.ts +120 -6
- package/engine/cli/luca-run.ts +12 -2
- package/engine/cli/run-modes.ts +1 -0
- package/engine/core/execute-pull-request.ts +11 -3
- package/engine/core/execute.ts +29 -6
- package/engine/core/network-wait.ts +88 -0
- package/engine/journal/journal-entry-schema.ts +6 -0
- package/engine/journal/journal-kinds.ts +2 -0
- package/engine/journal/journal-record-schema.ts +6 -0
- package/engine/journal/network-wait-entries.ts +29 -0
- package/engine/journal/replay.ts +4 -0
- package/engine/journal/ticket-replay.ts +2 -0
- package/engine/tracker/gh-retry.ts +38 -5
- package/engine/tracker/tracker.ts +30 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -144,6 +144,7 @@ export const statusColor = ({
|
|
|
144
144
|
return theme.colors.statusDanger
|
|
145
145
|
case 'limit_wait':
|
|
146
146
|
case 'usage_line_wait':
|
|
147
|
+
case 'network_wait':
|
|
147
148
|
return theme.colors.statusWarning
|
|
148
149
|
case 'starting':
|
|
149
150
|
case 'intake':
|
|
@@ -195,6 +196,8 @@ export const statusText = ({ status }: { status: RunStatus }): string => {
|
|
|
195
196
|
return 'limit wait'
|
|
196
197
|
case 'usage_line_wait':
|
|
197
198
|
return 'paused at the usage line'
|
|
199
|
+
case 'network_wait':
|
|
200
|
+
return 'Waiting for GitHub to be reachable'
|
|
198
201
|
case 'done':
|
|
199
202
|
return 'done'
|
|
200
203
|
case 'refused':
|
package/board/package.json
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* The engine check: finds runs whose engine process is gone although the run
|
|
3
3
|
* hasn't ended, restarts the ones that can go on from their journal, and
|
|
4
4
|
* marks the rest as stopped, with why. The runs the plugin didn't start are
|
|
5
|
-
* only labelled (#635), never restarted.
|
|
5
|
+
* only labelled (#635), never restarted. A run marked ended whose journal
|
|
6
|
+
* grew and that has a live engine again comes back (#701).
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import type { EngineProcess } from './board-server-engine'
|
|
10
|
+
import type { JournalFollow } from './board-server-follow'
|
|
9
11
|
import type { RunStore } from './board-server-store'
|
|
10
12
|
import type {
|
|
11
13
|
BoardServerConfig,
|
|
@@ -34,15 +36,17 @@ export const createEngineChecks = ({
|
|
|
34
36
|
store,
|
|
35
37
|
engine,
|
|
36
38
|
reads,
|
|
39
|
+
follow,
|
|
37
40
|
}: {
|
|
38
41
|
config: BoardServerConfig
|
|
39
42
|
store: RunStore
|
|
40
43
|
engine: EngineProcess
|
|
41
|
-
/** The runs the plugin didn't start, and how to read
|
|
44
|
+
/** The runs the plugin didn't start, and how to read every journal again. */
|
|
42
45
|
reads: {
|
|
43
46
|
outside: Map<string, OutsideRun>
|
|
44
|
-
|
|
47
|
+
readJournals: (args?: { release_held?: boolean }) => Promise<void>
|
|
45
48
|
}
|
|
49
|
+
follow: JournalFollow
|
|
46
50
|
}) => {
|
|
47
51
|
const {
|
|
48
52
|
list_processes,
|
|
@@ -171,7 +175,8 @@ export const createEngineChecks = ({
|
|
|
171
175
|
/** One check: see `checkEngines`. */
|
|
172
176
|
const runCheck = async (): Promise<EngineCheck> => {
|
|
173
177
|
await ready
|
|
174
|
-
|
|
178
|
+
// Rows held since the plugin loaded stop waiting for a push (#701).
|
|
179
|
+
await reads.readJournals({ release_held: true })
|
|
175
180
|
const summary: EngineCheck = { restarted: [], stopped: [] }
|
|
176
181
|
const candidates = [...runs.values()].filter((memory) =>
|
|
177
182
|
needsEngine({ state: memory.state })
|
|
@@ -179,7 +184,14 @@ export const createEngineChecks = ({
|
|
|
179
184
|
const others = [...reads.outside.values()].filter((run) =>
|
|
180
185
|
needsEngine({ state: run.state })
|
|
181
186
|
)
|
|
182
|
-
|
|
187
|
+
const returning = follow.returning()
|
|
188
|
+
if (
|
|
189
|
+
candidates.length === 0 &&
|
|
190
|
+
others.length === 0 &&
|
|
191
|
+
returning.length === 0
|
|
192
|
+
) {
|
|
193
|
+
return summary
|
|
194
|
+
}
|
|
183
195
|
let command_lines: string[]
|
|
184
196
|
try {
|
|
185
197
|
command_lines = await list_processes()
|
|
@@ -192,6 +204,13 @@ export const createEngineChecks = ({
|
|
|
192
204
|
return summary
|
|
193
205
|
}
|
|
194
206
|
const live = liveRunIds({ command_lines })
|
|
207
|
+
// An ended run whose journal grew and that has a live engine (such
|
|
208
|
+
// as `luca-run --resume`) is live again (#701).
|
|
209
|
+
await Promise.all(
|
|
210
|
+
returning
|
|
211
|
+
.filter((memory) => live.has(memory.entry.run_id))
|
|
212
|
+
.map((memory) => follow.comeBack({ memory }))
|
|
213
|
+
)
|
|
195
214
|
await labelOutside({ others, live })
|
|
196
215
|
const dead = candidates.filter(
|
|
197
216
|
(memory) => !live.has(memory.entry.run_id)
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Following the runs this plugin started on disk (#701): their journals are
|
|
3
|
+
* read again when they grow, so a run whose engine can't push (such as one
|
|
4
|
+
* resumed with `luca-run --resume`) still shows live. A run marked ended
|
|
5
|
+
* comes back when its journal grows and a live engine runs it.
|
|
6
|
+
*/
|
|
7
|
+
import type { RunStore } from './board-server-store'
|
|
8
|
+
import type { BoardServerConfig, RunMemory } from './board-server-types'
|
|
9
|
+
import { RESUMED_CLI_TEXT, resumedCliRow } from './engine-watch'
|
|
10
|
+
import { settle } from './reduce-board-settle'
|
|
11
|
+
import type { JournalFile } from './run-journals'
|
|
12
|
+
|
|
13
|
+
import type { BoardRow } from '../shared/board-rows'
|
|
14
|
+
|
|
15
|
+
export const createJournalFollow = ({
|
|
16
|
+
config,
|
|
17
|
+
store,
|
|
18
|
+
}: {
|
|
19
|
+
config: BoardServerConfig
|
|
20
|
+
store: RunStore
|
|
21
|
+
}) => {
|
|
22
|
+
const { log, now } = config
|
|
23
|
+
const {
|
|
24
|
+
runs,
|
|
25
|
+
enqueue,
|
|
26
|
+
applyInOrder,
|
|
27
|
+
appendRows,
|
|
28
|
+
header,
|
|
29
|
+
journalOf,
|
|
30
|
+
updateEntry,
|
|
31
|
+
} = store
|
|
32
|
+
|
|
33
|
+
/** Takes the rows a run still holds from its rebuild at plugin load. */
|
|
34
|
+
const takeHeld = ({ memory }: { memory: RunMemory }): BoardRow[] => {
|
|
35
|
+
const rows = [...memory.unsent_rows.values()].flat()
|
|
36
|
+
memory.unsent_rows.clear()
|
|
37
|
+
return rows
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reads again the journal of each run this plugin started that changed
|
|
42
|
+
* since the last read, and applies the records past its `next_seq`
|
|
43
|
+
* (the cursor pushes share), adding their rows to the chat at once.
|
|
44
|
+
* The rows a run still holds from its rebuild at plugin load
|
|
45
|
+
* (`unsent_rows`) go first, so they reach the chat even when no push
|
|
46
|
+
* comes: with the first rows read from disk, or on an engine check
|
|
47
|
+
* (`release_held`) after the first read since the load.
|
|
48
|
+
*/
|
|
49
|
+
const readFollowed = async ({
|
|
50
|
+
journals,
|
|
51
|
+
release_held = false,
|
|
52
|
+
}: {
|
|
53
|
+
journals: JournalFile[]
|
|
54
|
+
release_held?: boolean
|
|
55
|
+
}) => {
|
|
56
|
+
await Promise.all(
|
|
57
|
+
journals.map(({ run_id, size, changed_ms }) => {
|
|
58
|
+
const memory = runs.get(run_id)
|
|
59
|
+
if (!memory) return
|
|
60
|
+
const changed =
|
|
61
|
+
memory.size !== size || memory.changed_ms !== changed_ms
|
|
62
|
+
// The first read since the load leaves a push its chance.
|
|
63
|
+
const release =
|
|
64
|
+
release_held &&
|
|
65
|
+
memory.size !== null &&
|
|
66
|
+
memory.unsent_rows.size > 0
|
|
67
|
+
if (!changed && !release) return
|
|
68
|
+
return enqueue({
|
|
69
|
+
run_id,
|
|
70
|
+
work: async () => {
|
|
71
|
+
const { rows, applied } = applyInOrder({
|
|
72
|
+
memory,
|
|
73
|
+
records: changed ? await journalOf({ run_id }) : [],
|
|
74
|
+
})
|
|
75
|
+
memory.size = size
|
|
76
|
+
memory.changed_ms = changed_ms
|
|
77
|
+
const held =
|
|
78
|
+
applied > 0 || release ? takeHeld({ memory }) : []
|
|
79
|
+
if (applied === 0 && held.length === 0) return
|
|
80
|
+
await appendRows({
|
|
81
|
+
memory,
|
|
82
|
+
rows: [
|
|
83
|
+
...held,
|
|
84
|
+
...[...rows.values()].flat(),
|
|
85
|
+
header({ memory }),
|
|
86
|
+
],
|
|
87
|
+
})
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The runs marked ended whose journal grew past `ended_at_seq`. */
|
|
95
|
+
const returning = (): RunMemory[] =>
|
|
96
|
+
[...runs.values()].filter(
|
|
97
|
+
({ entry, next_seq }) =>
|
|
98
|
+
entry.ended !== null &&
|
|
99
|
+
entry.ended_at_seq !== null &&
|
|
100
|
+
next_seq - 1 > entry.ended_at_seq
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Brings back a run marked ended that has a live engine: clears its
|
|
105
|
+
* `ended`, re-adds its header, and says so in its chat. Its restart
|
|
106
|
+
* count is kept.
|
|
107
|
+
*/
|
|
108
|
+
const comeBack = ({ memory }: { memory: RunMemory }) =>
|
|
109
|
+
enqueue({
|
|
110
|
+
run_id: memory.entry.run_id,
|
|
111
|
+
work: async () => {
|
|
112
|
+
if (memory.entry.ended === null) return
|
|
113
|
+
const { run_id } = memory.entry
|
|
114
|
+
const seq = memory.next_seq - 1
|
|
115
|
+
updateEntry({ memory, change: { ended: null } })
|
|
116
|
+
const time = now().toISOString()
|
|
117
|
+
memory.rows_stopped = false
|
|
118
|
+
memory.append_failures = 0
|
|
119
|
+
memory.state = settle({
|
|
120
|
+
state: {
|
|
121
|
+
...memory.state,
|
|
122
|
+
run: { ...memory.state.run, engine_ended: null },
|
|
123
|
+
latest: RESUMED_CLI_TEXT,
|
|
124
|
+
latest_at: time,
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
log(
|
|
128
|
+
`[${run_id}] came back: its journal grew to seq ${seq} and a live engine runs it.`
|
|
129
|
+
)
|
|
130
|
+
await appendRows({
|
|
131
|
+
memory,
|
|
132
|
+
rows: [
|
|
133
|
+
resumedCliRow({ run_id, seq, time }),
|
|
134
|
+
header({ memory }),
|
|
135
|
+
],
|
|
136
|
+
})
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return { readFollowed, returning, comeBack }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export type JournalFollow = ReturnType<typeof createJournalFollow>
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
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,
|
|
4
|
-
* journals), and `reply.post`.
|
|
3
|
+
* a workspace's chats and the runs the plugin didn't start, all followed on
|
|
4
|
+
* disk through their journals), and `reply.post`.
|
|
5
5
|
*/
|
|
6
6
|
import { resolve } from 'node:path'
|
|
7
7
|
|
|
8
8
|
import { z } from 'zod'
|
|
9
9
|
|
|
10
|
+
import type { JournalFollow } from './board-server-follow'
|
|
10
11
|
import type { RunStore } from './board-server-store'
|
|
11
12
|
import type { BoardServerConfig, OutsideRun } from './board-server-types'
|
|
12
13
|
import { readLiveFile, withLive } from './live-file'
|
|
13
14
|
import { createBoardState } from './reduce-board'
|
|
14
15
|
import type { ReplyRun } from './reply-post'
|
|
16
|
+
import { createRepoMatch } from './repo-match'
|
|
15
17
|
import { listJournals } from './run-journals'
|
|
16
18
|
|
|
17
19
|
import type {
|
|
@@ -38,22 +40,31 @@ const OUTSIDE_RUNS_SHOWN = 50
|
|
|
38
40
|
export const createBoardReads = ({
|
|
39
41
|
config,
|
|
40
42
|
store,
|
|
43
|
+
follow,
|
|
41
44
|
}: {
|
|
42
45
|
config: BoardServerConfig
|
|
43
46
|
store: RunStore
|
|
47
|
+
follow: JournalFollow
|
|
44
48
|
}) => {
|
|
45
|
-
const { runs_dir = null } = config
|
|
49
|
+
const { runs_dir = null, run_command, env, home_dir } = config
|
|
46
50
|
const { runs, poster, applyInOrder, journalOf, ready } = store
|
|
47
51
|
const outside = new Map<string, OutsideRun>()
|
|
52
|
+
const { sameRepo } = createRepoMatch({ run_command, env, home_dir })
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
|
-
* Reads the journals
|
|
51
|
-
*
|
|
52
|
-
*
|
|
55
|
+
* Reads the journals on disk, the latest changed first: a new or changed
|
|
56
|
+
* journal is read again and its new records applied. The runs this
|
|
57
|
+
* plugin started are all followed (see `readFollowed`, which
|
|
58
|
+
* `release_held` passes to); of the others, the latest
|
|
59
|
+
* `OUTSIDE_RUNS_SHOWN`.
|
|
53
60
|
*/
|
|
54
|
-
const
|
|
61
|
+
const readJournals = async ({
|
|
62
|
+
release_held = false,
|
|
63
|
+
}: { release_held?: boolean } = {}) => {
|
|
55
64
|
if (runs_dir === null) return
|
|
56
|
-
const
|
|
65
|
+
const all = await listJournals({ runs_dir })
|
|
66
|
+
await follow.readFollowed({ journals: all, release_held })
|
|
67
|
+
const journals = all
|
|
57
68
|
.filter((journal) => !runs.has(journal.run_id))
|
|
58
69
|
.slice(0, OUTSIDE_RUNS_SHOWN)
|
|
59
70
|
const shown = new Set(journals.map((journal) => journal.run_id))
|
|
@@ -92,31 +103,47 @@ export const createBoardReads = ({
|
|
|
92
103
|
)
|
|
93
104
|
}
|
|
94
105
|
|
|
106
|
+
/** The runs this plugin didn't start that are in `directory`'s repo. */
|
|
107
|
+
const outsideIn = async ({
|
|
108
|
+
directory,
|
|
109
|
+
}: {
|
|
110
|
+
directory: string | null
|
|
111
|
+
}): Promise<OutsideRun[]> => {
|
|
112
|
+
if (directory === null) return []
|
|
113
|
+
const others = [...outside.values()]
|
|
114
|
+
const shown = await Promise.all(
|
|
115
|
+
others.map((run) =>
|
|
116
|
+
run.repo === null
|
|
117
|
+
? false
|
|
118
|
+
: sameRepo({ directory, repo: run.repo })
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return others.filter((_, index) => shown[index])
|
|
122
|
+
}
|
|
123
|
+
|
|
95
124
|
/**
|
|
96
125
|
* `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
|
|
98
|
-
* first, and one of them in full, with its live file laid
|
|
126
|
+
* this plugin didn't start whose repo is in the workspace folder's git
|
|
127
|
+
* repo, newest first, and one of them in full, with its live file laid
|
|
128
|
+
* over it.
|
|
99
129
|
*/
|
|
100
130
|
const readBoard = async (
|
|
101
131
|
input: BoardReadInput
|
|
102
132
|
): Promise<BoardReadOutput> => {
|
|
103
133
|
await ready
|
|
104
|
-
await
|
|
134
|
+
await readJournals()
|
|
105
135
|
const directory = input.directory ? resolve(input.directory) : null
|
|
106
136
|
const list = [
|
|
107
137
|
...[...runs.values()].filter(
|
|
108
138
|
(memory) => memory.entry.workspace_id === input.workspace_id
|
|
109
139
|
),
|
|
110
|
-
...
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
needsEngine({ state: run.state }),
|
|
118
|
-
},
|
|
119
|
-
})),
|
|
140
|
+
...(await outsideIn({ directory })).map((run) => ({
|
|
141
|
+
state: {
|
|
142
|
+
...run.state,
|
|
143
|
+
engine_gone:
|
|
144
|
+
run.engine_gone && needsEngine({ state: run.state }),
|
|
145
|
+
},
|
|
146
|
+
})),
|
|
120
147
|
]
|
|
121
148
|
.map(({ state }) => state)
|
|
122
149
|
.toSorted((left, right) =>
|
|
@@ -167,7 +194,7 @@ export const createBoardReads = ({
|
|
|
167
194
|
input: ReplyPostInput
|
|
168
195
|
): Promise<ReplyPostOutput> => {
|
|
169
196
|
await ready
|
|
170
|
-
await
|
|
197
|
+
await readJournals()
|
|
171
198
|
const memory = runs.get(input.run_id)
|
|
172
199
|
const other = outside.get(input.run_id)
|
|
173
200
|
const run: ReplyRun | null = memory
|
|
@@ -186,5 +213,5 @@ export const createBoardReads = ({
|
|
|
186
213
|
return poster.postReply({ run, input })
|
|
187
214
|
}
|
|
188
215
|
|
|
189
|
-
return { outside,
|
|
216
|
+
return { outside, readJournals, readBoard, postReply }
|
|
190
217
|
}
|
|
@@ -172,7 +172,8 @@ export const createRunStore = ({ config }: { config: BoardServerConfig }) => {
|
|
|
172
172
|
/**
|
|
173
173
|
* Knows a run this plugin started, rebuilt from its journal `records`.
|
|
174
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`)
|
|
175
|
+
* kept until it sends those records again (see `handleEngineEvent`) or
|
|
176
|
+
* the disk follow adds them (see `readFollowed`).
|
|
176
177
|
*/
|
|
177
178
|
const remember = ({
|
|
178
179
|
entry,
|
|
@@ -194,6 +195,8 @@ export const createRunStore = ({ config }: { config: BoardServerConfig }) => {
|
|
|
194
195
|
append_failures: 0,
|
|
195
196
|
rows_stopped: false,
|
|
196
197
|
unsent_rows: new Map(),
|
|
198
|
+
size: null,
|
|
199
|
+
changed_ms: null,
|
|
197
200
|
}
|
|
198
201
|
const { rows } = applyInOrder({ memory, records })
|
|
199
202
|
// A run whose engine ended stays ended after a plugin restart.
|
|
@@ -221,23 +224,44 @@ export const createRunStore = ({ config }: { config: BoardServerConfig }) => {
|
|
|
221
224
|
const journals = await Promise.all(
|
|
222
225
|
entries.map(({ run_id }) => journalOf({ run_id }))
|
|
223
226
|
)
|
|
224
|
-
entries.forEach((entry, index) =>
|
|
225
|
-
remember({ entry, records: journals[index] ?? [] })
|
|
226
|
-
|
|
227
|
+
entries.forEach((entry, index) => {
|
|
228
|
+
const memory = remember({ entry, records: journals[index] ?? [] })
|
|
229
|
+
// An entry ended before `ended_at_seq` existed ended at its
|
|
230
|
+
// journal's newest seq.
|
|
231
|
+
if (entry.ended && entry.ended_at_seq === null) {
|
|
232
|
+
updateEntry({
|
|
233
|
+
memory,
|
|
234
|
+
change: { ended_at_seq: memory.next_seq - 1 },
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
})
|
|
227
238
|
}
|
|
228
239
|
|
|
229
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* Changes a run's registry entry, and its copy in memory. Setting
|
|
242
|
+
* `ended` also sets `ended_at_seq` to the newest seq applied; clearing it
|
|
243
|
+
* clears that too.
|
|
244
|
+
*/
|
|
230
245
|
const updateEntry = ({
|
|
231
246
|
memory,
|
|
232
247
|
change,
|
|
233
248
|
}: {
|
|
234
249
|
memory: RunMemory
|
|
235
|
-
change: Partial<
|
|
250
|
+
change: Partial<
|
|
251
|
+
Pick<RunEntry, 'ended' | 'ended_at_seq' | 'restarts' | 'token'>
|
|
252
|
+
>
|
|
236
253
|
}) => {
|
|
254
|
+
const full =
|
|
255
|
+
change.ended === undefined
|
|
256
|
+
? change
|
|
257
|
+
: {
|
|
258
|
+
...change,
|
|
259
|
+
ended_at_seq: change.ended ? memory.next_seq - 1 : null,
|
|
260
|
+
}
|
|
237
261
|
memory.entry = registry.update({
|
|
238
262
|
run_id: memory.entry.run_id,
|
|
239
|
-
change,
|
|
240
|
-
}) ?? { ...memory.entry, ...
|
|
263
|
+
change: full,
|
|
264
|
+
}) ?? { ...memory.entry, ...full }
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
/** Marks a run's engine as ended (kept in the registry) and updates its header. */
|
|
@@ -68,11 +68,19 @@ export type RunMemory = Replay & {
|
|
|
68
68
|
append_failures: number
|
|
69
69
|
rows_stopped: boolean
|
|
70
70
|
/**
|
|
71
|
-
* The rows of records rebuilt from disk, by seq, that the
|
|
72
|
-
* have: the engine may have written them while the plugin
|
|
73
|
-
* Added when the engine sends those records again
|
|
71
|
+
* The rows of records rebuilt from disk at plugin load, by seq, that the
|
|
72
|
+
* chat may not have: the engine may have written them while the plugin
|
|
73
|
+
* was down. Added when the engine sends those records again, or, as an
|
|
74
|
+
* engine that can't push never does, by the disk follow (#701): with the
|
|
75
|
+
* first rows it reads, or on an engine check after its first read.
|
|
74
76
|
*/
|
|
75
77
|
unsent_rows: Map<number, BoardRow[]>
|
|
78
|
+
/**
|
|
79
|
+
* Its journal's size and change time when the board last read it from
|
|
80
|
+
* disk (#701), `null` before the first read; a change reads it again.
|
|
81
|
+
*/
|
|
82
|
+
size: number | null
|
|
83
|
+
changed_ms: number | null
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - `board-server-start.ts` and `board-server-resume.ts`: `run.start` and
|
|
8
8
|
* `/luca-run resume`.
|
|
9
9
|
* - `board-server-events.ts`: `engine.event`.
|
|
10
|
+
* - `board-server-follow.ts`: following the plugin's runs on disk, and an
|
|
11
|
+
* ended run that comes back.
|
|
10
12
|
* - `board-server-read.ts`: `board.read` and `reply.post`, and the runs the
|
|
11
13
|
* plugin didn't start.
|
|
12
14
|
* - `board-server-check.ts`: the engine check, which also labels the runs
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
import { createEngineChecks } from './board-server-check'
|
|
16
18
|
import { createEngineProcess } from './board-server-engine'
|
|
17
19
|
import { createEngineEvents } from './board-server-events'
|
|
20
|
+
import { createJournalFollow } from './board-server-follow'
|
|
18
21
|
import { createBoardReads } from './board-server-read'
|
|
19
22
|
import { createResumer } from './board-server-resume'
|
|
20
23
|
import { createStarter } from './board-server-start'
|
|
@@ -40,10 +43,11 @@ export type {
|
|
|
40
43
|
* checks (`checkEngines`) read processes through `list_processes` and ask
|
|
41
44
|
* the engine through `run_command`.
|
|
42
45
|
*
|
|
43
|
-
* With a `runs_dir`, every run is rebuilt from its journal there
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
46
|
+
* With a `runs_dir`, every run is rebuilt from its journal there on start,
|
|
47
|
+
* and followed there on each `board.read` and engine check: the runs this
|
|
48
|
+
* plugin started, and the runs it didn't start (shown read-only in each
|
|
49
|
+
* workspace in their repo). Without one, a restarted plugin knows its runs
|
|
50
|
+
* only from the registry and the engine's next send.
|
|
47
51
|
*
|
|
48
52
|
* Each run's `engine.event` work goes through its own queue, so rows never
|
|
49
53
|
* interleave. A failed row append is logged and never loses board state.
|
|
@@ -73,8 +77,9 @@ export const createBoardServer = (config: BoardServerConfig) => {
|
|
|
73
77
|
const resumer = createResumer({ config, store, engine })
|
|
74
78
|
const starter = createStarter({ config, store, engine, resumer })
|
|
75
79
|
const events = createEngineEvents({ store })
|
|
76
|
-
const
|
|
77
|
-
const
|
|
80
|
+
const follow = createJournalFollow({ config, store })
|
|
81
|
+
const reads = createBoardReads({ config, store, follow })
|
|
82
|
+
const checks = createEngineChecks({ config, store, engine, reads, follow })
|
|
78
83
|
|
|
79
84
|
/** Resolves when every queued piece of work (and any check) has finished. */
|
|
80
85
|
const idle = async () => {
|
|
@@ -52,6 +52,10 @@ export const RUN_VOCABULARY = {
|
|
|
52
52
|
}),
|
|
53
53
|
/** No earlier run on the repo is going anymore. */
|
|
54
54
|
repo_wait_ended: z.looseObject({}),
|
|
55
|
+
/** A step can't reach GitHub; the run waits for it (#701). */
|
|
56
|
+
network_wait_started: z.looseObject({ step: z.string() }),
|
|
57
|
+
/** The step's wait for GitHub is over: it worked, or the run gave up. */
|
|
58
|
+
network_wait_ended: z.looseObject({ step: z.string() }),
|
|
55
59
|
/** The engine started again on the run's journal, on Luca's `luca_version`. */
|
|
56
60
|
engine_resumed: z.looseObject({ luca_version: z.string() }),
|
|
57
61
|
/**
|
|
@@ -34,6 +34,8 @@ const BoardEntrySchema = z.discriminatedUnion('kind', [
|
|
|
34
34
|
entry({ kind: 'run_started' }),
|
|
35
35
|
entry({ kind: 'repo_wait_started' }),
|
|
36
36
|
entry({ kind: 'repo_wait_ended' }),
|
|
37
|
+
entry({ kind: 'network_wait_started' }),
|
|
38
|
+
entry({ kind: 'network_wait_ended' }),
|
|
37
39
|
entry({ kind: 'engine_resumed' }),
|
|
38
40
|
entry({ kind: 'config_reloaded' }),
|
|
39
41
|
entry({ kind: 'config_reload_refused' }),
|
|
@@ -376,9 +376,12 @@ export const describeRecord = ({
|
|
|
376
376
|
text: `The ${record.content.count} new ${record.content.count === 1 ? 'memory went' : 'memories went'} on the spec issue, since there is no PR.`,
|
|
377
377
|
tone: 'info',
|
|
378
378
|
})
|
|
379
|
-
// The repo wait shows as a banner on the panel (#632)
|
|
379
|
+
// The repo wait shows as a banner on the panel (#632), the network
|
|
380
|
+
// wait as the run's status (#701).
|
|
380
381
|
case 'repo_wait_started':
|
|
381
382
|
case 'repo_wait_ended':
|
|
383
|
+
case 'network_wait_started':
|
|
384
|
+
case 'network_wait_ended':
|
|
382
385
|
case 'intake_read':
|
|
383
386
|
case 'ticket_snapshot':
|
|
384
387
|
case 'baseline_tests':
|
|
@@ -134,8 +134,8 @@ export type StopWhy =
|
|
|
134
134
|
/**
|
|
135
135
|
* How the owner resumes run `run_id` with the board attached: the
|
|
136
136
|
* `/luca-run resume` slash command (see `resumeRow`). A bare
|
|
137
|
-
* `luca-run --resume`
|
|
138
|
-
*
|
|
137
|
+
* `luca-run --resume` runs without the board's token: the board follows it
|
|
138
|
+
* from its journal on disk, but the engine can't push to it.
|
|
139
139
|
*
|
|
140
140
|
* @example
|
|
141
141
|
* resumeHint({ run_id: 'r1' }) // 'type /luca-run resume r1 in a Paseo chat'
|
|
@@ -212,6 +212,37 @@ export const restartRow = ({
|
|
|
212
212
|
export const RESUMED_TEXT =
|
|
213
213
|
'The run was resumed from its journal with /luca-run resume.'
|
|
214
214
|
|
|
215
|
+
/** The words of the chat row for an ended run that came back (#701). */
|
|
216
|
+
export const RESUMED_CLI_TEXT = 'Resumed from the command line.'
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The chat row for an ended run that came back with a live engine the board
|
|
220
|
+
* didn't start, such as `luca-run --resume` (#701). Its id ends in the
|
|
221
|
+
* newest seq then, so a second comeback gets its own row.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* resumedCliRow({ run_id, seq: 1755, time: new Date().toISOString() })
|
|
225
|
+
* // id `${run_id}-resumed-cli-1755`
|
|
226
|
+
*/
|
|
227
|
+
export const resumedCliRow = ({
|
|
228
|
+
run_id,
|
|
229
|
+
seq,
|
|
230
|
+
time,
|
|
231
|
+
}: {
|
|
232
|
+
run_id: string
|
|
233
|
+
seq: number
|
|
234
|
+
time: string
|
|
235
|
+
}): BoardRow => ({
|
|
236
|
+
id: `${run_id}-resumed-cli-${seq}`,
|
|
237
|
+
kind: ROW_KIND.event,
|
|
238
|
+
data: {
|
|
239
|
+
time,
|
|
240
|
+
ticket: null,
|
|
241
|
+
text: RESUMED_CLI_TEXT,
|
|
242
|
+
tone: 'info',
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
|
|
215
246
|
/**
|
|
216
247
|
* The chat row for a run resumed with `/luca-run resume`; its id is unique
|
|
217
248
|
* per run and time, so each resume gets its own row.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** The Luca version of this board, stamped when the package was packed. */
|
|
2
|
-
export const LUCA_VERSION: string = "14.0.0-alpha.
|
|
2
|
+
export const LUCA_VERSION: string = "14.0.0-alpha.29"
|