@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4685 -4614
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +374 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
  25. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  26. package/dist/types/live/attestation.d.ts +2 -0
  27. package/dist/types/live/controller.d.ts +48 -0
  28. package/dist/types/live/protocol.d.ts +85 -0
  29. package/dist/types/live/protocol.test.d.ts +1 -0
  30. package/dist/types/live/transport.d.ts +35 -0
  31. package/dist/types/live/visualizer.d.ts +30 -0
  32. package/dist/types/lsp/client.d.ts +16 -1
  33. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  34. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  35. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  36. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  37. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  38. package/dist/types/modes/components/assistant-message.d.ts +1 -0
  39. package/dist/types/modes/components/custom-message.d.ts +1 -1
  40. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  41. package/dist/types/modes/components/message-frame.d.ts +8 -4
  42. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  43. package/dist/types/modes/components/session-account-selector.d.ts +11 -0
  44. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  45. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  46. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  48. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  49. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  50. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  51. package/dist/types/modes/interactive-mode.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +4 -1
  54. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  55. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  56. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  57. package/dist/types/registry/agent-registry.d.ts +11 -4
  58. package/dist/types/sdk.d.ts +15 -1
  59. package/dist/types/secrets/index.d.ts +15 -2
  60. package/dist/types/secrets/obfuscator.d.ts +113 -15
  61. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  62. package/dist/types/session/agent-session-events.d.ts +84 -0
  63. package/dist/types/session/agent-session-types.d.ts +321 -0
  64. package/dist/types/session/agent-session.d.ts +135 -753
  65. package/dist/types/session/async-job-delivery.d.ts +28 -0
  66. package/dist/types/session/auth-storage.d.ts +1 -1
  67. package/dist/types/session/bash-runner.d.ts +75 -0
  68. package/dist/types/session/blob-store.d.ts +11 -1
  69. package/dist/types/session/blob-store.test.d.ts +1 -0
  70. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  71. package/dist/types/session/eval-runner.d.ts +51 -0
  72. package/dist/types/session/irc-bridge.d.ts +46 -0
  73. package/dist/types/session/messages.d.ts +24 -0
  74. package/dist/types/session/model-controls.d.ts +180 -0
  75. package/dist/types/session/prewalk.d.ts +46 -0
  76. package/dist/types/session/queued-messages.d.ts +22 -0
  77. package/dist/types/session/redis-session-storage.d.ts +1 -0
  78. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  79. package/dist/types/session/role-models.d.ts +15 -0
  80. package/dist/types/session/session-advisors.d.ts +228 -0
  81. package/dist/types/session/session-entries.d.ts +8 -0
  82. package/dist/types/session/session-handoff.d.ts +70 -0
  83. package/dist/types/session/session-history-format.d.ts +6 -1
  84. package/dist/types/session/session-maintenance.d.ts +246 -0
  85. package/dist/types/session/session-manager.d.ts +41 -0
  86. package/dist/types/session/session-memory.d.ts +56 -0
  87. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  88. package/dist/types/session/session-stats.d.ts +48 -0
  89. package/dist/types/session/session-tools.d.ts +158 -0
  90. package/dist/types/session/session-workspace.d.ts +32 -0
  91. package/dist/types/session/stream-guards.d.ts +44 -0
  92. package/dist/types/session/streaming-output.d.ts +8 -0
  93. package/dist/types/session/todo-tracker.d.ts +56 -0
  94. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  95. package/dist/types/session/turn-recovery.d.ts +225 -0
  96. package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
  97. package/dist/types/stt/index.d.ts +0 -2
  98. package/dist/types/stt/stt-controller.d.ts +7 -0
  99. package/dist/types/system-prompt.d.ts +2 -0
  100. package/dist/types/task/executor.d.ts +4 -1
  101. package/dist/types/task/output-manager.d.ts +3 -1
  102. package/dist/types/tiny/title-client.d.ts +10 -0
  103. package/dist/types/tools/approval.d.ts +1 -0
  104. package/dist/types/tools/browser/launch.d.ts +5 -0
  105. package/dist/types/tools/builtin-names.d.ts +1 -1
  106. package/dist/types/tools/computer/protocol.d.ts +43 -0
  107. package/dist/types/tools/computer/supervisor.d.ts +32 -0
  108. package/dist/types/tools/computer/worker-entry.d.ts +1 -0
  109. package/dist/types/tools/computer/worker.d.ts +15 -0
  110. package/dist/types/tools/computer-renderer.d.ts +22 -0
  111. package/dist/types/tools/computer.d.ts +71 -0
  112. package/dist/types/tools/context.d.ts +2 -0
  113. package/dist/types/tools/default-renderer.d.ts +21 -0
  114. package/dist/types/tools/image-gen.d.ts +4 -4
  115. package/dist/types/tools/image-providers.d.ts +38 -0
  116. package/dist/types/tools/index.d.ts +7 -0
  117. package/dist/types/tools/path-utils.d.ts +8 -0
  118. package/dist/types/tools/todo.d.ts +10 -4
  119. package/dist/types/tools/xdev.d.ts +5 -1
  120. package/dist/types/tts/streaming-player.d.ts +10 -29
  121. package/dist/types/tts/vocalizer.d.ts +5 -0
  122. package/dist/types/utils/jj.d.ts +29 -0
  123. package/dist/types/utils/lang-from-path.d.ts +1 -0
  124. package/dist/types/utils/title-generator.d.ts +30 -0
  125. package/dist/types/utils/tools-manager.d.ts +1 -2
  126. package/dist/types/vibe/runtime.d.ts +36 -8
  127. package/dist/types/web/search/index.d.ts +1 -1
  128. package/dist/types/web/search/provider.d.ts +15 -5
  129. package/dist/types/web/search/types.d.ts +94 -0
  130. package/package.json +12 -12
  131. package/scripts/generate-share-viewer.ts +4 -6
  132. package/src/advisor/__tests__/advisor.test.ts +643 -0
  133. package/src/advisor/runtime.ts +144 -43
  134. package/src/async/job-manager.ts +90 -2
  135. package/src/cli/args.ts +3 -0
  136. package/src/cli/auth-gateway-cli.ts +18 -3
  137. package/src/cli/bench-cli.ts +395 -29
  138. package/src/cli/flag-tables.ts +3 -0
  139. package/src/cli/gc-cli.ts +2 -2
  140. package/src/cli/setup-cli.ts +2 -14
  141. package/src/cli/usage-cli.ts +10 -1
  142. package/src/cli.ts +8 -0
  143. package/src/collab/host.ts +3 -2
  144. package/src/commands/bench.ts +18 -4
  145. package/src/commands/launch.ts +4 -0
  146. package/src/config/model-registry.ts +16 -2
  147. package/src/config/models-config-schema.ts +15 -4
  148. package/src/config/provider-globals.ts +9 -9
  149. package/src/config/service-tier.ts +26 -1
  150. package/src/config/settings-schema.ts +270 -51
  151. package/src/config/settings.ts +81 -2
  152. package/src/dap/client.ts +37 -4
  153. package/src/edit/index.ts +49 -12
  154. package/src/eval/executor-base.ts +1 -0
  155. package/src/eval/js/executor.ts +2 -0
  156. package/src/eval/py/executor.ts +103 -33
  157. package/src/exec/bash-executor.ts +90 -6
  158. package/src/exec/direnv.ts +145 -0
  159. package/src/export/html/args.ts +20 -0
  160. package/src/export/html/index.ts +45 -47
  161. package/src/export/html/template.css +19 -1
  162. package/src/export/html/template.html +6 -0
  163. package/src/export/html/template.js +21 -0
  164. package/src/export/html/tool-views.generated.js +31 -31
  165. package/src/export/html/web-palette.ts +116 -132
  166. package/src/export/share.ts +249 -49
  167. package/src/extensibility/extensions/loader.ts +29 -1
  168. package/src/extensibility/extensions/runner.ts +6 -0
  169. package/src/extensibility/extensions/types.ts +33 -2
  170. package/src/extensibility/extensions/wrapper.ts +68 -12
  171. package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
  172. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  173. package/src/hindsight/state.ts +64 -6
  174. package/src/internal-urls/local-protocol.ts +2 -1
  175. package/src/internal-urls/memory-protocol.ts +2 -2
  176. package/src/internal-urls/skill-protocol.ts +2 -2
  177. package/src/internal-urls/ssh-protocol.ts +2 -1
  178. package/src/internal-urls/vault-protocol.ts +2 -1
  179. package/src/live/attestation.ts +91 -0
  180. package/src/live/controller.ts +517 -0
  181. package/src/live/prompts/agent-final-message.md +3 -0
  182. package/src/live/prompts/live-instructions.md +23 -0
  183. package/src/live/protocol.test.ts +140 -0
  184. package/src/live/protocol.ts +233 -0
  185. package/src/live/transport.ts +422 -0
  186. package/src/live/visualizer.ts +214 -0
  187. package/src/lsp/client.ts +29 -9
  188. package/src/lsp/index.ts +19 -4
  189. package/src/main.ts +8 -1
  190. package/src/mcp/config-writer.test.ts +43 -0
  191. package/src/mcp/config-writer.ts +109 -82
  192. package/src/mcp/oauth-discovery.ts +10 -2
  193. package/src/mcp/smithery-auth.test.ts +29 -0
  194. package/src/mcp/smithery-auth.ts +3 -2
  195. package/src/memories/index.ts +40 -20
  196. package/src/memory-backend/tool-names.ts +2 -0
  197. package/src/mnemopi/backend.ts +24 -7
  198. package/src/modes/acp/acp-agent.ts +56 -10
  199. package/src/modes/acp/acp-event-mapper.ts +8 -1
  200. package/src/modes/components/agent-hub.ts +1 -1
  201. package/src/modes/components/assistant-message.ts +7 -2
  202. package/src/modes/components/custom-message.ts +4 -1
  203. package/src/modes/components/login-dialog.ts +14 -4
  204. package/src/modes/components/message-frame.ts +14 -8
  205. package/src/modes/components/plan-review-overlay.ts +350 -35
  206. package/src/modes/components/session-account-selector.ts +62 -0
  207. package/src/modes/components/settings-defs.ts +23 -1
  208. package/src/modes/components/settings-selector.ts +169 -1
  209. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  210. package/src/modes/components/status-line/component.ts +123 -2
  211. package/src/modes/components/tool-execution.ts +28 -112
  212. package/src/modes/controllers/command-controller.ts +56 -15
  213. package/src/modes/controllers/event-controller.ts +111 -16
  214. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  215. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  216. package/src/modes/controllers/input-controller.ts +47 -39
  217. package/src/modes/controllers/live-command-controller.ts +255 -0
  218. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  219. package/src/modes/controllers/omfg-controller.ts +44 -40
  220. package/src/modes/controllers/selector-controller.ts +80 -10
  221. package/src/modes/controllers/session-focus-controller.ts +6 -1
  222. package/src/modes/interactive-mode.ts +176 -38
  223. package/src/modes/runtime-init.ts +2 -0
  224. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  225. package/src/modes/theme/theme.ts +1 -1
  226. package/src/modes/types.ts +4 -1
  227. package/src/modes/utils/context-usage.ts +23 -7
  228. package/src/modes/utils/ui-helpers.ts +10 -5
  229. package/src/plan-mode/plan-files.ts +40 -0
  230. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  231. package/src/prompts/bench/cache-prefix.md +3 -0
  232. package/src/prompts/bench/cache-suffix.md +1 -0
  233. package/src/prompts/system/computer-safety.md +14 -0
  234. package/src/prompts/system/custom-system-prompt.md +1 -1
  235. package/src/prompts/system/project-prompt.md +8 -1
  236. package/src/prompts/system/subagent-async-pending.md +6 -0
  237. package/src/prompts/system/system-prompt.md +1 -1
  238. package/src/prompts/system/workflow-notice.md +48 -40
  239. package/src/prompts/system/xdev-mount-notice.md +4 -0
  240. package/src/prompts/tools/computer.md +26 -0
  241. package/src/prompts/tools/todo.md +3 -1
  242. package/src/registry/agent-lifecycle.ts +116 -46
  243. package/src/registry/agent-registry.ts +35 -8
  244. package/src/registry/persisted-agents.ts +26 -2
  245. package/src/sdk.ts +215 -136
  246. package/src/secrets/index.ts +127 -4
  247. package/src/secrets/obfuscator.ts +2328 -105
  248. package/src/session/acp-permission-gate.ts +165 -0
  249. package/src/session/agent-session-error-log.test.ts +1 -1
  250. package/src/session/agent-session-events.ts +66 -0
  251. package/src/session/agent-session-types.ts +343 -0
  252. package/src/session/agent-session.ts +1940 -12218
  253. package/src/session/artifacts.ts +6 -5
  254. package/src/session/async-job-delivery.ts +74 -0
  255. package/src/session/auth-storage.ts +1 -0
  256. package/src/session/bash-runner.ts +326 -0
  257. package/src/session/blob-store.test.ts +56 -0
  258. package/src/session/blob-store.ts +18 -2
  259. package/src/session/checkpoint-entries.ts +81 -0
  260. package/src/session/eval-runner.ts +217 -0
  261. package/src/session/indexed-session-storage.ts +9 -2
  262. package/src/session/irc-bridge.ts +203 -0
  263. package/src/session/messages.ts +266 -1
  264. package/src/session/model-controls.ts +714 -0
  265. package/src/session/prewalk.ts +252 -0
  266. package/src/session/queued-messages.ts +93 -0
  267. package/src/session/redis-session-storage.ts +51 -11
  268. package/src/session/retry-fallback-chains.ts +455 -0
  269. package/src/session/role-models.ts +85 -0
  270. package/src/session/session-advisors.ts +1679 -0
  271. package/src/session/session-context.ts +6 -3
  272. package/src/session/session-entries.ts +8 -0
  273. package/src/session/session-handoff.ts +308 -0
  274. package/src/session/session-history-format.ts +20 -9
  275. package/src/session/session-loader.ts +0 -46
  276. package/src/session/session-maintenance.ts +2983 -0
  277. package/src/session/session-manager.ts +390 -30
  278. package/src/session/session-memory.ts +222 -0
  279. package/src/session/session-provider-boundary.ts +307 -0
  280. package/src/session/session-stats.ts +293 -0
  281. package/src/session/session-tools.ts +980 -0
  282. package/src/session/session-workspace.ts +53 -0
  283. package/src/session/stream-guards.ts +417 -0
  284. package/src/session/streaming-output.ts +52 -5
  285. package/src/session/todo-tracker.ts +380 -0
  286. package/src/session/ttsr-coordinator.ts +496 -0
  287. package/src/session/turn-recovery.ts +1629 -0
  288. package/src/slash-commands/builtin-registry.ts +257 -20
  289. package/src/slash-commands/helpers/session-pin.ts +44 -0
  290. package/src/slash-commands/helpers/usage-report.ts +22 -3
  291. package/src/stt/downloader.ts +0 -2
  292. package/src/stt/index.ts +0 -2
  293. package/src/stt/stt-controller.ts +57 -146
  294. package/src/system-prompt.ts +45 -17
  295. package/src/task/executor.ts +285 -70
  296. package/src/task/index.ts +98 -105
  297. package/src/task/output-manager.ts +25 -3
  298. package/src/task/persisted-revive.ts +4 -3
  299. package/src/task/structured-subagent.ts +1 -0
  300. package/src/tiny/title-client.ts +22 -0
  301. package/src/tools/approval.ts +55 -10
  302. package/src/tools/bash-interactive.ts +116 -86
  303. package/src/tools/bash-skill-urls.ts +28 -2
  304. package/src/tools/bash.ts +339 -123
  305. package/src/tools/browser/launch.ts +9 -1
  306. package/src/tools/browser/tab-supervisor.ts +8 -5
  307. package/src/tools/browser.ts +94 -47
  308. package/src/tools/builtin-names.ts +1 -0
  309. package/src/tools/computer/protocol.ts +28 -0
  310. package/src/tools/computer/supervisor.ts +258 -0
  311. package/src/tools/computer/worker-entry.ts +25 -0
  312. package/src/tools/computer/worker.ts +135 -0
  313. package/src/tools/computer-renderer.ts +108 -0
  314. package/src/tools/computer.ts +433 -0
  315. package/src/tools/context.ts +2 -0
  316. package/src/tools/default-renderer.ts +139 -0
  317. package/src/tools/essential-tools.ts +1 -0
  318. package/src/tools/image-gen.ts +20 -28
  319. package/src/tools/image-providers.ts +50 -0
  320. package/src/tools/index.ts +14 -0
  321. package/src/tools/path-utils.ts +49 -2
  322. package/src/tools/read.ts +156 -88
  323. package/src/tools/renderers.ts +9 -1
  324. package/src/tools/todo.ts +101 -11
  325. package/src/tools/vibe.ts +1 -2
  326. package/src/tools/write.ts +51 -1
  327. package/src/tools/xdev.ts +121 -29
  328. package/src/tts/streaming-player.ts +82 -283
  329. package/src/tts/vocalizer.ts +18 -1
  330. package/src/utils/clipboard.ts +1 -30
  331. package/src/utils/jj.ts +125 -4
  332. package/src/utils/lang-from-path.ts +7 -0
  333. package/src/utils/mac-file-urls.applescript +37 -0
  334. package/src/utils/title-generator.ts +137 -1
  335. package/src/utils/tool-choice.ts +14 -0
  336. package/src/utils/tools-manager.ts +1 -19
  337. package/src/vibe/runtime.ts +852 -72
  338. package/src/web/search/index.ts +3 -6
  339. package/src/web/search/provider.ts +31 -16
  340. package/src/web/search/providers/xai.ts +7 -1
  341. package/src/web/search/types.ts +3 -0
  342. package/dist/types/stt/recorder.d.ts +0 -30
  343. package/dist/types/stt/transcriber.d.ts +0 -14
  344. package/dist/types/stt/wav.d.ts +0 -29
  345. package/dist/types/tts/player.d.ts +0 -32
  346. package/src/stt/recorder.ts +0 -551
  347. package/src/stt/transcriber.ts +0 -60
  348. package/src/stt/wav.ts +0 -173
  349. package/src/tts/player.ts +0 -137
@@ -0,0 +1,233 @@
1
+ /** Frameless Bidi model used by Codex Desktop live calls. */
2
+ export const LIVE_MODEL: "gpt-live-1-codex" = "gpt-live-1-codex";
3
+
4
+ /** Maximum UTF-8 payload size accepted by each context append. */
5
+ export const CONTEXT_CHUNK_BYTES = 500;
6
+
7
+ /** Semantic stream selected for appended Frameless Bidi context. */
8
+ export type LiveContextChannel = "speakable" | "commentary";
9
+
10
+ /** Text content item accepted by Frameless Bidi context appends. */
11
+ export type LiveInputTextContent = { type: "input_text"; text: string };
12
+
13
+ /** Session object posted alongside the SDP when opening a live call. */
14
+ export type LiveSessionPayload = {
15
+ model: typeof LIVE_MODEL;
16
+ instructions: string;
17
+ audio: { output: { voice: string } };
18
+ delegation: { type: "client" };
19
+ };
20
+
21
+ /** Messages sent by the client over the Frameless Bidi data channel. */
22
+ export type LiveClientMessage =
23
+ | {
24
+ type: "delegation.context.append";
25
+ delegation_item_id: string;
26
+ channel?: LiveContextChannel;
27
+ content: LiveInputTextContent[];
28
+ }
29
+ | {
30
+ type: "session.context.append";
31
+ channel?: LiveContextChannel;
32
+ content: LiveInputTextContent[];
33
+ }
34
+ | { type: "session.close" };
35
+
36
+ /** Parsed Frameless Bidi server events, including unsupported wire event types. */
37
+ export type LiveServerEvent =
38
+ | {
39
+ type: "session.started" | "session.updated";
40
+ session: { id: string; instructions?: string };
41
+ }
42
+ | { type: "output_audio.delta"; audio: string }
43
+ | { type: "input_transcript.added" | "output_transcript.added"; item: { text: string } }
44
+ | { type: "turn.done"; turn: { role: "user" | "assistant"; transcript: string } }
45
+ | {
46
+ type: "delegation.created";
47
+ item: {
48
+ type: "delegation";
49
+ target: "client";
50
+ id: string;
51
+ content: LiveInputTextContent[];
52
+ };
53
+ }
54
+ | { type: "error"; message: string }
55
+ | { type: "unknown"; wireType: string };
56
+
57
+ type UnknownRecord = Record<string, unknown>;
58
+
59
+ function isRecord(value: unknown): value is UnknownRecord {
60
+ return typeof value === "object" && value !== null && !Array.isArray(value);
61
+ }
62
+
63
+ function parsePayload(payload: unknown): UnknownRecord | null {
64
+ let parsed = payload;
65
+ if (typeof payload === "string") {
66
+ try {
67
+ parsed = JSON.parse(payload);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ return isRecord(parsed) ? parsed : null;
73
+ }
74
+
75
+ function parseSessionEvent(
76
+ type: "session.started" | "session.updated",
77
+ payload: UnknownRecord,
78
+ ): LiveServerEvent | null {
79
+ const session = payload.session;
80
+ if (!isRecord(session) || typeof session.id !== "string") return null;
81
+ if (typeof session.instructions === "string") {
82
+ return { type, session: { id: session.id, instructions: session.instructions } };
83
+ }
84
+ return { type, session: { id: session.id } };
85
+ }
86
+
87
+ function parseTranscriptAddedEvent(
88
+ type: "input_transcript.added" | "output_transcript.added",
89
+ payload: UnknownRecord,
90
+ ): LiveServerEvent | null {
91
+ const item = payload.item;
92
+ if (!isRecord(item) || typeof item.text !== "string") return null;
93
+ return { type, item: { text: item.text } };
94
+ }
95
+
96
+ function parseTurnDoneEvent(payload: UnknownRecord): LiveServerEvent | null {
97
+ const turn = payload.turn;
98
+ if (!isRecord(turn) || (turn.role !== "user" && turn.role !== "assistant")) return null;
99
+ if (typeof turn.transcript !== "string") return null;
100
+ return { type: "turn.done", turn: { role: turn.role, transcript: turn.transcript } };
101
+ }
102
+
103
+ function parseDelegationCreatedEvent(payload: UnknownRecord): LiveServerEvent | null {
104
+ const item = payload.item;
105
+ if (!isRecord(item) || item.type !== "delegation" || item.target !== "client" || typeof item.id !== "string") {
106
+ return null;
107
+ }
108
+ if (!Array.isArray(item.content)) return null;
109
+ const content: LiveInputTextContent[] = [];
110
+ for (const candidate of item.content) {
111
+ if (!isRecord(candidate) || candidate.type !== "input_text" || typeof candidate.text !== "string") continue;
112
+ content.push({ type: "input_text", text: candidate.text });
113
+ }
114
+ return {
115
+ type: "delegation.created",
116
+ item: { type: "delegation", target: "client", id: item.id, content },
117
+ };
118
+ }
119
+
120
+ function stringifyErrorValue(value: unknown): string | null {
121
+ if (typeof value === "string") return value;
122
+ if (value === undefined) return null;
123
+ try {
124
+ return JSON.stringify(value) ?? null;
125
+ } catch {
126
+ return String(value);
127
+ }
128
+ }
129
+
130
+ function parseErrorEvent(payload: UnknownRecord): LiveServerEvent | null {
131
+ if (typeof payload.message === "string") return { type: "error", message: payload.message };
132
+ const error = payload.error;
133
+ if (isRecord(error) && typeof error.message === "string") {
134
+ return { type: "error", message: error.message };
135
+ }
136
+ const message = stringifyErrorValue(error);
137
+ return message === null ? null : { type: "error", message };
138
+ }
139
+
140
+ /** Parse a JSON string or decoded value from the Frameless Bidi data channel. */
141
+ export function parseLiveServerEvent(payload: unknown): LiveServerEvent | null {
142
+ const parsed = parsePayload(payload);
143
+ if (!parsed || typeof parsed.type !== "string") return null;
144
+
145
+ switch (parsed.type) {
146
+ case "session.started":
147
+ case "session.updated":
148
+ return parseSessionEvent(parsed.type, parsed);
149
+ case "output_audio.delta":
150
+ return typeof parsed.audio === "string" ? { type: parsed.type, audio: parsed.audio } : null;
151
+ case "input_transcript.added":
152
+ case "output_transcript.added":
153
+ return parseTranscriptAddedEvent(parsed.type, parsed);
154
+ case "turn.done":
155
+ return parseTurnDoneEvent(parsed);
156
+ case "delegation.created":
157
+ return parseDelegationCreatedEvent(parsed);
158
+ case "error":
159
+ return parseErrorEvent(parsed);
160
+ default:
161
+ return { type: "unknown", wireType: parsed.type };
162
+ }
163
+ }
164
+
165
+ /** Build the session object posted in the multipart WebRTC call request. */
166
+ export function buildLiveSessionPayload(instructions: string, voice: string): LiveSessionPayload {
167
+ return {
168
+ model: LIVE_MODEL,
169
+ instructions,
170
+ audio: { output: { voice } },
171
+ delegation: { type: "client" },
172
+ };
173
+ }
174
+
175
+ /** Build a context append associated with a server-created delegation. */
176
+ export function buildDelegationContextAppend(
177
+ delegationItemId: string,
178
+ text: string,
179
+ channel?: LiveContextChannel,
180
+ ): LiveClientMessage {
181
+ return {
182
+ type: "delegation.context.append",
183
+ delegation_item_id: delegationItemId,
184
+ ...(channel === undefined ? {} : { channel }),
185
+ content: [{ type: "input_text", text }],
186
+ };
187
+ }
188
+
189
+ /** Build context appended to the live session outside a delegation. */
190
+ export function buildSessionContextAppend(text: string, channel?: LiveContextChannel): LiveClientMessage {
191
+ return {
192
+ type: "session.context.append",
193
+ ...(channel === undefined ? {} : { channel }),
194
+ content: [{ type: "input_text", text }],
195
+ };
196
+ }
197
+
198
+ /** Build the message that gracefully closes a live session. */
199
+ export function buildSessionClose(): LiveClientMessage {
200
+ return { type: "session.close" };
201
+ }
202
+
203
+ function utf8ByteLength(codePoint: number): number {
204
+ if (codePoint <= 0x7f) return 1;
205
+ if (codePoint <= 0x7ff) return 2;
206
+ if (codePoint <= 0xffff) return 3;
207
+ return 4;
208
+ }
209
+
210
+ /** Split context into character-safe chunks of at most 500 UTF-8 bytes. */
211
+ export function chunkLiveContext(text: string): string[] {
212
+ if (text.length === 0) return [""];
213
+
214
+ const chunks: string[] = [];
215
+ let chunkStart = 0;
216
+ let chunkBytes = 0;
217
+ let index = 0;
218
+ while (index < text.length) {
219
+ const codePoint = text.codePointAt(index);
220
+ if (codePoint === undefined) break;
221
+ const characterLength = codePoint > 0xffff ? 2 : 1;
222
+ const characterBytes = utf8ByteLength(codePoint);
223
+ if (chunkBytes + characterBytes > CONTEXT_CHUNK_BYTES) {
224
+ chunks.push(text.slice(chunkStart, index));
225
+ chunkStart = index;
226
+ chunkBytes = 0;
227
+ }
228
+ chunkBytes += characterBytes;
229
+ index += characterLength;
230
+ }
231
+ chunks.push(text.slice(chunkStart));
232
+ return chunks;
233
+ }
@@ -0,0 +1,422 @@
1
+ import { type AuthStorage, isAuthRetryableError, type OAuthAccess, withOAuthAccess } from "@oh-my-pi/pi-ai";
2
+ import { getProxyForProvider, wrapFetchForProxy } from "@oh-my-pi/pi-ai/utils/proxy";
3
+ import {
4
+ CODEX_BASE_URL,
5
+ CODEX_CLIENT_VERSION,
6
+ getCodexAccountId,
7
+ OPENAI_HEADERS,
8
+ } from "@oh-my-pi/pi-catalog/wire/codex";
9
+ import { LiveWebRtcPeer } from "@oh-my-pi/pi-natives";
10
+ import { generateCodexAttestation } from "./attestation";
11
+ import {
12
+ buildLiveSessionPayload,
13
+ type LiveClientMessage,
14
+ type LiveServerEvent,
15
+ parseLiveServerEvent,
16
+ } from "./protocol";
17
+
18
+ const SIGNALING_URL = `${CODEX_BASE_URL}/codex/realtime/calls?intent=quicksilver&architecture=avas`;
19
+ const MAX_ERROR_BODY_LENGTH = 2_048;
20
+ const SIDEBAND_CONNECT_ATTEMPTS = 5;
21
+ const SIDEBAND_CONNECT_TIMEOUT_MS = 15_000;
22
+ const LIVE_PROVIDER = "openai-codex";
23
+ const LIVE_ORIGINATOR = "Codex Desktop";
24
+ const LIVE_CALL_ID_PATTERN = /^rtc_[\w-]+$/;
25
+
26
+ type Lifecycle = "idle" | "connecting" | "connected" | "closing" | "closed";
27
+
28
+ interface LiveSignalingResult {
29
+ answer: string;
30
+ callId: string;
31
+ access: OAuthAccess;
32
+ attestation: string | undefined;
33
+ }
34
+
35
+ class LiveSignalingError extends Error {
36
+ status: number;
37
+ errorMessage: string;
38
+
39
+ constructor(status: number, message: string) {
40
+ super(message);
41
+ this.name = "LiveSignalingError";
42
+ this.status = status;
43
+ this.errorMessage = message;
44
+ }
45
+ }
46
+
47
+ /** Callbacks emitted by the live WebRTC transport. */
48
+ export interface LiveTransportCallbacks {
49
+ onEvent(event: LiveServerEvent): void;
50
+ onOutputLevel(level: number): void;
51
+ }
52
+
53
+ /** Configuration required to establish a Codex live call. */
54
+ export interface LiveTransportOptions {
55
+ authStorage: AuthStorage;
56
+ sessionId: string;
57
+ instructions: string;
58
+ voice: string;
59
+ callbacks: LiveTransportCallbacks;
60
+ signal?: AbortSignal;
61
+ }
62
+
63
+ /** Extracts the server-assigned `rtc_*` call ID from a signaling Location header. */
64
+ export function parseLiveCallId(location: string | null): string | undefined {
65
+ if (!location) return undefined;
66
+ return location
67
+ .split("?", 1)[0]
68
+ ?.split("/")
69
+ .find(segment => LIVE_CALL_ID_PATTERN.test(segment));
70
+ }
71
+
72
+ /** Builds the Frameless Bidi sideband WebSocket URL for an accepted Codex call. */
73
+ export function buildLiveSidebandUrl(callId: string): string {
74
+ const url = new URL(`https://api.openai.com/v1/live/${encodeURIComponent(callId)}`);
75
+ url.protocol = "wss:";
76
+ return url.toString();
77
+ }
78
+
79
+ function liveSessionHeaders(
80
+ access: OAuthAccess,
81
+ sessionId: string,
82
+ realtimeSessionId: string,
83
+ attestation: string | undefined,
84
+ ): Record<string, string> {
85
+ const headers: Record<string, string> = {
86
+ Authorization: `Bearer ${access.accessToken}`,
87
+ "OpenAI-Alpha": "quicksilver=v2",
88
+ "User-Agent": `Codex Desktop/${CODEX_CLIENT_VERSION}`,
89
+ "x-session-id": realtimeSessionId,
90
+ [OPENAI_HEADERS.ORIGINATOR]: LIVE_ORIGINATOR,
91
+ [OPENAI_HEADERS.VERSION]: CODEX_CLIENT_VERSION,
92
+ [OPENAI_HEADERS.SCOPED_SESSION_ID]: sessionId,
93
+ [OPENAI_HEADERS.THREAD_ID]: sessionId,
94
+ };
95
+ const accountId = access.accountId ?? getCodexAccountId(access.accessToken);
96
+ if (accountId) headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
97
+ if (attestation) headers["x-oai-attestation"] = attestation;
98
+ return headers;
99
+ }
100
+
101
+ function boundedErrorBody(body: string, statusText: string): string {
102
+ const normalized = body.trim().replaceAll(/\s+/g, " ");
103
+ if (!normalized) return statusText || "empty response body";
104
+ if (normalized.length <= MAX_ERROR_BODY_LENGTH) return normalized;
105
+ return `${normalized.slice(0, MAX_ERROR_BODY_LENGTH)}…`;
106
+ }
107
+
108
+ function isAuthError(error: unknown): boolean {
109
+ return isAuthRetryableError(error);
110
+ }
111
+
112
+ function abortReason(signal: AbortSignal | undefined): Error {
113
+ if (signal?.reason instanceof Error) return signal.reason;
114
+ return new DOMException("Live connection aborted", "AbortError");
115
+ }
116
+
117
+ /** Native WebRTC transport for a Codex Frameless Bidi live session. */
118
+ export class CodexLiveTransport {
119
+ readonly #options: LiveTransportOptions;
120
+ #peer: LiveWebRtcPeer | undefined;
121
+ readonly #realtimeSessionId = crypto.randomUUID();
122
+ #sideband: Bun.WebSocket | undefined;
123
+ #state: Lifecycle = "idle";
124
+ #connectPromise: Promise<void> | undefined;
125
+ #closePromise: Promise<void> | undefined;
126
+ #sendTail: Promise<void> = Promise.resolve();
127
+ #muted = false;
128
+ #unexpectedFailureReported = false;
129
+ readonly #abortListener: () => void;
130
+
131
+ constructor(options: LiveTransportOptions) {
132
+ this.#options = options;
133
+ this.#abortListener = () => {
134
+ void this.close();
135
+ };
136
+ if (!options.signal?.aborted) options.signal?.addEventListener("abort", this.#abortListener, { once: true });
137
+ }
138
+
139
+ /** Establish the native peer, perform Codex signaling, and wait for the data channel. */
140
+ connect(): Promise<void> {
141
+ if (this.#state === "connected") return Promise.resolve();
142
+ if (this.#connectPromise) return this.#connectPromise;
143
+ if (this.#state === "closing" || this.#state === "closed")
144
+ return Promise.reject(new Error("Live transport is closed"));
145
+ if (this.#options.signal?.aborted) return Promise.reject(abortReason(this.#options.signal));
146
+ this.#state = "connecting";
147
+ const operation = this.#connect().catch(async error => {
148
+ await this.close();
149
+ throw error;
150
+ });
151
+ this.#connectPromise = operation;
152
+ return operation;
153
+ }
154
+
155
+ async #connect(): Promise<void> {
156
+ const peer = new LiveWebRtcPeer(
157
+ (error, payload) => {
158
+ if (error) {
159
+ this.#handlePeerFailure(error.message);
160
+ } else {
161
+ this.#handleServerEvent(payload);
162
+ }
163
+ },
164
+ (error, level) => {
165
+ if (error) {
166
+ this.#handlePeerFailure(error.message);
167
+ } else {
168
+ this.#handleOutputLevel(level);
169
+ }
170
+ },
171
+ (error, message) => this.#handlePeerFailure(error?.message ?? message),
172
+ );
173
+ this.#peer = peer;
174
+ const offer = await peer.createOffer();
175
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
176
+ const signaling = await this.#signal(offer);
177
+ await peer.acceptAnswer(signaling.answer);
178
+ peer.setMuted(this.#muted);
179
+ await peer.waitForOpen();
180
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
181
+ await this.#connectSideband(signaling.callId, signaling.access, signaling.attestation);
182
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
183
+ this.#state = "connected";
184
+ }
185
+
186
+ async #signal(offer: string): Promise<LiveSignalingResult> {
187
+ const attestation = await generateCodexAttestation();
188
+ return await withOAuthAccess(
189
+ this.#options.authStorage,
190
+ LIVE_PROVIDER,
191
+ access => this.#signalWithAccess(offer, access, attestation),
192
+ {
193
+ sessionId: this.#options.sessionId,
194
+ signal: this.#options.signal,
195
+ isAuthError,
196
+ missingAccessMessage: "No Codex OAuth credential is available for a live call.",
197
+ },
198
+ );
199
+ }
200
+
201
+ async #signalWithAccess(
202
+ offer: string,
203
+ access: OAuthAccess,
204
+ attestation: string | undefined,
205
+ ): Promise<LiveSignalingResult> {
206
+ const headers = new Headers({
207
+ ...liveSessionHeaders(access, this.#options.sessionId, this.#realtimeSessionId, attestation),
208
+ Accept: "*/*",
209
+ "Content-Type": "application/json",
210
+ });
211
+ const fetchImpl = wrapFetchForProxy(fetch, LIVE_PROVIDER);
212
+ const response = await fetchImpl(SIGNALING_URL, {
213
+ method: "POST",
214
+ headers,
215
+ body: JSON.stringify({
216
+ sdp: offer,
217
+ session: buildLiveSessionPayload(this.#options.instructions, this.#options.voice),
218
+ }),
219
+ signal: this.#options.signal,
220
+ });
221
+ const responseBody = await response.text();
222
+ if (!response.ok) {
223
+ const detail = boundedErrorBody(responseBody, response.statusText);
224
+ throw new LiveSignalingError(response.status, `Codex live signaling failed (${response.status}): ${detail}`);
225
+ }
226
+ const answer = responseBody;
227
+ if (!answer.trim())
228
+ throw new LiveSignalingError(response.status, "Codex live signaling returned an empty SDP answer");
229
+ const callId = parseLiveCallId(response.headers.get("location"));
230
+ if (!callId) {
231
+ throw new LiveSignalingError(response.status, "Codex live signaling returned no valid call ID");
232
+ }
233
+ return { answer, callId, access, attestation };
234
+ }
235
+
236
+ async #connectSideband(callId: string, access: OAuthAccess, attestation: string | undefined): Promise<void> {
237
+ let failure = new Error("Codex live sideband connection failed");
238
+ for (let attempt = 0; attempt < SIDEBAND_CONNECT_ATTEMPTS; attempt++) {
239
+ try {
240
+ await this.#openSideband(callId, access, attestation);
241
+ return;
242
+ } catch (cause) {
243
+ failure = cause instanceof Error ? cause : new Error(String(cause));
244
+ if (this.#options.signal?.aborted) throw abortReason(this.#options.signal);
245
+ if (attempt + 1 < SIDEBAND_CONNECT_ATTEMPTS) await Bun.sleep(200 * 2 ** attempt);
246
+ }
247
+ }
248
+ throw failure;
249
+ }
250
+
251
+ async #openSideband(callId: string, access: OAuthAccess, attestation: string | undefined): Promise<void> {
252
+ const url = buildLiveSidebandUrl(callId);
253
+ const options = {
254
+ headers: liveSessionHeaders(access, this.#options.sessionId, this.#realtimeSessionId, attestation),
255
+ proxy: getProxyForProvider(LIVE_PROVIDER),
256
+ } satisfies Bun.WebSocketOptions;
257
+ const socket: Bun.WebSocket = Reflect.construct(WebSocket, [url, options]);
258
+ socket.binaryType = "nodebuffer";
259
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
260
+ let opened = false;
261
+ let settled = false;
262
+ let timeout: NodeJS.Timeout | undefined;
263
+ const cleanup = (): void => {
264
+ if (timeout) {
265
+ clearTimeout(timeout);
266
+ timeout = undefined;
267
+ }
268
+ this.#options.signal?.removeEventListener("abort", onAbort);
269
+ };
270
+ const rejectConnect = (error: Error): void => {
271
+ if (settled) return;
272
+ settled = true;
273
+ cleanup();
274
+ reject(error);
275
+ };
276
+ const onAbort = (): void => {
277
+ socket.close(1000, "aborted");
278
+ rejectConnect(abortReason(this.#options.signal));
279
+ };
280
+ socket.onopen = () => {
281
+ if (settled) {
282
+ socket.close(1000, "stale");
283
+ return;
284
+ }
285
+ opened = true;
286
+ settled = true;
287
+ cleanup();
288
+ this.#sideband = socket;
289
+ resolve();
290
+ };
291
+ socket.onmessage = event => {
292
+ if (typeof event.data !== "string") {
293
+ this.#reportFailure("Codex live sideband returned an unexpected binary frame.");
294
+ return;
295
+ }
296
+ this.#handleSidebandEvent(event.data);
297
+ };
298
+ socket.onerror = event => {
299
+ const detail = event instanceof ErrorEvent && event.message ? `: ${event.message}` : "";
300
+ if (!opened) {
301
+ rejectConnect(new Error(`Codex live sideband connection failed${detail}`));
302
+ socket.close(1011, "connection failed");
303
+ return;
304
+ }
305
+ this.#reportFailure(`Codex live sideband failed${detail}`);
306
+ };
307
+ socket.onclose = event => {
308
+ if (!opened) {
309
+ rejectConnect(new Error(`Codex live sideband closed before connecting (${event.code})`));
310
+ return;
311
+ }
312
+ if (this.#sideband !== socket) return;
313
+ this.#sideband = undefined;
314
+ if (this.#state === "connecting" || this.#state === "connected") {
315
+ const detail = event.reason ? `: ${event.reason}` : "";
316
+ this.#reportFailure(`Codex live sideband closed (${event.code})${detail}`);
317
+ }
318
+ };
319
+ if (this.#options.signal?.aborted) {
320
+ onAbort();
321
+ } else {
322
+ this.#options.signal?.addEventListener("abort", onAbort, { once: true });
323
+ timeout = setTimeout(() => {
324
+ socket.close(1000, "connect timeout");
325
+ rejectConnect(new Error("Codex live sideband connection timed out"));
326
+ }, SIDEBAND_CONNECT_TIMEOUT_MS);
327
+ timeout.unref?.();
328
+ }
329
+ await promise;
330
+ }
331
+
332
+ #handleSidebandEvent(payload: string): void {
333
+ if (this.#state === "closing" || this.#state === "closed") return;
334
+ const event = parseLiveServerEvent(payload);
335
+ if (!event) return;
336
+ try {
337
+ this.#options.callbacks.onEvent(event);
338
+ } catch {}
339
+ }
340
+
341
+ #handleServerEvent(payload: string): void {
342
+ if (this.#state === "closing" || this.#state === "closed") return;
343
+ const event = parseLiveServerEvent(payload);
344
+ if (!event || (this.#sideband?.readyState === WebSocket.OPEN && event.type !== "error")) return;
345
+ try {
346
+ this.#options.callbacks.onEvent(event);
347
+ } catch {}
348
+ }
349
+
350
+ #handleOutputLevel(level: number): void {
351
+ if (this.#state !== "connected" || !Number.isFinite(level)) return;
352
+ try {
353
+ this.#options.callbacks.onOutputLevel(Math.min(1, Math.max(0, level)));
354
+ } catch {}
355
+ }
356
+
357
+ #handlePeerFailure(message: string): void {
358
+ this.#reportFailure(message);
359
+ }
360
+
361
+ #reportFailure(message: string): void {
362
+ if ((this.#state !== "connecting" && this.#state !== "connected") || this.#unexpectedFailureReported) {
363
+ return;
364
+ }
365
+ this.#unexpectedFailureReported = true;
366
+ try {
367
+ this.#options.callbacks.onEvent({ type: "error", message });
368
+ } catch {}
369
+ }
370
+
371
+ /** Serialize one Frameless Bidi control message onto the call's sideband WebSocket. */
372
+ send(message: LiveClientMessage): Promise<void> {
373
+ const operation = this.#sendTail.then(() => {
374
+ if (this.#state !== "connected") throw new Error("Live transport is not connected");
375
+ const sideband = this.#sideband;
376
+ if (!sideband || sideband.readyState !== WebSocket.OPEN) {
377
+ throw new Error("Codex live sideband is not connected");
378
+ }
379
+ sideband.send(JSON.stringify(message));
380
+ });
381
+ this.#sendTail = operation.catch(() => {});
382
+ return operation;
383
+ }
384
+
385
+ /** Queue 16 kHz mono Float32 PCM for native Opus transmission. */
386
+ pushAudio(samples: Float32Array): void {
387
+ if (this.#state !== "connected" || this.#muted || samples.length === 0) return;
388
+ this.#peer?.pushAudio(samples);
389
+ }
390
+
391
+ /** Enable or disable the native audio source and discard partial input when muted. */
392
+ async setMuted(muted: boolean): Promise<void> {
393
+ this.#muted = muted;
394
+ if (this.#state === "connected") this.#peer?.setMuted(muted);
395
+ }
396
+
397
+ /** Stop sideband signaling and the native WebRTC media peer. Safe to call repeatedly. */
398
+ close(): Promise<void> {
399
+ if (this.#closePromise) return this.#closePromise;
400
+ this.#state = "closing";
401
+ const operation = this.#close();
402
+ this.#closePromise = operation;
403
+ return operation;
404
+ }
405
+
406
+ async #close(): Promise<void> {
407
+ this.#options.signal?.removeEventListener("abort", this.#abortListener);
408
+ const sideband = this.#sideband;
409
+ const peer = this.#peer;
410
+ this.#sideband = undefined;
411
+ this.#peer = undefined;
412
+ if (sideband && (sideband.readyState === WebSocket.OPEN || sideband.readyState === WebSocket.CONNECTING)) {
413
+ sideband.close(1000, "done");
414
+ }
415
+ if (peer) {
416
+ try {
417
+ await peer.close();
418
+ } catch {}
419
+ }
420
+ this.#state = "closed";
421
+ }
422
+ }