@jwilger/pi-development-system 0.48.0 → 0.51.1

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,5 +1,7 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
1
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
+ import { parseSpawnOverrides } from "../../core/spawn-overrides.ts";
3
+ import { isParseError } from "../../core/types.ts";
4
+ import { DEFAULT_MANAGER_SETTINGS, type ManagerSettings } from "../prefs/settings.ts";
3
5
  import type {
4
6
  AgentDriver,
5
7
  AgentType,
@@ -13,21 +15,18 @@ import type {
13
15
  TranscriptObservation,
14
16
  TranscriptSnapshot,
15
17
  } from "../types.ts";
16
- import { immutableTranscriptValue } from "./transcript.ts";
17
- import { parseSpawnOverrides } from "../../core/spawn-overrides.ts";
18
- import { isParseError } from "../../core/types.ts";
19
18
  import { canonicalPath, inheritContext, isDescendant, parentPath } from "./paths.ts";
20
- import { DEFAULT_MANAGER_SETTINGS, type ManagerSettings } from "../prefs/settings.ts";
19
+ import { immutableTranscriptValue } from "./transcript.ts";
21
20
 
22
21
  interface Record {
23
22
  view: ThreadView;
24
23
  definition: AgentType;
25
24
  inherited: AgentMessage[];
26
- contextReady?: Promise<void>;
25
+ contextReady?: Promise<void> | undefined;
27
26
  contextPending?: boolean;
28
27
  startup: AbortController;
29
28
  driver?: AgentDriver;
30
- initializing?: Promise<AgentDriver>;
29
+ initializing?: Promise<AgentDriver> | undefined;
31
30
  run?: Promise<void>;
32
31
  started?: Promise<void>;
33
32
  sessionLeafId?: string | null;
@@ -40,18 +39,26 @@ interface Record {
40
39
  stopRequested: boolean;
41
40
  observationRevision?: number;
42
41
  driverGeneration?: number;
43
- transcriptSource?: TranscriptSnapshot;
42
+ transcriptSource?: TranscriptSnapshot | undefined;
44
43
  transcriptListeners?: Set<TranscriptListener>;
45
- detachTranscript?: () => void;
46
- observationError?: string;
44
+ detachTranscript?: (() => void) | undefined;
45
+ observationError?: string | undefined;
47
46
  }
48
47
  const active = (view: Pick<ThreadView, "state">) =>
49
48
  view.state === "starting" || view.state === "running";
49
+ const settledState = (record: {
50
+ stopRequested: boolean;
51
+ pauseRequested: boolean;
52
+ }): ThreadView["state"] => {
53
+ if (record.stopRequested) return "stopped";
54
+ return record.pauseRequested ? "paused" : "completed";
55
+ };
56
+ const swallow = (): undefined => undefined;
50
57
  const errorText = (error: unknown) => (error instanceof Error ? error.message : String(error));
51
58
 
52
59
  function abortable<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
53
60
  if (signal.aborted) {
54
- void work.catch(() => {});
61
+ work.catch(swallow);
55
62
  return Promise.reject(signal.reason);
56
63
  }
57
64
  return new Promise((resolve, reject) => {
@@ -75,7 +82,10 @@ export class ThreadManager {
75
82
  private records = new Map<string, Record>();
76
83
  private disposed = false;
77
84
  private epoch = 0;
78
- constructor(private options: ManagerOptions) {}
85
+ private options: ManagerOptions;
86
+ constructor(options: ManagerOptions) {
87
+ this.options = options;
88
+ }
79
89
 
80
90
  /** New limits affect future launches/resumes, never cancel existing work. */
81
91
  setLimits(limits: ManagerSettings): void {
@@ -158,14 +168,14 @@ export class ThreadManager {
158
168
  return {
159
169
  view,
160
170
  definition: structuredClone(record.definition),
161
- ...(!view.sessionFile && !record.contextPending
171
+ ...(!(view.sessionFile || record.contextPending)
162
172
  ? { inherited: structuredClone(record.inherited) }
163
173
  : {}),
164
174
  };
165
175
  });
166
176
  }
167
177
  restore(saved: SavedThread[]): void {
168
- if (this.records.size) throw new Error("Restore requires an empty thread registry");
178
+ if (this.records.size > 0) throw new Error("Restore requires an empty thread registry");
169
179
  const restored = new Map<string, Record>();
170
180
  for (const item of saved) {
171
181
  const savedView = structuredClone(item.view);
@@ -187,12 +197,12 @@ export class ThreadManager {
187
197
  inputTokens: storedView.inputTokens ?? 0,
188
198
  outputTokens: storedView.outputTokens ?? 0,
189
199
  };
190
- delete view.startedAt;
200
+ view.startedAt = undefined;
191
201
  if (view.parent !== parentPath(path)) throw new Error(`Invalid saved parent for ${path}`);
192
202
  if (active(view)) {
193
203
  view.state = "paused";
194
204
  view.status = "Interrupted by reload; send input to resume";
195
- delete view.output;
205
+ view.output = undefined;
196
206
  }
197
207
  restored.set(path, {
198
208
  view,
@@ -201,9 +211,7 @@ export class ThreadManager {
201
211
  // A reservation saved before its lexical parent opened has no snapshot yet.
202
212
  contextPending: !view.sessionFile && item.inherited === undefined && view.parent !== null,
203
213
  startup: new AbortController(),
204
- ...(Object.prototype.hasOwnProperty.call(savedView, "sessionLeafId")
205
- ? { sessionLeafId }
206
- : {}),
214
+ ...(Object.hasOwn(savedView, "sessionLeafId") ? { sessionLeafId } : {}),
207
215
  pauseRequested: false,
208
216
  stopRequested: false,
209
217
  });
@@ -257,19 +265,15 @@ export class ThreadManager {
257
265
  : level > maxLevels
258
266
  )
259
267
  throw new Error("Agent depth limit reached");
260
- this.archiveFinished(this.options.maxThreads ?? DEFAULT_MANAGER_SETTINGS.maxThreads);
261
268
  this.assertCapacity();
262
269
  const type = structuredClone(this.options.getType(args.type));
263
270
  if (overrides !== undefined) type.spawnOverrides = { ...overrides };
264
271
  const parentRecord = parent && parent !== "/root" ? this.record(parent) : undefined;
265
272
  this.assertStartableAncestors(path);
273
+ // Last, so a spawn refused by any check above drops nothing.
274
+ this.archiveFinished(this.options.maxThreads ?? DEFAULT_MANAGER_SETTINGS.maxThreads, path);
266
275
  // Reservation is synchronous: subtree cancellation sees children even during lazy parent reopen.
267
- const inherited =
268
- parent === "/root"
269
- ? inheritContext(this.options.rootSnapshot())
270
- : parentRecord?.driver
271
- ? inheritContext(parentRecord.driver.snapshot())
272
- : [];
276
+ const inherited = this.inheritedFor(parent, parentRecord);
273
277
  const now = Date.now();
274
278
  const record: Record = {
275
279
  view: {
@@ -298,7 +302,9 @@ export class ThreadManager {
298
302
  this.records.set(path, record);
299
303
  if (record.contextPending) this.prepareInheritedContext(record);
300
304
  this.start(record, args.task);
301
- return args.wait === false ? this.view(record) : this.wait(caller, path, undefined, signal);
305
+ return args.wait === false
306
+ ? this.view(record)
307
+ : await this.wait(caller, path, undefined, signal);
302
308
  }
303
309
 
304
310
  async steer(caller: string, path: string, message: string): Promise<ThreadView> {
@@ -321,7 +327,9 @@ export class ThreadManager {
321
327
  if (record.stopRequested && active(record.view))
322
328
  throw new Error("Thread is stopping; wait for stopped state before resuming");
323
329
  if (record.view.state === "running") {
324
- await record.driver!.steer(message);
330
+ const live = record.driver;
331
+ if (live === undefined) throw new Error("Running thread has no driver");
332
+ await live.steer(message);
325
333
  this.touch(record);
326
334
  return this.view(record);
327
335
  }
@@ -351,12 +359,13 @@ export class ThreadManager {
351
359
  const finish = (error?: Error) => {
352
360
  if (timer) clearTimeout(timer);
353
361
  signal?.removeEventListener("abort", cancel);
354
- error ? reject(error) : resolve();
362
+ if (error) reject(error);
363
+ else resolve();
355
364
  };
356
365
  const cancel = () => finish(new Error("Waiting cancelled; thread remains running"));
357
366
  signal?.addEventListener("abort", cancel, { once: true });
358
367
  if (timeoutMs !== undefined) timer = setTimeout(() => finish(), timeoutMs);
359
- record.run!.then(
368
+ (record.run ?? Promise.resolve()).then(
360
369
  () => finish(),
361
370
  (error) => finish(new Error(errorText(error))),
362
371
  );
@@ -404,7 +413,7 @@ export class ThreadManager {
404
413
  await item.run;
405
414
  item.view.state = "stopped";
406
415
  item.view.status = "Stopped; session retained";
407
- delete item.view.output;
416
+ item.view.output = undefined;
408
417
  this.touch(item);
409
418
  }),
410
419
  );
@@ -429,14 +438,15 @@ export class ThreadManager {
429
438
  // Idle/restored history may be lazily loaded, but attachment never prompts/resumes a turn.
430
439
  if (!active(record.view)) await this.ensureDriver(record, true);
431
440
  this.assertLive();
432
- const listeners = (record.transcriptListeners ??= new Set());
441
+ const listeners = record.transcriptListeners ?? new Set<TranscriptListener>();
442
+ record.transcriptListeners = listeners;
433
443
  const registered: TranscriptListener = (snapshot) => listener(snapshot);
434
444
  listeners.add(registered);
435
445
  if (record.driver) this.attachTranscript(record, record.driver);
436
446
  else {
437
447
  // A starting child gets a placeholder immediately; existing startup supplies the live source.
438
- void this.ensureDriver(record, true).catch((error) => {
439
- if (this.disposed || !listeners.has(registered)) return;
448
+ this.ensureDriver(record, true).catch((error) => {
449
+ if (this.isShutDown() || !listeners.has(registered)) return;
440
450
  record.observationError = errorText(error);
441
451
  this.publishTranscript(record);
442
452
  });
@@ -445,7 +455,7 @@ export class ThreadManager {
445
455
  snapshot: this.transcriptSnapshot(record),
446
456
  unsubscribe: () => {
447
457
  listeners.delete(registered);
448
- if (!listeners.size) {
458
+ if (listeners.size === 0) {
449
459
  record.detachTranscript?.();
450
460
  record.detachTranscript = undefined;
451
461
  record.transcriptSource = undefined;
@@ -473,7 +483,7 @@ export class ThreadManager {
473
483
  await Promise.all(
474
484
  records.map(async (record) => {
475
485
  await record.driver?.abort();
476
- await record.initializing?.catch(() => {});
486
+ await record.initializing?.catch(swallow);
477
487
  await record.run;
478
488
  record.driver?.dispose();
479
489
  }),
@@ -488,8 +498,8 @@ export class ThreadManager {
488
498
  record.view.state = "starting";
489
499
  record.view.status = "Starting";
490
500
  record.view.startedAt = Date.now();
491
- delete record.view.output;
492
- delete record.view.error;
501
+ record.view.output = undefined;
502
+ record.view.error = undefined;
493
503
  const epoch = this.epoch;
494
504
  let markStarted!: () => void;
495
505
  record.started = new Promise((resolve) => {
@@ -501,7 +511,7 @@ export class ThreadManager {
501
511
  if (contextReady) await abortable(contextReady, record.startup.signal);
502
512
  record.startup.signal.throwIfAborted();
503
513
  const driver = await this.ensureDriver(record);
504
- if (record.stopRequested || this.disposed) {
514
+ if (record.stopRequested || this.isShutDown()) {
505
515
  record.view.state = "stopped";
506
516
  return;
507
517
  }
@@ -511,11 +521,7 @@ export class ThreadManager {
511
521
  this.touch(record);
512
522
  markStarted();
513
523
  await task;
514
- record.view.state = record.stopRequested
515
- ? "stopped"
516
- : record.pauseRequested
517
- ? "paused"
518
- : "completed";
524
+ record.view.state = settledState(record);
519
525
  if (record.view.state === "completed") {
520
526
  record.view.output = driver.output();
521
527
  record.view.status = "Completed; session retained";
@@ -531,7 +537,7 @@ export class ThreadManager {
531
537
  record.liveOutputTokens = 0;
532
538
  markStarted(); // Failed/cancelled initialization must also release callers waiting to steer.
533
539
  this.touch(record);
534
- if (!this.disposed && epoch === this.epoch)
540
+ if (!this.isShutDown() && epoch === this.epoch)
535
541
  this.options.onEvent?.({
536
542
  kind: "settled",
537
543
  thread: this.view(record),
@@ -586,7 +592,7 @@ export class ThreadManager {
586
592
  return this.options.createDriver(options);
587
593
  })
588
594
  .then(async (driver) => {
589
- if (this.disposed || signal.aborted) {
595
+ if (this.isShutDown() || signal.aborted) {
590
596
  await driver.abort();
591
597
  driver.dispose();
592
598
  throw new Error("Driver startup cancelled");
@@ -615,7 +621,8 @@ export class ThreadManager {
615
621
  private attachTranscript(record: Record, driver: AgentDriver): void {
616
622
  if (record.detachTranscript || !record.transcriptListeners?.size) return;
617
623
  const observation = driver.observeTranscript?.((snapshot) => {
618
- if (this.disposed || record.driver !== driver || !record.transcriptListeners?.size) return;
624
+ if (this.isShutDown() || record.driver !== driver || !record.transcriptListeners?.size)
625
+ return;
619
626
  record.transcriptSource = snapshot;
620
627
  this.publishTranscript(record);
621
628
  });
@@ -641,7 +648,7 @@ export class ThreadManager {
641
648
  });
642
649
  }
643
650
  private publishTranscript(record: Record): void {
644
- if (this.disposed || !record.transcriptListeners?.size) return;
651
+ if (this.isShutDown() || !record.transcriptListeners?.size) return;
645
652
  record.observationRevision = (record.observationRevision ?? 0) + 1;
646
653
  const snapshot = this.transcriptSnapshot(record);
647
654
  for (const listener of record.transcriptListeners) {
@@ -655,7 +662,7 @@ export class ThreadManager {
655
662
  private touch(record: Record): void {
656
663
  record.view.updatedAt = Date.now();
657
664
  this.publishTranscript(record);
658
- if (!this.disposed) this.options.onEvent?.({ kind: "change", thread: this.view(record) });
665
+ if (!this.isShutDown()) this.options.onEvent?.({ kind: "change", thread: this.view(record) });
659
666
  }
660
667
  /** Fold driver-cumulative usage into persisted totals. Partials only refresh the live view. */
661
668
  private applyUsage(
@@ -669,7 +676,8 @@ export class ThreadManager {
669
676
  if (input === record.liveInputTokens && output === record.liveOutputTokens) return;
670
677
  record.liveInputTokens = input;
671
678
  record.liveOutputTokens = output;
672
- if (!this.disposed) this.options.onEvent?.({ kind: "metrics", thread: this.view(record) });
679
+ if (!this.isShutDown())
680
+ this.options.onEvent?.({ kind: "metrics", thread: this.view(record) });
673
681
  return;
674
682
  }
675
683
  record.view.inputTokens =
@@ -685,7 +693,7 @@ export class ThreadManager {
685
693
  const startedAt = record.view.startedAt;
686
694
  if (startedAt === undefined) return;
687
695
  record.view.elapsedMs = (record.view.elapsedMs ?? 0) + Math.max(0, Date.now() - startedAt);
688
- delete record.view.startedAt;
696
+ record.view.startedAt = undefined;
689
697
  }
690
698
  private prepareInheritedContext(record: Record, readOnly = false): Promise<void> | undefined {
691
699
  if (!record.contextPending) return record.contextReady;
@@ -709,7 +717,7 @@ export class ThreadManager {
709
717
  throw error;
710
718
  });
711
719
  record.contextReady = ready;
712
- void ready.catch(() => {});
720
+ ready.catch(swallow);
713
721
  return ready;
714
722
  }
715
723
  private sessionLeafId(record: Record): string | null | undefined {
@@ -730,7 +738,7 @@ export class ThreadManager {
730
738
  sessionFile: record.driver ? record.driver.sessionFile : record.view.sessionFile,
731
739
  });
732
740
  // devsys: show per-spawn pins so agent_status reports what the thread was told to run on.
733
- if (record.definition?.spawnOverrides) view.pinned = { ...record.definition.spawnOverrides };
741
+ if (record.definition.spawnOverrides) view.pinned = { ...record.definition.spawnOverrides };
734
742
  if (record.liveInputTokens) view.inputTokens = (view.inputTokens ?? 0) + record.liveInputTokens;
735
743
  if (record.liveOutputTokens)
736
744
  view.outputTokens = (view.outputTokens ?? 0) + record.liveOutputTokens;
@@ -741,8 +749,16 @@ export class ThreadManager {
741
749
  if (!record) throw new Error(`Unknown thread ${path}`);
742
750
  return record;
743
751
  }
752
+ private inheritedFor(parent: string | null, parentRecord: Record | undefined): AgentMessage[] {
753
+ if (parent === "/root") return inheritContext(this.options.rootSnapshot());
754
+ return parentRecord?.driver ? inheritContext(parentRecord.driver.snapshot()) : [];
755
+ }
756
+ /** A method, not a field read: the flag flips across awaits, which static narrowing cannot see. */
757
+ private isShutDown(): boolean {
758
+ return this.disposed;
759
+ }
744
760
  private assertLive(): void {
745
- if (this.disposed) throw new Error("Thread manager has shut down");
761
+ if (this.isShutDown()) throw new Error("Thread manager has shut down");
746
762
  }
747
763
  private assertCallerMaySteer(caller: string): void {
748
764
  if (caller !== "/root" && this.record(caller).stopRequested)
@@ -765,16 +781,33 @@ export class ThreadManager {
765
781
  * on their own history. Make room for one more thread by dropping the oldest finished
766
782
  * thread that has no retained descendant; live and paused threads are never dropped.
767
783
  * Throws only when every retained thread is still live, paused or a parent of one.
784
+ * `spawning` is the path about to be created: its ancestors are kept (a finished parent
785
+ * with no other child is still the new thread's parent).
768
786
  */
769
- private archiveFinished(limit: number): void {
787
+ private archiveFinished(limit: number, spawning: string): void {
770
788
  while (this.records.size >= limit) {
771
789
  const paths = [...this.records.keys()];
772
790
  const victim = paths.find((path) => {
773
- const state = this.records.get(path)?.view.state;
791
+ const held = this.records.get(path);
792
+ const state = held?.view.state;
774
793
  const finished = state === "completed" || state === "failed" || state === "stopped";
775
- return finished && !paths.some((other) => isDescendant(other, path));
794
+ // A driver still being created would land on a dropped record and never be disposed.
795
+ const opening =
796
+ held?.driver === undefined &&
797
+ (held?.initializing !== undefined || held?.contextReady !== undefined);
798
+ return (
799
+ finished &&
800
+ !opening &&
801
+ !isDescendant(spawning, path) &&
802
+ !paths.some((other) => isDescendant(other, path))
803
+ );
776
804
  });
777
805
  if (victim === undefined) throw new Error("Total thread limit reached");
806
+ const record = this.records.get(victim);
807
+ // Release what the record holds: shutdown() only walks retained records.
808
+ record?.detachTranscript?.();
809
+ record?.transcriptListeners?.clear();
810
+ record?.driver?.dispose();
778
811
  this.records.delete(victim);
779
812
  }
780
813
  }
@@ -1,46 +1,46 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
1
+ // biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile, realpath } from "node:fs/promises";
2
4
  import path from "node:path";
3
5
  import type { AssistantMessage } from "@earendil-works/pi-ai";
4
6
  import { clampThinkingLevel } from "@earendil-works/pi-ai/compat";
5
- import { existsSync } from "node:fs";
6
- import { mkdir, readFile, realpath } from "node:fs/promises";
7
7
  import {
8
8
  createAgentSession,
9
9
  createCodemodeExtension,
10
10
  createToolSearchExtension,
11
11
  DefaultResourceLoader,
12
+ type ExtensionContext,
12
13
  getAgentDir,
13
14
  ModelRuntime,
14
15
  SessionManager,
15
16
  SettingsManager,
16
- type ExtensionContext,
17
17
  } from "@earendil-works/pi-coding-agent";
18
- import {
19
- BOOTSTRAP_MESSAGE,
20
- buildBootstrapMessage,
21
- buildQueuedUserMessage,
22
- buildUpdateDetails,
23
- DurableMailbox,
24
- } from "./mailbox.ts";
25
- import { selectTools } from "../prefs/config.ts";
26
- import { TranscriptChannel } from "./transcript.ts";
27
- import type { InheritedToolSource } from "./inherited-tools.ts";
28
- import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
29
18
  import { applySpawnOverrides, modelSource } from "../../core/spawn-overrides.ts";
19
+ import { selectTools } from "../prefs/config.ts";
30
20
  import {
31
- modelIdentity,
32
21
  getModelPreferences,
33
- selectPreferredModel,
34
22
  ModelPreferenceError,
23
+ modelIdentity,
24
+ selectPreferredModel,
35
25
  } from "../prefs/models.ts";
26
+ import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
36
27
  import {
37
- THINKING_LEVELS,
38
28
  type AgentType,
39
- type ResolvedAgentSettings,
40
29
  type DriverFactory,
41
30
  type DriverOptions,
31
+ type ResolvedAgentSettings,
32
+ THINKING_LEVELS,
42
33
  type ThinkingLevel,
43
34
  } from "../types.ts";
35
+ import type { InheritedToolSource } from "./inherited-tools.ts";
36
+ import {
37
+ BOOTSTRAP_MESSAGE,
38
+ buildBootstrapMessage,
39
+ buildQueuedUserMessage,
40
+ buildUpdateDetails,
41
+ DurableMailbox,
42
+ } from "./mailbox.ts";
43
+ import { TranscriptChannel } from "./transcript.ts";
44
44
 
45
45
  const BUILTINS = ["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"];
46
46
  /** Persisted before transcript messages so older-leaf restores still carry ownership. */
@@ -157,7 +157,7 @@ export function createDriverFactory(
157
157
  type: AgentType,
158
158
  parentPath?: string | null,
159
159
  restored?: ReturnType<SessionManager["buildSessionContext"]>,
160
- ): { provider?: string; id?: string; thinkingLevel: ThinkingLevel };
160
+ ): { provider?: string | undefined; id?: string | undefined; thinkingLevel: ThinkingLevel };
161
161
  } {
162
162
  // Keep resolved settings even after disposal: descendants inherit settings, not the caller's history.
163
163
  const resolved = new Map<string, { provider: string; id: string; thinking: ThinkingLevel }>();
@@ -226,9 +226,9 @@ export function createDriverFactory(
226
226
  const candidates = filtering
227
227
  ? availableScopedModels(normalizeScopedModels(ctx.scopedModels))
228
228
  : availableModelCandidates();
229
- ({ provider, id } = parseModelIdentity(
230
- selectTypeModelPreference(type, candidates, filtering)!,
231
- ));
229
+ const preferred = selectTypeModelPreference(type, candidates, filtering);
230
+ if (preferred === undefined) throw new Error("No preferred model is available");
231
+ ({ provider, id } = parseModelIdentity(preferred));
232
232
  } else if (source === "restored" && restored?.model) {
233
233
  provider = restored.model.provider;
234
234
  id = restored.model.modelId;
@@ -252,7 +252,7 @@ export function createDriverFactory(
252
252
  const { provider, id, thinkingLevel } = resolveInitialSettings(type, parentPath);
253
253
  const model =
254
254
  normalizeScopedModels(ctx.scopedModels).find(
255
- ({ model }) => model.provider === provider && model.id === id,
255
+ (scoped) => scoped.model.provider === provider && scoped.model.id === id,
256
256
  )?.model ?? (provider && id ? ctx.modelRegistry.find(provider, id) : undefined);
257
257
  if (!model)
258
258
  throw new Error(
@@ -381,14 +381,15 @@ export function createDriverFactory(
381
381
  modelId: string | undefined,
382
382
  scopedModels: readonly ScopedModel[],
383
383
  ) => {
384
- if (!providerId || !modelId)
384
+ if (!(providerId && modelId))
385
385
  throw new Error(
386
386
  `Subagent model ${providerId ?? "(unset)"}/${modelId ?? "(unset)"} is unavailable; select a physical model in the root or agent type`,
387
387
  );
388
388
  await mirrorRuntimeAuth(providerId);
389
389
  const sourceModel =
390
- scopedModels.find(({ model }) => model.provider === providerId && model.id === modelId)
391
- ?.model ?? ctx.modelRegistry.find(providerId, modelId);
390
+ scopedModels.find(
391
+ (scoped) => scoped.model.provider === providerId && scoped.model.id === modelId,
392
+ )?.model ?? ctx.modelRegistry.find(providerId, modelId);
392
393
  if (sourceModel?.api === "pi-virtual") {
393
394
  throw new Error(
394
395
  `Virtual model ${providerId}/${modelId} cannot be reproduced through the public registry API. Set this agent type's model to a physical provider/model-id.`,
@@ -414,13 +415,16 @@ export function createDriverFactory(
414
415
  const rootActive = new Set(inherited.activeNames);
415
416
  // Preserve deferred/codemode exposure rather than flooding model declarations with MCP tools.
416
417
  const activeToolNames = toolNames.filter(
417
- (name) => localNames.has(name) || rootActive.has(name) || options.type.tools?.allow?.includes(name) && toolFiltering === "allowed",
418
+ (name) =>
419
+ localNames.has(name) ||
420
+ rootActive.has(name) ||
421
+ (options.type.tools?.allow?.includes(name) && toolFiltering === "allowed"),
418
422
  );
419
423
  const customTools = [...options.tools, ...externalTools].filter(
420
424
  (tool) => allowed.has(tool.name) && tool.name !== "codemode" && tool.name !== "tool_search",
421
425
  );
422
426
  const settingsManager = SettingsManager.inMemory({ cacheWarming: "off" });
423
- let parkQueue = () => {};
427
+ let parkQueue: () => void = () => undefined;
424
428
  const pauseBoundary = () => {
425
429
  if (!options.shouldPause()) return undefined;
426
430
  parkQueue();
@@ -558,7 +562,7 @@ export function createDriverFactory(
558
562
  throw error;
559
563
  }
560
564
  const streamFunction = session.agent.streamFunction;
561
- session.agent.streamFunction = async (requestModel, context, streamOptions) => {
565
+ session.agent.streamFunction = (requestModel, context, streamOptions) => {
562
566
  assertScopedRequestAuthorized(
563
567
  modelIdentity(requestModel),
564
568
  normalizeScopedModels(getRootContext().scopedModels),
@@ -579,7 +583,7 @@ export function createDriverFactory(
579
583
  sessionManager.appendMessage(buildBootstrapMessage());
580
584
  emit("activity", BOOTSTRAP_MESSAGE);
581
585
  };
582
- const queueAccepted = async (kind: "steer" | "followUp", content: string) => {
586
+ const queueAccepted = (kind: "steer" | "followUp", content: string): Promise<void> => {
583
587
  assertCurrentModelAuthorized();
584
588
  ensurePersistedSession();
585
589
  const accepted = mailbox.accept(kind, content);
@@ -587,6 +591,7 @@ export function createDriverFactory(
587
591
  if (kind === "followUp") session.agent.followUp(buildQueuedUserMessage(accepted));
588
592
  else session.agent.steer(buildQueuedUserMessage(accepted));
589
593
  mailbox.markEnqueued(accepted.id);
594
+ return Promise.resolve();
590
595
  };
591
596
  const resumePending = async () => {
592
597
  for (const accepted of mailbox.replayablePending()) {
@@ -641,7 +646,7 @@ export function createDriverFactory(
641
646
  const usage = message.usage;
642
647
  const input = usage.input + usage.cacheRead + usage.cacheWrite;
643
648
  const output = usage.output;
644
- if (!Number.isFinite(input) || !Number.isFinite(output)) return;
649
+ if (!(Number.isFinite(input) && Number.isFinite(output))) return;
645
650
  return { input, output, reported: input + output + usage.totalTokens > 0 };
646
651
  };
647
652
 
@@ -650,7 +655,7 @@ export function createDriverFactory(
650
655
  .find((entry) => entry.type === "custom" && entry.customType === THREAD_OWNERSHIP_TYPE);
651
656
  const inheritedMetadata =
652
657
  ownership?.type === "custom"
653
- ? (ownership.data as { inheritedCount?: unknown })?.inheritedCount
658
+ ? (ownership.data as { inheritedCount?: unknown }).inheritedCount
654
659
  : undefined;
655
660
  const inheritedCount =
656
661
  typeof inheritedMetadata === "number" &&
@@ -1,9 +1,8 @@
1
- // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
- import { Type } from "typebox";
3
1
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
4
- import type { ThreadManager } from "./manager.ts";
2
+ import { Type } from "typebox";
5
3
  import { ModelPreferenceError } from "../prefs/models.ts";
6
4
  import type { AgentType, ResolvedAgentSettings, ThreadView } from "../types.ts";
5
+ import type { ThreadManager } from "./manager.ts";
7
6
 
8
7
  const path = Type.String({ description: "Absolute agent path, or name relative to the caller" });
9
8
  const text = Type.String({ minLength: 1 });
@@ -30,8 +29,10 @@ export function agentTools(
30
29
  const make = (
31
30
  name: string,
32
31
  description: string,
32
+ // biome-ignore-start lint/suspicious/noExplicitAny: the host validates params against the TypeBox schema before execute runs, and each handler reads only fields that schema declares
33
33
  parameters: any,
34
34
  execute: (params: any, signal?: AbortSignal) => Promise<any> | any,
35
+ // biome-ignore-end lint/suspicious/noExplicitAny: end of the schema-validated boundary
35
36
  ): ToolDefinition => ({
36
37
  name,
37
38
  label: name,
@@ -53,12 +54,7 @@ export function agentTools(
53
54
  return {
54
55
  name,
55
56
  description,
56
- error:
57
- error instanceof ModelPreferenceError
58
- ? error.summary
59
- : error instanceof Error
60
- ? error.message
61
- : String(error),
57
+ error: describeError(error),
62
58
  };
63
59
  }
64
60
  });
@@ -99,7 +95,8 @@ export function agentTools(
99
95
  ),
100
96
  thinkingLevel: Type.Optional(
101
97
  Type.String({
102
- description: "Pin this child's thinking level: off, minimal, low, medium, high, xhigh or max.",
98
+ description:
99
+ "Pin this child's thinking level: off, minimal, low, medium, high, xhigh or max.",
103
100
  }),
104
101
  ),
105
102
  }),
@@ -175,3 +172,8 @@ export function agentTools(
175
172
  ),
176
173
  ];
177
174
  }
175
+
176
+ function describeError(error: unknown): string {
177
+ if (error instanceof ModelPreferenceError) return error.summary;
178
+ return error instanceof Error ? error.message : String(error);
179
+ }