@howarewoo/pi-subagents 0.2.1 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.1]
6
+
7
+ Requires Node 22.19+ and stable Pi 1.5.2 or newer, unchanged from 0.3.0. Tested host
8
+ remains Pi 1.5.2; newer stable hosts are allowed, not guaranteed compatible. No
9
+ configuration migration is required. Runtime behavior, permissions and lifecycle are
10
+ unchanged. Let active tasks finish before updating, then restart Pi; reload and shutdown
11
+ abort and drain active children rather than migrating them.
12
+
13
+ - Clarify the package description, add Pi/subagent discovery keywords, and lead the README with the supervised workflow, execution trade-offs, comparison links and a bounded read-only quick start.
14
+
15
+ - Shorten README selection guidance while preserving the comparison sources, feature coverage and execution boundaries.
16
+
17
+ - Add `pi-better-subagents` 0.32.4 to the README feature comparison with versioned sources and explicit unassessed entries. Explain when to choose this repo's permission hooks, exact model routing, validated batch results, Hub supervision and writer coordination, including sandbox, persistence and automation trade-offs. Add competitor-specific selection guidance for nicobailon, tintinweb and pi-better-subagents, identifying overlapping features and cases where each alternative is a better fit.
18
+
19
+ ## [0.3.0]
20
+
21
+ Requires Node 22.19+ and stable Pi 1.5.2 or newer, unchanged from 0.2.x. Tested host
22
+ remains Pi 1.5.2; newer stable hosts are allowed, not guaranteed compatible.
23
+
24
+ ### Breaking — completed-context resume and migration
25
+
26
+ - Resume of successfully completed work now continues the same full SDK conversation and tool history, with the same identity, generation and session count, rather than starting a fresh session. To start fresh work in a new generation, use explicit model-issued `delegate_coordinate` `followup` or `revive` and supply the context it needs; those operations do not replay the old conversation.
27
+ - Only the deadline window resets on resume; request/tool/spawn counters remain cumulative, with at most 64 execution windows per generation. Paused windows retain provisional structured submission state and publish no final outcome artifacts. Completed windows preserve immutable outcome/artifact evidence in `completedWindows`; later completed-window artifacts use `window-N/` without replacing earlier files. Resumed completed structured assignments require a new `submit_result`.
28
+ - Context restoration lasts only for the current parent runtime; saved transcripts are not reload/restart recovery state. Let active tasks drain before updating, then restart Pi. Reload/shutdown aborts and drains children rather than migrating them. Resume the exact parent generation first, then explicitly resume selected children; descendants never resume automatically. Existing authority, grants, authentication, capacity and writer checks still apply.
29
+
30
+ ### Changes
31
+
32
+ - Make the queued deadline/continuation lifecycle regression test deterministic with a controlled deadline clock, preserving pre-dispatch expiry and repeated continuation coverage while verifying shutdown disposes an actual retained SDK session. Runtime deadline policy is unchanged.
33
+
34
+ - Correct resume to continue paused or successfully completed conversations with the same identity, generation, session count and full SDK/tool history. Completed runtimes restore from the canonical SessionManager into the same transcript, never a result summary or silent fresh generation. Preserve cumulative budgets and earlier window artifacts/outcomes; revalidate current authority/auth/capacity, reject stopped/failed/retired/unavailable identities, and bound continuations to 64 windows. Explicit followup/revive behavior is unchanged.
35
+
36
+ - Preserve successfully completed descendants across ancestor completion for explicit parent-first same-context resume, without undoing explicit retirement. Validate retained post-handoff model availability on resume rather than requiring the obsolete admission model; keep lineage, grants and writer checks.
37
+
38
+ - Count individual subagent sessions, including retained historical generations, in the widget total using the same roster source as the unfiltered Hub. Distinct tasks sharing a profile and nested sessions each count separately; active widget rows remain bounded and scheduling/lifecycle behavior is unchanged.
39
+
5
40
  ## [0.2.1]
6
41
 
7
42
  Requires Node 22.19+ and stable Pi 1.5.2 or newer. Tested host remains Pi 1.5.2;
package/README.md CHANGED
@@ -1,9 +1,13 @@
1
- # @howarewoo/pi-subagents
1
+ # @howarewoo/pi-subagents — Pi subagent extension
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@howarewoo/pi-subagents)](https://www.npmjs.com/package/@howarewoo/pi-subagents)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/@howarewoo/pi-subagents)](https://www.npmjs.com/package/@howarewoo/pi-subagents)
5
5
 
6
- Delegate work to subagents in Pi, follow their progress, and steer or stop them from Subagent Hub. Subagents use your selected model by default, with their own assignments and explicit tool grants.
6
+ Delegate code exploration, reviews and implementation tasks in the [Pi coding agent](https://pi.dev). Follow each subagent's conversation and tool use, then steer or stop tasks from [Subagent Hub](#subagent-hub). Subagents use your selected model by default, with their own assignments and explicit tool grants.
7
+
8
+ Use this extension to supervise delegated work inside a running Pi session. Keep the parent running until tasks finish; writers serialize, and you manage any worktrees. See [why choose this repo](#why-choose-this-repo) and the [feature comparison](#feature-comparison) for the full trade-offs.
9
+
10
+ [Install](#installation) · [Quick start](#quick-start) · [Subagent Hub](#subagent-hub) · [Compare extensions](#feature-comparison) · [Documentation](#documentation)
7
11
 
8
12
  ![Terminal illustration of the pinned Subagents widget showing 48 tasks: 16 running and 32 queued, with a hidden nested-agent summary and an overflow link to Subagent Hub.](docs/images/subagent-widget.svg)
9
13
 
@@ -40,25 +44,29 @@ Restart Pi and run `/subagent-profiles` to check the installation.
40
44
  If you installed the former unscoped package, follow the
41
45
  [0.1.1 migration instructions](CHANGELOG.md#011) before installing the current package.
42
46
 
43
- ## Use delegation
47
+ ## Quick start
44
48
 
45
- Choose a model through Pi's `/login` and `/model` setup. Delegation uses that provider and may incur charges; it does not automatically switch to a cheaper model.
49
+ Start Pi in a project containing a `README.md` file. Choose a model through Pi's `/login` and `/model` setup. Delegation uses that provider and may incur charges; it does not automatically switch to a cheaper model.
46
50
 
47
51
  Ask Pi:
48
52
 
49
- > Use delegate_task with profile scout to locate the startup lifecycle code and report source locations. Run it in the foreground.
53
+ > Use delegate_task with profile scout to read README.md and summarize this project's purpose in three bullets. Run it in the foreground.
50
54
 
51
55
  The corresponding tool call is:
52
56
 
53
57
  ```json
54
58
  {
55
- "assignment": "Locate startup lifecycle code and report source locations",
59
+ "assignment": "Read README.md and summarize this project's purpose in three bullets",
56
60
  "profile": "scout",
57
61
  "background": false
58
62
  }
59
63
  ```
60
64
 
61
- Or submit a batch:
65
+ The foreground call returns the scout's summary to the parent. Open `/subagent-hub` to inspect its conversation and tool calls. This example needs only the `read` tool; it does not require enabling `grep`, `find`, `ls` or `bash`.
66
+
67
+ ## Use delegation
68
+
69
+ Submit a batch for parallel read-only work:
62
70
 
63
71
  ```json
64
72
  {
@@ -104,7 +112,7 @@ Pi discovers the native `subagent` tool at startup through its tool description
104
112
  - `worker` maps to the configured `implementation` profile; `scout`, `review` and `general` are also available.
105
113
  - Calls are async by default and wake the parent after drain. Set `async:false` to wait; use `tasks:[{agent,task,cwd?}]` for a batch.
106
114
  - Control a task with its returned `id` and `action:"status"`, `"steer"`, `"stop"` or `"resume"`. Steer/resume also require `message`.
107
- - Resume keeps a paused conversation in the same generation. Completed idle/parked work gets a fresh generation, so include needed prior conclusions.
115
+ - Resume continues a drained paused **or completed** conversation, preserving the same identity, generation, session count and full SDK conversation/tool history. Completed idle/parked agents restore their canonical session into a new SDK runtime using the same transcript, not a result summary. Missing restoration state, stopped/failed identities and retired descendants reject; there is no fresh-context fallback. Explicit `delegate_coordinate` followup/revive still start fresh work.
108
116
 
109
117
  The coordinator supplies an existing workspace and the complete handoff, including accessible skill/reference paths. The extension does not create or manage worktrees. This is the [supported orchestration interface](docs/delegation-contract.md#native-orchestration-compatibility), not compatibility with every competing workflow script or custom agent definition.
110
118
 
@@ -197,7 +205,7 @@ All bundled profiles may delegate to `scout` and `review` by default, within a t
197
205
  Default task limits are **30 minutes**, **100 model requests** and **100 tool calls**:
198
206
 
199
207
  - A deadline pauses the active subtree after tool/provider drain, releasing capacity and writer leases but retaining sessions and conversations.
200
- - Use `subagent` resume or root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` to continue the same identity and conversation. Only the deadline resets; request/tool/spawn counters remain cumulative.
208
+ - Use `subagent` resume or root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` to continue the same identity and conversation. Only the deadline resets; request/tool/spawn counters remain cumulative (at most 64 windows per generation). Completed windows retain earlier result/artifact evidence in `completedWindows`; a resumed completed structured assignment requires a new `submit_result`.
201
209
  - Resume the parent first, then explicitly selected children. Descendants never resume automatically.
202
210
  - Stop, shutdown and hard request/tool limits cancel and dispose. Unresponsive code must drain before pause or cancellation can release resources.
203
211
 
@@ -261,29 +269,94 @@ changelog entries on the next fresh launch after a version increase. See the
261
269
 
262
270
  ## Feature comparison
263
271
 
264
- Compared with two community alternatives using their documentation, checked on 2026-11-11. Tintinweb's messaging scope was additionally checked against source at revision `b72b414` (2026-11-12). This is a feature comparison, not a performance or reliability benchmark. UI cells name documented surfaces; they do not imply other features are absent.
265
-
266
- | Feature | @howarewoo/pi-subagents | [nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents) | [tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents) |
267
- | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
268
- | Background tasks | ✅ [Parent-bound by design; until exit or reload](#why-background-tasks-are-parent-bound) | ✅ Supported | ✅ Supported |
269
- | Nested delegation | ✅ [Supported](#why-nested-delegation-is-foreground-only) | ✅ Supported | ✅ Supported |
270
- | Task list / conversation | ✅ [Subagent Hub: split panes; full-width thread on narrow terminals](#why-the-ui-observes-one-native-registry) | ✅ Fleet inspector: roster and output pane | ✅ FleetView opens a separate conversation viewer |
271
- | Message / tool rendering | ✅ [Native Pi message and tool components](#why-the-ui-observes-one-native-registry) | ✅ Structured Markdown/tool transcripts | ✅ Markdown + session-registered tool renderers |
272
- | Bi-directional agent messaging | ✅ [Supported; parent/child and siblings](#why-agent-messaging-is-family-scoped) | ⚠️ [Child/supervisor requests and replies](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/workflows.md#supervisor-coordination-child-asks-parent) | ⚠️ [Parent-to-child steering + result return; no built-in child-initiated or sibling messaging](https://github.com/tintinweb/pi-subagents/blob/b72b414581d8aa9bbfb1d44c832aed0d0b0b9053/src/nested-tools.ts#L400-L430) |
273
- | Live follow / scrollback | ✅ Scroll up to pause; End to follow | ✅ Scroll away to pause; bottom resumes | ✅ Scroll up to pause; End to follow |
274
- | In-UI task controls | ✅ [Hub: steer, inbox, stop, wait, park](#why-controls-are-identity-pinned) | ✅ Inspector: steer/follow-up and stop async runs | ✅ Viewer: steer, stop, foreground-to-background handoff |
275
- | Tool / usage inspection | ✅ [Tools: sanitized arguments/results; Details: direct usage](#why-the-ui-observes-one-native-registry) | ✅ Expandable tool details + token counts | ✅ Expandable tool output + tokens; optional cost |
276
- | Schema-validated results | ✅ [Supported](#why-structured-results-require-explicit-submission) | ✅ Supported | ⚠️ Workflow agents only |
277
- | Managed Git worktrees | 🚫 [By design; caller-managed worktrees supported](#why-workspaces-are-caller-managed-and-writers-serialize) | ✅ Supported | ✅ Supported |
278
- | Workflow scripts | 🚫 [By design; caller-owned](#why-orchestration-stays-with-the-caller) | ✅ Supported | ✅ Supported |
279
- | Scheduled runs | 🚫 [By design; no scheduler](#why-orchestration-stays-with-the-caller) | ✅ Supported | ⚠️ Session-scoped agents only |
280
-
281
- Legend: ✅ Supported (stated design boundaries apply) · ⚠️ Supported with scope restrictions · 🚫 By design.
272
+ Compared with three community alternatives using their documentation. Nicobailon and tintinweb were checked on 2026-11-11; tintinweb's messaging scope was additionally checked against source at revision `b72b414` (2026-11-12). `pi-better-subagents` was checked against its published 0.32.4 documentation on 2026-11-13. This is a feature comparison, not a performance or reliability benchmark. UI cells name documented surfaces; they do not imply other features are absent.
273
+
274
+ | Feature | @howarewoo/pi-subagents | [nicobailon/pi-subagents](https://github.com/nicobailon/pi-subagents) | [tintinweb/pi-subagents](https://github.com/tintinweb/pi-subagents) | [pi-better-subagents](https://pi.dev/packages/pi-better-subagents) |
275
+ | ------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
276
+ | Background tasks | ✅ [Parent-bound by design; until exit or reload](#why-background-tasks-are-parent-bound) | ✅ Supported | ✅ Supported | ✅ Non-blocking `pi -p` child processes; completion callbacks |
277
+ | Nested delegation | ✅ [Supported](#why-nested-delegation-is-foreground-only) | ✅ Supported | ✅ Supported | ⚠️ Opt-in for unconfined children; disabled in sandbox |
278
+ | Task list / conversation | ✅ [Subagent Hub: split panes; full-width thread on narrow terminals](#why-the-ui-observes-one-native-registry) | ✅ Fleet inspector: roster and output pane | ✅ FleetView opens a separate conversation viewer | ✅ Background-work navigator; output/result tools |
279
+ | Message / tool rendering | ✅ [Native Pi message and tool components](#why-the-ui-observes-one-native-registry) | ✅ Structured Markdown/tool transcripts | ✅ Markdown + session-registered tool renderers | — Not assessed |
280
+ | Bi-directional agent messaging | ✅ [Supported; parent/child and siblings](#why-agent-messaging-is-family-scoped) | ⚠️ [Child/supervisor requests and replies](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/workflows.md#supervisor-coordination-child-asks-parent) | ⚠️ [Parent-to-child steering + result return; no built-in child-initiated or sibling messaging](https://github.com/tintinweb/pi-subagents/blob/b72b414581d8aa9bbfb1d44c832aed0d0b0b9053/src/nested-tools.ts#L400-L430) | ⚠️ Parent-to-child communication + result return; no blocking child-to-parent requests |
281
+ | Live follow / scrollback | ✅ Scroll up to pause; End to follow | ✅ Scroll away to pause; bottom resumes | ✅ Scroll up to pause; End to follow | — Not assessed |
282
+ | In-UI task controls | ✅ [Hub: steer, inbox, stop, wait, park](#why-controls-are-identity-pinned) | ✅ Inspector: steer/follow-up and stop async runs | ✅ Viewer: steer, stop, foreground-to-background handoff | ✅ Navigator stop controls |
283
+ | Tool / usage inspection | ✅ [Tools: sanitized arguments/results; Details: direct usage](#why-the-ui-observes-one-native-registry) | ✅ Expandable tool details + token counts | ✅ Expandable tool output + tokens; optional cost | ✅ Output/result tools expose optional cost and tool counts |
284
+ | Schema-validated results | ✅ [Supported](#why-structured-results-require-explicit-submission) | ✅ Supported | ⚠️ Workflow agents only | — No equivalent caller-supplied result schema documented |
285
+ | Managed Git worktrees | 🚫 [By design; caller-managed worktrees supported](#why-workspaces-are-caller-managed-and-writers-serialize) | ✅ Supported | ✅ Supported | ⚠️ Optional self-contained Git clone; not managed linked worktrees |
286
+ | Workflow scripts | 🚫 [By design; caller-owned](#why-orchestration-stays-with-the-caller) | ✅ Supported | ✅ Supported | — Not assessed |
287
+ | Scheduled runs | 🚫 [By design; no scheduler](#why-orchestration-stays-with-the-caller) | ✅ Supported | ⚠️ Session-scoped agents only | — Not assessed |
288
+
289
+ Legend: ✅ Supported (stated design boundaries apply) · ⚠️ Supported with scope restrictions · 🚫 By design · — Not established by the cited documentation or not assessed.
282
290
 
283
291
  🚫 marks a deliberate product boundary, not a missing implementation within the current
284
292
  contract. Linked explanations describe the trade-offs behind this repo's choices.
285
293
 
286
- Sources: [our contract](docs/delegation-contract.md); nicobailon's [README](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/README.md), [agents](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/agents.md), [workflows](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/workflows.md) and [observability](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/observability.md); tintinweb's [README](https://github.com/tintinweb/pi-subagents/blob/8df7fa7d8e8130151a8fbfa991a1d3fa6b443739/README.md) and [workflows](https://github.com/tintinweb/pi-subagents/blob/8df7fa7d8e8130151a8fbfa991a1d3fa6b443739/docs/workflows.md).
294
+ Sources: [our contract](docs/delegation-contract.md); nicobailon's [README](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/README.md), [agents](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/agents.md), [workflows](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/workflows.md) and [observability](https://github.com/nicobailon/pi-subagents/blob/c99503aed6d6b20d2397a1767f7d60730efe3963/docs/observability.md); tintinweb's [README](https://github.com/tintinweb/pi-subagents/blob/8df7fa7d8e8130151a8fbfa991a1d3fa6b443739/README.md) and [workflows](https://github.com/tintinweb/pi-subagents/blob/8df7fa7d8e8130151a8fbfa991a1d3fa6b443739/docs/workflows.md); pi-better-subagents' [usage guide (0.32.4)](https://cdn.jsdelivr.net/npm/pi-better-subagents@0.32.4/docs/usage.md) and [agent catalog (0.32.4)](https://cdn.jsdelivr.net/npm/pi-better-subagents@0.32.4/docs/agent-catalog.md).
295
+
296
+ ### Why choose this repo?
297
+
298
+ Choose this repo for a supervised workflow in an existing checkout:
299
+
300
+ - Child tools retain parent permission hooks. Profiles cap authority; trusted-project
301
+ settings and tasks can only narrow operator grants. Ambient extension/MCP runtimes
302
+ are not loaded, and extra leaf tools require operator review.
303
+ See [permissions and resources](#why-permissions-and-resources-are-explicit).
304
+ - Children use the parent's model and effort unless an exact operator-approved override
305
+ applies. Unavailable selections fail without fallback.
306
+ See [model routing](#why-model-routing-is-exact).
307
+ - `resultSchema` requires explicit, validated submission. Batches pass preflight before
308
+ any child starts, retain input order and report background outcomes after all siblings
309
+ drain. See [structured results](#why-structured-results-require-explicit-submission).
310
+ - Hub combines native Pi conversations, tool arguments/results, usage and identity-pinned
311
+ controls. Granted parent/child and sibling messaging stays bounded within the live
312
+ parent runtime. See [Hub](#subagent-hub) and [messaging](#why-agent-messaging-is-family-scoped).
313
+ - Writers serialize across worktrees, and coordinated parent model-tool mutations cannot
314
+ overlap a child writer. This reduces in-runtime conflicts at the cost of parallel
315
+ writing throughput. External editors and manual commands remain uncoordinated.
316
+ See [workspace ownership](#why-workspaces-are-caller-managed-and-writers-serialize).
317
+
318
+ #### Compared with nicobailon/pi-subagents
319
+
320
+ This repo keeps background assignments under one live parent responsible for grants,
321
+ admission, descendant drain and writer serialization. Nicobailon also runs foreground
322
+ children in-process, but uses a detached background runner. Both have fleet inspection
323
+ and schema-validated results.
324
+
325
+ Choose nicobailon for packaged review loops, council patterns, custom agents with
326
+ per-agent memory, managed worktrees, durable missions or scheduled runs. This repo
327
+ leaves workflows and workspace lifecycle to the caller, with no scheduler or restart
328
+ recovery.
329
+
330
+ #### Compared with tintinweb/pi-subagents
331
+
332
+ This repo supports granted parent/child and sibling messaging, plus schema-validated
333
+ results on ordinary assignments. Tintinweb's cited implementation has parent-to-child
334
+ steering and result return; its schema-validated results are documented for workflow
335
+ agents. This repo's Hub keeps the roster beside the conversation on wide terminals;
336
+ tintinweb's FleetView opens a separate viewer.
337
+
338
+ Choose tintinweb for Claude Code-style tool conventions, compatible JavaScript workflows,
339
+ persistent agent memory, session-scoped scheduling or managed worktrees that preserve
340
+ changes on a branch. Both support conversation continuation, but this repo retains it
341
+ only within the owning parent runtime; tintinweb documents persisted resume. Its worktree
342
+ isolation is not an OS sandbox.
343
+
344
+ #### Compared with pi-better-subagents
345
+
346
+ This repo supports foreground results or background completion, live family messaging
347
+ and explicitly submitted schema-validated results. `pi-better-subagents` uses independent,
348
+ non-blocking child processes with completion callbacks and no mid-task blocking
349
+ child-to-parent requests.
350
+
351
+ Choose `pi-better-subagents` for opt-in macOS/Linux sandboxing (off by default), saved
352
+ conversations another foreground session can continue, or optional sandbox-oriented Git
353
+ clones. This repo has no OS sandbox; changing `cwd` does not provide containment.
354
+ The other package also has an editable role/named-agent catalog and, with Background
355
+ Tasks loaded and its documented prerequisites met, workers that release slots while
356
+ waiting for long operations.
357
+
358
+ Recommendations apply to the cited versions, without a performance or reliability ranking.
359
+ Unassessed controls may exist in the alternatives.
287
360
 
288
361
  ## Architectural decisions
289
362
 
@@ -29,10 +29,10 @@ The model-only `subagent` tool is registered and described through Pi's public `
29
29
  - Launch: `{agent,task,cwd?,model?,limits?,async?}`. `agent` is required: `general`, `scout`, `review`, `implementation`, or explicit compatibility name `worker` for the configured `implementation` profile. Unknown/custom agent definitions are rejected, not silently mapped. Authority and prompts still resolve through the existing profiles; no ambient Markdown agent loading or role/model fallback is added.
30
30
  - Batch: `{tasks:[{agent,task,cwd?,model?,limits?}],cwd?,async?}`. Shared cwd is the default for items lacking one. Existing whole-batch preflight, FIFO limits, ordered outcomes and global writer serialization apply.
31
31
  - `async` defaults true; false waits for final results. Accepted calls return immediately with exact `id:"UUID@generation"`, `taskId` (original UUID), generation and normal snapshot fields/artifact references. Control handles never prefix-match or resolve to the latest generation. Native `delegate_task` retains its UUID+generation output and foreground default.
32
- - Read/control: `{action:"agents"}` or `"list"`; `{action:"status"|"stop",id}`; `{action:"steer"|"resume",id,message}`. Status can inspect a historical generation; mutations refuse stale handles. Resume continues a drained paused assignment in the same SDK conversation and generation. For completed idle/parked presence it still starts a fresh generation in the original workspace, requiring current narrowing checks; include needed prior conclusions for that fresh assignment. Native coordination remains available for richer inbox/wait/park behavior.
32
+ - Read/control: `{action:"agents"}` or `"list"`; `{action:"status"|"stop",id}`; `{action:"steer"|"resume",id,message}`. Status can inspect a historical generation; mutations refuse stale handles. Resume continues a drained paused or successfully completed (idle/parked) assignment in the same conversation and generation, without increasing session count. Completed runtime graphs are reconstructed from the retained canonical SessionManager, preserving system context and actual tool history in the same transcript; no previous-result-summary or fresh-context fallback is allowed. Stopped, failed, cancelled, retired or unavailable conversations reject without starting new work. Use explicit `delegate_coordinate` followup/revive for fresh work. Native coordination remains available for richer inbox/wait/park behavior.
33
33
  - Model overrides accept the native exact selection object or `provider/id[:effort]`. Omitted effort uses the parent's selected effort; overrides still require exact operator grants, availability/auth/scope and supported effort. Bare model IDs and unknown providers are not guessed. Omitting model retains ordinary parent/profile routing.
34
34
 
35
- An async compatibility launch sends one window-outcome custom message per single/batch call after actual drain (paused or completed), using public `sendMessage(...,{triggerTurn:true,deliverAs:"followUp"})` to wake the parent. Compatibility resume does the same once for its new execution window or fresh completed-agent generation. This may cause a parent model turn and provider charges; no polling or detached worker survives parent shutdown. The original native tool's notification-only behavior is unchanged. Neither a notification nor a receipt proves the model understood the outcome.
35
+ An async compatibility launch sends one window-outcome custom message per single/batch call after actual drain (paused or completed), using public `sendMessage(...,{triggerTurn:true,deliverAs:"followUp"})` to wake the parent. Compatibility resume does the same once for its new execution window; rejected resumes send no completion notification. This may cause a parent model turn and provider charges; no polling or detached worker survives parent shutdown. The original native tool's notification-only behavior is unchanged. Neither a notification nor a receipt proves the model understood the outcome.
36
36
 
37
37
  The coordinator owns workspace allocation, exclusive writer handoff, Git/PR publication and retirement under the active workflow. Supply a complete task and accessible skill/reference paths: general inherits available skills and restricted reviewed leaf capabilities as specified below; workers never inherit parent instructions/history or context files. No scripts/workflow engine, supervisor RPC, automatic worktree lifecycle, external CLI worker, retained-run migration or blanket competitor API parity is promised.
38
38
 
@@ -158,11 +158,11 @@ A parent waiting in its child-local `delegate_task` retains its permit. Nested b
158
158
 
159
159
  Deadlines pause rather than cancel. `queued|setup|running → pausing → paused` stops new dispatch, aborts SDK work and waits for actual subtree drain before releasing capacity and writer leases. Snapshots expose `pauseReason` (`deadline` or `ancestor deadline`), `retainedContext`, `window`, cumulative request/tool/spawn counters, and unfinalized structured submission state. Paused is neither success nor failure; no final outcome artifacts are published. A queued assignment can pause before any session/transcript exists.
160
160
 
161
- Root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` or `subagent` `{action:"resume",id:"UUID@generation",message:"Continue…"}` appends that message to the retained SDK conversation. UUID, generation, assignment, workspace, selected model/prewalk state, role instructions, all prior SDK messages/tool calls/results and structured submission state remain intact. Each explicit resume starts a fresh configured deadline window (still bounded by originally admitted and current limits). Cumulative requests/tools/spawns are **not** replenished. A pre-session queued pause starts the original assignment followed by the continuation when dispatched. No automatic replay, undo, process freezing, restart or cross-parent recovery is provided.
161
+ Root-only `delegate_coordinate` `{action:"resume",target:{id,generation},assignment:"Continue…"}` or `subagent` `{action:"resume",id:"UUID@generation",message:"Continue…"}` appends that message to the retained SDK conversation. UUID, generation, assignment, workspace, selected model/prewalk state, role instructions, all prior SDK messages/tool calls/results remain intact. A paused window retains its provisional structured submission state. Completed windows retain immutable result/outcome/artifact references in `completedWindows`; their existing files are never replaced. A resumed completed structured assignment requires a new submission (the previous validated value is evidence, not the new window's result). Later completed-window artifacts are published exclusively under `window-N/` inside the same task directory. Each explicit resume starts a fresh configured deadline window (still bounded by originally admitted and current limits). Cumulative requests/tools/spawns are **not** replenished. Each generation permits at most 64 execution windows, bounding completion evidence and notifications. A pre-session queued pause starts the original assignment followed by the continuation when dispatched. After a completed prewalk handoff, resume validates the retained current model and current/admitted lineage grants, not the obsolete admission model; it neither broadens model grants nor rearms prewalk. No automatic replay, undo, process freezing, restart or cross-parent recovery is provided.
162
162
 
163
- An ancestor deadline marks its entire active subtree before draining any member. Resume the exact original parent generation first, then explicitly resume selected children; resuming a parent does not resume descendants. Existing no-permit-lending, writer ancestry and admission rules still apply. Parent completion/stop retires its children, including paused sessions.
163
+ An ancestor deadline marks its entire active subtree before draining any member. Resume the exact original parent generation first, then explicitly resume selected children; resuming a parent does not resume descendants. Existing no-permit-lending, writer ancestry and admission rules still apply. Parent completion retires active/paused children, but preserves successfully completed retained child conversations for later explicit resume once that exact parent generation is running again. Explicit stop/retirement remains effective even across parent resume; failed/cancelled children cannot resume.
164
164
 
165
- Resume rechecks operator/trusted-project limits and grants, ancestor spawn/capability ceilings, model availability/authentication, callable tools/provenance and writer/capacity admission. It never broadens original admission. A revoked retained grant rejects rather than rebuilding the SDK tool/resource graph; restore the grant or stop and launch a newly authorized identity. Policy/auth/capacity failure leaves the paused conversation intact with an actionable tool error; there is no fallback model or replacement agent. Concurrent duplicate resumes reject atomically. Stop/shutdown wins a concurrent pending resume and disposes retained sessions.
165
+ Resume rechecks operator/trusted-project limits and grants, ancestor spawn/capability ceilings, model availability/authentication, callable tools/provenance and writer/capacity admission. It never broadens original admission. A revoked retained grant rejects rather than silently changing the admitted SDK tool/resource graph; restore the grant or stop and launch a newly authorized identity. Policy/auth/capacity/restoration failure leaves the paused or completed conversation intact with an actionable tool error; there is no fallback model or replacement agent. Concurrent duplicate resumes reject atomically. Stop/shutdown wins a concurrent pending resume and disposes retained sessions.
166
166
 
167
167
  Requests/tools remain hard cancellation guards. The first committed hard-limit reason (`requests` or `tools`) is retained and produces final `status:"cancelled"`, `limitReason`, and explanatory `error`. Stop before setup cancels without SDK setup/transcript. Request guards check **before forwarding** to the parent's public registry. Counts include failed dispatches but not rejected over-limit dispatches, auth preflight or provider-internal retries/subrequests. Tool guards count entered, granted custom-tool executions, including permission-denied forwarded calls; malformed/unknown calls rejected by Pi before execution are not counted. Requests still bound loops of rejected tools.
168
168
 
@@ -212,7 +212,7 @@ Operator `coordination` grants selected `message`, `inbox`, `wait` actions; abse
212
212
  | `status` | Root-only pinned `target`; returns `{snapshot,generations,agents}`. Snapshot/history describe assignments; agents describes current presence. Full detached details, bounded text preview. |
213
213
  | `stop` | Root-only pinned current `target`; closes idle/parked presence or aborts/drains active subtree. Completed outcomes/files are unchanged. Stopped identities cannot revive. |
214
214
  | `park` | Root-only current disposed idle target; records parked presence. Active parking rejects: stop active work explicitly. No live SDK session, shell, permit or writer lease is retained while idle or parked. |
215
- | `resume` | Root-only drained paused `target` and nonblank `assignment` continuation. Returns the same identity/window snapshot and sends one notification after that resumed window drains. No schema replacement or descendant auto-resume. |
215
+ | `resume` | Root-only drained paused or successfully completed `target` and nonblank `assignment` continuation. Returns the same identity/window snapshot and sends one notification after that resumed window drains. No schema replacement or descendant auto-resume. |
216
216
  | `followup` / `revive` | Root-only current target and nonblank `assignment`, optional supported `resultSchema`. Followup requires idle; revive requires parked. Returns the new admitted snapshot, not final success. Use wait/status for completion; no unsolicited duplicate generation-completion message is added. |
217
217
 
218
218
  A message returns `{id,sender,recipient,text,mode,status,error?}`. `accepted` means mailbox admission, `queued` means the public SDK accepted a steer/followUp, `handled` means an SDK input handler consumed it, `delivered` means the exact message entered the recipient transcript or an inbox tool/wait returned it. None means model acknowledgment, understanding or assignment completion. `not-running` means no running recipient (including an undelivered receipt whose recipient ended), and `error` preserves delivery errors. Unknown/foreign/stale identities or receipts reject visibly, not as apparent successful delivery. Inbox messages are not automatically steered; steer/followUp are also retained in the bounded mailbox. Their `delivered` state comes from actual SDK user-message events, not queue acceptance. Message text is literal prefixed data, not tool authority.
@@ -227,7 +227,7 @@ Followup/revive re-resolve operator/trusted-project policy and selected root mod
227
227
 
228
228
  The UUID stays stable and generation increments atomically **only after** synchronous admission checks. Concurrent renewals race on the original reference; exactly one can replace it. A newly owned generation directory `<id>-g<N>` holds its SDK session, `agent/`, transcript and exclusive artifacts. Generation 1 keeps `<id>/`. For these completed-agent fresh generations, no old files are opened for continuation, consulted, replayed, appended or migrated. The new prompt contains only the new explicit assignment plus current role/resources and identity; provide any relevant prior conclusions explicitly. This deliberately avoids retained-conversation replay/tool execution and does not claim a warm resumed Pi session.
229
229
 
230
- Descendant renewal requires its **exact original** `parentId+parentGeneration` still active, non-cancelling and authorized. Current ancestor spawn roles, depth/limits, remaining spawn count, immediate nested capacity and writer ancestry all revalidate before generation mutation/setup. Replaced/disposed/stopped ancestors reject; the caller must delegate a new identity through an authorized live parent, never reparent the old one. Parent completion/stop closes descendant idle/parked presence too. Old callbacks, steering, stops, submissions, message delivery and waits capture the originating entry/reference, never retarget by latest UUID. Wait outcomes remain pinned even if a later generation is admitted. Shutdown/reload closes all presence and waiters, drains actual resources and suppresses old-runtime final messages; no cross-parent/process revival exists.
230
+ Descendant renewal requires its **exact original** `parentId+parentGeneration` still active, non-cancelling and authorized. Current ancestor spawn roles, depth/limits, remaining spawn count, immediate nested capacity and writer ancestry all revalidate before generation mutation/setup. Replaced/disposed/stopped ancestors reject; the caller must delegate a new identity through an authorized live parent, never reparent the old one. Explicit parent stop closes descendant idle/parked presence too. Successful parent completion does not retire already-successful retained descendant conversations; their explicit resume still requires the exact original active parent generation. Old callbacks, steering, stops, submissions, message delivery and waits capture the originating entry/reference, never retarget by latest UUID. Wait outcomes remain pinned even if a later generation is admitted. Shutdown/reload closes all presence and waiters, drains actual resources and suppresses old-runtime final messages; no cross-parent/process revival exists.
231
231
 
232
232
  ## Writer coordination and unsupported shell boundary
233
233
 
@@ -256,7 +256,7 @@ Hub names are snapshot-derived assignment headlines plus role, not a naming regi
256
256
  | Outcome wait | Hub `w` → native event wait with explicit 1000ms bound; native tool supports 1–60000ms outcome/inbox/receipt waits with existing deadlock/refusal rules. |
257
257
  | Stop subtree | Hub `x` → pinned target → `y` confirmation → existing stop/drain, retaining capacity/writer until disposal. Queued/setup/running/paused UI targets supported; historical/pausing/draining/disposed UI targets refuse, including transitions during pinned input. `/subagent-stop` and native root stop retain existing semantics. |
258
258
  | Park | Hub `p` or native root tool; only disposed idle current identity with valid original parent. No warm SDK retained. |
259
- | Followup/revive | **Native model-issued `delegate_coordinate` or compatibility `subagent` resume**. Paused assignments continue the retained conversation; idle/parked assignments use fresh session/generation/current narrowed authority. Both require the exact active original parent for descendants. Hub command context lacks `tools/executeTool`: it cannot safely start a generation; f/r only explain refusal, allocate nothing and dispatch nothing. |
259
+ | Followup/revive | **Native model-issued `delegate_coordinate` followup/revive only**. Idle/parked assignments use fresh session/generation/current narrowed authority. `subagent` resume continues paused or successfully completed conversations with full restored context and unchanged identity/generation/session count. Both require the exact active original parent for descendants. Hub command context lacks `tools/executeTool`: it cannot safely start a generation; f/r only explain refusal, allocate nothing and dispatch nothing. |
260
260
  | Child coordination/spawn/model authority | Coordination/model authority remains operator default-off; spawn defaults to scout/review only. All remain project/request/ancestor subset-only; the UI cannot mint grants or impersonate a child. |
261
261
  | Manual !/!!, external editors/terminals, arbitrary extension JS | Uncoordinated; model-tool-only writer boundary, never a shell ban/wrapper/sandbox. |
262
262
  | Supplied workspace / production isolation | Explicit path/shell-cwd adaptation through root hooks is supported for existing supplied workspaces. Filesystem sandboxing, managed worktrees, copying/apply and cleanup remain unimplemented; cwd is not a resource grant. |
@@ -28,7 +28,7 @@ Startup guidance, examples and compatibility tests describe native Pi delegation
28
28
 
29
29
  The approved follow-up adds startup `subagent` discovery/guidance, common launch/control calls, supplied-workspace execution and native completion wake-ups. It does not import another extension or promise full API/configuration parity. Source/test manifest digest: `41d44bc852b2c6b60a61112232992a704d13a0e590abd5ac152a5f7b8f5d470c`.
30
30
 
31
- **Controlled SDK/CLI integration:** the default suite passes 145/145 with zero failures, cancellations or skips. Typecheck, SDK exercise, 19-file package dry run and diff checks pass. The actual isolated Pi CLI exposes `subagent` at ordinary startup without an enable call and completes two scouts without activating extra builtins. Real SDK children read/edit/write/run bash in a throwaway Git worktree containing spaces and an apostrophe; root hooks see the actual adapted arguments and retain their real parent cwd. Tests cover root tool/write refusal, failed-cd no-fallback behavior, global writer queue/parent-write exclusion, atomic invalid-cwd admission, search defaults/shorthand refusal, parent-policy retention, task-file cwd, explicitly granted nested workspace inheritance, exact model selection, idle-parent wake once, fresh-generation resume/workspace retention and stale-control refusal. The first focused attempt used obsolete edit arguments and allowed the controlled parent to replay worker steps; the fixture now uses the current `edits[]` schema and acknowledges parent results. No assertions, runner concurrency or timeouts were weakened.
31
+ **Controlled SDK/CLI integration:** the default suite passes 145/145 with zero failures, cancellations or skips. Typecheck, SDK exercise, 19-file package dry run and diff checks pass. The actual isolated Pi CLI exposes `subagent` at ordinary startup without an enable call and completes two scouts without activating extra builtins. Real SDK children read/edit/write/run bash in a throwaway Git worktree containing spaces and an apostrophe; root hooks see the actual adapted arguments and retain their real parent cwd. Tests cover root tool/write refusal, failed-cd no-fallback behavior, global writer queue/parent-write exclusion, atomic invalid-cwd admission, search defaults/shorthand refusal, parent-policy retention, task-file cwd, explicitly granted nested workspace inheritance, exact model selection, idle-parent wake once, completed-agent context restoration without fresh generations/sessions, paused resume identity/context retention and stale-control refusal. The first focused attempt used obsolete edit arguments and allowed the controlled parent to replay worker steps; the fixture now uses the current `edits[]` schema and acknowledges parent results. No assertions, runner concurrency or timeouts were weakened.
32
32
 
33
33
  **Unrun:** live-model automatic tool selection and complete multi-task delivery with disposable remote PR/stack creation. This run authorized neither live inference nor a separate smoke repository's external writes. Controlled SDK/CLI results do not prove a model automatically chooses the extension or follows a complete external workflow. No installed-package cutover, active-session reload, retained-run migration or new human visual acceptance was performed. Replacement use requires only one loaded `subagent` provider; Pi's first-registration behavior is not a provider-priority mechanism.
34
34
 
@@ -428,3 +428,7 @@ The new redacted ANSI and cursor-separated text captures are retained outside th
428
428
  Reviewed only this task's changes before final checks. Removed redundant shutdown waits/nested cleanup scaffolding by letting the existing cancellation method own the startup barrier, preserving `finally` disposal. Kept one child, one observable snapshot and one observer subscription; no scheduler, generalized verifier, recovery service or duplicate terminal renderer. Removed an arbitrary finite scripted-response queue in favor of reusing Pi's response factory for each actual request. The final checks cover these changes. For the UI repair, kept rendering stateless (theme recalculated each render), one shared status/activity presentation and native width helpers; no new timers, retained command UI context, scheduler or terminal renderer. The widget subscription is removed before shutdown/disposal so stale children cannot repaint a replacement session.
429
429
 
430
430
  For Slice 0, the scoped pass retained one native color conversion per render and reused the existing row-paint boundary; no color cache, theme classifier, palette configuration or verifier framework was added. Simplified the real-theme regression's per-row comparison before the final full-suite checks.
431
+
432
+ ### Same-conversation completed resume correction
433
+
434
+ Real SDK tests in `completed-resume.test.ts` and `compatibility.test.ts` exercise completed idle/parked continuation with original system messages, assistant tool calls and actual tool results, a stable transcript/identity/generation/session count, repeat completions, cumulative exhaustion, immutable earlier artifacts, and per-window structured submissions. Fault-injected unavailable canonical state and revoked policy reject without snapshot/model/event mutation; stop wins pending resume and cancellation drains the writer. Existing deadline-pause/subtree tests continue to exercise retained paused context. Restoration uses the supported `createAgentSession({sessionManager})` API, not direct agent message assignment or result summaries. Parent-lifetime only; no process-restart recovery is claimed.
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@howarewoo/pi-subagents",
3
- "version": "0.2.1",
4
- "description": "Pi-native single-assignment delegation with explicit profiles and parent-owned SDK children",
3
+ "version": "0.3.1",
4
+ "description": "Pi subagent extension for supervised coding tasks with live monitoring, steering, and explicit tool permissions.",
5
5
  "keywords": [
6
- "pi-package"
6
+ "agent-delegation",
7
+ "multi-agent",
8
+ "pi-coding-agent",
9
+ "pi-extension",
10
+ "pi-package",
11
+ "subagent",
12
+ "subagents"
7
13
  ],
8
14
  "homepage": "https://github.com/howarewoo/pi-subagents#readme",
9
15
  "bugs": {
@@ -34,6 +34,9 @@ export type ExecutionRecord = {
34
34
  timer?: ReturnType<typeof setTimeout>;
35
35
  run?: Promise<void>;
36
36
  session?: AgentSession;
37
+ /** Canonical entry tree survives runtime disposal; never substitute a result summary. */
38
+ sessionManager?: SessionManager;
39
+ resourcePrompt?: string;
37
40
  unsubscribe?: () => void;
38
41
  paused?: boolean;
39
42
  prompt?: (text: string) => Promise<void>;
@@ -89,11 +92,15 @@ export async function createChildExecution(
89
92
  ? entry.state.id
90
93
  : `${entry.state.id}-g${entry.state.generation}`,
91
94
  );
92
- await mkdir(taskDir, { mode: 0o700 }); // exclusive ownership: never reuse an existing task record
95
+ const restoring = !!entry.sessionManager;
96
+ if (!restoring) await mkdir(taskDir, { mode: 0o700 }); // exclusive initial ownership
93
97
  const agentDir = join(taskDir, "agent");
94
- await mkdir(agentDir, { mode: 0o700 });
98
+ if (!restoring) await mkdir(agentDir, { mode: 0o700 });
95
99
  const transcriptDir = join(taskDir, "transcripts");
96
- await mkdir(transcriptDir, { mode: 0o700 });
100
+ if (!restoring) await mkdir(transcriptDir, { mode: 0o700 });
101
+ // Each completed window publishes exclusively, leaving all earlier artifacts intact.
102
+ const outcomeDir = restoring ? join(taskDir, `window-${entry.state.window}`) : taskDir;
103
+ if (restoring) await mkdir(outcomeDir, { mode: 0o700 });
97
104
  const target =
98
105
  entry.state.prewalk?.status === "armed"
99
106
  ? validateModelSelection(input.context, entry.state.prewalk.target)
@@ -116,14 +123,15 @@ export async function createChildExecution(
116
123
  target ? [target] : [],
117
124
  );
118
125
  const resources = childResources(
119
- input.profile.prompt +
126
+ (entry.resourcePrompt ??=
127
+ input.profile.prompt +
120
128
  (target
121
129
  ? "\nPrewalk: investigate and plan using only granted context/tools. After the first completed edit or write the same conversation switches once to the explicitly granted implementation target; all normal budgets still apply."
122
130
  : "") +
123
131
  `\nYour delegation identity is ${entry.state.id} generation ${entry.state.generation}; parent ${entry.state.parentId}. Coordination, if granted, is limited to family identities explicitly supplied to you.` +
124
132
  (entry.validator
125
133
  ? "\nThis assignment requires a structured result. Call submit_result with the actual value; final prose is not a submission."
126
- : ""),
134
+ : "")),
127
135
  input.skills,
128
136
  );
129
137
  const loader = new DefaultResourceLoader({
@@ -316,9 +324,10 @@ export async function createChildExecution(
316
324
  compaction: { enabled: false },
317
325
  retry: { enabled: false },
318
326
  }),
319
- sessionManager: SessionManager.create(selection.cwd, transcriptDir),
327
+ sessionManager: entry.sessionManager ?? SessionManager.create(selection.cwd, transcriptDir),
320
328
  });
321
329
  entry.session = session;
330
+ entry.sessionManager = session.sessionManager;
322
331
  if (session.thinkingLevel !== selection.thinkingLevel)
323
332
  throw new Error("SDK did not preserve selected effort");
324
333
  if (
@@ -389,7 +398,7 @@ export async function createChildExecution(
389
398
  const artifacts: NonNullable<ChildSnapshot["artifacts"]> = {};
390
399
  try {
391
400
  artifacts.text = await saveOutcomeArtifact(
392
- taskDir,
401
+ outcomeDir,
393
402
  "result.txt",
394
403
  last?.content
395
404
  .filter((block) => block.type === "text")
@@ -398,7 +407,7 @@ export async function createChildExecution(
398
407
  );
399
408
  if (entry.state.outcome?.validation === "submitted")
400
409
  artifacts.structured = await saveOutcomeArtifact(
401
- taskDir,
410
+ outcomeDir,
402
411
  "structured.json",
403
412
  JSON.stringify(entry.state.outcome.value),
404
413
  );
@@ -1,6 +1,7 @@
1
1
  import { requireSupportedHost } from "./supported-host.ts";
2
2
  import { leafDenial, toolProvenance, type CapabilityDenial } from "./capabilities.ts";
3
3
  import { randomUUID } from "node:crypto";
4
+ import { readFileSync } from "node:fs";
4
5
  import {
5
6
  SessionManager,
6
7
  type AgentSession,
@@ -110,12 +111,14 @@ export class DelegationCoordinator {
110
111
  presence: this.presence.get(state.id),
111
112
  }));
112
113
  }
113
- generations(id: string): ChildSnapshot[] {
114
+ /** Full parent-lifetime session roster, including retained historical generations. */
115
+ sessions(): ChildSnapshot[] {
114
116
  return [...this.history.values()]
115
- .map((record) => record.state)
116
- .concat(this.list())
117
- .filter((state) => state.id === id)
118
- .map((state) => structuredClone(state));
117
+ .map((record) => structuredClone(record.state))
118
+ .concat(this.list());
119
+ }
120
+ generations(id: string): ChildSnapshot[] {
121
+ return this.sessions().filter((state) => state.id === id);
119
122
  }
120
123
  private listeners = new Set<(event: RegistryEvent) => void>();
121
124
  private parentMutations = new Set<string>();
@@ -224,6 +227,15 @@ export class DelegationCoordinator {
224
227
  throw new Error("Prewalk target requires an exact model grant");
225
228
  validateModelSelection(selection, input.profile.prewalk);
226
229
  }
230
+ this.validateAdmission(input, effective, input.model);
231
+ return selection;
232
+ }
233
+ /** Shared writer/lineage checks; resume supplies its retained selection, not admission defaults. */
234
+ private validateAdmission(
235
+ input: StartTask,
236
+ effective: ModelSelection,
237
+ explicit?: ModelSelection,
238
+ ): void {
227
239
  const writer = this.writer ? this.entries.get(this.writer) : undefined;
228
240
  if (
229
241
  isWriter(input.profile) &&
@@ -234,7 +246,7 @@ export class DelegationCoordinator {
234
246
  const parent = this.entry(input.parent);
235
247
  if (parent.cancelled || parent.paused || parent.state.disposed)
236
248
  throw new Error("Delegating parent is no longer active");
237
- this.assertDescendantModels(parent, input.profile, effective, input.model);
249
+ this.assertDescendantModels(parent, input.profile, effective, explicit);
238
250
  if (!parent.input.profile.spawn?.includes(input.profile.name))
239
251
  throw new Error("Ancestor spawn role denied");
240
252
  if (parent.state.depth >= Math.min(this.policy.depth, parent.state.limits.depth))
@@ -248,7 +260,6 @@ export class DelegationCoordinator {
248
260
  ancestor = this.entries.get(ancestor.state.parentId);
249
261
  }
250
262
  }
251
- return selection;
252
263
  }
253
264
  private assertDescendantModels(
254
265
  parent: Entry,
@@ -721,15 +732,56 @@ export class DelegationCoordinator {
721
732
  async resume(ref: ChildRef, text: string): Promise<ChildRef> {
722
733
  requireSupportedHost();
723
734
  const entry = this.entry(ref);
724
- if (entry.resuming || entry.state.status !== "paused" || entry.cancelled || this.closing)
725
- throw new Error("Resume requires a drained paused assignment; duplicate resume rejected");
735
+ const completed = entry.state.status === "settled" && entry.state.disposed;
736
+ const eligible = entry.state.status === "paused" || completed;
737
+ if (
738
+ entry.resuming ||
739
+ !eligible ||
740
+ entry.cancelled ||
741
+ this.closing ||
742
+ this.presence.get(ref.id) === "stopped"
743
+ )
744
+ throw new Error(
745
+ "Resume requires a drained paused or completed conversation; stopped/failed identities and duplicate resume rejected",
746
+ );
747
+ if ((entry.state.window ?? 1) >= MAX_GENERATIONS)
748
+ throw new Error("Resume window limit (64) reached");
749
+ if (
750
+ completed &&
751
+ (!entry.sessionManager ||
752
+ !entry.finalizedMessages?.length ||
753
+ JSON.stringify(entry.sessionManager.buildSessionContext().messages) !==
754
+ JSON.stringify(entry.finalizedMessages))
755
+ )
756
+ throw new Error("Resume unavailable: canonical conversation state was not retained");
757
+ if (completed) {
758
+ // SessionManager.open() can silently initialize missing/empty files. Never use that
759
+ // fallback here: the retained tree AND its original persisted transcript must agree.
760
+ try {
761
+ const persisted = readFileSync(entry.state.transcript!, "utf8")
762
+ .trim()
763
+ .split("\n")
764
+ .map((line) => JSON.parse(line));
765
+ const manager = entry.sessionManager!;
766
+ if (
767
+ JSON.stringify(persisted) !==
768
+ JSON.stringify([manager.getHeader(), ...manager.getEntries()])
769
+ )
770
+ throw new Error("transcript differs from retained canonical entries");
771
+ } catch (error) {
772
+ throw new Error(
773
+ `Resume unavailable: original transcript cannot be restored: ${String(error)}`,
774
+ );
775
+ }
776
+ } else if (entry.prompted && !entry.session) {
777
+ throw new Error("Resume unavailable: paused SDK conversation was not retained");
778
+ }
726
779
  if (!text.trim()) throw new Error("Nonempty continuation text is required");
727
780
  entry.resuming = true;
728
781
  try {
729
782
  const parent = this.renewalParent(ref);
730
783
  const input = entry.input;
731
784
  const policy = await resolveScheduling(input.context.cwd, input.context.isProjectTrusted());
732
- this.configure(policy);
733
785
  const limits = { ...entry.state.limits };
734
786
  for (const key of Object.keys(limits) as (keyof ExecutionLimits)[])
735
787
  limits[key] = Math.min(
@@ -810,21 +862,52 @@ export class DelegationCoordinator {
810
862
  throw new Error("Resume denied: delegation depth limit");
811
863
  // Reuse the original admission's writer and nested no-lending checks, without
812
864
  // admitting another task or charging another spawn.
813
- this.validate(input);
865
+ this.validateAdmission(
866
+ input,
867
+ selected,
868
+ entry.state.prewalk?.status === "armed" ? undefined : selected,
869
+ );
814
870
  this.renewalParent(ref);
815
871
  const available =
816
872
  this.active().length < policy.concurrency &&
817
873
  (!isWriter(input.profile) || (!this.writer && !this.parentMutations.size));
818
874
  if (parent && (this.queue.size || !available))
819
875
  throw new Error("Nested capacity unavailable; no permit lending");
820
- this.assertQueueCapacity([input]);
876
+ const previousPolicy = this.policy;
877
+ this.policy = policy;
878
+ try {
879
+ this.assertQueueCapacity([input]);
880
+ } finally {
881
+ this.policy = previousPolicy;
882
+ }
821
883
  if (
822
884
  entry.cancelled ||
823
885
  this.closing ||
824
- entry.state.disposed ||
825
- entry.state.status !== "paused"
886
+ this.presence.get(ref.id) === "stopped" ||
887
+ (completed ? entry.state.status !== "settled" : entry.state.status !== "paused")
826
888
  )
827
889
  throw new Error("Resume cancelled by stop or shutdown");
890
+ if (completed) {
891
+ const { window, status, result, outcome, artifacts, error, endedAt } = entry.state;
892
+ entry.state = {
893
+ ...entry.state,
894
+ completedWindows: [
895
+ ...(entry.state.completedWindows ?? []),
896
+ structuredClone({ window, status, result, outcome, artifacts, error, endedAt }),
897
+ ],
898
+ disposed: false,
899
+ endedAt: null,
900
+ outcome: entry.validator
901
+ ? { validation: "awaiting", attempts: 0, errors: [], finalized: false }
902
+ : undefined,
903
+ artifacts: undefined,
904
+ result: "",
905
+ progress: "",
906
+ };
907
+ entry.selection.model = validateModelSelection(input.context, selected);
908
+ entry.selection.thinkingLevel = selected.effort;
909
+ }
910
+ this.policy = policy;
828
911
  entry.done = new Promise<void>((resolve) => {
829
912
  entry.finish = resolve;
830
913
  });
@@ -989,24 +1072,27 @@ export class DelegationCoordinator {
989
1072
  dispose: () => this.dispose(entry),
990
1073
  });
991
1074
  }
992
- private closeDescendantPresence(entry: Entry): void {
1075
+ private closeDescendantPresence(entry: Entry, completed = false): void {
993
1076
  for (const child of this.entries.values())
994
1077
  if (
995
1078
  child.state.parentId === entry.state.id &&
996
1079
  child.state.parentGeneration === entry.state.generation
997
1080
  ) {
998
- this.presence.set(child.state.id, "stopped");
999
- this.closeDescendantPresence(child);
1081
+ // Ancestor completion closes live/paused descendants, but does not retire a
1082
+ // successful retained conversation. Explicit stop remains sticky.
1083
+ if (!(completed && child.state.disposed && child.state.status === "settled"))
1084
+ this.presence.set(child.state.id, "stopped");
1085
+ this.closeDescendantPresence(child, completed);
1000
1086
  }
1001
1087
  }
1002
1088
  private dispose(entry: Entry): void {
1003
1089
  if (entry.state.disposed) return;
1004
1090
  this.queue.delete(entry);
1005
1091
  clearTimeout(entry.timer);
1006
- entry.finalizedMessages = structuredClone(entry.session?.messages ?? []);
1092
+ if (entry.session) entry.finalizedMessages = structuredClone(entry.session.messages);
1007
1093
  entry.unsubscribe?.();
1008
1094
  entry.session?.dispose();
1009
- // Retain immutable evidence, not the disposed SDK session graph, while idle/parked.
1095
+ // Retain the authoritative SessionManager and immutable evidence, not the SDK runtime graph.
1010
1096
  entry.unsubscribe = undefined;
1011
1097
  entry.session = undefined;
1012
1098
  entry.prompt = undefined;
@@ -1015,11 +1101,14 @@ export class DelegationCoordinator {
1015
1101
  entry.run = undefined;
1016
1102
  entry.pauseDrain = undefined;
1017
1103
  if (this.writer === entry.state.id) this.writer = undefined;
1018
- this.closeDescendantPresence(entry);
1104
+ this.closeDescendantPresence(
1105
+ entry,
1106
+ entry.state.status === "settled" && !entry.cancelled && !this.closing,
1107
+ );
1019
1108
  this.presence.set(entry.state.id, entry.cancelled || this.closing ? "stopped" : "idle");
1020
1109
  this.update(entry, {
1021
1110
  disposed: true,
1022
- retainedContext: false,
1111
+ retainedContext: !!entry.sessionManager && entry.state.status === "settled",
1023
1112
  endedAt: Date.now(),
1024
1113
  writerLease: false,
1025
1114
  outcome: entry.state.outcome ? { ...entry.state.outcome, finalized: true } : undefined,
@@ -42,6 +42,13 @@ export type ChildSnapshot = Readonly<
42
42
  pauseReason?: string;
43
43
  retainedContext?: boolean;
44
44
  window?: number;
45
+ /** Immutable completion evidence from earlier windows of this same conversation. */
46
+ completedWindows?: ReadonlyArray<
47
+ Pick<
48
+ ChildSnapshot,
49
+ "window" | "status" | "result" | "outcome" | "artifacts" | "error" | "endedAt"
50
+ >
51
+ >;
45
52
  status:
46
53
  | "queued"
47
54
  | "setup"
package/src/hub.ts CHANGED
@@ -85,8 +85,7 @@ export function createHub(
85
85
  const roster = () =>
86
86
  taskTree(
87
87
  coordinator
88
- .list()
89
- .flatMap((child) => coordinator.generations(child.id))
88
+ .sessions()
90
89
  .filter((child) =>
91
90
  lineText(
92
91
  [
@@ -208,7 +207,7 @@ export function createHub(
208
207
  paint(
209
208
  interactionTarget
210
209
  ? "Resize · Esc cancels"
211
- : "Subagent Hub " + coordinator.list().length,
210
+ : "Subagent Hub " + coordinator.sessions().length,
212
211
  ),
213
212
  ];
214
213
  const inner = Math.max(0, width - 4);
@@ -107,7 +107,7 @@ export default function delegation(pi: ExtensionAPI) {
107
107
  (tui, theme) => {
108
108
  renderWidget = () => tui.requestRender();
109
109
  return createChildWidget(
110
- () => coordinator.list(),
110
+ () => coordinator.sessions(),
111
111
  theme,
112
112
  () => tui.terminal.rows,
113
113
  );
@@ -373,7 +373,7 @@ export default function delegation(pi: ExtensionAPI) {
373
373
  label: "Coordinate delegation",
374
374
  exposure: "model-only",
375
375
  description:
376
- "Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park, paused resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
376
+ "Live-parent identity-pinned coordination. message targets a running family agent (omit target for root inbox, direct children only); inbox reads own bounded mailbox; wait targets an outcome or own inbox with required 1–60000ms timeout, retaining capacity. No transcript access. Child actions require explicit operator grants. Root additionally has status/stop/park, paused/completed resume (assignment is the appended continuation), and idle followup/parked revive. Resume retains identity and full context, resets only the deadline window, and requires explicit child resumes after parent resume. Followup/revive create a fresh generation/session with current model/auth and narrowed current grants, no old tool replay or historical recovery. Message accepted/queued is NOT model acknowledgment; delivered means transcript/inbox visibility only. 4096-byte messages, 64 mailbox/waiters, 512 messages and 64 generations per live parent.",
377
377
  parameters: coordinationParameters,
378
378
  async execute(callerId, args, signal, _update, ctx) {
379
379
  if (["resume", "followup", "revive"].includes(args.action)) requireSupportedHost();
@@ -493,10 +493,10 @@ export default function delegation(pi: ExtensionAPI) {
493
493
  promptGuidelines: [
494
494
  "Use subagent for native worker delegation requested by the user or an active workflow; no extension-specific user instruction or enable call is needed. Use agent worker for implementation, scout for research, review for review, or general for restricted inherited capabilities.",
495
495
  "Pass subagent the approved task handoff and existing workspace cwd. Preserve selected model routing; no model override is needed to inherit the parent. Pass accessible skill/reference paths in the task; general inherits available skills and callable builtins, plus operator-reviewed leaf tools only; parent instructions/history and project trust are not inherited.",
496
- "subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume continues a paused conversation with its full context and cumulative budgets; completed idle/parked work starts a fresh generation.",
496
+ "subagent launches are async by default and wake this session after actual drain. Continue independent work or yield; do not poll or sleep. Use the exact returned id (UUID@generation) for status, steer, stop or resume. Resume continues a drained paused or completed conversation with its same identity/generation, full SDK context and cumulative budgets; unavailable context and stopped/failed identities reject. Use explicit delegate_coordinate followup/revive for fresh completed work.",
497
497
  ],
498
498
  description:
499
- "Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
499
+ "Native delegation for orchestration and coding task owners. Launch {agent,task,cwd?,model?,async?} or an ordered {tasks:[...],async?,cwd?} batch. worker maps explicitly to the implementation profile; unknown/custom agent names reject. Model strings require exact provider/id[:effort] and operator grants. Actions: agents, list, status/stop with exact returned id, steer/resume with id and message. Resume requires a drained paused or completed session and restores its full conversation with the same identity/generation and session count; unavailable context and stopped/failed identities reject. Uses the same registry, parent permissions, limits and global writer lease as delegate_task. Existing workspaces only: file paths and shell cwd are explicitly adapted through root builtin hooks. No automatic worktree creation/commits/apply/deletion, detached survival, script workflows or blanket competing API parity. Do not load another provider of the subagent tool alongside this extension.",
500
500
  parameters: compatibleParameters,
501
501
  async execute(callerId, args, signal, onUpdate, ctx) {
502
502
  if (!("action" in args) || args.action === "resume" || args.action === "steer")
@@ -564,20 +564,10 @@ export default function delegation(pi: ExtensionAPI) {
564
564
  if (args.action === "stop") await owner.stop(target);
565
565
  else if (args.action === "steer") await owner.steer(target, args.message);
566
566
  else if ("message" in args) {
567
- const presence = owner
568
- .agents()
569
- .find(
570
- (state) => state.id === target.id && state.generation === target.generation,
571
- )?.presence;
572
567
  const response = await coordinationTool(undefined, undefined, true).execute(
573
568
  callerId,
574
569
  {
575
- action:
576
- owner.snapshot(target)?.status === "paused"
577
- ? "resume"
578
- : presence === "parked"
579
- ? "revive"
580
- : "followup",
570
+ action: "resume",
581
571
  target,
582
572
  assignment: args.message,
583
573
  },
@@ -586,10 +576,6 @@ export default function delegation(pi: ExtensionAPI) {
586
576
  ctx,
587
577
  );
588
578
  const next = response.details as ChildSnapshot;
589
- if (next.generation !== target.generation)
590
- void owner.wait(next).then((state) => {
591
- if (!closing && coordinator === owner) show(compatible(state), state.transcript, true);
592
- });
593
579
  return result(compatible(next));
594
580
  }
595
581
  return result(compatible(owner.snapshot(target)!));
@@ -15,7 +15,7 @@ import {
15
15
  taskTree,
16
16
  } from "./task-presentation.ts";
17
17
 
18
- /** Event-driven, bounded current-agent tree; history and full metrics remain in the Hub. */
18
+ /** Full session totals with an event-driven, bounded active-agent tree; details remain in the Hub. */
19
19
  export function createChildWidget(
20
20
  snapshot: () => ChildSnapshot[],
21
21
  theme: Pick<Theme, "fg"> & Partial<Pick<Theme, "bold">>,