@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
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { $which, getPuppeteerDir, logger, removeWithRetries } from "@oh-my-pi/pi-utils";
5
- import type * as BrowsersNs from "@puppeteer/browsers";
5
+ import type * as BrowsersNs from "@oh-my-pi/pi-utils/browsers";
6
6
  import type { Browser, CDPSession, Page, default as Puppeteer, Target } from "puppeteer-core";
7
7
  import stealthTamperingScript from "../puppeteer/00_stealth_tampering.txt" with { type: "text" };
8
8
  import stealthActivityScript from "../puppeteer/01_stealth_activity.txt" with { type: "text" };
@@ -115,15 +115,15 @@ export async function loadPuppeteerInWorker(safeDir: string): Promise<typeof Pup
115
115
  let browsersModule: typeof BrowsersNs | undefined;
116
116
  async function loadBrowsers(): Promise<typeof BrowsersNs> {
117
117
  if (!browsersModule) {
118
- browsersModule = await import("@puppeteer/browsers");
118
+ browsersModule = await import("@oh-my-pi/pi-utils/browsers");
119
119
  }
120
120
  return browsersModule;
121
121
  }
122
122
 
123
123
  /**
124
- * Resolve the Chromium executable puppeteer will launch, lazily downloading it
125
- * on first use via @puppeteer/browsers. Skipped when a system Chromium (NixOS)
126
- * or PUPPETEER_EXECUTABLE_PATH is set. The browser is cached under
124
+ * Resolve the Chromium executable puppeteer will launch, honoring
125
+ * PUPPETEER_EXECUTABLE_PATH before system browser detection and lazily
126
+ * downloading Chromium otherwise. The browser is cached under
127
127
  * ~/.omp/puppeteer (getPuppeteerDir). Returns undefined when platform
128
128
  * detection fails (puppeteer default resolution takes over). Exported so
129
129
  * real-browser tests can probe launchability and skip on hosts missing
@@ -131,10 +131,10 @@ async function loadBrowsers(): Promise<typeof BrowsersNs> {
131
131
  */
132
132
  let chromiumExecutablePromise: Promise<string | undefined> | undefined;
133
133
  export async function ensureChromiumExecutable(): Promise<string | undefined> {
134
- const sysChrome = resolveSystemChromium();
135
- if (sysChrome) return sysChrome;
136
134
  const envPath = process.env.PUPPETEER_EXECUTABLE_PATH;
137
135
  if (envPath) return envPath;
136
+ const sysChrome = await resolveSystemChromium();
137
+ if (sysChrome) return sysChrome;
138
138
  if (chromiumExecutablePromise) return chromiumExecutablePromise;
139
139
 
140
140
  chromiumExecutablePromise = (async () => {
@@ -166,13 +166,13 @@ export async function ensureChromiumExecutable(): Promise<string | undefined> {
166
166
  buildId,
167
167
  cacheDir,
168
168
  platform,
169
- downloadProgressCallback: (downloaded, total) => {
170
- if (total <= 0) return;
171
- const pct = Math.floor((downloaded / total) * 100);
172
- if (pct >= lastReportedPercent + 10 || downloaded === total) {
169
+ downloadProgressCallback: ({ downloadedBytes, totalBytes }) => {
170
+ if (totalBytes <= 0) return;
171
+ const pct = Math.floor((downloadedBytes / totalBytes) * 100);
172
+ if (pct >= lastReportedPercent + 10 || downloadedBytes === totalBytes) {
173
173
  lastReportedPercent = pct;
174
174
  logger.debug(
175
- `Chromium download: ${pct}% (${Math.round(downloaded / 1_000_000)} / ${Math.round(total / 1_000_000)} MB)`,
175
+ `Chromium download: ${pct}% (${Math.round(downloadedBytes / 1_000_000)} / ${Math.round(totalBytes / 1_000_000)} MB)`,
176
176
  );
177
177
  }
178
178
  },
@@ -193,16 +193,47 @@ let resolvedChromium: string | null | undefined; // undefined = unchecked; null
193
193
  function isExecutableFile(p: string): boolean {
194
194
  try {
195
195
  const st = fs.statSync(p);
196
- return st.isFile();
196
+ if (!st.isFile()) return false;
197
+ if (process.platform === "win32") return true;
198
+ fs.accessSync(p, fs.constants.X_OK);
199
+ return true;
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ async function isChromiumExecutable(p: string): Promise<boolean> {
206
+ if (!isExecutableFile(p)) return false;
207
+ try {
208
+ const probeTimeoutMs = 3000;
209
+ const proc = Bun.spawn([p, "--version"], {
210
+ stdout: "pipe",
211
+ stderr: "ignore",
212
+ signal: AbortSignal.timeout(probeTimeoutMs),
213
+ killSignal: "SIGKILL",
214
+ });
215
+ const stdout = await Promise.race([
216
+ new Response(proc.stdout).text(),
217
+ Bun.sleep(probeTimeoutMs + 500).then(() => null),
218
+ ]);
219
+ if (stdout === null) return false;
220
+ await proc.exited;
221
+ return proc.exitCode === 0 && /Chrom|Edg/i.test(stdout);
197
222
  } catch {
198
223
  return false;
199
224
  }
200
225
  }
201
226
 
202
- function systemChromiumCandidates(): string[] {
203
- const home = os.homedir();
227
+ /** Flatpak application id published by the Ungoogled Chromium project. */
228
+ const UNGOOGLED_CHROMIUM_FLATPAK_ID = "io.github.ungoogled_software.ungoogled_chromium";
229
+
230
+ function systemChromiumCandidates(
231
+ platform: NodeJS.Platform = process.platform,
232
+ home = os.homedir(),
233
+ which: (name: string) => string | null | undefined = $which,
234
+ ): string[] {
204
235
  const candidates: string[] = [];
205
- switch (process.platform) {
236
+ switch (platform) {
206
237
  case "darwin": {
207
238
  for (const root of ["/Applications", path.join(home, "Applications")]) {
208
239
  candidates.push(
@@ -219,7 +250,7 @@ function systemChromiumCandidates(): string[] {
219
250
  case "linux": {
220
251
  const names = ["google-chrome-stable", "google-chrome", "chromium", "chromium-browser", "chrome"];
221
252
  for (const name of names) {
222
- const found = $which(name);
253
+ const found = which(name);
223
254
  if (found) candidates.push(found);
224
255
  }
225
256
  candidates.push(
@@ -238,6 +269,19 @@ function systemChromiumCandidates(): string[] {
238
269
  if (onNixos) {
239
270
  candidates.push(path.join(home, ".nix-profile/bin/chromium"), "/run/current-system/sw/bin/chromium");
240
271
  }
272
+ for (const name of ["ungoogled-chromium", "ungoogled-chromium-browser"]) {
273
+ const found = which(name);
274
+ if (found) candidates.push(found);
275
+ }
276
+ candidates.push(
277
+ // Ungoogled Chromium. Distro and AUR packages that keep the plain
278
+ // `chromium` name are already covered above; these are the paths
279
+ // unique to it, including the system and per-user Flatpak shims.
280
+ "/usr/bin/ungoogled-chromium",
281
+ "/usr/bin/ungoogled-chromium-browser",
282
+ `/var/lib/flatpak/exports/bin/${UNGOOGLED_CHROMIUM_FLATPAK_ID}`,
283
+ path.join(home, ".local/share/flatpak/exports/bin", UNGOOGLED_CHROMIUM_FLATPAK_ID),
284
+ );
241
285
  break;
242
286
  }
243
287
  case "win32": {
@@ -259,13 +303,13 @@ function systemChromiumCandidates(): string[] {
259
303
  return candidates;
260
304
  }
261
305
 
262
- function resolveSystemChromium(): string | undefined {
306
+ async function resolveSystemChromium(): Promise<string | undefined> {
263
307
  if (resolvedChromium !== undefined) return resolvedChromium ?? undefined;
264
308
  const seen = new Set<string>();
265
309
  for (const candidate of systemChromiumCandidates()) {
266
310
  if (!candidate || seen.has(candidate)) continue;
267
311
  seen.add(candidate);
268
- if (isExecutableFile(candidate)) {
312
+ if (await isChromiumExecutable(candidate)) {
269
313
  resolvedChromium = candidate;
270
314
  logger.debug("Using system Chrome/Chromium", { path: candidate });
271
315
  return candidate;
@@ -866,6 +910,19 @@ export async function applyStealthPatches(
866
910
  await injectStealthScripts(page);
867
911
  }
868
912
 
913
+ /** Exposes executable candidates for detection tests. */
914
+ export function systemChromiumCandidatesForTest(
915
+ platform: NodeJS.Platform = process.platform,
916
+ home?: string,
917
+ which?: (name: string) => string | null | undefined,
918
+ ): string[] {
919
+ return systemChromiumCandidates(platform, home, which);
920
+ }
921
+
922
+ export async function chromiumExecutableProbeForTest(executablePath: string): Promise<boolean> {
923
+ return isChromiumExecutable(executablePath);
924
+ }
925
+
869
926
  export function stealthIgnoreDefaultArgsForTest(executablePath: string | undefined): string[] {
870
927
  return stealthIgnoreDefaultArgs(executablePath);
871
928
  }
@@ -1,5 +1,5 @@
1
- import type * as ReadabilityNs from "@mozilla/readability";
2
- import type * as LinkedomNs from "linkedom";
1
+ import type * as DomNs from "@oh-my-pi/pi-utils/dom";
2
+ import type * as ReadabilityNs from "@oh-my-pi/pi-utils/readability";
3
3
  import { htmlToBasicMarkdown } from "../../web/scrapers/types";
4
4
 
5
5
  export type ReadableFormat = "text" | "markdown";
@@ -23,17 +23,17 @@ function normalize(text: string | null | undefined): string | undefined {
23
23
  let readabilityModule: typeof ReadabilityNs | undefined;
24
24
  async function loadReadability(): Promise<typeof ReadabilityNs> {
25
25
  if (!readabilityModule) {
26
- readabilityModule = await import("@mozilla/readability");
26
+ readabilityModule = await import("@oh-my-pi/pi-utils/readability");
27
27
  }
28
28
  return readabilityModule;
29
29
  }
30
30
 
31
- let linkedomModule: typeof LinkedomNs | undefined;
32
- async function loadLinkedom(): Promise<typeof LinkedomNs> {
33
- if (!linkedomModule) {
34
- linkedomModule = await import("linkedom");
31
+ let domModule: typeof DomNs | undefined;
32
+ async function loadDom(): Promise<typeof DomNs> {
33
+ if (!domModule) {
34
+ domModule = await import("@oh-my-pi/pi-utils/dom");
35
35
  }
36
- return linkedomModule;
36
+ return domModule;
37
37
  }
38
38
 
39
39
  /**
@@ -47,7 +47,7 @@ export async function extractReadableFromHtml(
47
47
  url: string,
48
48
  format: ReadableFormat,
49
49
  ): Promise<ReadableResult | null> {
50
- const [{ parseHTML }, { Readability }] = await Promise.all([loadLinkedom(), loadReadability()]);
50
+ const [{ parseHTML }, { Readability }] = await Promise.all([loadDom(), loadReadability()]);
51
51
  const { document } = parseHTML(html);
52
52
 
53
53
  // --- Primary: Readability article extraction ---
@@ -134,6 +134,7 @@ const tabs = new Map<string, TabSession>();
134
134
  // awaits) cannot interleave and leak a worker + browser refCount.
135
135
  const acquireChains = new Map<string, Promise<void>>();
136
136
  const GRACE_MS = 750;
137
+ const WORKER_INIT_TIMEOUT_MS = 15_000;
137
138
  // Names of tabs the supervisor force-killed (timeout past grace, failed recycle),
138
139
  // mapped to the kill reason. Lets the next `run` on that name explain WHY the tab
139
140
  // vanished instead of a bare "not alive". Cleared when the name is opened again.
@@ -272,7 +273,11 @@ async function acquireTabImpl(
272
273
  }
273
274
  let info: ReadyInfo;
274
275
  try {
275
- info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
276
+ info = await initializeTabWorker(
277
+ worker,
278
+ initPayload,
279
+ Math.max(WORKER_INIT_TIMEOUT_MS, opts.timeoutMs + GRACE_MS),
280
+ );
276
281
  } catch (error) {
277
282
  // `BuildMessage`-class failures arrive asynchronously via the worker's `error` event,
278
283
  // after `spawnTabWorker`'s synchronous try/catch has already returned. Fall back to
@@ -287,7 +292,11 @@ async function acquireTabImpl(
287
292
  });
288
293
  worker = await spawnInlineWorker();
289
294
  try {
290
- info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
295
+ info = await initializeTabWorker(
296
+ worker,
297
+ initPayload,
298
+ Math.max(WORKER_INIT_TIMEOUT_MS, opts.timeoutMs + GRACE_MS),
299
+ );
291
300
  } catch (inlineError) {
292
301
  await worker.terminate().catch(() => undefined);
293
302
  if (tempHold || browser.refCount === 0) await releaseBrowser(browser, { kill: false });
@@ -1014,7 +1023,7 @@ async function spawnInlineWorker(): Promise<WorkerHandle> {
1014
1023
  close: () => {},
1015
1024
  };
1016
1025
  const { WorkerCore } = await import("./tab-worker");
1017
- new WorkerCore(workerTransport);
1026
+ new WorkerCore(workerTransport, false);
1018
1027
  return {
1019
1028
  mode: "inline",
1020
1029
  send: msg =>
@@ -26,4 +26,4 @@ const transport: Transport = {
26
26
  },
27
27
  };
28
28
 
29
- new WorkerCore(transport);
29
+ new WorkerCore(transport, true);
@@ -3,7 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
 
5
5
  import { postmortem, Snowflake, untilAborted, withTimeout } from "@oh-my-pi/pi-utils";
6
- import type { HTMLElement } from "linkedom";
6
+ import type { HTMLElement } from "@oh-my-pi/pi-utils/dom";
7
7
  import type {
8
8
  Browser,
9
9
  CDPSession,
@@ -23,10 +23,15 @@ import { formatScreenshot } from "../render-utils";
23
23
  import {
24
24
  bindRunFacade,
25
25
  CELL_BUDGET_SLACK_MS,
26
+ installBrowserWorkerRejectionGuard,
27
+ isBrowserRunOwnedRejection,
28
+ markBrowserRunRejection,
26
29
  markHandled,
30
+ observeBrowserRunPromise,
27
31
  resolvePredicateTimeout,
28
32
  type WaitPredicateOptions,
29
33
  waitForRun,
34
+ withBrowserPromiseCombinatorTracking,
30
35
  } from "../run-scope";
31
36
  import { ToolAbortError, ToolError, throwIfAborted } from "../tool-errors";
32
37
  import {
@@ -44,6 +49,7 @@ import {
44
49
  loadPuppeteerInWorker,
45
50
  } from "./launch";
46
51
  import { extractReadableFromHtml, type ReadableFormat } from "./readable";
52
+
47
53
  import { cloneSafe, RunOutput } from "./run-output";
48
54
  import type {
49
55
  Observation,
@@ -701,6 +707,9 @@ interface ActiveRun {
701
707
  output: RunOutput;
702
708
  screenshots: ScreenshotResult[];
703
709
  pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
710
+ rejectionOwner: object;
711
+ floatingRejections: unknown[];
712
+ floatingFailure: { promise: Promise<never>; reject(reason?: unknown): void };
704
713
  /** Helper invocations currently awaiting the page/network, keyed by op id. */
705
714
  inflight: Map<number, InflightOp>;
706
715
  opCounter: number;
@@ -748,17 +757,75 @@ export class WorkerCore {
748
757
  #active: ActiveRun | null = null;
749
758
  #runtime: JsRuntime | null = null;
750
759
  #unsub: () => void;
760
+ #isolated: boolean;
761
+ #uninstallRejectionGuard: () => void;
751
762
  #mode?: WorkerInitPayload["mode"];
752
763
  #activateForScreenshot = true;
753
764
  #dialogPolicy?: DialogPolicy;
754
765
  #dialogHandler?: (dialog: Dialog) => void;
755
766
  #openDialog?: OpenDialogInfo;
756
767
 
757
- constructor(transport: Transport) {
768
+ constructor(transport: Transport, isolated: boolean) {
758
769
  this.#transport = transport;
770
+ this.#isolated = isolated;
759
771
  this.#unsub = this.#transport.onMessage(msg => {
760
772
  void this.#handleMessage(msg as WorkerInbound);
761
773
  });
774
+ this.#uninstallRejectionGuard = this.#installRejectionGuard();
775
+ }
776
+
777
+ #installRejectionGuard(): () => void {
778
+ if (!this.#isolated) {
779
+ return postmortem.interceptUnhandledRejections(reason => this.#consumeUnhandledRejection(reason));
780
+ }
781
+ return installBrowserWorkerRejectionGuard(reason => this.#consumeUnhandledRejection(reason));
782
+ }
783
+
784
+ #consumeUnhandledRejection(reason: unknown): boolean {
785
+ const active = this.#active;
786
+ if (!active) return false;
787
+ if (!isBrowserRunOwnedRejection(reason, active.rejectionOwner, `browser-run-${active.id}.js`)) return false;
788
+ this.#recordFloatingRejection(active, reason);
789
+ return true;
790
+ }
791
+
792
+ #recordFloatingRejection(active: ActiveRun, reason: unknown): void {
793
+ if (postmortem.isExpectedCleanupError(reason)) return;
794
+ if (this.#active !== active) {
795
+ this.#log("warn", "Unhandled rejection after browser run ended", {
796
+ runId: active.id,
797
+ error: reason instanceof Error ? reason.message : String(reason),
798
+ });
799
+ return;
800
+ }
801
+ const isFirst = active.floatingRejections.length === 0;
802
+ active.floatingRejections.push(reason);
803
+ if (isFirst) active.floatingFailure.reject(this.#floatingRejectionError(reason));
804
+ }
805
+
806
+ #floatingRejectionError(reason: unknown): Error {
807
+ const message = reason instanceof Error ? reason.message : String(reason);
808
+ const error = new Error(`Unhandled rejection (missing await?): ${message}`, { cause: reason });
809
+ if (reason instanceof Error) error.name = reason.name;
810
+ return error;
811
+ }
812
+
813
+ #foldFloatingRejections(active: ActiveRun, failure: { error: unknown } | undefined): { error: unknown } | undefined {
814
+ const rejections = active.floatingRejections;
815
+ if (rejections.length === 0) return failure;
816
+ let reported = rejections;
817
+ if (!failure) {
818
+ failure = { error: this.#floatingRejectionError(rejections[0]) };
819
+ reported = rejections.slice(1);
820
+ } else if (failure.error instanceof Error && failure.error.cause === rejections[0]) {
821
+ reported = rejections.slice(1);
822
+ }
823
+ for (const reason of reported) {
824
+ this.#log("warn", "Additional unhandled browser-run rejection", {
825
+ error: reason instanceof Error ? reason.message : String(reason),
826
+ });
827
+ }
828
+ return failure;
762
829
  }
763
830
 
764
831
  nextElementId(): number {
@@ -973,6 +1040,7 @@ export class WorkerCore {
973
1040
  const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
974
1041
  const output = new RunOutput();
975
1042
  const screenshots: ScreenshotResult[] = [];
1043
+ const floatingFailure = Promise.withResolvers<never>();
976
1044
  const active: ActiveRun = {
977
1045
  id: msg.id,
978
1046
  ac,
@@ -980,6 +1048,9 @@ export class WorkerCore {
980
1048
  output,
981
1049
  screenshots,
982
1050
  pendingTools: new Map(),
1051
+ rejectionOwner: {},
1052
+ floatingRejections: [],
1053
+ floatingFailure,
983
1054
  inflight: new Map(),
984
1055
  opCounter: 0,
985
1056
  };
@@ -995,10 +1066,11 @@ export class WorkerCore {
995
1066
  const tabApi = this.#createTabApi(msg.name, msg.timeoutMs, signal, msg.session, output, screenshots, active);
996
1067
  const runtime = this.#ensureRuntime(msg.session);
997
1068
  runtime.setCwd(msg.session.cwd);
1069
+ const onFloatingRejection = (reason: unknown): void => this.#recordFloatingRejection(active, reason);
998
1070
  runtime.setRunScope({
999
- page: bindRunFacade(runPage.page, signal),
1000
- browser: bindRunFacade(browser, signal),
1001
- tab: bindRunFacade(tabApi, signal),
1071
+ page: bindRunFacade(runPage.page, signal, active.rejectionOwner, onFloatingRejection),
1072
+ browser: bindRunFacade(browser, signal, active.rejectionOwner, onFloatingRejection),
1073
+ tab: bindRunFacade(tabApi, signal, active.rejectionOwner, onFloatingRejection),
1002
1074
  assert: (cond: unknown, text?: string): void => {
1003
1075
  if (!cond) throw new ToolError(text ?? "Assertion failed");
1004
1076
  },
@@ -1010,10 +1082,12 @@ export class WorkerCore {
1010
1082
  typeof msOrPredicate === "number"
1011
1083
  ? undefined
1012
1084
  : { timeout: resolvePredicateTimeout(msg.timeoutMs, opts?.timeout), interval: opts?.interval };
1013
- return markHandled(
1085
+ return observeBrowserRunPromise(
1014
1086
  this.#runOp(active, label, signal, Number.POSITIVE_INFINITY, sig =>
1015
1087
  waitForRun(msOrPredicate, sig, resolved),
1016
1088
  ),
1089
+ active.rejectionOwner,
1090
+ onFloatingRejection,
1017
1091
  );
1018
1092
  },
1019
1093
  });
@@ -1051,10 +1125,19 @@ export class WorkerCore {
1051
1125
  try {
1052
1126
  const hooks = this.#hooksForActiveRun();
1053
1127
  if (!hooks) throw new ToolError("Browser runtime started without an active run");
1054
- returnValue = await Promise.race([
1055
- runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, { runId: msg.id, cwd: msg.session.cwd }),
1056
- cancelRejection,
1057
- ]);
1128
+ returnValue = await withBrowserPromiseCombinatorTracking(
1129
+ active.rejectionOwner,
1130
+ onFloatingRejection,
1131
+ async () =>
1132
+ await Promise.race([
1133
+ runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, {
1134
+ runId: msg.id,
1135
+ cwd: msg.session.cwd,
1136
+ }),
1137
+ cancelRejection,
1138
+ floatingFailure.promise,
1139
+ ]),
1140
+ );
1058
1141
  completed = true;
1059
1142
  } finally {
1060
1143
  signal.removeEventListener("abort", onCancel);
@@ -1063,11 +1146,13 @@ export class WorkerCore {
1063
1146
  failure = { error };
1064
1147
  } finally {
1065
1148
  runAc.abort(postmortem.markExpectedCleanupError(new ToolAbortError("Browser run ended")));
1149
+ await Bun.sleep(0);
1066
1150
  try {
1067
1151
  await runPage?.cleanup();
1068
1152
  } catch (error) {
1069
1153
  failure = { error };
1070
1154
  }
1155
+ failure = this.#foldFloatingRejections(active, failure);
1071
1156
  if (this.#active?.id === msg.id) this.#active = null;
1072
1157
  }
1073
1158
  if (failure) {
@@ -1182,9 +1267,12 @@ export class WorkerCore {
1182
1267
  (opTimeout?.aborted || (err instanceof Error && err.name === "TimeoutError"))
1183
1268
  ) {
1184
1269
  const hint = selector ? await this.#selectorTimeoutHint(selector) : "";
1185
- throw new ToolError(`${label} timed out after ${perOpTimeoutMs}ms${hint}`);
1270
+ throw markBrowserRunRejection(
1271
+ new ToolError(`${label} timed out after ${perOpTimeoutMs}ms${hint}`),
1272
+ active.rejectionOwner,
1273
+ );
1186
1274
  }
1187
- throw err;
1275
+ throw markBrowserRunRejection(err, active.rejectionOwner);
1188
1276
  } finally {
1189
1277
  earlyAc.abort();
1190
1278
  active.inflight.delete(opId);
@@ -1868,6 +1956,7 @@ export class WorkerCore {
1868
1956
 
1869
1957
  async #close(): Promise<void> {
1870
1958
  this.#unsub();
1959
+ this.#uninstallRejectionGuard();
1871
1960
  this.#clearElementCache();
1872
1961
  const page = this.#page;
1873
1962
  if (this.#dialogHandler && page && !page.isClosed()) page.off("dialog", this.#dialogHandler);
@@ -516,7 +516,7 @@ function cleanFeedText(text: string): string {
516
516
  * Parse RSS/Atom feed to markdown
517
517
  */
518
518
  async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
519
- const { parseHTML } = await import("linkedom");
519
+ const { parseHTML } = await import("@oh-my-pi/pi-utils/dom");
520
520
  try {
521
521
  const doc = parseHTML(content).document;
522
522
 
package/src/tools/glob.ts CHANGED
@@ -6,7 +6,7 @@ import type { ToolExample } from "@oh-my-pi/pi-ai";
6
6
  import * as natives from "@oh-my-pi/pi-natives";
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
- import { formatGroupedPaths, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
9
+ import { formatGroupedPaths, hasFsCode, isEnoent, prompt, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { InternalUrlRouter } from "../internal-urls";
12
12
  import { splitMemoryGlobPattern } from "../internal-urls/memory-protocol";
@@ -415,7 +415,9 @@ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
415
415
  try {
416
416
  stat = await fs.promises.stat(target.searchPath);
417
417
  } catch (err) {
418
- if (isEnoent(err)) {
418
+ // ENAMETOOLONG can never name a real target; surface a clean
419
+ // "Path not found" instead of leaking the raw errno (issue #7597).
420
+ if (isEnoent(err) || hasFsCode(err, "ENAMETOOLONG")) {
419
421
  if (isSingle) throw new ToolError(`Path not found: ${scopePath}`);
420
422
  return [];
421
423
  }
@@ -3,7 +3,7 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import * as url from "node:url";
5
5
  import { glob } from "@oh-my-pi/pi-natives";
6
- import { isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix, untilAborted } from "@oh-my-pi/pi-utils";
6
+ import { hasFsCode, isEnoent, isEnotdir, stripWindowsExtendedLengthPathPrefix, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import type { Skill } from "../extensibility/skills";
8
8
  import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
9
9
  import { ToolAbortError, ToolError } from "./tool-errors";
@@ -337,6 +337,13 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
337
337
  * plus selector `1-2` (issue #4618). `lstat` inspects the entry itself, so a
338
338
  * dangling symlink is still detected as present; ambiguous errors resolve to
339
339
  * `"unknown"` so callers keep the raw path instead of guessing.
340
+ *
341
+ * `ENAMETOOLONG` resolves to `"missing"` rather than `"unknown"`: a path whose
342
+ * component or whole length exceeds the OS limit can never name a real single
343
+ * entry, so it is strictly stronger evidence of non-existence than `ENOENT`.
344
+ * Without this, a semicolon-joined `path` list long enough to trip the limit
345
+ * (bare filenames past `NAME_MAX`, or a total past `PATH_MAX`) was read as one
346
+ * literal path and the delimited split was suppressed (issue #7597).
340
347
  */
341
348
  export async function probeLiteralPathExists(filePath: string, cwd: string): Promise<"exists" | "missing" | "unknown"> {
342
349
  const resolved = resolveReadPath(filePath, cwd);
@@ -344,7 +351,7 @@ export async function probeLiteralPathExists(filePath: string, cwd: string): Pro
344
351
  await fs.promises.lstat(resolved);
345
352
  return "exists";
346
353
  } catch (err) {
347
- if (isEnoent(err) || isEnotdir(err)) return "missing";
354
+ if (isEnoent(err) || isEnotdir(err) || hasFsCode(err, "ENAMETOOLONG")) return "missing";
348
355
  return "unknown";
349
356
  }
350
357
  }
@@ -762,7 +769,10 @@ async function delimitedPathPartResolves(entry: string, cwd: string, splitter: P
762
769
  await fs.promises.stat(absoluteBasePath);
763
770
  return true;
764
771
  } catch (err) {
765
- if (isEnoent(err)) return false;
772
+ // ENOENT and ENAMETOOLONG both mean this string cannot name an existing
773
+ // path, so the whole entry does not resolve and the delimited split may
774
+ // proceed (issue #7597). Other errors (EACCES, transient I/O) stay fatal.
775
+ if (isEnoent(err) || hasFsCode(err, "ENAMETOOLONG")) return false;
766
776
  throw err;
767
777
  }
768
778
  }
@@ -806,10 +816,18 @@ async function tryDelimitedPathSplit(
806
816
  export async function splitDelimitedPathEntry(
807
817
  entry: string,
808
818
  cwd: string,
809
- options: { splitter?: PathEntrySplitter } = {},
819
+ options: {
820
+ splitter?: PathEntrySplitter;
821
+ routedUrlPredicate?: (entry: string) => boolean;
822
+ } = {},
810
823
  ): Promise<string[] | null> {
811
824
  const normalizedEntry = normalizePathLikeInput(entry);
812
825
  if (!hasTopLevelPathDelimiter(normalizedEntry)) return null;
826
+ const splitter = options.splitter ?? parseSearchPath;
827
+ if (options.routedUrlPredicate?.(normalizedEntry)) {
828
+ const parts = await tryDelimitedPathSplit(normalizedEntry, cwd, splitter, "semicolon", "none");
829
+ return parts?.every(options.routedUrlPredicate) ? parts : null;
830
+ }
813
831
  if (isInternalUrlPath(normalizedEntry)) return null;
814
832
  // A real POSIX file may contain the delimiter and a selector-shaped tail
815
833
  // (`a;b:1-2`, `a b:1-2`). Preserve the raw entry whenever the full literal
@@ -817,7 +835,6 @@ export async function splitDelimitedPathEntry(
817
835
  // splitters see it before delimiter expansion peels or splits (issue #4618
818
836
  // reviewer feedback: delimited expansion ran before the literal check).
819
837
  if ((await probeLiteralPathExists(normalizedEntry, cwd)) !== "missing") return null;
820
- const splitter = options.splitter ?? parseSearchPath;
821
838
  const peeledEntry = splitPathAndSel(normalizedEntry).path;
822
839
  if (!hasGlobPathChars(peeledEntry) && (await delimitedPathPartResolves(normalizedEntry, cwd, splitter))) {
823
840
  return null;