@gotgenes/pi-subagents 17.4.0 → 18.0.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.
@@ -0,0 +1,103 @@
1
+ ---
2
+ issue: 463
3
+ issue_title: "pi-subagents: add file-snapshot source to /subagent-sessions for evicted agents"
4
+ ---
5
+
6
+ # Retro: #463 — pi-subagents: add file-snapshot source to /subagent-sessions for evicted agents
7
+
8
+ ## Stage: Planning (2026-06-23T00:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 4-step plan for the Phase 19 Step 4b file-snapshot source: implement `fileSnapshotSource(outputFile, readFile)` in the pure `session-navigation.ts`, broaden the `/subagent-sessions` candidate set to evicted agents, and dual-source the handler by `NavigationEntry.kind`.
13
+ The central design fork — how evicted agents enter the picker — was resolved with the operator via `ask_user`.
14
+
15
+ ### Observations
16
+
17
+ - **Eviction is memory management.**
18
+ The cleanup sweep's `disposeSession()` frees the in-memory message history; the transcript survives only on disk.
19
+ So rendering an evicted agent *always* reads the file (`fileSnapshotSource`) regardless of candidate-set strategy — the strategy only affects the picker *label*.
20
+ - **Persisted child sessions carry no `type`/`description`.**
21
+ The JSONL has only the conversation plus a header (`id`, `timestamp`, `cwd`, `parentSession`).
22
+ A directory scan (the issue's literal wording) would therefore produce degraded labels and parse every file per open.
23
+ - **Decision: manager-retained descriptors over directory scan.**
24
+ The manager stashes a tiny no-messages `EvictedSubagent` descriptor in `cleanup()` before `removeRecord`, cleared in `clearCompleted()`/`dispose()`.
25
+ Rich labels identical to live entries, bounded memory, no per-open parse.
26
+ Coverage is limited to in-session evictions — which are the sweep's only targets, since a fresh manager per session never reloads prior-process subagents.
27
+ Operator confirmed; an `(evicted)` snapshot marker was also chosen for the label.
28
+ - **`NavigationEntry` becomes a discriminated union** (`live` | `evicted`); this breaks the handler, the `index.ts` call site, and both UI test files, so step 3 folds all of them into one commit.
29
+ - **SDK-runtime call kept direct.** `fileSnapshotSource` calls `parseSessionEntries` / `buildSessionContext` directly rather than injecting them — the injected `readFile` already provides the unit-test seam, and there is no `no-restricted-imports` rule.
30
+ - **Transient dead-code risk noted:** `fileSnapshotSource` and `listEvicted()` have no caller until step 3; flagged not to ship before step 3 lands (CI/`fallow` gate the pushed tip).
31
+ - Release: independent (Phase 19 Step 4b roadmap tag).
32
+
33
+ ## Stage: Implementation — TDD (2026-06-23T13:00:00Z)
34
+
35
+ ### Session summary
36
+
37
+ Executed all 4 plan steps in order: (1) `fileSnapshotSource` in the pure `session-navigation.ts`, (2) manager-retained `EvictedSubagent` descriptors (`cleanup` capture, `listEvicted`, `clearCompleted`/`dispose` clear), (3) the breaking `NavigationEntry` discriminated union + handler dual-source + `index.ts` wiring + all test updates in one commit, (4) architecture/ADR doc updates.
38
+ Test count went from 1088 to 1099 (+11); full suite, `check`, root `lint`, and `fallow dead-code` all green.
39
+
40
+ ### Observations
41
+
42
+ - **No deviations from the plan.**
43
+ All steps landed as written; Module-Level Changes matched the touched files exactly.
44
+ - **Exploratory probe paid off.**
45
+ A disposable script confirmed `buildSessionContext` auto-detects the leaf with no `leafId`, the `type !== "session"` filter drops the header, and empty entries yield `[]` — validating the `fileSnapshotSource` shape before writing the test.
46
+ - **Two ESLint auto-fixes during commit hooks:** a stray `!` non-null assertion in the manager test (step 2) and four `entry?.kind` optional chains on a non-nullish destructured `entry` (`@typescript-eslint/no-unnecessary-condition`, step 3).
47
+ Both fixed and re-committed; `check` + tests confirmed green after.
48
+ - **Transient dead code** between steps 1–2 and 3 (predicted in the plan) cleared at the step-3 tip; final `fallow dead-code` is clean.
49
+ - **Pre-completion reviewer: PASS** — deterministic checks, code design, test artifacts, Mermaid render, and all three cross-step invariants (no inbound core call, read-only overlay, renderer parity) verified; no follow-ups deferred.
50
+
51
+ ## Stage: Final Retrospective (2026-06-23T17:43:05Z)
52
+
53
+ ### Session summary
54
+
55
+ Single continuous session carried #463 from planning through ship: a 4-step plan, TDD implementation (+11 tests, 1088→1099), pre-completion PASS, and release of `pi-subagents` v17.5.0.
56
+ The defining moment was planning: an `ask_user` design gate plus operator Socratic pushback diverged the candidate-set design from the issue's literal "directory scan" wording to manager-retained descriptors.
57
+ Execution was notably clean — no rework, no plan deviations, two auto-fixed lint nits.
58
+
59
+ ### Observations
60
+
61
+ #### What went well
62
+
63
+ - **The `ask_user` gate caught a real design fork the issue body got wrong.**
64
+ The issue's "Proposed change" said "enumerate persisted child-session JSONL files" (a directory scan).
65
+ Exploration revealed the persisted session carries no subagent `type`/`description`, so a scan yields degraded labels.
66
+ The gate surfaced descriptors-vs-scan-vs-hybrid; the operator's "why are we evicting at all?"
67
+ and "tell me more about how the labels degrade" drove a better-grounded decision (descriptors).
68
+ This is the `/plan-issue` "treat Proposed change as a hypothesis" contract working as intended — novel because the divergence was the *enumeration mechanism*, not a surface ambiguity.
69
+ - **Exploratory probe before the first TDD test.**
70
+ A disposable script (`explore-session.mjs`, deleted after) confirmed `buildSessionContext` auto-detects the leaf with no `leafId` and the `type !== "session"` filter drops the header — validating the `fileSnapshotSource` shape before any test was written (the `testing` skill's "inspect the actual runtime shape first" rule).
71
+ - **The plan predicted transient dead code and sequenced to clear it.**
72
+ `fileSnapshotSource` and `listEvicted()` had no caller until step 3; the plan flagged this and ordered the integration step to close it, so the final `fallow dead-code` gate passed with no surprise suppressions.
73
+ - **Pre-completion reviewer ran on `claude-sonnet-4-6`** — model appropriate for the judgment-heavy invariant/design checklist; returned PASS.
74
+
75
+ #### What caused friction (agent side)
76
+
77
+ - `other` — the first `ask_user` led with the enumeration mechanism (descriptors/scan/hybrid) before establishing *why eviction creates the problem*.
78
+ The operator's first reply was "Why are we 'evicting' subagents in the first place?", i.e. asking for the framing I already held but had not front-loaded.
79
+ Impact: two extra `ask_user` round-trips before the decision — but they produced a better-understood outcome, so net-positive; no rework.
80
+ - `other` (lint nit) — four `entry?.kind` optional chains on a destructured array element tripped `@typescript-eslint/no-unnecessary-condition`, and a stray `!` tripped `no-unnecessary-type-assertion`; both surfaced via the pre-commit hook.
81
+ Impact: two re-commits during steps 2–3, auto-fixed, zero rework.
82
+
83
+ #### What caused friction (user side)
84
+
85
+ - None.
86
+ The operator's Socratic questions mid-`ask_user` were the session's highest-value input — they converted a plausible-but-degraded design (scan) into the right one (descriptors).
87
+ If anything, this is a model for how the gate should be used.
88
+
89
+ ### Diagnostic details
90
+
91
+ - **Model-performance correlation** — the `pre-completion-reviewer` subagent ran on `anthropic/claude-sonnet-4-6` (judgment-heavy review): appropriate, no mismatch.
92
+ Main-thread `model_change` events included a `deepseek-v4-flash` variant; no quality degradation was observed in any artifact (plan, code, tests all clean), so no actionable correlation.
93
+ - **Escalation-delay tracking** — no `rabbit-hole` points; longest same-error streak was the two lint auto-fixes, each resolved in one re-commit.
94
+ - **Unused-tool detection** — exploration used `colgrep`, `grep`, and a disposable runtime probe; no missed-tool opportunities.
95
+ - **Feedback-loop gap analysis** — verification ran incrementally: `pnpm run check` after the shared-interface steps (2 and 3), per-file `vitest` on each red/green cycle, and the full suite + lint + `fallow` before the docs commit.
96
+ No end-only verification.
97
+
98
+ ### Changes made
99
+
100
+ 1. Appended this Final Retrospective stage entry to `packages/pi-subagents/docs/retro/0463-file-snapshot-source-evicted-agents.md`.
101
+
102
+ No `AGENTS.md` or prompt changes — the session's friction was net-positive (the `ask_user` round-trips) or auto-fixed with zero rework (the lint nits).
103
+ Two candidate changes were considered and rejected as noise (a `code-design` note on `entry?.kind` optional chaining; an `ask-user` "why-before-mechanism" guidance already covered by the skill).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "17.4.0",
3
+ "version": "18.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/index.ts CHANGED
@@ -8,10 +8,9 @@
8
8
  * steer_subagent — LLM-callable: send a steering message to a running agent
9
9
  *
10
10
  * Commands:
11
- * /agents — Interactive agent management menu
12
11
  */
13
12
 
14
- import { join } from "node:path";
13
+ import { readFileSync } from "node:fs";
15
14
  import {
16
15
  createAgentSession,
17
16
  DefaultResourceLoader,
@@ -26,7 +25,6 @@ import { InterruptHandler, SessionLifecycleHandler, ToolStartHandler } from "#sr
26
25
  import { createChildLifecyclePublisher } from "#src/lifecycle/child-lifecycle";
27
26
  import { ConcurrencyLimiter } from "#src/lifecycle/concurrency-limiter";
28
27
  import { createSubagentSession, type SubagentSessionDeps } from "#src/lifecycle/create-subagent-session";
29
- import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
30
28
  import { SubagentManager } from "#src/lifecycle/subagent-manager";
31
29
  import { CompositeSubagentObserver } from "#src/observation/composite-subagent-observer";
32
30
  import { type NotificationDetails, NotificationManager } from "#src/observation/notification";
@@ -44,8 +42,6 @@ import { SettingsManager } from "#src/settings";
44
42
  import { AgentTool } from "#src/tools/agent-tool";
45
43
  import { GetResultTool } from "#src/tools/get-result-tool";
46
44
  import { SteerTool } from "#src/tools/steer-tool";
47
- import { FsAgentFileOps } from "#src/ui/agent-file-ops";
48
- import { AgentsMenuHandler } from "#src/ui/agent-menu";
49
45
  import { AgentWidget } from "#src/ui/agent-widget";
50
46
  import { SessionNavigatorHandler } from "#src/ui/session-navigator";
51
47
  import { SubagentsSettingsHandler } from "#src/ui/subagents-settings";
@@ -161,47 +157,32 @@ export default function (pi: ExtensionAPI) {
161
157
 
162
158
  pi.registerTool(new SteerTool(manager, pi.events).toToolDefinition());
163
159
 
164
- // ---- /agents interactive menu ----
165
-
166
- const agentsMenu = new AgentsMenuHandler(
167
- manager,
168
- registry,
169
- settings,
170
- new FsAgentFileOps(),
171
- join(getAgentDir(), "agents"),
172
- join(process.cwd(), ".pi", "agents"),
173
- );
174
-
175
- pi.registerCommand("agents", {
176
- description: "Manage agents",
177
- handler: async (_args, ctx) => {
178
- await agentsMenu.handle({
179
- ui: ctx.ui,
180
- modelRegistry: ctx.modelRegistry,
181
- parentSnapshot: buildParentSnapshot(ctx),
182
- });
183
- },
184
- });
185
-
186
- // ---- /subagents-settings command ----
160
+ // ---- /subagents:settings command ----
187
161
 
188
162
  const subagentsSettings = new SubagentsSettingsHandler(settings);
189
163
 
190
- pi.registerCommand("subagents-settings", {
164
+ pi.registerCommand("subagents:settings", {
191
165
  description: "Configure subagent settings (concurrency, turn limits)",
192
166
  handler: async (_args, ctx) => {
193
167
  await subagentsSettings.handle({ ui: ctx.ui });
194
168
  },
195
169
  });
196
170
 
197
- // ---- /subagent-sessions command ----
171
+ // ---- /subagents:sessions command ----
198
172
 
199
173
  const sessionNavigator = new SessionNavigatorHandler();
200
174
 
201
- pi.registerCommand("subagent-sessions", {
175
+ pi.registerCommand("subagents:sessions", {
202
176
  description: "View a subagent's session transcript (read-only)",
203
177
  handler: async (_args, ctx) => {
204
- await sessionNavigator.handle({ ui: ctx.ui, agents: manager.listAgents(), registry, cwd: ctx.cwd });
178
+ await sessionNavigator.handle({
179
+ ui: ctx.ui,
180
+ agents: manager.listAgents(),
181
+ evicted: manager.listEvicted(),
182
+ registry,
183
+ cwd: ctx.cwd,
184
+ readFile: (path) => readFileSync(path, "utf8"),
185
+ });
205
186
  },
206
187
  });
207
188
  }
@@ -14,12 +14,31 @@ import type { CreateSubagentSessionParams } from "#src/lifecycle/create-subagent
14
14
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
15
15
  import { Subagent, type SubagentLifecycleObserver } from "#src/lifecycle/subagent";
16
16
  import type { SubagentSession } from "#src/lifecycle/subagent-session";
17
- import { SubagentState } from "#src/lifecycle/subagent-state";
17
+ import { SubagentState, type SubagentStatus } from "#src/lifecycle/subagent-state";
18
18
  import type { WorkspaceProvider } from "#src/lifecycle/workspace";
19
19
 
20
20
  import type { RunConfig } from "#src/runtime";
21
21
  import type { AgentInvocation, CompactionInfo, ParentSessionInfo, SubagentType, ThinkingLevel } from "#src/types";
22
22
 
23
+ /**
24
+ * A lightweight snapshot of a subagent evicted by the 10-minute cleanup sweep.
25
+ *
26
+ * The sweep frees the heavy in-memory session (its message history included);
27
+ * this descriptor retains only the fields the session navigator needs to label
28
+ * the agent in the picker, plus the persisted `outputFile` to source its
29
+ * transcript from disk. Carries no messages, so memory stays bounded.
30
+ */
31
+ export interface EvictedSubagent {
32
+ readonly id: string;
33
+ readonly type: SubagentType;
34
+ readonly description: string;
35
+ readonly status: SubagentStatus;
36
+ readonly startedAt: number;
37
+ readonly completedAt: number | undefined;
38
+ readonly toolUses: number;
39
+ readonly outputFile: string;
40
+ }
41
+
23
42
  /** Observer interface for agent lifecycle notifications. */
24
43
  export interface SubagentManagerObserver {
25
44
  onSubagentStarted(record: Subagent): void;
@@ -65,6 +84,8 @@ export interface AgentSpawnConfig {
65
84
 
66
85
  export class SubagentManager {
67
86
  private agents = new Map<string, Subagent>();
87
+ /** Descriptors of agents removed by the cleanup sweep, keyed by id — navigable from disk. */
88
+ private readonly evicted = new Map<string, EvictedSubagent>();
68
89
  private cleanupInterval: ReturnType<typeof setInterval>;
69
90
  private readonly observer?: SubagentManagerObserver;
70
91
  private readonly createSubagentSession: (params: CreateSubagentSessionParams) => Promise<SubagentSession>;
@@ -220,6 +241,11 @@ export class SubagentManager {
220
241
  );
221
242
  }
222
243
 
244
+ /** Descriptors of agents evicted by the cleanup sweep, most recent first. */
245
+ listEvicted(): EvictedSubagent[] {
246
+ return [...this.evicted.values()].sort((a, b) => b.startedAt - a.startedAt);
247
+ }
248
+
223
249
  abort(id: string): boolean {
224
250
  const record = this.agents.get(id);
225
251
  if (!record) return false;
@@ -245,6 +271,9 @@ export class SubagentManager {
245
271
  for (const [id, record] of this.agents) {
246
272
  if (record.status === "running" || record.status === "queued") continue;
247
273
  if ((record.completedAt ?? 0) >= cutoff) continue;
274
+ // Retain a navigable descriptor before freeing the heavy session. Only an
275
+ // agent with a persisted file can be sourced from disk after eviction.
276
+ if (record.outputFile) this.evicted.set(id, toEvictedSubagent(record, record.outputFile));
248
277
  this.removeRecord(id, record);
249
278
  }
250
279
  }
@@ -258,6 +287,8 @@ export class SubagentManager {
258
287
  if (record.status === "running" || record.status === "queued") continue;
259
288
  this.removeRecord(id, record);
260
289
  }
290
+ // Evicted descriptors belong to the session that swept them — a new session starts empty.
291
+ this.evicted.clear();
261
292
  }
262
293
 
263
294
  /** Whether any agents are still running or queued. */
@@ -314,5 +345,20 @@ export class SubagentManager {
314
345
  record.disposeSession();
315
346
  }
316
347
  this.agents.clear();
348
+ this.evicted.clear();
317
349
  }
318
350
  }
351
+
352
+ /** Capture an evicted agent's navigable fields from its record. */
353
+ function toEvictedSubagent(record: Subagent, outputFile: string): EvictedSubagent {
354
+ return {
355
+ id: record.id,
356
+ type: record.type,
357
+ description: record.description,
358
+ status: record.status,
359
+ startedAt: record.startedAt,
360
+ completedAt: record.completedAt,
361
+ toolUses: record.toolUses,
362
+ outputFile,
363
+ };
364
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * session-navigation.ts — Pure selection and transcript-sourcing for native session navigation.
3
3
  *
4
- * Splits the unit-testable core of the `/subagent-sessions` command from its TUI
4
+ * Splits the unit-testable core of the `/subagents:sessions` command from its TUI
5
5
  * wiring (`session-navigator.ts`): which subagents are navigable and how a picked
6
6
  * agent's transcript is sourced (live, in this slice).
7
7
  *
@@ -12,8 +12,9 @@
12
12
  * components require a `TUI`, `cwd`, and markdown theme.
13
13
  */
14
14
 
15
- import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
15
+ import { buildSessionContext, parseSessionEntries, type SessionEntry, type ToolDefinition } from "@earendil-works/pi-coding-agent";
16
16
  import type { AgentConfigLookup } from "#src/config/agent-types";
17
+ import type { EvictedSubagent } from "#src/lifecycle/subagent-manager";
17
18
  import type { SubagentStatus } from "#src/lifecycle/subagent-state";
18
19
  import type { AgentSessionEvent, SessionMessage, SubagentType } from "#src/types";
19
20
  import { formatDuration, getDisplayName } from "#src/ui/display";
@@ -37,10 +38,24 @@ export interface NavigableSubagent {
37
38
  getToolDefinition(name: string): ToolDefinition | undefined;
38
39
  }
39
40
 
40
- /** A navigable entry: a record plus the label shown in the picker. */
41
- export interface NavigationEntry {
42
- readonly record: NavigableSubagent;
43
- readonly label: string;
41
+ /**
42
+ * A navigable entry plus the label shown in the picker.
43
+ *
44
+ * A `live` entry sources its transcript from the in-memory record; an `evicted`
45
+ * entry sources it from the persisted session file (the record is gone).
46
+ */
47
+ export type NavigationEntry =
48
+ | { readonly kind: "live"; readonly label: string; readonly record: NavigableSubagent }
49
+ | { readonly kind: "evicted"; readonly label: string; readonly outputFile: string };
50
+
51
+ /** The fields `buildLabel` reads — shared by a live record and an evicted descriptor. */
52
+ interface LabelFields {
53
+ readonly type: SubagentType;
54
+ readonly description: string;
55
+ readonly status: SubagentStatus;
56
+ readonly startedAt: number;
57
+ readonly completedAt: number | undefined;
58
+ readonly toolUses: number;
44
59
  }
45
60
 
46
61
  /** Running-agent streaming state, surfaced by a live source. */
@@ -61,14 +76,52 @@ export interface TranscriptSource {
61
76
  getToolDefinition(name: string): ToolDefinition | undefined;
62
77
  }
63
78
 
64
- /** Filter the agents to those with a viewable session and label each for the picker. */
79
+ /**
80
+ * Label every navigable subagent for the picker: live records with a viewable
81
+ * session, then agents evicted by the cleanup sweep (deduped against live ids).
82
+ */
65
83
  export function listNavigableAgents(
66
84
  agents: readonly NavigableSubagent[],
85
+ evicted: readonly EvictedSubagent[],
67
86
  registry: AgentConfigLookup,
68
87
  ): NavigationEntry[] {
69
- return agents
88
+ const live = agents
70
89
  .filter((record) => record.isSessionReady())
71
- .map((record) => ({ record, label: buildLabel(record, registry) }));
90
+ .map((record): NavigationEntry => ({ kind: "live", record, label: buildLabel(record, registry) }));
91
+ const liveIds = new Set(agents.map((record) => record.id));
92
+ const evictedEntries = evicted
93
+ .filter((descriptor) => !liveIds.has(descriptor.id))
94
+ .map((descriptor): NavigationEntry => ({
95
+ kind: "evicted",
96
+ outputFile: descriptor.outputFile,
97
+ label: buildLabel(descriptor, registry, true),
98
+ }));
99
+ return [...live, ...evictedEntries];
100
+ }
101
+
102
+ /**
103
+ * Source a transcript from a persisted child-session JSONL snapshot.
104
+ *
105
+ * For an agent evicted from the manager's map by the 10-minute cleanup sweep:
106
+ * the in-memory record (and its message history) is gone, but the session file
107
+ * survives on disk. Reads the file, drops the `SessionHeader`, and resolves the
108
+ * message list via Pi's own parser. A static snapshot — no subscription, no
109
+ * streaming, no live tool registry. `readFile` is injected so this module makes
110
+ * no `fs` calls.
111
+ */
112
+ export function fileSnapshotSource(
113
+ outputFile: string,
114
+ readFile: (path: string) => string,
115
+ ): TranscriptSource {
116
+ const entries = parseSessionEntries(readFile(outputFile));
117
+ const sessionEntries = entries.filter((entry): entry is SessionEntry => entry.type !== "session");
118
+ const { messages } = buildSessionContext(sessionEntries);
119
+ return {
120
+ getMessages: () => messages,
121
+ subscribe: () => undefined,
122
+ streaming: () => undefined,
123
+ getToolDefinition: () => undefined,
124
+ };
72
125
  }
73
126
 
74
127
  /** Source a transcript live from an in-memory record (this slice's only source). */
@@ -84,8 +137,9 @@ export function liveSource(record: NavigableSubagent): TranscriptSource {
84
137
  };
85
138
  }
86
139
 
87
- function buildLabel(record: NavigableSubagent, registry: AgentConfigLookup): string {
88
- const name = getDisplayName(record.type, registry);
89
- const duration = formatDuration(record.startedAt, record.completedAt);
90
- return `${name} (${record.description}) · ${record.toolUses} tools · ${record.status} · ${duration}`;
140
+ function buildLabel(fields: LabelFields, registry: AgentConfigLookup, evicted = false): string {
141
+ const name = getDisplayName(fields.type, registry);
142
+ const duration = formatDuration(fields.startedAt, fields.completedAt);
143
+ const marker = evicted ? " · evicted (snapshot)" : "";
144
+ return `${name} (${fields.description}) · ${fields.toolUses} tools · ${fields.status} · ${duration}${marker}`;
91
145
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * session-navigator.ts — The `/subagent-sessions` command: pick a subagent and
2
+ * session-navigator.ts — The `/subagents:sessions` command: pick a subagent and
3
3
  * read its transcript through Pi's own per-entry session components.
4
4
  *
5
5
  * SDK/TUI consumer half of native session navigation. The unit-testable core
@@ -38,9 +38,10 @@ import {
38
38
  visibleWidth,
39
39
  } from "@earendil-works/pi-tui";
40
40
  import type { AgentConfigLookup } from "#src/config/agent-types";
41
+ import type { EvictedSubagent } from "#src/lifecycle/subagent-manager";
41
42
  import type { SessionMessage } from "#src/types";
42
43
  import { describeActivity, type Theme } from "#src/ui/display";
43
- import { listNavigableAgents, liveSource, type NavigableSubagent, type TranscriptSource } from "#src/ui/session-navigation";
44
+ import { fileSnapshotSource, listNavigableAgents, liveSource, type NavigableSubagent, type TranscriptSource } from "#src/ui/session-navigation";
44
45
 
45
46
  // ─────────────────────────────────────────────────────────────────────────────
46
47
 
@@ -64,13 +65,17 @@ export interface SessionNavigatorUI {
64
65
  custom<R>(component: OverlayComponentFactory<R>, options?: unknown): Promise<R>;
65
66
  }
66
67
 
67
- /** Parameters for one `/subagent-sessions` invocation. */
68
+ /** Parameters for one `/subagents:sessions` invocation. */
68
69
  export interface SessionNavigatorParams {
69
70
  ui: SessionNavigatorUI;
70
71
  agents: readonly NavigableSubagent[];
72
+ /** Descriptors of agents evicted by the cleanup sweep, sourced from disk when picked. */
73
+ evicted: readonly EvictedSubagent[];
71
74
  registry: AgentConfigLookup;
72
75
  /** Working directory for tool-call rendering (relative path display). */
73
76
  cwd: string;
77
+ /** Reads a persisted session file for the file-snapshot source. */
78
+ readFile: (path: string) => string;
74
79
  }
75
80
 
76
81
  /** Options for the read-only transcript overlay. */
@@ -84,15 +89,15 @@ export interface TranscriptOverlayOptions {
84
89
  }
85
90
 
86
91
  /**
87
- * Handler for the `/subagent-sessions` slash command.
92
+ * Handler for the `/subagents:sessions` slash command.
88
93
  *
89
94
  * Lists navigable subagents, lets the operator pick one, and opens its transcript
90
95
  * read-only. Receives the agent snapshot (`manager.listAgents()`) rather than the
91
96
  * manager, so it stays a reactive consumer with no inbound call into the core.
92
97
  */
93
98
  export class SessionNavigatorHandler {
94
- async handle({ ui, agents, registry, cwd }: SessionNavigatorParams): Promise<void> {
95
- const entries = listNavigableAgents(agents, registry);
99
+ async handle({ ui, agents, evicted, registry, cwd, readFile }: SessionNavigatorParams): Promise<void> {
100
+ const entries = listNavigableAgents(agents, evicted, registry);
96
101
  if (entries.length === 0) {
97
102
  ui.notify("No subagent sessions to view.", "info");
98
103
  return;
@@ -105,7 +110,13 @@ export class SessionNavigatorHandler {
105
110
  const entry = entries.find((candidate) => candidate.label === choice);
106
111
  if (!entry) return;
107
112
 
108
- const source = liveSource(entry.record);
113
+ let source: TranscriptSource;
114
+ try {
115
+ source = entry.kind === "live" ? liveSource(entry.record) : fileSnapshotSource(entry.outputFile, readFile);
116
+ } catch {
117
+ ui.notify("Could not read the session transcript file.", "error");
118
+ return;
119
+ }
109
120
  const markdownTheme = getMarkdownTheme();
110
121
  await ui.custom<undefined>(
111
122
  (tui, theme, _keybindings, done) =>
@@ -1,6 +1,6 @@
1
1
  // ---- Narrow interfaces ----
2
2
 
3
- /** Narrow settings interface required by the subagents-settings command. */
3
+ /** Narrow settings interface required by the subagents:settings command. */
4
4
  export interface SubagentsSettingsManager {
5
5
  readonly maxConcurrent: number;
6
6
  readonly defaultMaxTurns: number | undefined;
@@ -20,7 +20,7 @@ export interface SubagentsSettingsUI {
20
20
  // ---- Class ----
21
21
 
22
22
  /**
23
- * Handler for the `/subagents-settings` slash command.
23
+ * Handler for the `/subagents:settings` slash command.
24
24
  *
25
25
  * Call `handle({ ui })` from the Pi command registration to open the interactive
26
26
  * settings list. Lifted from `AgentsMenuHandler.showSettings`.