@oh-my-pi/pi-coding-agent 15.10.12 → 15.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/dist/cli.js +869 -825
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/capability/mcp.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/config/keybindings.d.ts +6 -1
  7. package/dist/types/config/settings-schema.d.ts +66 -34
  8. package/dist/types/export/html/template.generated.d.ts +1 -1
  9. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  10. package/dist/types/extensibility/shared-events.d.ts +2 -2
  11. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  12. package/dist/types/internal-urls/index.d.ts +1 -0
  13. package/dist/types/internal-urls/types.d.ts +1 -1
  14. package/dist/types/irc/bus.d.ts +66 -0
  15. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  16. package/dist/types/mcp/oauth-flow.d.ts +6 -1
  17. package/dist/types/mcp/transports/stdio.d.ts +1 -0
  18. package/dist/types/mcp/types.d.ts +2 -0
  19. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  20. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  21. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  22. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  23. package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  25. package/dist/types/modes/components/status-line/types.d.ts +3 -0
  26. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  27. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  28. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  29. package/dist/types/modes/components/welcome.d.ts +3 -9
  30. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
  32. package/dist/types/modes/interactive-mode.d.ts +3 -2
  33. package/dist/types/modes/theme/theme.d.ts +3 -1
  34. package/dist/types/modes/types.d.ts +3 -2
  35. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  36. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  37. package/dist/types/registry/agent-registry.d.ts +16 -5
  38. package/dist/types/session/agent-session.d.ts +35 -30
  39. package/dist/types/session/messages.d.ts +2 -4
  40. package/dist/types/session/session-history-format.d.ts +12 -0
  41. package/dist/types/session/session-manager.d.ts +21 -3
  42. package/dist/types/session/streaming-output.d.ts +23 -0
  43. package/dist/types/task/executor.d.ts +11 -2
  44. package/dist/types/task/index.d.ts +11 -4
  45. package/dist/types/task/output-manager.d.ts +0 -7
  46. package/dist/types/task/repair-args.d.ts +8 -7
  47. package/dist/types/task/types.d.ts +55 -51
  48. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  49. package/dist/types/tools/find.d.ts +0 -11
  50. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  51. package/dist/types/tools/index.d.ts +1 -3
  52. package/dist/types/tools/irc.d.ts +76 -38
  53. package/dist/types/tools/job.d.ts +7 -1
  54. package/dist/types/tools/render-utils.d.ts +22 -0
  55. package/examples/extensions/with-deps/package.json +1 -0
  56. package/package.json +11 -10
  57. package/scripts/bundle-dist.ts +28 -19
  58. package/src/async/index.ts +0 -1
  59. package/src/capability/mcp.ts +1 -0
  60. package/src/cli/gallery-cli.ts +6 -5
  61. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  62. package/src/cli/gallery-fixtures/types.ts +5 -0
  63. package/src/cli.ts +20 -6
  64. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  65. package/src/config/keybindings.ts +6 -1
  66. package/src/config/mcp-schema.json +4 -0
  67. package/src/config/settings-schema.ts +68 -41
  68. package/src/config/settings.ts +7 -0
  69. package/src/edit/renderer.ts +96 -46
  70. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  71. package/src/eval/agent-bridge.ts +3 -16
  72. package/src/eval/js/shared/prelude.txt +1 -1
  73. package/src/eval/py/prelude.py +5 -6
  74. package/src/export/html/template.generated.ts +1 -1
  75. package/src/export/html/template.js +44 -14
  76. package/src/extensibility/custom-tools/types.ts +2 -2
  77. package/src/extensibility/shared-events.ts +2 -2
  78. package/src/internal-urls/docs-index.generated.ts +9 -9
  79. package/src/internal-urls/history-protocol.ts +113 -0
  80. package/src/internal-urls/index.ts +1 -0
  81. package/src/internal-urls/router.ts +3 -1
  82. package/src/internal-urls/types.ts +1 -1
  83. package/src/irc/bus.ts +292 -0
  84. package/src/main.ts +8 -60
  85. package/src/mcp/manager.ts +3 -0
  86. package/src/mcp/oauth-discovery.ts +27 -2
  87. package/src/mcp/oauth-flow.ts +47 -1
  88. package/src/mcp/transports/stdio.ts +3 -0
  89. package/src/mcp/types.ts +2 -0
  90. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  91. package/src/modes/components/assistant-message.ts +15 -0
  92. package/src/modes/components/btw-panel.ts +5 -1
  93. package/src/modes/components/compaction-summary-message.ts +68 -32
  94. package/src/modes/components/custom-editor.ts +10 -0
  95. package/src/modes/components/mcp-add-wizard.ts +13 -0
  96. package/src/modes/components/settings-selector.ts +2 -0
  97. package/src/modes/components/status-line/component.ts +22 -12
  98. package/src/modes/components/status-line/types.ts +3 -0
  99. package/src/modes/components/tool-execution.ts +31 -1
  100. package/src/modes/components/transcript-container.ts +99 -18
  101. package/src/modes/components/tree-selector.ts +6 -1
  102. package/src/modes/components/ttsr-notification.ts +72 -30
  103. package/src/modes/components/welcome.ts +9 -33
  104. package/src/modes/controllers/event-controller.ts +93 -4
  105. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  106. package/src/modes/controllers/input-controller.ts +18 -2
  107. package/src/modes/controllers/mcp-command-controller.ts +34 -2
  108. package/src/modes/controllers/selector-controller.ts +25 -17
  109. package/src/modes/controllers/tool-args-reveal.ts +174 -0
  110. package/src/modes/interactive-mode.ts +17 -15
  111. package/src/modes/theme/theme.ts +24 -5
  112. package/src/modes/types.ts +3 -5
  113. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  114. package/src/modes/utils/ui-helpers.ts +51 -49
  115. package/src/prompts/system/irc-incoming.md +3 -4
  116. package/src/prompts/system/orchestrate-notice.md +2 -2
  117. package/src/prompts/system/subagent-system-prompt.md +0 -5
  118. package/src/prompts/system/system-prompt.md +1 -0
  119. package/src/prompts/system/workflow-notice.md +2 -2
  120. package/src/prompts/tools/eval.md +3 -3
  121. package/src/prompts/tools/irc.md +29 -19
  122. package/src/prompts/tools/read.md +2 -2
  123. package/src/prompts/tools/task-summary.md +5 -16
  124. package/src/prompts/tools/task.md +43 -29
  125. package/src/registry/agent-lifecycle.ts +218 -0
  126. package/src/registry/agent-registry.ts +16 -5
  127. package/src/sdk.ts +29 -9
  128. package/src/session/agent-session.ts +268 -241
  129. package/src/session/messages.ts +11 -78
  130. package/src/session/session-history-format.ts +246 -0
  131. package/src/session/session-manager.ts +59 -5
  132. package/src/session/streaming-output.ts +60 -0
  133. package/src/task/executor.ts +855 -466
  134. package/src/task/index.ts +723 -794
  135. package/src/task/output-manager.ts +0 -11
  136. package/src/task/render.ts +142 -66
  137. package/src/task/repair-args.ts +21 -9
  138. package/src/task/types.ts +73 -66
  139. package/src/tools/ask.ts +4 -2
  140. package/src/tools/bash.ts +15 -5
  141. package/src/tools/browser/tab-worker.ts +26 -7
  142. package/src/tools/browser.ts +28 -1
  143. package/src/tools/find.ts +2 -27
  144. package/src/tools/grouped-file-output.ts +1 -118
  145. package/src/tools/index.ts +4 -12
  146. package/src/tools/irc.ts +596 -171
  147. package/src/tools/job.ts +41 -7
  148. package/src/tools/read.ts +57 -1
  149. package/src/tools/render-utils.ts +56 -0
  150. package/src/tools/renderers.ts +2 -0
  151. package/src/tools/resolve.ts +4 -1
  152. package/src/tools/write.ts +65 -47
  153. package/src/web/search/providers/anthropic.ts +29 -4
  154. package/dist/types/async/support.d.ts +0 -2
  155. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  156. package/dist/types/task/simple-mode.d.ts +0 -8
  157. package/src/async/support.ts +0 -5
  158. package/src/task/simple-mode.ts +0 -27
@@ -13,8 +13,8 @@ Worth it when the task benefits from decomposition + parallel coverage, or from
13
13
  <helpers>
14
14
  State persists across cells, so scout in one cell and fan out in the next. Every cell has:
15
15
 
16
- - `agent(prompt, *, agent_type="task", model=None, context=None, label=None, schema=None)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent_type` picks a discovered agent ("explore", "reviewer", "oracle", …); `context` is shared background; `label` names the artifact. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep.
17
- - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool runs as wide as a `task` tool batch — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
16
+ - `agent(prompt, *, agent_type="task", model=None, label=None, schema=None)` — run ONE subagent; returns its final text, or the validated object when `schema` (a JSON Schema dict) is given. With `schema` the subagent is forced to emit structured output that is validated for you — branch on the object, not on parsed prose. `agent_type` picks a discovered agent ("explore", "reviewer", "oracle", …); `label` names the artifact. Shared background goes in a `local://` file referenced from each prompt, not a parameter. Subagents are told their final text IS the return value, so they hand back raw data. `agent()` blocks until the subagent finishes; eval-spawned agents nest at most 3 deep.
17
+ - `parallel(thunks)` — run zero-arg callables concurrently through a bounded pool, preserving input order; returns once all finish. The pool is bounded by the session's `task` concurrency — don't hand-tune it; fan out as wide as the work divides. A thunk that raises propagates — wrap risky work in `try/except` inside the thunk to keep partial results. In a loop, bind each closure's value with a default arg (`lambda d=d: …`) or every thunk captures the last one.
18
18
  - `pipeline(items, *stages)` — map items through `stages` left-to-right. There is a BARRIER between stages: ALL items clear stage N before stage N+1 begins. Each stage is a one-arg callable; stage 1 gets the original item, later stages get the previous result. Same pool width as `parallel()`.
19
19
  - `completion(prompt, *, model="default", system=None, schema=None)` — oneshot, stateless model call (no tools, no history). Tiers: "smol", "default", "slow". Cheap classification/scoring inside a fan-out.
20
20
  - `log(message)` — emit a progress line above the status tree. `phase(title)` — start a phase; the status lines that follow group under it.
@@ -46,9 +46,9 @@ tool.<name>(args) → unknown
46
46
  Invoke any session tool by name. `args` is the tool's parameter object.
47
47
  completion(prompt, model?="default", system?=None, schema?=None) → str | dict
48
48
  Oneshot, stateless completion (no history, no tools). `model` picks a tier: "smol" (fast), "default" (this session's model), "slow" (most capable). Pass `system` for a system prompt. Pass a JSON-Schema `schema` to force structured output and get the parsed object back; otherwise returns the completion text.
49
- {{#if spawns}}agent(prompt, agent_type?="task", model?=None, context?=None, label?=None, schema?=None) → str | dict
50
- Run a subagent and return its final output. Defaults to the bundled "task" agent; pass `agent_type`/`agentType` for another discovered agent. Pass a JSON-Schema `schema` to force structured output and get the parsed object back.
51
- {{#if js}} In JS, pass options as one trailing object — never positional: agent(prompt, { agentType, context, schema }).
49
+ {{#if spawns}}agent(prompt, agent_type?="task", model?=None, label?=None, schema?=None) → str | dict
50
+ Run a subagent and return its final output. Defaults to the bundled "task" agent; pass `agent_type`/`agentType` for another discovered agent. Pass a JSON-Schema `schema` to force structured output and get the parsed object back. Share background by writing a `local://` file and referencing it in the prompt.
51
+ {{#if js}} In JS, pass options as one trailing object — never positional: agent(prompt, { agentType, schema }).
52
52
  {{/if}}
53
53
  {{/if}}
54
54
  parallel(thunks) → list
@@ -1,11 +1,15 @@
1
- Sends short text messages to other live agents in this process and receives their prose replies.
1
+ Sends short text messages to other agents in this process and receives theirs.
2
2
 
3
3
  <instruction>
4
4
  - The main agent is addressable as `Main`. Subagents reuse their task id (e.g. `AuthLoader`, or `AuthLoader-2` when the name repeats).
5
- - `op: "list"` returns the current set of visible peers. Use it before sending if you are not sure who is live.
6
- - `op: "send"` delivers `message` to `to`. `to` may be a specific id or `"all"` to broadcast.
7
- - Replies are generated on a side channel that does not wait for the recipient's main loop, so it is safe to IRC an agent that is mid tool call.
8
- - The exchange (question + auto-reply) is injected into the recipient's history; they see it on their next turn and can follow up.
5
+ - `op: "list"` every addressable peer with status (`running` | `idle` | `parked`), unread count, parent, and last activity. Use it before sending if you are not sure who exists.
6
+ - `op: "send"` fire-and-forget delivery of `message` to `to` (a peer id, or `"all"` to broadcast to live peers). Returns per-recipient receipts immediately; it NEVER waits for the recipient to act. Receipt outcomes: `injected` (recipient was mid-turn; message folded in at their next step boundary), `woken` (idle recipient started a turn), `revived` (parked recipient was brought back and woken), `failed`.
7
+ - Messaging an `idle` or `parked` peer is how you wake it there is no separate revive call.
8
+ - `send` with `await: true` — convenience round-trip: send, then block until the next message from that peer arrives (or the timeout passes). Invalid with `to: "all"`.
9
+ - `op: "wait"` — block until a message arrives (optionally only `from` a specific peer); consumes and returns it. A timeout is a clean "no message" result, not an error.
10
+ - `op: "inbox"` — drain pending messages without blocking (`peek: true` to leave them unread).
11
+ - `replyTo` — set it to the id of the message you are answering so the sender can correlate.
12
+ - Nobody answers on a peer's behalf anymore: a reply only arrives when the recipient actually sends one. For background on what a peer has been doing, `read` `history://<id>` instead of interrogating them.
9
13
  </instruction>
10
14
 
11
15
  <when_to_use>
@@ -21,29 +25,35 @@ NEVER use `irc` for: routine progress updates, things a tool call can verify, or
21
25
  <etiquette>
22
26
  These rules apply to both sending and replying.
23
27
  - **Plain prose only.** NEVER send structured JSON status payloads (e.g. `{"type":"task_completed",…}`). Write a normal sentence: "Done with the auth refactor — left a TODO in `src/server/auth.ts` for the rate limiter."
24
- - **NEVER quote the message you are replying to.** Lead with the answer.
25
- - **Use IRC, not terminal tools, to learn about peers.** NEVER `grep` artifacts, read other sessions' JSONL files, or shell-poke to figure out what another agent is doing. DM them.
26
- - **One round-trip is enough.** Replies arrive synchronously when the recipient is reachable. NEVER follow up with "did you get my message?". If `delivered` is empty or the result was `failed`, the peer is unavailable — move on or report the blocker; NEVER retry in a loop.
28
+ - **NEVER quote the message you are replying to.** Lead with the answer; set `replyTo` instead.
29
+ - **Use IRC, not terminal tools, to learn about peers.** NEVER `grep` artifacts, read other sessions' JSONL files, or shell-poke to figure out what another agent is doing. DM them, or `read` `history://<id>`.
30
+ - **Send, then keep working.** `send` returns immediately — only `wait` (or `await: true`) when you genuinely cannot proceed without the answer. NEVER follow up with "did you get my message?"; a `failed` receipt means the peer is unreachable — move on or report the blocker; NEVER retry in a loop.
31
+ - **Answer when a response is expected.** When an incoming message asks something, reply with `irc send` to the sender (you may finish your current step first).
27
32
  - **Stay terse.** A DM is a chat message, not a memo. One question per send. Share file paths and artifacts via `local://` / `memory://` / `artifact://` URLs instead of pasting blobs.
28
33
  - **Address peers by id.** Use the exact id from `op: "list"` (e.g. `AuthLoader`, `Main`). NEVER invent friendly names.
29
34
  - **NEVER IRC for things a tool would answer.** If a `read`, `grep`, or build command resolves the question, do that first.
30
- - **Answer incoming IRC messages before continuing.** Address the question directly; do not repeat it back to the user.
31
35
  </etiquette>
32
36
 
33
37
  <output>
34
- - `send`: returns each recipient that received the message and any prose replies that arrived.
35
- - `list`: returns peers and channels visible to the caller.
38
+ - `send`: per-recipient delivery receipts (`injected` / `woken` / `revived` / `failed`); with `await: true`, also the reply (or a timeout notice).
39
+ - `wait`: the consumed message, or a clean timeout notice.
40
+ - `inbox`: pending messages, oldest first.
41
+ - `list`: peers with status, unread count, parent, and last activity.
36
42
  </output>
37
43
 
38
44
  <examples>
39
45
  # List peers
40
46
  `{"op": "list"}`
41
- # Direct message to the main agent (waits for prose reply)
42
- `{"op": "send", "to": "Main", "message": "Should I prefer JWT or session cookies for the auth flow?"}`
43
- # Unexpected state ask the originator
44
- `{"op": "send", "to": "Main", "message": "Assignment says edit src/auth/jwt.ts but the file does not exist. Is the new path src/server/auth/jwt.ts?"}`
45
- # Blocked by a peerask them directly
46
- `{"op": "send", "to": "AuthLoader", "message": "Are you still touching src/server/auth.ts? I need to add a 401 path; OK to proceed or should I wait?"}`
47
- # Broadcast to discover who owns something (no replies, just informs them)
48
- `{"op": "send", "to": "all", "message": "About to refactor src/server/middleware/*. Anyone already in there?", "awaitReply": false}`
47
+ # Fire-and-forget DM keep working, check inbox later
48
+ `{"op": "send", "to": "AuthLoader", "message": "Are you still touching src/server/auth.ts? I need to add a 401 path."}`
49
+ # Round-trip when you cannot proceed without the answer
50
+ `{"op": "send", "to": "Main", "message": "Should I prefer JWT or session cookies for the auth flow?", "await": true}`
51
+ # Wake a parked agent (same send the bus revives it)
52
+ `{"op": "send", "to": "SchemaMigrator", "message": "The users table changed again; please re-check your migration."}`
53
+ # Block until a specific peer answers
54
+ `{"op": "wait", "from": "AuthLoader", "timeoutMs": 60000}`
55
+ # Drain pending messages
56
+ `{"op": "inbox"}`
57
+ # Broadcast to live peers (no replies expected)
58
+ `{"op": "send", "to": "all", "message": "About to refactor src/server/middleware/*. Anyone already in there?"}`
49
59
  </examples>
@@ -8,7 +8,7 @@ Read files, directories, archives, SQLite databases, images, documents, internal
8
8
 
9
9
  ## Parameters
10
10
 
11
- - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
11
+ - `path` — required. Local path, internal URI (`skill://`, `agent://`, `artifact://`, `history://`, `memory://`, `rule://`, `local://`, `vault://`, `mcp://`, `omp://`, `issue://`, `pr://`), or URL. Append `:<sel>` for line ranges, raw mode, or special modes (e.g. `src/foo.ts:50-200`, `src/foo.ts:raw`, `db.sqlite:users:42`).
12
12
 
13
13
  ## Selectors
14
14
 
@@ -74,7 +74,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
74
74
 
75
75
  # Internal URIs
76
76
 
77
- `skill://<name>`, `agent://<id>`, `artifact://<id>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `vault://<vault>/<path>`, `mcp://<uri>`, `omp://<doc>.md`, `issue://<N>`, and `pr://<N>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
77
+ `skill://<name>`, `agent://<id>`, `artifact://<id>`, `history://<agentId>`, `memory://root`, `rule://<name>`, `local://<name>.md`, `vault://<vault>/<path>`, `mcp://<uri>`, `omp://<doc>.md`, `issue://<N>`, and `pr://<N>` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated. `history://<agentId>` is an agent's transcript as concise markdown; bare `history://` lists agents.
78
78
 
79
79
  <critical>
80
80
  - You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
@@ -1,28 +1,17 @@
1
- <task-summary>
2
- <header>{{successCount}}/{{totalCount}} succeeded{{#if hasCancelledNote}} ({{cancelledCount}} cancelled){{/if}} [{{duration}}]</header>
3
-
4
- {{#each summaries}}
5
- <agent id="{{id}}" agent="{{agent}}">
6
- <status>{{status}}</status>
1
+ <task-result id="{{id}}" agent="{{agentName}}" status="{{status}}" duration="{{duration}}">
7
2
  {{#if meta}}<meta lines="{{meta.lineCount}}" size="{{meta.charSize}}" />{{/if}}
8
3
  {{#if truncated}}
9
- <preview full-path="agent://{{id}}">
4
+ <preview full-output="agent://{{id}}">
10
5
  {{preview}}
11
6
  </preview>
12
7
  {{else}}
13
- <result>
8
+ <output>
14
9
  {{preview}}
15
- </result>
10
+ </output>
16
11
  {{/if}}
17
- </agent>
18
- {{#unless @last}}
19
- ---
20
- {{/unless}}
21
- {{/each}}
22
-
23
12
  {{#if mergeSummary}}
24
13
  <merge-summary>
25
14
  {{mergeSummary}}
26
15
  </merge-summary>
27
16
  {{/if}}
28
- </task-summary>
17
+ </task-result>
@@ -1,43 +1,56 @@
1
- Launches subagents to parallelize workflows.
1
+ {{#if asyncEnabled}}{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
2
2
 
3
- {{#if asyncEnabled}}
4
- - Results are delivered automatically when complete.
5
- - The tool result lists the assigned task ids (e.g. `AuthLoader`) those are the live agent ids.
6
- {{#if ircEnabled}}
7
- - Coordinate with running tasks via `irc` using those ids. `job cancel` terminates a task and **cannot carry a message** — only use it for stalled/abandoned work.
8
- - If genuinely blocked on completion, wait with `job poll`; otherwise keep working.
9
- {{else}}
10
- - If genuinely blocked on completion, wait with `job poll`; otherwise keep working.
11
- - Use `job list` to snapshot manager state; `cancel: [id]` only to actually stop a stuck task.
12
- {{/if}}
13
- {{/if}}
3
+ - Spawning is non-blocking: the call returns immediately with the agent id{{#if batchEnabled}}s{{/if}} and job id{{#if batchEnabled}}s{{/if}}; each result is delivered automatically when that agent yields.
4
+ - Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
5
+ - If genuinely blocked on a result, wait with `job poll`; otherwise keep working. `job cancel` terminates a task and **cannot carry a message** — only for stalled/abandoned work.
6
+ {{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Runs ONE subagent synchronously per call.{{/if}}
14
7
 
8
+ - Spawning is blocking: the call returns only after the agent{{#if batchEnabled}}s{{/if}} finish; results arrive inline.
9
+ - Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
10
+ {{/if}}
15
11
  {{#if ircEnabled}}
16
- Subagents have no conversation history, but they can reach you and their siblings live via the `irc` tool. Front-load every fact, file path, and direction they need in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
17
- {{else}}
18
- Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if contextEnabled}}`context` or `assignment`{{else}}each `assignment`{{/if}}.
12
+ - Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
19
13
  {{/if}}
20
14
 
15
+ <lifecycle>
16
+ - Finished agents stay alive: `idle` first, then `parked` after a TTL.{{#if ircEnabled}} Both remain addressable and revivable: messaging one via `irc` wakes it and runs your message as a follow-up turn. **Prefer messaging an agent that already holds the relevant context over spawning fresh** — check `irc` op:"list" for candidates.{{/if}}
17
+ - `history://<id>` is the agent's transcript; `agent://<id>` its latest output artifact.
18
+ </lifecycle>
19
+
21
20
  <parameters>
22
- - `agent`: agent type for all tasks
23
- - `tasks`: tasks to execute in parallel
24
- - `.id`: CamelCase, ≤32 chars
25
- - `.description`: UI label only — subagent never sees it
26
- - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
27
- {{#if contextEnabled}}- `context`: shared background prepended to every assignment; session-specific only{{/if}}
28
- {{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
29
- {{#if isolationEnabled}}- `isolated`: run in isolated env; use when tasks edit overlapping files{{/if}}
21
+ - `agent`: agent type to spawn
22
+ {{#if batchEnabled}}
23
+ - `context`: shared background prepended to every assignment — goal, constraints, shared contract (see context-fmt); REQUIRED, session-specific only
24
+ - `tasks`: tasks to spawnone subagent per item, all in parallel:
25
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
26
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
27
+ - `description`: UI label only subagent never sees it
28
+ {{#if isolationEnabled}}
29
+ - `isolated`: run this spawn in an isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
30
+ {{/if}}
31
+ {{else}}
32
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
33
+ - `description`: UI label only — subagent never sees it
34
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
35
+ {{#if isolationEnabled}}
36
+ - `isolated`: run in isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
37
+ {{/if}}
38
+ {{/if}}
30
39
  </parameters>
31
40
 
32
41
  <rules>
33
- - **Maximize batch width.** Spawn the widest parallel set the work decomposes into. NEVER spawn a single-task batch for divisible work, or defer work that could have been concurrent.
34
- - **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.
42
+ - **Maximize fan-out.** Issue the widest {{#if batchEnabled}}`tasks[]` batch (or set of parallel `task` calls){{else}}set of parallel `task` calls{{/if}} the work decomposes into. NEVER serialize work that could run concurrently.
43
+ - **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files.
35
44
  - No globs, no "update all", no package-wide scope. Fan out.
36
45
  - NEVER slow down or serialize because tasks might overlap on some files. Agents resolve collisions among themselves in real time.
37
- - Pass large payloads via `local://<path>` URIs, not inline. {{#if contextEnabled}} (other than the context){{/if}}
38
- {{#if contextEnabled}}- Put shared constraints in `context` once; do not duplicate across assignments.{{/if}}
46
+ - Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
47
+ {{#if batchEnabled}}
48
+ - **Shared background** lives in `context` once — never duplicated across assignments. Pass large payloads via `local://<path>` URIs, not inline.
49
+ {{else}}
50
+ - **Shared background**: write it ONCE to a `local://` file (e.g. `local://ctx.md`) and reference that path in each assignment. Pass large payloads via `local://<path>` URIs, not inline.
51
+ {{/if}}
39
52
  - Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
40
- - **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands — they cannot do it and the turn is wasted. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
53
+ - **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
41
54
  - **No reasoning offload**: NEVER offload reasoning, analysis, design, or decision-making to `quick_task` or `explore` — they run minimal-effort / small models for mechanical lookups and data collection only. Keep judgment and synthesis in your own context; delegate hard thinking to `task`, `plan`, or `oracle`.
42
55
  </rules>
43
56
 
@@ -51,9 +64,10 @@ Test: can task B run correctly without seeing A's output? If no, sequence A →
51
64
  Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
52
65
  Parallel when tasks touch disjoint files or are independent refactors/tests.
53
66
  {{/if}}
67
+ {{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
54
68
  </parallelization>
55
69
 
56
- {{#if contextEnabled}}
70
+ {{#if batchEnabled}}
57
71
  <context-fmt>
58
72
  # Goal ← one sentence: what the batch accomplishes
59
73
  # Constraints ← MUST/NEVER rules and session decisions
@@ -0,0 +1,218 @@
1
+ /**
2
+ * AgentLifecycleManager - Owns the idle → parked → revived lifecycle of
3
+ * adopted subagents.
4
+ *
5
+ * The task executor hands a finished agent over via {@link AgentLifecycleManager.adopt};
6
+ * from then on the manager arms a TTL timer whenever the agent goes `idle`,
7
+ * parks it on expiry (disposes the live session, keeps the AgentRef +
8
+ * sessionFile), and revives it on demand through
9
+ * {@link AgentLifecycleManager.ensureLive}. Only this manager flips
10
+ * `parked` ↔ `idle`.
11
+ */
12
+
13
+ import { logger } from "@oh-my-pi/pi-utils";
14
+ import type { AgentSession } from "../session/agent-session";
15
+ import { AgentRegistry, MAIN_AGENT_ID, type RegistryEvent } from "./agent-registry";
16
+
17
+ export type AgentReviver = () => Promise<AgentSession>;
18
+
19
+ export interface AdoptOptions {
20
+ /** TTL before an idle agent is parked. <= 0 disables parking. */
21
+ idleTtlMs: number;
22
+ /** Recreates a live AgentSession from the ref's sessionFile. Absent => not resumable after park (e.g. isolated runs). */
23
+ revive?: AgentReviver;
24
+ }
25
+
26
+ interface AdoptedAgent {
27
+ idleTtlMs: number;
28
+ revive?: AgentReviver;
29
+ timer?: NodeJS.Timeout;
30
+ }
31
+
32
+ export class AgentLifecycleManager {
33
+ static #global: AgentLifecycleManager | undefined;
34
+
35
+ static global(): AgentLifecycleManager {
36
+ if (!AgentLifecycleManager.#global) {
37
+ AgentLifecycleManager.#global = new AgentLifecycleManager();
38
+ }
39
+ return AgentLifecycleManager.#global;
40
+ }
41
+
42
+ /** Reset the global manager. Test-only. */
43
+ static resetGlobalForTests(): void {
44
+ const current = AgentLifecycleManager.#global;
45
+ if (current) {
46
+ current.#unsubscribe?.();
47
+ current.#unsubscribe = undefined;
48
+ for (const adopted of current.#adopted.values()) {
49
+ clearTimeout(adopted.timer);
50
+ }
51
+ current.#adopted.clear();
52
+ current.#revivals.clear();
53
+ current.#parking.clear();
54
+ }
55
+ AgentLifecycleManager.#global = undefined;
56
+ }
57
+
58
+ readonly #registry: AgentRegistry;
59
+ readonly #adopted = new Map<string, AdoptedAgent>();
60
+ /** Ids whose session is being disposed by {@link park} right now. */
61
+ readonly #parking = new Set<string>();
62
+ /** In-flight revives, so concurrent {@link ensureLive} calls coalesce. */
63
+ readonly #revivals = new Map<string, Promise<AgentSession>>();
64
+ #unsubscribe: (() => void) | undefined;
65
+
66
+ constructor(registry: AgentRegistry = AgentRegistry.global()) {
67
+ this.#registry = registry;
68
+ this.#unsubscribe = registry.onChange(event => this.#onRegistryEvent(event));
69
+ }
70
+
71
+ /**
72
+ * Take ownership of a finished subagent. Caller has already set registry
73
+ * status to "idle". Arms the TTL timer (idleTtlMs <= 0 adopts without one).
74
+ */
75
+ adopt(id: string, opts: AdoptOptions): void {
76
+ if (id === MAIN_AGENT_ID) return;
77
+ if (!this.#registry.get(id)) {
78
+ logger.warn("AgentLifecycleManager.adopt: unknown agent id", { id });
79
+ return;
80
+ }
81
+ const existing = this.#adopted.get(id);
82
+ clearTimeout(existing?.timer);
83
+ const adopted: AdoptedAgent = { idleTtlMs: opts.idleTtlMs, revive: opts.revive };
84
+ this.#adopted.set(id, adopted);
85
+ this.#armTimer(id, adopted);
86
+ }
87
+
88
+ /** True if the id is adopted (parked or live). */
89
+ has(id: string): boolean {
90
+ return this.#adopted.has(id);
91
+ }
92
+
93
+ /** True while {@link park} is disposing this agent's session (lets dispose hooks distinguish park from teardown). */
94
+ isParking(id: string): boolean {
95
+ return this.#parking.has(id);
96
+ }
97
+
98
+ /**
99
+ * Dispose the live session, detach it from the registry, and mark the
100
+ * agent `parked`. No-op unless the id is adopted and live.
101
+ */
102
+ async park(id: string): Promise<void> {
103
+ const adopted = this.#adopted.get(id);
104
+ if (!adopted) return;
105
+ const ref = this.#registry.get(id);
106
+ if (!ref?.session) return;
107
+ if (adopted.timer) {
108
+ clearTimeout(adopted.timer);
109
+ adopted.timer = undefined;
110
+ }
111
+ this.#parking.add(id);
112
+ try {
113
+ try {
114
+ await ref.session.dispose();
115
+ } catch (error) {
116
+ logger.warn("AgentLifecycleManager.park: session dispose failed", { id, error: String(error) });
117
+ }
118
+ this.#registry.detachSession(id);
119
+ this.#registry.setStatus(id, "parked");
120
+ } finally {
121
+ this.#parking.delete(id);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Return the live session, reviving from the sessionFile if parked.
127
+ * Throws a plain Error if the id is unknown or parked without a reviver.
128
+ * Concurrent calls share one in-flight revive.
129
+ */
130
+ async ensureLive(id: string): Promise<AgentSession> {
131
+ const ref = this.#registry.get(id);
132
+ if (!ref) {
133
+ throw new Error(
134
+ `Unknown agent "${id}" — it was never registered or has been released. If a transcript exists, read history://${id}.`,
135
+ );
136
+ }
137
+ if (ref.session) return ref.session;
138
+ const inflight = this.#revivals.get(id);
139
+ if (inflight) return inflight;
140
+ const adopted = this.#adopted.get(id);
141
+ if (ref.status !== "parked" || !adopted?.revive) {
142
+ throw new Error(
143
+ `Agent "${id}" is ${ref.status} and cannot be revived${adopted?.revive ? "" : " (no reviver registered)"}. Its transcript remains readable at history://${id}.`,
144
+ );
145
+ }
146
+ const revival = this.#revive(id, adopted.revive, ref.sessionFile);
147
+ this.#revivals.set(id, revival);
148
+ try {
149
+ return await revival;
150
+ } finally {
151
+ this.#revivals.delete(id);
152
+ }
153
+ }
154
+
155
+ /** Hard removal: dispose if live, unregister from registry, drop timers. */
156
+ async release(id: string): Promise<void> {
157
+ const adopted = this.#adopted.get(id);
158
+ clearTimeout(adopted?.timer);
159
+ this.#adopted.delete(id);
160
+ const ref = this.#registry.get(id);
161
+ if (ref?.session) {
162
+ try {
163
+ await ref.session.dispose();
164
+ } catch (error) {
165
+ logger.warn("AgentLifecycleManager.release: session dispose failed", { id, error: String(error) });
166
+ }
167
+ }
168
+ this.#registry.unregister(id);
169
+ }
170
+
171
+ /** Teardown everything (process exit / main session dispose). */
172
+ async dispose(): Promise<void> {
173
+ this.#unsubscribe?.();
174
+ this.#unsubscribe = undefined;
175
+ const ids = [...this.#adopted.keys()];
176
+ await Promise.all(ids.map(id => this.release(id)));
177
+ this.#revivals.clear();
178
+ this.#parking.clear();
179
+ }
180
+
181
+ async #revive(id: string, revive: AgentReviver, sessionFile: string | null): Promise<AgentSession> {
182
+ const session = await revive();
183
+ this.#registry.attachSession(id, session, sessionFile);
184
+ // Emits status_changed → "idle", which re-arms the TTL timer below.
185
+ this.#registry.setStatus(id, "idle");
186
+ return session;
187
+ }
188
+
189
+ #armTimer(id: string, adopted: AdoptedAgent): void {
190
+ if (adopted.idleTtlMs <= 0) return;
191
+ clearTimeout(adopted.timer);
192
+ const timer = setTimeout(() => {
193
+ adopted.timer = undefined;
194
+ void this.park(id);
195
+ }, adopted.idleTtlMs);
196
+ timer.unref?.();
197
+ adopted.timer = timer;
198
+ }
199
+
200
+ #onRegistryEvent(event: RegistryEvent): void {
201
+ const adopted = this.#adopted.get(event.ref.id);
202
+ if (!adopted) return;
203
+ if (event.type === "removed") {
204
+ clearTimeout(adopted.timer);
205
+ this.#adopted.delete(event.ref.id);
206
+ return;
207
+ }
208
+ if (event.type !== "status_changed") return;
209
+ if (event.ref.status === "running") {
210
+ if (adopted.timer) {
211
+ clearTimeout(adopted.timer);
212
+ adopted.timer = undefined;
213
+ }
214
+ } else if (event.ref.status === "idle") {
215
+ this.#armTimer(event.ref.id, adopted);
216
+ }
217
+ }
218
+ }
@@ -1,16 +1,26 @@
1
1
  /**
2
- * AgentRegistry - Process-global registry of live AgentSession instances.
2
+ * AgentRegistry - Process-global registry of agents (the main session plus
3
+ * every subagent), keyed by stable id.
3
4
  *
4
- * Tracks every alive agent (the main session plus every subagent) so the
5
- * `irc` tool can address peers by id. Sessions are registered explicitly at
6
- * creation and removed when the owner releases them.
5
+ * Tracks each agent's status and (when live) its AgentSession so peers can be
6
+ * addressed by id (`irc`, `task resume`, `history://`). Sessions are
7
+ * registered explicitly at creation; finished agents stay registered as
8
+ * `idle` (live) or `parked` (session disposed, ref + sessionFile retained for
9
+ * revival) and are only removed on explicit release/teardown.
7
10
  */
8
11
 
9
12
  import type { AgentSession } from "../session/agent-session";
10
13
 
11
14
  export const MAIN_AGENT_ID = "Main";
12
15
 
13
- export type AgentStatus = "running" | "idle" | "completed" | "aborted";
16
+ /**
17
+ * - `running`: a turn is in flight.
18
+ * - `idle`: live AgentSession in memory, awaiting work. Finished agents are
19
+ * `idle`, not removed.
20
+ * - `parked`: session disposed; AgentRef + sessionFile retained, revivable.
21
+ * - `aborted`: hard-killed, terminal.
22
+ */
23
+ export type AgentStatus = "running" | "idle" | "parked" | "aborted";
14
24
  export type AgentKind = "main" | "sub";
15
25
 
16
26
  export interface AgentRef {
@@ -19,6 +29,7 @@ export interface AgentRef {
19
29
  kind: AgentKind;
20
30
  parentId?: string;
21
31
  status: AgentStatus;
32
+ /** Null exactly when parked/aborted. */
22
33
  session: AgentSession | null;
23
34
  sessionFile: string | null;
24
35
  createdAt: number;
package/src/sdk.ts CHANGED
@@ -34,7 +34,7 @@ import {
34
34
  Snowflake,
35
35
  } from "@oh-my-pi/pi-utils";
36
36
  import chalk from "chalk";
37
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
37
+ import { type AsyncJob, AsyncJobManager } from "./async";
38
38
  import { loadCapability } from "./capability";
39
39
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
40
40
  import { bucketRules } from "./capability/rule-buckets";
@@ -93,6 +93,7 @@ import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memor
93
93
  import type { MnemopiSessionState } from "./mnemopi/state";
94
94
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
95
95
  import lateDiagnosticTemplate from "./prompts/tools/lsp-late-diagnostic.md" with { type: "text" };
96
+ import { AgentLifecycleManager } from "./registry/agent-lifecycle";
96
97
  import { AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
97
98
  import {
98
99
  collectEnvSecrets,
@@ -1293,7 +1294,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1293
1294
  let hasSession = false;
1294
1295
  let hasRegistered = false;
1295
1296
  const enableLsp = options.enableLsp ?? true;
1296
- const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
1297
1297
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
1298
1298
  const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
1299
1299
  const ASYNC_PREVIEW_MAX_CHARS = 4_000;
@@ -1326,7 +1326,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1326
1326
  // (issue #1923). The `instance()` guard means later sessions also skip
1327
1327
  // constructing an orphaned manager that nothing would ever route to.
1328
1328
  const asyncJobManager =
1329
- backgroundJobsEnabled && !options.parentTaskPrefix && !AsyncJobManager.instance()
1329
+ !options.parentTaskPrefix && !AsyncJobManager.instance()
1330
1330
  ? new AsyncJobManager({
1331
1331
  maxRunningJobs: asyncMaxJobs,
1332
1332
  onJobComplete: async (jobId, result, job) => {
@@ -1351,6 +1351,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1351
1351
  const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
1352
1352
  const resolvedAgentDisplayName =
1353
1353
  options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
1354
+ const agentKind = (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? ("sub" as const) : ("main" as const);
1355
+ /**
1356
+ * Forget the agent ref on teardown — unless the agent is being parked (or is
1357
+ * already parked). Parking disposes the session but keeps the ref addressable
1358
+ * (history://, revive); only process teardown / explicit kill unregisters.
1359
+ */
1360
+ const unregisterUnlessParked = (): void => {
1361
+ if (agentRegistry.get(resolvedAgentId)?.status === "parked") return;
1362
+ if (AgentLifecycleManager.global().isParking(resolvedAgentId)) return;
1363
+ agentRegistry.unregister(resolvedAgentId);
1364
+ };
1354
1365
  const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
1355
1366
 
1356
1367
  try {
@@ -1409,7 +1420,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1409
1420
  getTurnBudget: () => sessionManager.getTurnBudget(),
1410
1421
  recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
1411
1422
  getClientBridge: () => session?.clientBridge,
1412
- getCompactContext: () => session.formatCompactContext(),
1413
1423
  queueDeferredDiagnostics: entry => session?.yieldQueue.enqueue(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, entry),
1414
1424
  bumpFileMutationVersion: path => {
1415
1425
  const next = (fileMutationVersions.get(path) ?? 0) + 1;
@@ -2083,7 +2093,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2083
2093
  agentRegistry.register({
2084
2094
  id: resolvedAgentId,
2085
2095
  displayName: resolvedAgentDisplayName,
2086
- kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
2096
+ kind: agentKind,
2087
2097
  parentId: options.parentTaskPrefix,
2088
2098
  session: null,
2089
2099
  sessionFile: sessionManager.getSessionFile() ?? null,
@@ -2320,7 +2330,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2320
2330
  ttsrManager,
2321
2331
  obfuscator,
2322
2332
  agentId: resolvedAgentId,
2323
- agentRegistry,
2324
2333
  providerSessionId: options.providerSessionId,
2325
2334
  parentEvalSessionId: options.parentEvalSessionId,
2326
2335
  });
@@ -2341,15 +2350,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2341
2350
 
2342
2351
  // Attach the live session to the pre-registered ref so peers can route IRC
2343
2352
  // messages here. Refresh sessionFile in case it was unavailable at pre-register
2344
- // time. The dispose wrapper below unregisters on teardown.
2353
+ // time. The dispose wrapper below unregisters on teardown (unless parked).
2345
2354
  agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
2346
2355
  {
2347
2356
  const originalDispose = session.dispose.bind(session);
2348
2357
  session.dispose = async () => {
2349
2358
  try {
2359
+ // Reject new session work (Python/eval starts) the moment disposal
2360
+ // begins — the lifecycle await below opens an async gap before
2361
+ // AgentSession.dispose() would otherwise set its guards.
2362
+ session.beginDispose();
2363
+ if (agentKind === "main") {
2364
+ // Top-level teardown owns the global agent lifecycle: park timers,
2365
+ // adopted subagent sessions, revivers. Tear it down while shared
2366
+ // resources (kernels, MCP, LSP) are still live. Subagent disposal
2367
+ // must NOT touch the global lifecycle.
2368
+ await AgentLifecycleManager.global().dispose();
2369
+ }
2350
2370
  await originalDispose();
2351
2371
  } finally {
2352
- agentRegistry.unregister(resolvedAgentId);
2372
+ unregisterUnlessParked();
2353
2373
  unsubscribeCredentialDisabled?.();
2354
2374
  }
2355
2375
  };
@@ -2502,7 +2522,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2502
2522
  if (hasSession) {
2503
2523
  await session.dispose();
2504
2524
  } else {
2505
- if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
2525
+ if (hasRegistered) unregisterUnlessParked();
2506
2526
  if (asyncJobManager) {
2507
2527
  if (AsyncJobManager.instance() === asyncJobManager) {
2508
2528
  AsyncJobManager.setInstance(undefined);