@wjarka/cezarion 0.14.9-dev.933 → 0.14.9-dev.936

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.
@@ -41,17 +41,11 @@ export declare function ciToolRoutes(capabilities: Map<string, Capability>): imp
41
41
  "/api/v1/tools/ci-wait": {
42
42
  $post: {
43
43
  output: {
44
- prUrl: string;
45
- repository: string;
46
- prNumber: number;
47
- headSha: string;
48
- waitId: string;
49
- registeredAt: string;
50
- deadline: string;
51
- phase: "delivered" | "parked" | "registered" | "wake-pending" | "withdrawn";
44
+ code: "authentication" | "capacity" | "command_failed" | "gh_missing" | "inaccessible_pr" | "invalid_request" | "malformed_data" | "output_limit" | "persistence" | "query_timeout" | "unauthorized" | "unavailable" | "unsupported_host" | "wait_conflict";
45
+ message: string;
52
46
  };
53
47
  outputFormat: "json";
54
- status: import("hono/utils/http-status").ContentfulStatusCode;
48
+ status: 503;
55
49
  input: {
56
50
  json: {
57
51
  pr: string;
@@ -60,11 +54,17 @@ export declare function ciToolRoutes(capabilities: Map<string, Capability>): imp
60
54
  };
61
55
  } | {
62
56
  output: {
63
- code: "authentication" | "capacity" | "command_failed" | "gh_missing" | "inaccessible_pr" | "invalid_request" | "malformed_data" | "output_limit" | "persistence" | "query_timeout" | "unauthorized" | "unavailable" | "unsupported_host" | "wait_conflict";
64
- message: string;
57
+ prUrl: string;
58
+ repository: string;
59
+ prNumber: number;
60
+ headSha: string;
61
+ waitId: string;
62
+ registeredAt: string;
63
+ deadline: string;
64
+ phase: "delivered" | "parked" | "registered" | "wake-pending" | "withdrawn";
65
65
  };
66
66
  outputFormat: "json";
67
- status: 503;
67
+ status: import("hono/utils/http-status").ContentfulStatusCode;
68
68
  input: {
69
69
  json: {
70
70
  pr: string;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Synchronous, dependency-free proof that a crashed worker generation's processes are gone
3
+ * (#469, spec 2026-09-26-worker-orphan-finalization). Sync so `continueRun` stays sync.
4
+ * Every uncertainty answers "alive" or "unknown", never "gone".
5
+ */
6
+ export type RecordedProcess = {
7
+ pid: number;
8
+ startToken?: string;
9
+ };
10
+ export type WorkerProcessRecord = {
11
+ generation: string;
12
+ controller: RecordedProcess;
13
+ processes: RecordedProcess[];
14
+ };
15
+ export type GenerationLiveness = 'gone' | 'alive' | 'unknown';
16
+ /** `/proc/<pid>/stat` field 22 (`starttime`); `comm` may hold spaces and `)`, so parse after the LAST `)`. */
17
+ export declare function parseProcStat(stat: string): {
18
+ state: string;
19
+ startToken: string;
20
+ } | undefined;
21
+ /** One process incarnation, so a reused PID never matches. Absent when the platform cannot say. */
22
+ export declare function processStartToken(pid: number, platform?: NodeJS.Platform): string | undefined;
23
+ /** Live and the same incarnation. A token-less entry (or an unreadable current token) counts while the PID exists. */
24
+ export declare function recordedProcessLive(entry: RecordedProcess): boolean;
25
+ export declare function isCurrentProcess(entry: RecordedProcess): boolean;
26
+ /** The `/proc` reads the Linux scan makes; injectable because EACCES cannot be staged for real. */
27
+ export type ProcReader = {
28
+ readdir: () => string[];
29
+ readlink: (pid: string) => string;
30
+ ownerUid: (pid: string) => number | undefined;
31
+ startedAtMs: (pid: string) => number | undefined;
32
+ };
33
+ /** The darwin scan: `lsof` for working directories, `ps` for our own user's processes. */
34
+ export type DarwinReader = {
35
+ lsof: () => {
36
+ ok: boolean;
37
+ stdout: string;
38
+ };
39
+ ownProcesses: () => Array<{
40
+ pid: number;
41
+ startedAtMs?: number;
42
+ }> | undefined;
43
+ };
44
+ /** PIDs (never this process) whose working directory is one of `dirs` or beneath it, in one scan
45
+ * pass; `unknown` when no scan can run. cezar's own short-lived git children in a worktree make
46
+ * this read "alive" briefly: conservative, and a retry self-heals. `since` (epoch ms) is the
47
+ * earliest moment the generation's processes can have started; see the EACCES rule below. */
48
+ export declare function processesWithCwdUnder(dirs: string | readonly string[], platform?: NodeJS.Platform, proc?: ProcReader, since?: number, darwin?: DarwinReader): number[] | 'unknown';
49
+ export type GenerationProbe = {
50
+ liveness: GenerationLiveness;
51
+ controller?: number;
52
+ pids: number[];
53
+ };
54
+ /** A missing record (legacy) relies on the working-directory scan alone. `paths` are the
55
+ * worktree and every scratch location, which finalization deletes. A live foreign controller
56
+ * short-circuits the scan; otherwise `pids` names every live recorded or scanned process. */
57
+ export declare function inspectGeneration({ record, paths, since }: {
58
+ record?: WorkerProcessRecord;
59
+ paths: readonly string[];
60
+ since?: number;
61
+ }): GenerationProbe;
62
+ export declare function probeGeneration(input: {
63
+ record?: WorkerProcessRecord;
64
+ paths: readonly string[];
65
+ since?: number;
66
+ }): GenerationLiveness;
@@ -0,0 +1,215 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readdirSync, readFileSync, readlinkSync, realpathSync, statSync } from 'node:fs';
3
+ import { resolve, sep } from 'node:path';
4
+ /** `/proc/<pid>/stat` field 22 (`starttime`); `comm` may hold spaces and `)`, so parse after the LAST `)`. */
5
+ export function parseProcStat(stat) {
6
+ const fields = stat.slice(stat.lastIndexOf(')') + 1).trim().split(/\s+/);
7
+ return stat.includes(')') && fields.length >= 20 && /^\d+$/.test(fields[19]) ? { state: fields[0], startToken: fields[19] } : undefined;
8
+ }
9
+ function procStat(pid) {
10
+ try {
11
+ return parseProcStat(readFileSync(`/proc/${pid}/stat`, 'utf8'));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ let bootId;
18
+ /** `starttime` counts ticks since boot, so it repeats across reboots; the boot id scopes it. */
19
+ function linuxBootId() {
20
+ if (bootId === undefined) {
21
+ try {
22
+ bootId = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim() || null;
23
+ }
24
+ catch {
25
+ bootId = null;
26
+ }
27
+ }
28
+ return bootId ?? undefined;
29
+ }
30
+ /** One process incarnation, so a reused PID never matches. Absent when the platform cannot say. */
31
+ export function processStartToken(pid, platform = process.platform) {
32
+ if (!Number.isSafeInteger(pid) || pid <= 0)
33
+ return undefined;
34
+ if (platform === 'linux') {
35
+ const start = procStat(pid)?.startToken;
36
+ const boot = linuxBootId();
37
+ return start && boot ? `${boot}:${start}` : start;
38
+ }
39
+ if (platform !== 'darwin')
40
+ return undefined;
41
+ // `lstart` is locale- and zone-formatted; pin both so the token is stable across cezar processes.
42
+ const ps = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], { encoding: 'utf8', timeout: 2_000, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' } });
43
+ const token = ps.status === 0 ? ps.stdout.trim() : '';
44
+ return token || undefined;
45
+ }
46
+ function pidExists(pid) {
47
+ if (!Number.isSafeInteger(pid) || pid <= 0)
48
+ return false;
49
+ try {
50
+ process.kill(pid, 0);
51
+ }
52
+ catch (error) {
53
+ return error.code === 'EPERM';
54
+ }
55
+ // A zombie has exited; only its parent's wait remains.
56
+ return process.platform !== 'linux' || procStat(pid)?.state !== 'Z';
57
+ }
58
+ const LINUX_TOKEN = /^(?:[0-9a-f-]{36}:)?(\d+)$/;
59
+ /** Liveness-only comparison: when exactly one Linux token lacks the boot id (it was unreadable on one
60
+ * side), the `starttime` suffix decides. Reaping still requires an exact match. */
61
+ function sameIncarnation(recorded, current) {
62
+ if (recorded === current)
63
+ return true;
64
+ const a = LINUX_TOKEN.exec(recorded), b = LINUX_TOKEN.exec(current);
65
+ return !!a && !!b && recorded.includes(':') !== current.includes(':') && a[1] === b[1];
66
+ }
67
+ /** Live and the same incarnation. A token-less entry (or an unreadable current token) counts while the PID exists. */
68
+ export function recordedProcessLive(entry) {
69
+ if (!pidExists(entry.pid))
70
+ return false;
71
+ if (entry.startToken === undefined)
72
+ return true;
73
+ const current = processStartToken(entry.pid);
74
+ return current === undefined || sameIncarnation(entry.startToken, current);
75
+ }
76
+ export function isCurrentProcess(entry) {
77
+ if (entry.pid !== process.pid)
78
+ return false;
79
+ const own = processStartToken(process.pid);
80
+ return entry.startToken === undefined || own === undefined || sameIncarnation(entry.startToken, own);
81
+ }
82
+ let clockTicks;
83
+ let bootTimeMs;
84
+ /** Wall-clock start of a process: boot time (`/proc/stat` btime) plus its starttime ticks. */
85
+ function procStartedAtMs(pid) {
86
+ try {
87
+ bootTimeMs ??= Number(/^btime (\d+)$/m.exec(readFileSync('/proc/stat', 'utf8'))?.[1]) * 1000;
88
+ clockTicks ??= Number(spawnSync('getconf', ['CLK_TCK'], { encoding: 'utf8', timeout: 2_000 }).stdout.trim()) || 100;
89
+ const start = procStat(Number(pid))?.startToken;
90
+ return start === undefined || !Number.isFinite(bootTimeMs) ? undefined : bootTimeMs + Number(start) / clockTicks * 1000;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ const realDarwin = {
97
+ lsof: () => {
98
+ const lsof = spawnSync('lsof', ['-a', '-d', 'cwd', '-Fpn'], { encoding: 'utf8', timeout: 5_000, maxBuffer: 16 * 1024 * 1024 });
99
+ // Status 1 is ordinary (some process unreadable); completeness is judged against `ps` instead.
100
+ return { ok: !lsof.error && (lsof.status === 0 || lsof.status === 1) && !!lsof.stdout, stdout: lsof.stdout ?? '' };
101
+ },
102
+ ownProcesses: () => {
103
+ const uid = process.getuid?.();
104
+ if (uid === undefined)
105
+ return undefined;
106
+ const ps = spawnSync('ps', ['-U', String(uid), '-o', 'pid=,lstart='], { encoding: 'utf8', timeout: 2_000, env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' } });
107
+ if (ps.error || ps.status !== 0 || !ps.stdout)
108
+ return undefined;
109
+ return ps.stdout.split('\n').flatMap(line => {
110
+ const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(line);
111
+ // `ps` lists itself, and lsof (which ran first) never saw it.
112
+ if (!match || Number(match[1]) === ps.pid)
113
+ return [];
114
+ const startedAtMs = Date.parse(`${match[2]} UTC`);
115
+ return [{ pid: Number(match[1]), ...(Number.isFinite(startedAtMs) ? { startedAtMs } : {}) }];
116
+ });
117
+ },
118
+ };
119
+ const realProc = {
120
+ readdir: () => readdirSync('/proc'),
121
+ readlink: pid => readlinkSync(`/proc/${pid}/cwd`),
122
+ ownerUid: pid => { try {
123
+ return statSync(`/proc/${pid}`).uid;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ } },
128
+ startedAtMs: procStartedAtMs,
129
+ };
130
+ /** PIDs (never this process) whose working directory is one of `dirs` or beneath it, in one scan
131
+ * pass; `unknown` when no scan can run. cezar's own short-lived git children in a worktree make
132
+ * this read "alive" briefly: conservative, and a retry self-heals. `since` (epoch ms) is the
133
+ * earliest moment the generation's processes can have started; see the EACCES rule below. */
134
+ export function processesWithCwdUnder(dirs, platform = process.platform, proc = realProc, since, darwin = realDarwin) {
135
+ const targets = (typeof dirs === 'string' ? [dirs] : dirs).map(dir => { try {
136
+ return realpathSync(dir);
137
+ }
138
+ catch {
139
+ return resolve(dir);
140
+ } });
141
+ const under = (cwd) => { const path = cwd.replace(/ \(deleted\)$/, ''); return targets.some(target => path === target || path.startsWith(target + sep)); };
142
+ const found = [];
143
+ if (platform === 'linux') {
144
+ let entries;
145
+ try {
146
+ entries = proc.readdir();
147
+ }
148
+ catch {
149
+ return 'unknown';
150
+ }
151
+ const uid = process.getuid?.();
152
+ for (const entry of entries) {
153
+ if (!/^\d+$/.test(entry) || Number(entry) === process.pid)
154
+ continue;
155
+ try {
156
+ if (under(proc.readlink(entry)))
157
+ found.push(Number(entry));
158
+ }
159
+ catch (error) {
160
+ // A vanished process is gone; another user's is unreadable by design and skipped. Our own
161
+ // user's non-dumpable processes are unreadable too (systemd --user, sshd, gpg-agent, which
162
+ // every host has), so they cannot all block the proof: one that started before the worker
163
+ // existed cannot be its descendant. A later one is a possible holder, never signalled.
164
+ if (error.code === 'ENOENT' || uid === undefined || proc.ownerUid(entry) !== uid)
165
+ continue;
166
+ const started = since === undefined ? undefined : proc.startedAtMs(entry);
167
+ if (started === undefined || started >= since)
168
+ found.push(Number(entry));
169
+ }
170
+ }
171
+ return found;
172
+ }
173
+ if (platform !== 'darwin')
174
+ return 'unknown';
175
+ const lsof = darwin.lsof();
176
+ if (!lsof.ok)
177
+ return 'unknown';
178
+ const seen = new Set();
179
+ let pid;
180
+ for (const line of lsof.stdout.split('\n')) {
181
+ if (line.startsWith('p')) {
182
+ pid = Number(line.slice(1));
183
+ seen.add(pid);
184
+ }
185
+ else if (line.startsWith('n') && pid !== undefined && pid !== process.pid && under(line.slice(1)))
186
+ found.push(pid);
187
+ }
188
+ // lsof silently omits what it cannot read. An own-user process it omitted is judged by the
189
+ // Linux EACCES rule: a possible holder unless it predates the worker. Other users' are skipped.
190
+ const own = darwin.ownProcesses();
191
+ if (!own)
192
+ return 'unknown';
193
+ for (const entry of own) {
194
+ if (seen.has(entry.pid) || entry.pid === process.pid)
195
+ continue;
196
+ if (since === undefined || entry.startedAtMs === undefined || entry.startedAtMs >= since)
197
+ found.push(entry.pid);
198
+ }
199
+ return [...new Set(found)];
200
+ }
201
+ /** A missing record (legacy) relies on the working-directory scan alone. `paths` are the
202
+ * worktree and every scratch location, which finalization deletes. A live foreign controller
203
+ * short-circuits the scan; otherwise `pids` names every live recorded or scanned process. */
204
+ export function inspectGeneration({ record, paths, since }) {
205
+ if (record && !isCurrentProcess(record.controller) && recordedProcessLive(record.controller))
206
+ return { liveness: 'alive', controller: record.controller.pid, pids: [] };
207
+ const recorded = record?.processes.filter(recordedProcessLive).map(entry => entry.pid) ?? [];
208
+ const scan = processesWithCwdUnder(paths, process.platform, realProc, since);
209
+ const pids = [...new Set([...recorded, ...(scan === 'unknown' ? [] : scan)])];
210
+ return { liveness: pids.length ? 'alive' : scan === 'unknown' ? 'unknown' : 'gone', pids };
211
+ }
212
+ export function probeGeneration(input) {
213
+ return inspectGeneration(input).liveness;
214
+ }
215
+ //# sourceMappingURL=process-liveness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process-liveness.js","sourceRoot":"","sources":["../../src/delegation/process-liveness.ts"],"names":[],"mappings":"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"}
@@ -82,13 +82,13 @@ export declare function createDelegationRoutes(service: DelegationService, crede
82
82
  status: 200;
83
83
  input: {
84
84
  json: {
85
- requestIds: string[];
86
- mode?: "all" | "any" | "one" | undefined;
87
- timeoutSeconds?: number | undefined;
88
- } | {
89
85
  workerIds: string[];
90
86
  timeoutSeconds?: number | undefined;
91
87
  mode?: "all" | "any" | "one" | undefined;
88
+ } | {
89
+ requestIds: string[];
90
+ mode?: "all" | "any" | "one" | undefined;
91
+ timeoutSeconds?: number | undefined;
92
92
  };
93
93
  };
94
94
  };
@@ -439,18 +439,6 @@ export declare function createDelegationRoutes(service: DelegationService, crede
439
439
  } & {
440
440
  "/:workerId": {
441
441
  $get: {
442
- output: {
443
- error: string;
444
- code?: "invalid_input" | undefined;
445
- };
446
- outputFormat: "json";
447
- status: 400;
448
- input: {
449
- param: {
450
- workerId: string;
451
- };
452
- };
453
- } | {
454
442
  output: {
455
443
  workerId: string;
456
444
  parentRunId: string;
@@ -527,11 +515,7 @@ export declare function createDelegationRoutes(service: DelegationService, crede
527
515
  workerId: string;
528
516
  };
529
517
  };
530
- };
531
- };
532
- } & {
533
- "/:workerId/collect": {
534
- $post: {
518
+ } | {
535
519
  output: {
536
520
  error: string;
537
521
  code?: "invalid_input" | undefined;
@@ -542,10 +526,12 @@ export declare function createDelegationRoutes(service: DelegationService, crede
542
526
  param: {
543
527
  workerId: string;
544
528
  };
545
- } & {
546
- json: Record<string, never>;
547
529
  };
548
- } | {
530
+ };
531
+ };
532
+ } & {
533
+ "/:workerId/collect": {
534
+ $post: {
549
535
  output: {
550
536
  workerId: string;
551
537
  parentRunId: string;
@@ -647,11 +633,7 @@ export declare function createDelegationRoutes(service: DelegationService, crede
647
633
  } & {
648
634
  json: Record<string, never>;
649
635
  };
650
- };
651
- };
652
- } & {
653
- "/:workerId/steer": {
654
- $post: {
636
+ } | {
655
637
  output: {
656
638
  error: string;
657
639
  code?: "invalid_input" | undefined;
@@ -663,11 +645,13 @@ export declare function createDelegationRoutes(service: DelegationService, crede
663
645
  workerId: string;
664
646
  };
665
647
  } & {
666
- json: {
667
- text: string;
668
- };
648
+ json: Record<string, never>;
669
649
  };
670
- } | {
650
+ };
651
+ };
652
+ } & {
653
+ "/:workerId/steer": {
654
+ $post: {
671
655
  output: {
672
656
  workerId: string;
673
657
  state: "delivered" | "queued";
@@ -683,11 +667,7 @@ export declare function createDelegationRoutes(service: DelegationService, crede
683
667
  text: string;
684
668
  };
685
669
  };
686
- };
687
- };
688
- } & {
689
- "/:workerId/stop": {
690
- $post: {
670
+ } | {
691
671
  output: {
692
672
  error: string;
693
673
  code?: "invalid_input" | undefined;
@@ -699,9 +679,15 @@ export declare function createDelegationRoutes(service: DelegationService, crede
699
679
  workerId: string;
700
680
  };
701
681
  } & {
702
- json: Record<string, never>;
682
+ json: {
683
+ text: string;
684
+ };
703
685
  };
704
- } | {
686
+ };
687
+ };
688
+ } & {
689
+ "/:workerId/stop": {
690
+ $post: {
705
691
  output: {
706
692
  workerId: string;
707
693
  state: "stopping" | "terminated";
@@ -715,11 +701,7 @@ export declare function createDelegationRoutes(service: DelegationService, crede
715
701
  } & {
716
702
  json: Record<string, never>;
717
703
  };
718
- };
719
- };
720
- } & {
721
- "/:workerId/destroy": {
722
- $post: {
704
+ } | {
723
705
  output: {
724
706
  error: string;
725
707
  code?: "invalid_input" | undefined;
@@ -733,7 +715,11 @@ export declare function createDelegationRoutes(service: DelegationService, crede
733
715
  } & {
734
716
  json: Record<string, never>;
735
717
  };
736
- } | {
718
+ };
719
+ };
720
+ } & {
721
+ "/:workerId/destroy": {
722
+ $post: {
737
723
  output: {
738
724
  workerId: string;
739
725
  state: "complete" | "incomplete";
@@ -756,6 +742,20 @@ export declare function createDelegationRoutes(service: DelegationService, crede
756
742
  } & {
757
743
  json: Record<string, never>;
758
744
  };
745
+ } | {
746
+ output: {
747
+ error: string;
748
+ code?: "invalid_input" | undefined;
749
+ };
750
+ outputFormat: "json";
751
+ status: 400;
752
+ input: {
753
+ param: {
754
+ workerId: string;
755
+ };
756
+ } & {
757
+ json: Record<string, never>;
758
+ };
759
759
  } | {
760
760
  output: {
761
761
  workerId: string;
@@ -785,11 +785,13 @@ export declare function createDelegationRoutes(service: DelegationService, crede
785
785
  "/:workerId/diff": {
786
786
  $get: {
787
787
  output: {
788
- error: string;
789
- code?: "invalid_input" | undefined;
788
+ workerId: string;
789
+ baselineSha: string;
790
+ diff: string;
791
+ truncated: boolean;
790
792
  };
791
793
  outputFormat: "json";
792
- status: 400;
794
+ status: 200;
793
795
  input: {
794
796
  param: {
795
797
  workerId: string;
@@ -797,13 +799,11 @@ export declare function createDelegationRoutes(service: DelegationService, crede
797
799
  };
798
800
  } | {
799
801
  output: {
800
- workerId: string;
801
- baselineSha: string;
802
- diff: string;
803
- truncated: boolean;
802
+ error: string;
803
+ code?: "invalid_input" | undefined;
804
804
  };
805
805
  outputFormat: "json";
806
- status: 200;
806
+ status: 400;
807
807
  input: {
808
808
  param: {
809
809
  workerId: string;
@@ -13,6 +13,8 @@ export declare const delegationEnabled: () => boolean;
13
13
  export declare class DelegationService {
14
14
  private projects;
15
15
  private serial;
16
+ /** How long destroy waits for proven termination; private and overridable so tests need not wait it out. */
17
+ private terminationTimeoutMs;
16
18
  registerProject(project: DelegationProject): () => void;
17
19
  private context;
18
20
  private serialized;
@@ -80,6 +80,8 @@ export const delegationEnabled = () => process.env.CEZ_DELEGATION === '1';
80
80
  export class DelegationService {
81
81
  projects = new Map();
82
82
  serial = new Map();
83
+ /** How long destroy waits for proven termination; private and overridable so tests need not wait it out. */
84
+ terminationTimeoutMs = 30_000;
83
85
  registerProject(project) {
84
86
  const existing = this.projects.get(project.id);
85
87
  if (existing?.store === project.store && existing.manager === project.manager)
@@ -452,9 +454,10 @@ export class DelegationService {
452
454
  });
453
455
  }
454
456
  async inspect(caller, params) {
455
- const { worker, parent } = this.target(caller, params, 'inspect');
457
+ const { project, worker, parent } = this.target(caller, params, 'inspect');
456
458
  if (worker.delegation?.role !== 'worker')
457
459
  throw new DelegationPolicyError('denied_scope', 'Worker scope denied');
460
+ project.manager.settleOrphanedWorkerExecution(worker.id); // #469: an orphan that died after recovery settles
458
461
  const outcome = workerOutcome(worker, new Date().toISOString());
459
462
  const wait = parent.delegation?.role === 'root' ? parent.delegation.wait : undefined;
460
463
  return { workerId: worker.id, parentRunId: parent.id, status: worker.status, workspace: worker.delegation.workspace,
@@ -477,6 +480,7 @@ export class DelegationService {
477
480
  return project.store.commitWorkerResult(caller.runId, evidence, project.store.readWorkerResultDiff(caller.runId, workerId));
478
481
  }
479
482
  authorizeWorker(caller, worker, 'inspect', parent, project.id);
483
+ project.manager.settleOrphanedWorkerExecution(workerId); // #469: before `settled` is computed
480
484
  if (parent?.delegation?.role === 'root' && parent.delegation.receipts.some(receipt => receipt.workerId === workerId && receipt.deletion?.phase === 'pending')) {
481
485
  const retained = project.store.readWorkerResult(parent.id, workerId);
482
486
  if (!retained || !project.store.canDeleteRun(workerId))
@@ -576,8 +580,16 @@ export class DelegationService {
576
580
  persist('terminating', ['process', ...resources]);
577
581
  project.manager.requestWorkerStop(workerId);
578
582
  let result;
579
- if (!await project.manager.awaitRunTermination(workerId, 30_000)) {
580
- result = { workerId, state: 'incomplete', remaining: ['process', ...resources], error: 'Worker termination is not proven; retry cleanup later' };
583
+ if (!await project.manager.awaitRunTermination(workerId, this.terminationTimeoutMs, { reapOrphans: true })) {
584
+ // #469: name what blocks a crashed generation; other causes keep the generic message.
585
+ const taken = project.manager.takeWorkerTerminationBlocker(workerId);
586
+ const reason = taken && (taken.blocker.kind === 'unreadable' ? 'worker process record is unreadable; termination cannot be proven'
587
+ : taken.blocker.kind === 'controller' ? `the worker is still controlled by a live cezar (pid ${taken.blocker.pid})`
588
+ : `${taken.blocker.pids.length === 1 ? 'process' : 'processes'} ${taken.blocker.pids.join(', ')} still ${taken.blocker.pids.length === 1 ? 'holds' : 'hold'} the worker's worktree or scratch`);
589
+ if (taken?.changed)
590
+ project.store.appendEvent(workerId, { type: 'lifecycle', message: `destroy blocked: ${reason}` });
591
+ result = { workerId, state: 'incomplete', remaining: ['process', ...resources], error: !taken ? 'Worker termination is not proven; retry cleanup later'
592
+ : taken.blocker.kind === 'unreadable' ? reason : `Worker termination is not proven: ${reason}; retry cleanup later` };
581
593
  }
582
594
  else {
583
595
  persist('cleaning', resources);