@herbertgao/pi-subagents 0.16.0 → 0.17.0

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,10 +1,8 @@
1
1
  /**
2
2
  * agent-manager.ts — Tracks agents, background execution, resume support.
3
3
  *
4
- * Background agents are subject to a configurable concurrency limit (default: 4).
5
- * Excess agents are queued and auto-started as running agents complete.
6
- * Foreground agents bypass the queue (they block the parent anyway), and so do
7
- * nested children — see `occupiesPoolSlot`.
4
+ * Background and blocking foreground agents use independent concurrency pools.
5
+ * Nested children use neither pool, avoiding parent/child deadlocks.
8
6
  */
9
7
 
10
8
  import { randomUUID } from "node:crypto"
@@ -17,6 +15,7 @@ import type {
17
15
  ExtensionContext,
18
16
  } from "@earendil-works/pi-coding-agent"
19
17
  import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js"
18
+ import { describeModel } from "./model-resolver.js"
20
19
  import type {
21
20
  AgentInvocation,
22
21
  AgentRecord,
@@ -43,6 +42,8 @@ export type CompactionInfo = {
43
42
 
44
43
  /** Default max concurrent background agents. */
45
44
  const DEFAULT_MAX_CONCURRENT = 10
45
+ /** Default max concurrent blocking agents. 0 means unlimited. */
46
+ const DEFAULT_MAX_CONCURRENT_FOREGROUND = 0
46
47
 
47
48
  /**
48
49
  * Validate a caller-supplied SpawnOptions.cwd. `undefined`/`null` mean "unset"
@@ -85,6 +86,14 @@ function occupiesPoolSlot(
85
86
  return !!record.isBackground && record.parentAgentId === undefined
86
87
  }
87
88
 
89
+ function occupiesForegroundSlot(
90
+ record: Pick<AgentRecord, "blocking" | "parentAgentId">,
91
+ ): boolean {
92
+ return !!record.blocking && record.parentAgentId === undefined
93
+ }
94
+
95
+ type Pool = "background" | "foreground"
96
+
88
97
  interface SpawnArgs {
89
98
  pi: ExtensionAPI
90
99
  ctx: ExtensionContext
@@ -107,6 +116,8 @@ interface SpawnOptions {
107
116
  * scheduler so a fired job can't be deferred past its trigger window.
108
117
  */
109
118
  bypassQueue?: boolean
119
+ /** Set only by spawnAndWait; detached/RPC spawns do not occupy the foreground pool. */
120
+ blocking?: boolean
110
121
  /** Isolation mode — "worktree" creates a temp git worktree for the agent. */
111
122
  isolation?: IsolationMode
112
123
  /**
@@ -122,6 +133,10 @@ interface SpawnOptions {
122
133
  invocation?: AgentInvocation
123
134
  /** Parent abort signal — when aborted, the subagent is also stopped. */
124
135
  signal?: AbortSignal
136
+ /** Called when this specific record has started and has a promise. */
137
+ onSpawned?: (id: string) => void
138
+ /** Called when this record enters its pool's queue. */
139
+ onQueued?: (id: string, ahead: number) => void
125
140
  /** Called on tool start/end with activity info (for streaming progress to UI). */
126
141
  onToolActivity?: (activity: ToolActivity) => void
127
142
  /** Called on streaming text deltas from the assistant response. */
@@ -207,14 +222,20 @@ export class AgentManager {
207
222
  private onCompact?: OnAgentCompact
208
223
  private onUsage?: OnAgentUsage
209
224
  private maxConcurrent: number
225
+ private maxConcurrentForeground = DEFAULT_MAX_CONCURRENT_FOREGROUND
210
226
  /** Base repos worktrees were created from — so dispose() can prune them all,
211
227
  * not just the parent repo (caller-supplied cwd can target other repos). */
212
228
  private worktreeRepos = new Set<string>()
213
229
 
214
- /** Queue of background agents waiting to start. */
215
- private queue: { id: string; start: () => void }[] = []
216
- /** Number of currently running background agents. */
230
+ /** Agents waiting on either independent concurrency pool. */
231
+ private queue: {
232
+ id: string
233
+ pool: Pool
234
+ start: () => void
235
+ release: () => void
236
+ }[] = []
217
237
  private runningBackground = 0
238
+ private runningForeground = 0
218
239
 
219
240
  constructor(
220
241
  onComplete?: OnAgentComplete,
@@ -244,6 +265,31 @@ export class AgentManager {
244
265
  return this.maxConcurrent
245
266
  }
246
267
 
268
+ /** Update the max concurrent blocking agents limit. 0 means unlimited. */
269
+ setMaxConcurrentForeground(n: number) {
270
+ this.maxConcurrentForeground = Math.max(0, n)
271
+ this.drainQueue()
272
+ }
273
+
274
+ getMaxConcurrentForeground(): number {
275
+ return this.maxConcurrentForeground
276
+ }
277
+
278
+ private poolFor(record: AgentRecord): Pool | undefined {
279
+ if (occupiesPoolSlot(record)) return "background"
280
+ if (this.maxConcurrentForeground > 0 && occupiesForegroundSlot(record)) {
281
+ return "foreground"
282
+ }
283
+ return undefined
284
+ }
285
+
286
+ private poolHasRoom(pool: Pool): boolean {
287
+ return pool === "background"
288
+ ? this.runningBackground < this.maxConcurrent
289
+ : this.maxConcurrentForeground === 0 ||
290
+ this.runningForeground < this.maxConcurrentForeground
291
+ }
292
+
247
293
  /**
248
294
  * Spawn an agent and return its ID immediately (for background use).
249
295
  * If the concurrency limit is reached, the agent is queued.
@@ -278,6 +324,7 @@ export class AgentManager {
278
324
  // only filter excludes only explicit `false`, so undefined agents — which
279
325
  // have no inline surface — stay visible instead of vanishing.
280
326
  isBackground: options.isBackground,
327
+ blocking: options.blocking,
281
328
  invocation: options.invocation,
282
329
  depth: options.depth ?? 1,
283
330
  parentAgentId: options.parentAgentId,
@@ -288,13 +335,24 @@ export class AgentManager {
288
335
 
289
336
  const args: SpawnArgs = { pi, ctx, type, prompt, options }
290
337
 
291
- if (
292
- occupiesPoolSlot(record) &&
293
- !options.bypassQueue &&
294
- this.runningBackground >= this.maxConcurrent
295
- ) {
296
- // Queue it will be started when a running agent completes
297
- this.queue.push({ id, start: () => this.startAgent(id, record, args) })
338
+ const pool = this.poolFor(record)
339
+ if (pool && !options.bypassQueue && !this.poolHasRoom(pool)) {
340
+ record.status = "queued"
341
+ if (!this.armQueuedAbort(id, options.signal)) return id
342
+ let release!: () => void
343
+ record.startGate = new Promise<void>((resolve) => {
344
+ release = resolve
345
+ })
346
+ this.queue.push({
347
+ id,
348
+ pool,
349
+ start: () => this.startAgent(id, record, args),
350
+ release,
351
+ })
352
+ options.onQueued?.(
353
+ id,
354
+ this.queue.filter((entry) => entry.pool === pool).length - 1,
355
+ )
298
356
  return id
299
357
  }
300
358
 
@@ -309,6 +367,20 @@ export class AgentManager {
309
367
  return id
310
368
  }
311
369
 
370
+ private armQueuedAbort(id: string, signal?: AbortSignal): boolean {
371
+ if (!signal) return true
372
+ if (signal.aborted) {
373
+ const record = this.agents.get(id)
374
+ if (record) {
375
+ record.status = "stopped"
376
+ record.completedAt = Date.now()
377
+ }
378
+ return false
379
+ }
380
+ signal.addEventListener("abort", () => this.abort(id), { once: true })
381
+ return true
382
+ }
383
+
312
384
  /** Actually start an agent (called immediately or from queue drain). */
313
385
  private startAgent(
314
386
  id: string,
@@ -349,16 +421,23 @@ export class AgentManager {
349
421
 
350
422
  record.status = "running"
351
423
  record.startedAt = Date.now()
352
- if (occupiesPoolSlot(record)) this.runningBackground++
424
+ record.startGate = undefined
425
+ const pool = this.poolFor(record)
426
+ if (pool === "background") this.runningBackground++
427
+ else if (pool === "foreground") this.runningForeground++
353
428
  this.onStart?.(record)
354
429
 
355
430
  // Wire parent abort signal to stop the subagent when the parent is interrupted
356
431
  let detachParentSignal: (() => void) | undefined
357
432
  if (options.signal) {
358
- const onParentAbort = () => this.abort(id)
359
- options.signal.addEventListener("abort", onParentAbort, { once: true })
360
- detachParentSignal = () =>
361
- options.signal!.removeEventListener("abort", onParentAbort)
433
+ if (options.signal.aborted) {
434
+ this.abort(id)
435
+ } else {
436
+ const onParentAbort = () => this.abort(id)
437
+ options.signal.addEventListener("abort", onParentAbort, { once: true })
438
+ detachParentSignal = () =>
439
+ options.signal!.removeEventListener("abort", onParentAbort)
440
+ }
362
441
  }
363
442
  const detach = () => {
364
443
  detachParentSignal?.()
@@ -407,6 +486,18 @@ export class AgentManager {
407
486
  },
408
487
  onSessionCreated: (session) => {
409
488
  record.session = session
489
+ if (session.model) {
490
+ record.invocation ??= {}
491
+ const requested =
492
+ record.invocation.requestedThinking ?? record.invocation.thinking
493
+ Object.assign(record.invocation, describeModel(session.model))
494
+ if (session.thinkingLevel) {
495
+ record.invocation.thinking = session.thinkingLevel
496
+ if (requested && requested !== session.thinkingLevel) {
497
+ record.invocation.requestedThinking = requested
498
+ }
499
+ }
500
+ }
410
501
  // Flush any steers that arrived before the session was ready
411
502
  if (record.pendingSteers?.length) {
412
503
  for (const msg of record.pendingSteers) {
@@ -468,24 +559,7 @@ export class AgentManager {
468
559
 
469
560
  this.abortOwnedChildren(id)
470
561
 
471
- // Fire onComplete for foreground agents too — lifecycle symmetry.
472
- // Mark resultConsumed so the callback skips notifications (result returned inline).
473
- if (!options.isBackground) {
474
- record.resultConsumed = true
475
- try {
476
- this.onComplete?.(record)
477
- } catch {
478
- /* ignore completion side-effect errors */
479
- }
480
- } else {
481
- if (occupiesPoolSlot(record)) this.runningBackground--
482
- try {
483
- this.onComplete?.(record)
484
- } catch {
485
- /* ignore completion side-effect errors */
486
- }
487
- this.drainQueue()
488
- }
562
+ this.settleRun(record, true, pool)
489
563
  return responseText
490
564
  })
491
565
  .catch((err) => {
@@ -524,25 +598,35 @@ export class AgentManager {
524
598
 
525
599
  this.abortOwnedChildren(id)
526
600
 
527
- // Fire onComplete for foreground agents too — lifecycle symmetry.
528
- // Mark resultConsumed so the callback skips notifications (result returned inline).
529
- if (!options.isBackground) {
530
- record.resultConsumed = true
531
- this.onComplete?.(record)
532
- } else {
533
- if (occupiesPoolSlot(record)) this.runningBackground--
534
- this.onComplete?.(record)
535
- this.drainQueue()
536
- }
601
+ this.settleRun(record, false, pool)
537
602
  return ""
538
603
  })
539
604
 
540
605
  record.promise = promise
541
606
 
542
- // Notify caller that spawn is complete (record is in the map, promise is set).
543
- // Called synchronously — onSessionCreated fires asynchronously inside runAgent.
544
- // Used by spawnAndWait to let the caller set up output files before streaming starts.
545
- this.onSpawned?.(id)
607
+ // Per-call hook: safe for parallel and deferred foreground starts.
608
+ options.onSpawned?.(id)
609
+ }
610
+
611
+ private settleRun(
612
+ record: AgentRecord,
613
+ guardCallback: boolean,
614
+ pool: Pool | undefined,
615
+ ): void {
616
+ if (!record.isBackground) record.resultConsumed = true
617
+ if (pool === "background") this.runningBackground--
618
+ else if (pool === "foreground") this.runningForeground--
619
+
620
+ if (guardCallback) {
621
+ try {
622
+ this.onComplete?.(record)
623
+ } catch {
624
+ /* ignore completion side-effect errors */
625
+ }
626
+ } else {
627
+ this.onComplete?.(record)
628
+ }
629
+ if (record.isBackground || pool) this.drainQueue()
546
630
  }
547
631
 
548
632
  /**
@@ -557,43 +641,41 @@ export class AgentManager {
557
641
  }
558
642
  }
559
643
 
560
- /** Start queued agents up to the concurrency limit. */
644
+ /** Start the earliest queued agent whose own pool has room. */
561
645
  private drainQueue() {
562
- while (
563
- this.queue.length > 0 &&
564
- this.runningBackground < this.maxConcurrent
565
- ) {
566
- const next = this.queue.shift()!
646
+ for (;;) {
647
+ const index = this.queue.findIndex((entry) =>
648
+ this.poolHasRoom(entry.pool),
649
+ )
650
+ if (index === -1) return
651
+ const [next] = this.queue.splice(index, 1)
567
652
  const record = this.agents.get(next.id)
568
- if (record?.status !== "queued") continue
569
653
  try {
570
- next.start()
654
+ if (record?.status === "queued") next.start()
571
655
  } catch (err) {
572
- // Late failure (e.g. strict worktree-isolation) — surface on the record
573
- // so the user/agent can see it via /agents, then keep draining.
574
- record.status = "error"
575
- record.error = err instanceof Error ? err.message : String(err)
576
- record.completedAt = Date.now()
577
- this.onComplete?.(record)
656
+ if (record) {
657
+ if (next.pool === "foreground") record.resultConsumed = true
658
+ record.status = "error"
659
+ record.error = err instanceof Error ? err.message : String(err)
660
+ record.completedAt = Date.now()
661
+ this.onComplete?.(record)
662
+ }
663
+ } finally {
664
+ next.release()
578
665
  }
579
666
  }
580
667
  }
581
668
 
582
- /**
583
- * Called synchronously right after spawn, before onSessionCreated fires.
584
- * Lets the caller set up the output file path on the record.
585
- * The record is guaranteed to be in this.agents at this point.
586
- */
587
- private onSpawned?: (id: string) => void
669
+ private dequeue(pred: (entry: { id: string; pool: Pool }) => boolean): void {
670
+ const kept: typeof this.queue = []
671
+ for (const entry of this.queue) {
672
+ if (pred(entry)) entry.release()
673
+ else kept.push(entry)
674
+ }
675
+ this.queue = kept
676
+ }
588
677
 
589
- /**
590
- * Spawn an agent and wait for completion (foreground use).
591
- * Foreground agents bypass the concurrency queue.
592
- * Returns { id, record } so callers can access the agent ID.
593
- *
594
- * @param onSpawned - Called synchronously after spawn(), before onSessionCreated fires.
595
- * Use this to set record.outputFile so streamToOutputFile can pick it up.
596
- */
678
+ /** Spawn an agent, applying the blocking foreground pool, and await it. */
597
679
  async spawnAndWait(
598
680
  pi: ExtensionAPI,
599
681
  ctx: ExtensionContext,
@@ -602,23 +684,18 @@ export class AgentManager {
602
684
  options: Omit<SpawnOptions, "isBackground">,
603
685
  onSpawned?: (id: string) => void,
604
686
  ): Promise<{ id: string; record: AgentRecord }> {
605
- // Temporarily register the onSpawned hook so startAgent can call it.
606
- const prevOnSpawned = this.onSpawned
607
- this.onSpawned = onSpawned
608
- let id: string
609
- try {
610
- // spawn() invokes onSpawned synchronously before returning. Restore the
611
- // shared hook immediately so unrelated concurrent spawns cannot inherit
612
- // this foreground caller's callback while its run is awaited.
613
- id = this.spawn(pi, ctx, type, prompt, {
614
- ...options,
615
- isBackground: false,
616
- })
617
- } finally {
618
- this.onSpawned = prevOnSpawned
619
- }
687
+ const id = this.spawn(pi, ctx, type, prompt, {
688
+ ...options,
689
+ isBackground: false,
690
+ blocking: true,
691
+ onSpawned,
692
+ })
620
693
  const record = this.agents.get(id)!
621
- await record.promise
694
+ if (record.status === "queued") await record.startGate
695
+ if (record.promise) await record.promise
696
+ if (!record.promise && record.status === "error") {
697
+ throw new Error(record.error ?? "Agent failed to start")
698
+ }
622
699
  return { id, record }
623
700
  }
624
701
 
@@ -661,12 +738,14 @@ export class AgentManager {
661
738
  record.status = "queued"
662
739
 
663
740
  const start = () => this.startResume(id, record, prompt, signal, options)
664
- if (
665
- occupiesPoolSlot(record) &&
666
- this.runningBackground >= this.maxConcurrent
667
- ) {
668
- // At the concurrency limit — queue it, drains when a slot frees.
669
- this.queue.push({ id, start })
741
+ if (occupiesPoolSlot(record) && !this.poolHasRoom("background")) {
742
+ // Detached resumes remain on the background pool only.
743
+ this.queue.push({
744
+ id,
745
+ pool: "background",
746
+ start,
747
+ release: () => {},
748
+ })
670
749
  } else {
671
750
  start()
672
751
  }
@@ -863,9 +942,9 @@ export class AgentManager {
863
942
  const record = this.agents.get(id)
864
943
  if (!record) return false
865
944
 
866
- // Remove from queue if queued
945
+ // Remove from queue if queued and release any blocking waiter.
867
946
  if (record.status === "queued") {
868
- this.queue = this.queue.filter((q) => q.id !== id)
947
+ this.dequeue((q) => q.id === id)
869
948
  record.status = "stopped"
870
949
  record.completedAt = Date.now()
871
950
  return true
@@ -928,7 +1007,7 @@ export class AgentManager {
928
1007
  count++
929
1008
  }
930
1009
  }
931
- this.queue = []
1010
+ this.dequeue(() => true)
932
1011
  // Abort running agents
933
1012
  for (const record of this.agents.values()) {
934
1013
  if (record.status === "running") {
@@ -958,8 +1037,7 @@ export class AgentManager {
958
1037
 
959
1038
  async dispose(): Promise<void> {
960
1039
  clearInterval(this.cleanupInterval)
961
- // Clear queue
962
- this.queue = []
1040
+ this.dequeue(() => true)
963
1041
  const sessions = [...this.agents.values()].map((record) => record.session)
964
1042
  this.agents.clear()
965
1043
  await Promise.all(sessions.map((session) => shutdownChildSession(session)))
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Cross-extension RPC handlers for the subagents extension.
3
3
  *
4
- * Exposes ping, spawn, and stop RPCs over the pi.events event bus,
4
+ * Exposes ping, spawn, stop, and consume RPCs over the pi.events event bus,
5
5
  * using per-request scoped reply channels.
6
6
  *
7
7
  * Reply envelope follows pi-mono convention:
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { type ModelRegistry, resolveModel } from "./model-resolver.js"
13
+ import { checkModelScope } from "./model-scope.js"
13
14
 
14
15
  /** Minimal event bus interface needed by the RPC handlers. */
15
16
  export interface EventBus {
@@ -25,7 +26,7 @@ export type RpcReply<T = void> =
25
26
  /** RPC protocol version — bumped when the envelope or method contracts change. */
26
27
  export const PROTOCOL_VERSION = 2
27
28
 
28
- /** Minimal AgentManager interface needed by the spawn/stop RPCs. */
29
+ /** Minimal AgentManager interface needed by the spawn/stop/consume RPCs. */
29
30
  export interface SpawnCapable {
30
31
  spawn(
31
32
  pi: unknown,
@@ -35,6 +36,7 @@ export interface SpawnCapable {
35
36
  options: any,
36
37
  ): string
37
38
  abort(id: string): boolean
39
+ consumeResult(id: string): boolean
38
40
  }
39
41
 
40
42
  export interface RpcDeps {
@@ -48,6 +50,7 @@ export interface RpcHandle {
48
50
  unsubPing: () => void
49
51
  unsubSpawn: () => void
50
52
  unsubStop: () => void
53
+ unsubConsume: () => void
51
54
  }
52
55
 
53
56
  /**
@@ -76,7 +79,7 @@ function handleRpc<P extends { requestId: string }>(
76
79
  }
77
80
 
78
81
  /**
79
- * Register ping, spawn, and stop RPC handlers on the event bus.
82
+ * Register ping, spawn, stop, and consume RPC handlers on the event bus.
80
83
  * Returns unsub functions for cleanup.
81
84
  */
82
85
  export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
@@ -102,21 +105,40 @@ export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
102
105
  // agent's auth lookup doesn't crash with "No API key found for
103
106
  // undefined".
104
107
  let normalizedOptions = options ?? {}
105
- if (typeof normalizedOptions.model === "string") {
106
- const registry = (ctx as { modelRegistry?: ModelRegistry }).modelRegistry
107
- if (!registry) {
108
+ const override = normalizedOptions.model
109
+ // null means inherit, matching the runner's `model ?? default` behavior.
110
+ if (override != null) {
111
+ const { modelRegistry, cwd } = ctx as {
112
+ modelRegistry?: ModelRegistry
113
+ cwd?: string
114
+ }
115
+ const label =
116
+ typeof override === "string"
117
+ ? override
118
+ : `${override.provider}/${override.id}`
119
+ if (!modelRegistry) {
108
120
  throw new Error(
109
- `Model override "${normalizedOptions.model}" provided but ctx.modelRegistry is unavailable`,
121
+ `Model override "${label}" provided but ctx.modelRegistry is unavailable`,
110
122
  )
111
123
  }
112
- const resolved = resolveModel(normalizedOptions.model, registry)
113
- if (typeof resolved === "string") {
114
- // resolveModel returns a human-readable error string when the
115
- // input doesn't match any available model. Surface it instead of
116
- // silently falling back so the caller sees the auth/typo issue.
117
- throw new Error(resolved)
124
+
125
+ let model = override
126
+ if (typeof override === "string") {
127
+ const resolved = resolveModel(override, modelRegistry)
128
+ if (typeof resolved === "string") throw new Error(resolved)
129
+ model = resolved
130
+ normalizedOptions = { ...normalizedOptions, model: resolved }
118
131
  }
119
- normalizedOptions = { ...normalizedOptions, model: resolved }
132
+
133
+ const verdict = checkModelScope({
134
+ model,
135
+ cwd: cwd ?? process.cwd(),
136
+ modelRegistry,
137
+ callerSupplied: true,
138
+ agentLabel: type,
139
+ modelInput: label,
140
+ })
141
+ if (verdict.kind === "error") throw new Error(verdict.message)
120
142
  }
121
143
 
122
144
  return { id: manager.spawn(pi, ctx, type, prompt, normalizedOptions) }
@@ -130,5 +152,14 @@ export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
130
152
  },
131
153
  )
132
154
 
133
- return { unsubPing, unsubSpawn, unsubStop }
155
+ const unsubConsume = handleRpc<{
156
+ requestId: string
157
+ agentId: string
158
+ }>(events, "subagents:rpc:consume", ({ agentId }) => {
159
+ if (!manager.consumeResult(agentId)) {
160
+ throw new Error("Agent not found or still running")
161
+ }
162
+ })
163
+
164
+ return { unsubPing, unsubSpawn, unsubStop, unsubConsume }
134
165
  }
@@ -141,13 +141,21 @@ function loadFromDir(
141
141
  * Read and parse one agent file, or warn and return undefined for the caller to
142
142
  * skip. Under strict mode the same failure aborts startup while naming the file.
143
143
  */
144
+ export function parseAgentFrontmatter<T extends Record<string, unknown>>(
145
+ content: string,
146
+ ): { frontmatter: T; body: string } {
147
+ return parseFrontmatter<T>(
148
+ content.startsWith("\uFEFF") ? content.slice(1) : content,
149
+ )
150
+ }
151
+
144
152
  function readAgentFile(
145
153
  path: string,
146
154
  strict: boolean,
147
155
  warnings: WarningState,
148
156
  ): { frontmatter: Record<string, unknown>; body: string } | undefined {
149
157
  try {
150
- return parseFrontmatter<Record<string, unknown>>(
158
+ return parseAgentFrontmatter<Record<string, unknown>>(
151
159
  readFileSync(path, "utf-8"),
152
160
  )
153
161
  } catch (err) {