minecodex 0.1.15 → 0.1.16

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.
package/README.md CHANGED
@@ -26,6 +26,8 @@ mcx install
26
26
 
27
27
  运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
28
28
 
29
+ MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
30
+
29
31
  打开本地控制台:
30
32
 
31
33
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,77 @@
1
+ function hasExactArgument(command, argument) {
2
+ const value = String(command);
3
+ let offset = value.indexOf(argument);
4
+ while (offset >= 0) {
5
+ const before = offset === 0 || /\s/.test(value[offset - 1]);
6
+ const end = offset + argument.length;
7
+ const after = end === value.length || /\s/.test(value[end]);
8
+ if (before && after) return true;
9
+ offset = value.indexOf(argument, offset + 1);
10
+ }
11
+ return false;
12
+ }
13
+
14
+ export function hasLoopbackCdpArguments(command, cdpPort) {
15
+ return hasExactArgument(command, "--remote-debugging-address=127.0.0.1")
16
+ && hasExactArgument(command, `--remote-debugging-port=${cdpPort}`)
17
+ && hasExactArgument(command, `--remote-allow-origins=http://127.0.0.1:${cdpPort}`);
18
+ }
19
+
20
+ function requireProcess(processInfo, context) {
21
+ if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) {
22
+ throw new Error(`MineCodex could not verify the ${context} ChatGPT process.`);
23
+ }
24
+ if (typeof processInfo.command !== "string" || !processInfo.command.trim()) {
25
+ throw new Error(`MineCodex could not verify the ${context} ChatGPT command line.`);
26
+ }
27
+ if (processInfo.verified === false) {
28
+ throw new Error(`MineCodex rejected an unverified ${context} ChatGPT process.`);
29
+ }
30
+ }
31
+
32
+ export class ChatGPTLaunchCoordinator {
33
+ constructor({ platform, cdpPort }) {
34
+ this.platform = platform;
35
+ this.cdpPort = cdpPort;
36
+ }
37
+
38
+ async attach(processInfo, owned, { startRuntime, waitForHealthy }) {
39
+ requireProcess(processInfo, owned ? "LaunchServices-created" : "observed");
40
+ await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
41
+ const status = await startRuntime();
42
+ const healthy = await waitForHealthy(status);
43
+ await this.platform.assertNativeCodexCdp(processInfo, this.cdpPort);
44
+ return {
45
+ ...healthy,
46
+ codex: { pid: processInfo.pid, owned, command: processInfo.command ?? null },
47
+ };
48
+ }
49
+
50
+ async ensureObservedLaunch(processInfo, { stopRuntime, startRuntime, waitForHealthy }) {
51
+ requireProcess(processInfo, "observed");
52
+ if (typeof this.platform?.terminateNativeCodex !== "function"
53
+ || typeof this.platform?.openNativeCodex !== "function"
54
+ || typeof this.platform?.assertNativeCodexCdp !== "function") {
55
+ throw new Error("MineCodex platform adapter cannot coordinate an official ChatGPT launch.");
56
+ }
57
+
58
+ await stopRuntime();
59
+ if (hasLoopbackCdpArguments(processInfo.command, this.cdpPort)) {
60
+ return this.attach(processInfo, false, { startRuntime, waitForHealthy });
61
+ }
62
+
63
+ const terminated = await this.platform.terminateNativeCodex(processInfo.pid, processInfo.command);
64
+ if (terminated !== 1) {
65
+ throw new Error(`MineCodex refused to replace ChatGPT pid ${processInfo.pid} after identity changed.`);
66
+ }
67
+ const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
68
+ return this.attach(launched, true, { startRuntime, waitForHealthy });
69
+ }
70
+
71
+ async relaunchOwned({ stopRuntime, terminateOwned, startRuntime, waitForHealthy }) {
72
+ await stopRuntime();
73
+ await terminateOwned();
74
+ const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
75
+ return this.attach(launched, true, { startRuntime, waitForHealthy });
76
+ }
77
+ }
@@ -290,11 +290,13 @@ export async function createControlServer({
290
290
  service: { running: true },
291
291
  runtime: {
292
292
  running: runtime.running,
293
+ healthy: runtime.healthy === true,
293
294
  pid: runtime.pid ?? null,
294
295
  runtimeSessionId: runtime.runtimeSessionId ?? null,
295
296
  ready: runtime.ready ?? null,
296
297
  plugins: runtime.plugins ?? runtime.pluginStates ?? [],
297
298
  renderer: runtime.renderer ?? null,
299
+ codex: runtime.codex ?? null,
298
300
  failures: runtime.failures ?? [],
299
301
  },
300
302
  runtimeSessionId: runtime.runtimeSessionId ?? null,
@@ -3,6 +3,7 @@ import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/pr
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { hasLoopbackCdpArguments } from "./chatgpt-launch-coordinator.mjs";
6
7
 
7
8
  const defaultExecFile = promisify(execFileCallback);
8
9
  const defaultFileSystem = Object.freeze({ mkdir, readFile, rename, rm, stat, unlink, writeFile });
@@ -11,6 +12,8 @@ export const SERVICE_LABEL = "com.vontean.minecodex.background";
11
12
  export const LEGACY_SERVICE_LABEL = "com.vontean.minecodex";
12
13
  export const CODEX_APP_PATH = "/Applications/ChatGPT.app";
13
14
  export const CODEX_EXECUTABLE = `${CODEX_APP_PATH}/Contents/MacOS/ChatGPT`;
15
+ export const CODEX_BUNDLE_IDENTIFIER = "com.openai.codex";
16
+ export const CODEX_TEAM_IDENTIFIER = "2DC432GLL2";
14
17
  export const MIN_NODE_VERSION = Object.freeze({ major: 22, minor: 5, patch: 0 });
15
18
  export const MIN_MACOS_VERSION = Object.freeze({ major: 13, darwinMajor: 22 });
16
19
 
@@ -90,6 +93,40 @@ export async function resolveCodexInstallation({
90
93
  );
91
94
  }
92
95
 
96
+ export async function verifyCodexInstallation({ installation, execFile = defaultExecFile } = {}) {
97
+ if (!installation?.appPath || !installation?.executable) {
98
+ throw new Error("Codex Desktop installation is incomplete.");
99
+ }
100
+ const infoPath = path.join(installation.appPath, "Contents", "Info.plist");
101
+ const { stdout: bundleIdentifier } = await execFile("/usr/bin/plutil", [
102
+ "-extract", "CFBundleIdentifier", "raw", "-o", "-", infoPath,
103
+ ]);
104
+ if (String(bundleIdentifier).trim() !== CODEX_BUNDLE_IDENTIFIER) {
105
+ throw new Error(`Refusing non-official ChatGPT bundle: ${String(bundleIdentifier).trim() || "unknown"}`);
106
+ }
107
+ await execFile("/usr/bin/codesign", ["--verify", "--deep", "--strict", installation.appPath]);
108
+ const { stderr: details } = await execFile("/usr/bin/codesign", [
109
+ "-dv", "--verbose=4", installation.appPath,
110
+ ]);
111
+ const authority = String(details).match(/^Authority=(.+)$/m)?.[1] ?? "";
112
+ const identifier = String(details).match(/^Identifier=(.+)$/m)?.[1] ?? "";
113
+ const teamIdentifier = String(details).match(/^TeamIdentifier=(.+)$/m)?.[1] ?? "";
114
+ if (
115
+ identifier !== CODEX_BUNDLE_IDENTIFIER
116
+ || teamIdentifier !== CODEX_TEAM_IDENTIFIER
117
+ || !/\bOpenAI\b/i.test(authority)
118
+ ) {
119
+ throw new Error("Refusing ChatGPT with an unexpected signing identity.");
120
+ }
121
+ return Object.freeze({
122
+ ...installation,
123
+ bundleIdentifier: CODEX_BUNDLE_IDENTIFIER,
124
+ signingIdentity: authority,
125
+ teamIdentifier,
126
+ verified: true,
127
+ });
128
+ }
129
+
93
130
  export function parseProcessList(output) {
94
131
  return String(output).split("\n").flatMap((line) => {
95
132
  const match = line.match(/^\s*(\d+)\s+(.+)$/);
@@ -116,6 +153,22 @@ function hasManagedProfile(command, profileDir) {
116
153
  return false;
117
154
  }
118
155
 
156
+ function extractCdpPort(command) {
157
+ const match = String(command).match(/(?:^|\s)--remote-debugging-port=(\d+)(?:\s|$)/);
158
+ return match ? Number(match[1]) : null;
159
+ }
160
+
161
+ function parseCodexOwnerMarker(content) {
162
+ try {
163
+ const marker = JSON.parse(String(content));
164
+ if (marker?.version !== 1 || !Number.isInteger(marker.pid) || marker.pid <= 1) return null;
165
+ if (typeof marker.command !== "string" || !marker.command.trim()) return null;
166
+ return marker;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
119
172
  export function serviceIsAbsent(error) {
120
173
  return /could not find service|service not found|no such process|domain does not support specified action/i.test(
121
174
  `${error?.stderr ?? ""}\n${error?.message ?? ""}`,
@@ -136,6 +189,48 @@ async function waitForProcessExit(pid, { processApi, timeoutMs = 8_000, sleep =
136
189
  throw new Error(`Process ${pid} did not exit in time`);
137
190
  }
138
191
 
192
+ async function waitForProcessesExit(
193
+ pids,
194
+ { processApi, timeoutMs = 8_000, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) },
195
+ ) {
196
+ const pending = new Set(pids);
197
+ const deadline = Date.now() + timeoutMs;
198
+ while (Date.now() < deadline) {
199
+ for (const pid of pending) {
200
+ try {
201
+ processApi.kill(pid, 0);
202
+ } catch (error) {
203
+ if (error.code === "ESRCH") pending.delete(pid);
204
+ else throw error;
205
+ }
206
+ }
207
+ if (pending.size === 0) return;
208
+ await sleep(100);
209
+ }
210
+ throw new Error(`ChatGPT process tree did not exit in time: ${[...pending].join(", ")}`);
211
+ }
212
+
213
+ function descendantPids(output, rootPid) {
214
+ const childrenByParent = new Map();
215
+ for (const line of String(output).split("\n")) {
216
+ const match = line.match(/^\s*(\d+)\s+(\d+)\s+/);
217
+ if (!match) continue;
218
+ const pid = Number(match[1]);
219
+ const parentPid = Number(match[2]);
220
+ const children = childrenByParent.get(parentPid) ?? [];
221
+ children.push(pid);
222
+ childrenByParent.set(parentPid, children);
223
+ }
224
+ const descendants = [];
225
+ const pending = [...(childrenByParent.get(rootPid) ?? [])];
226
+ while (pending.length) {
227
+ const pid = pending.shift();
228
+ descendants.push(pid);
229
+ pending.push(...(childrenByParent.get(pid) ?? []));
230
+ }
231
+ return descendants;
232
+ }
233
+
139
234
  function serviceTarget(uid) {
140
235
  return `gui/${uid}/${SERVICE_LABEL}`;
141
236
  }
@@ -169,7 +264,7 @@ export class MacPlatformAdapter {
169
264
  execFile = defaultExecFile,
170
265
  fileSystem = defaultFileSystem,
171
266
  processApi = process,
172
- sleep,
267
+ sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
173
268
  codexInstallation,
174
269
  } = {}) {
175
270
  this.uid = uid;
@@ -181,6 +276,8 @@ export class MacPlatformAdapter {
181
276
  this.processApi = processApi;
182
277
  this.sleep = sleep;
183
278
  this.codexInstallation = codexInstallation;
279
+ this.codexInstallationFingerprint = null;
280
+ this.codexInstallationInjected = Boolean(codexInstallation);
184
281
  }
185
282
 
186
283
  assertInstallSupported() {
@@ -194,7 +291,7 @@ export class MacPlatformAdapter {
194
291
  }
195
292
 
196
293
  async assertCodexInstalled() {
197
- this.codexInstallation = await resolveCodexInstallation({
294
+ const installation = await resolveCodexInstallation({
198
295
  homeDir: this.homeDir,
199
296
  fileExists: async (filePath) => {
200
297
  try {
@@ -206,11 +303,36 @@ export class MacPlatformAdapter {
206
303
  }
207
304
  },
208
305
  });
306
+ this.codexInstallation = await verifyCodexInstallation({ installation, execFile: this.execFile });
307
+ this.codexInstallationFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
308
+ this.codexInstallationInjected = false;
209
309
  return this.codexInstallation;
210
310
  }
211
311
 
212
312
  async codex() {
213
- return this.codexInstallation ?? this.assertCodexInstalled();
313
+ if (!this.codexInstallation) return this.assertCodexInstalled();
314
+ if (this.codexInstallationInjected) return this.codexInstallation;
315
+ const currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
316
+ if (this.codexInstallationFingerprint == null) {
317
+ this.codexInstallationFingerprint = currentFingerprint;
318
+ } else if (currentFingerprint !== this.codexInstallationFingerprint) {
319
+ return this.assertCodexInstalled();
320
+ }
321
+ return this.codexInstallation;
322
+ }
323
+
324
+ async readCodexInstallationFingerprint(installation) {
325
+ if (typeof this.fileSystem.stat !== "function") return null;
326
+ const paths = [
327
+ installation.executable,
328
+ path.join(installation.appPath, "Contents", "Info.plist"),
329
+ path.join(installation.appPath, "Contents", "_CodeSignature", "CodeResources"),
330
+ ];
331
+ const records = await Promise.all(paths.map(async (filePath) => {
332
+ const info = await this.fileSystem.stat(filePath);
333
+ return [filePath, info.dev ?? null, info.ino ?? null, info.size ?? null, info.mtimeMs ?? null];
334
+ }));
335
+ return JSON.stringify(records);
214
336
  }
215
337
 
216
338
  async isLaunchdServiceRunning(target) {
@@ -428,12 +550,34 @@ export class MacPlatformAdapter {
428
550
  async listCodexProcesses() {
429
551
  const installation = await this.codex();
430
552
  const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,command="]);
431
- return parseProcessList(stdout).filter(({ command }) => isExactCodexProcess(command, installation));
553
+ return parseProcessList(stdout)
554
+ .filter(({ command }) => isExactCodexProcess(command, installation))
555
+ .map((processInfo) => ({
556
+ ...processInfo,
557
+ appPath: installation.appPath,
558
+ executable: installation.executable,
559
+ bundleIdentifier: installation.bundleIdentifier ?? CODEX_BUNDLE_IDENTIFIER,
560
+ signingIdentity: installation.signingIdentity ?? null,
561
+ verified: installation.verified !== false,
562
+ }));
432
563
  }
433
564
 
434
565
  async ownedCodexProcesses(paths) {
435
566
  const processes = await this.listCodexProcesses();
436
- return processes.filter(({ command }) => hasManagedProfile(command, paths.profileDir));
567
+ let marker = null;
568
+ try {
569
+ marker = parseCodexOwnerMarker(await this.fileSystem.readFile(paths.codexPidPath, "utf8"));
570
+ } catch (error) {
571
+ if (error.code !== "ENOENT") throw error;
572
+ }
573
+ return processes.filter(({ pid, command, verified }) => verified === true && (
574
+ hasManagedProfile(command, paths.profileDir)
575
+ || (
576
+ pid === marker?.pid
577
+ && command === marker.command
578
+ && hasLoopbackCdpArguments(command, extractCdpPort(command))
579
+ )
580
+ ));
437
581
  }
438
582
 
439
583
  async assertOwnedCodexStopped(paths) {
@@ -457,45 +601,92 @@ export class MacPlatformAdapter {
457
601
  }
458
602
 
459
603
  async terminateOwnedCodex(paths) {
460
- let pid;
461
- try {
462
- pid = Number((await this.fileSystem.readFile(paths.codexPidPath, "utf8")).trim());
463
- } catch (error) {
464
- if (error.code === "ENOENT") {
465
- await this.assertOwnedCodexStopped(paths);
466
- return false;
467
- }
468
- throw error;
469
- }
470
- if (!Number.isInteger(pid) || pid <= 1) {
471
- await this.assertOwnedCodexStopped(paths);
472
- return false;
473
- }
474
- const processInfo = (await this.ownedCodexProcesses(paths)).find((candidate) => candidate.pid === pid);
604
+ const processInfo = (await this.ownedCodexProcesses(paths))[0];
475
605
  if (!processInfo) {
476
606
  await this.assertOwnedCodexStopped(paths);
477
607
  return false;
478
608
  }
479
- this.processApi.kill(pid, "SIGTERM");
480
- await waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep });
609
+ this.processApi.kill(processInfo.pid, "SIGTERM");
610
+ await waitForProcessExit(processInfo.pid, { processApi: this.processApi, sleep: this.sleep });
481
611
  await this.fileSystem.unlink(paths.codexPidPath).catch(() => {});
482
612
  await this.assertOwnedCodexStopped(paths);
483
613
  return true;
484
614
  }
485
615
 
486
- async terminateNativeCodex() {
487
- const processes = (await this.listCodexProcesses()).filter(({ command }) => !command.includes("--user-data-dir="));
488
- for (const { pid } of processes) this.processApi.kill(pid, "SIGTERM");
489
- await Promise.all(processes.map(({ pid }) => waitForProcessExit(pid, { processApi: this.processApi, sleep: this.sleep })));
490
- return processes.length;
616
+ async terminateNativeCodex(pid, expectedCommand = null) {
617
+ if (!Number.isInteger(pid) || pid <= 1) {
618
+ throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
619
+ }
620
+ const processInfo = (await this.listCodexProcesses()).find((candidate) => (
621
+ candidate.pid === pid
622
+ && candidate.verified === true
623
+ && !candidate.command.includes("--user-data-dir=")
624
+ && (!expectedCommand || candidate.command === expectedCommand)
625
+ ));
626
+ if (!processInfo) return 0;
627
+ const { stdout: processTree } = await this.execFile("/bin/ps", ["-axo", "pid=,ppid=,command="]);
628
+ const ownedProcessTree = [pid, ...descendantPids(processTree, pid)];
629
+ this.processApi.kill(pid, "SIGTERM");
630
+ await waitForProcessesExit(ownedProcessTree, { processApi: this.processApi, sleep: this.sleep });
631
+ return 1;
632
+ }
633
+
634
+ async assertNativeCodexCdp(processInfo, cdpPort) {
635
+ if (!processInfo?.verified || !Number.isInteger(processInfo.pid) || !hasLoopbackCdpArguments(processInfo.command, cdpPort)) {
636
+ throw new Error("MineCodex could not bind the loopback CDP endpoint to a verified ChatGPT process.");
637
+ }
638
+ const { stdout } = await this.execFile("/usr/sbin/lsof", [
639
+ "-nP", "-a", "-p", String(processInfo.pid), `-iTCP:${cdpPort}`, "-sTCP:LISTEN", "-Fn",
640
+ ]);
641
+ const fields = String(stdout).split("\n");
642
+ if (!fields.includes(`p${processInfo.pid}`) || !fields.includes(`n127.0.0.1:${cdpPort}`)) {
643
+ throw new Error(`ChatGPT pid ${processInfo.pid} does not own loopback CDP port ${cdpPort}.`);
644
+ }
645
+ return true;
491
646
  }
492
647
 
493
648
  async openBrowser(url) {
494
649
  await this.execFile("/usr/bin/open", [url]);
495
650
  }
496
651
 
497
- async openNativeCodex() {
652
+ async openNativeCodex({ cdpPort } = {}) {
498
653
  const installation = await this.codex();
499
- await this.execFile("/usr/bin/open", ["-a", installation.appPath]);
654
+ if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
655
+ await this.execFile("/usr/bin/open", ["-a", installation.appPath]);
656
+ return null;
657
+ }
658
+ const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
659
+ await this.execFile("/usr/bin/open", [
660
+ "-n",
661
+ "-a",
662
+ installation.appPath,
663
+ "--args",
664
+ "--remote-debugging-address=127.0.0.1",
665
+ `--remote-debugging-port=${cdpPort}`,
666
+ `--remote-allow-origins=http://127.0.0.1:${cdpPort}`,
667
+ ]);
668
+ return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
669
+ }
670
+
671
+ async waitForNativeCodex({ cdpPort, excludedPids = new Set(), timeoutMs = 15_000 } = {}) {
672
+ const deadline = Date.now() + timeoutMs;
673
+ while (Date.now() < deadline) {
674
+ const candidates = (await this.listCodexProcesses()).filter(({ pid, command, verified }) => (
675
+ verified === true
676
+ && !excludedPids.has(pid)
677
+ && !command.includes("--user-data-dir=")
678
+ && hasLoopbackCdpArguments(command, cdpPort)
679
+ ));
680
+ if (candidates.length > 1) {
681
+ throw new Error(`LaunchServices exposed multiple new ChatGPT processes on CDP port ${cdpPort}.`);
682
+ }
683
+ const processInfo = candidates[0];
684
+ if (processInfo) {
685
+ await this.assertNativeCodexCdp(processInfo, cdpPort);
686
+ return processInfo;
687
+ }
688
+ await this.sleep?.(50);
689
+ }
690
+ throw new Error(`LaunchServices did not expose a loopback CDP ChatGPT process on port ${cdpPort}.`);
500
691
  }
501
692
  }
@@ -1,5 +1,6 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { ChatGPTLaunchCoordinator } from "./chatgpt-launch-coordinator.mjs";
3
4
  import { readConfig, FEATURE_IDS } from "./config.mjs";
4
5
 
5
6
  export const RUNTIME_PLUGIN_STATES = Object.freeze([
@@ -19,19 +20,6 @@ function hasExited(child) {
19
20
  return Boolean(child && (child.exitCode != null || child.signalCode != null));
20
21
  }
21
22
 
22
- function hasExactCommandArgument(command, argument) {
23
- const value = String(command);
24
- let offset = value.indexOf(argument);
25
- while (offset >= 0) {
26
- const before = offset === 0 || /\s/.test(value[offset - 1]);
27
- const end = offset + argument.length;
28
- const after = end === value.length || /\s/.test(value[end]);
29
- if (before && after) return true;
30
- offset = value.indexOf(argument, offset + 1);
31
- }
32
- return false;
33
- }
34
-
35
23
  function normalizedFailure(failure) {
36
24
  if (!failure || typeof failure !== "object") return failure ?? null;
37
25
  return {
@@ -134,6 +122,7 @@ export class RuntimeManager {
134
122
  stopTimeoutMs = 5_000,
135
123
  manualLaunchPollMs = 1_000,
136
124
  cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231),
125
+ launchCoordinator = null,
137
126
  }) {
138
127
  this.paths = paths;
139
128
  this.platform = platform;
@@ -157,6 +146,13 @@ export class RuntimeManager {
157
146
  this.manualLaunchWake = null;
158
147
  this.manualLaunchBaselinePids = new Set();
159
148
  this.manualLaunchArmed = false;
149
+ this.manualLaunchSuppressionDepth = 0;
150
+ this.enhancedCodex = null;
151
+ this.enhancementFailure = null;
152
+ this.launchCoordinator = launchCoordinator ?? new ChatGPTLaunchCoordinator({
153
+ platform,
154
+ cdpPort,
155
+ });
160
156
  }
161
157
 
162
158
  enqueue(operation) {
@@ -165,36 +161,33 @@ export class RuntimeManager {
165
161
  return next;
166
162
  }
167
163
 
168
- async startInternal({ launchCodex = false } = {}) {
164
+ async startInternal() {
169
165
  if (this.child && !hasExited(this.child)) return this.status();
170
166
  this.clearChildState();
171
167
  this.stopping = false;
172
168
  const config = await readConfig(this.paths.configPath);
173
169
  const features = enabledFeatureIds(config);
174
- const codexInstallation = typeof this.platform?.codex === "function"
175
- ? await this.platform.codex()
176
- : null;
177
- const codexExecutable = codexInstallation?.executable
178
- ?? process.env.MINECODEX_CODEX_EXECUTABLE;
170
+ if (typeof this.platform?.codex === "function") await this.platform.codex();
179
171
  await mkdir(this.paths.supportDir, { recursive: true, mode: 0o700 });
180
172
  await unlink(this.paths.runtimeReadyPath).catch((error) => {
181
173
  if (error.code !== "ENOENT") throw error;
182
174
  });
175
+ const runtimeEnvironment = {
176
+ ...process.env,
177
+ CODEX_FEATURES_ROOT: this.paths.featuresRoot,
178
+ CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
179
+ CODEX_RUNTIME_CDP_PORT: String(this.cdpPort),
180
+ CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
181
+ CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
182
+ MINECODEX_ENABLED_FEATURES: features.join(","),
183
+ MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
184
+ MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
185
+ };
186
+ delete runtimeEnvironment.MINECODEX_LAUNCH_CODEX;
187
+ delete runtimeEnvironment.MINECODEX_CODEX_EXECUTABLE;
183
188
  const child = this.spawnProcess(process.execPath, [this.paths.runtimeEntry], {
184
189
  cwd: this.paths.packageRoot,
185
- env: {
186
- ...process.env,
187
- CODEX_FEATURES_ROOT: this.paths.featuresRoot,
188
- CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
189
- CODEX_RUNTIME_CDP_PORT: String(this.cdpPort),
190
- CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
191
- CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
192
- MINECODEX_ENABLED_FEATURES: features.join(","),
193
- MINECODEX_CODEX_PID_FILE: this.paths.codexPidPath,
194
- MINECODEX_RUNTIME_READY_FILE: this.paths.runtimeReadyPath,
195
- MINECODEX_LAUNCH_CODEX: launchCodex ? "1" : "0",
196
- ...(codexExecutable ? { MINECODEX_CODEX_EXECUTABLE: codexExecutable } : {}),
197
- },
190
+ env: runtimeEnvironment,
198
191
  stdio: "inherit",
199
192
  });
200
193
  this.child = child;
@@ -261,9 +254,8 @@ export class RuntimeManager {
261
254
  async nativeCodexSnapshot() {
262
255
  if (typeof this.platform?.snapshotNativeCodexState !== "function") return null;
263
256
  const snapshot = await this.platform.snapshotNativeCodexState();
264
- const cdpArgument = `--remote-debugging-port=${this.cdpPort}`;
265
257
  const processes = (Array.isArray(snapshot?.processes) ? snapshot.processes : [])
266
- .filter(({ command }) => !hasExactCommandArgument(command, cdpArgument));
258
+ .filter(({ command }) => !String(command).includes("--user-data-dir="));
267
259
  return {
268
260
  count: processes.length,
269
261
  processes,
@@ -271,11 +263,24 @@ export class RuntimeManager {
271
263
  };
272
264
  }
273
265
 
266
+ async syncManualLaunchBaseline() {
267
+ const snapshot = await this.nativeCodexSnapshot();
268
+ if (!snapshot) return;
269
+ this.manualLaunchBaselinePids = new Set(snapshot.pids);
270
+ this.manualLaunchArmed = true;
271
+ }
272
+
273
+ async clearOwnedCodexMarker() {
274
+ await unlink(this.paths.codexPidPath).catch((error) => {
275
+ if (error.code !== "ENOENT") this.logger.warn?.("MineCodex could not clear its ChatGPT owner marker", error.message);
276
+ });
277
+ }
278
+
274
279
  async startManualLaunchMonitor() {
275
280
  if (this.manualLaunchMonitorPromise || typeof this.platform?.snapshotNativeCodexState !== "function") return;
276
- const snapshot = await this.nativeCodexSnapshot();
277
- this.manualLaunchBaselinePids = new Set(snapshot?.pids ?? []);
278
- this.manualLaunchArmed = this.manualLaunchBaselinePids.size === 0;
281
+ // 登录恢复时 ChatGPT 可能先于服务启动,首次扫描也必须纳入增强流程。
282
+ this.manualLaunchBaselinePids.clear();
283
+ this.manualLaunchArmed = true;
279
284
  this.manualLaunchMonitorStopped = false;
280
285
  const monitor = this.monitorManualCodexLaunches().catch((error) => {
281
286
  if (!this.manualLaunchMonitorStopped) {
@@ -323,83 +328,108 @@ export class RuntimeManager {
323
328
  }
324
329
 
325
330
  async reconcileManualCodexLaunch() {
326
- if (this.manualLaunchMonitorStopped) return false;
331
+ if (this.manualLaunchMonitorStopped || this.manualLaunchSuppressionDepth > 0) return false;
327
332
  const snapshot = await this.nativeCodexSnapshot();
328
333
  if (!snapshot) return false;
329
-
330
- if (this.manualLaunchBaselinePids.size > 0) {
331
- const baselineStillRunning = [...this.manualLaunchBaselinePids]
332
- .some((pid) => snapshot.pids.has(pid));
333
- if (baselineStillRunning) return false;
334
- this.manualLaunchBaselinePids.clear();
335
- if (snapshot.count === 0) {
336
- this.manualLaunchArmed = true;
337
- return false;
338
- }
339
- this.manualLaunchArmed = true;
334
+ if (this.enhancedCodex && !snapshot.pids.has(this.enhancedCodex.pid)) {
335
+ if (this.enhancedCodex.owned) await this.clearOwnedCodexMarker();
336
+ this.enhancedCodex = null;
340
337
  }
341
-
342
338
  if (snapshot.count === 0) {
339
+ if (this.enhancedCodex?.owned) await this.clearOwnedCodexMarker();
340
+ this.enhancedCodex = null;
341
+ this.manualLaunchBaselinePids.clear();
343
342
  this.manualLaunchArmed = true;
344
343
  return false;
345
344
  }
346
345
  if (!this.manualLaunchArmed) return false;
346
+ const newProcesses = snapshot.processes.filter(({ pid }) => !this.manualLaunchBaselinePids.has(pid));
347
+ this.manualLaunchBaselinePids = new Set(snapshot.pids);
348
+ if (newProcesses.length === 0) return false;
349
+ if (newProcesses.length > 1) {
350
+ this.enhancementFailure = {
351
+ code: "MULTIPLE_CHATGPT_LAUNCHES",
352
+ message: "MineCodex found multiple new official ChatGPT processes and did not terminate any of them.",
353
+ phase: "launch",
354
+ };
355
+ return false;
356
+ }
347
357
 
348
- const observedPids = new Set(snapshot.pids);
358
+ const observedProcess = newProcesses[0];
349
359
  this.manualLaunchArmed = false;
350
- this.manualLaunchBaselinePids = observedPids;
360
+ this.enhancementFailure = null;
361
+ this.manualLaunchSuppressionDepth += 1;
351
362
  return this.enqueue(async () => {
352
- if (this.manualLaunchMonitorStopped) return false;
353
- const latest = await this.nativeCodexSnapshot();
354
- const observedProcessStillRunning = latest
355
- && [...observedPids].some((pid) => latest.pids.has(pid));
356
- if (!observedProcessStillRunning) {
357
- if (!latest?.count) {
358
- this.manualLaunchBaselinePids.clear();
363
+ try {
364
+ if (this.manualLaunchMonitorStopped) return false;
365
+ const latest = await this.nativeCodexSnapshot();
366
+ const currentProcess = latest?.processes.find((candidate) => (
367
+ candidate.pid === observedProcess.pid
368
+ && candidate.command === observedProcess.command
369
+ ));
370
+ if (!currentProcess) {
359
371
  this.manualLaunchArmed = true;
372
+ return false;
360
373
  }
361
- return false;
362
- }
363
-
364
- await this.stopInternal();
365
- let terminated;
366
- try {
367
- terminated = await this.platform.terminateNativeCodex();
368
- } catch (error) {
369
- await this.restoreIdleRuntimeAfterAdoptionFailure(error);
370
- throw error;
371
- }
372
-
373
- if (!(terminated > 0)) {
374
- await this.startInternal();
375
- this.manualLaunchBaselinePids.clear();
376
- this.manualLaunchArmed = true;
377
- return false;
378
- }
379
-
380
- try {
381
- const status = await this.startInternal({ launchCodex: true });
382
- this.manualLaunchBaselinePids.clear();
383
- this.manualLaunchArmed = true;
384
- return status;
385
- } catch (error) {
386
- this.manualLaunchBaselinePids.clear();
374
+ try {
375
+ const result = await this.launchCoordinator.ensureObservedLaunch(currentProcess, {
376
+ stopRuntime: () => this.stopInternal(),
377
+ startRuntime: () => this.startInternal(),
378
+ waitForHealthy: () => this.waitForHealthyRuntime(),
379
+ });
380
+ this.enhancedCodex = result.codex;
381
+ this.enhancementFailure = null;
382
+ if (this.enhancedCodex.owned) await this.writeOwnedCodexPid(this.enhancedCodex);
383
+ await this.syncManualLaunchBaseline();
384
+ return result;
385
+ } catch (error) {
386
+ this.enhancementFailure = normalizedFailure({
387
+ code: error.code ?? "CHATGPT_ENHANCEMENT_FAILED",
388
+ message: error.message,
389
+ phase: "launch",
390
+ });
391
+ try {
392
+ await this.startInternal();
393
+ } catch (recoveryError) {
394
+ throw new Error(
395
+ `${error.message}; idle RuntimeHost recovery failed: ${recoveryError.message}`,
396
+ { cause: error },
397
+ );
398
+ }
399
+ throw error;
400
+ }
401
+ } finally {
387
402
  this.manualLaunchArmed = true;
388
- await this.restoreIdleRuntimeAfterAdoptionFailure(error);
389
- throw error;
403
+ await this.syncManualLaunchBaseline().catch((error) => {
404
+ this.logger.warn?.("MineCodex could not refresh the ChatGPT launch baseline", error.message);
405
+ });
406
+ this.manualLaunchSuppressionDepth -= 1;
390
407
  }
391
408
  });
392
409
  }
393
410
 
394
- async restoreIdleRuntimeAfterAdoptionFailure(failure) {
395
- try {
396
- await this.startInternal();
397
- } catch (recoveryError) {
398
- throw new Error(
399
- `${failure.message}; idle RuntimeHost recovery failed: ${recoveryError.message}`,
400
- { cause: failure },
401
- );
411
+ async writeOwnedCodexPid(processInfo) {
412
+ if (!this.paths.codexPidPath) return;
413
+ const marker = {
414
+ version: 1,
415
+ pid: processInfo?.pid,
416
+ command: processInfo?.command,
417
+ };
418
+ if (!Number.isInteger(marker.pid) || marker.pid <= 1 || typeof marker.command !== "string" || !marker.command) {
419
+ throw new Error("MineCodex cannot persist incomplete ChatGPT ownership identity.");
402
420
  }
421
+ await writeFile(this.paths.codexPidPath, `${JSON.stringify(marker)}\n`, { mode: 0o600 });
422
+ }
423
+
424
+ async waitForHealthyRuntime(initialStatus = null, timeoutMs = 25_000) {
425
+ const deadline = Date.now() + timeoutMs;
426
+ let status = initialStatus ?? this.status();
427
+ while (Date.now() < deadline) {
428
+ if (!status.healthy) status = await this.refreshStatus();
429
+ if (status.healthy) return status;
430
+ await this.sleep(100);
431
+ }
432
+ throw new Error("MineCodex RuntimeHost did not prove renderer discovery and feature injection.");
403
433
  }
404
434
 
405
435
  async stopInternal() {
@@ -477,11 +507,54 @@ export class RuntimeManager {
477
507
 
478
508
  restart() {
479
509
  if (this.restartInFlight) return this.restartInFlight;
510
+ this.manualLaunchSuppressionDepth += 1;
480
511
  const operation = this.enqueue(async () => {
481
- await this.stopInternal();
482
- await this.platform.terminateOwnedCodex(this.paths);
483
- await this.platform.terminateNativeCodex?.();
484
- return this.startInternal({ launchCodex: true });
512
+ try {
513
+ const previousCodex = this.enhancedCodex;
514
+ if (previousCodex?.owned && typeof this.platform?.terminateNativeCodex === "function") {
515
+ const result = await this.launchCoordinator.relaunchOwned({
516
+ stopRuntime: () => this.stopInternal(),
517
+ terminateOwned: async () => {
518
+ const terminated = await this.platform.terminateNativeCodex(previousCodex.pid, previousCodex.command);
519
+ if (terminated !== 1) throw new Error(`MineCodex could not terminate owned ChatGPT pid ${previousCodex.pid}.`);
520
+ },
521
+ startRuntime: () => this.startInternal(),
522
+ waitForHealthy: (status) => this.waitForHealthyRuntime(status),
523
+ });
524
+ this.enhancedCodex = result.codex;
525
+ this.enhancementFailure = null;
526
+ if (this.enhancedCodex.pid) await this.writeOwnedCodexPid(this.enhancedCodex);
527
+ await this.syncManualLaunchBaseline();
528
+ return result;
529
+ }
530
+ if (previousCodex && !previousCodex.owned) {
531
+ await this.stopInternal();
532
+ const healthy = await this.waitForHealthyRuntime(await this.startInternal());
533
+ await this.syncManualLaunchBaseline();
534
+ return { ...healthy, codex: previousCodex };
535
+ }
536
+ await this.stopInternal();
537
+ if (!previousCodex) await this.platform.terminateOwnedCodex?.(this.paths);
538
+ this.enhancedCodex = null;
539
+ this.enhancementFailure = null;
540
+ if (typeof this.platform?.openNativeCodex !== "function") return this.startInternal();
541
+ const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
542
+ const status = await this.startInternal();
543
+ const healthy = await this.waitForHealthyRuntime(status);
544
+ this.enhancedCodex = {
545
+ pid: launched?.pid ?? null,
546
+ owned: true,
547
+ command: launched?.command ?? null,
548
+ };
549
+ if (this.enhancedCodex.pid) await this.writeOwnedCodexPid(this.enhancedCodex);
550
+ await this.syncManualLaunchBaseline();
551
+ return { ...healthy, codex: this.enhancedCodex };
552
+ } finally {
553
+ await this.syncManualLaunchBaseline().catch((error) => {
554
+ this.logger.warn?.("MineCodex could not refresh the ChatGPT launch baseline", error.message);
555
+ });
556
+ this.manualLaunchSuppressionDepth -= 1;
557
+ }
485
558
  });
486
559
  this.restartInFlight = operation;
487
560
  const shared = this.restartInFlight.finally(() => {
@@ -492,8 +565,10 @@ export class RuntimeManager {
492
565
 
493
566
  status() {
494
567
  const running = Boolean(this.child && !hasExited(this.child));
568
+ const healthy = running && this.ready?.renderer?.active === true;
495
569
  return {
496
570
  running,
571
+ healthy,
497
572
  pid: this.child?.pid ?? null,
498
573
  appliedFeatures: running ? [...this.appliedFeatures] : [],
499
574
  plugins: running ? [...this.plugins] : [],
@@ -501,7 +576,10 @@ export class RuntimeManager {
501
576
  ready: running ? this.ready : null,
502
577
  runtimeSessionId: running ? this.ready?.runtimeSessionId ?? null : null,
503
578
  renderer: running ? this.ready?.renderer ?? null : null,
504
- failures: running ? [...(this.ready?.failures ?? [])] : [],
579
+ codex: this.enhancedCodex ? { ...this.enhancedCodex } : null,
580
+ failures: running
581
+ ? [...(this.ready?.failures ?? []), ...(this.enhancementFailure ? [this.enhancementFailure] : [])]
582
+ : (this.enhancementFailure ? [this.enhancementFailure] : []),
505
583
  };
506
584
  }
507
585
  }
@@ -1,5 +1,4 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
2
  import { mkdir, stat } from "node:fs/promises";
4
3
  import path from "node:path";
5
4
 
@@ -321,6 +320,9 @@ export function createInjectionSource(features, {
321
320
  [data-codex-model-slider-trigger-label] {
322
321
  display: inline; overflow: visible; text-overflow: clip; white-space: nowrap;
323
322
  }
323
+ [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
324
+ display: inline-flex; align-items: center; align-self: center;
325
+ }
324
326
  html[data-codex-model-slider-selecting-effort] [role="menu"][data-state="open"]:not(:has([data-reasoning-slider])) {
325
327
  visibility: hidden !important; opacity: 0 !important; animation: none !important; pointer-events: none !important;
326
328
  }
@@ -374,7 +376,6 @@ export function createInjectionSource(features, {
374
376
  if (modelSelectorFeature) {
375
377
  document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
376
378
  document.documentElement.append(modelSelectorStyle);
377
- void loadNativeFastIcon();
378
379
  }
379
380
 
380
381
  for (const feature of pageScriptFeatures) {
@@ -617,32 +618,6 @@ export function createInjectionSource(features, {
617
618
  return icon;
618
619
  }
619
620
 
620
- async function loadNativeFastIcon() {
621
- const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
622
- if (!sourceLink?.href) return;
623
- try {
624
- const source = await fetch(sourceLink.href).then((response) => response.text());
625
- const active = source.match(/d:`(M11\.9125 21\.4125[^`]+)`,fill:`currentColor`/);
626
- const inactive = source.match(/d:`(M7\.38 16\.2207[^`]+)`,fill:`currentColor`/);
627
- if (!active?.[1] || !inactive?.[1]) return;
628
- modelSelectorNativeFastIcons = {
629
- active: { viewBox: "0 0 24 24", markup: `<path d="${active[1]}" fill="currentColor" />` },
630
- inactive: {
631
- viewBox: "0 0 20 20",
632
- markup: `<g transform="translate(2.43 1.609)"><path d="${inactive[1]}" fill="currentColor" /></g>`,
633
- },
634
- };
635
- document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
636
- const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
637
- const content = button.querySelector("[data-codex-model-slider-fast-content]");
638
- if (icon && content) content.replaceChildren(icon);
639
- });
640
- queueEnsure();
641
- } catch {
642
- // Leave the native control untouched when this private Codex asset moves.
643
- }
644
- }
645
-
646
621
  function nativeCssModuleClass(tokenName, root = document) {
647
622
  const pattern = new RegExp(`^_${tokenName}_[A-Za-z0-9_-]+$`);
648
623
  const live = Array.from(root.querySelectorAll(`[class*="_${tokenName}_"]`))
@@ -674,11 +649,33 @@ export function createInjectionSource(features, {
674
649
  return null;
675
650
  }
676
651
 
652
+ function captureNativeFastIcon() {
653
+ const capture = (state, pathPrefix) => {
654
+ if (modelSelectorNativeFastIcons?.[state]) return;
655
+ const path = Array.from(document.querySelectorAll("svg path")).find(
656
+ (candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
657
+ );
658
+ const svg = path?.closest("svg");
659
+ if (!svg) return;
660
+ const template = svg.cloneNode(true);
661
+ template.removeAttribute("id");
662
+ template.removeAttribute("width");
663
+ template.removeAttribute("height");
664
+ if (!modelSelectorNativeFastIcons) modelSelectorNativeFastIcons = {};
665
+ modelSelectorNativeFastIcons[state] = template;
666
+ };
667
+ capture("active", "M11.9125 21.4125");
668
+ capture("inactive", "M7.38 16.2207");
669
+ return modelSelectorNativeFastIcons;
670
+ }
671
+
677
672
  function createNativeFastIcon(active) {
678
- const definition = modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
679
- const icon = createModelSelectorIcon(definition, { brand: true });
673
+ const template = captureNativeFastIcon()?.[active ? "active" : "inactive"];
674
+ if (!template) return null;
675
+ const icon = template.cloneNode(true);
676
+ icon.setAttribute("aria-hidden", "true");
680
677
  const iconClass = nativeCssModuleClass("FastModeIcon");
681
- if (icon && iconClass) icon.classList.add(iconClass);
678
+ if (iconClass && !icon.classList.contains(iconClass)) icon.classList.add(iconClass);
682
679
  return icon;
683
680
  }
684
681
 
@@ -1847,8 +1844,14 @@ export function createInjectionSource(features, {
1847
1844
  const model = modelDefinitionFor(identity, selector);
1848
1845
  const efforts = model?.supportedReasoningLevels ?? [];
1849
1846
  if (!model || efforts.length < 2) return false;
1850
- const selectedEffort = document.querySelector("[data-codex-intelligence-trigger]")
1851
- ?.getAttribute("data-selected-reasoning-effort") ?? model.defaultReasoningLevel ?? efforts[0].effort;
1847
+ // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
1848
+ // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
1849
+ const nativeModel = nativeCatalogModelFor(identity);
1850
+ const selectedEffort = nativeModel?.defaultReasoningLevel
1851
+ ?? document.querySelector("[data-codex-intelligence-trigger]")
1852
+ ?.getAttribute("data-selected-reasoning-effort")
1853
+ ?? model.defaultReasoningLevel
1854
+ ?? efforts[0].effort;
1852
1855
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
1853
1856
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
1854
1857
  const key = `${identity.raw}:${selectedEffort}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
@@ -1857,7 +1860,6 @@ export function createInjectionSource(features, {
1857
1860
  parentMenu.firstElementChild
1858
1861
  ?.querySelector("[data-model-picker-power-slider] [class*='_TickRail_'] > span"),
1859
1862
  );
1860
- const nativeModel = nativeCatalogModelFor(identity);
1861
1863
  const templateWaitCount = Number(parentMenu.getAttribute("data-codex-model-slider-template-wait") ?? 0);
1862
1864
  if (nativeModel?.slug.toLowerCase().startsWith("gpt-") && !nativeTemplateReady && templateWaitCount < 3) {
1863
1865
  parentMenu.setAttribute("data-codex-model-slider-template-wait", String(templateWaitCount + 1));
@@ -1953,6 +1955,7 @@ export function createInjectionSource(features, {
1953
1955
  function ensureModelSelector() {
1954
1956
  const selector = modelSelectorFeature?.modelSelector;
1955
1957
  if (!selector) return;
1958
+ captureNativeFastIcon();
1956
1959
  pruneModelSubmenuPositionObservers();
1957
1960
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
1958
1961
  enhanceComposerModelTrigger(selector);
@@ -2989,6 +2992,42 @@ export function createInjectionSource(features, {
2989
2992
  return null;
2990
2993
  }
2991
2994
 
2995
+ function nativeJsxRuntime(appModule) {
2996
+ const candidates = Object.values(appModule).filter((value) => {
2997
+ if (typeof value !== "function" || value.length !== 0) return false;
2998
+ let source;
2999
+ try {
3000
+ source = Function.prototype.toString.call(value);
3001
+ } catch {
3002
+ return false;
3003
+ }
3004
+ if (source.length > 1200) return false;
3005
+ if (
3006
+ source.includes("useState")
3007
+ || source.includes("memo_cache_sentinel")
3008
+ || source.includes("(0,")
3009
+ || source.startsWith("class ")
3010
+ ) return false;
3011
+ return source.includes("jsxs") && source.includes("Fragment");
3012
+ });
3013
+ for (const candidate of candidates) {
3014
+ let runtime;
3015
+ try {
3016
+ runtime = candidate();
3017
+ } catch {
3018
+ continue;
3019
+ }
3020
+ if (
3021
+ runtime
3022
+ && typeof runtime === "object"
3023
+ && typeof runtime.jsx === "function"
3024
+ && typeof runtime.jsxs === "function"
3025
+ && "Fragment" in runtime
3026
+ ) return runtime;
3027
+ }
3028
+ return null;
3029
+ }
3030
+
2992
3031
  async function loadNativeTabCapability() {
2993
3032
  if (nativeTabCapability) return nativeTabCapability;
2994
3033
  if (nativeTabCapabilityPromise) return nativeTabCapabilityPromise;
@@ -2997,28 +3036,24 @@ export function createInjectionSource(features, {
2997
3036
  const moduleUrl = await waitForNativeTabModuleUrl();
2998
3037
  if (!moduleUrl) throw new Error("Codex app-initial modulepreload was not found");
2999
3038
  const appModule = await import(moduleUrl);
3000
- if (typeof appModule.Gtt !== "function") {
3001
- throw new Error("Codex right-panel controller factory is unavailable");
3002
- }
3003
- if (typeof appModule.hkt !== "function" || typeof appModule.Skt !== "function") {
3004
- throw new Error("Codex React runtime factories are unavailable");
3005
- }
3006
- const controller = appModule.Gtt("right");
3007
- const jsx = appModule.hkt();
3008
- const React = appModule.Skt();
3039
+ const controller = Object.values(appModule).find((value) => (
3040
+ value
3041
+ && typeof value === "object"
3042
+ && typeof value.openTab === "function"
3043
+ && typeof value.closeTab === "function"
3044
+ && typeof value.activateTab === "function"
3045
+ && value.panelId === "right"
3046
+ ));
3047
+ if (!controller) throw new Error("Codex right-panel controller is unavailable");
3048
+ const jsx = nativeJsxRuntime(appModule);
3009
3049
  if (
3010
- typeof controller?.openTab !== "function"
3011
- || typeof controller?.activateTab !== "function"
3012
- || typeof controller?.closeTab !== "function"
3013
- || !controller?.tabById$
3050
+ !controller?.tabById$
3014
3051
  || typeof jsx?.jsx !== "function"
3015
- || typeof React?.useLayoutEffect !== "function"
3016
- || typeof React?.useRef !== "function"
3052
+ || typeof jsx?.jsxs !== "function"
3017
3053
  ) throw new Error("Codex native tab capability is incomplete");
3018
3054
  nativeTabCapability = {
3019
3055
  controller,
3020
3056
  jsx,
3021
- React,
3022
3057
  moduleAsset: moduleUrl.split("/").pop(),
3023
3058
  };
3024
3059
  nativeTabCapabilityError = null;
@@ -3055,38 +3090,10 @@ export function createInjectionSource(features, {
3055
3090
  const detailKey = `${feature.id}:${detail.id}`;
3056
3091
  let Component = nativeDetailComponents.get(detailKey);
3057
3092
  if (Component) return Component;
3058
- const { jsx, React } = capability;
3093
+ const { jsx } = capability;
3094
+ const frameName = surfaceFrameName(feature.id);
3059
3095
  Component = function CodexPersonalDetailTab() {
3060
- const elementRef = React.useRef(null);
3061
- const frameRef = React.useRef(null);
3062
- const recordKeyRef = React.useRef(null);
3063
- const frameNameRef = React.useRef(null);
3064
- frameNameRef.current ??= surfaceFrameName(feature.id);
3065
- React.useLayoutEffect(() => {
3066
- const element = elementRef.current;
3067
- const frame = frameRef.current;
3068
- if (!element || !frame) return undefined;
3069
- const instanceId = crypto.randomUUID?.()
3070
- ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3071
- const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
3072
- recordKeyRef.current = recordKey;
3073
- registerSurface(recordKey, feature, "detail", detail.surfaceUrl, element, frame, detail.id);
3074
- let keys = nativeDetailSurfaceKeys.get(detailKey);
3075
- if (!keys) {
3076
- keys = new Set();
3077
- nativeDetailSurfaceKeys.set(detailKey, keys);
3078
- }
3079
- keys.add(recordKey);
3080
- queueTheme();
3081
- return () => {
3082
- keys.delete(recordKey);
3083
- if (keys.size === 0) nativeDetailSurfaceKeys.delete(detailKey);
3084
- removeSurfaceRecord(recordKey);
3085
- recordKeyRef.current = null;
3086
- };
3087
- }, []);
3088
3096
  return jsx.jsx("div", {
3089
- ref: elementRef,
3090
3097
  "data-codex-personal-native-detail": detailKey,
3091
3098
  style: {
3092
3099
  height: "100%",
@@ -3095,8 +3102,7 @@ export function createInjectionSource(features, {
3095
3102
  background: "var(--color-token-main-surface-primary)",
3096
3103
  },
3097
3104
  children: jsx.jsx("iframe", {
3098
- ref: frameRef,
3099
- name: frameNameRef.current,
3105
+ name: frameName,
3100
3106
  src: "about:blank",
3101
3107
  title: detail.label,
3102
3108
  allow: "clipboard-write",
@@ -3124,17 +3130,17 @@ export function createInjectionSource(features, {
3124
3130
  return reloaded;
3125
3131
  }
3126
3132
 
3127
- async function waitForNativeDetailTabMount(scope, controller, tabId, detailKey) {
3133
+ async function waitForNativeDetailTabElement(detailKey) {
3128
3134
  const deadline = performance.now() + 2_000;
3129
3135
  do {
3130
- let registered = false;
3131
- try {
3132
- registered = scope.get(controller.tabById$, tabId) != null;
3133
- } catch {}
3134
- if (registered && nativeDetailSurfaceKeys.has(detailKey)) return true;
3136
+ const element = document.querySelector(
3137
+ `[data-codex-personal-native-detail="${detailKey}"]`,
3138
+ );
3139
+ const frame = element?.querySelector("iframe");
3140
+ if (element && frame) return { element, frame };
3135
3141
  await new Promise((resolve) => setTimeout(resolve, 25));
3136
3142
  } while (performance.now() < deadline);
3137
- return false;
3143
+ return null;
3138
3144
  }
3139
3145
 
3140
3146
  async function openNativeDetailTab(feature, detail) {
@@ -3173,7 +3179,14 @@ export function createInjectionSource(features, {
3173
3179
  icon: nativeDetailIcon(feature, detail, jsx),
3174
3180
  isClosable: true,
3175
3181
  props: {},
3176
- onClose: () => nativeOpenTabSessions.delete(session),
3182
+ onClose: () => {
3183
+ nativeOpenTabSessions.delete(session);
3184
+ const closedKeys = nativeDetailSurfaceKeys.get(detailKey);
3185
+ if (closedKeys) {
3186
+ for (const key of closedKeys) removeSurfaceRecord(key);
3187
+ nativeDetailSurfaceKeys.delete(detailKey);
3188
+ }
3189
+ },
3177
3190
  });
3178
3191
  } catch (error) {
3179
3192
  if (isNewSession) nativeOpenTabSessions.delete(session);
@@ -3181,15 +3194,29 @@ export function createInjectionSource(features, {
3181
3194
  return null;
3182
3195
  }
3183
3196
 
3184
- if (!await waitForNativeDetailTabMount(scope, controller, tabId, detailKey)) {
3185
- if (isNewSession) nativeOpenTabSessions.delete(session);
3186
- if (!focusedExisting) {
3187
- try {
3188
- controller.closeTab(scope, tabId);
3189
- } catch {}
3197
+ if (!nativeDetailSurfaceKeys.has(detailKey)) {
3198
+ const mounted = await waitForNativeDetailTabElement(detailKey);
3199
+ if (!mounted) {
3200
+ if (isNewSession) nativeOpenTabSessions.delete(session);
3201
+ if (!focusedExisting) {
3202
+ try {
3203
+ controller.closeTab(scope, tabId);
3204
+ } catch {}
3205
+ }
3206
+ nativeTabCapabilityError = new Error("Codex native right-panel Tab did not mount");
3207
+ return null;
3190
3208
  }
3191
- nativeTabCapabilityError = new Error("Codex native right-panel Tab did not mount");
3192
- return null;
3209
+ const instanceId = crypto.randomUUID?.()
3210
+ ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
3211
+ const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
3212
+ registerSurface(recordKey, feature, "detail", detail.surfaceUrl, mounted.element, mounted.frame, detail.id);
3213
+ let keys = nativeDetailSurfaceKeys.get(detailKey);
3214
+ if (!keys) {
3215
+ keys = new Set();
3216
+ nativeDetailSurfaceKeys.set(detailKey, keys);
3217
+ }
3218
+ keys.add(recordKey);
3219
+ queueTheme();
3193
3220
  }
3194
3221
 
3195
3222
  nativeTabCapabilityError = null;
@@ -3207,6 +3234,9 @@ export function createInjectionSource(features, {
3207
3234
  }
3208
3235
 
3209
3236
  function closeNativeDetailTabs() {
3237
+ for (const keys of Array.from(nativeDetailSurfaceKeys.values())) {
3238
+ for (const key of keys) removeSurfaceRecord(key);
3239
+ }
3210
3240
  for (const session of Array.from(nativeOpenTabSessions)) {
3211
3241
  try {
3212
3242
  session.controller.closeTab(session.scope, session.tabId);
@@ -3923,7 +3953,7 @@ export class CodexRuntime {
3923
3953
  cdpPort = 9231,
3924
3954
  logger = console,
3925
3955
  fetchImpl = globalThis.fetch,
3926
- spawnProcess = spawn,
3956
+ launchApplication = null,
3927
3957
  connectClient = connect,
3928
3958
  sleep = (timeoutMs) => new Promise((resolve) => setTimeout(resolve, timeoutMs)),
3929
3959
  availabilityTimeoutMs = 20_000,
@@ -3942,7 +3972,7 @@ export class CodexRuntime {
3942
3972
  this.cdpPort = cdpPort;
3943
3973
  this.logger = logger;
3944
3974
  this.fetchImpl = fetchImpl;
3945
- this.spawnProcess = spawnProcess;
3975
+ this.launchApplication = launchApplication;
3946
3976
  this.connectClient = connectClient;
3947
3977
  this.sleep = sleep;
3948
3978
  this.availabilityTimeoutMs = availabilityTimeoutMs;
@@ -4079,17 +4109,19 @@ export class CodexRuntime {
4079
4109
  }
4080
4110
 
4081
4111
  launchManagedCodex() {
4082
- const child = this.spawnProcess(
4083
- this.appPath,
4084
- [
4112
+ if (typeof this.launchApplication !== "function") {
4113
+ throw new Error("CodexRuntime requires an explicit application launcher for isolated tests.");
4114
+ }
4115
+ const child = this.launchApplication({
4116
+ appPath: this.appPath,
4117
+ args: [
4085
4118
  `--user-data-dir=${this.profileDir}`,
4086
4119
  "--remote-debugging-address=127.0.0.1",
4087
4120
  `--remote-debugging-port=${this.cdpPort}`,
4088
4121
  `--remote-allow-origins=http://127.0.0.1:${this.cdpPort}`,
4089
4122
  "--no-first-run",
4090
4123
  ],
4091
- { detached: true, stdio: "ignore" },
4092
- );
4124
+ });
4093
4125
  this.managedCodexChild = child;
4094
4126
  this.appPid = child.pid ?? null;
4095
4127
  this.notifyManagedCodexPidChange(this.appPid);
@@ -17,7 +17,6 @@ const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot
17
17
  const legacyDataDir = process.env.CODEX_IMAGE_HOST_DATA_DIR ?? path.join(os.homedir(), ".codex-image-host");
18
18
  const profileDir = process.env.CODEX_RUNTIME_PROFILE_DIR ?? path.join(legacyDataDir, "codex-profile");
19
19
  const cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231);
20
- const launchCodex = process.env.MINECODEX_LAUNCH_CODEX === "1";
21
20
 
22
21
  const discoveredFeatures = await discoverFeatures(featuresRoot);
23
22
  if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
@@ -66,13 +65,12 @@ runtime = new CodexRuntime({
66
65
  featureInstanceId,
67
66
  profileDir,
68
67
  cdpPort,
69
- appPath: process.env.MINECODEX_CODEX_EXECUTABLE || undefined,
70
68
  onManagedCodexPidChange: updateCodexPidFile,
71
69
  onStatusChange: requestReadyFileRefresh,
72
70
  });
73
71
 
74
72
  try {
75
- await runtime.start({ launchCodex });
73
+ await runtime.start();
76
74
  runtimeReady = true;
77
75
  await writeReadyFile();
78
76
  } catch (error) {