minecodex 0.1.19 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,9 +46,10 @@ const SOURCE_ICON_MARKUP = {
46
46
  ].join(""),
47
47
  project: '<path fill-rule="evenodd" clip-rule="evenodd" d="M4.75488 2.1416C5.30942 2.14164 5.74594 2.23705 6.11816 2.38965C6.48323 2.53934 6.76728 2.73817 7.00391 2.9043L7.02148 2.91699C7.47057 3.23238 7.8162 3.47463 8.55176 3.47461H11.333C12.7194 3.47484 13.8311 4.61217 13.8311 6L13.875 6.38281H13.8594C14.8729 6.38292 15.5982 7.3629 15.3018 8.33203L14.0068 12.5586C13.7703 13.3297 13.0576 13.8563 12.251 13.8564H3.83984C3.4199 13.8564 3.04144 13.7174 2.73828 13.4883L2.67383 13.4346C1.99907 12.9811 1.55577 12.2065 1.55566 11.3311L0.941406 4.66699C0.941406 3.2792 2.05315 2.1419 3.43945 2.1416H4.75488ZM4.7627 7.42969C4.56039 7.42972 4.3807 7.5625 4.32129 7.75586L3.08594 11.7891C2.96123 12.1965 3.18214 12.6072 3.54883 12.7529C3.63476 12.7768 3.74102 12.7958 3.88184 12.8086H12.251C12.5974 12.8085 12.9033 12.5821 13.0049 12.251L14.2998 8.02539C14.3901 7.72947 14.1688 7.42979 13.8594 7.42969H4.7627ZM3.43945 3.19141C2.64724 3.1917 1.99121 3.84481 1.99121 4.66699L2.49316 10.1201L3.32031 7.44922C3.51452 6.81571 4.10008 6.38284 4.7627 6.38281H12.8252L12.7812 6C12.7812 5.22902 12.2045 4.607 11.4795 4.53223L11.333 4.52441H8.55176C8.05756 4.52442 7.64464 4.44062 7.2666 4.2793C6.91453 4.12896 6.6274 3.92345 6.41797 3.77637L6.40039 3.76367C6.16212 3.59639 5.96404 3.46151 5.71973 3.36133C5.54113 3.28812 5.32754 3.2289 5.05176 3.2041L4.75488 3.19141H3.43945Z" fill="currentColor"></path>',
48
48
  chat: '<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"></path>',
49
+ work: '<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"></path>',
49
50
  };
50
51
 
51
- let facets = { total: 0, chats: 0, projects: [] };
52
+ let facets = { total: 0, chats: 0, works: 0, projects: [] };
52
53
  let selectedScope = "all";
53
54
  let groupByThread = false;
54
55
  let viewContext = "browse";
@@ -155,6 +156,9 @@ function sourcePresentation(image) {
155
156
  if (image.sourceKind === "project") {
156
157
  return { kind: "project", label: image.projectName || "Project" };
157
158
  }
159
+ if (image.sourceKind === "work") {
160
+ return { kind: "work", label: image.threadTitle || "Work" };
161
+ }
158
162
  return { kind: "chat", label: image.threadTitle || "Untitled chat" };
159
163
  }
160
164
 
@@ -170,6 +174,7 @@ function createSourceValue(kind, label, className = "") {
170
174
  function selectedFacet() {
171
175
  if (selectedScope === "all") return { label: "All images", count: facets.total, kind: "all" };
172
176
  if (selectedScope === "chat") return { label: "Chats", count: facets.chats, kind: "chat" };
177
+ if (selectedScope === "work") return { label: "Work", count: facets.works, kind: "work" };
173
178
  const project = facets.projects.find((item) => item.scope === selectedScope);
174
179
  return project
175
180
  ? { label: project.name, count: project.count, kind: "project" }
@@ -210,6 +215,7 @@ function createSourceOption({ scope, label, count, kind }) {
210
215
  function renderSourceMenu() {
211
216
  const all = createSourceOption({ scope: "all", label: "All images", count: facets.total, kind: "all" });
212
217
  const chats = createSourceOption({ scope: "chat", label: "Chats", count: facets.chats, kind: "chat" });
218
+ const work = createSourceOption({ scope: "work", label: "Work", count: facets.works, kind: "work" });
213
219
  const divider = document.createElement("div");
214
220
  divider.className = "source-menu-divider";
215
221
  divider.setAttribute("role", "separator");
@@ -219,7 +225,7 @@ function renderSourceMenu() {
219
225
  count: project.count,
220
226
  kind: "project",
221
227
  }));
222
- sourceMenu.replaceChildren(all, chats, ...(projects.length ? [divider, ...projects] : []));
228
+ sourceMenu.replaceChildren(all, chats, work, ...(projects.length ? [divider, ...projects] : []));
223
229
  updateFilterTrigger();
224
230
  }
225
231
 
@@ -259,6 +265,7 @@ async function loadFacets() {
259
265
  if (
260
266
  selectedScope !== "all"
261
267
  && selectedScope !== "chat"
268
+ && selectedScope !== "work"
262
269
  && !facets.projects.some((project) => project.scope === selectedScope)
263
270
  ) {
264
271
  selectedScope = "all";
@@ -362,8 +369,12 @@ function createThreadBoard(group) {
362
369
  button.className = "thread-board";
363
370
  button.type = "button";
364
371
  button.dataset.threadId = group.threadId;
365
- const sourceLabel = group.sourceKind === "project" ? (group.projectName || "Project") : "Chat";
366
- const threadTitle = group.threadTitle || (group.sourceKind === "project" ? "Untitled task" : "Untitled chat");
372
+ const sourceLabel = group.sourceKind === "project"
373
+ ? (group.projectName || "Project")
374
+ : group.sourceKind === "work" ? "Work" : "Chat";
375
+ const threadTitle = group.threadTitle || (
376
+ group.sourceKind === "project" ? "Untitled task" : group.sourceKind === "work" ? "Untitled work" : "Untitled chat"
377
+ );
367
378
  button.setAttribute("aria-label", `Open ${threadTitle}, ${group.imageCount} images`);
368
379
 
369
380
  const preview = document.createElement("span");
@@ -392,7 +403,11 @@ function createThreadBoard(group) {
392
403
 
393
404
  const caption = document.createElement("span");
394
405
  caption.className = "thread-board-caption";
395
- caption.append(createSourceValue(group.sourceKind === "project" ? "project" : "chat", sourceLabel, "card-source"));
406
+ caption.append(createSourceValue(
407
+ group.sourceKind === "project" ? "project" : group.sourceKind === "work" ? "work" : "chat",
408
+ sourceLabel,
409
+ "card-source",
410
+ ));
396
411
  const title = document.createElement("span");
397
412
  title.className = "thread-board-title";
398
413
  title.textContent = threadTitle;
@@ -609,12 +624,18 @@ function resetCopyFeedback() {
609
624
 
610
625
  function updateSourceAction(image) {
611
626
  const archived = Boolean(image.archived);
612
- sourceButton.disabled = archived;
613
- sourceAction.classList.toggle("is-disabled", archived);
614
- sourceTooltip.hidden = !archived;
615
- if (archived) {
627
+ const unavailable = archived || !image.sourceUrl;
628
+ sourceButton.disabled = unavailable;
629
+ sourceAction.classList.toggle("is-disabled", unavailable);
630
+ sourceTooltip.hidden = !unavailable;
631
+ if (unavailable) {
616
632
  sourceButton.setAttribute("aria-describedby", sourceTooltip.id);
617
- sourceButton.setAttribute("aria-label", "Open Thread unavailable: this Thread has been archived");
633
+ sourceTooltip.textContent = archived
634
+ ? "This Thread has been archived and can’t be opened."
635
+ : "This ChatGPT conversation cannot be opened from Images.";
636
+ sourceButton.setAttribute("aria-label", archived
637
+ ? "Open Thread unavailable: this Thread has been archived"
638
+ : "Open conversation unavailable");
618
639
  } else {
619
640
  sourceButton.removeAttribute("aria-describedby");
620
641
  sourceButton.removeAttribute("aria-label");
@@ -719,10 +740,11 @@ function trapDetailContextFocus(event) {
719
740
  function showImageDetails(image) {
720
741
  activeImage = image;
721
742
  const isProject = image.sourceKind === "project";
743
+ const isWork = image.sourceKind === "work";
722
744
  drawerProjectRow.hidden = !isProject;
723
745
  if (isProject) drawerProject.textContent = image.projectName || "Project";
724
- drawerTaskLabel.textContent = isProject ? "Thread" : "Chat";
725
- drawerThread.textContent = image.threadTitle || (isProject ? "Untitled task" : "Untitled chat");
746
+ drawerTaskLabel.textContent = isProject ? "Thread" : isWork ? "Work" : "Chat";
747
+ drawerThread.textContent = image.threadTitle || (isProject ? "Untitled task" : isWork ? "Untitled work" : "Untitled chat");
726
748
  drawerImage.src = image.fileUrl;
727
749
  drawerImage.alt = image.prompt || "Generated image";
728
750
  if (image.width && image.height) {
@@ -959,7 +981,7 @@ rescan.addEventListener("click", async () => {
959
981
 
960
982
  drawer.querySelector(".drawer-close").addEventListener("click", closeDrawer);
961
983
  sourceButton.addEventListener("click", () => {
962
- if (activeImage && !activeImage.archived) openSourceThread(activeImage.threadId);
984
+ if (activeImage && !activeImage.archived && activeImage.sourceUrl) openSourceThread(activeImage.threadId);
963
985
  });
964
986
 
965
987
  promptCopyButton.addEventListener("click", async () => {
@@ -22,6 +22,13 @@ distributed under MineCodex's MIT License.
22
22
  (`icons/xiaomi.svg`), licensed under CC0 1.0. The Xiaomi brand mark remains
23
23
  the property of Xiaomi Corporation.
24
24
 
25
+ ## Meta logo (provider mark)
26
+
27
+ `assets/providers/muse.svg` is the official Meta Platforms logo rendered in the
28
+ same 24×24 provider-mark style (Lobe Icons) as the other provider icons in this
29
+ folder. The Meta mark remains the property of Meta Platforms, Inc.; the SVG
30
+ wrapper is distributed under MineCodex's MIT License.
31
+
25
32
  ## Lucide
26
33
 
27
34
  Lucide icons distributed by MineCodex include the Model Slider UI icons, Notes action and
@@ -0,0 +1 @@
1
+ <svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Meta</title><path d="M6.897 4h-.024l-.031 2.615h.022c1.715 0 3.046 1.357 5.94 6.246l.175.297.012.02 1.62-2.438-.012-.019a48.763 48.763 0 00-1.098-1.716 28.01 28.01 0 00-1.175-1.629C10.413 4.932 8.812 4 6.896 4z" fill="url(#lobe-icons-meta-0-_R_0_)"></path><path d="M6.873 4C4.95 4.01 3.247 5.258 2.02 7.17a4.352 4.352 0 00-.01.017l2.254 1.231.011-.017c.718-1.083 1.61-1.774 2.568-1.785h.021L6.896 4h-.023z" fill="url(#lobe-icons-meta-1-_R_0_)"></path><path d="M2.019 7.17l-.011.017C1.2 8.447.598 9.995.274 11.664l-.005.022 2.534.6.004-.022c.27-1.467.786-2.828 1.456-3.845l.011-.017L2.02 7.17z" fill="url(#lobe-icons-meta-2-_R_0_)"></path><path d="M2.807 12.264l-2.533-.6-.005.022c-.177.918-.267 1.851-.269 2.786v.023l2.598.233v-.023a12.591 12.591 0 01.21-2.44z" fill="url(#lobe-icons-meta-3-_R_0_)"></path><path d="M2.677 15.537a5.462 5.462 0 01-.079-.813v-.022L0 14.468v.024a8.89 8.89 0 00.146 1.652l2.535-.585a4.106 4.106 0 01-.004-.022z" fill="url(#lobe-icons-meta-4-_R_0_)"></path><path d="M3.27 16.89c-.284-.31-.484-.756-.589-1.328l-.004-.021-2.535.585.004.021c.192 1.01.568 1.85 1.106 2.487l.014.017 2.018-1.745a2.106 2.106 0 01-.015-.016z" fill="url(#lobe-icons-meta-5-_R_0_)"></path><path d="M10.78 9.654c-1.528 2.35-2.454 3.825-2.454 3.825-2.035 3.2-2.739 3.917-3.871 3.917a1.545 1.545 0 01-1.186-.508l-2.017 1.744.014.017C2.01 19.518 3.058 20 4.356 20c1.963 0 3.374-.928 5.884-5.33l1.766-3.13a41.283 41.283 0 00-1.227-1.886z" fill="#0082FB"></path><path d="M13.502 5.946l-.016.016c-.4.43-.786.908-1.16 1.416.378.483.768 1.024 1.175 1.63.48-.743.928-1.345 1.367-1.807l.016-.016-1.382-1.24z" fill="url(#lobe-icons-meta-6-_R_0_)"></path><path d="M20.918 5.713C19.853 4.633 18.583 4 17.225 4c-1.432 0-2.637.787-3.723 1.944l-.016.016 1.382 1.24.016-.017c.715-.747 1.408-1.12 2.176-1.12.826 0 1.6.39 2.27 1.075l.015.016 1.589-1.425-.016-.016z" fill="#0082FB"></path><path d="M23.998 14.125c-.06-3.467-1.27-6.566-3.064-8.396l-.016-.016-1.588 1.424.015.016c1.35 1.392 2.277 3.98 2.361 6.971v.023h2.292v-.022z" fill="url(#lobe-icons-meta-7-_R_0_)"></path><path d="M23.998 14.15v-.023h-2.292v.022c.004.14.006.282.006.424 0 .815-.121 1.474-.368 1.95l-.011.022 1.708 1.782.013-.02c.62-.96.946-2.293.946-3.91 0-.083 0-.165-.002-.247z" fill="url(#lobe-icons-meta-8-_R_0_)"></path><path d="M21.344 16.52l-.011.02c-.214.402-.519.67-.917.787l.778 2.462a3.493 3.493 0 00.438-.182 3.558 3.558 0 001.366-1.218l.044-.065.012-.02-1.71-1.784z" fill="url(#lobe-icons-meta-9-_R_0_)"></path><path d="M19.92 17.393c-.262 0-.492-.039-.718-.14l-.798 2.522c.449.153.927.222 1.46.222.492 0 .943-.073 1.352-.215l-.78-2.462c-.167.05-.341.075-.517.073z" fill="url(#lobe-icons-meta-10-_R_0_)"></path><path d="M18.323 16.534l-.014-.017-1.836 1.914.016.017c.637.682 1.246 1.105 1.937 1.337l.797-2.52c-.291-.125-.573-.353-.9-.731z" fill="url(#lobe-icons-meta-11-_R_0_)"></path><path d="M18.309 16.515c-.55-.642-1.232-1.712-2.303-3.44l-1.396-2.336-.011-.02-1.62 2.438.012.02.989 1.668c.959 1.61 1.74 2.774 2.493 3.585l.016.016 1.834-1.914a2.353 2.353 0 01-.014-.017z" fill="url(#lobe-icons-meta-12-_R_0_)"></path><defs><linearGradient id="lobe-icons-meta-0-_R_0_" x1="75.897%" x2="26.312%" y1="89.199%" y2="12.194%"><stop offset=".06%" stop-color="#0867DF"></stop><stop offset="45.39%" stop-color="#0668E1"></stop><stop offset="85.91%" stop-color="#0064E0"></stop></linearGradient><linearGradient id="lobe-icons-meta-1-_R_0_" x1="21.67%" x2="97.068%" y1="75.874%" y2="23.985%"><stop offset="13.23%" stop-color="#0064DF"></stop><stop offset="99.88%" stop-color="#0064E0"></stop></linearGradient><linearGradient id="lobe-icons-meta-2-_R_0_" x1="38.263%" x2="60.895%" y1="89.127%" y2="16.131%"><stop offset="1.47%" stop-color="#0072EC"></stop><stop offset="68.81%" stop-color="#0064DF"></stop></linearGradient><linearGradient id="lobe-icons-meta-3-_R_0_" x1="47.032%" x2="52.15%" y1="90.19%" y2="15.745%"><stop offset="7.31%" stop-color="#007CF6"></stop><stop offset="99.43%" stop-color="#0072EC"></stop></linearGradient><linearGradient id="lobe-icons-meta-4-_R_0_" x1="52.155%" x2="47.591%" y1="58.301%" y2="37.004%"><stop offset="7.31%" stop-color="#007FF9"></stop><stop offset="100%" stop-color="#007CF6"></stop></linearGradient><linearGradient id="lobe-icons-meta-5-_R_0_" x1="37.689%" x2="61.961%" y1="12.502%" y2="63.624%"><stop offset="7.31%" stop-color="#007FF9"></stop><stop offset="100%" stop-color="#0082FB"></stop></linearGradient><linearGradient id="lobe-icons-meta-6-_R_0_" x1="34.808%" x2="62.313%" y1="68.859%" y2="23.174%"><stop offset="27.99%" stop-color="#007FF8"></stop><stop offset="91.41%" stop-color="#0082FB"></stop></linearGradient><linearGradient id="lobe-icons-meta-7-_R_0_" x1="43.762%" x2="57.602%" y1="6.235%" y2="98.514%"><stop offset="0%" stop-color="#0082FB"></stop><stop offset="99.95%" stop-color="#0081FA"></stop></linearGradient><linearGradient id="lobe-icons-meta-8-_R_0_" x1="60.055%" x2="39.88%" y1="4.661%" y2="69.077%"><stop offset="6.19%" stop-color="#0081FA"></stop><stop offset="100%" stop-color="#0080F9"></stop></linearGradient><linearGradient id="lobe-icons-meta-9-_R_0_" x1="30.282%" x2="61.081%" y1="59.32%" y2="33.244%"><stop offset="0%" stop-color="#027AF3"></stop><stop offset="100%" stop-color="#0080F9"></stop></linearGradient><linearGradient id="lobe-icons-meta-10-_R_0_" x1="20.433%" x2="82.112%" y1="50.001%" y2="50.001%"><stop offset="0%" stop-color="#0377EF"></stop><stop offset="99.94%" stop-color="#0279F1"></stop></linearGradient><linearGradient id="lobe-icons-meta-11-_R_0_" x1="40.303%" x2="72.394%" y1="35.298%" y2="57.811%"><stop offset=".19%" stop-color="#0471E9"></stop><stop offset="100%" stop-color="#0377EF"></stop></linearGradient><linearGradient id="lobe-icons-meta-12-_R_0_" x1="32.254%" x2="68.003%" y1="19.719%" y2="84.908%"><stop offset="27.65%" stop-color="#0867DF"></stop><stop offset="100%" stop-color="#0471E9"></stop></linearGradient></defs></svg>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "model-slider",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "label": {
6
6
  "en": "Model Slider",
7
7
  "zh-CN": "模型滑条"
@@ -16,12 +16,17 @@
16
16
  "modelSelector": {
17
17
  "maxVisibleItems": 6,
18
18
  "favoriteStorageKey": "codex-model-slider:favorites:v1",
19
+ "reasoningEffortOverrides": {
20
+ "opencode-go/hy3": ["low", "medium", "high"],
21
+ "opencode-go/muse-spark-1.2-contributor": ["low", "medium", "high", "xhigh"]
22
+ },
19
23
  "providerAliases": {
20
24
  "opencode-go": "OpenCode Go",
21
25
  "packyapi": "PackyAPI",
22
26
  "xai": "xAI",
23
27
  "zhipu": "Zhipu AI",
24
28
  "google": "Google",
29
+ "meta": "Meta",
25
30
  "anthropic": "Anthropic",
26
31
  "openai": "OpenAI"
27
32
  },
@@ -49,7 +54,8 @@
49
54
  { "id": "kimi", "keywords": ["kimi", "moonshot"], "icon": { "resource": "assets/providers/kimi.svg" } },
50
55
  { "id": "minimax", "keywords": ["minimax"], "icon": { "resource": "assets/providers/minimax.svg" } },
51
56
  { "id": "qwen", "keywords": ["qwen", "qwq"], "icon": { "resource": "assets/providers/qwen.svg" } },
52
- { "id": "hunyuan", "keywords": ["hunyuan"], "icon": { "resource": "assets/providers/hunyuan.svg" } },
57
+ { "id": "hunyuan", "keywords": ["hunyuan", "opencode-go/hy3"], "icon": { "resource": "assets/providers/hunyuan.svg" } },
58
+ { "id": "muse", "keywords": ["muse", "muse-spark"], "icon": { "resource": "assets/providers/muse.svg" } },
53
59
  { "id": "zhipu", "keywords": ["zhipu", "chatglm", "glm"], "icon": { "resource": "assets/providers/zhipu.svg" } },
54
60
  { "id": "openai", "keywords": ["openai", "gpt", "codex", "o1", "o3", "o4"], "icon": { "resource": "assets/providers/openai.svg" } }
55
61
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.1.19",
3
+ "version": "0.2.1",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -65,7 +65,33 @@ export class ChatGPTLaunchCoordinator {
65
65
  throw new Error(`MineCodex refused to replace ChatGPT pid ${processInfo.pid} after identity changed.`);
66
66
  }
67
67
  const launched = await this.platform.openNativeCodex({ cdpPort: this.cdpPort });
68
- return this.attach(launched, true, { startRuntime, waitForHealthy });
68
+ try {
69
+ return await this.attach(launched, true, { startRuntime, waitForHealthy });
70
+ } catch (error) {
71
+ const rollbackErrors = [];
72
+ try {
73
+ await stopRuntime();
74
+ } catch (rollbackError) {
75
+ rollbackErrors.push(rollbackError);
76
+ }
77
+ try {
78
+ const rolledBack = await this.platform.terminateNativeCodex(launched?.pid, launched?.command);
79
+ if (rolledBack !== 1) {
80
+ throw new Error(`MineCodex could not roll back LaunchServices ChatGPT pid ${launched?.pid}.`);
81
+ }
82
+ } catch (rollbackError) {
83
+ rollbackErrors.push(rollbackError);
84
+ }
85
+ if (rollbackErrors.length) {
86
+ const rollbackFailure = new Error(
87
+ `${error.message}; ChatGPT launch rollback failed: ${rollbackErrors.map(({ message }) => message).join("; ")}`,
88
+ { cause: error },
89
+ );
90
+ rollbackFailure.retryProcessPids = Number.isInteger(launched?.pid) ? [launched.pid] : [];
91
+ throw rollbackFailure;
92
+ }
93
+ throw error;
94
+ }
69
95
  }
70
96
 
71
97
  async relaunchOwned({ stopRuntime, terminateOwned, startRuntime, waitForHealthy }) {
@@ -613,18 +613,6 @@ export class MacPlatformAdapter {
613
613
  return true;
614
614
  }
615
615
 
616
- async codexProcessStartedMsAgo(processInfo) {
617
- if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) return null;
618
- const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,lstart="]);
619
- const line = String(stdout).split("\n").find((row) => {
620
- const match = row.match(/\s*(\d+)\s+(.*)/);
621
- return match?.[1] === String(processInfo.pid);
622
- });
623
- const startedAt = line ? Date.parse(line.replace(/^\s*\d+\s+/, "")) : Number.NaN;
624
- if (!line || !Number.isFinite(startedAt)) return null;
625
- return Math.max(0, Date.now() - startedAt);
626
- }
627
-
628
616
  async terminateNativeCodex(pid, expectedCommand = null) {
629
617
  if (!Number.isInteger(pid) || pid <= 1) {
630
618
  throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
2
+ import { mkdir, open, readFile, rename, unlink, writeFile } from "node:fs/promises";
3
3
  import { ChatGPTLaunchCoordinator } from "./chatgpt-launch-coordinator.mjs";
4
4
  import { readConfig, FEATURE_IDS } from "./config.mjs";
5
5
 
@@ -121,9 +121,9 @@ export class RuntimeManager {
121
121
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
122
122
  stopTimeoutMs = 5_000,
123
123
  manualLaunchPollMs = 150,
124
- manualTakeoverGraceMs = 60_000,
125
124
  cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231),
126
125
  launchCoordinator = null,
126
+ fileSystem = { open, rename, unlink },
127
127
  }) {
128
128
  this.paths = paths;
129
129
  this.platform = platform;
@@ -133,8 +133,8 @@ export class RuntimeManager {
133
133
  this.sleep = sleep;
134
134
  this.stopTimeoutMs = stopTimeoutMs;
135
135
  this.manualLaunchPollMs = manualLaunchPollMs;
136
- this.manualTakeoverGraceMs = manualTakeoverGraceMs;
137
136
  this.cdpPort = cdpPort;
137
+ this.fileSystem = fileSystem;
138
138
  this.child = null;
139
139
  this.appliedFeatures = [];
140
140
  this.plugins = [];
@@ -265,10 +265,11 @@ export class RuntimeManager {
265
265
  };
266
266
  }
267
267
 
268
- async syncManualLaunchBaseline() {
268
+ async syncManualLaunchBaseline({ excludePids = [] } = {}) {
269
269
  const snapshot = await this.nativeCodexSnapshot();
270
270
  if (!snapshot) return;
271
- this.manualLaunchBaselinePids = new Set(snapshot.pids);
271
+ const excluded = new Set(excludePids);
272
+ this.manualLaunchBaselinePids = new Set([...snapshot.pids].filter((pid) => !excluded.has(pid)));
272
273
  this.manualLaunchArmed = true;
273
274
  }
274
275
 
@@ -329,19 +330,6 @@ export class RuntimeManager {
329
330
  this.manualLaunchArmed = false;
330
331
  }
331
332
 
332
- async isManualTakeoverAllowed(processInfo) {
333
- if (typeof this.platform?.codexProcessStartedMsAgo !== "function") return true;
334
- let startedMsAgo;
335
- try {
336
- startedMsAgo = await this.platform.codexProcessStartedMsAgo(processInfo);
337
- } catch (error) {
338
- this.logger.warn?.("MineCodex could not read the ChatGPT start time", error.message);
339
- return true;
340
- }
341
- if (!Number.isInteger(startedMsAgo) || startedMsAgo < 0) return true;
342
- return startedMsAgo < this.manualTakeoverGraceMs;
343
- }
344
-
345
333
  async reconcileManualCodexLaunch() {
346
334
  if (this.manualLaunchMonitorStopped || this.manualLaunchSuppressionDepth > 0) return false;
347
335
  const snapshot = await this.nativeCodexSnapshot();
@@ -375,6 +363,7 @@ export class RuntimeManager {
375
363
  this.enhancementFailure = null;
376
364
  this.manualLaunchSuppressionDepth += 1;
377
365
  return this.enqueue(async () => {
366
+ const retryProcessPids = new Set();
378
367
  try {
379
368
  if (this.manualLaunchMonitorStopped) return false;
380
369
  const latest = await this.nativeCodexSnapshot();
@@ -387,14 +376,6 @@ export class RuntimeManager {
387
376
  return false;
388
377
  }
389
378
  try {
390
- if (!(await this.isManualTakeoverAllowed(currentProcess))) {
391
- this.enhancementFailure = {
392
- code: "OFFICIAL_CHATGPT_IN_USE",
393
- message: "MineCodex left the official ChatGPT instance running because it was already in use.",
394
- phase: "launch",
395
- };
396
- return null;
397
- }
398
379
  const result = await this.launchCoordinator.ensureObservedLaunch(currentProcess, {
399
380
  stopRuntime: () => this.stopInternal(),
400
381
  startRuntime: () => this.startInternal(),
@@ -406,6 +387,8 @@ export class RuntimeManager {
406
387
  await this.syncManualLaunchBaseline();
407
388
  return result;
408
389
  } catch (error) {
390
+ retryProcessPids.add(observedProcess.pid);
391
+ for (const pid of error.retryProcessPids ?? []) retryProcessPids.add(pid);
409
392
  this.enhancementFailure = normalizedFailure({
410
393
  code: error.code ?? "CHATGPT_ENHANCEMENT_FAILED",
411
394
  message: error.message,
@@ -423,7 +406,7 @@ export class RuntimeManager {
423
406
  }
424
407
  } finally {
425
408
  this.manualLaunchArmed = true;
426
- await this.syncManualLaunchBaseline().catch((error) => {
409
+ await this.syncManualLaunchBaseline({ excludePids: retryProcessPids }).catch((error) => {
427
410
  this.logger.warn?.("MineCodex could not refresh the ChatGPT launch baseline", error.message);
428
411
  });
429
412
  this.manualLaunchSuppressionDepth -= 1;
@@ -441,7 +424,21 @@ export class RuntimeManager {
441
424
  if (!Number.isInteger(marker.pid) || marker.pid <= 1 || typeof marker.command !== "string" || !marker.command) {
442
425
  throw new Error("MineCodex cannot persist incomplete ChatGPT ownership identity.");
443
426
  }
444
- await writeFile(this.paths.codexPidPath, `${JSON.stringify(marker)}\n`, { mode: 0o600 });
427
+ const temporaryPath = `${this.paths.codexPidPath}.${process.pid}.${Date.now()}.tmp`;
428
+ let committed = false;
429
+ try {
430
+ const handle = await this.fileSystem.open(temporaryPath, "wx", 0o600);
431
+ try {
432
+ await handle.writeFile(`${JSON.stringify(marker)}\n`);
433
+ await handle.sync?.();
434
+ } finally {
435
+ await handle.close();
436
+ }
437
+ await this.fileSystem.rename(temporaryPath, this.paths.codexPidPath);
438
+ committed = true;
439
+ } finally {
440
+ if (!committed) await this.fileSystem.unlink(temporaryPath).catch(() => {});
441
+ }
445
442
  }
446
443
 
447
444
  async waitForHealthyRuntime(initialStatus = null, timeoutMs = 25_000) {