@alecsibilia/luca 14.0.0-alpha.34 → 14.0.0-alpha.35
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/package.json +1 -1
- package/board/server/luca-version.ts +1 -1
- package/engine/core/execute-build.ts +3 -0
- package/engine/core/execute.ts +17 -3
- package/engine/core/live-file.ts +7 -0
- package/engine/testing/unstick-fixtures.ts +77 -0
- package/package.json +3 -3
- package/skills/luca-unstick/scripts/stuck-records.ts +82 -1
- package/skills/luca-unstick/scripts/stuck-summary.ts +11 -1
- package/skills/luca-watch/SKILL.md +1 -1
- package/skills/luca-watch/scripts/wait-for-change.ts +64 -3
package/CHANGELOG.md
CHANGED
package/board/package.json
CHANGED
|
@@ -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.35"
|
|
@@ -275,6 +275,9 @@ export const executeBuildAction = async ({
|
|
|
275
275
|
exclusive: action.exclusive,
|
|
276
276
|
})
|
|
277
277
|
case 'join_run_branch': {
|
|
278
|
+
// Already joined: its commits are on the run branch, and its
|
|
279
|
+
// join keeps the record of which ones are its own (#719).
|
|
280
|
+
if (state.tickets[action.ticket]?.joined?.ok === true) return
|
|
278
281
|
const worktree = ticketWorktree({ state, ticket: action.ticket })
|
|
279
282
|
const runBranch = need({
|
|
280
283
|
value: state.run_branch,
|
package/engine/core/execute.ts
CHANGED
|
@@ -83,9 +83,10 @@ type InFlight = { action: EngineAction; done: Promise<void> }
|
|
|
83
83
|
* only alone, once everything in flight has settled; and one action on
|
|
84
84
|
* the run branch at a time (a new worktree, a join, its gates, a push, a
|
|
85
85
|
* rebase). A stop action waits for everything in flight, then the journal is
|
|
86
|
-
* read again.
|
|
87
|
-
*
|
|
88
|
-
*
|
|
86
|
+
* read again. A step that ends while the engine decides makes it decide
|
|
87
|
+
* again, so no step runs twice from a stale decision (#719). If an action
|
|
88
|
+
* throws (such as a launcher stop), the rest are let finish, then the first
|
|
89
|
+
* error is thrown. `max_steps` counts the actions started.
|
|
89
90
|
*
|
|
90
91
|
* With `jev`, the decision model is asked around each step in **shadow
|
|
91
92
|
* mode** and its answers are journaled but never acted on (see
|
|
@@ -350,8 +351,21 @@ export const runEngine = async ({
|
|
|
350
351
|
// and no fix round goes to a closed one.
|
|
351
352
|
await closeFinished({ all: false })
|
|
352
353
|
const records = journal.read()
|
|
354
|
+
const readWhile = [...inFlight.keys()]
|
|
353
355
|
const crashes = crashesIn(records)
|
|
354
356
|
const decided = await decideNow(records)
|
|
357
|
+
// A step that ended while the engine decided freed its key, but
|
|
358
|
+
// the decision was made from records where it had not ended: it
|
|
359
|
+
// would run that step again, such as a second join (#719).
|
|
360
|
+
// Decide again from the journal as it is now. The journal tells
|
|
361
|
+
// first: `step_ended` is written before the key is freed.
|
|
362
|
+
const endedSince = journal
|
|
363
|
+
.read()
|
|
364
|
+
.slice(records.length)
|
|
365
|
+
.some(({ kind }) => kind === 'step_ended')
|
|
366
|
+
if (endedSince || readWhile.some((key) => !inFlight.has(key))) {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
355
369
|
const actions = redosFirst({ actions: decided.actions, crashes })
|
|
356
370
|
const stop = actions.find((action) => stops.has(action.type))
|
|
357
371
|
if (stop !== undefined && inFlight.size === 0) {
|
package/engine/core/live-file.ts
CHANGED
|
@@ -107,6 +107,12 @@ const LiveFeedEntrySchema = z.object({
|
|
|
107
107
|
/** The whole live file, as each write has it. */
|
|
108
108
|
export const LiveFileSchema = z.object({
|
|
109
109
|
written_at: z.string(),
|
|
110
|
+
/**
|
|
111
|
+
* The engine's pid (#727), so `/luca-watch` and `/luca-unstick` can
|
|
112
|
+
* tell a live engine from a stopped one whatever the file's age.
|
|
113
|
+
* Missing in files from older engines.
|
|
114
|
+
*/
|
|
115
|
+
pid: z.number().int().optional(),
|
|
110
116
|
/** The run's first plan-usage reading, as its `rate_limit_event` sent it. */
|
|
111
117
|
plan_usage: z.record(z.string(), z.unknown()).nullable(),
|
|
112
118
|
agents: z.array(LiveAgentSchema),
|
|
@@ -203,6 +209,7 @@ export const createLiveFile = ({ file }: { file: string }) => {
|
|
|
203
209
|
changed = false
|
|
204
210
|
const live: LiveFile = {
|
|
205
211
|
written_at: new Date(at).toISOString(),
|
|
212
|
+
pid: process.pid,
|
|
206
213
|
plan_usage,
|
|
207
214
|
agents: [...agents].map(([key, agent]) => ({
|
|
208
215
|
...agent,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
gatesRun,
|
|
6
|
+
implemented,
|
|
7
|
+
intakePassed,
|
|
8
|
+
practiceTicket,
|
|
9
|
+
runBranchCreated,
|
|
10
|
+
stuckReported,
|
|
11
|
+
ticketStuck,
|
|
12
|
+
ticketWorktreeCreated,
|
|
13
|
+
} from './build-fixtures'
|
|
14
|
+
import { recordsFrom } from './intake-fixtures'
|
|
15
|
+
|
|
16
|
+
import type { JournalEntry } from '../journal/journal-record'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Journals for the `/luca-unstick` helper's tests (`stuck-summary*.test.ts`),
|
|
20
|
+
* written to disk as the engine writes them.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const UNSTICK_TICKETS = [
|
|
24
|
+
practiceTicket({ number: 11, title: 'Add sum' }),
|
|
25
|
+
practiceTicket({ number: 12, title: 'Add product' }),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
/** Ticket #11 stuck on a bad test, reported on the spec. */
|
|
29
|
+
export const stuckOnBadTest = (): JournalEntry[] => [
|
|
30
|
+
...intakePassed({ tickets: UNSTICK_TICKETS }),
|
|
31
|
+
runBranchCreated(),
|
|
32
|
+
ticketWorktreeCreated({ ticket: 11 }),
|
|
33
|
+
implemented({
|
|
34
|
+
ticket: 11,
|
|
35
|
+
outcome: 'bad_test',
|
|
36
|
+
reason: 'It pins build addresses.',
|
|
37
|
+
}),
|
|
38
|
+
gatesRun({ ticket: 11, target: 'ticket', ok: false }),
|
|
39
|
+
ticketStuck({ ticket: 11, reason: 'bad_test', detail: 'Sent back twice.' }),
|
|
40
|
+
stuckReported({ ticket: 11 }),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
/** A run's journal to write: its entries, repo, and first seq. */
|
|
44
|
+
export type UnstickRun = {
|
|
45
|
+
/** The runs folder. */
|
|
46
|
+
dir: string
|
|
47
|
+
run_id?: string
|
|
48
|
+
entries: JournalEntry[]
|
|
49
|
+
repo?: string
|
|
50
|
+
first_seq?: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Writes a run's journal to the runs folder `dir`, its seqs starting at
|
|
55
|
+
* `first_seq` and its `run_started` naming `repo`, and returns its path.
|
|
56
|
+
*/
|
|
57
|
+
export const writeRunFile = async ({
|
|
58
|
+
dir,
|
|
59
|
+
run_id = 'run-1',
|
|
60
|
+
entries,
|
|
61
|
+
repo = '/repos/app',
|
|
62
|
+
first_seq = 1,
|
|
63
|
+
}: UnstickRun): Promise<string> => {
|
|
64
|
+
const records = recordsFrom({ entries }).map((record) => ({
|
|
65
|
+
...(record.kind === 'run_started'
|
|
66
|
+
? { ...record, content: { ...record.content, repo } }
|
|
67
|
+
: record),
|
|
68
|
+
seq: record.seq + first_seq - 1,
|
|
69
|
+
}))
|
|
70
|
+
await mkdir(join(dir, run_id), { recursive: true })
|
|
71
|
+
const file = join(dir, run_id, 'journal.jsonl')
|
|
72
|
+
await Bun.write(
|
|
73
|
+
file,
|
|
74
|
+
records.map((record) => JSON.stringify(record)).join('\n') + '\n'
|
|
75
|
+
)
|
|
76
|
+
return file
|
|
77
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alecsibilia/luca",
|
|
3
3
|
"description": "Luca: turns a spec on GitHub Issues into a reviewed pull request, run from Paseo with Claude Code",
|
|
4
|
-
"version": "14.0.0-alpha.
|
|
4
|
+
"version": "14.0.0-alpha.35",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Alec Sibilia <sibilia.alec@gmail.com>",
|
|
7
7
|
"homepage": "https://github.com/asibilia/luca-framework#readme",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@anthropic-ai/claude-agent-sdk": "0.3.273",
|
|
39
|
-
"@getpaseo/client": "0.
|
|
40
|
-
"@getpaseo/plugin": "0.
|
|
39
|
+
"@getpaseo/client": "0.14.0",
|
|
40
|
+
"@getpaseo/plugin": "0.14.0",
|
|
41
41
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
42
42
|
"lodash": "^4.17.21",
|
|
43
43
|
"zod": "^4.3.6"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* are installed.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, readFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
13
14
|
|
|
14
15
|
/** Every journal record kind the helper reads. */
|
|
15
16
|
export const KINDS_READ = [
|
|
@@ -459,14 +460,57 @@ export const registryEntry = ({
|
|
|
459
460
|
}
|
|
460
461
|
}
|
|
461
462
|
|
|
462
|
-
/**
|
|
463
|
+
/**
|
|
464
|
+
* A live file written at most this long ago means a live engine: the
|
|
465
|
+
* board's `LIVE_STALE_MS` (`packages/board/shared/board-state-live.ts`).
|
|
466
|
+
* The engine writes it at least every 5 seconds while it runs.
|
|
467
|
+
*/
|
|
468
|
+
export const LIVE_STALE_MS = 60_000
|
|
469
|
+
|
|
470
|
+
/** What a run's live file says of its engine: when it was written, and its pid. */
|
|
471
|
+
export type LiveFacts = {
|
|
472
|
+
written_at: string
|
|
473
|
+
/** `null` in a file from an engine older than #727. */
|
|
474
|
+
pid: number | null
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Whether a run's engine is running now; `null` when unknown. It runs when
|
|
479
|
+
* a command line from `ps` names the run, or its live file shows it. An
|
|
480
|
+
* engine started with `luca-run --spec <n>` and no `--run-id` names no run
|
|
481
|
+
* id (#727), so only its live file shows it. When the file names the
|
|
482
|
+
* engine's pid, it runs exactly while `alive(pid)`, however old the file
|
|
483
|
+
* (a laptop asleep) or fresh (an engine that just stopped). A file with no
|
|
484
|
+
* pid counts while written at most `LIVE_STALE_MS` from `now`, either way,
|
|
485
|
+
* as the board checks.
|
|
486
|
+
*/
|
|
463
487
|
export const engineRunning = ({
|
|
464
488
|
run_id,
|
|
465
489
|
command_lines,
|
|
490
|
+
live,
|
|
491
|
+
alive,
|
|
492
|
+
now,
|
|
466
493
|
}: {
|
|
467
494
|
run_id: string
|
|
468
495
|
command_lines: string[] | null
|
|
496
|
+
/** The run's live file (`readLiveFile`), or `null`. */
|
|
497
|
+
live: LiveFacts | null
|
|
498
|
+
alive: (pid: number) => boolean
|
|
499
|
+
now: number
|
|
469
500
|
}): boolean | null => {
|
|
501
|
+
if (live !== null) {
|
|
502
|
+
if (live.pid !== null) {
|
|
503
|
+
if (alive(live.pid)) return true
|
|
504
|
+
} else {
|
|
505
|
+
const written = Date.parse(live.written_at)
|
|
506
|
+
if (
|
|
507
|
+
Number.isFinite(written) &&
|
|
508
|
+
Math.abs(now - written) <= LIVE_STALE_MS
|
|
509
|
+
) {
|
|
510
|
+
return true
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
470
514
|
if (command_lines === null) return null
|
|
471
515
|
return command_lines.some((line) => {
|
|
472
516
|
const tokens = line.split(/\s+/)
|
|
@@ -480,6 +524,43 @@ export const engineRunning = ({
|
|
|
480
524
|
})
|
|
481
525
|
}
|
|
482
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Run `run_id`'s live file (`live.json`, kept by the engine while it runs):
|
|
529
|
+
* its `written_at` and the engine's `pid`, or `null` when it is missing or
|
|
530
|
+
* unreadable.
|
|
531
|
+
*/
|
|
532
|
+
export const readLiveFile = ({
|
|
533
|
+
runs_dir,
|
|
534
|
+
run_id,
|
|
535
|
+
}: {
|
|
536
|
+
runs_dir: string
|
|
537
|
+
run_id: string
|
|
538
|
+
}): LiveFacts | null => {
|
|
539
|
+
const file = join(runs_dir, run_id, 'live.json')
|
|
540
|
+
if (!existsSync(file)) return null
|
|
541
|
+
try {
|
|
542
|
+
const live: unknown = JSON.parse(readFileSync(file, 'utf8'))
|
|
543
|
+
if (!isObject(live) || typeof live.written_at !== 'string') return null
|
|
544
|
+
return {
|
|
545
|
+
written_at: live.written_at,
|
|
546
|
+
pid: typeof live.pid === 'number' ? live.pid : null,
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
return null
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Whether process `pid` is alive: signal 0 checks without sending. */
|
|
554
|
+
export const pidAlive = (pid: number): boolean => {
|
|
555
|
+
try {
|
|
556
|
+
process.kill(pid, 0)
|
|
557
|
+
return true
|
|
558
|
+
} catch (error) {
|
|
559
|
+
// EPERM: it's alive, just not ours to signal.
|
|
560
|
+
return isObject(error) && error.code === 'EPERM'
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
483
564
|
/** Every process's command line, or `null` when `ps` fails. */
|
|
484
565
|
export const commandLines = (): string[] | null => {
|
|
485
566
|
const ps = Bun.spawnSync(['ps', '-axww', '-o', 'command='])
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
numberOr,
|
|
41
41
|
objectOf,
|
|
42
42
|
openStuck,
|
|
43
|
+
pidAlive,
|
|
44
|
+
readLiveFile,
|
|
43
45
|
readRecords,
|
|
44
46
|
registryEntry,
|
|
45
47
|
repoOfUrl,
|
|
@@ -54,7 +56,9 @@ export {
|
|
|
54
56
|
cutOffSteps,
|
|
55
57
|
engineRunning,
|
|
56
58
|
KINDS_READ,
|
|
59
|
+
LIVE_STALE_MS,
|
|
57
60
|
openStuck,
|
|
61
|
+
readLiveFile,
|
|
58
62
|
readRecords,
|
|
59
63
|
registryEntry,
|
|
60
64
|
repoOfUrl,
|
|
@@ -539,7 +543,13 @@ const main = async () => {
|
|
|
539
543
|
records,
|
|
540
544
|
ticket: args.ticket,
|
|
541
545
|
registry,
|
|
542
|
-
running: engineRunning({
|
|
546
|
+
running: engineRunning({
|
|
547
|
+
run_id,
|
|
548
|
+
command_lines: commandLines(),
|
|
549
|
+
live: readLiveFile({ runs_dir: args.runs_dir, run_id }),
|
|
550
|
+
alive: pidAlive,
|
|
551
|
+
now: Date.now(),
|
|
552
|
+
}),
|
|
543
553
|
log_tail: logTail(registry?.log_path ?? null),
|
|
544
554
|
})
|
|
545
555
|
)
|
|
@@ -55,7 +55,7 @@ It polls the journal every five seconds and prints one JSON line, `{ "event", "l
|
|
|
55
55
|
|
|
56
56
|
- `stuck`: a `stuck_reported`; `detail` is its comment body. Run the summary for the actual reason (from `ticket_stuck`, `final_review_stuck`, or `run_stuck`), then sections 3 to 5.
|
|
57
57
|
- `ended`: the run is over (a `run_ended`, or an older run's `pull_request_opened`, `intake_refused`, `nothing_to_do`, `stop` reply, or a billing or crash `run_stopped`). Report the outcome in one line, with the PR URL when present, log it, and stop.
|
|
58
|
-
- `engine_gone`: no live engine names the run. Section 6. The helper answers `engine_gone` at once while no engine is live, so after an engine-gone handoff (including a failed or used-up resume) log it and stop; the owner starts `/luca-watch` again after resuming the run.
|
|
58
|
+
- `engine_gone`: no live engine names the run, and the run's live file (`live.json`) shows none: the pid it names is not running, or, from an older engine with no pid, it is over a minute old. Section 6. The helper answers `engine_gone` at once while no engine is live, so after an engine-gone handoff (including a failed or used-up resume) log it and stop; the owner starts `/luca-watch` again after resuming the run.
|
|
59
59
|
- `quiet`: five quiet minutes. Call it again with the same cursor and stay in the loop. Don't log quiet polls.
|
|
60
60
|
- `error`: a missing journal, an unknown run, an invalid cursor, or bad arguments (exit code 2) stop the watcher, with a handoff. A process-inspection error (the process list kept failing) comes back only after the helper's five-minute wait, during which it still returns new journal records; it gets one serious handoff per changed error plus seq. Then call the helper again and keep processing journal events, but never resume without reliable process evidence. Any other journal-read error: try again on the next loop, and hand it off after three consecutive failures.
|
|
61
61
|
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - `ended`: the oldest ending after the cursor (`run_ended`, or a legacy
|
|
14
14
|
* one: `pull_request_opened`, `intake_refused`, `nothing_to_do`, a `stop`
|
|
15
15
|
* reply, a billing or crash `run_stopped`), or one before it.
|
|
16
|
-
* - `engine_gone`: no live engine names the run,
|
|
16
|
+
* - `engine_gone`: no live engine names the run, its live file shows no
|
|
17
|
+
* live engine, and it has not ended.
|
|
17
18
|
* - `quiet`: five minutes with none of these. Call it again.
|
|
18
19
|
* - `error`: the journal can't be read; or the process list kept failing
|
|
19
20
|
* until the quiet deadline, while the journal still showed nothing new.
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
* skipped. A missing journal, an unknown run, an invalid cursor or bad
|
|
24
25
|
* arguments print an `error` line and exit 2; every other event exits 0.
|
|
25
26
|
*
|
|
26
|
-
* It only reads: the journal and the process list. It imports nothing but
|
|
27
|
+
* It only reads: the journal, the run's live file, and the process list. It imports nothing but
|
|
27
28
|
* Bun's and Node's own modules, since it runs from
|
|
28
29
|
* `~/.claude/skills/luca-watch/`, where no packages are installed.
|
|
29
30
|
*/
|
|
@@ -185,6 +186,58 @@ const engineLive = ({
|
|
|
185
186
|
)
|
|
186
187
|
})
|
|
187
188
|
|
|
189
|
+
/** The engine's live file in a run folder: `LIVE_FILE` in `src/core/live-file.ts`. */
|
|
190
|
+
const LIVE_FILE = 'live.json'
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* A live file written at most this long ago means a live engine: the
|
|
194
|
+
* board's `LIVE_STALE_MS` (`packages/board/shared/board-state-live.ts`).
|
|
195
|
+
* The engine writes it at least every 5 seconds while it runs.
|
|
196
|
+
*/
|
|
197
|
+
export const LIVE_STALE_MS = 60_000
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Whether run `run_id`'s live file shows a live engine (#727). An engine
|
|
201
|
+
* started with `luca-run --spec <n>` and no `--run-id` makes the run id
|
|
202
|
+
* itself, so no command line names it; its live file still shows it. When
|
|
203
|
+
* the file names the engine's pid, the engine is live exactly while that
|
|
204
|
+
* pid is in `processes`, however old the file (a laptop asleep) or fresh
|
|
205
|
+
* (an engine that just stopped). A file from an older engine, with no pid,
|
|
206
|
+
* counts while it was written at most `LIVE_STALE_MS` from `now`, either
|
|
207
|
+
* way, as the board checks. A missing or unreadable file shows nothing.
|
|
208
|
+
*/
|
|
209
|
+
const liveFileLive = async ({
|
|
210
|
+
runs_dir,
|
|
211
|
+
run_id,
|
|
212
|
+
processes,
|
|
213
|
+
now,
|
|
214
|
+
}: {
|
|
215
|
+
runs_dir: string
|
|
216
|
+
run_id: string
|
|
217
|
+
processes: ProcessRow[]
|
|
218
|
+
now: number
|
|
219
|
+
}): Promise<boolean> => {
|
|
220
|
+
try {
|
|
221
|
+
const live: unknown = await Bun.file(
|
|
222
|
+
join(runs_dir, run_id, LIVE_FILE)
|
|
223
|
+
).json()
|
|
224
|
+
if (!isObject(live)) return false
|
|
225
|
+
if (typeof live.pid === 'number') {
|
|
226
|
+
return processes.some(({ pid }) => pid === live.pid)
|
|
227
|
+
}
|
|
228
|
+
const written_at =
|
|
229
|
+
typeof live.written_at === 'string'
|
|
230
|
+
? Date.parse(live.written_at)
|
|
231
|
+
: Number.NaN
|
|
232
|
+
return (
|
|
233
|
+
Number.isFinite(written_at) &&
|
|
234
|
+
Math.abs(now - written_at) <= LIVE_STALE_MS
|
|
235
|
+
)
|
|
236
|
+
} catch {
|
|
237
|
+
return false
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
188
241
|
/** `pid` and every parent above it, from `processes`. Pure. */
|
|
189
242
|
const ancestorPids = ({
|
|
190
243
|
pid,
|
|
@@ -274,7 +327,15 @@ export const waitForChange = async ({
|
|
|
274
327
|
const own = own_pids.flatMap((pid) =>
|
|
275
328
|
ancestorPids({ pid, processes: rows })
|
|
276
329
|
)
|
|
277
|
-
if (
|
|
330
|
+
if (
|
|
331
|
+
!engineLive({ run_id, processes: rows, own_pids: own }) &&
|
|
332
|
+
!(await liveFileLive({
|
|
333
|
+
runs_dir,
|
|
334
|
+
run_id,
|
|
335
|
+
processes: rows,
|
|
336
|
+
now: now(),
|
|
337
|
+
}))
|
|
338
|
+
) {
|
|
278
339
|
return {
|
|
279
340
|
event: 'engine_gone',
|
|
280
341
|
last_seq: highest,
|