@ferris1225/pi-subagents 4.3.9 → 4.3.10

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.
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Detached Git worktree isolation for write-capable sub-agents.
3
3
  *
4
- * A handle is created before a child is queued and stays owned by the logical
5
- * thread across retries, model candidates, and resumes. Finalize
4
+ * A handle is created in the bounded queue and stays owned by the logical run
5
+ * across startup retries and model candidates. Finalize
6
6
  * is idempotent: it records a binary patch, applies it to the original working
7
7
  * tree without touching its index, then removes/prunes the temporary worktree.
8
8
  * Failed integration deliberately retains both the worktree and patch.
@@ -17,7 +17,6 @@ import {
17
17
  runCommand,
18
18
  type CommandResult,
19
19
  type CommandRunner,
20
- WORKTREE_PATCH_MAX_BYTES,
21
20
  } from "./git-command.ts";
22
21
  import { writeTempOwnerMarker } from "./temp-hygiene.ts";
23
22
 
@@ -25,9 +24,7 @@ export type IsolationMode = "shared" | "worktree";
25
24
 
26
25
  const WORKTREE_TEMP_DIR_PREFIX = "pi-subagent-worktree-";
27
26
 
28
- /** Short stable identity of one isolated worktree group (the mkdtemp suffix).
29
- * Continuation generations create a fresh worktree, so the identity
30
- * visibly changes when the group's filesystem boundary changes. */
27
+ /** Short stable identity of one isolated worktree group (the mkdtemp suffix). */
31
28
  export function worktreeGroupId(worktree: Pick<WorktreeIsolation, "tempDir">): string {
32
29
  const base = worktree.tempDir.split(/[\\/]/).filter(Boolean).pop() ?? worktree.tempDir;
33
30
  return base.startsWith(WORKTREE_TEMP_DIR_PREFIX)
@@ -60,9 +57,8 @@ export interface WorktreeCheckpointRef {
60
57
  }
61
58
 
62
59
  /** Persistable projection of one worktree handle: enough to rebuild the
63
- * handle after a reload or restart. Patch bytes are deliberately omitted
64
- * only the checkpoint commit, which lives in the shared repository object
65
- * store, is needed to seed a continuation. */
60
+ * handle after a reload or restart. Patch bytes are deliberately omitted;
61
+ * the checkpoint commit remains in the shared repository as recovery evidence. */
66
62
  export interface WorktreeSnapshot {
67
63
  originalCwd: string;
68
64
  originalRoot: string;
@@ -81,11 +77,6 @@ export interface WorktreeCreateOptions {
81
77
  /** Parent directory for the worktree group: the project-scoped durable
82
78
  * worktrees root, so isolation never lands in the OS temp directory. */
83
79
  tempBaseDir: string;
84
- /** Complete source generation checkpoint merged onto the current HEAD. */
85
- seedCheckpoint?: WorktreeCheckpoint;
86
- /** The seed is already present in the parent checkout, so only later edits
87
- * should be integrated when this continuation settles. */
88
- seedIsIntegrated?: boolean;
89
80
  }
90
81
 
91
82
  export type WorktreeFinalizationStatus = "integrated" | "no_changes" | "retained";
@@ -110,17 +101,12 @@ export interface WorktreeIsolation {
110
101
  readonly tempDir: string;
111
102
  readonly patchPath: string;
112
103
  readonly head: string;
113
- /** Diff base for final integration; a continuation baseline commit when
114
- * the generation was seeded with already-integrated work. */
104
+ /** Diff base retained in snapshots so recovery never reapplies integrated work. */
115
105
  readonly integrationBaseHead: string;
116
106
  readonly state: "active" | "finalizing" | WorktreeFinalizationStatus;
117
- /** Checkpoint retained after finalization for continuation resumes. */
107
+ /** Checkpoint retained after finalization for recovery evidence. */
118
108
  getContinuationCheckpoint(): WorktreeCheckpoint | undefined;
119
- /** Capture the complete isolated filesystem state for a fresh continuation.
120
- * The synthetic commit lets Git merge an already-committed seed without
121
- * attempting to apply the same patch twice. */
122
- snapshotCheckpoint(): Promise<WorktreeCheckpoint>;
123
- /** Remove a newly-created continuation that failed before it was dispatched. */
109
+ /** Remove an unused worktree whose dispatch was cancelled. */
124
110
  discard(): Promise<void>;
125
111
  /** Whether anything is pending against the integration base — the same
126
112
  * question finalization answers as `hadChanges`, asked before settlement so
@@ -319,8 +305,7 @@ class GitWorktreeIsolation implements WorktreeIsolation {
319
305
  private currentState: WorktreeIsolation["state"] = "active";
320
306
  private finalization?: Promise<WorktreeFinalization>;
321
307
  private discardPromise?: Promise<void>;
322
- /** Full workspace checkpoint relative to the generation's starting HEAD,
323
- * retained after cleanup so settled threads can continue safely. */
308
+ /** Full workspace checkpoint retained for manual recovery. */
324
309
  private continuationCheckpoint?: WorktreeCheckpoint;
325
310
 
326
311
  constructor(
@@ -332,8 +317,7 @@ class GitWorktreeIsolation implements WorktreeIsolation {
332
317
  readonly patchPath: string,
333
318
  readonly head: string,
334
319
  private readonly runner: CommandRunner,
335
- /** May be a synthetic tree commit representing a seed that the parent
336
- * checkout already contains. Finalization then integrates only new edits. */
320
+ /** A restored base may represent work already integrated into the parent. */
337
321
  readonly integrationBaseHead: string = head,
338
322
  restored?: {
339
323
  state: WorktreeIsolation["state"];
@@ -354,19 +338,6 @@ class GitWorktreeIsolation implements WorktreeIsolation {
354
338
  return this.continuationCheckpoint ? cloneCheckpoint(this.continuationCheckpoint) : undefined;
355
339
  }
356
340
 
357
- async snapshotCheckpoint(): Promise<WorktreeCheckpoint> {
358
- if (this.continuationCheckpoint) return cloneCheckpoint(this.continuationCheckpoint);
359
- if (this.currentState === "no_changes") {
360
- return { baseHead: this.head, commit: this.head, patch: Buffer.alloc(0) };
361
- }
362
- if (!existsSync(this.worktreePath)) {
363
- throw new Error(`Cannot snapshot isolated worktree after it was removed: ${this.worktreePath}`);
364
- }
365
- const snapshot = await this.collectChanges(this.head);
366
- this.continuationCheckpoint = await this.createCheckpoint(snapshot.stdout);
367
- return cloneCheckpoint(this.continuationCheckpoint);
368
- }
369
-
370
341
  discard(): Promise<void> {
371
342
  if (this.discardPromise) return this.discardPromise;
372
343
  if (this.finalization) {
@@ -385,8 +356,8 @@ class GitWorktreeIsolation implements WorktreeIsolation {
385
356
 
386
357
  async hasPendingChanges(): Promise<boolean> {
387
358
  // A settled worktree already recorded the answer; asking Git again after
388
- // removal would fail. Diffing the integration base (not HEAD) keeps a
389
- // continuation honest: only this generation's own work counts.
359
+ // removal would fail. Diffing the integration base (not HEAD) excludes
360
+ // previously integrated work from the recovered run's pending changes.
390
361
  if (this.currentState === "no_changes") return false;
391
362
  if (this.currentState !== "active" || !existsSync(this.worktreePath)) return true;
392
363
  const diff = await this.collectChanges(this.integrationBaseHead);
@@ -633,50 +604,6 @@ export async function createWorktreeIsolation(
633
604
  await mkdir(isolatedCwd, { recursive: true });
634
605
  await linkNodeModules(runner, target.originalRoot, worktreePath);
635
606
 
636
- let integrationBaseHead = target.head;
637
- const checkpoint = options.seedCheckpoint;
638
- if (checkpoint && checkpoint.patch.length > WORKTREE_PATCH_MAX_BYTES) {
639
- throw new Error(
640
- `Isolated checkpoint exceeds the ${WORKTREE_PATCH_MAX_BYTES}-byte patch limit (${checkpoint.patch.length} bytes).`,
641
- );
642
- }
643
- if (checkpoint && checkpoint.patch.length > 0) {
644
- // Merge the checkpoint commit with today's HEAD instead of blindly
645
- // applying its old patch. If the parent committed generation one after
646
- // integration, Git recognizes the equivalent tree and produces HEAD
647
- // unchanged; unrelated newer commits are preserved by the three-way merge.
648
- const merged = await runGit(
649
- runner,
650
- worktreePath,
651
- ["merge-tree", "--write-tree", "--messages", target.head, checkpoint.commit],
652
- `Merging isolated checkpoint into continuation ${worktreePath}`,
653
- );
654
- const mergedTree = merged.stdout.toString("utf8").split(/\r?\n/, 1)[0]?.trim();
655
- if (!mergedTree) throw new Error("Git returned no merged continuation tree id.");
656
- await runGit(
657
- runner,
658
- worktreePath,
659
- ["read-tree", "--reset", "-u", mergedTree],
660
- `Materializing isolated checkpoint in ${worktreePath}`,
661
- );
662
- if (options.seedIsIntegrated) {
663
- const commit = await runGit(
664
- runner,
665
- worktreePath,
666
- [
667
- "-c", "user.name=pi-subagents",
668
- "-c", "user.email=pi-subagents@example.invalid",
669
- "commit-tree", mergedTree,
670
- "-p", target.head,
671
- "-m", "pi-subagents continuation baseline",
672
- ],
673
- `Creating continuation baseline commit in ${worktreePath}`,
674
- );
675
- integrationBaseHead = commit.stdout.toString("utf8").trim();
676
- if (!integrationBaseHead) throw new Error("Git returned no continuation baseline commit id.");
677
- }
678
- }
679
-
680
607
  return new GitWorktreeIsolation(
681
608
  target.originalCwd,
682
609
  target.originalRoot,
@@ -686,7 +613,6 @@ export async function createWorktreeIsolation(
686
613
  patchPath,
687
614
  target.head,
688
615
  runner,
689
- integrationBaseHead,
690
616
  );
691
617
  } catch (error) {
692
618
  const rollbackErrors: string[] = [];
@@ -795,8 +721,7 @@ export function normalizeWorktreeSnapshot(value: unknown): WorktreeSnapshot | nu
795
721
  /** Rebuild a handle from a persisted snapshot. Returns undefined when the
796
722
  * on-disk worktree that an active/retained snapshot promises is gone; settled
797
723
  * states (integrated/no_changes) intentionally need no filesystem. The
798
- * restored checkpoint carries no patch bytes only its commit is consumed by
799
- * continuation seeds. */
724
+ * restored checkpoint retains its commit identity without duplicating patch bytes. */
800
725
  export async function restoreWorktreeIsolation(
801
726
  snapshot: WorktreeSnapshot,
802
727
  options: { runner?: CommandRunner } = {},
@@ -43,9 +43,8 @@ const THREADS_MANIFEST_VERSION = 1;
43
43
  * win over the age rule. */
44
44
  export const PROJECT_ROOT_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1_000;
45
45
 
46
- /** Fixed retention: parked work (which may hold unintegrated changes) stops
47
- * being resumable after a month. Older manifests may still carry settled
48
- * records from previous versions; restore discards them on sight. */
46
+ /** Interrupted work retains its recovery record for a month. Older manifests
47
+ * may still carry settled records; restore discards those on sight. */
49
48
  export const PARKED_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1_000;
50
49
 
51
50
  /** Result excerpts are for status display after restore, not full transcripts. */
@@ -84,7 +83,7 @@ export interface ThreadRecord {
84
83
  executionCwd: string;
85
84
  /** Resolved (clamped) level of the last generation. */
86
85
  thinkingLevel?: string;
87
- /** Level the dispatch requested; a resume after a restart re-runs at it. */
86
+ /** Originally requested level, retained with the recovery metadata. */
88
87
  requestedThinkingLevel?: string;
89
88
  isolation: IsolationMode;
90
89
  state: "parked" | "completed" | "failed";
@@ -106,9 +105,8 @@ export function currentBootId(now = Date.now()): number {
106
105
  /** Whether a record's `childPids` can still name processes of this boot. Pids
107
106
  * are only unique within a boot: after a restart the same number belongs to
108
107
  * whatever claimed it, so restore must not signal them. Records written before
109
- * this field existed carry no boot id and count as unverifiable leaving a
110
- * stray child alive costs a resumable session nothing, while killing an
111
- * unrelated process tree is not recoverable. */
108
+ * this field existed carry no boot id and count as unverifiable; leaving a
109
+ * stray child alive is safer than killing an unrelated process tree. */
112
110
  export function isCurrentBoot(record: ThreadRecord, now = Date.now()): boolean {
113
111
  if (record.bootId === undefined) return false;
114
112
  return Math.abs(record.bootId - currentBootId(now)) <= BOOT_ID_TOLERANCE_MS;
@@ -375,7 +373,7 @@ function summarizeResult(result: SingleResult): ThreadResultSummary | undefined
375
373
 
376
374
  /** Project a live thread into its durable record. Only handles whose
377
375
  * filesystem is still meaningful are persisted; finalized-and-removed
378
- * worktrees keep just their checkpoint commit for continuation resumes. */
376
+ * worktrees keep their checkpoint commit as recovery evidence. */
379
377
  export function threadRecordFromThread(
380
378
  thread: SubagentThread,
381
379
  state: "parked" | "completed" | "failed",
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Shared per-session runtime state for pi-subagents.
3
3
  *
4
- * The extension registers several tools (subagent, subagent_control/stop)
4
+ * The extension registers dispatch, read-only status, and destructive stop tools
5
5
  * that share the background queue, completion batcher, abort controllers per
6
6
  * run, and settled-results store.
7
7
  * `createRuntime` builds those once per extension load and hands the same object
8
8
  * to every registration site, so state stays in one place without globals.
9
9
  */
10
10
 
11
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
- import type { PhaseScope, PhaseScopeInput } from "../delegation/phase-scope.ts";
11
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
+ import type { PhaseScope } from "../delegation/phase-scope.ts";
13
13
  import { rmSync } from "node:fs";
14
14
  import { resolveSubagentConcurrency, BackgroundTaskQueue } from "../execution/background.ts";
15
15
  import {
@@ -23,21 +23,12 @@ import { type ThinkingLevel } from "../configuration/config.ts";
23
23
  import { removeThreadRecord, threadRecordFromThread, upsertThreadRecord, type ThreadRecord } from "./durable.ts";
24
24
  import { isRunActiveStatus, monitor } from "../presentation/monitor.ts";
25
25
  import type { RpcRunControl } from "../execution/rpc-control.ts";
26
- import type { StartBackgroundInternal } from "./thread-shared.ts";
27
26
  import { isFailedResult, type SingleResult } from "../execution/spawn.ts";
28
27
  import type { IsolationMode, WorktreeFinalization, WorktreeIsolation } from "../isolation/worktree.ts";
29
28
 
30
- export type ThreadState =
31
- | "queued"
32
- | "resuming"
33
- | "running"
34
- | "interrupting"
35
- | "parked"
36
- | "completed"
37
- | "failed"
38
- | "stopped";
29
+ export type ThreadState = "queued" | "running" | "interrupting" | "parked" | "completed" | "failed" | "stopped";
39
30
 
40
- export type ThreadLifecycleOperation = "park" | "resume" | "stop" | "settle";
31
+ export type ThreadLifecycleOperation = "stop" | "settle";
41
32
 
42
33
  export interface SubagentThread {
43
34
  id: number;
@@ -46,53 +37,36 @@ export interface SubagentThread {
46
37
  task: string;
47
38
  phaseId?: string;
48
39
  scope?: PhaseScope;
49
- /** Monotonic continuation scope visible while resume preflight is in flight. */
50
- admissionScope?: PhaseScope;
51
40
  /** Capability snapshot used by declared-scope admission, including after restore. */
52
41
  writeCapable?: boolean;
53
42
  /** Caller-facing cwd in the original worktree. */
54
43
  cwd: string;
55
44
  /** Actual child cwd (the equivalent path inside an isolated worktree). */
56
45
  executionCwd: string;
57
- /** Level actually used, after clamping to the effective model's capability. */
58
46
  thinkingLevel?: ThinkingLevel;
59
- /** Level the dispatch asked for, before clamping; replayed on every resume. */
60
47
  requestedThinkingLevel?: ThinkingLevel;
61
48
  isolation: IsolationMode;
62
49
  worktree?: WorktreeIsolation;
63
- /** Durable restoration failure that permanently blocks continuation. */
64
- resumeUnavailableReason?: string;
65
50
  /** Original durable evidence retained when its worktree handle is unavailable. */
66
51
  restorationRecord?: ThreadRecord;
67
52
  state: ThreadState;
68
53
  control: RpcRunControl;
69
54
  queueController?: AbortController;
70
- /** Resolves only after the current generation's child process, isolation
71
- * finalization, and queue work have fully quiesced and released their
72
- * concurrency slot. */
55
+ /** Resolves after the child, isolation finalization, and queue work fully quiesce. */
73
56
  generationCompletion: Promise<void>;
74
- /** Synchronous CAS used by lifecycle controls across their async preflight. */
57
+ /** Arbitration between asynchronous settlement and destructive stop. */
75
58
  lifecycleVersion: number;
76
59
  lifecycleOperation?: ThreadLifecycleOperation;
77
60
  sessionId?: string;
78
61
  sessionDir?: string;
79
- /** Active execution time accumulated across retained resume generations. */
80
62
  elapsedMs: number;
81
- /** Most recent generation result, retained for parked destructive-stop output. */
63
+ /** Terminal or interrupted partial result; independent of the transient monitor row. */
82
64
  lastResult?: SingleResult;
83
65
  /** A destructive stop retires context even if the active child settles later. */
84
66
  retireOnSettle?: boolean;
85
67
  retired?: boolean;
86
- /** Installed by dispatch so the control tool can restart the same logical id. */
87
- resume: (
88
- objective?: string,
89
- ctx?: ExtensionContext,
90
- metadata?: { scope?: PhaseScopeInput },
91
- ) => Promise<SingleResult>;
92
- /** Dispatch-owned, generation-guarded worktree settlement hook. Its apply
93
- * runs under the canonical original-repository lane. */
68
+ /** All owners finalize under the same original-repository lane. */
94
69
  finalizeIsolation: (generation: number, result?: SingleResult) => Promise<WorktreeFinalization | undefined>;
95
- /** Best-effort shutdown notification for retained integration artifacts. */
96
70
  notifyIsolationFailure?: (finalization: WorktreeFinalization) => void;
97
71
  isolationFailureNotified?: boolean;
98
72
  }
@@ -104,9 +78,6 @@ export interface SubagentRuntime {
104
78
  getActiveTools: () => string[];
105
79
  /** False after session_shutdown; guards delivery and queue work. */
106
80
  sessionActive: boolean;
107
- /** The process-wide background dispatcher. Set at tool registration so
108
- * threads restored from the durable manifest can resume before any dispatch. */
109
- dispatcher?: StartBackgroundInternal;
110
81
  /** Resolves when the load-time durable restore pass has finished. Everything
111
82
  * that answers "which threads exist" awaits it — the lookup tools, a fresh
112
83
  * dispatch before it allocates a run id, and the restored-thread notice — so
@@ -140,9 +111,6 @@ export interface SubagentRuntime {
140
111
  registerRunResult: (runId: number, result: SingleResult) => void;
141
112
  /** Logical threads outlive process attempts and completed generations. */
142
113
  threads: Map<number, SubagentThread>;
143
- /** Resume setup that has claimed a thread but has not yet enqueued its
144
- * next generation. Shutdown invalidates these claims and waits for cleanup. */
145
- preflightOperations: Set<Promise<void>>;
146
114
  /** Every session directory retained for this parent session. */
147
115
  sessionDirs: Set<string>;
148
116
  retainSession: (result: Pick<SingleResult, "sessionDir">) => void;
@@ -242,7 +210,6 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
242
210
  settledRuns: new Map<number, SingleResult>(),
243
211
  settledListeners: new Map<number, Set<(result: SingleResult) => void>>(),
244
212
  threads: new Map<number, SubagentThread>(),
245
- preflightOperations: new Set<Promise<void>>(),
246
213
  sessionDirs: new Set<string>(),
247
214
  retainSession: (result) => {
248
215
  if (result.sessionDir) runtime.sessionDirs.add(result.sessionDir);
@@ -278,11 +245,9 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
278
245
  if (!runtime.sessionActive) return;
279
246
  runtime.sessionActive = false;
280
247
  const shutdownThreads = [...runtime.threads.values()];
281
- const liveStates = new Set(["queued", "resuming", "running", "interrupting"]);
248
+ const liveStates = new Set(["queued", "running", "interrupting"]);
282
249
  const previousStates = new Map(shutdownThreads.map((thread) => [thread.id, thread.state] as const));
283
- // Invalidate every lifecycle claim synchronously before the first await.
284
- // Resume preflight checks both this version and sessionActive, then
285
- // cleans any worktree/session it created before resolving its tracker.
250
+ // Invalidate pending lifecycle claims synchronously before the first await.
286
251
  // A generation already inside its settlement keeps its own claim: it
287
252
  // finalizes its worktree and persists its terminal record itself.
288
253
  const interrupting = shutdownThreads.filter((thread) =>
@@ -299,29 +264,21 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
299
264
  if (thread.lifecycleOperation === "settle") continue;
300
265
  thread.lifecycleOperation = "stop";
301
266
  // Deliberately NOT retireOnSettle: shutdown interrupts to the last
302
- // checkpoint but keeps the session/worktree resumable across reload.
267
+ // checkpoint and preserves session/worktree artifacts for manual recovery.
303
268
  thread.retireOnSettle = false;
304
269
  if (liveStates.has(thread.state)) thread.state = "stopped";
305
270
  }
306
- const preflights = [...runtime.preflightOperations];
307
271
  runtime.completionBatcher.dispose();
308
- // Held items are dropped exactly like the batcher's abandoned ones: the
309
- // session is gone, so there is no window left to deliver them into.
272
+ // The parent session is gone; no window remains for buffered delivery.
310
273
  heldCompletions = [];
311
274
  compactionInFlight = false;
312
275
  runtime.backgroundQueue.cancelAll();
313
- // Await live RPC process-tree cleanup and continuation preflight rollback
314
- // before persisting records or releasing ownership maps.
276
+ // Quiesce child processes and owned queue work before persisting records.
315
277
  await Promise.all([
316
- Promise.all(
317
- interrupting.map((thread) =>
318
- thread.control.stop("Parent session shut down").catch(() => undefined),
319
- ),
320
- ),
321
- Promise.allSettled(preflights),
278
+ ...interrupting.map((thread) => thread.control.stop("Parent session shut down").catch(() => undefined)),
322
279
  runtime.backgroundQueue.waitForIdle(),
323
280
  ]);
324
- // Only interrupted (parked) threads stay resumable across reloads:
281
+ // Only interrupted work keeps recovery artifacts across reloads:
325
282
  // each keeps its durable record and retained artifacts. Settled
326
283
  // threads drop their record — the manifest exists only while
327
284
  // unfinished work needs it — and their sessions are deleted now. A
@@ -332,16 +289,13 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
332
289
  const records: ThreadRecord[] = [];
333
290
  for (const thread of runtime.threads.values()) {
334
291
  if (thread.retired) continue;
335
- if (thread.resumeUnavailableReason) {
336
- // Restoration failures retain their durable evidence and session until
337
- // an explicit destructive stop retires them.
338
- if (thread.restorationRecord) {
339
- records.push({
340
- ...thread.restorationRecord,
341
- updatedAt: Date.now(),
342
- elapsedMs: thread.elapsedMs,
343
- });
344
- }
292
+ if (thread.restorationRecord) {
293
+ // Keep recovery evidence until an explicit destructive stop retires it.
294
+ records.push({
295
+ ...thread.restorationRecord,
296
+ updatedAt: Date.now(),
297
+ elapsedMs: thread.elapsedMs,
298
+ });
345
299
  continue;
346
300
  }
347
301
  const previous = previousStates.get(thread.id) ?? thread.state;
@@ -382,7 +336,6 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
382
336
  // sessionDirs entries still referenced by records stay owned by the
383
337
  // manifest; the next process re-registers them at restore.
384
338
  runtime.sessionDirs.clear();
385
- runtime.preflightOperations.clear();
386
339
  runtime.threads.clear();
387
340
  monitor.clear();
388
341
  },