minecodex 0.1.14 → 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.
@@ -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
  }