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.
- package/README.md +2 -0
- package/features/model-slider/codex-feature.json +1 -0
- package/package.json +1 -1
- package/packages/cli/src/chatgpt-launch-coordinator.mjs +77 -0
- package/packages/cli/src/control-server.mjs +2 -0
- package/packages/cli/src/platform.mjs +220 -29
- package/packages/cli/src/runtime-manager.mjs +178 -100
- package/packages/runtime-host/src/codex-runtime.mjs +180 -109
- package/packages/runtime-host/src/main.mjs +1 -3
|
@@ -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(
|
|
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
|
-
|
|
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 }) => !
|
|
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
|
-
|
|
277
|
-
this.manualLaunchBaselinePids
|
|
278
|
-
this.manualLaunchArmed =
|
|
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
|
-
|
|
331
|
-
|
|
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
|
|
358
|
+
const observedProcess = newProcesses[0];
|
|
349
359
|
this.manualLaunchArmed = false;
|
|
350
|
-
this.
|
|
360
|
+
this.enhancementFailure = null;
|
|
361
|
+
this.manualLaunchSuppressionDepth += 1;
|
|
351
362
|
return this.enqueue(async () => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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.
|
|
389
|
-
|
|
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
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
}
|