pi-subagents 0.29.0 → 0.30.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.
@@ -40,6 +40,7 @@ import {
40
40
  resolveChildMaxSubagentDepth,
41
41
  } from "../../shared/types.ts";
42
42
  import { nestedResultsPath, resolveInheritedNestedRouteFromEnv, resolveNestedParentAddressFromEnv, writeNestedEvent } from "../shared/nested-events.ts";
43
+ import type { ImportedAsyncRoot } from "./chain-root-attachment.ts";
43
44
 
44
45
  const require = createRequire(import.meta.url);
45
46
  const piPackageRoot = resolvePiPackageRoot();
@@ -101,6 +102,7 @@ interface AsyncExecutionContext {
101
102
  interface AsyncChainParams {
102
103
  chain: ChainStep[];
103
104
  task?: string;
105
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
104
106
  resultMode?: Exclude<SubagentRunMode, "single">;
105
107
  agents: AgentConfig[];
106
108
  ctx: AsyncExecutionContext;
@@ -157,6 +159,33 @@ interface AsyncExecutionResult {
157
159
  isError?: boolean;
158
160
  }
159
161
 
162
+ export interface AsyncRunnerStepBuildParams {
163
+ chain: ChainStep[];
164
+ task?: string;
165
+ attachRoot?: ImportedAsyncRoot & { agent: string; outputName?: string; label?: string };
166
+ resultMode?: SubagentRunMode;
167
+ agents: AgentConfig[];
168
+ ctx: AsyncExecutionContext;
169
+ availableModels?: AvailableModelInfo[];
170
+ cwd?: string;
171
+ chainSkills?: string[];
172
+ sessionFilesByFlatIndex?: (string | undefined)[];
173
+ dynamicFanoutMaxItems?: number;
174
+ maxSubagentDepth: number;
175
+ asyncDir: string;
176
+ validateOutputBindings?: boolean;
177
+ }
178
+
179
+ export type AsyncRunnerStepBuildResult =
180
+ | {
181
+ steps: RunnerStep[];
182
+ runnerCwd: string;
183
+ workflowGraph: ReturnType<typeof buildWorkflowGraphSnapshot>;
184
+ eventChain: ChainStep[];
185
+ originalTask?: string;
186
+ }
187
+ | { error: string };
188
+
160
189
  export function formatAsyncStartedMessage(headline: string): string {
161
190
  return [
162
191
  headline,
@@ -174,6 +203,14 @@ export function isAsyncAvailable(): boolean {
174
203
  return jitiCliPath !== undefined;
175
204
  }
176
205
 
206
+ function resolveAsyncRunnerNodeCommand(): string {
207
+ const basename = path.basename(process.execPath).toLowerCase();
208
+ if (basename === "node" || basename === "node.exe" || basename === "nodejs" || basename === "nodejs.exe") {
209
+ return process.execPath;
210
+ }
211
+ return process.platform === "win32" ? "node.exe" : "node";
212
+ }
213
+
177
214
  /**
178
215
  * Spawn the async runner process
179
216
  */
@@ -195,8 +232,9 @@ function spawnRunner(cfg: object, suffix: string, cwd: string): { pid?: number;
195
232
  const cfgPath = getAsyncConfigPath(suffix);
196
233
  fs.writeFileSync(cfgPath, JSON.stringify(cfg));
197
234
  const runner = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-runner.ts");
235
+ const nodeCommand = resolveAsyncRunnerNodeCommand();
198
236
 
199
- const proc = spawn(process.execPath, [jitiCliPath, runner, cfgPath], {
237
+ const proc = spawn(nodeCommand, [jitiCliPath, runner, cfgPath], {
200
238
  cwd,
201
239
  detached: true,
202
240
  stdio: "ignore",
@@ -225,36 +263,28 @@ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
225
263
  class UnavailableSubagentSkillError extends Error {}
226
264
  class AsyncStartValidationError extends Error {}
227
265
 
228
- /**
229
- * Execute a chain asynchronously
230
- */
231
- export function executeAsyncChain(
232
- id: string,
233
- params: AsyncChainParams,
234
- ): AsyncExecutionResult {
266
+ export function buildAsyncRunnerSteps(id: string, params: AsyncRunnerStepBuildParams): AsyncRunnerStepBuildResult {
235
267
  const {
236
268
  chain,
237
269
  agents,
238
270
  ctx,
239
271
  cwd,
240
- maxOutput,
241
- artifactsDir,
242
- artifactConfig,
243
- shareEnabled,
244
- sessionRoot,
245
272
  sessionFilesByFlatIndex,
246
273
  maxSubagentDepth,
247
- worktreeSetupHook,
248
- worktreeSetupHookTimeoutMs,
249
- controlConfig,
250
- controlIntercomTarget,
251
- childIntercomTarget,
252
- nestedRoute,
274
+ asyncDir,
253
275
  } = params;
254
276
  const resultMode = params.resultMode ?? "chain";
255
277
  const chainSkills = params.chainSkills ?? [];
256
278
  const availableModels = params.availableModels;
257
279
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
280
+ const graphChain: ChainStep[] = params.attachRoot
281
+ ? [{
282
+ agent: params.attachRoot.agent,
283
+ task: `Attach async root ${params.attachRoot.runId}`,
284
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
285
+ ...(params.attachRoot.outputName ? { as: params.attachRoot.outputName } : {}),
286
+ }, ...chain]
287
+ : chain;
258
288
  const firstStep = chain[0];
259
289
  const originalTask = params.task ?? (firstStep
260
290
  ? (isParallelStep(firstStep)
@@ -264,46 +294,28 @@ export function executeAsyncChain(
264
294
  : (firstStep as SequentialStep).task)
265
295
  : undefined);
266
296
  try {
267
- validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
297
+ if (params.validateOutputBindings !== false) {
298
+ validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
299
+ }
268
300
  } catch (error) {
269
- if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
301
+ if (error instanceof ChainOutputValidationError) return { error: error.message };
270
302
  throw error;
271
303
  }
272
- const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
304
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: graphChain });
273
305
 
274
306
  for (const s of chain) {
275
307
  const stepAgents = isParallelStep(s)
276
308
  ? s.parallel.map((t) => t.agent)
277
309
  : isDynamicParallelStep(s)
278
310
  ? [s.parallel.agent]
279
- : [(s as SequentialStep).agent];
311
+ : [(s as SequentialStep).agent];
280
312
  for (const agentName of stepAgents) {
281
313
  if (!agents.find((x) => x.name === agentName)) {
282
- return {
283
- content: [{ type: "text", text: `Unknown agent: ${agentName}` }],
284
- isError: true,
285
- details: { mode: resultMode, results: [] },
286
- };
314
+ return { error: `Unknown agent: ${agentName}` };
287
315
  }
288
316
  }
289
317
  }
290
318
 
291
- const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
292
- const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
293
- const asyncDir = inheritedNestedRoute
294
- ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
295
- : path.join(ASYNC_DIR, id);
296
- try {
297
- fs.mkdirSync(asyncDir, { recursive: true });
298
- } catch (error) {
299
- const message = error instanceof Error ? error.message : String(error);
300
- return {
301
- content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
302
- isError: true,
303
- details: { mode: resultMode, results: [] },
304
- };
305
- }
306
-
307
319
  let progressInstructionCreated = false;
308
320
  const buildStepOverrides = (s: SequentialStep): StepOverrides => {
309
321
  const stepSkillInput = normalizeSkillInput(s.skill);
@@ -361,6 +373,7 @@ export function executeAsyncChain(
361
373
  ),
362
374
  tools: a.tools,
363
375
  extensions: a.extensions,
376
+ subagentOnlyExtensions: a.subagentOnlyExtensions,
364
377
  mcpDirectTools: a.mcpDirectTools,
365
378
  completionGuard: a.completionGuard,
366
379
  systemPrompt,
@@ -392,9 +405,8 @@ export function executeAsyncChain(
392
405
  return sessionFile;
393
406
  };
394
407
 
395
- let steps: RunnerStep[];
396
408
  try {
397
- steps = chain.map((s, stepIndex) => {
409
+ const builtSteps = chain.map((s, stepIndex) => {
398
410
  if (isParallelStep(s)) {
399
411
  const parallelBehaviors = s.parallel.map((task) => {
400
412
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
@@ -450,12 +462,102 @@ export function executeAsyncChain(
450
462
  }
451
463
  return buildSeqStep(s as SequentialStep, nextSessionFile());
452
464
  });
465
+ const steps = params.attachRoot
466
+ ? [{
467
+ agent: params.attachRoot.agent,
468
+ task: "",
469
+ label: params.attachRoot.label ?? `Attached root ${params.attachRoot.runId}`,
470
+ outputName: params.attachRoot.outputName,
471
+ importAsyncRoot: {
472
+ runId: params.attachRoot.runId,
473
+ asyncDir: params.attachRoot.asyncDir,
474
+ resultPath: params.attachRoot.resultPath,
475
+ index: params.attachRoot.index,
476
+ },
477
+ inheritProjectContext: false,
478
+ inheritSkills: false,
479
+ }, ...builtSteps]
480
+ : builtSteps;
481
+ return { steps, runnerCwd, workflowGraph, eventChain: graphChain, ...(originalTask !== undefined ? { originalTask } : {}) };
453
482
  } catch (error) {
454
- if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return formatAsyncStartError(resultMode, error.message);
483
+ if (error instanceof UnavailableSubagentSkillError || error instanceof AsyncStartValidationError) return { error: error.message };
455
484
  throw error;
456
485
  }
486
+ }
487
+
488
+ /**
489
+ * Execute a chain asynchronously
490
+ */
491
+ export function executeAsyncChain(
492
+ id: string,
493
+ params: AsyncChainParams,
494
+ ): AsyncExecutionResult {
495
+ const {
496
+ chain,
497
+ agents,
498
+ ctx,
499
+ cwd,
500
+ maxOutput,
501
+ artifactsDir,
502
+ artifactConfig,
503
+ shareEnabled,
504
+ sessionRoot,
505
+ sessionFilesByFlatIndex,
506
+ maxSubagentDepth,
507
+ worktreeSetupHook,
508
+ worktreeSetupHookTimeoutMs,
509
+ controlConfig,
510
+ controlIntercomTarget,
511
+ childIntercomTarget,
512
+ nestedRoute,
513
+ } = params;
514
+ const resultMode = params.resultMode ?? "chain";
515
+ const inheritedNestedRoute = resolveInheritedNestedRouteFromEnv();
516
+ const nestedAddress = inheritedNestedRoute ? resolveNestedParentAddressFromEnv() : undefined;
517
+ const asyncDir = inheritedNestedRoute
518
+ ? path.join(TEMP_ROOT_DIR, "nested-subagent-runs", inheritedNestedRoute.rootRunId, id)
519
+ : path.join(ASYNC_DIR, id);
520
+ try {
521
+ fs.mkdirSync(asyncDir, { recursive: true });
522
+ } catch (error) {
523
+ const message = error instanceof Error ? error.message : String(error);
524
+ return {
525
+ content: [{ type: "text", text: `Failed to create async run directory '${asyncDir}': ${message}` }],
526
+ isError: true,
527
+ details: { mode: resultMode, results: [] },
528
+ };
529
+ }
530
+
531
+ const built = buildAsyncRunnerSteps(id, {
532
+ chain,
533
+ task: params.task,
534
+ attachRoot: params.attachRoot,
535
+ resultMode,
536
+ agents,
537
+ ctx,
538
+ availableModels: params.availableModels,
539
+ cwd,
540
+ chainSkills: params.chainSkills,
541
+ sessionFilesByFlatIndex,
542
+ dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
543
+ maxSubagentDepth,
544
+ asyncDir,
545
+ });
546
+ if ("error" in built) {
547
+ try {
548
+ fs.rmSync(asyncDir, { recursive: true, force: true });
549
+ } catch {
550
+ // Best-effort cleanup for validation failures before the runner is spawned.
551
+ }
552
+ return formatAsyncStartError(resultMode, built.error);
553
+ }
554
+ const { steps, runnerCwd, workflowGraph, eventChain } = built;
457
555
  let childTargetIndex = 0;
458
556
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
557
+ if (!("parallel" in step) && step.importAsyncRoot) {
558
+ childTargetIndex++;
559
+ return [undefined];
560
+ }
459
561
  if ("parallel" in step) {
460
562
  if (!Array.isArray(step.parallel)) {
461
563
  childTargetIndex++;
@@ -513,17 +615,17 @@ export function executeAsyncChain(
513
615
  }
514
616
 
515
617
  if (spawnResult.pid) {
516
- const firstStep = chain[0];
517
- const firstAgents = isParallelStep(firstStep)
518
- ? firstStep.parallel.map((t) => t.agent)
519
- : isDynamicParallelStep(firstStep)
520
- ? [firstStep.parallel.agent]
521
- : [(firstStep as SequentialStep).agent];
618
+ const eventFirstStep = eventChain[0];
619
+ const firstAgents = isParallelStep(eventFirstStep)
620
+ ? eventFirstStep.parallel.map((t) => t.agent)
621
+ : isDynamicParallelStep(eventFirstStep)
622
+ ? [eventFirstStep.parallel.agent]
623
+ : [(eventFirstStep as SequentialStep).agent];
522
624
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
523
625
  const flatAgents: string[] = [];
524
626
  let flatStepStart = 0;
525
- for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
526
- const step = chain[stepIndex]!;
627
+ for (let stepIndex = 0; stepIndex < eventChain.length; stepIndex++) {
628
+ const step = eventChain[stepIndex]!;
527
629
  if (isParallelStep(step)) {
528
630
  parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
529
631
  flatAgents.push(...step.parallel.map((task) => task.agent));
@@ -561,7 +663,7 @@ export function executeAsyncChain(
561
663
  state: "running",
562
664
  agent: firstAgents[0],
563
665
  agents: flatAgents,
564
- chainStepCount: chain.length,
666
+ chainStepCount: eventChain.length,
565
667
  parallelGroups,
566
668
  startedAt: now,
567
669
  lastUpdate: now,
@@ -578,15 +680,15 @@ export function executeAsyncChain(
578
680
  mode: resultMode,
579
681
  agent: firstAgents[0],
580
682
  agents: flatAgents,
581
- task: isParallelStep(firstStep)
582
- ? firstStep.parallel[0]?.task?.slice(0, 50)
583
- : isDynamicParallelStep(firstStep)
584
- ? firstStep.parallel.task?.slice(0, 50)
585
- : (firstStep as SequentialStep).task?.slice(0, 50),
586
- chain: chain.map((s) =>
683
+ task: isParallelStep(eventFirstStep)
684
+ ? eventFirstStep.parallel[0]?.task?.slice(0, 50)
685
+ : isDynamicParallelStep(eventFirstStep)
686
+ ? eventFirstStep.parallel.task?.slice(0, 50)
687
+ : (eventFirstStep as SequentialStep).task?.slice(0, 50),
688
+ chain: eventChain.map((s) =>
587
689
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
588
690
  ),
589
- chainStepCount: chain.length,
691
+ chainStepCount: eventChain.length,
590
692
  parallelGroups,
591
693
  workflowGraph,
592
694
  cwd: runnerCwd,
@@ -691,6 +793,7 @@ export function executeAsyncSingle(
691
793
  ),
692
794
  tools: agentConfig.tools,
693
795
  extensions: agentConfig.extensions,
796
+ subagentOnlyExtensions: agentConfig.subagentOnlyExtensions,
694
797
  mcpDirectTools: agentConfig.mcpDirectTools,
695
798
  completionGuard: agentConfig.completionGuard,
696
799
  systemPrompt,
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus } from "../../shared/types.ts";
3
+ import { ASYNC_DIR, RESULTS_DIR, type AsyncStatus, type SubagentState } from "../../shared/types.ts";
4
4
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
5
5
  import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
6
6
 
7
+ export const ASYNC_RESUME_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
8
+
7
9
  export interface AsyncResumeParams {
8
10
  id?: string;
9
11
  runId?: string;
@@ -18,6 +20,10 @@ export interface AsyncResumeDeps {
18
20
  now?: () => number;
19
21
  }
20
22
 
23
+ export interface AsyncResumeOptions {
24
+ requireSessionFile?: boolean;
25
+ }
26
+
21
27
  export type AsyncResumeTarget = {
22
28
  kind: "live" | "revive";
23
29
  runId: string;
@@ -30,6 +36,47 @@ export type AsyncResumeTarget = {
30
36
  sessionFile?: string;
31
37
  };
32
38
 
39
+ type KillFn = (pid: number, signal?: NodeJS.Signals | 0) => boolean;
40
+
41
+ function readAsyncStatus(asyncDir: string): AsyncStatus | null {
42
+ const statusPath = path.join(asyncDir, "status.json");
43
+ try {
44
+ return JSON.parse(fs.readFileSync(statusPath, "utf-8")) as AsyncStatus;
45
+ } catch (error) {
46
+ const code = error && typeof error === "object" && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
47
+ if (code === "ENOENT") return null;
48
+ throw error;
49
+ }
50
+ }
51
+
52
+ export function interruptLiveAsyncResumeTarget(input: {
53
+ target: AsyncResumeTarget & { kind: "live" };
54
+ state?: Pick<SubagentState, "asyncJobs">;
55
+ kill?: KillFn;
56
+ now?: () => number;
57
+ }): { ok: true; asyncId: string } | { ok: false; message: string } {
58
+ const asyncId = input.target.runId;
59
+ if (!input.target.asyncDir) {
60
+ return { ok: false, message: `Async run ${asyncId} is live but does not have an async directory to interrupt.` };
61
+ }
62
+ const status = readAsyncStatus(input.target.asyncDir);
63
+ if (!status || status.state !== "running" || typeof status.pid !== "number") {
64
+ return { ok: false, message: `Async run ${asyncId} is live but no interrupt-capable runner pid was found.` };
65
+ }
66
+ try {
67
+ (input.kill ?? process.kill)(status.pid, ASYNC_RESUME_INTERRUPT_SIGNAL);
68
+ const tracked = input.state?.asyncJobs.get(asyncId);
69
+ if (tracked) {
70
+ tracked.activityState = undefined;
71
+ tracked.updatedAt = input.now?.() ?? Date.now();
72
+ }
73
+ return { ok: true, asyncId };
74
+ } catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ return { ok: false, message: `Failed to interrupt async run ${asyncId}: ${message}` };
77
+ }
78
+ }
79
+
33
80
  interface AsyncResultFile {
34
81
  id?: string;
35
82
  runId?: string;
@@ -236,9 +283,10 @@ function validateResumeSessionFile(runId: string, sessionFile: string): string {
236
283
  return resolved;
237
284
  }
238
285
 
239
- export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}): AsyncResumeTarget {
286
+ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncResumeDeps = {}, options: AsyncResumeOptions = {}): AsyncResumeTarget {
240
287
  const asyncDirRoot = deps.asyncDirRoot ?? ASYNC_DIR;
241
288
  const resultsDir = deps.resultsDir ?? RESULTS_DIR;
289
+ const requireSessionFile = options.requireSessionFile ?? true;
242
290
  const location = resolveAsyncRunLocation(params, asyncDirRoot, resultsDir);
243
291
  if (!location.asyncDir && !location.resultPath) {
244
292
  throw new Error("Async run not found. Provide id or dir.");
@@ -313,8 +361,8 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
313
361
  const sessionFile = statusSteps[index]?.sessionFile
314
362
  ?? resultSteps[index]?.sessionFile
315
363
  ?? (stepCount === 1 ? status?.sessionFile ?? result?.sessionFile : undefined);
316
- if (!sessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
317
- const resolvedSessionFile = validateResumeSessionFile(runId, sessionFile);
364
+ if (!sessionFile && requireSessionFile) throw new Error(`Async run '${runId}' child ${index} does not have a persisted session file to resume from.`);
365
+ const resolvedSessionFile = sessionFile ? validateResumeSessionFile(runId, sessionFile) : undefined;
318
366
 
319
367
  return {
320
368
  kind: "revive",
@@ -325,7 +373,7 @@ export function resolveAsyncResumeTarget(params: AsyncResumeParams, deps: AsyncR
325
373
  index,
326
374
  intercomTarget: resolveSubagentIntercomTarget(runId, agent, index),
327
375
  cwd: status?.cwd ?? result?.cwd,
328
- sessionFile: resolvedSessionFile,
376
+ ...(resolvedSessionFile ? { sessionFile: resolvedSessionFile } : {}),
329
377
  };
330
378
  }
331
379
 
@@ -56,6 +56,7 @@ export interface AsyncRunSummary {
56
56
  endedAt?: number;
57
57
  currentStep?: number;
58
58
  chainStepCount?: number;
59
+ pendingAppends?: number;
59
60
  parallelGroups?: AsyncParallelGroupStatus[];
60
61
  steps: AsyncRunStepSummary[];
61
62
  sessionDir?: string;
@@ -188,6 +189,7 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
188
189
  endedAt: status.endedAt,
189
190
  currentStep: status.currentStep,
190
191
  ...(status.chainStepCount !== undefined ? { chainStepCount: status.chainStepCount } : {}),
192
+ ...(status.pendingAppends !== undefined ? { pendingAppends: status.pendingAppends } : {}),
191
193
  ...(parallelGroups.length ? { parallelGroups } : {}),
192
194
  steps: summarizedSteps,
193
195
  ...(nestedChildren.length ? { nestedChildren } : {}),
@@ -309,7 +311,8 @@ function formatRunHeader(run: AsyncRunSummary): string {
309
311
  const stepLabel = formatAsyncRunProgressLabel(run);
310
312
  const cwd = run.cwd ? shortenPath(run.cwd) : shortenPath(run.asyncDir);
311
313
  const activity = formatActivityFacts(run);
312
- return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel} | ${cwd}`;
314
+ const pending = run.pendingAppends ? ` | ${run.pendingAppends} pending append${run.pendingAppends === 1 ? "" : "s"}` : "";
315
+ return `${run.id} | ${run.state}${activity ? ` | ${activity}` : ""} | ${run.mode} | ${stepLabel}${pending} | ${cwd}`;
313
316
  }
314
317
 
315
318
  export function formatAsyncRunList(runs: AsyncRunSummary[], heading = "Active async runs"): string {