@wjarka/cezarion 0.14.9-dev.915 → 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.
@@ -219,6 +219,16 @@ export declare function pastedAttachmentsNote(attachments: PersistedAttachment[]
219
219
  export declare const VARIANT_LETTERS: readonly ['A', 'B', 'C'];
220
220
  /** Only an original workflow that has never executed may return to its queue. */
221
221
  export declare function isUntouchedCancelledRun(run: RunRecord): boolean;
222
+ /** #469: why destroy could not prove a crashed generation's termination. */
223
+ export type WorkerTerminationBlocker = {
224
+ kind: 'unreadable';
225
+ } | {
226
+ kind: 'controller';
227
+ pid: number;
228
+ } | {
229
+ kind: 'processes';
230
+ pids: number[];
231
+ };
222
232
  /**
223
233
  * The mini workflow engine: executes a `WorkflowDef` against a repo, one step
224
234
  * at a time, persisting every event to the RunStore (which the SSE endpoints
@@ -248,9 +258,53 @@ export declare class RunManager {
248
258
  * exception or an interrupt request cannot stand in for session termination. */
249
259
  private trackWorkerSessionResult;
250
260
  private persistWorkerCompletion;
261
+ /** #469: the generation a dead controller left `starting`, if this manager may judge it. A
262
+ * generation this process controls is never judged here: the in-memory execution map owns it,
263
+ * and a disposed manager's still-running sessions look like orphans. `busy` is transient (this
264
+ * manager holds the run); `unknown` is a present record that proves nothing. */
265
+ private orphanState;
266
+ private orphanedWorkerGeneration;
267
+ /** Completes a crashed generation's proof once every one of its processes is proven gone (#469).
268
+ * Sync and signal-free; recovery, resume, delivery, collect and destroy then take their normal paths.
269
+ * A non-gone probe is cached briefly: on darwin the scan is a synchronous `lsof` on the event loop. */
270
+ settleOrphanedWorkerExecution(runId: string, opts?: {
271
+ admitting?: boolean;
272
+ fresh?: boolean;
273
+ }): boolean;
274
+ private readonly orphanProbes;
275
+ private readonly orphanReprobes;
276
+ private readonly orphanBlockers;
277
+ private readonly reportedOrphanBlockers;
278
+ private orphanReprobeMs;
279
+ private orphanReprobeLimitMs;
280
+ private orphanReprobeSlowMs;
281
+ private orphanTermGraceMs;
282
+ /** #469: a survivor that dies after recovery has no other wake source (no exit callback for a
283
+ * process another cezar spawned). Unref'd; finalization's `run` event then lets worker waits and
284
+ * outcomes observe it. Fast for the first window, then slow but uncapped, so a long-lived
285
+ * survivor is still noticed when it exits. A tick while this manager holds the run is skipped;
286
+ * only terminal reasons (proof settled, generation changed, run gone, unknown record, dispose)
287
+ * stop it. */
288
+ private armOrphanReprobe;
289
+ private clearOrphanReprobe;
290
+ /** Why the last destroy could not prove termination (#469), and whether that differs from the
291
+ * reason last reported, so a retry loop appends one lifecycle event per distinct blocker set. */
292
+ takeWorkerTerminationBlocker(runId: string): {
293
+ blocker: WorkerTerminationBlocker;
294
+ changed: boolean;
295
+ } | undefined;
296
+ /** Continue over a live orphan names what still runs; other refusals keep the generic text. */
297
+ private orphanAdmissionRefusal;
298
+ /** Destroy only (#469): signal the recorded, token-verified survivors of a generation whose
299
+ * controller is proven dead, then finalize on `gone`. Scan-only processes are never signalled. */
300
+ private reapOrphanedWorker;
301
+ /** Best effort: a failed write leaves the working-directory scan as this process's evidence. */
302
+ private recordWorkerProcess;
251
303
  private trackTurnEnd;
252
304
  requestWorkerStop(runId: string): WorkerStopResult;
253
- awaitRunTermination(runId: string, timeoutMs: number): Promise<boolean>;
305
+ awaitRunTermination(runId: string, timeoutMs: number, opts?: {
306
+ reapOrphans?: boolean;
307
+ }): Promise<boolean>;
254
308
  /** Private cleanup capability, never serialized. Obtain after stop/termination
255
309
  * under the service's destroy lock; each invocation rechecks the same generation. */
256
310
  getWorkerNoMaterializationProof(runId: string): WorkerNoMaterializationProof | undefined;
@@ -9,6 +9,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync
9
9
  import { dirname, join } from 'node:path';
10
10
  import { parseAskMarkerResult, stripAskMarker, } from '../core/ask.js';
11
11
  import { hasRegisteredRunProcess, onUsage, registerRunProcess, unregisterRunProcess } from '../core/process-usage.js';
12
+ import { inspectGeneration, isCurrentProcess, processStartToken, recordedProcessLive } from '../delegation/process-liveness.js';
12
13
  import { parseUsageLimit } from '../core/usage-limit.js';
13
14
  import { createRunner } from '../core/runner-factory.js';
14
15
  import { modelConflictsWithRunner } from '../core/model-presets.js';
@@ -41,7 +42,7 @@ import { answersQuestion, openQuestions, questionMessage } from '../delegation/q
41
42
  import { workerOutcome } from '../runs/delegation-state.js';
42
43
  import { loadWorkflows } from './load.js';
43
44
  import { reclaimWorktrees, rematerializeReclaimedWorktree } from '../runs/retention.js';
44
- import { AgentTempDirError, agentTmpEnv, removeAgentTmpDir, sweepAgentTmpDirs, } from '../runs/agent-tmpdir.js';
45
+ import { AgentTempDirError, agentTmpEnv, agentTmpDirLocations, removeAgentTmpDir, sweepAgentTmpDirs, } from '../runs/agent-tmpdir.js';
45
46
  import { extractTaskRefs, refineTaskRefs, titleRefNumber } from '../runs/task-refs.js';
46
47
  import { parseTaskMarkers, stripTaskMarkers } from '../runs/task-markers.js';
47
48
  import { autoNamingActive, generateRunName, liveTitleUpdatesEnabled, postValidateTitle } from '../runs/auto-name.js';
@@ -426,6 +427,12 @@ export function isUntouchedCancelledRun(run) {
426
427
  return run.status === 'cancelled' && !run.startedAt && !!run.workflowDef &&
427
428
  run.steps.every(step => step.status === 'pending' && !step.startedAt && !step.sessionId);
428
429
  }
430
+ /** #469: how long a non-gone orphan probe stands before the next scan. */
431
+ const ORPHAN_PROBE_CACHE_MS = 2_000;
432
+ /** #469: admission refused because a process of the previous generation still runs. */
433
+ class WorkerOrphanAliveError extends Error {
434
+ }
435
+ const admissionError = (error, fallback) => error instanceof WorkerOrphanAliveError ? error.message : fallback;
429
436
  /**
430
437
  * The mini workflow engine: executes a `WorkflowDef` against a repo, one step
431
438
  * at a time, persisting every event to the RunStore (which the SSE endpoints
@@ -455,6 +462,13 @@ export class RunManager {
455
462
  beginWorkerExecution(runId, admitted = true) {
456
463
  if (this.store.getRun(runId)?.delegation?.role !== 'worker')
457
464
  return;
465
+ // #469: one site for Continue, --resume, parent replies and queued revival after a crash.
466
+ // Fresh: admission is one-shot, so a cached "alive" must not refuse an orphan that has since died.
467
+ if (!this.executions.has(runId))
468
+ this.settleOrphanedWorkerExecution(runId, { admitting: true, fresh: true });
469
+ const refusal = this.executions.has(runId) ? undefined : this.orphanAdmissionRefusal(runId);
470
+ if (refusal)
471
+ throw new WorkerOrphanAliveError(refusal);
458
472
  const existing = this.executions.get(runId);
459
473
  if (existing) {
460
474
  const proof = this.store.readWorkerExecution(runId);
@@ -469,6 +483,9 @@ export class RunManager {
469
483
  const promise = new Promise(done => { resolve = done; });
470
484
  this.executions.set(runId, { generation, admitted, promise, resolve, sessions: new Set(), turns: new Set(), deliveries: new Set() });
471
485
  this.finalizedWorkers.delete(runId);
486
+ // #469: an earlier generation's destroy blocker never describes this one.
487
+ this.orphanBlockers.delete(runId);
488
+ this.reportedOrphanBlockers.delete(runId);
472
489
  this.stoppedWorkers.delete(runId);
473
490
  }
474
491
  async finishWorkerExecution(runId) {
@@ -518,6 +535,196 @@ export class RunManager {
518
535
  }
519
536
  catch { /* Finalized in this process; preserve exact generation for an explicit retry. */ }
520
537
  }
538
+ /** #469: the generation a dead controller left `starting`, if this manager may judge it. A
539
+ * generation this process controls is never judged here: the in-memory execution map owns it,
540
+ * and a disposed manager's still-running sessions look like orphans. `busy` is transient (this
541
+ * manager holds the run); `unknown` is a present record that proves nothing. */
542
+ orphanState(runId, admitting = false) {
543
+ const run = this.store.getRun(runId);
544
+ if (run?.delegation?.role !== 'worker' || this.disposed)
545
+ return { state: 'none' };
546
+ if (this.executions.has(runId) || this.active.has(runId) || this.starting.has(runId) || (!admitting && this.queue.includes(runId)))
547
+ return { state: 'busy' };
548
+ const proof = this.store.readWorkerExecution(runId);
549
+ if (proof?.phase !== 'starting')
550
+ return { state: 'none' };
551
+ const record = this.store.readWorkerProcesses(runId, proof.generation);
552
+ // A present record that proves nothing (unreadable, malformed, another generation) blocks both
553
+ // finalization and reaping; only an absent one is legacy, scan-only evidence.
554
+ if (record === 'unknown')
555
+ return { state: 'unknown' };
556
+ if (record !== 'absent' && isCurrentProcess(record.controller))
557
+ return { state: 'none' };
558
+ // Finalization deletes the scratch too, so a process working there keeps the generation alive.
559
+ return { state: 'orphan', generation: proof.generation, ...(record === 'absent' ? {} : { record }),
560
+ paths: [run.delegation.workspace.path, ...agentTmpDirLocations(this.dataDir, runId)],
561
+ // No process of this worker can predate its record (1 s slack for tick rounding).
562
+ ...(Number.isFinite(Date.parse(run.createdAt)) ? { since: Date.parse(run.createdAt) - 1_000 } : {}) };
563
+ }
564
+ orphanedWorkerGeneration(runId, admitting = false) {
565
+ const orphan = this.orphanState(runId, admitting);
566
+ return orphan.state === 'orphan' ? orphan : undefined;
567
+ }
568
+ /** Completes a crashed generation's proof once every one of its processes is proven gone (#469).
569
+ * Sync and signal-free; recovery, resume, delivery, collect and destroy then take their normal paths.
570
+ * A non-gone probe is cached briefly: on darwin the scan is a synchronous `lsof` on the event loop. */
571
+ settleOrphanedWorkerExecution(runId, opts = {}) {
572
+ const orphan = this.orphanedWorkerGeneration(runId, opts.admitting);
573
+ if (!orphan)
574
+ return false;
575
+ const cached = this.orphanProbes.get(runId);
576
+ if (!opts.fresh && cached?.generation === orphan.generation && Date.now() - cached.at < ORPHAN_PROBE_CACHE_MS)
577
+ return false;
578
+ const probe = inspectGeneration(orphan);
579
+ if (probe.liveness !== 'gone') {
580
+ this.orphanProbes.set(runId, { generation: orphan.generation, at: Date.now(), probe });
581
+ return false;
582
+ }
583
+ // A stale `alive` must not outlive a `gone` probe, even when the commit below fails.
584
+ this.orphanProbes.delete(runId);
585
+ try {
586
+ if (!this.store.commitWorkerExecutionComplete(runId, orphan.generation))
587
+ return false;
588
+ }
589
+ catch {
590
+ return false;
591
+ }
592
+ this.orphanProbes.delete(runId);
593
+ this.orphanBlockers.delete(runId);
594
+ this.reportedOrphanBlockers.delete(runId);
595
+ this.clearOrphanReprobe(runId);
596
+ this.store.appendEvent(runId, { type: 'lifecycle', message: "the interrupted worker's processes are gone; its execution was finalized" });
597
+ removeAgentTmpDir(this.dataDir, runId);
598
+ return true;
599
+ }
600
+ orphanProbes = new Map();
601
+ orphanReprobes = new Map();
602
+ orphanBlockers = new Map();
603
+ reportedOrphanBlockers = new Map();
604
+ // Private and overridable so tests need not wait out production cadence.
605
+ orphanReprobeMs = 15_000;
606
+ orphanReprobeLimitMs = 15 * 60_000;
607
+ orphanReprobeSlowMs = 60_000;
608
+ orphanTermGraceMs = 10_000;
609
+ /** #469: a survivor that dies after recovery has no other wake source (no exit callback for a
610
+ * process another cezar spawned). Unref'd; finalization's `run` event then lets worker waits and
611
+ * outcomes observe it. Fast for the first window, then slow but uncapped, so a long-lived
612
+ * survivor is still noticed when it exits. A tick while this manager holds the run is skipped;
613
+ * only terminal reasons (proof settled, generation changed, run gone, unknown record, dispose)
614
+ * stop it. */
615
+ armOrphanReprobe(runId) {
616
+ const armed = this.orphanedWorkerGeneration(runId);
617
+ if (this.disposed || this.orphanReprobes.has(runId) || !armed)
618
+ return;
619
+ const slowAfter = Date.now() + this.orphanReprobeLimitMs;
620
+ const schedule = () => {
621
+ const timer = setTimeout(() => {
622
+ const orphan = this.orphanState(runId);
623
+ if (orphan.state === 'busy')
624
+ return schedule();
625
+ if (orphan.state !== 'orphan' || orphan.generation !== armed.generation || this.settleOrphanedWorkerExecution(runId))
626
+ this.clearOrphanReprobe(runId);
627
+ else
628
+ schedule();
629
+ }, Date.now() < slowAfter ? this.orphanReprobeMs : this.orphanReprobeSlowMs);
630
+ timer.unref?.();
631
+ this.orphanReprobes.set(runId, timer);
632
+ };
633
+ schedule();
634
+ }
635
+ clearOrphanReprobe(runId) {
636
+ const timer = this.orphanReprobes.get(runId);
637
+ if (timer)
638
+ clearTimeout(timer);
639
+ this.orphanReprobes.delete(runId);
640
+ }
641
+ /** Why the last destroy could not prove termination (#469), and whether that differs from the
642
+ * reason last reported, so a retry loop appends one lifecycle event per distinct blocker set. */
643
+ takeWorkerTerminationBlocker(runId) {
644
+ const blocker = this.orphanBlockers.get(runId);
645
+ if (!blocker)
646
+ return undefined;
647
+ const key = JSON.stringify(blocker);
648
+ const changed = this.reportedOrphanBlockers.get(runId) !== key;
649
+ this.reportedOrphanBlockers.set(runId, key);
650
+ return { blocker, changed };
651
+ }
652
+ /** Continue over a live orphan names what still runs; other refusals keep the generic text. */
653
+ orphanAdmissionRefusal(runId) {
654
+ const orphan = this.orphanedWorkerGeneration(runId, true);
655
+ const probe = orphan && this.orphanProbes.get(runId);
656
+ if (!probe || probe.generation !== orphan.generation)
657
+ return undefined;
658
+ if (probe.probe.controller !== undefined)
659
+ return `the worker is still controlled by a live cezar (pid ${probe.probe.controller})`;
660
+ const pid = probe.probe.pids[0];
661
+ return pid === undefined ? undefined : `a process of the previous execution is still running (pid ${pid})`;
662
+ }
663
+ /** Destroy only (#469): signal the recorded, token-verified survivors of a generation whose
664
+ * controller is proven dead, then finalize on `gone`. Scan-only processes are never signalled. */
665
+ async reapOrphanedWorker(runId, timeoutMs) {
666
+ const deadline = Date.now() + Math.min(30_000, Math.max(0, Number.isFinite(timeoutMs) ? timeoutMs : 0));
667
+ this.orphanBlockers.delete(runId);
668
+ const initial = this.orphanState(runId);
669
+ if (initial.state === 'unknown') {
670
+ this.orphanBlockers.set(runId, { kind: 'unreadable' });
671
+ return;
672
+ }
673
+ const orphan = this.orphanedWorkerGeneration(runId);
674
+ if (!orphan || this.settleOrphanedWorkerExecution(runId, { fresh: true }))
675
+ return;
676
+ const same = () => this.orphanedWorkerGeneration(runId)?.generation === orphan.generation;
677
+ // >= 500 ms: every probe may be a synchronous darwin `lsof`.
678
+ const pause = () => new Promise(resolve => setTimeout(resolve, Math.max(0, Math.min(500, deadline - Date.now()))));
679
+ // A live controller is another cezar's: nothing to signal or wait for. Otherwise signal only
680
+ // recorded, token-verified survivors (none for a legacy record-less generation), then wait
681
+ // out every other holder until the deadline; the scan's processes are never signalled.
682
+ if (!orphan.record || !recordedProcessLive(orphan.record.controller)) {
683
+ if (orphan.record) {
684
+ const targets = orphan.record.processes.filter(entry => entry.startToken !== undefined && recordedProcessLive(entry));
685
+ const signal = (name) => {
686
+ for (const entry of targets) {
687
+ if (!same())
688
+ return;
689
+ // Re-verified immediately before every signal, exactly: a reused PID is never touched.
690
+ if (processStartToken(entry.pid) === entry.startToken)
691
+ try {
692
+ process.kill(entry.pid, name);
693
+ }
694
+ catch { /* already gone */ }
695
+ }
696
+ };
697
+ signal('SIGTERM');
698
+ const killAt = Math.min(deadline, Date.now() + this.orphanTermGraceMs);
699
+ while (Date.now() < killAt && targets.some(recordedProcessLive))
700
+ await pause();
701
+ if (targets.some(recordedProcessLive))
702
+ signal('SIGKILL');
703
+ }
704
+ while (!this.settleOrphanedWorkerExecution(runId, { fresh: true }) && Date.now() < deadline && same())
705
+ await pause();
706
+ }
707
+ if (!same())
708
+ return;
709
+ const probe = this.orphanProbes.get(runId)?.probe;
710
+ if (probe?.controller !== undefined)
711
+ this.orphanBlockers.set(runId, { kind: 'controller', pid: probe.controller });
712
+ else if (probe?.pids.length)
713
+ this.orphanBlockers.set(runId, { kind: 'processes', pids: probe.pids });
714
+ }
715
+ /** Best effort: a failed write leaves the working-directory scan as this process's evidence. */
716
+ recordWorkerProcess(runId, pid) {
717
+ const generation = this.executions.get(runId)?.generation;
718
+ if (!generation)
719
+ return;
720
+ try {
721
+ if (!this.store.appendWorkerProcess(runId, generation, pid))
722
+ console.warn(`[cez] worker ${runId} process record unavailable; relying on the working-directory scan`);
723
+ }
724
+ catch {
725
+ console.warn(`[cez] worker ${runId} process record write failed; relying on the working-directory scan`);
726
+ }
727
+ }
521
728
  trackTurnEnd(runId, text) {
522
729
  const task = this.recordTurnEnd(runId, text);
523
730
  const turns = this.executions.get(runId)?.turns;
@@ -532,12 +739,16 @@ export class RunManager {
532
739
  this.cancel(runId);
533
740
  return { workerId: runId, state: !this.executions.has(runId) && this.store.readWorkerExecution(runId)?.phase === 'complete' ? 'terminated' : 'stopping' };
534
741
  }
535
- async awaitRunTermination(runId, timeoutMs) {
742
+ async awaitRunTermination(runId, timeoutMs, opts = {}) {
536
743
  const execution = this.executions.get(runId);
537
744
  if (!execution) {
538
745
  const generation = this.finalizedWorkers.get(runId);
539
746
  if (!this.disposed && generation)
540
747
  this.persistWorkerCompletion(runId, generation);
748
+ else if (!generation && opts.reapOrphans)
749
+ await this.reapOrphanedWorker(runId, timeoutMs);
750
+ else if (!generation)
751
+ this.settleOrphanedWorkerExecution(runId);
541
752
  return this.store.readWorkerExecution(runId)?.phase === 'complete';
542
753
  }
543
754
  if (this.disposed)
@@ -717,6 +928,11 @@ export class RunManager {
717
928
  this.ciResources.release();
718
929
  this.delegationProvisioner = undefined;
719
930
  this.finalizedWorkers.clear();
931
+ for (const runId of [...this.orphanReprobes.keys()])
932
+ this.clearOrphanReprobe(runId);
933
+ this.orphanProbes.clear();
934
+ this.orphanBlockers.clear();
935
+ this.reportedOrphanBlockers.clear();
720
936
  for (const settle of this.terminationWaiters)
721
937
  settle();
722
938
  this.store.off('run', this.onDelegationRun);
@@ -1480,11 +1696,11 @@ export class RunManager {
1480
1696
  try {
1481
1697
  this.beginWorkerExecution(candidate);
1482
1698
  }
1483
- catch {
1699
+ catch (error) {
1484
1700
  this.queue.splice(next, 1);
1485
1701
  this.pendingJobs.delete(candidate);
1486
1702
  this.pendingContinuations.delete(candidate);
1487
- this.store.updateRun(candidate, { status: 'failed', error: 'worker execution checkpoint unavailable', finishedAt: new Date().toISOString() });
1703
+ this.store.updateRun(candidate, { status: 'failed', error: admissionError(error, 'worker execution checkpoint unavailable'), finishedAt: new Date().toISOString() });
1488
1704
  continue;
1489
1705
  }
1490
1706
  }
@@ -1722,6 +1938,11 @@ export class RunManager {
1722
1938
  this.store.appendEvent(run.id, { type: 'note', message: `replaying ${replayed.length} message${replayed.length === 1 ? '' : 's'} accepted before the restart but not read` });
1723
1939
  }
1724
1940
  }
1941
+ // #469: a generation whose processes died with the old controller is finalized first, so it
1942
+ // re-launches through the ordinary Continue path and its scratch is not retained.
1943
+ for (const run of this.store.listRuns())
1944
+ if (run.delegation?.role === 'worker' && !this.settleOrphanedWorkerExecution(run.id))
1945
+ this.armOrphanReprobe(run.id);
1725
1946
  this.reconcileWorkerWaits();
1726
1947
  const live = this.store
1727
1948
  .listRuns()
@@ -4503,8 +4724,8 @@ export class RunManager {
4503
4724
  } };
4504
4725
  this.beginWorkerExecution(runId, false);
4505
4726
  }
4506
- catch {
4507
- return { ok: false, error: 'original execution checkpoint unavailable' };
4727
+ catch (error) {
4728
+ return { ok: false, error: admissionError(error, 'original execution checkpoint unavailable') };
4508
4729
  }
4509
4730
  const content = [
4510
4731
  ...(opts.text?.trim() ? [{ type: 'text', text: opts.text.trim() }] : []), ...(opts.images ?? []),
@@ -4525,8 +4746,8 @@ export class RunManager {
4525
4746
  try {
4526
4747
  this.beginWorkerExecution(runId, !deferForCapacity);
4527
4748
  }
4528
- catch {
4529
- return { ok: false, error: 'worker execution checkpoint unavailable' };
4749
+ catch (error) {
4750
+ return { ok: false, error: admissionError(error, 'worker execution checkpoint unavailable') };
4530
4751
  }
4531
4752
  // Everything that could refuse this continuation has now passed, so a pending usage-limit
4532
4753
  // resume is superseded either way: this IS that resume (it re-stamps its own counter), or a
@@ -5168,8 +5389,10 @@ export class RunManager {
5168
5389
  try {
5169
5390
  this.flushDeferred(runId);
5170
5391
  this.flushAgentInputs(runId);
5171
- if (session.pid !== undefined)
5392
+ if (session.pid !== undefined) {
5172
5393
  registerRunProcess(runId, session.pid);
5394
+ this.recordWorkerProcess(runId, session.pid);
5395
+ }
5173
5396
  setupComplete = true;
5174
5397
  const result = await session.result.finally(() => state.agentInputFlight?.settled).finally(() => this.requeueUnreadAtClose(runId, state, session));
5175
5398
  if (this.preserveRunAfterDisposal(runId, state))
@@ -6033,8 +6256,10 @@ export class RunManager {
6033
6256
  try {
6034
6257
  this.flushDeferred(runId);
6035
6258
  this.flushAgentInputs(runId);
6036
- if (session.pid !== undefined)
6259
+ if (session.pid !== undefined) {
6037
6260
  registerRunProcess(runId, session.pid);
6261
+ this.recordWorkerProcess(runId, session.pid);
6262
+ }
6038
6263
  setupComplete = true;
6039
6264
  const result = await session.result.finally(() => state.agentInputFlight?.settled).finally(() => this.requeueUnreadAtClose(runId, state, session));
6040
6265
  if (this.isDisposedDelegatedRun(runId))