@nklisch/pi-enhanced 0.2.7 → 0.3.1

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/node_modules/@nklisch/pi-clearance/native/clearance-core.linux-x64-gnu.node +0 -0
  3. package/node_modules/@nklisch/pi-clearance/native/clearance-core.win32-x64-msvc.node +0 -0
  4. package/node_modules/@nklisch/pi-plugins/README.md +4 -2
  5. package/node_modules/@nklisch/pi-plugins/dist/hooks.js +81 -8
  6. package/node_modules/@nklisch/pi-plugins/dist/hooks.js.map +1 -1
  7. package/node_modules/@nklisch/pi-plugins/dist/mcp.js +32 -1
  8. package/node_modules/@nklisch/pi-plugins/dist/mcp.js.map +1 -1
  9. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.d.ts +10 -4
  10. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js +144 -10
  11. package/node_modules/@nklisch/pi-plugins/dist/pi/plugin-manager.js.map +1 -1
  12. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js +40 -14
  13. package/node_modules/@nklisch/pi-plugins/dist/runtime-discovery.js.map +1 -1
  14. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/CHANGELOG.md +27 -0
  15. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/README.md +76 -47
  16. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/public.d.ts +171 -140
  17. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/dist/settings.d.ts +2 -2
  18. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/FORK-MAINTENANCE.md +8 -7
  19. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/VISION.md +12 -10
  20. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/architecture/architecture.md +118 -104
  21. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/comparison-with-upstream.md +3 -3
  22. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/docs/decisions/0004-reconsider-ui-direction.md +5 -0
  23. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/package.json +1 -1
  24. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/custom-agents.ts +31 -4
  25. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/default-agents.ts +1 -1
  26. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/config/invocation-config.ts +7 -4
  27. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/index.ts +18 -9
  28. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/layered-settings.ts +2 -2
  29. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/child-lifecycle.ts +10 -63
  30. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/concurrency-limiter.ts +122 -76
  31. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/create-subagent-session.ts +3 -5
  32. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/lifecycle-interceptor.ts +4 -3
  33. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/run-listeners.ts +2 -23
  34. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-manager.ts +177 -252
  35. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-session.ts +90 -280
  36. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent-state.ts +153 -239
  37. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/subagent.ts +569 -700
  38. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/lifecycle/workspace.ts +1 -1
  39. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/notification.ts +78 -122
  40. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/renderer.ts +15 -42
  41. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/observation/subagent-events-observer.ts +44 -95
  42. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service-adapter.ts +157 -90
  43. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/service/service.ts +60 -68
  44. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/conversation.ts +3 -3
  45. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query-source.ts +28 -0
  46. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/session/query.ts +488 -0
  47. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/settings.ts +1 -1
  48. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/agent-tool.ts +171 -249
  49. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-tool.ts +51 -94
  50. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/helpers.ts +9 -6
  51. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/list-tool.ts +46 -0
  52. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/parent-tool-registry.ts +17 -0
  53. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/query-session-tool.ts +412 -0
  54. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/result-renderer.ts +27 -111
  55. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/resume-tool.ts +105 -0
  56. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/spawn-config.ts +26 -8
  57. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/steer-tool.ts +27 -111
  58. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/stop-tool.ts +53 -0
  59. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/types.ts +11 -5
  60. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/agent-widget.ts +33 -27
  61. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/display.ts +6 -3
  62. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigation.ts +121 -10
  63. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/session-navigator.ts +430 -112
  64. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/subagents-settings.ts +1 -1
  65. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/ui/widget-renderer.ts +8 -9
  66. package/node_modules/@nklisch/pi-plugins/package.json +2 -2
  67. package/package.json +1 -1
  68. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/background-spawner.ts +0 -81
  69. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/foreground-runner.ts +0 -149
  70. package/node_modules/@nklisch/pi-plugins/node_modules/@nklisch/pi-subagents/src/tools/get-result-report.ts +0 -77
@@ -87,14 +87,14 @@ export class AgentWidget implements SubagentManagerObserver {
87
87
  private widgetFrame = 0;
88
88
  private widgetInterval: ReturnType<typeof setInterval> | undefined;
89
89
  /**
90
- * Bounded reactive read model: lifecycle callbacks add active background
90
+ * Bounded reactive read model: lifecycle callbacks add active detached
91
91
  * records and the small terminal linger set; render ticks never ask the
92
92
  * manager to clone or sort its retained history.
93
93
  */
94
- private readonly backgroundAgents = new Map<string, Subagent>();
94
+ private readonly detachedAgents = new Map<string, Subagent>();
95
95
  /** Tracks how many turns each finished agent has survived. */
96
96
  private finishedTurnAge = new Map<string, number>();
97
- /** How many extra turns errors/aborted agents linger (completed agents clear after 1 turn). */
97
+ /** How many extra turns errors/stopped agents linger (completed agents clear after 1 turn). */
98
98
  private static readonly ERROR_LINGER_TURNS = 2;
99
99
  /** Pi renders the whole component tree for each requestRender call. */
100
100
  private static readonly STATUS_REFRESH_INTERVAL_MS = 500;
@@ -122,7 +122,7 @@ export class AgentWidget implements SubagentManagerObserver {
122
122
  // Lifecycle events can arrive before the first tool gives us a UI context.
123
123
  // Refresh when that context becomes available, but let update() decide
124
124
  // whether any active record actually needs an animation timer.
125
- if (contextChanged && this.uiCtx && this.backgroundAgents.size > 0) this.refresh();
125
+ if (contextChanged && this.uiCtx && this.detachedAgents.size > 0) this.refresh();
126
126
  }
127
127
 
128
128
  /**
@@ -131,13 +131,13 @@ export class AgentWidget implements SubagentManagerObserver {
131
131
  */
132
132
  onTurnStart() {
133
133
  // Age terminal records and remove them before the next render. Completed
134
- // records linger for one turn; errors and aborted records linger for two.
134
+ // records linger for one turn; errors and stopped records linger for two.
135
135
  for (const [id, age] of this.finishedTurnAge) {
136
- const record = this.backgroundAgents.get(id);
136
+ const record = this.detachedAgents.get(id);
137
137
  const nextAge = age + 1;
138
138
  if (!record || nextAge >= this.maxFinishedAge(record.status)) {
139
139
  this.finishedTurnAge.delete(id);
140
- this.backgroundAgents.delete(id);
140
+ this.detachedAgents.delete(id);
141
141
  } else {
142
142
  this.finishedTurnAge.set(id, nextAge);
143
143
  }
@@ -149,47 +149,47 @@ export class AgentWidget implements SubagentManagerObserver {
149
149
 
150
150
  /** A subagent started running — ensure the update loop is live and render. */
151
151
  onSubagentStarted(record: Subagent) {
152
- if (!this.trackBackground(record)) return;
152
+ if (!this.trackDetached(record)) return;
153
153
  this.finishedTurnAge.delete(record.id);
154
154
  this.startLoop();
155
155
  }
156
156
 
157
- /** A background subagent was created (queued) — ensure the loop is live and render. */
157
+ /** A detached subagent was created (queued) — ensure the loop is live and render. */
158
158
  onSubagentCreated(record: Subagent) {
159
- if (!this.trackBackground(record)) return;
159
+ if (!this.trackDetached(record)) return;
160
160
  this.finishedTurnAge.delete(record.id);
161
161
  this.startLoop();
162
162
  }
163
163
 
164
164
  /** A subagent completed — seed its bounded linger entry and render. */
165
165
  onSubagentCompleted(record: Subagent) {
166
- if (!this.trackBackground(record)) return;
166
+ if (!this.trackDetached(record)) return;
167
167
  this.finishedTurnAge.set(record.id, 0);
168
168
  this.refresh();
169
169
  }
170
170
 
171
171
  /** A resumed subagent started — ensure the update loop is live and render. */
172
172
  onSubagentResumedStarted(record: Subagent) {
173
- if (!this.trackBackground(record)) return;
173
+ if (!this.trackDetached(record)) return;
174
174
  this.finishedTurnAge.delete(record.id);
175
175
  this.startLoop();
176
176
  }
177
177
 
178
178
  /** A resumed subagent settled — seed its terminal linger entry. */
179
179
  onSubagentResumed(record: Subagent) {
180
- if (!this.trackBackground(record)) return;
180
+ if (!this.trackDetached(record)) return;
181
181
  this.finishedTurnAge.set(record.id, 0);
182
182
  this.refresh();
183
183
  }
184
184
 
185
185
  /** A subagent's session compacted — render to refresh the compaction count. */
186
186
  onSubagentCompacted(record: Subagent, _info: CompactionInfo) {
187
- if (this.trackBackground(record)) this.refresh();
187
+ if (this.trackDetached(record)) this.refresh();
188
188
  }
189
189
 
190
190
  /** Remove terminal state when the manager clears a parent session. */
191
191
  onSubagentCleared(record: Subagent) {
192
- if (!this.backgroundAgents.delete(record.id)) return;
192
+ if (!this.detachedAgents.delete(record.id)) return;
193
193
  this.finishedTurnAge.delete(record.id);
194
194
  this.refresh();
195
195
  }
@@ -226,19 +226,23 @@ export class AgentWidget implements SubagentManagerObserver {
226
226
  return ERROR_STATUSES.has(status) ? AgentWidget.ERROR_LINGER_TURNS : 1;
227
227
  }
228
228
 
229
- /** Add a record only when its immutable invocation snapshot marks it background. */
230
- private trackBackground(record: Subagent): boolean {
231
- if (record.invocation?.runInBackground !== true) return false;
232
- this.backgroundAgents.set(record.id, record);
229
+ /** Add a record only when its current run mode is detached. */
230
+ private trackDetached(record: Subagent): boolean {
231
+ if (record.mode !== "detached") {
232
+ this.detachedAgents.delete(record.id);
233
+ this.finishedTurnAge.delete(record.id);
234
+ return false;
235
+ }
236
+ this.detachedAgents.set(record.id, record);
233
237
  return true;
234
238
  }
235
239
 
236
240
  /** Drop terminal entries whose turn-based linger window has already elapsed. */
237
241
  private pruneExpiredFinished(): void {
238
- for (const [id, record] of this.backgroundAgents) {
242
+ for (const [id, record] of this.detachedAgents) {
239
243
  if (record.isActive()) continue;
240
244
  if (record.completedAt == null || !this.shouldShowFinished(id, record.status)) {
241
- this.backgroundAgents.delete(id);
245
+ this.detachedAgents.delete(id);
242
246
  this.finishedTurnAge.delete(id);
243
247
  }
244
248
  }
@@ -250,12 +254,14 @@ export class AgentWidget implements SubagentManagerObserver {
250
254
  id: record.id,
251
255
  type: record.type,
252
256
  status: record.status,
257
+ terminalReason: record.stateTerminalReason,
253
258
  description: record.description,
254
259
  modelLabel: record.modelLabel,
255
260
  thinkingLevel: record.effectiveThinkingLevel,
256
261
  toolUses: record.toolUses,
257
262
  startedAt: record.startedAt,
258
263
  completedAt: record.completedAt,
264
+ activeRuntimeMs: record.activeRuntimeMs,
259
265
  error: record.error,
260
266
  lifetimeUsage: record.lifetimeUsage,
261
267
  compactionCount: record.compactionCount,
@@ -270,7 +276,7 @@ export class AgentWidget implements SubagentManagerObserver {
270
276
  /** Delegate rendering to the pure widget-renderer module. */
271
277
  private renderWidget(tui: any, theme: Theme): string[] {
272
278
  return renderWidgetLines({
273
- agents: [...this.backgroundAgents.values()].map(r => this.toWidgetAgent(r)),
279
+ agents: [...this.detachedAgents.values()].map(r => this.toWidgetAgent(r)),
274
280
  registry: this.registry,
275
281
  spinnerFrame: this.widgetFrame,
276
282
  terminalWidth: tui.terminal.columns,
@@ -295,7 +301,7 @@ export class AgentWidget implements SubagentManagerObserver {
295
301
  this.lastStatusText = undefined;
296
302
  }
297
303
  this.stopTimer();
298
- this.backgroundAgents.clear();
304
+ this.detachedAgents.clear();
299
305
  this.finishedTurnAge.clear();
300
306
  }
301
307
 
@@ -316,8 +322,8 @@ export class AgentWidget implements SubagentManagerObserver {
316
322
  if (!this.uiCtx) return;
317
323
 
318
324
  this.pruneExpiredFinished();
319
- const backgroundAgents = [...this.backgroundAgents.values()];
320
- const state = assembleWidgetState(backgroundAgents, (id, status) => this.shouldShowFinished(id, status));
325
+ const detachedAgents = [...this.detachedAgents.values()];
326
+ const state = assembleWidgetState(detachedAgents, (id, status) => this.shouldShowFinished(id, status));
321
327
 
322
328
  if (!state.hasActive && !state.hasFinished) {
323
329
  this.clearWidget();
@@ -329,7 +335,7 @@ export class AgentWidget implements SubagentManagerObserver {
329
335
  // static completion widget remains registered.
330
336
  if (state.hasActive) this.ensureTimer();
331
337
  else this.stopTimer();
332
- this.updateStatusBar(state, backgroundAgents);
338
+ this.updateStatusBar(state, detachedAgents);
333
339
  this.widgetFrame++;
334
340
 
335
341
  // Register widget callback once; subsequent updates use requestRender()
@@ -387,7 +393,7 @@ export class AgentWidget implements SubagentManagerObserver {
387
393
  this.widgetRegistered = false;
388
394
  this.tui = undefined;
389
395
  this.lastStatusText = undefined;
390
- this.backgroundAgents.clear();
396
+ this.detachedAgents.clear();
391
397
  this.finishedTurnAge.clear();
392
398
  }
393
399
  }
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { AgentConfigLookup } from "#src/config/agent-types";
9
9
  import { GLYPHS } from "#src/ui/glyphs";
10
+ import type { SubagentTerminalReason } from "#src/lifecycle/subagent-state";
10
11
  import type { AgentInvocation, SubagentType, ThinkingLevel } from "#src/types";
11
12
 
12
13
  // ---- Types ----
@@ -24,7 +25,8 @@ export interface AgentDetails {
24
25
  toolUses: number;
25
26
  tokens: string;
26
27
  durationMs: number;
27
- status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
28
+ status: "queued" | "running" | "completed" | "stopped" | "error";
29
+ terminalReason?: SubagentTerminalReason;
28
30
  /** Human-readable description of what the agent is currently doing. */
29
31
  activity?: string;
30
32
  /** Current spinner frame index (for animated running indicator). */
@@ -49,7 +51,7 @@ export interface AgentDetails {
49
51
  export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
50
52
 
51
53
  /** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
52
- export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
54
+ export const ERROR_STATUSES = new Set(["error", "stopped"]);
53
55
 
54
56
  /** Tool name → human-readable action for activity descriptions. */
55
57
  const TOOL_DISPLAY: Record<string, string> = {
@@ -146,8 +148,9 @@ export function buildInvocationTags(
146
148
  if (!invocation) return { tags };
147
149
  if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
148
150
  if (invocation.inheritContext) tags.push("inherit context");
149
- if (invocation.runInBackground) tags.push("background");
151
+ if (invocation.mode) tags.push(`mode: ${invocation.mode}`);
150
152
  if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
153
+ if (invocation.timeoutSeconds != null) tags.push(`timeout: ${invocation.timeoutSeconds}s`);
151
154
  return { modelName: invocation.modelName, tags };
152
155
  }
153
156
 
@@ -3,20 +3,22 @@
3
3
  *
4
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
- * agent's transcript is sourced (live, in this slice).
6
+ * agent's transcript is sourced (live or a retained file snapshot).
7
7
  *
8
8
  * The `TranscriptSource` seam decouples *how messages are sourced* (live record
9
- * here; a file snapshot in a follow-up) from *how they render* — the renderer
9
+ * or a retained file snapshot) from *how they render* — the renderer
10
10
  * (`session-navigator.ts`, which mounts Pi's per-entry components) talks only to
11
11
  * this seam. Rendering lives in the SDK/TUI module because the per-entry
12
- * components require a `TUI`, `cwd`, and markdown theme.
12
+ * components require a `TUI`, `cwd`, and markdown theme. The query tool uses
13
+ * the same JSONL adapter so file and live reads cannot drift.
13
14
  */
14
15
 
15
- import { buildSessionContext, parseSessionEntries, type SessionEntry, type ToolDefinition } from "@earendil-works/pi-coding-agent";
16
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
16
17
  import type { AgentConfigLookup } from "#src/config/agent-types";
17
18
  import type { SubagentStatus } from "#src/lifecycle/subagent-state";
18
19
  import type { AgentSessionEvent, SessionMessage, SubagentType, ThinkingLevel } from "#src/types";
19
20
  import { formatDuration, formatModelThinking, getDisplayName } from "#src/ui/display";
21
+ import { parseSessionFileMessages } from "#src/session/query-source";
20
22
 
21
23
  // ─────────────────────────────────────────────────────────────────────────────
22
24
 
@@ -37,14 +39,17 @@ export interface NavigableSubagent {
37
39
  readonly agentMessages: readonly SessionMessage[];
38
40
  isSessionReady(): boolean;
39
41
  subscribeToUpdates(fn: (event: AgentSessionEvent) => void): (() => void) | undefined;
42
+ /** Notifies consumers when a retained session is released and changes source. */
43
+ subscribeToRecordUpdates?: (fn: () => void) => () => void;
40
44
  getToolDefinition(name: string): ToolDefinition | undefined;
41
45
  }
42
46
 
43
47
  /**
44
48
  * A navigable entry plus the label shown in the picker.
45
49
  *
46
- * A `live` entry sources its transcript from the in-memory session; an
47
- * `evicted`-kind entry is a persisted snapshot for a released live session.
50
+ * A `live` entry sources its transcript from the in-memory session and can
51
+ * transition to its retained file; an `evicted`-kind entry is a persisted
52
+ * snapshot for a released live session.
48
53
  */
49
54
  export interface RunDisplayMetadata {
50
55
  readonly modelLabel: string;
@@ -75,6 +80,12 @@ export interface StreamingState {
75
80
  readonly responseText: string;
76
81
  }
77
82
 
83
+ /** Source state shown when a live record is released or its snapshot cannot load. */
84
+ export type TranscriptSourceAvailability =
85
+ | { readonly kind: "live"; readonly path?: string }
86
+ | { readonly kind: "file"; readonly path: string }
87
+ | { readonly kind: "unavailable"; readonly path?: string; readonly error?: string };
88
+
78
89
  /** Liveness-agnostic transcript source consumed by the renderer. */
79
90
  export interface TranscriptSource {
80
91
  /** Current message history. */
@@ -85,6 +96,8 @@ export interface TranscriptSource {
85
96
  streaming(): StreamingState | undefined;
86
97
  /** Resolve a registered tool definition by name, for Pi's tool-execution components. */
87
98
  getToolDefinition(name: string): ToolDefinition | undefined;
99
+ /** Current source state; unavailable retains the last readable messages. */
100
+ availability?(): TranscriptSourceAvailability;
88
101
  }
89
102
 
90
103
  /**
@@ -126,18 +139,17 @@ export function fileSnapshotSource(
126
139
  outputFile: string,
127
140
  readFile: (path: string) => string,
128
141
  ): TranscriptSource {
129
- const entries = parseSessionEntries(readFile(outputFile));
130
- const sessionEntries = entries.filter((entry): entry is SessionEntry => entry.type !== "session");
131
- const { messages } = buildSessionContext(sessionEntries);
142
+ const messages = parseSessionFileMessages(readFile(outputFile));
132
143
  return {
133
144
  getMessages: () => messages,
134
145
  subscribe: () => undefined,
135
146
  streaming: () => undefined,
136
147
  getToolDefinition: () => undefined,
148
+ availability: () => ({ kind: "file", path: outputFile }),
137
149
  };
138
150
  }
139
151
 
140
- /** Source a transcript live from an in-memory record (this slice's only source). */
152
+ /** Source a transcript live from an in-memory record. */
141
153
  export function liveSource(record: NavigableSubagent): TranscriptSource {
142
154
  return {
143
155
  getMessages: () => record.agentMessages,
@@ -147,6 +159,105 @@ export function liveSource(record: NavigableSubagent): TranscriptSource {
147
159
  ? { activeTools: record.activeTools, responseText: record.responseText }
148
160
  : undefined,
149
161
  getToolDefinition: (name) => record.getToolDefinition(name),
162
+ availability: () => ({ kind: "live", ...(record.outputFile ? { path: record.outputFile } : {}) }),
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Keep a live viewer useful across retention release. The source swaps to the
168
+ * persisted snapshot on the release notification and retains the last live
169
+ * content when that snapshot cannot be read.
170
+ */
171
+ export function liveFileSource(
172
+ record: NavigableSubagent,
173
+ readFile: (path: string) => string,
174
+ ): TranscriptSource {
175
+ let mode: "live" | "file" | "unavailable" = record.isSessionReady() ? "live" : "unavailable";
176
+ let messages: readonly SessionMessage[] = record.agentMessages;
177
+ let error: string | undefined;
178
+ let fileAttempted = false;
179
+
180
+ const swapIfReleased = (): void => {
181
+ if (record.isSessionReady()) {
182
+ mode = "live";
183
+ error = undefined;
184
+ messages = record.agentMessages;
185
+ return;
186
+ }
187
+ if (fileAttempted || !record.outputFile) {
188
+ if (!record.outputFile && mode === "live") mode = "unavailable";
189
+ return;
190
+ }
191
+ fileAttempted = true;
192
+ try {
193
+ messages = parseSessionFileMessages(readFile(record.outputFile));
194
+ mode = "file";
195
+ error = undefined;
196
+ } catch (cause) {
197
+ mode = "unavailable";
198
+ error = cause instanceof Error ? cause.message : String(cause);
199
+ }
200
+ };
201
+
202
+ const notify = (onChange: () => void): void => {
203
+ try {
204
+ // Do not replace the last live snapshot with Subagent's empty post-release
205
+ // getter before the file adapter succeeds; the degraded path must retain
206
+ // content that was already visible.
207
+ if (mode === "live" && record.isSessionReady()) messages = record.agentMessages;
208
+ swapIfReleased();
209
+ onChange();
210
+ } catch (cause) {
211
+ mode = "unavailable";
212
+ error = cause instanceof Error ? cause.message : String(cause);
213
+ onChange();
214
+ }
215
+ };
216
+
217
+ return {
218
+ getMessages: () => {
219
+ try {
220
+ if (mode === "live") {
221
+ if (!record.isSessionReady()) swapIfReleased();
222
+ else messages = record.agentMessages;
223
+ } else swapIfReleased();
224
+ } catch (cause) {
225
+ mode = "unavailable";
226
+ error = cause instanceof Error ? cause.message : String(cause);
227
+ }
228
+ return messages;
229
+ },
230
+ subscribe: (onChange) => {
231
+ const unsubscribers: Array<() => void> = [];
232
+ try {
233
+ const unsubscribe = record.subscribeToUpdates(() => notify(onChange));
234
+ if (unsubscribe) unsubscribers.push(unsubscribe);
235
+ } catch (cause) {
236
+ mode = "unavailable";
237
+ error = cause instanceof Error ? cause.message : String(cause);
238
+ }
239
+ if (record.subscribeToRecordUpdates) {
240
+ try {
241
+ unsubscribers.push(record.subscribeToRecordUpdates(() => notify(onChange)));
242
+ } catch (cause) {
243
+ mode = "unavailable";
244
+ error = cause instanceof Error ? cause.message : String(cause);
245
+ }
246
+ }
247
+ return unsubscribers.length === 0 ? undefined : () => {
248
+ for (const unsubscribe of unsubscribers) unsubscribe();
249
+ };
250
+ },
251
+ streaming: () => mode === "live" && record.status === "running"
252
+ ? { activeTools: record.activeTools, responseText: record.responseText }
253
+ : undefined,
254
+ getToolDefinition: (name) => mode === "live" ? record.getToolDefinition(name) : undefined,
255
+ availability: () => {
256
+ swapIfReleased();
257
+ if (mode === "live") return { kind: "live", ...(record.outputFile ? { path: record.outputFile } : {}) };
258
+ if (mode === "file" && record.outputFile) return { kind: "file", path: record.outputFile };
259
+ return { kind: "unavailable", ...(record.outputFile ? { path: record.outputFile } : {}), ...(error ? { error } : {}) };
260
+ },
150
261
  };
151
262
  }
152
263