@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.0

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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
@@ -1,43 +1,51 @@
1
- Launches subagents to parallelize workflows.
1
+ {{#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.
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
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}}
7
+ - Coordinate with running agents via `irc` using their ids. Agents reach you and their siblings live the same way.
13
8
  {{/if}}
14
9
 
15
- {{#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}}.
19
- {{/if}}
10
+ <lifecycle>
11
+ - 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}}
12
+ - `history://<id>` is the agent's transcript; `agent://<id>` its latest output artifact.
13
+ </lifecycle>
20
14
 
21
15
  <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}}
16
+ - `agent`: agent type to spawn
17
+ {{#if batchEnabled}}
18
+ - `context`: shared background prepended to every assignment — goal, constraints, shared contract (see context-fmt); REQUIRED, session-specific only
19
+ - `tasks`: tasks to spawnone subagent per item, all in parallel:
20
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
21
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
22
+ - `description`: UI label only subagent never sees it
23
+ {{#if isolationEnabled}}
24
+ - `isolated`: run this spawn in an isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
25
+ {{/if}}
26
+ {{else}}
27
+ - `id`: stable agent id, CamelCase, ≤32 chars; generated when omitted
28
+ - `description`: UI label only — subagent never sees it
29
+ - `assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED
30
+ {{#if isolationEnabled}}
31
+ - `isolated`: run in isolated env; returns patches. Isolated agents are torn down at completion — not addressable afterwards
32
+ {{/if}}
33
+ {{/if}}
30
34
  </parameters>
31
35
 
32
36
  <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.
37
+ - **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.
38
+ - **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
39
  - No globs, no "update all", no package-wide scope. Fan out.
36
40
  - 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}}
41
+ - 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}}.
42
+ {{#if batchEnabled}}
43
+ - **Shared background** lives in `context` once — never duplicated across assignments. Pass large payloads via `local://<path>` URIs, not inline.
44
+ {{else}}
45
+ - **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.
46
+ {{/if}}
39
47
  - 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`).
48
+ - **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
49
  - **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
50
  </rules>
43
51
 
@@ -51,9 +59,10 @@ Test: can task B run correctly without seeing A's output? If no, sequence A →
51
59
  Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
52
60
  Parallel when tasks touch disjoint files or are independent refactors/tests.
53
61
  {{/if}}
62
+ {{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
54
63
  </parallelization>
55
64
 
56
- {{#if contextEnabled}}
65
+ {{#if batchEnabled}}
57
66
  <context-fmt>
58
67
  # Goal ← one sentence: what the batch accomplishes
59
68
  # 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";
@@ -89,16 +89,18 @@ import type { HindsightSessionState } from "./hindsight/state";
89
89
  import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
90
90
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
91
91
  import { discoverAndLoadMCPTools, MCPManager, type MCPToolsLoadResult } from "./mcp";
92
- import { resolveMemoryBackend } from "./memory-backend";
92
+ import { createSessionMemoryRuntimeContext, resolveMemoryBackend } from "./memory-backend";
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,
99
100
  deobfuscateSessionContext,
100
101
  loadSecrets,
101
102
  obfuscateMessages,
103
+ obfuscateProviderContext,
102
104
  SecretObfuscator,
103
105
  } from "./secrets";
104
106
  import { AgentSession } from "./session/agent-session";
@@ -134,6 +136,7 @@ import {
134
136
  parseThinkingLevel,
135
137
  resolveProvisionalAutoLevel,
136
138
  resolveThinkingLevelForModel,
139
+ shouldDisableReasoning,
137
140
  toReasoningEffort,
138
141
  } from "./thinking";
139
142
  import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode";
@@ -1291,7 +1294,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1291
1294
  let hasSession = false;
1292
1295
  let hasRegistered = false;
1293
1296
  const enableLsp = options.enableLsp ?? true;
1294
- const backgroundJobsEnabled = isBackgroundJobSupportEnabled(settings);
1295
1297
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
1296
1298
  const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
1297
1299
  const ASYNC_PREVIEW_MAX_CHARS = 4_000;
@@ -1324,7 +1326,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1324
1326
  // (issue #1923). The `instance()` guard means later sessions also skip
1325
1327
  // constructing an orphaned manager that nothing would ever route to.
1326
1328
  const asyncJobManager =
1327
- backgroundJobsEnabled && !options.parentTaskPrefix && !AsyncJobManager.instance()
1329
+ !options.parentTaskPrefix && !AsyncJobManager.instance()
1328
1330
  ? new AsyncJobManager({
1329
1331
  maxRunningJobs: asyncMaxJobs,
1330
1332
  onJobComplete: async (jobId, result, job) => {
@@ -1349,6 +1351,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1349
1351
  const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
1350
1352
  const resolvedAgentDisplayName =
1351
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
+ };
1352
1365
  const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
1353
1366
 
1354
1367
  try {
@@ -1407,7 +1420,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1407
1420
  getTurnBudget: () => sessionManager.getTurnBudget(),
1408
1421
  recordEvalSubagentUsage: output => sessionManager.recordEvalSubagentOutput(output),
1409
1422
  getClientBridge: () => session?.clientBridge,
1410
- getCompactContext: () => session.formatCompactContext(),
1411
1423
  queueDeferredDiagnostics: entry => session?.yieldQueue.enqueue(LSP_LATE_DIAGNOSTIC_MESSAGE_TYPE, entry),
1412
1424
  bumpFileMutationVersion: path => {
1413
1425
  const next = (fileMutationVersions.get(path) ?? 0) + 1;
@@ -1791,6 +1803,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1791
1803
  cwd,
1792
1804
  sessionManager,
1793
1805
  modelRegistry,
1806
+ () => (hasSession ? createSessionMemoryRuntimeContext(session, agentDir, cwd) : undefined),
1794
1807
  );
1795
1808
 
1796
1809
  credentialDisabledTarget = extensionRunner;
@@ -2080,7 +2093,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2080
2093
  agentRegistry.register({
2081
2094
  id: resolvedAgentId,
2082
2095
  displayName: resolvedAgentDisplayName,
2083
- kind: (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main",
2096
+ kind: agentKind,
2084
2097
  parentId: options.parentTaskPrefix,
2085
2098
  session: null,
2086
2099
  sessionFile: sessionManager.getSessionFile() ?? null,
@@ -2138,6 +2151,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2138
2151
  if (!obfuscator?.hasSecrets()) return converted;
2139
2152
  return obfuscateMessages(obfuscator, converted);
2140
2153
  };
2154
+
2141
2155
  const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
2142
2156
  const withContext = await extensionRunner.emitContext(messages);
2143
2157
  return wrapSteeringForModel(withContext);
@@ -2173,6 +2187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2173
2187
  systemPrompt,
2174
2188
  model,
2175
2189
  thinkingLevel: toReasoningEffort(effectiveThinkingLevel),
2190
+ disableReasoning: shouldDisableReasoning(effectiveThinkingLevel),
2176
2191
  tools: initialTools,
2177
2192
  },
2178
2193
  convertToLlm: convertToLlmFinal,
@@ -2181,6 +2196,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2181
2196
  sessionId: providerSessionId,
2182
2197
  promptCacheKey: options.providerPromptCacheKey,
2183
2198
  transformContext,
2199
+ transformProviderContext: obfuscator ? context => obfuscateProviderContext(obfuscator, context) : undefined,
2184
2200
  steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
2185
2201
  followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
2186
2202
  interruptMode: settings.get("interruptMode") ?? "immediate",
@@ -2267,6 +2283,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2267
2283
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
2268
2284
  sessionManager,
2269
2285
  settings,
2286
+ autoApprove: options.autoApprove,
2270
2287
  evalKernelOwnerId,
2271
2288
  // Defined only for top-level sessions (creation is gated above).
2272
2289
  // AgentSession uses this to decide whether it may dispose the global
@@ -2313,7 +2330,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2313
2330
  ttsrManager,
2314
2331
  obfuscator,
2315
2332
  agentId: resolvedAgentId,
2316
- agentRegistry,
2317
2333
  providerSessionId: options.providerSessionId,
2318
2334
  parentEvalSessionId: options.parentEvalSessionId,
2319
2335
  });
@@ -2334,15 +2350,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2334
2350
 
2335
2351
  // Attach the live session to the pre-registered ref so peers can route IRC
2336
2352
  // messages here. Refresh sessionFile in case it was unavailable at pre-register
2337
- // time. The dispose wrapper below unregisters on teardown.
2353
+ // time. The dispose wrapper below unregisters on teardown (unless parked).
2338
2354
  agentRegistry.attachSession(resolvedAgentId, session, sessionManager.getSessionFile() ?? null);
2339
2355
  {
2340
2356
  const originalDispose = session.dispose.bind(session);
2341
2357
  session.dispose = async () => {
2342
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
+ }
2343
2370
  await originalDispose();
2344
2371
  } finally {
2345
- agentRegistry.unregister(resolvedAgentId);
2372
+ unregisterUnlessParked();
2346
2373
  unsubscribeCredentialDisabled?.();
2347
2374
  }
2348
2375
  };
@@ -2495,7 +2522,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2495
2522
  if (hasSession) {
2496
2523
  await session.dispose();
2497
2524
  } else {
2498
- if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
2525
+ if (hasRegistered) unregisterUnlessParked();
2499
2526
  if (asyncJobManager) {
2500
2527
  if (AsyncJobManager.instance() === asyncJobManager) {
2501
2528
  AsyncJobManager.setInstance(undefined);
@@ -4,7 +4,14 @@ import { YAML } from "bun";
4
4
  import type { SecretEntry } from "./obfuscator";
5
5
  import { compileSecretRegex } from "./regex";
6
6
 
7
- export { deobfuscateSessionContext, obfuscateMessages, type SecretEntry, SecretObfuscator } from "./obfuscator";
7
+ export {
8
+ deobfuscateSessionContext,
9
+ obfuscateMessages,
10
+ obfuscateProviderContext,
11
+ obfuscateProviderTools,
12
+ type SecretEntry,
13
+ SecretObfuscator,
14
+ } from "./obfuscator";
8
15
 
9
16
  /**
10
17
  * Load secrets from project-local and global secrets.yml files.
@@ -1,4 +1,5 @@
1
- import type { Message, TextContent } from "@oh-my-pi/pi-ai";
1
+ import type { Context, Message, Tool } from "@oh-my-pi/pi-ai";
2
+ import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
2
3
  import type { SessionContext } from "../session/session-manager";
3
4
  import { compileSecretRegex } from "./regex";
4
5
 
@@ -184,6 +185,12 @@ export class SecretObfuscator {
184
185
  return deepWalkStrings(obj, s => this.deobfuscate(s));
185
186
  }
186
187
 
188
+ /** Deep-walk an object, obfuscating all string values. */
189
+ obfuscateObject<T>(obj: T): T {
190
+ if (!this.#hasAny) return obj;
191
+ return deepWalkStrings(obj, s => this.obfuscate(s));
192
+ }
193
+
187
194
  /** Find the obfuscate index for a known secret value. */
188
195
  #findObfuscateIndex(secret: string): number | undefined {
189
196
  // Check plain mappings first
@@ -211,25 +218,34 @@ export function deobfuscateSessionContext(
211
218
  // Message obfuscation (outbound to LLM)
212
219
  // ═══════════════════════════════════════════════════════════════════════════
213
220
 
214
- /** Obfuscate all text content in LLM messages (for outbound interception). */
221
+ /** Obfuscate all string content in LLM messages (for outbound interception). */
215
222
  export function obfuscateMessages(obfuscator: SecretObfuscator, messages: Message[]): Message[] {
216
- return messages.map(msg => {
217
- if (!Array.isArray(msg.content)) return msg;
223
+ return obfuscator.obfuscateObject(messages);
224
+ }
218
225
 
219
- let changed = false;
220
- const content = msg.content.map(block => {
221
- if (block.type === "text") {
222
- const obfuscated = obfuscator.obfuscate(block.text);
223
- if (obfuscated !== block.text) {
224
- changed = true;
225
- return { ...block, text: obfuscated } as TextContent;
226
- }
227
- }
228
- return block;
229
- });
226
+ /** Obfuscate provider request context without walking live tool schema instances. */
227
+ export function obfuscateProviderContext(obfuscator: SecretObfuscator | undefined, context: Context): Context {
228
+ if (!obfuscator?.hasSecrets()) return context;
229
+ return {
230
+ ...context,
231
+ systemPrompt: obfuscator.obfuscateObject(context.systemPrompt),
232
+ messages: obfuscator.obfuscateObject(context.messages),
233
+ tools: obfuscateProviderTools(obfuscator, context.tools),
234
+ };
235
+ }
230
236
 
231
- return changed ? ({ ...msg, content } as typeof msg) : msg;
232
- });
237
+ /** Convert tool schemas to wire JSON Schema before obfuscating provider-visible strings. */
238
+ export function obfuscateProviderTools(
239
+ obfuscator: SecretObfuscator | undefined,
240
+ tools: Tool[] | undefined,
241
+ ): Tool[] | undefined {
242
+ if (!tools || !obfuscator?.hasSecrets()) return tools;
243
+ return tools.map(tool => ({
244
+ ...tool,
245
+ description: obfuscator.obfuscate(tool.description),
246
+ parameters: obfuscator.obfuscateObject(toolWireSchema(tool)),
247
+ customFormat: tool.customFormat ? obfuscator.obfuscateObject(tool.customFormat) : undefined,
248
+ }));
233
249
  }
234
250
 
235
251
  // ═══════════════════════════════════════════════════════════════════════════
@@ -262,7 +278,7 @@ function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
262
278
  });
263
279
  return (changed ? result : obj) as unknown as T;
264
280
  }
265
- if (obj !== null && typeof obj === "object") {
281
+ if (obj !== null && typeof obj === "object" && isPlainRecord(obj)) {
266
282
  let changed = false;
267
283
  const result: Record<string, unknown> = {};
268
284
  for (const key of Object.keys(obj)) {
@@ -275,3 +291,8 @@ function deepWalkStrings<T>(obj: T, transform: (s: string) => string): T {
275
291
  }
276
292
  return obj;
277
293
  }
294
+
295
+ function isPlainRecord(obj: object): obj is Record<string, unknown> {
296
+ const prototype = Object.getPrototypeOf(obj);
297
+ return prototype === Object.prototype || prototype === null;
298
+ }