shariq-pi-extensions 0.2.27 → 0.2.28

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.
@@ -44,7 +44,7 @@ State is stored in Pi session entries and reconstructed on resume, reload, and t
44
44
 
45
45
  The subagent extension runs flat Pi child agents with profiles, capability policies, continuation, result delivery, optional worktrees, pre-warmed task dispatch, instant cascading cancellation, cross-session persistence, and a dashboard. Configuration lives in `<agent-dir>/subagents.json`; trusted projects may override it through their Pi config directory. The configured concurrency ceiling is 50.
46
46
 
47
- The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. These lifecycle operations render as compact expandable main-chat cards; running children share the bounded Active work dock and completed counts no longer occupy footer space after settlement. All completed subagent snapshots and transcripts persist across Pi restarts (`<agent-dir>/subagents/runs/`), allowing `resume_from` to resume completed workers at any point. Interruption immediately cascades across all child fibers in `<10ms`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
47
+ The extension supplies tools including `spawn_agent`, `task`, `check_agent`, `list_agents`, `wait_agent`, `send_message`, `close_agent`, `reply_question`, and `apply_agent_changes`. These lifecycle operations render as compact expandable main-chat cards; only active manager entries contribute to the footer and bounded Active work dock, so persisted history never inflates live failure counts. Cancellation and reload interruption use a distinct `cancelled` state rather than `error`. All completed and cancelled production subagent snapshots and transcripts persist across Pi restarts (`<agent-dir>/subagents/runs/`), allowing `resume_from` to resume completed workers at any point. Interruption immediately cascades across all child fibers in `<10ms`. Child settlement stays in a private extension queue while the parent is active, then starts one custom-result turn at Pi's safe idle edge with the summary guaranteed in model context and never rendered as user-authored or follow-up input; status tools are for explicit inspection, not waiting.
48
48
 
49
49
  ### [Orchestration](../extensions/orchestration/README.md)
50
50
 
@@ -10,7 +10,7 @@ The system is deliberately flat. Only the main Pi thread can spawn subagents. Ch
10
10
 
11
11
  - **Pre-Warmed Pool Dispatch:** Pre-warms unique agent IDs and allocation buffers ahead of time, eliminating string-formatting and crypto overhead on the critical path for sub-millisecond task dispatch.
12
12
  - **Instant Cascading Cancellation:** Structured Effect-TS fiber supervision cascades immediate abort signals to all running subagents and child processes in `<10ms`, guaranteeing zero orphan processes upon interruption or parent turn cancellation.
13
- - **Cross-Session Snapshot Persistence:** Full subagent snapshots and transcripts are automatically persisted under `~/.pi/agent/subagents/runs/<id>/snapshot.json`, enabling discovery and resumption (`resume_from`) across Pi restarts.
13
+ - **Cross-Session Snapshot Persistence:** Full production subagent snapshots and transcripts are automatically persisted under `~/.pi/agent/subagents/runs/<id>/snapshot.json`, enabling discovery and resumption (`resume_from`) across Pi restarts. Test-only manager backends do not write into user state.
14
14
 
15
15
  ## Parent tools
16
16
 
@@ -70,7 +70,7 @@ Project configuration is ignored when the project is not trusted. Concurrency is
70
70
 
71
71
  New children start with independent context by default. `fork_turns` may be `all` or a positive number of recent user turns. Forking keeps user messages and final assistant text while removing thinking, tool calls, and tool results so the child never inherits an unresolved tool protocol.
72
72
 
73
- Every child uses a persistent Pi session file. `resume_from` continues a completed child with its full transcript, tool state, and logical agent id, including after a parent `/reload` or resume. Non-secret metadata is stored both in the parent session and in `~/.pi/agent/subagents/catalog.json`, so children created by an ephemeral or different parent process remain discoverable. Existing legacy ids remain valid catalog keys.
73
+ Every child uses a persistent Pi session file. `resume_from` continues a completed or cancelled child with its full transcript, tool state, and logical agent id, including after a parent `/reload` or resume. Cancellation and reload interruption are recorded as `cancelled`, not as failures; historical snapshots remain discoverable but only active manager entries contribute to the live footer and Active work dock. Non-secret metadata is stored both in the parent session and in `~/.pi/agent/subagents/catalog.json`, so children created by an ephemeral or different parent process remain discoverable. Existing legacy ids remain valid catalog keys.
74
74
 
75
75
  ## Worktree isolation
76
76
 
@@ -80,7 +80,7 @@ Children share the requested workspace by default. Use `isolation: "worktree"` o
80
80
 
81
81
  ## UI
82
82
 
83
- Subagent lifecycle tools render as compact main-chat cards with expandable detail. Running children appear in the shared bounded **Active work** dock with elapsed time and their current tool, then disappear when settled because the result card becomes the durable timeline record. `/subagents` or `/subagents agents` opens the live operations dashboard and takeover UI. `/btw <question>` starts a read-only, context-aware side investigation owned by the user; it opens directly in takeover view and records its answer without waking the parent model. Wide terminals use a split-pane agent list and selected-agent inspector with running/completed/failed counts, model/profile/access metadata, a context meter, current tool activity, queue state, elapsed time, turns, cwd, and latest output. Aborting a running child from the dashboard requires pressing `x` twice; Escape or navigation cancels an armed abort. The takeover view adds a live transcript, context meter, active-tool state, scrolling, child interruption, and follow-up input. `/subagents peers` shows the persistent peer-message audit trail. `/subagents profiles` browses profiles and personas, and `/subagents config` edits validated configuration.
83
+ Subagent lifecycle tools render as compact main-chat cards with expandable detail. Running children appear in the shared bounded **Active work** dock with elapsed time and their current tool, then disappear when settled because the result card becomes the durable timeline record. `/subagents` or `/subagents agents` opens the live operations dashboard and takeover UI. `/btw <question>` starts a read-only, context-aware side investigation owned by the user; it opens directly in takeover view and records its answer without waking the parent model. Wide terminals use a split-pane agent list and selected-agent inspector with running/completed/cancelled/failed counts, model/profile/access metadata, a context meter, current tool activity, queue state, elapsed time, turns, cwd, and latest output. Aborting a running child from the dashboard requires pressing `x` twice; Escape or navigation cancels an armed abort. The takeover view adds a live transcript, context meter, active-tool state, scrolling, child interruption, and follow-up input. `/subagents peers` shows the persistent peer-message audit trail. `/subagents profiles` browses profiles and personas, and `/subagents config` edits validated configuration.
84
84
 
85
85
  ## Architecture
86
86
 
@@ -512,7 +512,7 @@ export default function (pi: ExtensionAPI) {
512
512
 
513
513
  const updateStatus = (manager: SubagentManagerShape) => {
514
514
  if (!ui) return;
515
- const subs = manager.view.list();
515
+ const subs = manager.view.active();
516
516
  const runningSnapshots = subs.filter((snap) => snap.status === "running");
517
517
  const failed = subs.filter((snap) => snap.status === "error").length;
518
518
  if (activityContext) {
@@ -843,7 +843,7 @@ export default function (pi: ExtensionAPI) {
843
843
  const sections: string[] = [];
844
844
  let remainingBytes = WAIT_OUTPUT_MAX_BYTES;
845
845
  for (const snap of settled) {
846
- const verb = snap.status === "error" ? "failed" : "finished";
846
+ const verb = snap.status === "error" ? "failed" : snap.status === "cancelled" ? "was cancelled" : "finished";
847
847
  let section = `## ${snap.id} "${snap.title}" ${verb}`;
848
848
  if (snap.errorText) section += `\nError: ${snap.errorText}`;
849
849
  const headerBytes = Buffer.byteLength(section, "utf8") + 2;
@@ -996,7 +996,7 @@ export default function (pi: ExtensionAPI) {
996
996
  },
997
997
  renderResult(result, { expanded }, theme) {
998
998
  const details = result.details as { id?: string; status?: string; turns?: number } | undefined;
999
- const state = details?.status === "error" ? "error" : details?.status === "running" ? "active" : "success";
999
+ const state = details?.status === "error" ? "error" : details?.status === "running" ? "active" : details?.status === "cancelled" ? "muted" : "success";
1000
1000
  return new Text(toolResultCard(result, expanded, theme, state, details?.id ?? "subagent", `${details?.status ?? "unknown"} · ${details?.turns ?? 0} turns`), 0, 0);
1001
1001
  },
1002
1002
  });
@@ -1294,13 +1294,14 @@ export default function (pi: ExtensionAPI) {
1294
1294
  status?: string;
1295
1295
  };
1296
1296
  const failed = details.status === "error";
1297
- const icon = failed ? theme.fg("error", "x") : theme.fg("success", "■");
1297
+ const cancelled = details.status === "cancelled";
1298
+ const icon = failed ? theme.fg("error", "x") : cancelled ? theme.fg("muted", "■") : theme.fg("success", "■");
1298
1299
  const header =
1299
1300
  `${icon} ` +
1300
1301
  theme.fg("accent", theme.bold(`subagent ${details.id ?? "?"}`)) +
1301
1302
  theme.fg(
1302
1303
  "muted",
1303
- ` · ${details.title ?? ""} · ${failed ? "failed" : "finished"}`,
1304
+ ` · ${details.title ?? ""} · ${failed ? "failed" : cancelled ? "cancelled" : "finished"}`,
1304
1305
  );
1305
1306
 
1306
1307
  const content =
@@ -1340,10 +1341,11 @@ export default function (pi: ExtensionAPI) {
1340
1341
  const data = entry.data;
1341
1342
  if (!data) return new Text(theme.fg("warning", "By-the-way result unavailable"), 0, 0);
1342
1343
  const failed = data.status === "error";
1344
+ const cancelled = data.status === "cancelled";
1343
1345
  const header =
1344
- `${theme.fg(failed ? "error" : "success", "■")} ` +
1346
+ `${theme.fg(failed ? "error" : cancelled ? "muted" : "success", "■")} ` +
1345
1347
  theme.fg("accent", theme.bold(`by the way · ${data.title}`)) +
1346
- theme.fg("muted", ` · ${failed ? "failed" : "answered"} · ${data.id}`);
1348
+ theme.fg("muted", ` · ${failed ? "failed" : cancelled ? "cancelled" : "answered"} · ${data.id}`);
1347
1349
  const body = [data.errorText ? `Error: ${data.errorText}` : "", data.answer]
1348
1350
  .filter(Boolean)
1349
1351
  .join("\n\n");
@@ -48,7 +48,7 @@ function validRecord(value: unknown): value is ArchivedSubagent {
48
48
  typeof record.cwd === "string" &&
49
49
  typeof record.sessionFile === "string" &&
50
50
  typeof record.updatedAt === "number" &&
51
- (record.status === "running" || record.status === "done" || record.status === "error")
51
+ (record.status === "running" || record.status === "done" || record.status === "error" || record.status === "cancelled")
52
52
  );
53
53
  }
54
54
 
@@ -27,7 +27,7 @@ export const REASONING_EFFORTS = [
27
27
  ] as const;
28
28
  export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
29
29
 
30
- export type SubagentStatus = "running" | "done" | "error";
30
+ export type SubagentStatus = "running" | "done" | "error" | "cancelled";
31
31
 
32
32
  /** Parent-session context resolved by the tool layer and passed opaquely. */
33
33
  export interface PeerAgent {
@@ -24,7 +24,7 @@ import {
24
24
  } from "effect";
25
25
  import type { SubagentBackend, SubagentSession } from "./backend.ts";
26
26
  import { BackendRegistry } from "./backend.ts";
27
- import { loadPersistedSnapshots, saveSnapshot } from "./storage.ts";
27
+ import { loadPersistedSnapshots } from "./storage.ts";
28
28
  import type {
29
29
  BackendName,
30
30
  LiveToolState,
@@ -95,6 +95,7 @@ interface Entry {
95
95
  /** Synchronous bridge for the TUI. Snapshots are live objects; do not mutate. */
96
96
  export interface SubagentReadModel {
97
97
  list(): ReadonlyArray<SubagentSnapshot>;
98
+ active(): ReadonlyArray<SubagentSnapshot>;
98
99
  get(id: string): SubagentSnapshot | undefined;
99
100
  size(): number;
100
101
  /** Any-change notification (footer status, dashboard). */
@@ -329,7 +330,7 @@ const makeManager = Effect.gen(function* () {
329
330
  s.finalText = outcome.partialText ?? "";
330
331
  break;
331
332
  case "Interrupted":
332
- s.status = "error";
333
+ s.status = "cancelled";
333
334
  s.errorText = "Run was aborted";
334
335
  s.finalText = outcome.partialText ?? "";
335
336
  break;
@@ -339,7 +340,6 @@ const makeManager = Effect.gen(function* () {
339
340
  s.liveTools = [];
340
341
  s.queued = [];
341
342
  const consumed = (waitInterest.get(s.id) ?? 0) > 0;
342
- saveSnapshot(s as SubagentSnapshot);
343
343
  notify(s.id);
344
344
  try {
345
345
  // During teardown, don't queue results into a shutting-down session.
@@ -776,6 +776,7 @@ const makeManager = Effect.gen(function* () {
776
776
  );
777
777
  return [...active, ...historical];
778
778
  },
779
+ active: () => [...entries.values()].map((entry) => entry.snapshot),
779
780
  get: (id) => entries.get(id)?.snapshot ?? persistedSnapshots.get(id),
780
781
  size: () => entries.size,
781
782
  subscribe: (listener) => {
@@ -83,11 +83,11 @@ export const SUBAGENT_LIST_TOOL_DESCRIPTION =
83
83
  export function buildSubagentResultMessage(options: {
84
84
  id: string;
85
85
  title: string;
86
- status: "running" | "done" | "error";
86
+ status: "running" | "done" | "error" | "cancelled";
87
87
  errorText?: string;
88
88
  output: string;
89
89
  }) {
90
- const verb = options.status === "error" ? "failed" : "finished";
90
+ const verb = options.status === "error" ? "failed" : options.status === "cancelled" ? "was cancelled" : "finished";
91
91
  let text = `Pi subagent ${options.id} "${options.title}" ${verb}.`;
92
92
  if (options.errorText) text += `\nError: ${options.errorText}`;
93
93
  text += `\n\n${options.output}`;
@@ -26,6 +26,20 @@ export function saveSnapshot(snapshot: SubagentSnapshot) {
26
26
  }
27
27
  }
28
28
 
29
+ export function normalizePersistedSnapshot(snap: SubagentSnapshot): SubagentSnapshot {
30
+ if (snap.status === "running") {
31
+ return {
32
+ ...snap,
33
+ status: "cancelled",
34
+ errorText: "Pi exited or reloaded while this subagent was active.",
35
+ };
36
+ }
37
+ if (snap.status === "error" && /abort|cancel|interrupt|exited or reloaded/i.test(snap.errorText ?? "")) {
38
+ return { ...snap, status: "cancelled" };
39
+ }
40
+ return snap;
41
+ }
42
+
29
43
  export function loadPersistedSnapshots(): SubagentSnapshot[] {
30
44
  let names: string[] = [];
31
45
  try {
@@ -38,16 +52,9 @@ export function loadPersistedSnapshots(): SubagentSnapshot[] {
38
52
  try {
39
53
  const file = path.join(rootDir(), name, "snapshot.json");
40
54
  if (!fs.existsSync(file)) continue;
41
- let snap = JSON.parse(fs.readFileSync(file, "utf8")) as SubagentSnapshot;
42
- if (!snap.id || !snap.title) continue;
43
- if (snap.status === "running") {
44
- snap = {
45
- ...snap,
46
- status: "error",
47
- errorText: "Pi exited or reloaded while this subagent was active.",
48
- };
49
- }
50
- snapshots.push(snap);
55
+ const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as SubagentSnapshot;
56
+ if (!parsed.id || !parsed.title) continue;
57
+ snapshots.push(normalizePersistedSnapshot(parsed));
51
58
  } catch {
52
59
  // Best effort recovery
53
60
  }
@@ -41,6 +41,8 @@ function statusGlyph(snap: SubagentSnapshot, theme: Theme): string {
41
41
  return theme.fg("success", "■");
42
42
  case "error":
43
43
  return theme.fg("error", "■");
44
+ case "cancelled":
45
+ return theme.fg("muted", "■");
44
46
  }
45
47
  }
46
48
 
@@ -52,6 +54,8 @@ function statusWord(snap: SubagentSnapshot, theme: Theme): string {
52
54
  return theme.fg("success", "done");
53
55
  case "error":
54
56
  return theme.fg("error", "failed");
57
+ case "cancelled":
58
+ return theme.fg("muted", "cancelled");
55
59
  }
56
60
  }
57
61
 
@@ -314,6 +318,7 @@ export class SubagentDashboard implements Component {
314
318
  const running = subs.filter((snap) => snap.status === "running").length;
315
319
  const done = subs.filter((snap) => snap.status === "done").length;
316
320
  const failed = subs.filter((snap) => snap.status === "error").length;
321
+ const cancelled = subs.filter((snap) => snap.status === "cancelled").length;
317
322
  const selected = subs[this.selection.index];
318
323
 
319
324
  const headerLeft = theme.fg("accent", theme.bold("Subagent operations"));
@@ -321,6 +326,7 @@ export class SubagentDashboard implements Component {
321
326
  running > 0 ? stateLabel(theme, "warning", `${running} running`) : "",
322
327
  done > 0 ? stateLabel(theme, "success", `${done} done`) : "",
323
328
  failed > 0 ? stateLabel(theme, "error", `${failed} failed`) : "",
329
+ cancelled > 0 ? stateLabel(theme, "muted", `${cancelled} cancelled`) : "",
324
330
  ].filter(Boolean).join(theme.fg("dim", " · "));
325
331
  const lines = [joinSides(` ${headerLeft}`, `${counts || theme.fg("muted", "idle")} `, width)];
326
332
 
@@ -416,7 +422,7 @@ export class SubagentDashboard implements Component {
416
422
  height: number,
417
423
  ): string[] {
418
424
  const theme = this.theme;
419
- const state = snap.status === "running" ? "warning" : snap.status === "done" ? "success" : "error";
425
+ const state = snap.status === "running" ? "warning" : snap.status === "done" ? "success" : snap.status === "cancelled" ? "muted" : "error";
420
426
  const percent = contextPercent(snap.usage);
421
427
  const activeTools = snap.liveTools.filter((tool) => !tool.done);
422
428
  const lines: string[] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shariq-pi-extensions",
3
- "version": "0.2.27",
3
+ "version": "0.2.28",
4
4
  "description": "Cross-platform extension suite for the Pi coding agent.",
5
5
  "license": "MIT",
6
6
  "author": "Shariq Riaz",