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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  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 +316 -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/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -0,0 +1,490 @@
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_HEADER_VALUES,
8
+ OPENAI_HEADERS,
9
+ } from "@oh-my-pi/pi-catalog/wire/codex";
10
+ import type { Browser, HTTPRequest, Page } from "puppeteer-core";
11
+ import { launchHeadlessBrowser } from "../tools/browser/launch";
12
+ import audioWorkletSource from "./audio-worklet.txt" with { type: "text" };
13
+ import browserRuntimeSource from "./browser-runtime.txt" with { type: "text" };
14
+ import {
15
+ buildLiveSessionPayload,
16
+ type LiveClientMessage,
17
+ type LiveServerEvent,
18
+ parseLiveServerEvent,
19
+ } from "./protocol";
20
+
21
+ const SIGNALING_URL = `${CODEX_BASE_URL}/codex/realtime/calls?intent=quicksilver&architecture=avas`;
22
+ const MAX_ERROR_BODY_LENGTH = 2_048;
23
+ const MAX_HOST_AUDIO_SAMPLES = 32_000;
24
+ const SIDEBAND_CONNECT_ATTEMPTS = 5;
25
+ const SIDEBAND_CONNECT_TIMEOUT_MS = 15_000;
26
+ const LIVE_PROVIDER = "openai-codex";
27
+ const LIVE_CALL_ID_PATTERN = /^rtc_[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i;
28
+
29
+ type Lifecycle = "idle" | "connecting" | "connected" | "closing" | "closed";
30
+
31
+ type BrowserLiveApi = {
32
+ start(workletSource: string): Promise<string>;
33
+ acceptAnswer(sdp: string): Promise<void>;
34
+ waitForOpen(): Promise<void>;
35
+ send(payload: string): void;
36
+ pushAudio(payload: string): void;
37
+ setMuted(muted: boolean): void;
38
+ close(): Promise<void>;
39
+ };
40
+
41
+ declare global {
42
+ var ompCodexLive: BrowserLiveApi;
43
+ }
44
+
45
+ interface QueuedAudio {
46
+ payload: string;
47
+ sampleCount: number;
48
+ }
49
+
50
+ interface LiveSignalingResult {
51
+ answer: string;
52
+ callId: string;
53
+ access: OAuthAccess;
54
+ }
55
+
56
+ class LiveSignalingError extends Error {
57
+ status: number;
58
+ errorMessage: string;
59
+
60
+ constructor(status: number, message: string) {
61
+ super(message);
62
+ this.name = "LiveSignalingError";
63
+ this.status = status;
64
+ this.errorMessage = message;
65
+ }
66
+ }
67
+
68
+ /** Callbacks emitted by the live WebRTC transport. */
69
+ export interface LiveTransportCallbacks {
70
+ onEvent(event: LiveServerEvent): void;
71
+ onOutputLevel(level: number): void;
72
+ }
73
+
74
+ /** Configuration required to establish a Codex live call. */
75
+ export interface LiveTransportOptions {
76
+ authStorage: AuthStorage;
77
+ sessionId: string;
78
+ instructions: string;
79
+ voice: string;
80
+ callbacks: LiveTransportCallbacks;
81
+ signal?: AbortSignal;
82
+ }
83
+
84
+ /** Extracts the server-assigned `rtc_<uuid>` call ID from a signaling Location header. */
85
+ export function parseLiveCallId(location: string | null): string | undefined {
86
+ if (!location) return undefined;
87
+ return location
88
+ .split("?", 1)[0]
89
+ ?.split("/")
90
+ .find(segment => LIVE_CALL_ID_PATTERN.test(segment));
91
+ }
92
+
93
+ /** Builds the Frameless Bidi sideband WebSocket URL for an accepted Codex call. */
94
+ export function buildLiveSidebandUrl(callId: string): string {
95
+ const url = new URL(`${CODEX_BASE_URL}/codex/${encodeURIComponent(callId)}`);
96
+ url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
97
+ return url.toString();
98
+ }
99
+
100
+ function liveSessionHeaders(access: OAuthAccess, sessionId: string): Record<string, string> {
101
+ const headers: Record<string, string> = {
102
+ Authorization: `Bearer ${access.accessToken}`,
103
+ "OpenAI-Alpha": "quicksilver=v2",
104
+ "x-session-id": sessionId,
105
+ [OPENAI_HEADERS.ORIGINATOR]: OPENAI_HEADER_VALUES.ORIGINATOR_CODEX,
106
+ [OPENAI_HEADERS.VERSION]: CODEX_CLIENT_VERSION,
107
+ [OPENAI_HEADERS.SCOPED_SESSION_ID]: sessionId,
108
+ [OPENAI_HEADERS.THREAD_ID]: sessionId,
109
+ };
110
+ const accountId = access.accountId ?? getCodexAccountId(access.accessToken);
111
+ if (accountId) headers[OPENAI_HEADERS.ACCOUNT_ID] = accountId;
112
+ return headers;
113
+ }
114
+
115
+ function boundedErrorBody(body: string, statusText: string): string {
116
+ const normalized = body.trim().replaceAll(/\s+/g, " ");
117
+ if (!normalized) return statusText || "empty response body";
118
+ if (normalized.length <= MAX_ERROR_BODY_LENGTH) return normalized;
119
+ return `${normalized.slice(0, MAX_ERROR_BODY_LENGTH)}…`;
120
+ }
121
+
122
+ function isAuthError(error: unknown): boolean {
123
+ return isAuthRetryableError(error);
124
+ }
125
+
126
+ function abortReason(signal: AbortSignal | undefined): Error {
127
+ if (signal?.reason instanceof Error) return signal.reason;
128
+ return new DOMException("Live connection aborted", "AbortError");
129
+ }
130
+
131
+ function encodePcm(samples: Float32Array): string {
132
+ return Buffer.from(samples.buffer, samples.byteOffset, samples.byteLength).toString("base64");
133
+ }
134
+
135
+ /** Headless-Chromium WebRTC transport for a Codex Frameless Bidi live session. */
136
+ export class CodexLiveTransport {
137
+ readonly #options: LiveTransportOptions;
138
+ #browser: Browser | undefined;
139
+ #page: Page | undefined;
140
+ #sideband: Bun.WebSocket | undefined;
141
+ #state: Lifecycle = "idle";
142
+ #connectPromise: Promise<void> | undefined;
143
+ #closePromise: Promise<void> | undefined;
144
+ #sendTail: Promise<void> = Promise.resolve();
145
+ #audioQueue: QueuedAudio[] = [];
146
+ #queuedAudioSamples = 0;
147
+ #audioPumpRunning = false;
148
+ #muted = false;
149
+ #unexpectedFailureReported = false;
150
+ readonly #abortListener: () => void;
151
+
152
+ constructor(options: LiveTransportOptions) {
153
+ this.#options = options;
154
+ this.#abortListener = () => {
155
+ void this.close();
156
+ };
157
+ if (!options.signal?.aborted) options.signal?.addEventListener("abort", this.#abortListener, { once: true });
158
+ }
159
+
160
+ /** Establish the browser peer, perform Codex signaling, and wait for the data channel. */
161
+ connect(): Promise<void> {
162
+ if (this.#state === "connected") return Promise.resolve();
163
+ if (this.#connectPromise) return this.#connectPromise;
164
+ if (this.#state === "closing" || this.#state === "closed")
165
+ return Promise.reject(new Error("Live transport is closed"));
166
+ if (this.#options.signal?.aborted) return Promise.reject(abortReason(this.#options.signal));
167
+ this.#state = "connecting";
168
+ const operation = this.#connect().catch(async error => {
169
+ await this.close();
170
+ throw error;
171
+ });
172
+ this.#connectPromise = operation;
173
+ return operation;
174
+ }
175
+
176
+ async #connect(): Promise<void> {
177
+ const browser = await launchHeadlessBrowser({
178
+ headless: true,
179
+ args: ["--autoplay-policy=no-user-gesture-required"],
180
+ ignoreDefaultArgs: ["--mute-audio"],
181
+ });
182
+ this.#browser = browser;
183
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
184
+ const page = await browser.newPage();
185
+ this.#page = page;
186
+ await page.setRequestInterception(true);
187
+ const serveBlankPage = (request: HTTPRequest): void => {
188
+ void request
189
+ .respond({ status: 200, contentType: "text/html", body: "<!doctype html><title>Codex Live</title>" })
190
+ .catch(() => {});
191
+ };
192
+ page.on("request", serveBlankPage);
193
+ try {
194
+ await page.goto("http://localhost/", { waitUntil: "domcontentloaded" });
195
+ } finally {
196
+ page.off("request", serveBlankPage);
197
+ await page.setRequestInterception(false);
198
+ }
199
+ await page.exposeFunction("__ompLiveServerEvent", (payload: string) => this.#handleServerEvent(payload));
200
+ await page.exposeFunction("__ompLiveOutputLevel", (level: number) => this.#handleOutputLevel(level));
201
+ await page.exposeFunction("__ompLiveFailure", (message: string) => this.#handleBrowserFailure(message));
202
+ await page.evaluate(source => Function(source)(), browserRuntimeSource);
203
+ const offer = await page.evaluate(source => globalThis.ompCodexLive.start(source), audioWorkletSource);
204
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
205
+ const signaling = await this.#signal(offer);
206
+ await page.evaluate(sdp => globalThis.ompCodexLive.acceptAnswer(sdp), signaling.answer);
207
+ await page.evaluate(muted => globalThis.ompCodexLive.setMuted(muted), this.#muted);
208
+ await page.evaluate(() => globalThis.ompCodexLive.waitForOpen());
209
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
210
+ await this.#connectSideband(signaling.callId, signaling.access);
211
+ if (this.#state !== "connecting") throw abortReason(this.#options.signal);
212
+ this.#state = "connected";
213
+ }
214
+
215
+ async #signal(offer: string): Promise<LiveSignalingResult> {
216
+ return await withOAuthAccess(
217
+ this.#options.authStorage,
218
+ LIVE_PROVIDER,
219
+ access => this.#signalWithAccess(offer, access),
220
+ {
221
+ sessionId: this.#options.sessionId,
222
+ signal: this.#options.signal,
223
+ isAuthError,
224
+ missingAccessMessage: "No Codex OAuth credential is available for a live call.",
225
+ },
226
+ );
227
+ }
228
+
229
+ async #signalWithAccess(offer: string, access: OAuthAccess): Promise<LiveSignalingResult> {
230
+ const headers = new Headers({
231
+ ...liveSessionHeaders(access, this.#options.sessionId),
232
+ Accept: "application/sdp",
233
+ "Content-Type": "application/json",
234
+ });
235
+ const fetchImpl = wrapFetchForProxy(fetch, LIVE_PROVIDER);
236
+ const response = await fetchImpl(SIGNALING_URL, {
237
+ method: "POST",
238
+ headers,
239
+ body: JSON.stringify({
240
+ sdp: offer,
241
+ session: buildLiveSessionPayload(this.#options.instructions, this.#options.voice),
242
+ }),
243
+ signal: this.#options.signal,
244
+ });
245
+ const responseBody = await response.text();
246
+ if (!response.ok) {
247
+ const detail = boundedErrorBody(responseBody, response.statusText);
248
+ throw new LiveSignalingError(response.status, `Codex live signaling failed (${response.status}): ${detail}`);
249
+ }
250
+ const answer = responseBody.trim();
251
+ if (!answer) throw new LiveSignalingError(response.status, "Codex live signaling returned an empty SDP answer");
252
+ const callId = parseLiveCallId(response.headers.get("location"));
253
+ if (!callId) {
254
+ throw new LiveSignalingError(response.status, "Codex live signaling returned no valid call ID");
255
+ }
256
+ return { answer, callId, access };
257
+ }
258
+
259
+ async #connectSideband(callId: string, access: OAuthAccess): Promise<void> {
260
+ let failure = new Error("Codex live sideband connection failed");
261
+ for (let attempt = 0; attempt < SIDEBAND_CONNECT_ATTEMPTS; attempt++) {
262
+ try {
263
+ await this.#openSideband(callId, access);
264
+ return;
265
+ } catch (cause) {
266
+ failure = cause instanceof Error ? cause : new Error(String(cause));
267
+ if (this.#options.signal?.aborted) throw abortReason(this.#options.signal);
268
+ if (attempt + 1 < SIDEBAND_CONNECT_ATTEMPTS) await Bun.sleep(200 * 2 ** attempt);
269
+ }
270
+ }
271
+ throw failure;
272
+ }
273
+
274
+ async #openSideband(callId: string, access: OAuthAccess): Promise<void> {
275
+ const url = buildLiveSidebandUrl(callId);
276
+ const options = {
277
+ headers: liveSessionHeaders(access, this.#options.sessionId),
278
+ proxy: getProxyForProvider(LIVE_PROVIDER),
279
+ } satisfies Bun.WebSocketOptions;
280
+ const socket: Bun.WebSocket = Reflect.construct(WebSocket, [url, options]);
281
+ socket.binaryType = "nodebuffer";
282
+ const { promise, resolve, reject } = Promise.withResolvers<void>();
283
+ let opened = false;
284
+ let settled = false;
285
+ let timeout: NodeJS.Timeout | undefined;
286
+ const cleanup = (): void => {
287
+ if (timeout) {
288
+ clearTimeout(timeout);
289
+ timeout = undefined;
290
+ }
291
+ this.#options.signal?.removeEventListener("abort", onAbort);
292
+ };
293
+ const rejectConnect = (error: Error): void => {
294
+ if (settled) return;
295
+ settled = true;
296
+ cleanup();
297
+ reject(error);
298
+ };
299
+ const onAbort = (): void => {
300
+ socket.close(1000, "aborted");
301
+ rejectConnect(abortReason(this.#options.signal));
302
+ };
303
+ socket.onopen = () => {
304
+ if (settled) {
305
+ socket.close(1000, "stale");
306
+ return;
307
+ }
308
+ opened = true;
309
+ settled = true;
310
+ cleanup();
311
+ this.#sideband = socket;
312
+ resolve();
313
+ };
314
+ socket.onmessage = event => {
315
+ if (typeof event.data !== "string") {
316
+ this.#reportFailure("Codex live sideband returned an unexpected binary frame.");
317
+ return;
318
+ }
319
+ this.#handleSidebandEvent(event.data);
320
+ };
321
+ socket.onerror = event => {
322
+ const detail = event instanceof ErrorEvent && event.message ? `: ${event.message}` : "";
323
+ if (!opened) {
324
+ rejectConnect(new Error(`Codex live sideband connection failed${detail}`));
325
+ socket.close(1011, "connection failed");
326
+ return;
327
+ }
328
+ this.#reportFailure(`Codex live sideband failed${detail}`);
329
+ };
330
+ socket.onclose = event => {
331
+ if (!opened) {
332
+ rejectConnect(new Error(`Codex live sideband closed before connecting (${event.code})`));
333
+ return;
334
+ }
335
+ if (this.#sideband !== socket) return;
336
+ this.#sideband = undefined;
337
+ if (this.#state === "connecting" || this.#state === "connected") {
338
+ const detail = event.reason ? `: ${event.reason}` : "";
339
+ this.#reportFailure(`Codex live sideband closed (${event.code})${detail}`);
340
+ }
341
+ };
342
+ if (this.#options.signal?.aborted) {
343
+ onAbort();
344
+ } else {
345
+ this.#options.signal?.addEventListener("abort", onAbort, { once: true });
346
+ timeout = setTimeout(() => {
347
+ socket.close(1000, "connect timeout");
348
+ rejectConnect(new Error("Codex live sideband connection timed out"));
349
+ }, SIDEBAND_CONNECT_TIMEOUT_MS);
350
+ timeout.unref?.();
351
+ }
352
+ await promise;
353
+ }
354
+
355
+ #handleSidebandEvent(payload: string): void {
356
+ if (this.#state === "closing" || this.#state === "closed") return;
357
+ const event = parseLiveServerEvent(payload);
358
+ if (!event) return;
359
+ try {
360
+ this.#options.callbacks.onEvent(event);
361
+ } catch {}
362
+ }
363
+
364
+ #handleServerEvent(payload: string): void {
365
+ if (this.#state === "closing" || this.#state === "closed") return;
366
+ const event = parseLiveServerEvent(payload);
367
+ if (!event || (this.#sideband?.readyState === WebSocket.OPEN && event.type !== "error")) return;
368
+ try {
369
+ this.#options.callbacks.onEvent(event);
370
+ } catch {}
371
+ }
372
+
373
+ #handleOutputLevel(level: number): void {
374
+ if (this.#state !== "connected" || !Number.isFinite(level)) return;
375
+ try {
376
+ this.#options.callbacks.onOutputLevel(Math.min(1, Math.max(0, level)));
377
+ } catch {}
378
+ }
379
+
380
+ #handleBrowserFailure(message: string): void {
381
+ this.#reportFailure(message);
382
+ }
383
+
384
+ #reportFailure(message: string): void {
385
+ if ((this.#state !== "connecting" && this.#state !== "connected") || this.#unexpectedFailureReported) {
386
+ return;
387
+ }
388
+ this.#unexpectedFailureReported = true;
389
+ try {
390
+ this.#options.callbacks.onEvent({ type: "error", message });
391
+ } catch {}
392
+ }
393
+
394
+ /** Serialize one Frameless Bidi control message onto the call's sideband WebSocket. */
395
+ send(message: LiveClientMessage): Promise<void> {
396
+ const operation = this.#sendTail.then(() => {
397
+ if (this.#state !== "connected") throw new Error("Live transport is not connected");
398
+ const sideband = this.#sideband;
399
+ if (!sideband || sideband.readyState !== WebSocket.OPEN) {
400
+ throw new Error("Codex live sideband is not connected");
401
+ }
402
+ sideband.send(JSON.stringify(message));
403
+ });
404
+ this.#sendTail = operation.catch(() => {});
405
+ return operation;
406
+ }
407
+
408
+ /** Queue 16 kHz mono Float32 PCM for continuous browser-side resampling and playback. */
409
+ pushAudio(samples: Float32Array): void {
410
+ if (this.#state !== "connected" || this.#muted || samples.length === 0) return;
411
+ const retained =
412
+ samples.length > MAX_HOST_AUDIO_SAMPLES ? samples.subarray(samples.length - MAX_HOST_AUDIO_SAMPLES) : samples;
413
+ const queued = { payload: encodePcm(retained), sampleCount: retained.length };
414
+ this.#audioQueue.push(queued);
415
+ this.#queuedAudioSamples += queued.sampleCount;
416
+ while (this.#queuedAudioSamples > MAX_HOST_AUDIO_SAMPLES && this.#audioQueue.length > 1) {
417
+ const stale = this.#audioQueue.shift();
418
+ if (stale) this.#queuedAudioSamples -= stale.sampleCount;
419
+ }
420
+ if (!this.#audioPumpRunning) void this.#pumpAudio();
421
+ }
422
+
423
+ async #pumpAudio(): Promise<void> {
424
+ this.#audioPumpRunning = true;
425
+ try {
426
+ while (this.#state === "connected" && !this.#muted) {
427
+ const queued = this.#audioQueue.shift();
428
+ if (!queued) break;
429
+ this.#queuedAudioSamples -= queued.sampleCount;
430
+ const page = this.#page;
431
+ if (!page) break;
432
+ await page.evaluate(payload => globalThis.ompCodexLive.pushAudio(payload), queued.payload);
433
+ }
434
+ } catch {
435
+ this.#audioQueue.length = 0;
436
+ this.#queuedAudioSamples = 0;
437
+ } finally {
438
+ this.#audioPumpRunning = false;
439
+ if (this.#audioQueue.length > 0 && this.#state === "connected" && !this.#muted) void this.#pumpAudio();
440
+ }
441
+ }
442
+
443
+ /** Enable or disable the browser audio source and discard queued input when muted. */
444
+ async setMuted(muted: boolean): Promise<void> {
445
+ this.#muted = muted;
446
+ this.#audioQueue.length = 0;
447
+ this.#queuedAudioSamples = 0;
448
+ const page = this.#page;
449
+ if (!page || this.#state !== "connected") return;
450
+ await page.evaluate(value => globalThis.ompCodexLive.setMuted(value), muted);
451
+ }
452
+
453
+ /** Stop audio, WebRTC, the page, and Chromium. Safe to call repeatedly. */
454
+ close(): Promise<void> {
455
+ if (this.#closePromise) return this.#closePromise;
456
+ this.#state = "closing";
457
+ const operation = this.#close();
458
+ this.#closePromise = operation;
459
+ return operation;
460
+ }
461
+
462
+ async #close(): Promise<void> {
463
+ this.#options.signal?.removeEventListener("abort", this.#abortListener);
464
+ this.#audioQueue.length = 0;
465
+ this.#queuedAudioSamples = 0;
466
+ const sideband = this.#sideband;
467
+ const page = this.#page;
468
+ const browser = this.#browser;
469
+ this.#sideband = undefined;
470
+ this.#page = undefined;
471
+ this.#browser = undefined;
472
+ if (sideband && (sideband.readyState === WebSocket.OPEN || sideband.readyState === WebSocket.CONNECTING)) {
473
+ sideband.close(1000, "done");
474
+ }
475
+ if (page) {
476
+ try {
477
+ await page.evaluate(() => globalThis.ompCodexLive?.close());
478
+ } catch {}
479
+ try {
480
+ await page.close();
481
+ } catch {}
482
+ }
483
+ if (browser) {
484
+ try {
485
+ await browser.close();
486
+ } catch {}
487
+ }
488
+ this.#state = "closed";
489
+ }
490
+ }