@oh-my-pi/pi-coding-agent 17.2.9 → 17.2.11

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 (281) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/{CHANGELOG-8yy8h3bc.md → CHANGELOG-d9xenpn9.md} +83 -0
  3. package/dist/cli.js +10208 -12596
  4. package/dist/types/capability/mcp.d.ts +11 -0
  5. package/dist/types/capability/skill.d.ts +6 -0
  6. package/dist/types/cli/args.d.ts +1 -0
  7. package/dist/types/cli/command-help.d.ts +3 -0
  8. package/dist/types/commands/share.d.ts +25 -0
  9. package/dist/types/commit/agentic/index.d.ts +3 -1
  10. package/dist/types/commit/execute.d.ts +32 -0
  11. package/dist/types/commit/index.d.ts +2 -1
  12. package/dist/types/commit/pipeline.d.ts +7 -1
  13. package/dist/types/config/model-registry.d.ts +4 -0
  14. package/dist/types/config/model-resolver.d.ts +13 -0
  15. package/dist/types/config/settings-schema.d.ts +1 -34
  16. package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
  17. package/dist/types/discovery/agent-plugins.d.ts +1 -0
  18. package/dist/types/discovery/contained-path.d.ts +34 -0
  19. package/dist/types/discovery/index.d.ts +1 -0
  20. package/dist/types/extensibility/custom-commands/types.d.ts +5 -5
  21. package/dist/types/extensibility/custom-tools/types.d.ts +3 -3
  22. package/dist/types/extensibility/extensions/loader.d.ts +9 -2
  23. package/dist/types/extensibility/extensions/types.d.ts +45 -8
  24. package/dist/types/extensibility/hooks/types.d.ts +5 -5
  25. package/dist/types/extensibility/shared-events.d.ts +3 -2
  26. package/dist/types/extensibility/skills.d.ts +5 -0
  27. package/dist/types/index.d.ts +2 -2
  28. package/dist/types/main.d.ts +1 -1
  29. package/dist/types/mcp/transports/header-policy.d.ts +45 -0
  30. package/dist/types/mcp/types.d.ts +15 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +1 -1
  32. package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
  33. package/dist/types/modes/acp/acp-event-mapper.d.ts +1 -1
  34. package/dist/types/modes/acp/acp-mode.d.ts +1 -1
  35. package/dist/types/modes/components/agent-hub-projection.d.ts +38 -0
  36. package/dist/types/modes/components/agent-hub-renderer.d.ts +41 -0
  37. package/dist/types/modes/components/agent-hub.d.ts +14 -5
  38. package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
  39. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  40. package/dist/types/modes/components/custom-editor.d.ts +1 -2
  41. package/dist/types/modes/components/model-browser.d.ts +9 -1
  42. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  43. package/dist/types/modes/components/tool-execution.d.ts +2 -0
  44. package/dist/types/modes/interactive-mode.d.ts +8 -1
  45. package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
  46. package/dist/types/registry/agent-registry.d.ts +47 -0
  47. package/dist/types/registry/persisted-agents.d.ts +3 -1
  48. package/dist/types/secrets/index.d.ts +23 -1
  49. package/dist/types/session/agent-session-events.d.ts +2 -2
  50. package/dist/types/session/agent-session.d.ts +4 -2
  51. package/dist/types/session/messages.d.ts +1 -0
  52. package/dist/types/session/prewalk.d.ts +3 -1
  53. package/dist/types/session/session-entries.d.ts +10 -0
  54. package/dist/types/session/session-loader.d.ts +11 -1
  55. package/dist/types/session/session-manager.d.ts +9 -1
  56. package/dist/types/session/session-tools.d.ts +18 -1
  57. package/dist/types/session/turn-recovery.d.ts +7 -2
  58. package/dist/types/slash-commands/acp-builtins.d.ts +1 -1
  59. package/dist/types/slash-commands/available-commands.d.ts +1 -1
  60. package/dist/types/task/executor.d.ts +6 -0
  61. package/dist/types/task/index.d.ts +4 -3
  62. package/dist/types/task/read-only-policy.d.ts +3 -0
  63. package/dist/types/task/structured-subagent.d.ts +2 -0
  64. package/dist/types/task/types.d.ts +4 -0
  65. package/dist/types/tools/bash.d.ts +3 -3
  66. package/dist/types/tools/browser/launch.d.ts +2 -1
  67. package/dist/types/tools/browser/tab-worker.d.ts +2 -2
  68. package/dist/types/tools/path-utils.d.ts +1 -0
  69. package/dist/types/tools/read.d.ts +1 -5
  70. package/dist/types/tools/run-scope.d.ts +27 -1
  71. package/dist/types/tools/shell-tokenize.d.ts +16 -0
  72. package/dist/types/tools/terminal-output.d.ts +1 -1
  73. package/dist/types/utils/turndown.d.ts +1 -1
  74. package/dist/types/vibe/runtime.d.ts +2 -3
  75. package/dist/types/web/scrapers/readthedocs.d.ts +0 -3
  76. package/dist/types/web/search/providers/browser-headers.d.ts +2 -3
  77. package/examples/custom-tools/README.md +1 -1
  78. package/examples/extensions/README.md +1 -1
  79. package/examples/extensions/api-demo.ts +5 -6
  80. package/examples/extensions/chalk-logger.ts +1 -1
  81. package/examples/extensions/hello.ts +2 -2
  82. package/examples/extensions/reload-runtime.ts +1 -1
  83. package/examples/extensions/tools.ts +2 -2
  84. package/examples/extensions/with-deps/index.ts +1 -1
  85. package/examples/sdk/06-extensions.ts +1 -1
  86. package/package.json +15 -31
  87. package/scripts/bundle-dist.ts +1 -11
  88. package/scripts/legacy-pi-virtual-module.ts +4 -1
  89. package/src/advisor/runtime.ts +54 -0
  90. package/src/capability/mcp.ts +11 -0
  91. package/src/capability/skill.ts +6 -0
  92. package/src/cli/agents-cli.ts +1 -1
  93. package/src/cli/args.ts +24 -2
  94. package/src/cli/auth-broker-cli.ts +1 -1
  95. package/src/cli/auth-gateway-cli.ts +1 -1
  96. package/src/cli/bench-cli.ts +1 -1
  97. package/src/cli/claude-trace-cli.ts +1 -1
  98. package/src/cli/command-help.ts +4 -0
  99. package/src/cli/config-cli.ts +1 -1
  100. package/src/cli/dry-balance-cli.ts +1 -1
  101. package/src/cli/file-processor.ts +1 -1
  102. package/src/cli/flag-tables.ts +4 -0
  103. package/src/cli/gallery-cli.ts +1 -1
  104. package/src/cli/grep-cli.ts +1 -1
  105. package/src/cli/grievances-cli.ts +1 -1
  106. package/src/cli/help-extra.ts +1 -1
  107. package/src/cli/models-cli.ts +1 -1
  108. package/src/cli/plugin-cli.ts +1 -1
  109. package/src/cli/read-cli.ts +1 -1
  110. package/src/cli/setup-cli.ts +1 -1
  111. package/src/cli/shell-cli.ts +1 -1
  112. package/src/cli/ssh-cli.ts +1 -1
  113. package/src/cli/stats-cli.ts +1 -1
  114. package/src/cli/tiny-models-cli.ts +1 -1
  115. package/src/cli/ttsr-cli.ts +1 -1
  116. package/src/cli/update-cli.ts +1 -1
  117. package/src/cli/usage-cli.ts +1 -1
  118. package/src/cli/web-search-cli.ts +1 -1
  119. package/src/cli/worktree-cli.ts +1 -1
  120. package/src/cli-commands.ts +5 -0
  121. package/src/commands/commit.ts +16 -3
  122. package/src/commands/say.ts +1 -1
  123. package/src/commands/share.ts +71 -0
  124. package/src/commands/token.ts +1 -1
  125. package/src/commit/agentic/agent.ts +1 -1
  126. package/src/commit/agentic/index.ts +39 -21
  127. package/src/commit/cli.ts +1 -1
  128. package/src/commit/execute.ts +56 -0
  129. package/src/commit/index.ts +2 -1
  130. package/src/commit/pipeline.ts +20 -7
  131. package/src/config/model-discovery.ts +1 -1
  132. package/src/config/model-registry.ts +21 -1
  133. package/src/config/model-resolver.ts +54 -7
  134. package/src/config/settings-schema.ts +2 -33
  135. package/src/config/settings.ts +46 -0
  136. package/src/discovery/agent-plugin-format.ts +551 -0
  137. package/src/discovery/agent-plugins.ts +341 -0
  138. package/src/discovery/agents.ts +4 -2
  139. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +5 -4
  140. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -1
  141. package/src/discovery/claude-plugins.ts +21 -5
  142. package/src/discovery/contained-path.ts +78 -0
  143. package/src/discovery/helpers.ts +23 -9
  144. package/src/discovery/index.ts +1 -0
  145. package/src/discovery/omp-plugins.ts +20 -7
  146. package/src/eval/py/runner.py +38 -1
  147. package/src/exec/non-interactive-env.ts +1 -0
  148. package/src/extensibility/custom-commands/loader.ts +4 -4
  149. package/src/extensibility/custom-commands/types.ts +5 -5
  150. package/src/extensibility/custom-tools/loader.ts +4 -4
  151. package/src/extensibility/custom-tools/types.ts +3 -3
  152. package/src/extensibility/extensions/loader.ts +85 -20
  153. package/src/extensibility/extensions/runner.ts +6 -0
  154. package/src/extensibility/extensions/types.ts +51 -8
  155. package/src/extensibility/hooks/loader.ts +4 -5
  156. package/src/extensibility/hooks/types.ts +5 -5
  157. package/src/extensibility/plugins/legacy-pi-compat.ts +53 -0
  158. package/src/extensibility/plugins/marketplace/manager.ts +2 -1
  159. package/src/extensibility/shared-events.ts +3 -2
  160. package/src/extensibility/skills.ts +9 -0
  161. package/src/index.ts +2 -2
  162. package/src/internal-urls/memory-protocol.ts +5 -1
  163. package/src/internal-urls/skill-protocol.ts +14 -0
  164. package/src/internal-urls/vault-protocol.ts +2 -2
  165. package/src/launch/client.ts +11 -1
  166. package/src/launch/protocol.ts +7 -2
  167. package/src/launch/terminal-output.ts +1 -1
  168. package/src/main.ts +88 -23
  169. package/src/markit/converters/docx.ts +1 -1
  170. package/src/markit/converters/epub.ts +1 -1
  171. package/src/markit/converters/pptx.ts +1 -1
  172. package/src/markit/converters/xlsx.ts +1 -1
  173. package/src/mcp/config.ts +3 -0
  174. package/src/mcp/manager.ts +12 -9
  175. package/src/mcp/transports/header-policy.ts +95 -0
  176. package/src/mcp/transports/http.ts +35 -52
  177. package/src/mcp/transports/sse.ts +20 -27
  178. package/src/mcp/types.ts +15 -0
  179. package/src/modes/acp/acp-agent.ts +20 -9
  180. package/src/modes/acp/acp-client-bridge.ts +1 -1
  181. package/src/modes/acp/acp-event-mapper.ts +1 -1
  182. package/src/modes/acp/acp-mode.ts +1 -1
  183. package/src/modes/components/agent-dashboard.ts +2 -0
  184. package/src/modes/components/agent-hub-projection.ts +248 -0
  185. package/src/modes/components/agent-hub-renderer.ts +194 -0
  186. package/src/modes/components/agent-hub.ts +674 -207
  187. package/src/modes/components/agent-transcript-viewer.ts +3 -0
  188. package/src/modes/components/assistant-message.ts +1 -1
  189. package/src/modes/components/chat-transcript-builder.ts +3 -0
  190. package/src/modes/components/custom-editor.ts +0 -9
  191. package/src/modes/components/extensions/inspector-panel.ts +11 -6
  192. package/src/modes/components/footer.ts +1 -3
  193. package/src/modes/components/model-browser.ts +11 -3
  194. package/src/modes/components/model-hub.ts +4 -1
  195. package/src/modes/components/status-line/component.ts +1 -0
  196. package/src/modes/components/status-line/segments.ts +10 -7
  197. package/src/modes/components/status-line/types.ts +2 -0
  198. package/src/modes/components/tool-execution.ts +13 -16
  199. package/src/modes/components/tree-selector.ts +62 -3
  200. package/src/modes/controllers/command-controller.ts +4 -1
  201. package/src/modes/controllers/event-controller.ts +44 -20
  202. package/src/modes/controllers/extension-ui-controller.ts +2 -2
  203. package/src/modes/controllers/input-controller.ts +20 -2
  204. package/src/modes/controllers/mcp-command-controller.ts +81 -20
  205. package/src/modes/controllers/selector-controller.ts +41 -49
  206. package/src/modes/controllers/streaming-reveal.ts +1 -1
  207. package/src/modes/interactive-mode.ts +13 -2
  208. package/src/modes/rpc/rpc-mode.ts +2 -2
  209. package/src/modes/runtime-init.ts +1 -1
  210. package/src/modes/theme/theme.ts +2 -2
  211. package/src/modes/utils/transcript-render-helpers.ts +4 -2
  212. package/src/modes/utils/ui-helpers.ts +1 -0
  213. package/src/prompts/tools/computer.md +1 -1
  214. package/src/registry/agent-lifecycle.ts +14 -7
  215. package/src/registry/agent-registry.ts +65 -2
  216. package/src/registry/persisted-agents.ts +341 -16
  217. package/src/sdk.ts +6 -49
  218. package/src/secrets/index.ts +52 -1
  219. package/src/session/agent-session-events.ts +2 -2
  220. package/src/session/agent-session.ts +54 -16
  221. package/src/session/messages.ts +1 -0
  222. package/src/session/prewalk.ts +57 -17
  223. package/src/session/session-context.ts +3 -5
  224. package/src/session/session-entries.ts +10 -0
  225. package/src/session/session-handoff.ts +5 -1
  226. package/src/session/session-listing.ts +1 -1
  227. package/src/session/session-loader.ts +74 -8
  228. package/src/session/session-manager.ts +22 -2
  229. package/src/session/session-paths.ts +57 -9
  230. package/src/session/session-tools.ts +65 -4
  231. package/src/session/todo-tracker.ts +11 -1
  232. package/src/session/turn-recovery.ts +96 -74
  233. package/src/slash-commands/acp-builtins.ts +1 -1
  234. package/src/slash-commands/available-commands.ts +1 -1
  235. package/src/slash-commands/builtin-registry.ts +12 -8
  236. package/src/task/executor.ts +70 -20
  237. package/src/task/index.ts +30 -34
  238. package/src/task/isolation-runner.ts +24 -11
  239. package/src/task/persisted-revive.ts +12 -5
  240. package/src/task/read-only-policy.ts +27 -0
  241. package/src/task/structured-subagent.ts +14 -4
  242. package/src/task/types.ts +4 -0
  243. package/src/tools/auto-generated-guard.ts +1 -1
  244. package/src/tools/bash-interactive.ts +4 -4
  245. package/src/tools/bash-skill-urls.ts +15 -0
  246. package/src/tools/bash.ts +16 -17
  247. package/src/tools/browser/cmux/cmux-tab.ts +66 -18
  248. package/src/tools/browser/launch.ts +41 -12
  249. package/src/tools/browser/readable.ts +9 -9
  250. package/src/tools/browser/tab-supervisor.ts +12 -3
  251. package/src/tools/browser/tab-worker-entry.ts +1 -1
  252. package/src/tools/browser/tab-worker.ts +101 -12
  253. package/src/tools/debug.ts +1 -1
  254. package/src/tools/fetch.ts +1 -1
  255. package/src/tools/jtd-to-json-schema.ts +123 -21
  256. package/src/tools/path-utils.ts +9 -2
  257. package/src/tools/read.ts +21 -9
  258. package/src/tools/run-scope.ts +290 -4
  259. package/src/tools/shell-tokenize.ts +98 -0
  260. package/src/tools/terminal-output.ts +1 -1
  261. package/src/tools/todo.ts +1 -1
  262. package/src/utils/clipboard.ts +10 -2
  263. package/src/utils/external-editor.ts +4 -2
  264. package/src/utils/jj.ts +1 -1
  265. package/src/utils/turndown.ts +1 -2
  266. package/src/vibe/runtime.ts +5 -5
  267. package/src/web/scrapers/arxiv.ts +1 -1
  268. package/src/web/scrapers/go-pkg.ts +1 -1
  269. package/src/web/scrapers/iacr.ts +1 -1
  270. package/src/web/scrapers/readthedocs.ts +2 -1
  271. package/src/web/scrapers/twitter.ts +2 -2
  272. package/src/web/scrapers/types.ts +1 -1
  273. package/src/web/scrapers/wikipedia.ts +1 -1
  274. package/src/web/search/providers/browser-headers.ts +12 -39
  275. package/src/web/search/providers/codex.ts +3 -26
  276. package/src/web/search/providers/ecosia.ts +1 -1
  277. package/src/web/search/providers/exa.ts +1 -1
  278. package/src/web/search/providers/google.ts +1 -1
  279. package/src/web/search/providers/mojeek.ts +1 -1
  280. package/src/web/search/providers/startpage.ts +1 -1
  281. package/src/markit/converters/mammoth.d.ts +0 -24
@@ -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);
@@ -505,7 +505,7 @@ const ADAPTER_UNAVAILABLE_MESSAGES: Readonly<Record<string, string>> = {
505
505
  dlv: "adapter 'dlv' is not available: install with 'go install github.com/go-delve/delve/cmd/dlv@latest'",
506
506
  rdbg: "adapter 'rdbg' is not available: install with 'gem install debug'",
507
507
  "js-debug-adapter":
508
- "adapter 'js-debug-adapter' is not available: install vscode-js-debug with Mason or set JS_DEBUG_DAP_SERVER to dapDebugServer.js",
508
+ "adapter 'js-debug-adapter' is not available: download it from https://github.com/microsoft/vscode-js-debug",
509
509
  };
510
510
 
511
511
  const ADAPTER_CANONICAL_COMMANDS: Readonly<Record<string, string>> = {
@@ -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
 
@@ -8,6 +8,7 @@
8
8
  * @see https://datatracker.ietf.org/doc/html/rfc8927
9
9
  */
10
10
 
11
+ import { isRecord } from "@oh-my-pi/pi-utils";
11
12
  import type { JTDPrimitive } from "./jtd-utils.js";
12
13
  import {
13
14
  isJTDDiscriminator,
@@ -134,6 +135,18 @@ function convertSchema(schema: unknown): unknown {
134
135
  return {};
135
136
  }
136
137
 
138
+ const jtdOnlyPrimitiveTypes: Record<string, true> = {
139
+ timestamp: true,
140
+ float32: true,
141
+ float64: true,
142
+ int8: true,
143
+ uint8: true,
144
+ int16: true,
145
+ uint16: true,
146
+ int32: true,
147
+ uint32: true,
148
+ };
149
+
137
150
  /**
138
151
  * Detect if a schema is JTD format (vs JSON Schema).
139
152
  *
@@ -155,11 +168,8 @@ export function isJTDSchema(schema: unknown): boolean {
155
168
  if ("ref" in obj) return true;
156
169
 
157
170
  // JTD type primitives (JSON Schema doesn't have int32, float64, etc.)
158
- if ("type" in obj) {
159
- const jtdPrimitives = ["timestamp", "float32", "float64", "int8", "uint8", "int16", "uint16", "int32", "uint32"];
160
- if (jtdPrimitives.includes(obj.type as string)) {
161
- return true;
162
- }
171
+ if (typeof obj.type === "string" && Object.hasOwn(jtdOnlyPrimitiveTypes, obj.type)) {
172
+ return true;
163
173
  }
164
174
 
165
175
  // JTD properties form without type: "object" (JSON Schema requires it)
@@ -170,36 +180,128 @@ export function isJTDSchema(schema: unknown): boolean {
170
180
  return false;
171
181
  }
172
182
 
173
- function normalizeMixedSchemaNode(schema: unknown): unknown {
174
- if (schema === null || typeof schema !== "object") {
175
- return schema;
183
+ function isUnambiguousJTDSchema(schema: unknown): boolean {
184
+ if (!isRecord(schema)) return false;
185
+
186
+ if (isRecord(schema.elements) || isRecord(schema.values) || isRecord(schema.optionalProperties)) {
187
+ return true;
188
+ }
189
+ if (typeof schema.ref === "string") return true;
190
+ if (typeof schema.type === "string" && Object.hasOwn(jtdOnlyPrimitiveTypes, schema.type)) {
191
+ return true;
192
+ }
193
+ if (typeof schema.discriminator !== "string" || !isRecord(schema.mapping)) {
194
+ return false;
176
195
  }
177
196
 
178
- if (Array.isArray(schema)) {
179
- return schema.map(item => normalizeMixedSchemaNode(item));
197
+ for (const key in schema.mapping) {
198
+ if (!Object.hasOwn(schema.mapping, key)) continue;
199
+ const mapping = schema.mapping[key];
200
+ if (!isRecord(mapping)) return false;
201
+
202
+ let hasSchemaProperties = false;
203
+ if (Object.hasOwn(mapping, "properties")) {
204
+ if (!isRecord(mapping.properties)) return false;
205
+ hasSchemaProperties = true;
206
+ }
207
+ if (Object.hasOwn(mapping, "optionalProperties")) {
208
+ if (!isRecord(mapping.optionalProperties)) return false;
209
+ hasSchemaProperties = true;
210
+ }
211
+ if (!hasSchemaProperties) return false;
180
212
  }
181
213
 
182
- if (isJTDSchema(schema)) {
183
- // `convertSchema` is itself fully recursive and emits pure JSON Schema, so
184
- // re-walking the result with `normalizeMixedSchemaNode` is unnecessary and
185
- // unsafe: it would treat user-named properties whose keys happen to be JTD
186
- // keywords (e.g. `ref`, `elements`) as nested JTD forms (#1345).
187
- return convertSchema(schema);
214
+ return true;
215
+ }
216
+
217
+ function normalizeJsonSchemaArray(value: unknown): unknown {
218
+ if (!Array.isArray(value)) return value;
219
+
220
+ let normalized: unknown[] | undefined;
221
+ for (let index = 0; index < value.length; index++) {
222
+ const item = value[index];
223
+ const converted = normalizeJsonSchemaNode(item);
224
+ if (converted === item) continue;
225
+ normalized ??= value.slice();
226
+ normalized[index] = converted;
227
+ }
228
+ return normalized ?? value;
229
+ }
230
+
231
+ function normalizeJsonSchemaMap(value: unknown): unknown {
232
+ if (!isRecord(value)) return value;
233
+
234
+ let normalized: Record<string, unknown> | undefined;
235
+ for (const key in value) {
236
+ if (!Object.hasOwn(value, key)) continue;
237
+ const item = value[key];
238
+ const converted = normalizeJsonSchemaNode(item);
239
+ if (converted === item) continue;
240
+ normalized ??= { ...value };
241
+ normalized[key] = converted;
188
242
  }
243
+ return normalized ?? value;
244
+ }
189
245
 
190
- const normalized: Record<string, unknown> = {};
191
- for (const [key, value] of Object.entries(schema)) {
192
- normalized[key] = normalizeMixedSchemaNode(value);
246
+ function normalizeJsonSchemaNode(schema: unknown): unknown {
247
+ if (!isRecord(schema)) return schema;
248
+ if (isUnambiguousJTDSchema(schema)) return convertSchema(schema);
249
+
250
+ let normalized: Record<string, unknown> | undefined;
251
+ for (const key in schema) {
252
+ if (!Object.hasOwn(schema, key)) continue;
253
+
254
+ const value = schema[key];
255
+ let converted: unknown;
256
+ switch (key) {
257
+ case "not":
258
+ case "if":
259
+ case "then":
260
+ case "else":
261
+ case "items":
262
+ case "contains":
263
+ case "propertyNames":
264
+ case "additionalProperties":
265
+ case "unevaluatedProperties":
266
+ case "unevaluatedItems":
267
+ case "contentSchema":
268
+ converted = normalizeJsonSchemaNode(value);
269
+ break;
270
+ case "allOf":
271
+ case "anyOf":
272
+ case "oneOf":
273
+ case "prefixItems":
274
+ converted = normalizeJsonSchemaArray(value);
275
+ break;
276
+ case "properties":
277
+ case "patternProperties":
278
+ case "$defs":
279
+ case "definitions":
280
+ case "dependentSchemas":
281
+ converted = normalizeJsonSchemaMap(value);
282
+ break;
283
+ default:
284
+ continue;
285
+ }
286
+
287
+ if (converted === value) continue;
288
+ normalized ??= { ...schema };
289
+ normalized[key] = converted;
193
290
  }
194
291
 
195
- return normalized;
292
+ return normalized ?? schema;
196
293
  }
294
+
197
295
  /**
198
296
  * Convert JTD schema to JSON Schema.
199
297
  * If already JSON Schema, returns as-is.
200
298
  */
201
299
  export function jtdToJsonSchema(schema: unknown): unknown {
202
- return normalizeMixedSchemaNode(schema);
300
+ if (isJTDSchema(schema)) {
301
+ // convertSchema is recursive; re-walking its JSON Schema output caused #1345.
302
+ return convertSchema(schema);
303
+ }
304
+ return normalizeJsonSchemaNode(schema);
203
305
  }
204
306
 
205
307
  /**
@@ -816,10 +816,18 @@ async function tryDelimitedPathSplit(
816
816
  export async function splitDelimitedPathEntry(
817
817
  entry: string,
818
818
  cwd: string,
819
- options: { splitter?: PathEntrySplitter } = {},
819
+ options: {
820
+ splitter?: PathEntrySplitter;
821
+ routedUrlPredicate?: (entry: string) => boolean;
822
+ } = {},
820
823
  ): Promise<string[] | null> {
821
824
  const normalizedEntry = normalizePathLikeInput(entry);
822
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
+ }
823
831
  if (isInternalUrlPath(normalizedEntry)) return null;
824
832
  // A real POSIX file may contain the delimiter and a selector-shaped tail
825
833
  // (`a;b:1-2`, `a b:1-2`). Preserve the raw entry whenever the full literal
@@ -827,7 +835,6 @@ export async function splitDelimitedPathEntry(
827
835
  // splitters see it before delimiter expansion peels or splits (issue #4618
828
836
  // reviewer feedback: delimited expansion ran before the literal check).
829
837
  if ((await probeLiteralPathExists(normalizedEntry, cwd)) !== "missing") return null;
830
- const splitter = options.splitter ?? parseSearchPath;
831
838
  const peeledEntry = splitPathAndSel(normalizedEntry).path;
832
839
  if (!hasGlobPathChars(peeledEntry) && (await delimitedPathPartResolves(normalizedEntry, cwd, splitter))) {
833
840
  return null;
package/src/tools/read.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  readImageMetadata,
27
27
  untilAborted,
28
28
  } from "@oh-my-pi/pi-utils";
29
- import { LRUCache } from "lru-cache/raw";
29
+ import { LRUCache } from "@oh-my-pi/pi-utils/lru";
30
30
  import {
31
31
  canonicalSnapshotKey,
32
32
  getFileSnapshotStore,
@@ -721,6 +721,10 @@ const readSchema = type({
721
721
  ),
722
722
  });
723
723
 
724
+ const readSchemaWithoutMemory = type({
725
+ path: type("string").describe("Local path, internal URI (e.g. skill://), or URL. Inline selectors are supported."),
726
+ });
727
+
724
728
  export type ReadToolInput = typeof readSchema.infer;
725
729
 
726
730
  export interface ReadToolDetails {
@@ -866,7 +870,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
866
870
  readonly label = "Read";
867
871
  readonly loadMode = "essential";
868
872
  description: string;
869
- readonly parameters = readSchema;
873
+ get parameters(): typeof readSchema {
874
+ return this.session.settings.get("memory.backend") === "off" ? readSchemaWithoutMemory : readSchema;
875
+ }
870
876
  readonly strict = true;
871
877
 
872
878
  readonly #autoResizeImages: boolean;
@@ -962,8 +968,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
962
968
  async #tryReadDelimitedPaths(
963
969
  readPath: string,
964
970
  signal?: AbortSignal,
971
+ routedUrlPredicate?: (entry: string) => boolean,
965
972
  ): Promise<AgentToolResult<ReadToolDetails> | null> {
966
- const parts = await splitDelimitedPathEntry(readPath, this.session.cwd);
973
+ const parts = await splitDelimitedPathEntry(readPath, this.session.cwd, { routedUrlPredicate });
967
974
  if (!parts) return null;
968
975
 
969
976
  const notice = `Note: interpreted as ${parts.length} paths: ${parts.join(", ")}`;
@@ -2306,9 +2313,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2306
2313
  }
2307
2314
 
2308
2315
  // Handle native OMP URLs and custom-scheme resources advertised by MCP servers.
2309
- // Use the internal-URL-aware splitter so malformed selectors are peeled
2310
- // off the URL and surfaced via parseSel rather than confusing handlers.
2311
2316
  const internalRouter = InternalUrlRouter.instance();
2317
+ const delimitedInternalResult = internalRouter.canResolve(readPath)
2318
+ ? await this.#tryReadDelimitedPaths(readPath, signal, entry => internalRouter.canResolve(entry))
2319
+ : null;
2320
+ if (delimitedInternalResult) return delimitedInternalResult;
2321
+
2322
+ // Peel malformed selectors through the internal-URL-aware parser before routing.
2312
2323
  let promotedSelector: string | undefined;
2313
2324
  if (internalRouter.canResolve(readPath)) {
2314
2325
  const internalTarget = splitInternalUrlSel(readPath);
@@ -2486,6 +2497,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2486
2497
  const imageMetadata = await readImageMetadata(absolutePath);
2487
2498
  const mimeType = imageMetadata?.mimeType;
2488
2499
  const ext = path.extname(absolutePath).toLowerCase();
2500
+ const resolvedDisplayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
2489
2501
  const shouldConvertWithMarkit = CONVERTIBLE_EXTENSIONS.has(ext);
2490
2502
 
2491
2503
  // Profiler reports (macOS `sample` call trees, V8 `.cpuprofile` JSON):
@@ -2530,7 +2542,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2530
2542
  fileSize,
2531
2543
  }));
2532
2544
  } else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
2533
- const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
2545
+ const notebookText = await readEditableNotebookText(absolutePath, resolvedDisplayPath);
2534
2546
  if (isMultiRange(parsed) && parsed.kind === "lines") {
2535
2547
  return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
2536
2548
  details: { resolvedPath: absolutePath },
@@ -2549,7 +2561,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2549
2561
  const result = await convertFileWithMarkit(absolutePath, signal);
2550
2562
  if (result.ok) {
2551
2563
  const renderedContent =
2552
- ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, localReadPath) : result.content;
2564
+ ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, resolvedDisplayPath) : result.content;
2553
2565
  // Route the converted markdown through the in-memory text builder
2554
2566
  // so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
2555
2567
  // raw mode apply against the converted output. Without this,
@@ -2592,7 +2604,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2592
2604
  return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
2593
2605
  .text(
2594
2606
  prependSuffixResolutionNotice(
2595
- `[Cannot read binary file '${formatPathRelativeToCwd(absolutePath, this.session.cwd)}' (${formatBytes(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`,
2607
+ `[Cannot read binary file '${resolvedDisplayPath}' (${formatBytes(fileSize)}); not valid UTF-8 text. Use ':raw' to read bytes verbatim.]`,
2596
2608
  suffixResolution,
2597
2609
  ),
2598
2610
  )
@@ -2609,7 +2621,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2609
2621
  if (summary?.parsed && summary.elided) {
2610
2622
  const renderedSummary = this.#renderSummary(summary);
2611
2623
  const footer = formatSummaryElisionFooter(
2612
- localReadPath,
2624
+ resolvedDisplayPath,
2613
2625
  renderedSummary.elidedRanges,
2614
2626
  renderedSummary.elidedLines,
2615
2627
  );