@ryan_nookpi/pi-extension-subagent 0.3.4 → 0.4.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.
package/cli.ts CHANGED
@@ -53,8 +53,9 @@ export const SUBAGENT_CLI_HELP_TEXT = [
53
53
  " subagent help",
54
54
  " subagent agents",
55
55
  " subagent runs",
56
- " subagent status <runId>",
57
- " subagent detail <runId>",
56
+ " subagent status <runId|groupId>",
57
+ " subagent detail <runId|groupId>",
58
+ " (groupId = the b_.../p_... id returned by batch/chain launches; finished groups are retained briefly)",
58
59
  "",
59
60
  " Execution:",
60
61
  " subagent run <agent> [--main|--isolated] -- <task>",
@@ -167,6 +168,11 @@ function parseInteger(raw: string): number | null {
167
168
  return Number.isInteger(value) ? value : null;
168
169
  }
169
170
 
171
+ /** Group IDs returned by batch (`b_...`) and chain (`p_...`) launches. */
172
+ function isGroupId(raw: string): boolean {
173
+ return /^[bp]_/.test(raw);
174
+ }
175
+
170
176
  function parseRunTarget(
171
177
  raw: string,
172
178
  knownRunIds: number[] | undefined,
@@ -462,33 +468,35 @@ export function parseSubagentToolCommand(
462
468
  return { type: "params", params: { asyncAction: "list" } };
463
469
 
464
470
  case "status": {
465
- const runIdRaw = args[0];
466
- if (!runIdRaw)
471
+ const idRaw = args[0];
472
+ if (!idRaw)
467
473
  return {
468
474
  type: "error",
469
- message: `❌ status requires <runId>\n\n✓ Example: subagent status 22\n\nSee all runs with: subagent runs`,
475
+ message: `❌ status requires <runId|groupId>\n\n✓ Example: subagent status 22\n✓ Example: subagent status b_1712... (batch/chain)\n\nSee all runs with: subagent runs`,
470
476
  };
471
- const runId = parseInteger(runIdRaw);
477
+ if (isGroupId(idRaw)) return { type: "params", params: { asyncAction: "status", groupId: idRaw } };
478
+ const runId = parseInteger(idRaw);
472
479
  if (runId === null)
473
480
  return {
474
481
  type: "error",
475
- message: `❌ Invalid runId: "${runIdRaw}"\n\nThe runId must be a number. See all runs with: subagent runs`,
482
+ message: `❌ Invalid id: "${idRaw}"\n\nUse a numeric runId or a batch/chain groupId (b_.../p_...). See all runs with: subagent runs`,
476
483
  };
477
484
  return { type: "params", params: { asyncAction: "status", runId } };
478
485
  }
479
486
 
480
487
  case "detail": {
481
- const runIdRaw = args[0];
482
- if (!runIdRaw)
488
+ const idRaw = args[0];
489
+ if (!idRaw)
483
490
  return {
484
491
  type: "error",
485
- message: `❌ detail requires <runId>\n\n✓ Example: subagent detail 22\n\nSee all runs with: subagent runs`,
492
+ message: `❌ detail requires <runId|groupId>\n\n✓ Example: subagent detail 22\n✓ Example: subagent detail b_1712... (batch/chain)\n\nSee all runs with: subagent runs`,
486
493
  };
487
- const runId = parseInteger(runIdRaw);
494
+ if (isGroupId(idRaw)) return { type: "params", params: { asyncAction: "detail", groupId: idRaw } };
495
+ const runId = parseInteger(idRaw);
488
496
  if (runId === null)
489
497
  return {
490
498
  type: "error",
491
- message: `❌ Invalid runId: "${runIdRaw}"\n\nThe runId must be a number. See all runs with: subagent runs`,
499
+ message: `❌ Invalid id: "${idRaw}"\n\nUse a numeric runId or a batch/chain groupId (b_.../p_...). See all runs with: subagent runs`,
492
500
  };
493
501
  return { type: "params", params: { asyncAction: "detail", runId } };
494
502
  }
package/commands.ts CHANGED
@@ -51,7 +51,7 @@ import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session
51
51
  import { SUBAGENT_COMMANDS, type SubagentCommandName } from "./registration-manifest.js";
52
52
  import { readSessionReplayItems, SubagentSessionReplayOverlay } from "./replay.js";
53
53
  import { invokeWithAutoRetry, MAX_SUBAGENT_AUTO_RETRIES } from "./retry.js";
54
- import { getLatestRun, removeRun, trimCommandRunHistory } from "./run-utils.js";
54
+ import { evictStaleFinishedGroups, getLatestRun, removeRun, trimCommandRunHistory } from "./run-utils.js";
55
55
  import {
56
56
  getFinalOutput,
57
57
  getLastNonEmptyLine,
@@ -824,6 +824,7 @@ function restoreRunsFromSession(store: SubagentStore, ctx: any, pi?: ExtensionAP
824
824
  }
825
825
 
826
826
  evictStalePendingGroupCompletions(STALE_PENDING_COMPLETION_MS);
827
+ evictStaleFinishedGroups(store);
827
828
 
828
829
  // Fallback: if this session has no subagent markers at all, but we recently
829
830
  // had in-memory runs for the same session file, reuse that snapshot so
package/constants.ts CHANGED
@@ -30,6 +30,12 @@ export const SUBAGENT_STRONG_WAIT_MESSAGE =
30
30
  /** Maximum age (ms) for pending cross-session completions before eviction. */
31
31
  export const STALE_PENDING_COMPLETION_MS = 30 * 60 * 1_000;
32
32
 
33
+ /** Max number of finished batch/chain group snapshots retained for `status`/`detail` queries. */
34
+ export const MAX_FINISHED_GROUPS = 20;
35
+
36
+ /** Max age (ms) a finished group snapshot is retained before eviction. */
37
+ export const FINISHED_GROUP_TTL_MS = 30 * 60 * 1_000;
38
+
33
39
  /** Short label shown in the widget when inside a child session. */
34
40
  export const PARENT_HINT = "↩ parent (><)";
35
41
 
package/lifecycle.ts CHANGED
@@ -114,6 +114,7 @@ export function shutdownSubagentRuns(store: SubagentStore, pi: ExtensionAPI, rea
114
114
  store.globalLiveRuns.clear();
115
115
  store.batchGroups.clear();
116
116
  store.pipelines.clear();
117
+ store.finishedGroups.clear();
117
118
  store.recentLaunchTimestamps.clear();
118
119
  store.commandRuns.clear();
119
120
  store.commandWidgetCtx = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/run-utils.ts CHANGED
@@ -7,8 +7,15 @@
7
7
  */
8
8
 
9
9
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
+ import { FINISHED_GROUP_TTL_MS, MAX_FINISHED_GROUPS, STATUS_OUTPUT_PREVIEW_MAX_CHARS } from "./constants.js";
10
11
  import type { SubagentStore } from "./store.js";
11
- import type { CommandRunState } from "./types.js";
12
+ import type {
13
+ BatchGroupState,
14
+ CommandRunState,
15
+ FinishedGroupMember,
16
+ FinishedGroupSnapshot,
17
+ PipelineState,
18
+ } from "./types.js";
12
19
  import { updateCommandRunsWidget, type WidgetRenderCtx } from "./widget.js";
13
20
 
14
21
  export interface RemoveRunOptions {
@@ -201,3 +208,115 @@ export function trimCommandRunHistory(
201
208
 
202
209
  return removedRunIds;
203
210
  }
211
+
212
+ // ── Finished-group retention ────────────────────────────────────────────────
213
+ // Batch/chain groups are deleted from the live store once their completion is
214
+ // delivered. To keep `subagent status/detail <groupId>` working for a short
215
+ // window afterward, we retain an immutable snapshot per finished group.
216
+
217
+ function memberOutput(run: CommandRunState, fallback?: string): string {
218
+ return run.lastOutput?.trim() || run.lastLine?.trim() || fallback?.trim() || "(no output)";
219
+ }
220
+
221
+ /** Build a finished-group snapshot from a completed batch group. */
222
+ export function snapshotBatchGroup(
223
+ store: SubagentStore,
224
+ batch: BatchGroupState,
225
+ terminalStatus: FinishedGroupSnapshot["terminalStatus"],
226
+ ): FinishedGroupSnapshot {
227
+ let failed = 0;
228
+ const members: FinishedGroupMember[] = batch.runIds.map((runId) => {
229
+ const run = store.commandRuns.get(runId);
230
+ if (!run) {
231
+ if (batch.failedRunIds.has(runId)) failed++;
232
+ return {
233
+ summaryLine: `#${runId} [gone] (run no longer available)`,
234
+ output: batch.pendingResults.get(runId)?.trim() || "(no output)",
235
+ };
236
+ }
237
+ if (run.status === "error" || batch.failedRunIds.has(runId)) failed++;
238
+ return { summaryLine: formatCommandRunSummary(run), output: memberOutput(run, batch.pendingResults.get(runId)) };
239
+ });
240
+ return {
241
+ groupId: batch.batchId,
242
+ kind: "batch",
243
+ terminalStatus,
244
+ finishedAt: Date.now(),
245
+ total: batch.runIds.length,
246
+ failed,
247
+ members,
248
+ };
249
+ }
250
+
251
+ /** Build a finished-group snapshot from a completed pipeline. */
252
+ export function snapshotPipeline(
253
+ pipeline: PipelineState,
254
+ terminalStatus: FinishedGroupSnapshot["terminalStatus"],
255
+ ): FinishedGroupSnapshot {
256
+ const members: FinishedGroupMember[] = pipeline.stepResults.map((step, index) => ({
257
+ summaryLine: `Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}`,
258
+ output: step.output?.trim() || "(no output)",
259
+ task: step.task,
260
+ }));
261
+ return {
262
+ groupId: pipeline.pipelineId,
263
+ kind: "chain",
264
+ terminalStatus,
265
+ finishedAt: Date.now(),
266
+ total: pipeline.stepResults.length,
267
+ failed: pipeline.stepResults.filter((step) => step.status === "error").length,
268
+ members,
269
+ };
270
+ }
271
+
272
+ /** Retain a finished-group snapshot, evicting the oldest entries past the cap. */
273
+ export function retireFinishedGroup(store: SubagentStore, snapshot: FinishedGroupSnapshot): void {
274
+ // Re-insert to refresh insertion order (most-recent last).
275
+ store.finishedGroups.delete(snapshot.groupId);
276
+ store.finishedGroups.set(snapshot.groupId, snapshot);
277
+ while (store.finishedGroups.size > MAX_FINISHED_GROUPS) {
278
+ const oldest = store.finishedGroups.keys().next().value;
279
+ if (oldest === undefined) break;
280
+ store.finishedGroups.delete(oldest);
281
+ }
282
+ }
283
+
284
+ /** Evict finished-group snapshots older than the retention TTL. Returns count removed. */
285
+ export function evictStaleFinishedGroups(store: SubagentStore, now = Date.now()): number {
286
+ let removed = 0;
287
+ for (const [groupId, snapshot] of store.finishedGroups) {
288
+ if (now - snapshot.finishedAt > FINISHED_GROUP_TTL_MS) {
289
+ store.finishedGroups.delete(groupId);
290
+ removed++;
291
+ }
292
+ }
293
+ return removed;
294
+ }
295
+
296
+ function formatFinishedAge(finishedAt: number, now = Date.now()): string {
297
+ const seconds = Math.max(0, Math.round((now - finishedAt) / 1000));
298
+ if (seconds < 60) return `${seconds}s ago`;
299
+ const minutes = Math.round(seconds / 60);
300
+ return `${minutes}m ago`;
301
+ }
302
+
303
+ /** Render a retained finished-group snapshot for `status` (summary) or `detail` (with output). */
304
+ export function formatFinishedGroupStatus(snapshot: FinishedGroupSnapshot, detailed: boolean): string {
305
+ const label = snapshot.kind === "batch" ? "subagent-batch" : "subagent-chain";
306
+ const failedSuffix = snapshot.failed > 0 ? `, ${snapshot.failed} failed` : "";
307
+ const header = `[${label}#${snapshot.groupId}] ${snapshot.terminalStatus} · ${snapshot.total} ${
308
+ snapshot.kind === "batch" ? "runs" : "steps"
309
+ }${failedSuffix} · finished ${formatFinishedAge(snapshot.finishedAt)}`;
310
+ const body = snapshot.members
311
+ .map((member) => {
312
+ if (!detailed) return member.summaryLine;
313
+ const output =
314
+ member.output.length > STATUS_OUTPUT_PREVIEW_MAX_CHARS
315
+ ? `${member.output.slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS)}\n\n... [truncated]`
316
+ : member.output;
317
+ const taskLine = member.task ? `Task: ${member.task}\n` : "";
318
+ return `${member.summaryLine}\n${taskLine}${output}`;
319
+ })
320
+ .join(detailed ? "\n\n" : "\n");
321
+ return `${header}\n\n${body}`;
322
+ }
package/store.ts CHANGED
@@ -5,7 +5,14 @@
5
5
  import type { Message } from "@earendil-works/pi-ai";
6
6
  import { visibleWidth } from "@earendil-works/pi-tui";
7
7
  import { getDisplayItems, getFinalOutput, getLastNonEmptyLine, getLatestActivityPreview } from "./runner.js";
8
- import type { BatchGroupState, CommandRunState, GlobalRunEntry, PipelineState, SingleResult } from "./types.js";
8
+ import type {
9
+ BatchGroupState,
10
+ CommandRunState,
11
+ FinishedGroupSnapshot,
12
+ GlobalRunEntry,
13
+ PipelineState,
14
+ SingleResult,
15
+ } from "./types.js";
9
16
  import type { WidgetRenderCtx } from "./widget.js";
10
17
 
11
18
  export const COLLAPSED_ITEM_COUNT = 10;
@@ -40,6 +47,8 @@ export interface SubagentStore {
40
47
  batchGroups: Map<string, BatchGroupState>;
41
48
  /** In-memory sequential pipelines launched via the tool. */
42
49
  pipelines: Map<string, PipelineState>;
50
+ /** Retained snapshots of finished batch/chain groups, keyed by groupId (insertion-ordered). */
51
+ finishedGroups: Map<string, FinishedGroupSnapshot>;
43
52
  }
44
53
 
45
54
  export function createStore(): SubagentStore {
@@ -59,6 +68,7 @@ export function createStore(): SubagentStore {
59
68
  recentLaunchTimestamps: new Map(),
60
69
  batchGroups: new Map(),
61
70
  pipelines: new Map(),
71
+ finishedGroups: new Map(),
62
72
  };
63
73
  }
64
74
 
package/tool-execute.ts CHANGED
@@ -41,7 +41,17 @@ import {
41
41
  import { clearPendingGroupCompletion, upsertPendingGroupCompletion } from "./group-pending.js";
42
42
  import { enqueueSubagentInvocation } from "./invocation-queue.js";
43
43
  import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
44
- import { clearFinishedRuns, formatCommandRunSummary, removeRun, trimCommandRunHistory } from "./run-utils.js";
44
+ import {
45
+ clearFinishedRuns,
46
+ evictStaleFinishedGroups,
47
+ formatCommandRunSummary,
48
+ formatFinishedGroupStatus,
49
+ removeRun,
50
+ retireFinishedGroup,
51
+ snapshotBatchGroup,
52
+ snapshotPipeline,
53
+ trimCommandRunHistory,
54
+ } from "./run-utils.js";
45
55
  import { getFinalOutput, getLastNonEmptyLine, runSingleAgent } from "./runner.js";
46
56
  import {
47
57
  buildMainContextText,
@@ -54,10 +64,12 @@ import {
54
64
  import { formatStarterPackNotice, offerStarterPackIfEmpty } from "./starter-pack.js";
55
65
  import { type SubagentStore, updateRunFromResult } from "./store.js";
56
66
  import type {
67
+ BatchGroupState,
57
68
  BatchOrChainItem,
58
69
  CommandRunState,
59
70
  OnUpdateCallback,
60
71
  PendingCompletion,
72
+ PipelineState,
61
73
  PipelineStepResult,
62
74
  SingleResult,
63
75
  SubagentDetails,
@@ -583,6 +595,47 @@ function formatPipelineSummary(
583
595
  return `[subagent-chain#${pipelineId}] ${terminalStatus}\n\n${steps}`;
584
596
  }
585
597
 
598
+ function previewOutput(output: string): string {
599
+ const trimmed = output.trim() || "(no output yet)";
600
+ return trimmed.length > STATUS_OUTPUT_PREVIEW_MAX_CHARS
601
+ ? `${trimmed.slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS)}\n\n... [truncated]`
602
+ : trimmed;
603
+ }
604
+
605
+ function formatBatchGroupStatus(store: SubagentStore, batch: BatchGroupState, detailed: boolean): string {
606
+ const total = batch.runIds.length;
607
+ const done = batch.completedRunIds.size;
608
+ const failed = batch.failedRunIds.size;
609
+ const header = `[subagent-batch#${batch.batchId}] running · ${done}/${total} finished${
610
+ failed > 0 ? `, ${failed} failed` : ""
611
+ }`;
612
+ const body = batch.runIds
613
+ .map((runId) => {
614
+ const run = store.commandRuns.get(runId);
615
+ if (!run) return `#${runId} (unavailable)`;
616
+ const summary = formatCommandRunSummary(run);
617
+ if (!detailed) return summary;
618
+ return `${summary}\n${previewOutput(run.lastOutput ?? run.lastLine ?? "")}`;
619
+ })
620
+ .join(detailed ? "\n\n" : "\n");
621
+ return `${header}\n\n${body}`;
622
+ }
623
+
624
+ function formatPipelineGroupStatus(store: SubagentStore, pipeline: PipelineState, detailed: boolean): string {
625
+ const finishedCount = pipeline.stepResults.length;
626
+ const header = `[subagent-chain#${pipeline.pipelineId}] running · step ${pipeline.currentIndex + 1} (${finishedCount} finished)`;
627
+ const parts = pipeline.stepResults.map((step, index) => {
628
+ const line = `Step ${index + 1} · #${step.runId} ${step.agent} · ${step.status}`;
629
+ return detailed ? `${line}\nTask: ${step.task}\n${previewOutput(step.output)}` : line;
630
+ });
631
+ const runningRunId = pipeline.stepRunIds[finishedCount];
632
+ if (runningRunId !== undefined) {
633
+ const run = store.commandRuns.get(runningRunId);
634
+ if (run) parts.push(`Step ${finishedCount + 1} · ${formatCommandRunSummary(run)}`);
635
+ }
636
+ return `${header}\n\n${parts.join(detailed ? "\n\n" : "\n")}`;
637
+ }
638
+
586
639
  function toLaunchSummary(
587
640
  runState: Pick<CommandRunState, "agent" | "id" | "batchId" | "pipelineId" | "pipelineStepIndex">,
588
641
  mode: SubagentLaunchSummary["mode"],
@@ -842,6 +895,43 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
842
895
  };
843
896
  }
844
897
 
898
+ if ((asyncAction === "status" || asyncAction === "detail") && typeof cmdParams.groupId === "string") {
899
+ evictStaleFinishedGroups(store);
900
+ const groupId = cmdParams.groupId;
901
+ const detailed = asyncAction === "detail";
902
+ const finished = store.finishedGroups.get(groupId);
903
+ if (finished) {
904
+ return {
905
+ content: [{ type: "text", text: withIdleRunWarning(formatFinishedGroupStatus(finished, detailed)) }],
906
+ details: makeDetails("single"),
907
+ };
908
+ }
909
+ const batch = store.batchGroups.get(groupId);
910
+ if (batch) {
911
+ return {
912
+ content: [{ type: "text", text: withIdleRunWarning(formatBatchGroupStatus(store, batch, detailed)) }],
913
+ details: makeDetails("single"),
914
+ };
915
+ }
916
+ const pipeline = store.pipelines.get(groupId);
917
+ if (pipeline) {
918
+ return {
919
+ content: [{ type: "text", text: withIdleRunWarning(formatPipelineGroupStatus(store, pipeline, detailed)) }],
920
+ details: makeDetails("single"),
921
+ };
922
+ }
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: `Unknown subagent group "${groupId}". Finished groups are retained only briefly; use \`subagent runs\` to inspect individual runs.`,
928
+ },
929
+ ],
930
+ details: makeDetails("single"),
931
+ isError: true,
932
+ };
933
+ }
934
+
845
935
  const rawRunIds = Array.isArray(cmdParams.runIds) ? cmdParams.runIds : undefined;
846
936
  const invalidRunIds = (rawRunIds ?? []).filter((value) => !Number.isInteger(value));
847
937
  if (invalidRunIds.length > 0) {
@@ -1471,6 +1561,9 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1471
1561
  const orderedRuns = runStates.map(({ runState }) => runState);
1472
1562
  const hasError = finalizedRuns.some((finalized) => finalized.isError);
1473
1563
  const content = formatBatchSummary(batchId, orderedRuns, hasError ? "error" : "completed");
1564
+ const batchForSnapshot = store.batchGroups.get(batchId);
1565
+ if (batchForSnapshot)
1566
+ retireFinishedGroup(store, snapshotBatchGroup(store, batchForSnapshot, hasError ? "error" : "completed"));
1474
1567
  for (const { runState } of runStates) cleanupRunAfterFinalDelivery(runState.id);
1475
1568
  clearPendingGroupCompletion("batch", batchId);
1476
1569
  store.batchGroups.delete(batchId);
@@ -1525,6 +1618,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1525
1618
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1526
1619
  },
1527
1620
  };
1621
+ retireFinishedGroup(store, snapshotBatchGroup(store, batch, batchTerminalStatus));
1528
1622
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1529
1623
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1530
1624
  clearPendingGroupCompletion("batch", batchId);
@@ -1579,6 +1673,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1579
1673
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1580
1674
  },
1581
1675
  };
1676
+ retireFinishedGroup(store, snapshotBatchGroup(store, batch, "error"));
1582
1677
  if (isInOriginSession(ctx, batch.originSessionFile)) {
1583
1678
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1584
1679
  clearPendingGroupCompletion("batch", batchId);
@@ -1754,6 +1849,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1754
1849
  const hasError = pipeline.stepResults.some((step) => step.status === "error");
1755
1850
  if (terminalStatus === "completed" && hasError) terminalStatus = "error";
1756
1851
  const content = formatPipelineSummary(pipelineId, pipeline.stepResults, terminalStatus);
1852
+ retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
1757
1853
  for (const runId of pipeline.stepRunIds) cleanupRunAfterFinalDelivery(runId);
1758
1854
  clearPendingGroupCompletion("chain", pipelineId);
1759
1855
  store.pipelines.delete(pipelineId);
@@ -1892,6 +1988,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1892
1988
  runSummaries: orderedRuns.map((run) => buildRunAnalyticsSummary(run)),
1893
1989
  },
1894
1990
  };
1991
+ retireFinishedGroup(store, snapshotPipeline(pipeline, terminalStatus));
1895
1992
  if (isInOriginSession(ctx, pipeline.originSessionFile)) {
1896
1993
  pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
1897
1994
  clearPendingGroupCompletion("chain", pipelineId);
package/types.ts CHANGED
@@ -183,6 +183,30 @@ export interface PipelineState {
183
183
  pendingCompletion?: PendingCompletion;
184
184
  }
185
185
 
186
+ /** A single member (batch run or chain step) captured in a finished-group snapshot. */
187
+ export interface FinishedGroupMember {
188
+ /** Pre-rendered one-line summary, frozen at retirement time. */
189
+ summaryLine: string;
190
+ /** Full member output, truncated only when rendered. */
191
+ output: string;
192
+ /** Chain step task (omitted for batch runs). */
193
+ task?: string;
194
+ }
195
+
196
+ /**
197
+ * Immutable snapshot of a completed batch/chain group, retained briefly so
198
+ * `subagent status/detail <groupId>` still works after the live group is gone.
199
+ */
200
+ export interface FinishedGroupSnapshot {
201
+ groupId: string;
202
+ kind: "batch" | "chain";
203
+ terminalStatus: "completed" | "error" | "stopped";
204
+ finishedAt: number;
205
+ total: number;
206
+ failed: number;
207
+ members: FinishedGroupMember[];
208
+ }
209
+
186
210
  export const ListAgentsParams = Type.Object({});
187
211
 
188
212
  export const SubagentParams = Type.Object({