@esso0428/pi-subagents 0.17.34 → 0.17.35

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/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.17.35] - 2026-10-05
11
+
12
+ ### Added
13
+ - **Scope agent history and the roster to the current session**: agent records now carry the session that spawned them, so `/agents` offers `Agent history this session` alongside `Agent history all sessions`, and the above-editor roster shows only this session's agents. Records restored from a session branch are stamped with the current session — a fork continues its parent's context, so those agents count as present here — while records reloaded from project checkpoints keep their own stamp, because those files mix every session that ran in the directory.
14
+ - **Legacy session history**: `/agents` → `Agent history all sessions` also recovers `subagents:record` entries written by earlier builds, so history from before durable checkpoints stayed reachable. Nothing writes those entries any more, and the scan is scoped to the project's own session directory.
15
+
16
+ ### Changed
17
+ - **Stopped appending `subagents:record` to the session branch on completion**: durable checkpoints supersede it. They survive reboot, record the owning session, and do not grow the parent session file. The entry is still read for backward compatibility.
18
+
19
+ ### Fixed
20
+ - **Orphaned nested rows keep their place**: a nested child whose parent record was removed keeps its original indent and says its parent is gone, instead of being promoted to a top-level row.
21
+
10
22
  ## [0.17.34] - 2026-10-05
11
23
 
12
24
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esso0428/pi-subagents",
3
- "version": "0.17.34",
3
+ "version": "0.17.35",
4
4
  "description": "A pi extension that brings smart Claude Code-style autonomous sub-agents to pi, with npm:pi-subagents-style JSON agent overrides.",
5
5
  "author": "ESSO0428",
6
6
  "repository": {
@@ -82,10 +82,18 @@ export function formatAgentHistoryOption(record: AgentRecord, now: number): stri
82
82
  export function buildAgentStatusMenuEntries(
83
83
  records: readonly AgentRecord[],
84
84
  cwd: string | undefined,
85
+ sessionId?: string,
85
86
  ): string[] {
86
87
  const { active, history } = splitAgentRecords(records, cwd);
88
+ // This-session history is what you almost always want, so it is the default
89
+ // entry. Project-wide history stays one keystroke away for the occasional
90
+ // "which agent was that, two sessions ago" lookup. Records restored from a
91
+ // branch or checkpoint are stamped with the current session, so an inherited
92
+ // agent counts as present here rather than disappearing into the other bucket.
93
+ const inSession = sessionId ? history.filter((record) => record.sessionId === sessionId) : [];
87
94
  return [
88
95
  ...(active.length > 0 ? [`Running agents (${active.length})`] : []),
89
- ...(history.length > 0 ? [`Agent history (${history.length})`] : []),
96
+ ...(inSession.length > 0 ? [`Agent history this session (${inSession.length})`] : []),
97
+ ...(history.length > 0 ? [`Agent history all sessions (${history.length})`] : []),
90
98
  ];
91
99
  }
@@ -159,6 +159,8 @@ interface SpawnArgs {
159
159
  }
160
160
 
161
161
  export interface SpawnOptions extends Partial<Omit<NestedSpawnOptions, "description" | "signal" | "onAssistantUsage" | "onSessionCreated">> {
162
+ /** Overrides the session stamp applied at spawn (used by restore paths). */
163
+ sessionId?: string;
162
164
  description: string;
163
165
  model?: Model<any>;
164
166
  maxTurns?: number;
@@ -284,6 +286,7 @@ export class AgentManager {
284
286
  // flattens, because nothing on disk records the nesting.
285
287
  ...(record.parentAgentId !== undefined && { parentAgentId: record.parentAgentId }),
286
288
  ...(record.depth !== undefined && { depth: record.depth }),
289
+ ...(record.sessionId !== undefined && { sessionId: record.sessionId }),
287
290
  };
288
291
  return checkpoint;
289
292
  }
@@ -341,6 +344,10 @@ export class AgentManager {
341
344
  this.recoveryCwds.set(checkpoint.id, cwd);
342
345
  continue;
343
346
  }
347
+ // No session stamp here: these files are project-level and mix every
348
+ // session that ever ran in this directory. Re-stamping them would make
349
+ // "this session" mean "everything", which is the thing the scope exists to
350
+ // avoid. Records with no stamp stay reachable under the all-sessions entry.
344
351
  this.agents.set(checkpoint.id, this.createRestoredRecord({
345
352
  ...checkpoint,
346
353
  status,
@@ -391,6 +398,7 @@ export class AgentManager {
391
398
  parentDescription: options.parentAgentId ? this.agents.get(options.parentAgentId)?.description : undefined,
392
399
  maxSubagentDepth: options.maxSubagentDepth,
393
400
  rootSessionId: options.rootSessionId ?? ctx.sessionManager?.getSessionId?.(),
401
+ sessionId: options.sessionId ?? ctx.sessionManager?.getSessionId?.(),
394
402
  liveActivity: {
395
403
  activeTools: new Map(),
396
404
  responseText: "",
@@ -780,11 +788,16 @@ export class AgentManager {
780
788
  }
781
789
 
782
790
  /** Restore terminal records persisted by a parent branch without runtime handles. */
783
- restoreCompleted(records: readonly unknown[]): void {
791
+ /**
792
+ * Restore terminal records carried by a parent branch. A fork continues its
793
+ * parent's context, so those agents count as present in this session and are
794
+ * stamped with the current session id rather than the one that spawned them.
795
+ */
796
+ restoreCompleted(records: readonly unknown[], sessionId?: string): void {
784
797
  const latest = new Map<string, ReturnType<typeof this.createRestoredRecord>>();
785
798
  for (const value of records) {
786
799
  if (isRestorableAgentRecord(value)) {
787
- latest.set(value.id, this.createRestoredRecord(value));
800
+ latest.set(value.id, this.createRestoredRecord({ ...value, sessionId }));
788
801
  }
789
802
  }
790
803
 
@@ -811,6 +824,7 @@ export class AgentManager {
811
824
  compactionCount?: number;
812
825
  parentAgentId?: string;
813
826
  depth?: number;
827
+ sessionId?: string;
814
828
  }): AgentRecord {
815
829
  return {
816
830
  id: record.id,
@@ -830,6 +844,7 @@ export class AgentManager {
830
844
  compactionCount: record.compactionCount ?? 0,
831
845
  parentAgentId: record.parentAgentId,
832
846
  depth: record.depth,
847
+ sessionId: record.sessionId,
833
848
  };
834
849
  }
835
850
 
@@ -36,6 +36,13 @@ export interface AgentRecoveryCheckpoint {
36
36
  parentAgentId?: string;
37
37
  /** Nesting depth of a nested child; absent on top-level agents. */
38
38
  depth?: number;
39
+ /**
40
+ * Session this record was spawned in. Absent on older checkpoints, and
41
+ * re-stamped with the *current* session when a branch or checkpoint restore
42
+ * brings the record forward, so "this session" means "present in this session"
43
+ * rather than "originally spawned here".
44
+ */
45
+ sessionId?: string;
39
46
  }
40
47
 
41
48
  function isSafeString(value: unknown, maxLength: number): value is string {
@@ -104,6 +111,7 @@ export function isAgentRecoveryCheckpoint(value: unknown): value is AgentRecover
104
111
  || (checkpoint.compactionCount as number) < 0) return false;
105
112
 
106
113
  if (checkpoint.parentAgentId !== undefined && !isSafeString(checkpoint.parentAgentId, 256)) return false;
114
+ if (checkpoint.sessionId !== undefined && !isSafeString(checkpoint.sessionId, 256)) return false;
107
115
  if (checkpoint.depth !== undefined && (!Number.isInteger(checkpoint.depth) || (checkpoint.depth as number) < 0)) return false;
108
116
 
109
117
  const status = checkpoint.status as AgentRecoveryStatus;
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import { loadCustomAgents } from "./custom-agents.js";
26
26
  import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
27
27
  import { GroupJoinManager } from "./group-join.js";
28
28
  import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
29
+ import { readLegacySessionRecords } from "./legacy-session-records.js";
29
30
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
30
31
  import { isScopeModelsEnabled, setScopeModelsEnabled } from "./model-scope.js";
31
32
  import { getMaxSubagentDepth, setMaxSubagentDepth } from "./nested-tools.js";
@@ -443,21 +444,10 @@ export default function (pi: ExtensionAPI) {
443
444
  pi.events.emit("subagents:completed", eventData);
444
445
  }
445
446
 
446
- // Persist final record for cross-extension history reconstruction
447
- pi.appendEntry("subagents:record", {
448
- id: record.id, type: record.type, description: record.description,
449
- status: record.status,
450
- // Durable transcripts are the source of truth for full output. Avoid
451
- // copying a potentially large result into the parent session branch;
452
- // get_subagent_result reloads it on demand after cleanup/restart.
453
- result: record.transcriptPath ? undefined : record.result,
454
- error: record.error,
455
- startedAt: record.startedAt, completedAt: record.completedAt,
456
- toolUses: record.toolUses,
457
- lifetimeUsage: record.lifetimeUsage,
458
- invocation: record.invocation,
459
- transcriptPath: record.transcriptPath,
460
- });
447
+ // Durable checkpoints replaced this entry: they survive reboot, carry the
448
+ // owning session, and do not bloat the parent session branch. It is still
449
+ // read for sessions recorded before that change, so history written by an
450
+ // older build stays reachable.
461
451
 
462
452
  // Explicit wait-group members never emit individual notifications. Result
463
453
  // consumption does not remove membership; the sealed group still delivers
@@ -590,10 +580,11 @@ export default function (pi: ExtensionAPI) {
590
580
  resetAgentMenuSelections();
591
581
  currentCtx = ctx;
592
582
  manager.clearCompleted(true);
583
+ const sessionId = ctx.sessionManager?.getSessionId?.();
593
584
  const branch = ctx.sessionManager?.getBranch?.() ?? [];
594
585
  manager.restoreCompleted(branch
595
586
  .filter((entry: any) => entry?.type === "custom" && entry?.customType === "subagents:record")
596
- .map((entry: any) => entry.data));
587
+ .map((entry: any) => entry.data), sessionId);
597
588
  // Checkpoint files cover agents whose parent session never got a terminal
598
589
  // branch entry (shutdown, session switch, or a process restart).
599
590
  manager.restoreRecovered(ctx.cwd);
@@ -666,6 +657,7 @@ export default function (pi: ExtensionAPI) {
666
657
  const ctx = currentCtx;
667
658
  if (ctx) void viewAgentConversation(ctx as ExtensionCommandContext, record, mode);
668
659
  },
660
+ getSessionId: () => currentCtx?.sessionManager?.getSessionId?.(),
669
661
  },
670
662
  );
671
663
 
@@ -1709,7 +1701,7 @@ Terse command-style prompts produce shallow, generic work.
1709
1701
  // Keep active agents and terminal history in separate menu entries.
1710
1702
  const records = manager.listAgents();
1711
1703
  const { active, history } = splitAgentRecords(records, ctx.cwd);
1712
- options.push(...buildAgentStatusMenuEntries(records, ctx.cwd));
1704
+ options.push(...buildAgentStatusMenuEntries(records, ctx.cwd, ctx.sessionManager?.getSessionId?.()));
1713
1705
 
1714
1706
  // Agent types list
1715
1707
  if (allNames.length > 0) {
@@ -1742,8 +1734,11 @@ Terse command-style prompts produce shallow, generic work.
1742
1734
  if (choice.startsWith("Running agents (")) {
1743
1735
  await showRunningAgents(ctx);
1744
1736
  await showAgentsMenu(ctx);
1745
- } else if (choice.startsWith("Agent history (")) {
1746
- await showAgentHistory(ctx);
1737
+ } else if (choice.startsWith("Agent history this session (")) {
1738
+ await showAgentHistory(ctx, "this-session");
1739
+ await showAgentsMenu(ctx);
1740
+ } else if (choice.startsWith("Agent history all sessions (")) {
1741
+ await showAgentHistory(ctx, "all-sessions");
1747
1742
  await showAgentsMenu(ctx);
1748
1743
  } else if (choice.startsWith("Agent types (")) {
1749
1744
  await showAllAgentsList(ctx);
@@ -1922,21 +1917,39 @@ Terse command-style prompts produce shallow, generic work.
1922
1917
  await showRunningAgents(ctx);
1923
1918
  }
1924
1919
 
1925
- async function showAgentHistory(ctx: ExtensionCommandContext) {
1920
+ async function showAgentHistory(ctx: ExtensionCommandContext, scope: "this-session" | "all-sessions" = "all-sessions") {
1926
1921
  const { history } = splitAgentRecords(manager.listAgents(), ctx.cwd);
1927
- if (history.length === 0) {
1928
- ctx.ui.notify("No agent history.", "info");
1922
+ const sessionId = ctx.sessionManager?.getSessionId?.();
1923
+ const scoped = scope === "this-session" && sessionId
1924
+ ? history.filter((record) => record.sessionId === sessionId)
1925
+ : history;
1926
+ if (scoped.length === 0) {
1927
+ ctx.ui.notify(scope === "this-session" ? "No agent history in this session." : "No agent history.", "info");
1929
1928
  return;
1930
1929
  }
1931
1930
 
1932
- const pairs = history.map((record) => ({ record, label: formatAgentHistoryOption(record, Date.now()) }));
1931
+ // Sessions recorded before durable checkpoints only exist as
1932
+ // `subagents:record` entries in their session file. They are merged here for
1933
+ // the project-wide view; the this-session view never needs the scan.
1934
+ const merged = scope === "this-session"
1935
+ ? scoped
1936
+ : mergeLegacyRecords(scoped, await readLegacySessionRecords(ctx.cwd));
1937
+ const pairs = merged.map((record) => ({ record, label: formatAgentHistoryOption(record, Date.now()) }));
1933
1938
  makeUniqueAgentOptionLabels(pairs);
1934
- const record = await selectAgentFromReadOnlyList(ctx, "Agent history", pairs, historyAgentSelection);
1939
+ const title = scope === "this-session" ? "Agent history — this session" : "Agent history — all sessions";
1940
+ const record = await selectAgentFromReadOnlyList(ctx, title, pairs, historyAgentSelection);
1935
1941
  if (!record) return;
1936
1942
 
1937
1943
  await viewAgentConversation(ctx, record, "history");
1938
1944
  // Back-navigation: re-show the list at the previously selected agent.
1939
- await showAgentHistory(ctx);
1945
+ await showAgentHistory(ctx, scope);
1946
+ }
1947
+
1948
+ /** In-memory records win; legacy session entries only fill genuine gaps. */
1949
+ function mergeLegacyRecords(current: readonly AgentRecord[], legacy: readonly AgentRecord[]): AgentRecord[] {
1950
+ const known = new Set(current.map((record) => record.id));
1951
+ return [...current, ...legacy.filter((record) => !known.has(record.id))]
1952
+ .sort((a, b) => b.startedAt - a.startedAt);
1940
1953
  }
1941
1954
 
1942
1955
  async function viewAgentConversation(
@@ -0,0 +1,159 @@
1
+ import { createReadStream, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
+ import { encodeCwd } from "./output-file.js";
5
+ import type { AgentInvocation, AgentRecord } from "./types.js";
6
+
7
+ /**
8
+ * Reads `subagents:record` entries out of session files.
9
+ *
10
+ * Older builds appended one such entry per finished agent to the parent session
11
+ * branch. Durable checkpoints superseded that: they survive reboot, carry the
12
+ * owning session, and do not bloat the session file. This reader exists purely
13
+ * so history written before that change stays reachable from `/agents`; nothing
14
+ * writes these entries any more.
15
+ *
16
+ * Session files can be very large, so each is streamed line by line and only
17
+ * lines mentioning the entry type are parsed.
18
+ */
19
+
20
+ const RECORD_MARKER = '"subagents:record"';
21
+ const SESSION_EXTENSION = ".jsonl";
22
+
23
+ /** `<timestamp>_<sessionId>.jsonl` — the sessionId is the part after the first underscore. */
24
+ function sessionIdFromFile(name: string): string | undefined {
25
+ if (!name.endsWith(SESSION_EXTENSION)) return undefined;
26
+ const base = name.slice(0, -SESSION_EXTENSION.length);
27
+ const separator = base.indexOf("_");
28
+ if (separator < 0) return undefined;
29
+ const id = base.slice(separator + 1);
30
+ return /^[0-9a-f-]{8,}$/i.test(id) ? id : undefined;
31
+ }
32
+
33
+ function isRecordLike(value: unknown): value is Record<string, unknown> {
34
+ if (!value || typeof value !== "object") return false;
35
+ const entry = value as Record<string, unknown>;
36
+ if (typeof entry.id !== "string" || entry.id.length === 0) return false;
37
+ return typeof entry.type === "string" && typeof entry.description === "string";
38
+ }
39
+
40
+ function toRecord(entry: Record<string, unknown>, sessionId: string): AgentRecord {
41
+ const startedAt = typeof entry.startedAt === "number" ? entry.startedAt : 0;
42
+ const transcriptPath = typeof entry.transcriptPath === "string" ? entry.transcriptPath : undefined;
43
+ return {
44
+ id: entry.id as string,
45
+ type: entry.type as string,
46
+ description: entry.description as string,
47
+ status: (typeof entry.status === "string" ? entry.status : "completed") as AgentRecord["status"],
48
+ startedAt,
49
+ completedAt: typeof entry.completedAt === "number" ? entry.completedAt : startedAt,
50
+ result: typeof entry.result === "string" ? entry.result : undefined,
51
+ error: typeof entry.error === "string" ? entry.error : undefined,
52
+ toolUses: typeof entry.toolUses === "number" ? entry.toolUses : 0,
53
+ compactionCount: 0,
54
+ lifetimeUsage: (entry.lifetimeUsage as AgentRecord["lifetimeUsage"]) ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
55
+ invocation: entry.invocation as AgentInvocation | undefined,
56
+ transcriptPath,
57
+ historyFile: transcriptPath,
58
+ sessionId,
59
+ };
60
+ }
61
+
62
+ /** Yield the parsed JSON value of each line that carries the marker. */
63
+ async function* matchingLines(path: string): AsyncGenerator<unknown> {
64
+ const stream = createReadStream(path, { encoding: "utf-8" });
65
+ let buffer = "";
66
+ for await (const chunk of stream) {
67
+ buffer += chunk;
68
+ let newline = buffer.indexOf("\n");
69
+ while (newline >= 0) {
70
+ const line = buffer.slice(0, newline);
71
+ buffer = buffer.slice(newline + 1);
72
+ if (line.includes(RECORD_MARKER)) {
73
+ try {
74
+ yield JSON.parse(line);
75
+ } catch { /* a malformed line must not hide the rest of the session */ }
76
+ }
77
+ newline = buffer.indexOf("\n");
78
+ }
79
+ }
80
+ if (buffer.includes(RECORD_MARKER)) {
81
+ try {
82
+ yield JSON.parse(buffer);
83
+ } catch { /* ignore trailing partial line */ }
84
+ }
85
+ }
86
+
87
+ function collectEntries(parsed: unknown, out: Record<string, unknown>[]): void {
88
+ if (Array.isArray(parsed)) {
89
+ for (const item of parsed) collectEntries(item, out);
90
+ return;
91
+ }
92
+ if (!parsed || typeof parsed !== "object") return;
93
+ const node = parsed as Record<string, unknown>;
94
+ if (node.customType === "subagents:record" && isRecordLike(node.data)) {
95
+ out.push(node.data);
96
+ }
97
+ for (const value of Object.values(node)) {
98
+ if (value && typeof value === "object") collectEntries(value, out);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Agent records this extension wrote into session files before durable
104
+ * checkpoints. Sessions whose directory does not exist simply contribute none.
105
+ */
106
+ export async function readLegacySessionRecords(cwd: string, agentDir: string = getAgentDir()): Promise<AgentRecord[]> {
107
+ const directory = join(agentDir, "sessions");
108
+ let files: string[];
109
+ try {
110
+ files = readdirSync(directory).filter((name) => name.endsWith(SESSION_EXTENSION));
111
+ } catch {
112
+ return [];
113
+ }
114
+
115
+ const records: AgentRecord[] = [];
116
+ const seen = new Set<string>();
117
+ void files;
118
+
119
+ // pi scopes sessions by project: <sessions>/--<encoded-cwd>--/<file>. Without
120
+ // this filter every project's agents would show up in every other project.
121
+ const projectDir = `--${encodeCwd(cwd)}--`;
122
+ const entriesIn: string[] = [];
123
+ try {
124
+ for (const name of readdirSync(directory, { withFileTypes: true })) {
125
+ if (name.isDirectory() && name.name === projectDir) entriesIn.push(name.name);
126
+ }
127
+ } catch {
128
+ return [];
129
+ }
130
+ if (entriesIn.length === 0) return records;
131
+
132
+ // Session files live one directory deeper: <sessions>/<encoded-cwd>/<file>.
133
+ for (const name of entriesIn.map((entryName) => ({ isDirectory: () => true, name: entryName }))) {
134
+ let sessionFiles: string[];
135
+ try {
136
+ sessionFiles = readdirSync(join(directory, name.name));
137
+ } catch {
138
+ continue;
139
+ }
140
+ for (const sessionFile of sessionFiles) {
141
+ const sessionId = sessionIdFromFile(sessionFile);
142
+ if (!sessionId) continue;
143
+ const entries: Record<string, unknown>[] = [];
144
+ try {
145
+ for await (const parsed of matchingLines(join(directory, name.name, sessionFile))) {
146
+ collectEntries(parsed, entries);
147
+ }
148
+ } catch {
149
+ continue;
150
+ }
151
+ for (const entry of entries) {
152
+ if (seen.has(entry.id as string)) continue;
153
+ seen.add(entry.id as string);
154
+ records.push(toRecord(entry, sessionId));
155
+ }
156
+ }
157
+ }
158
+ return records;
159
+ }
package/src/types.ts CHANGED
@@ -140,6 +140,8 @@ export interface AgentRecord {
140
140
  depth?: number;
141
141
  /** Parent agent ID for ownership-scoped nested controls. */
142
142
  parentAgentId?: string;
143
+ /** Session this record belongs to; re-stamped on restore. */
144
+ sessionId?: string;
143
145
  /** Parent description shown in a nested conversation viewer. */
144
146
  parentDescription?: string;
145
147
  /** Effective inherited nesting cap for this branch. */
@@ -57,6 +57,13 @@ export type AgentWidgetOpenCallback = (record: AgentRecord, mode: AgentWidgetOpe
57
57
  export type AgentWidgetOptions = {
58
58
  canOpenHistory: (record: AgentRecord) => boolean;
59
59
  onOpen: AgentWidgetOpenCallback;
60
+ /**
61
+ * Current session id. The roster shows only agents belonging to it, so a
62
+ * long-lived project directory does not fill the widget with work from other
63
+ * sessions. Read live — the session id is only known once pi has started.
64
+ * Nested children carry their parent's session id, so trees stay intact.
65
+ */
66
+ getSessionId?: () => string | undefined;
60
67
  };
61
68
  /** @deprecated Use AgentWidgetOpenMode. */
62
69
  export type AgentOpenMode = AgentWidgetOpenMode;
@@ -307,6 +314,7 @@ export class AgentWidget {
307
314
  private options: AgentWidgetOptions = {
308
315
  canOpenHistory: (record) => record.session !== undefined || record.completedAt !== undefined,
309
316
  onOpen: () => {},
317
+ getSessionId: () => undefined,
310
318
  },
311
319
  ) {}
312
320
 
@@ -322,9 +330,15 @@ export class AgentWidget {
322
330
  * - `all`: every agent.
323
331
  */
324
332
  private widgetAgents() {
325
- const all = this.manager.listAgents();
333
+ if (this.mode() === "off") return [];
334
+ // Scope to this session before the mode filter. Restored records from other
335
+ // sessions are still openable through `/agents` history; they just do not
336
+ // clutter the roster sitting under the editor.
337
+ const sessionId = this.options.getSessionId?.();
338
+ const all = sessionId
339
+ ? this.manager.listAgents().filter(record => record.sessionId === sessionId)
340
+ : this.manager.listAgents();
326
341
  switch (this.mode()) {
327
- case "off": return [];
328
342
  case "background": return all.filter(a => a.parentAgentId !== undefined || a.isBackground !== false);
329
343
  default: return all;
330
344
  }