@oh-my-pi/pi-coding-agent 17.2.8 → 17.2.10

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 (246) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/dist/{CHANGELOG-0b0w17se.md → CHANGELOG-rhwzpexa.md} +94 -1
  3. package/dist/cli.js +13216 -15614
  4. package/dist/types/async/job-manager.d.ts +12 -0
  5. package/dist/types/cli/args.d.ts +1 -0
  6. package/dist/types/config/model-resolver.d.ts +13 -0
  7. package/dist/types/discovery/agents.d.ts +11 -0
  8. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  9. package/dist/types/extensibility/custom-tools/types.d.ts +1 -1
  10. package/dist/types/extensibility/extensions/loader.d.ts +2 -2
  11. package/dist/types/extensibility/extensions/types.d.ts +34 -8
  12. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  13. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +10 -0
  14. package/dist/types/hindsight/state.d.ts +0 -1
  15. package/dist/types/index.d.ts +2 -2
  16. package/dist/types/lsp/utils.d.ts +8 -0
  17. package/dist/types/main.d.ts +1 -1
  18. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  19. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  20. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  21. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  22. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  23. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  24. package/dist/types/modes/components/agent-hub.d.ts +12 -5
  25. package/dist/types/modes/components/model-browser.d.ts +9 -1
  26. package/dist/types/modes/interactive-mode.d.ts +8 -1
  27. package/dist/types/modes/session-observer-registry.d.ts +2 -0
  28. package/dist/types/registry/agent-lifecycle.d.ts +1 -1
  29. package/dist/types/registry/agent-registry.d.ts +47 -0
  30. package/dist/types/registry/persisted-agents.d.ts +3 -1
  31. package/dist/types/session/agent-session-types.d.ts +7 -0
  32. package/dist/types/session/agent-session.d.ts +8 -4
  33. package/dist/types/session/session-entries.d.ts +10 -0
  34. package/dist/types/session/session-loader.d.ts +11 -1
  35. package/dist/types/session/session-manager.d.ts +9 -1
  36. package/dist/types/session/session-tools.d.ts +10 -1
  37. package/dist/types/session/turn-recovery.d.ts +6 -2
  38. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  39. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  40. package/dist/types/task/executor.d.ts +10 -0
  41. package/dist/types/task/index.d.ts +2 -3
  42. package/dist/types/task/read-only-policy.d.ts +3 -0
  43. package/dist/types/task/structured-subagent.d.ts +2 -0
  44. package/dist/types/task/types.d.ts +4 -0
  45. package/dist/types/tools/bash.d.ts +3 -3
  46. package/dist/types/tools/browser/launch.d.ts +3 -0
  47. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  48. package/dist/types/tools/path-utils.d.ts +8 -0
  49. package/dist/types/tools/read.d.ts +3 -5
  50. package/dist/types/tools/run-scope.d.ts +27 -1
  51. package/dist/types/tools/shell-tokenize.d.ts +21 -3
  52. package/dist/types/tools/terminal-output.d.ts +1 -1
  53. package/dist/types/utils/changelog.d.ts +3 -2
  54. package/dist/types/utils/late-cleanup.d.ts +2 -0
  55. package/dist/types/utils/turndown.d.ts +1 -1
  56. package/dist/types/vibe/runtime.d.ts +4 -3
  57. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  58. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  59. package/examples/custom-tools/README.md +1 -1
  60. package/examples/extensions/README.md +1 -1
  61. package/examples/extensions/api-demo.ts +5 -6
  62. package/examples/extensions/chalk-logger.ts +1 -1
  63. package/examples/extensions/hello.ts +2 -2
  64. package/examples/extensions/reload-runtime.ts +1 -1
  65. package/examples/extensions/tools.ts +2 -2
  66. package/examples/extensions/with-deps/index.ts +1 -1
  67. package/examples/sdk/06-extensions.ts +1 -1
  68. package/package.json +15 -31
  69. package/scripts/bundle-dist.ts +1 -11
  70. package/scripts/omp +11 -2
  71. package/src/advisor/runtime.ts +54 -0
  72. package/src/async/job-manager.ts +26 -0
  73. package/src/cli/agents-cli.ts +1 -1
  74. package/src/cli/args.ts +24 -2
  75. package/src/cli/auth-broker-cli.ts +1 -1
  76. package/src/cli/auth-gateway-cli.ts +1 -1
  77. package/src/cli/bench-cli.ts +1 -1
  78. package/src/cli/claude-trace-cli.ts +1 -1
  79. package/src/cli/config-cli.ts +1 -1
  80. package/src/cli/dry-balance-cli.ts +1 -1
  81. package/src/cli/file-processor.ts +1 -1
  82. package/src/cli/flag-tables.ts +4 -0
  83. package/src/cli/grep-cli.ts +1 -1
  84. package/src/cli/grievances-cli.ts +1 -1
  85. package/src/cli/help-extra.ts +1 -1
  86. package/src/cli/models-cli.ts +1 -1
  87. package/src/cli/plugin-cli.ts +1 -1
  88. package/src/cli/read-cli.ts +1 -1
  89. package/src/cli/setup-cli.ts +1 -1
  90. package/src/cli/shell-cli.ts +1 -1
  91. package/src/cli/ssh-cli.ts +1 -1
  92. package/src/cli/stats-cli.ts +4 -4
  93. package/src/cli/tiny-models-cli.ts +1 -1
  94. package/src/cli/ttsr-cli.ts +1 -1
  95. package/src/cli/update-cli.ts +3 -57
  96. package/src/cli/usage-cli.ts +1 -1
  97. package/src/cli/web-search-cli.ts +1 -1
  98. package/src/cli/worktree-cli.ts +1 -1
  99. package/src/commands/say.ts +1 -1
  100. package/src/commands/token.ts +1 -1
  101. package/src/commit/agentic/agent.ts +1 -1
  102. package/src/commit/cli.ts +1 -1
  103. package/src/config/config-file.ts +2 -2
  104. package/src/config/keybindings.ts +3 -3
  105. package/src/config/model-discovery.ts +36 -2
  106. package/src/config/model-registry.ts +4 -1
  107. package/src/config/model-resolver.ts +54 -7
  108. package/src/discovery/agents.ts +73 -3
  109. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  110. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  111. package/src/discovery/claude.ts +8 -5
  112. package/src/discovery/cursor.ts +8 -5
  113. package/src/discovery/gemini.ts +11 -8
  114. package/src/discovery/vscode.ts +1 -0
  115. package/src/discovery/windsurf.ts +4 -2
  116. package/src/edit/hashline/filesystem.ts +7 -7
  117. package/src/eval/agent-bridge.ts +1 -3
  118. package/src/eval/jl/prelude.jl +4 -4
  119. package/src/eval/js/shared/prelude.txt +2 -2
  120. package/src/eval/py/prelude.py +0 -3
  121. package/src/eval/py/runner.py +38 -1
  122. package/src/eval/rb/prelude.rb +1 -2
  123. package/src/extensibility/custom-commands/loader.ts +4 -4
  124. package/src/extensibility/custom-commands/types.ts +5 -5
  125. package/src/extensibility/custom-tools/loader.ts +4 -4
  126. package/src/extensibility/custom-tools/types.ts +1 -1
  127. package/src/extensibility/extensions/loader.ts +7 -6
  128. package/src/extensibility/extensions/types.ts +39 -8
  129. package/src/extensibility/hooks/loader.ts +4 -5
  130. package/src/extensibility/hooks/types.ts +5 -5
  131. package/src/extensibility/legacy-pi-coding-agent-shim.ts +23 -0
  132. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  133. package/src/hindsight/state.ts +2 -22
  134. package/src/index.ts +2 -2
  135. package/src/internal-urls/memory-protocol.ts +33 -0
  136. package/src/launch/terminal-output.ts +1 -1
  137. package/src/lsp/client.ts +2 -2
  138. package/src/lsp/utils.ts +29 -0
  139. package/src/main.ts +85 -21
  140. package/src/markit/converters/docx.ts +1 -1
  141. package/src/markit/converters/epub.ts +1 -1
  142. package/src/markit/converters/pptx.ts +1 -1
  143. package/src/markit/converters/xlsx.ts +1 -1
  144. package/src/modes/acp/acp-agent.ts +7 -6
  145. package/src/modes/acp/acp-client-bridge.ts +1 -1
  146. package/src/modes/acp/acp-event-mapper.ts +1 -1
  147. package/src/modes/acp/acp-mode.ts +1 -1
  148. package/src/modes/components/agent-hub-projection.ts +248 -0
  149. package/src/modes/components/agent-hub-renderer.ts +194 -0
  150. package/src/modes/components/agent-hub.ts +670 -192
  151. package/src/modes/components/agent-transcript-viewer.ts +1 -3
  152. package/src/modes/components/assistant-message.ts +1 -1
  153. package/src/modes/components/bordered-loader.ts +1 -1
  154. package/src/modes/components/custom-editor.ts +2 -2
  155. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  156. package/src/modes/components/footer.ts +1 -3
  157. package/src/modes/components/hook-input.ts +1 -1
  158. package/src/modes/components/hook-selector.ts +2 -2
  159. package/src/modes/components/model-browser.ts +11 -3
  160. package/src/modes/components/model-hub.ts +4 -1
  161. package/src/modes/components/session-selector.ts +2 -2
  162. package/src/modes/components/status-line/segments.ts +4 -3
  163. package/src/modes/controllers/event-controller.ts +188 -7
  164. package/src/modes/controllers/extension-ui-controller.ts +4 -3
  165. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  166. package/src/modes/controllers/selector-controller.ts +40 -49
  167. package/src/modes/controllers/streaming-reveal.ts +1 -1
  168. package/src/modes/interactive-mode.ts +13 -2
  169. package/src/modes/print-mode.ts +29 -12
  170. package/src/modes/rpc/rpc-mode.ts +2 -2
  171. package/src/modes/runtime-init.ts +1 -1
  172. package/src/modes/session-observer-registry.ts +5 -0
  173. package/src/modes/theme/theme.ts +2 -2
  174. package/src/prompts/tools/computer.md +1 -1
  175. package/src/registry/agent-lifecycle.ts +35 -10
  176. package/src/registry/agent-registry.ts +65 -2
  177. package/src/registry/persisted-agents.ts +341 -16
  178. package/src/sdk.ts +11 -5
  179. package/src/session/agent-session-types.ts +8 -0
  180. package/src/session/agent-session.ts +177 -188
  181. package/src/session/session-entries.ts +10 -0
  182. package/src/session/session-listing.ts +1 -1
  183. package/src/session/session-loader.ts +74 -8
  184. package/src/session/session-manager.ts +22 -2
  185. package/src/session/session-paths.ts +105 -110
  186. package/src/session/session-tools.ts +28 -1
  187. package/src/session/settings-stream-fn.ts +7 -3
  188. package/src/session/turn-recovery.ts +236 -18
  189. package/src/session/unexpected-stop-classifier.ts +12 -3
  190. package/src/slash-commands/acp-builtins.ts +1 -1
  191. package/src/slash-commands/available-commands.ts +1 -1
  192. package/src/slash-commands/helpers/stats-dashboard.ts +2 -1
  193. package/src/task/agents.ts +1 -1
  194. package/src/task/executor.ts +173 -47
  195. package/src/task/index.ts +9 -28
  196. package/src/task/isolation-runner.ts +43 -12
  197. package/src/task/persisted-revive.ts +12 -5
  198. package/src/task/read-only-policy.ts +27 -0
  199. package/src/task/structured-subagent.ts +31 -6
  200. package/src/task/types.ts +4 -0
  201. package/src/tiny/worker.ts +1 -1
  202. package/src/tools/auto-generated-guard.ts +1 -1
  203. package/src/tools/bash-interactive.ts +4 -4
  204. package/src/tools/bash-interceptor.ts +8 -4
  205. package/src/tools/bash.ts +6 -6
  206. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  207. package/src/tools/browser/launch.ts +76 -19
  208. package/src/tools/browser/readable.ts +9 -9
  209. package/src/tools/browser/tab-supervisor.ts +12 -3
  210. package/src/tools/browser/tab-worker-entry.ts +1 -1
  211. package/src/tools/browser/tab-worker.ts +101 -12
  212. package/src/tools/fetch.ts +1 -1
  213. package/src/tools/glob.ts +4 -2
  214. package/src/tools/path-utils.ts +22 -5
  215. package/src/tools/read.ts +22 -5
  216. package/src/tools/run-scope.ts +290 -4
  217. package/src/tools/shell-tokenize.ts +38 -9
  218. package/src/tools/terminal-output.ts +1 -1
  219. package/src/tools/todo.ts +1 -1
  220. package/src/utils/changelog.ts +5 -4
  221. package/src/utils/clipboard.ts +10 -2
  222. package/src/utils/external-editor.ts +4 -2
  223. package/src/utils/jj.ts +1 -1
  224. package/src/utils/late-cleanup.ts +17 -0
  225. package/src/utils/turndown.ts +1 -2
  226. package/src/vibe/runtime.ts +202 -58
  227. package/src/web/scrapers/arxiv.ts +1 -1
  228. package/src/web/scrapers/go-pkg.ts +1 -1
  229. package/src/web/scrapers/hackage.ts +1 -12
  230. package/src/web/scrapers/iacr.ts +1 -1
  231. package/src/web/scrapers/readthedocs.ts +2 -1
  232. package/src/web/scrapers/twitter.ts +2 -2
  233. package/src/web/scrapers/types.ts +1 -1
  234. package/src/web/scrapers/wikipedia.ts +1 -1
  235. package/src/web/search/providers/browser-headers.ts +12 -39
  236. package/src/web/search/providers/codex.ts +3 -26
  237. package/src/web/search/providers/duckduckgo.ts +1 -1
  238. package/src/web/search/providers/ecosia.ts +2 -2
  239. package/src/web/search/providers/exa.ts +1 -1
  240. package/src/web/search/providers/firecrawl.ts +1 -1
  241. package/src/web/search/providers/google.ts +1 -1
  242. package/src/web/search/providers/mojeek.ts +2 -2
  243. package/src/web/search/providers/perplexity.ts +1 -1
  244. package/src/web/search/providers/public.ts +1 -1
  245. package/src/web/search/providers/startpage.ts +2 -2
  246. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -110,6 +110,23 @@ const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL)
110
110
 
111
111
  export class SelectorController {
112
112
  constructor(private ctx: InteractiveModeContext) {}
113
+ /**
114
+ * Mount a primary fullscreen menu through the one polished modal path shared
115
+ * by Settings, Model Hub, and Agent Hub.
116
+ */
117
+ #showFullscreenMenu(component: Component): OverlayHandle {
118
+ const handle = this.ctx.ui.showOverlay(component, {
119
+ anchor: "bottom-center",
120
+ width: "100%",
121
+ maxHeight: "100%",
122
+ margin: 0,
123
+ fullscreen: true,
124
+ });
125
+ this.ctx.ui.setFocus(component);
126
+ this.ctx.ui.requestRender();
127
+ return handle;
128
+ }
129
+
113
130
  #defaultRoleMutationTail = Promise.resolve();
114
131
 
115
132
  async #acquireDefaultRoleMutation(): Promise<() => void> {
@@ -242,15 +259,7 @@ export class SelectorController {
242
259
  },
243
260
  },
244
261
  );
245
- overlayHandle = this.ctx.ui.showOverlay(selector, {
246
- anchor: "bottom-center",
247
- width: "100%",
248
- maxHeight: "100%",
249
- margin: 0,
250
- fullscreen: true,
251
- });
252
- this.ctx.ui.setFocus(selector);
253
- this.ctx.ui.requestRender();
262
+ overlayHandle = this.#showFullscreenMenu(selector);
254
263
  });
255
264
  }
256
265
 
@@ -1023,15 +1032,7 @@ export class SelectorController {
1023
1032
  initialProviderId: hubOptions.initialProviderId,
1024
1033
  },
1025
1034
  );
1026
- overlayHandle = this.ctx.ui.showOverlay(hub, {
1027
- anchor: "bottom-center",
1028
- width: "100%",
1029
- maxHeight: "100%",
1030
- margin: 0,
1031
- fullscreen: true,
1032
- });
1033
- this.ctx.ui.setFocus(hub);
1034
- this.ctx.ui.requestRender();
1035
+ overlayHandle = this.#showFullscreenMenu(hub);
1035
1036
  }
1036
1037
 
1037
1038
  /** /login round-trip for a locked provider; reopen the hub on that provider only after a successful login. */
@@ -2000,27 +2001,23 @@ export class SelectorController {
2000
2001
  ...this.ctx.keybindings.getKeys("app.agents.hub"),
2001
2002
  ...this.ctx.keybindings.getKeys("app.session.observe"),
2002
2003
  ];
2003
- let hub: AgentHubOverlayComponent | undefined;
2004
-
2005
- // Render the hub inline in the editor slot — the same anchored region
2006
- // every other selector (model, session, tree, the `ask` tool) uses —
2007
- // rather than a floating overlay. A non-fullscreen overlay composited over
2008
- // a live transcript strands a stale copy in native scrollback every time a
2009
- // running subagent's progress grows the frame and scrolls the window; the
2010
- // hub is opened mid-run, so those copies stacked into a wall of duplicate
2011
- // "Agent Hub" frames bleeding the task tree behind them. As an editor-slot
2012
- // component it rides the normal append-only commit path: the transcript
2013
- // commits above it exactly once and the hub repaints in place.
2004
+ let overlayHandle: OverlayHandle | undefined;
2005
+ let closed = false;
2006
+
2014
2007
  const done = () => {
2015
- hub?.dispose();
2016
- this.ctx.editorContainer.clear();
2017
- this.ctx.editorContainer.addChild(this.ctx.editor);
2018
- this.ctx.ui.setFocus(this.ctx.editor);
2008
+ if (closed) return;
2009
+ closed = true;
2010
+ hub.dispose();
2011
+ overlayHandle?.hide();
2012
+ // A gated empty Hub may never have been mounted. Restoring editor
2013
+ // focus in that case would steal focus from a menu opened meanwhile.
2014
+ if (overlayHandle) this.focusActiveEditorArea();
2019
2015
  this.ctx.ui.requestRender();
2020
2016
  };
2021
2017
 
2022
- hub = new AgentHubOverlayComponent({
2018
+ const hub = new AgentHubOverlayComponent({
2023
2019
  observers,
2020
+ settings: this.ctx.settings,
2024
2021
  hubKeys,
2025
2022
  expandKeys: this.ctx.keybindings.getKeys("app.tools.expand"),
2026
2023
  onDone: done,
@@ -2038,30 +2035,24 @@ export class SelectorController {
2038
2035
  });
2039
2036
 
2040
2037
  const showReadyHub = () => {
2041
- // The double-← gesture passes requireContent so it stays inert when
2042
- // neither live nor persisted subagents are available. Persisted rows now
2043
- // load asynchronously, so defer the gate until that scan has refreshed the
2044
- // hub instead of treating the initial empty table as authoritative.
2038
+ if (closed) return;
2039
+ // The double-← gesture stays inert when neither live nor persisted
2040
+ // subagents are available, so wait for discovery before making the gate.
2045
2041
  if (options?.requireContent && hub.isEmpty) {
2046
- hub.dispose();
2042
+ done();
2047
2043
  return;
2048
2044
  }
2049
2045
 
2050
- this.ctx.editorContainer.clear();
2051
- this.ctx.editorContainer.addChild(hub);
2052
- this.ctx.ui.setFocus(hub);
2053
- // When the hub was raised by the editor's double-← gesture, prime its own
2054
- // close detector so the *next* single ← dismisses it — the two taps that
2055
- // opened it were consumed by the editor's detector (issue #4780).
2046
+ // Prime the detector before the first frame when the editor's double-←
2047
+ // gesture opened the hub, so the next single ← dismisses it.
2056
2048
  if (options?.armCloseTap) hub.armCloseTap();
2057
- this.ctx.ui.requestRender();
2049
+ overlayHandle = this.#showFullscreenMenu(hub);
2058
2050
  };
2059
2051
 
2060
2052
  if (options?.requireContent && hub.isEmpty) {
2061
2053
  void hub.persistedSubagentsReady.then(showReadyHub);
2062
- return;
2054
+ } else {
2055
+ showReadyHub();
2063
2056
  }
2064
-
2065
- showReadyHub();
2066
2057
  }
2067
2058
  }
@@ -1,6 +1,6 @@
1
1
  import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
2
  import { type Component, getSegmenter } from "@oh-my-pi/pi-tui";
3
- import { LRUCache } from "lru-cache/raw";
3
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
4
4
  import { formatThinkingForDisplay, hasDisplayableThinking } from "../../utils/thinking-display";
5
5
  import type { AssistantMessageComponent } from "../components/assistant-message";
6
6
 
@@ -52,7 +52,7 @@ import {
52
52
  prompt,
53
53
  setProjectDir,
54
54
  } from "@oh-my-pi/pi-utils";
55
- import chalk from "chalk";
55
+ import chalk from "@oh-my-pi/pi-utils/chalk";
56
56
  import { reset as resetCapabilities } from "../capability";
57
57
  import type { CollabGuestLink } from "../collab/guest";
58
58
  import type { CollabHost } from "../collab/host";
@@ -4141,7 +4141,14 @@ export class InteractiveMode implements InteractiveModeContext {
4141
4141
  this.ui.requestRender();
4142
4142
  }
4143
4143
 
4144
- /** Defer transcript command panels until the active turn can no longer grow above them. */
4144
+ /**
4145
+ * Defer transcript command panels while the agent is streaming, then mount
4146
+ * them at the next settle, terminal or not. A non-terminal settle is only a
4147
+ * scheduling pause, so resumed streaming can still land below a panel
4148
+ * flushed there. That is preferred over leaving it queued behind a command
4149
+ * the user runs during the pause, which mounts immediately and would put the
4150
+ * older panel out of order.
4151
+ */
4145
4152
  presentCommandOutput(content: Component | readonly Component[]): void {
4146
4153
  if (!this.session.isStreaming) {
4147
4154
  this.present(content);
@@ -4154,6 +4161,10 @@ export class InteractiveMode implements InteractiveModeContext {
4154
4161
  this.#pendingCommandOutputSessionId = sessionId;
4155
4162
  const items = Array.isArray(content) ? content : [content as Component];
4156
4163
  this.#pendingCommandOutput.push(...items);
4164
+ // No feedback here on purpose: mounting anything into the transcript
4165
+ // mid-turn (even a status line) re-renders rows below the growing live
4166
+ // block and duplicates them in native scrollback — the exact regression
4167
+ // issues #4806/#6767 pin. The queue flushes at the next settle.
4157
4168
  }
4158
4169
 
4159
4170
  /** Mount every command panel queued for the current session while the agent was streaming. */
@@ -91,11 +91,29 @@ export function printableEvent(event: AgentSessionEvent): unknown {
91
91
  export async function runPrintMode(session: AgentSession, options: PrintModeOptions): Promise<void> {
92
92
  const { mode, messages = [], initialMessage, initialImages, printThoughts } = options;
93
93
 
94
+ // process.stdout.write is fire-and-forget: a large final record (e.g. a
95
+ // multi-MB agent_end) can be dropped when the process exits before the pipe
96
+ // drains, truncating the record mid-line while the process still exits 0.
97
+ // Serialize every stdout write on the previous write's completion callback so
98
+ // records stay ordered and honor backpressure, then block shutdown on the
99
+ // tail before dispose/exit. Same truncation class as issue #5309 (issue #7635).
100
+ let stdoutTail: Promise<void> = Promise.resolve();
101
+ const writeStdoutLine = (text: string): void => {
102
+ stdoutTail = stdoutTail.then(() => {
103
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
104
+ process.stdout.write(text, err => {
105
+ if (err) reject(err);
106
+ else resolve();
107
+ });
108
+ return promise;
109
+ });
110
+ };
111
+
94
112
  // Emit session header for JSON mode
95
113
  if (mode === "json") {
96
114
  const header = session.sessionManager.getHeader();
97
115
  if (header) {
98
- process.stdout.write(`${JSON.stringify(header)}\n`);
116
+ writeStdoutLine(`${JSON.stringify(header)}\n`);
99
117
  }
100
118
  }
101
119
  // Set up extensions for print mode (no UI, no command context)
@@ -172,7 +190,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
172
190
  }
173
191
  // In JSON mode, output all events
174
192
  if (mode === "json") {
175
- process.stdout.write(`${JSON.stringify(printableEvent(event))}\n`);
193
+ writeStdoutLine(`${JSON.stringify(printableEvent(event))}\n`);
176
194
  }
177
195
  });
178
196
 
@@ -186,12 +204,14 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
186
204
  // Send initial message with attachments
187
205
  if (initialMessage !== undefined) {
188
206
  writeTextWorkingIndicator();
207
+ if (mode === "text") session.setTextOutputCommitted(false);
189
208
  await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
190
209
  }
191
210
 
192
211
  // Send remaining messages
193
212
  for (const message of messages) {
194
213
  writeTextWorkingIndicator();
214
+ if (mode === "text") session.setTextOutputCommitted(false);
195
215
  await logger.time("print:prompt:next", () => session.prompt(message));
196
216
  }
197
217
 
@@ -243,23 +263,20 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
243
263
  // Output text content
244
264
  for (const content of assistantMsg.content) {
245
265
  if (content.type === "text") {
246
- process.stdout.write(`${sanitizeText(content.text)}\n`);
266
+ writeStdoutLine(`${sanitizeText(content.text)}\n`);
247
267
  } else if (printThoughts && content.type === "thinking" && content.thinking.trim().length > 0) {
248
- process.stdout.write(`${sanitizeText(content.thinking)}\n`);
268
+ writeStdoutLine(`${sanitizeText(content.thinking)}\n`);
249
269
  }
250
270
  }
251
271
  }
272
+ session.setTextOutputCommitted(true);
252
273
  }
253
274
 
254
275
  await session.waitForAdvisorCatchup(PRINT_MODE_ADVISOR_DRAIN_TIMEOUT_MS);
255
276
 
256
- // Ensure stdout, including late JSON advisor events, is fully flushed before returning.
257
- // This prevents race conditions where the process exits before all output is written.
258
- await new Promise<void>((resolve, reject) => {
259
- process.stdout.write("", err => {
260
- if (err) reject(err);
261
- else resolve();
262
- });
263
- });
277
+ // Block shutdown until every serialized stdout write (including the final
278
+ // agent_end and late JSON advisor events) has drained; process.exit would
279
+ // otherwise discard the buffered tail and truncate the last record.
280
+ await stdoutTail;
264
281
  await session.dispose({ mnemopiConsolidateTimeoutMs: SHUTDOWN_CONSOLIDATE_BUDGET_MS });
265
282
  }
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { once } from "node:events";
14
14
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
15
- import { isZodSchema, zodToWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
15
+ import { toolWireSchema } from "@oh-my-pi/pi-ai/utils/schema";
16
16
  import { $env, isRecord, readLines, Snowflake } from "@oh-my-pi/pi-utils";
17
17
  import { reset as resetCapabilities } from "../../capability";
18
18
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
@@ -1095,7 +1095,7 @@ export async function runRpcMode(
1095
1095
  dumpTools: session.agent.state.tools.map(tool => ({
1096
1096
  name: tool.name,
1097
1097
  description: tool.description,
1098
- parameters: isZodSchema(tool.parameters) ? zodToWireSchema(tool.parameters) : tool.parameters,
1098
+ parameters: toolWireSchema(tool),
1099
1099
  examples: tool.examples,
1100
1100
  })),
1101
1101
  contextUsage: session.getContextUsage(),
@@ -84,7 +84,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
84
84
  session.sessionManager.appendLabelChange(targetId, label);
85
85
  },
86
86
  getActiveTools: () => session.getEnabledToolNames(),
87
- getAllTools: () => session.getAllToolNames(),
87
+ getAllTools: () => session.getAllToolInfos(),
88
88
  setActiveTools: (toolNames: string[]) => session.setActiveToolsByName(toolNames),
89
89
  getCommands: () => getSessionSlashCommands(session),
90
90
  setModel: model => runExtensionSetModel(session, model),
@@ -91,6 +91,11 @@ export class SessionObserverRegistry {
91
91
  this.#notifyListeners("main");
92
92
  }
93
93
 
94
+ /** Return one tracked session without copying or sorting the registry. */
95
+ getSession(id: string): ObservableSession | undefined {
96
+ return this.#sessions.get(id);
97
+ }
98
+
94
99
  getSessions(): ObservableSession[] {
95
100
  const sessions = [...this.#sessions.values()];
96
101
  sessions.sort((a, b) => {
@@ -20,8 +20,8 @@ import type {
20
20
  TerminalAppearance,
21
21
  } from "@oh-my-pi/pi-tui";
22
22
  import { adjustHsv, colorLuma, getCustomThemesDir, isEnoent, logger, relativeLuminance } from "@oh-my-pi/pi-utils";
23
- import chalk from "chalk";
24
- import { LRUCache } from "lru-cache/raw";
23
+ import chalk from "@oh-my-pi/pi-utils/chalk";
24
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
25
25
  // Embed theme JSON files at build time
26
26
  import darkThemeJson from "./dark.json" with { type: "json" };
27
27
  import { defaultThemes } from "./defaults";
@@ -16,7 +16,7 @@ Controls the host desktop with a JS script: windows, screenshots, native input,
16
16
  - Pointer `x,y` are pixels in the MOST RECENT screenshot of the SAME target (window or desktop). No screenshot of that target yet → coordinate input throws. AX coordinates (`.bounds()`, `elementAt`) are global desktop coords — two spaces, both converted automatically; never mix them.
17
17
  - Each `.ax()` of a window starts a new ref generation; refs from the current and previous snapshot stay valid, older ones throw StaleRef — re-snapshot, don't guess.
18
18
  - Input defaults to `delivery: "background"` — delivered to the target window without touching the user's focus, pointer, or window order. On macOS, keyboard input to an app with multiple windows throws `BackgroundUnavailable` because the OS accepts only a process id and could send keys to a different window; retry with `delivery: "foreground"` (briefly activates the target, acts, restores focus) or act through AX instead. Targets whose input stack drops other background events also throw `BackgroundUnavailable` naming the window class and event kind. Never assume a background action landed because no error was displayed — errors are how this surface reports failure.
19
- - Wayland only: there is no per-window background input (compositor-focus-only); use AX actions, or `delivery: "foreground"`.
19
+ - Wayland only: per-window native input and `raise()` are unavailable; use AX actions, or desktop input after focusing the target yourself.
20
20
  - `read_only: true` for pure inspection — input and mutation throw, approval is lighter.
21
21
  - Screenshots auto-display to you and save full-res to a temp path; pass `{silent: true}` in loops.
22
22
 
@@ -20,18 +20,31 @@
20
20
  * a superseded revive) can never clobber a newer same-id ref.
21
21
  */
22
22
 
23
- import { logger } from "@oh-my-pi/pi-utils";
23
+ import * as fs from "node:fs/promises";
24
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
24
25
  import type { AgentSession } from "../session/agent-session";
26
+ import { trackLateCleanup } from "../utils/late-cleanup";
25
27
  import {
26
28
  type AgentRef,
27
29
  type AgentRefExpectation,
28
30
  AgentRegistry,
31
+ getAgentTombstonePath,
29
32
  MAIN_AGENT_ID,
30
33
  type RegistryEvent,
31
34
  } from "./agent-registry";
32
35
 
33
36
  export type AgentReviver = (expected: AgentRef) => Promise<AgentSession>;
34
37
 
38
+ const AGENT_RELEASE_GRACE_MS = 5000;
39
+
40
+ async function persistAgentTombstone(sessionFile: string): Promise<void> {
41
+ try {
42
+ await fs.writeFile(getAgentTombstonePath(sessionFile), "", { encoding: "utf8", flag: "wx", mode: 0o600 });
43
+ } catch (error) {
44
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
45
+ }
46
+ }
47
+
35
48
  /**
36
49
  * Builds a reviver for a `parked` ref restored from disk (Agent Hub scan,
37
50
  * collab mirror, resumed process) that carries a sessionFile but no in-memory
@@ -379,13 +392,10 @@ export class AgentLifecycleManager {
379
392
  }
380
393
 
381
394
  if (options?.tombstone) {
382
- // Explicit kill: mark the ref terminal `aborted` and detach the session
383
- // BEFORE disposing. aborted refs must satisfy the AgentRef invariant
384
- // (session === null) so ensureLive / hub focus can never route into a
385
- // disposed session; setting the terminal status first also makes
386
- // createAgentSession's dispose wrapper (unregisterUnlessParked) preserve
387
- // the ref instead of removing it, so a later persisted-subagent rescan
388
- // skips it. The transcript file is left intact (history://<id>).
395
+ // Persist the terminal decision before detaching the session. The
396
+ // sidecar prevents a later discovery pass from reviving this transcript
397
+ // as a fresh parked ref.
398
+ if (ref.sessionFile) await persistAgentTombstone(ref.sessionFile);
389
399
  this.#registry.setStatus(id, "aborted", ref);
390
400
  }
391
401
  const live = this.#registry.get(id) === ref ? ref.session : null;
@@ -402,11 +412,26 @@ export class AgentLifecycleManager {
402
412
  }
403
413
 
404
414
  /** Teardown everything (process exit / main session dispose). */
405
- async dispose(): Promise<void> {
415
+ async dispose(deadlineAt: number = Date.now() + AGENT_RELEASE_GRACE_MS): Promise<void> {
406
416
  this.#unsubscribe?.();
407
417
  this.#unsubscribe = undefined;
408
418
  const ids = [...new Set([...this.#adopted.keys(), ...this.#parks.keys()])];
409
- await Promise.all(ids.map(id => this.release(id)));
419
+ await Promise.all(
420
+ ids.map(async id => {
421
+ const release = this.release(id).then(() => {});
422
+ try {
423
+ await untilAborted(AbortSignal.timeout(Math.max(0, deadlineAt - Date.now())), () => release);
424
+ } catch (error) {
425
+ if (Date.now() >= deadlineAt) {
426
+ trackLateCleanup(release, { id, resource: "adopted-agent" });
427
+ }
428
+ logger.warn("Agent cleanup exceeded its deadline", {
429
+ id,
430
+ error: error instanceof Error ? error.message : String(error),
431
+ });
432
+ }
433
+ }),
434
+ );
410
435
  this.#revivals.clear();
411
436
  this.#parks.clear();
412
437
  this.#persistedReviverFactory = undefined;
@@ -14,6 +14,13 @@ import { oneLineLabel } from "../task/types";
14
14
 
15
15
  export const MAIN_AGENT_ID = "Main";
16
16
 
17
+ /** Sidecar marker retained beside a child transcript after an explicit kill. */
18
+ export const AGENT_TOMBSTONE_SUFFIX = ".tombstone";
19
+
20
+ export function getAgentTombstonePath(sessionFile: string): string {
21
+ return `${sessionFile}${AGENT_TOMBSTONE_SUFFIX}`;
22
+ }
23
+
17
24
  /**
18
25
  * - `running`: a turn is in flight.
19
26
  * - `idle`: live AgentSession in memory, awaiting work. Finished agents are
@@ -22,6 +29,8 @@ export const MAIN_AGENT_ID = "Main";
22
29
  * - `aborted`: hard-killed, terminal.
23
30
  */
24
31
  export type AgentStatus = "running" | "idle" | "parked" | "aborted";
32
+ /** Provenance of a displayed duration: active runtime, transcript span, or unavailable. */
33
+ export type AgentDurationKind = "active" | "span" | "unknown";
25
34
  /**
26
35
  * - `main`/`sub`: the user-facing agent tree (driving agent + task subagents).
27
36
  * - `advisor`: a passive review transcript persisted like a subagent for usage
@@ -30,6 +39,35 @@ export type AgentStatus = "running" | "idle" | "parked" | "aborted";
30
39
  */
31
40
  export type AgentKind = "main" | "sub" | "advisor";
32
41
 
42
+ /** Persisted per-agent totals reconstructed from the child session transcript. */
43
+ export interface AgentMetricsSummary {
44
+ tokens: number;
45
+ requests: number;
46
+ tools: number;
47
+ cost: number;
48
+ durationMs: number;
49
+ durationKind?: AgentDurationKind;
50
+ contextTokens?: number;
51
+ contextWindow?: number;
52
+ }
53
+
54
+ /** Historical identity and telemetry that remain available after the live session is disposed. */
55
+ export interface AgentHistorySummary {
56
+ agent?: string;
57
+ modelRole?: string;
58
+ resolvedModel?: string;
59
+ /** Whether the last resolved model was selected by retry fallback routing. */
60
+ resolvedModelIsFallback?: boolean;
61
+ metrics?: AgentMetricsSummary;
62
+ readOnly?: boolean;
63
+ /** Durable task output artifact, when the executor wrote one. */
64
+ outputPath?: string;
65
+ /** Captured isolated-worktree patch, when patch capture succeeded. */
66
+ patchPath?: string;
67
+ /** Isolated branch identity, when branch-mode capture succeeded. */
68
+ branchName?: string;
69
+ }
70
+
33
71
  export interface AgentRef {
34
72
  id: string;
35
73
  displayName: string;
@@ -43,6 +81,8 @@ export interface AgentRef {
43
81
  lastActivity: number;
44
82
  /** Short gist of what the agent is currently doing (latest intent or tool), for the work-aware roster. Display-only. */
45
83
  activity?: string;
84
+ /** Persisted identity and telemetry restored after the live observer is gone. */
85
+ history?: AgentHistorySummary;
46
86
  }
47
87
 
48
88
  export type AgentRefExpectation = AgentRef | AgentSession;
@@ -50,6 +90,7 @@ export type AgentRefExpectation = AgentRef | AgentSession;
50
90
  export type RegistryEvent =
51
91
  | { type: "registered"; ref: AgentRef }
52
92
  | { type: "status_changed"; ref: AgentRef }
93
+ | { type: "metadata_changed"; ref: AgentRef }
53
94
  | { type: "removed"; ref: AgentRef };
54
95
 
55
96
  type RegistryListener = (event: RegistryEvent) => void;
@@ -62,6 +103,14 @@ export interface RegisterInput {
62
103
  session: AgentSession | null;
63
104
  sessionFile?: string | null;
64
105
  status?: AgentStatus;
106
+ /** Last persisted task summary, when restoring a historical agent. */
107
+ activity?: string;
108
+ /** Original registration timestamp, when known from persisted history. */
109
+ createdAt?: number;
110
+ /** Last transcript activity timestamp, when known from persisted history. */
111
+ lastActivity?: number;
112
+ /** Persisted identity and telemetry restored after the live observer is gone. */
113
+ history?: AgentHistorySummary;
65
114
  }
66
115
 
67
116
  export class AgentRegistry {
@@ -96,8 +145,10 @@ export class AgentRegistry {
96
145
  status: input.status ?? "running",
97
146
  session: input.session,
98
147
  sessionFile: input.sessionFile ?? null,
99
- createdAt: now,
100
- lastActivity: now,
148
+ createdAt: input.createdAt ?? now,
149
+ lastActivity: input.lastActivity ?? now,
150
+ activity: input.activity,
151
+ history: input.history,
101
152
  };
102
153
  this.#refs.set(ref.id, ref);
103
154
  this.#emit({ type: "registered", ref });
@@ -116,6 +167,18 @@ export class AgentRegistry {
116
167
  return current === expected && current.status === "parked" && !current.session ? current : undefined;
117
168
  }
118
169
 
170
+ /** Attach transcript-derived identity and telemetry without changing lifecycle state. */
171
+ setHistory(id: string, history: AgentHistorySummary, expectedSessionFile?: string): boolean {
172
+ const ref = this.#refs.get(id);
173
+ if (!ref || (expectedSessionFile !== undefined && ref.sessionFile !== expectedSessionFile)) return false;
174
+ const definedHistory = Object.fromEntries(
175
+ Object.entries(history).filter(([, value]) => value !== undefined),
176
+ ) as AgentHistorySummary;
177
+ ref.history = { ...ref.history, ...definedHistory };
178
+ this.#emit({ type: "metadata_changed", ref });
179
+ return true;
180
+ }
181
+
119
182
  setStatus(id: string, status: AgentStatus, expected?: AgentRefExpectation): boolean {
120
183
  const ref = this.#refs.get(id);
121
184
  if (!ref || !this.#matchesExpected(ref, expected)) return false;