@bitkyc08/opencodex 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -30,12 +30,14 @@
30
30
  * catalog.ts:40-54) so tests can point fixtures via env or the explicit
31
31
  * `configPath` parameter without fighting the module-load-time const in paths.ts.
32
32
  */
33
- import { existsSync, readFileSync, unlinkSync } from "node:fs";
34
- import { join, resolve } from "node:path";
33
+ import { existsSync, readFileSync, statSync, unlinkSync } from "node:fs";
34
+ import { delimiter, dirname, join, resolve } from "node:path";
35
+ import { createRequire } from "node:module";
35
36
  import { realpathSync } from "node:fs";
36
- import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, expandUserPath } from "../config";
37
+ import { AtomicWriteResidualTempError, AtomicWriteSecretResidualError, atomicWriteFile, expandUserPath, getConfigDir } from "../config";
37
38
  import { forgetEphemeralSecretPath } from "../lib/windows-secret-acl";
38
39
  import { CODEX_CONFIG_PATH } from "./paths";
40
+ import { resolveAndPersistCodexRuntime } from "./runtime";
39
41
 
40
42
  /** Upstream codex-rs feature key: allow `request_user_input` in Default mode. */
41
43
  export const DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY = "default_mode_request_user_input";
@@ -451,16 +453,46 @@ function encodeTomlBasicString(value: string): string {
451
453
  */
452
454
  function decodeTomlStringToken(token: string): string | null {
453
455
  if (token.length < 2) return null;
456
+ // Multi-line literal string: `'''...'''` verbatim (backslashes not special).
457
+ if (token.startsWith("'''")) {
458
+ return token.endsWith("'''") && token.length >= 6
459
+ ? normalizeTomlMultilineBody(token.slice(3, -3))
460
+ : null;
461
+ }
462
+ // Multi-line basic string: `"""..."""` with escapes.
463
+ if (token.startsWith('"""')) {
464
+ if (!token.endsWith('"""') || token.length < 6) return null;
465
+ return decodeBasicStringBody(normalizeTomlMultilineBody(token.slice(3, -3)), true);
466
+ }
454
467
  if (token.startsWith("'")) {
455
468
  return token.endsWith("'") ? token.slice(1, -1) : null;
456
469
  }
457
470
  if (!token.startsWith('"') || !token.endsWith('"')) return null;
458
- const body = token.slice(1, -1);
471
+ return decodeBasicStringBody(token.slice(1, -1));
472
+ }
473
+
474
+ /** Apply TOML's multi-line newline rules before string-body decoding. */
475
+ function normalizeTomlMultilineBody(body: string): string {
476
+ const normalized = body.replace(/\r\n/g, "\n");
477
+ return normalized.startsWith("\n") ? normalized.slice(1) : normalized;
478
+ }
479
+
480
+ /** Unescape the body of a TOML basic string (single- or multi-line). */
481
+ function decodeBasicStringBody(body: string, multiline = false): string | null {
459
482
  let out = "";
460
483
  for (let i = 0; i < body.length; i++) {
461
484
  const ch = body[i];
462
485
  if (ch !== "\\") { out += ch; continue; }
463
486
  const esc = body[++i];
487
+ if (multiline) {
488
+ let newline = i;
489
+ while (body[newline] === " " || body[newline] === "\t") newline++;
490
+ if (body[newline] === "\n") {
491
+ i = newline;
492
+ while (body[i + 1] === " " || body[i + 1] === "\t" || body[i + 1] === "\n") i++;
493
+ continue;
494
+ }
495
+ }
464
496
  switch (esc) {
465
497
  case "\\": out += "\\"; break;
466
498
  case '"': out += '"'; break;
@@ -499,6 +531,28 @@ function scanTomlValueEnd(text: string, start: number): number {
499
531
  while (i < text.length && (text[i] === " " || text[i] === "\t")) i++;
500
532
  const first = text[i];
501
533
  if (first === '"') {
534
+ // Multi-line basic string: `"""..."""`. The closing delimiter is a triple
535
+ // quote; a single or double quote inside the body does not end the token.
536
+ if (text[i + 1] === '"' && text[i + 2] === '"') {
537
+ i += 3;
538
+ while (i < text.length) {
539
+ if (text[i] === "\\") { i += 2; continue; }
540
+ if (text[i] === '"' && text[i + 1] === '"' && text[i + 2] === '"') {
541
+ // TOML permits up to two quotes immediately inside the closing
542
+ // delimiter, so a 4- or 5-quote run means the extra one or two
543
+ // quotes are part of the value. Consume the full valid run and
544
+ // keep the surplus quotes in the body.
545
+ let end = i + 3;
546
+ if (text[end] === '"') {
547
+ end++;
548
+ if (text[end] === '"') end++;
549
+ }
550
+ return end;
551
+ }
552
+ i++;
553
+ }
554
+ return text.length;
555
+ }
502
556
  i++;
503
557
  while (i < text.length) {
504
558
  if (text[i] === "\\") { i += 2; continue; }
@@ -508,6 +562,24 @@ function scanTomlValueEnd(text: string, start: number): number {
508
562
  return text.length;
509
563
  }
510
564
  if (first === "'") {
565
+ // Multi-line literal string: `'''...'''`.
566
+ if (text[i + 1] === "'" && text[i + 2] === "'") {
567
+ i += 3;
568
+ while (i < text.length) {
569
+ if (text[i] === "'" && text[i + 1] === "'" && text[i + 2] === "'") {
570
+ // Same as multi-line basic: up to two surplus single quotes can
571
+ // precede the closing delimiter and remain part of the value.
572
+ let end = i + 3;
573
+ if (text[end] === "'") {
574
+ end++;
575
+ if (text[end] === "'") end++;
576
+ }
577
+ return end;
578
+ }
579
+ i++;
580
+ }
581
+ return text.length;
582
+ }
511
583
  const close = text.indexOf("'", i + 1);
512
584
  return close === -1 ? text.length : close + 1;
513
585
  }
@@ -546,6 +618,40 @@ function findInlineTableEnd(text: string, openIdx: number): number {
546
618
 
547
619
  interface InlineEntry { keyStart: number; valueStart: number; valueEnd: number }
548
620
 
621
+ /** Locate a top-level assignment while skipping complete (possibly multiline) values. */
622
+ function findTomlAssignment(text: string, key: string): InlineEntry | null {
623
+ let lineStart = 0;
624
+ while (lineStart < text.length) {
625
+ let i = lineStart;
626
+ while (text[i] === " " || text[i] === "\t") i++;
627
+ const keyStart = i;
628
+ let keyText = "";
629
+ if (text[i] === '"' || text[i] === "'") {
630
+ const keyEnd = scanTomlValueEnd(text, i);
631
+ keyText = decodeTomlStringToken(text.slice(i, keyEnd)) ?? "";
632
+ i = keyEnd;
633
+ } else {
634
+ const match = /^[A-Za-z0-9_-]+/.exec(text.slice(i));
635
+ if (match) {
636
+ keyText = match[0];
637
+ i += match[0].length;
638
+ }
639
+ }
640
+ while (text[i] === " " || text[i] === "\t") i++;
641
+ if (keyText && text[i] === "=") {
642
+ const valueStart = i + 1;
643
+ const valueEnd = scanTomlValueEnd(text, valueStart);
644
+ if (keyText === key) return { keyStart, valueStart, valueEnd };
645
+ const nextLine = text.indexOf("\n", valueEnd);
646
+ lineStart = nextLine === -1 ? text.length : nextLine + 1;
647
+ continue;
648
+ }
649
+ const nextLine = text.indexOf("\n", lineStart);
650
+ lineStart = nextLine === -1 ? text.length : nextLine + 1;
651
+ }
652
+ return null;
653
+ }
654
+
549
655
  /**
550
656
  * Locate `key = value` inside the inline-table body spanning [bodyStart, bodyEnd)
551
657
  * (exclusive of the braces), string-aware on both keys and values, or null.
@@ -579,6 +685,24 @@ function findInlineEntry(text: string, bodyStart: number, bodyEnd: number, key:
579
685
  return null;
580
686
  }
581
687
 
688
+ /** Whether a TOML assignment's value starts with a triple-quoted string. */
689
+ function isMultilineTomlString(text: string, valueStart: number): boolean {
690
+ let start = valueStart;
691
+ while (text[start] === " " || text[start] === "\t") start++;
692
+ return text.startsWith('"""', start) || text.startsWith("'''", start);
693
+ }
694
+
695
+ /** Whether one field in a supported inline `multi_agent_v2` table is triple-quoted. */
696
+ function hasInlineMultilineTomlString(text: string, assignment: InlineEntry, key: string): boolean {
697
+ let openIdx = assignment.valueStart;
698
+ while (text[openIdx] === " " || text[openIdx] === "\t") openIdx++;
699
+ if (text[openIdx] !== "{") return false;
700
+ const closeIdx = findInlineTableEnd(text, openIdx);
701
+ if (closeIdx === -1) return false;
702
+ const entry = findInlineEntry(text, openIdx + 1, closeIdx, key);
703
+ return entry !== null && isMultilineTomlString(text, entry.valueStart);
704
+ }
705
+
582
706
  /**
583
707
  * Set or remove one scalar key inside a top-level TOML table, preserving every
584
708
  * other line byte-for-byte — including the existing value's trailing comment,
@@ -602,22 +726,19 @@ function editScalarInTable(content: string, table: string, key: string, encoded:
602
726
  for (let i = headerIdx + 1; i < lines.length; i++) {
603
727
  if (/^\s*\[/.test(lines[i])) { end = i; break; }
604
728
  }
605
- const keyRe = new RegExp(`^(\\s*)${key}\\s*=\\s*`);
606
729
  for (let i = headerIdx + 1; i < end; i++) {
607
- const m = lines[i].match(keyRe);
608
- if (!m) continue;
730
+ const entry = findTomlAssignment(lines[i], key);
731
+ if (!entry) continue;
609
732
  // The existing value may itself contain '#', so scan the value token
610
733
  // string-aware instead of splitting on '#'.
611
734
  const line = lines[i];
612
- const valueStart = m[0].length;
613
- const valueEnd = scanTomlValueEnd(line, valueStart);
614
- const trailing = line.slice(valueEnd);
735
+ const trailing = line.slice(entry.valueEnd);
615
736
  if (encoded === null) {
616
737
  lines.splice(i, 1);
617
738
  return applyEol(lines.join("\n"), eol);
618
739
  }
619
- if (line.slice(valueStart, valueEnd).trim() === encoded) return content;
620
- lines[i] = `${m[1]}${key} = ${encoded}${trailing}`;
740
+ if (line.slice(entry.valueStart, entry.valueEnd).trim() === encoded) return content;
741
+ lines[i] = `${line.slice(0, entry.keyStart)}${key} = ${encoded}${trailing}`;
621
742
  return applyEol(lines.join("\n"), eol);
622
743
  }
623
744
  if (encoded === null) return content;
@@ -669,31 +790,95 @@ export function setAgentsMaxDepth(value: number | null, configPath?: string): Co
669
790
  * Reads both the dedicated-table and inline forms; dedicated wins, mirroring
670
791
  * `getMaxConcurrentThreads` precedence.
671
792
  */
672
- export function getSubagentDeveloperInstructions(configPath?: string): string | null {
793
+ /**
794
+ * Read a string-valued `features.multi_agent_v2` scalar (dedicated table or inline
795
+ * form; dedicated wins, mirroring `getMaxConcurrentThreads` precedence). Returns
796
+ * `null` when the key is absent or the config is unreadable. A present empty string
797
+ * round-trips faithfully — some upstream keys treat `""` and `null` differently.
798
+ */
799
+ function getV2StringField(key: string, configPath?: string): string | null {
673
800
  const content = readConfigText(configPath);
674
801
  if (content === null) return null;
675
- const table = tomlTableBody(content, "features.multi_agent_v2");
802
+ const table = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
676
803
  if (table !== null) {
677
- const m = table.match(/^\s*subagent_developer_instructions\s*=\s*/m);
678
- if (m) {
679
- const valueStart = m.index! + m[0].length;
680
- const token = table.slice(valueStart, scanTomlValueEnd(table, valueStart)).trim();
804
+ const entry = findTomlAssignment(table, key);
805
+ if (entry) {
806
+ const token = table.slice(entry.valueStart, entry.valueEnd).trim();
681
807
  return decodeTomlStringToken(token);
682
808
  }
683
809
  return null;
684
810
  }
685
- const features = tomlTableBody(content, "features");
811
+ const features = tomlTableBodyForStringFields(content, "features");
686
812
  if (features === null) return null;
687
- const m = features.match(/multi_agent_v2\s*=\s*\{/);
688
- if (!m) return null;
689
- const openIdx = m.index! + m[0].length - 1;
813
+ const v2Entry = findTomlAssignment(features, "multi_agent_v2");
814
+ if (!v2Entry) return null;
815
+ let openIdx = v2Entry.valueStart;
816
+ while (features[openIdx] === " " || features[openIdx] === "\t") openIdx++;
817
+ if (features[openIdx] !== "{") return null;
690
818
  const closeIdx = findInlineTableEnd(features, openIdx);
691
819
  if (closeIdx === -1) return null;
692
- const entry = findInlineEntry(features, openIdx + 1, closeIdx, "subagent_developer_instructions");
820
+ const entry = findInlineEntry(features, openIdx + 1, closeIdx, key);
693
821
  if (!entry) return null;
694
822
  return decodeTomlStringToken(features.slice(entry.valueStart, entry.valueEnd).trim());
695
823
  }
696
824
 
825
+ /**
826
+ * A table body scanner that skips complete TOML values before recognizing the
827
+ * next header. Unlike the legacy line scanner, bracket-shaped prose inside a
828
+ * multi-line string cannot truncate the table.
829
+ */
830
+ function tomlTableBodyForStringFields(content: string, header: string): string | null {
831
+ const escaped = escapeRegExp(header);
832
+ const match = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`, "m").exec(content);
833
+ if (!match) return null;
834
+ const newline = content.indexOf("\n", match.index + match[0].length);
835
+ if (newline === -1) return "";
836
+ const bodyStart = newline + 1;
837
+ let lineStart = bodyStart;
838
+ while (lineStart < content.length) {
839
+ let cursor = lineStart;
840
+ while (content[cursor] === " " || content[cursor] === "\t") cursor++;
841
+ if (content[cursor] === "[") return content.slice(bodyStart, lineStart);
842
+ const lineEnd = content.indexOf("\n", cursor);
843
+ const boundedEnd = lineEnd === -1 ? content.length : lineEnd;
844
+ let keyEnd = cursor;
845
+ if (content[keyEnd] === '"' || content[keyEnd] === "'") {
846
+ keyEnd = scanTomlValueEnd(content, keyEnd);
847
+ } else {
848
+ const keyMatch = /^[A-Za-z0-9_.-]+/.exec(content.slice(keyEnd, boundedEnd));
849
+ if (keyMatch) keyEnd += keyMatch[0].length;
850
+ }
851
+ while (content[keyEnd] === " " || content[keyEnd] === "\t") keyEnd++;
852
+ if (content[keyEnd] === "=" && keyEnd < boundedEnd) {
853
+ const valueEnd = scanTomlValueEnd(content, keyEnd + 1);
854
+ const nextLine = content.indexOf("\n", valueEnd);
855
+ lineStart = nextLine === -1 ? content.length : nextLine + 1;
856
+ } else {
857
+ lineStart = lineEnd === -1 ? content.length : lineEnd + 1;
858
+ }
859
+ }
860
+ return content.slice(bodyStart);
861
+ }
862
+
863
+ function escapeRegExp(value: string): string {
864
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
865
+ }
866
+
867
+ /** Bare and quoted TOML forms of one known-safe key. */
868
+ function tomlKeyPattern(key: string): string {
869
+ const escaped = escapeRegExp(key);
870
+ return `(?:${escaped}|"${escaped}"|'${escaped}')`;
871
+ }
872
+
873
+ export function getSubagentDeveloperInstructions(configPath?: string): string | null {
874
+ return getV2StringField("subagent_developer_instructions", configPath);
875
+ }
876
+
877
+ /** Current `features.multi_agent_v2.multi_agent_mode_hint_text`, or null when unset. */
878
+ export function getMultiAgentModeHintText(configPath?: string): string | null {
879
+ return getV2StringField("multi_agent_mode_hint_text", configPath);
880
+ }
881
+
697
882
  /**
698
883
  * Persist `features.multi_agent_v2.subagent_developer_instructions`, or remove the
699
884
  * key when `value` is null. Handles all three existing encodings: the dedicated
@@ -705,19 +890,49 @@ export function getSubagentDeveloperInstructions(configPath?: string): string |
705
890
  * carries `#[serde(deny_unknown_fields)]`, so a misspelling is not ignored — it is
706
891
  * a hard config-parse failure for the user's Codex.
707
892
  */
708
- export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
893
+ function setV2StringField(key: string, value: string | null, configPath?: string): ConfigEditResult {
709
894
  const path = configPath ?? activeCodexConfigPath();
710
895
  const content = readConfigText(path);
711
896
  if (content === null) return { ok: false, error: `config.toml not readable at ${path}` };
712
897
  const encoded = value === null ? null : encodeTomlBasicString(value);
713
898
 
714
- if (tomlTableBody(content, "features.multi_agent_v2") !== null) {
715
- const next = editScalarInTable(content, "features.multi_agent_v2", "subagent_developer_instructions", encoded);
899
+ // A parsed V2 object without one of the supported source forms came from
900
+ // dotted/quoted path segments. Appending a dedicated table would redefine it
901
+ // and make Codex reject the file, so fail closed without changing any bytes.
902
+ const parsedFeatures = parsedTomlTable(content, "features");
903
+ const parsedV2 = parsedFeatures === null ? null : plainTomlRecord(parsedFeatures.multi_agent_v2);
904
+ const dedicatedV2 = tomlTableBodyForStringFields(content, "features.multi_agent_v2") !== null;
905
+ const featuresBody = tomlTableBodyForStringFields(content, "features");
906
+ const featuresV2Entry = featuresBody === null ? null : findTomlAssignment(featuresBody, "multi_agent_v2");
907
+ const supportedFeaturesEntry = featuresV2Entry !== null;
908
+ if (parsedV2 !== null && !dedicatedV2 && !supportedFeaturesEntry) {
909
+ return { ok: false, error: "dotted or quoted multi_agent_v2 config is not supported for managed string fields" };
910
+ }
911
+
912
+ const dedicatedStringBody = tomlTableBodyForStringFields(content, "features.multi_agent_v2");
913
+ if (dedicatedStringBody !== null) {
914
+ // Multiline TOML strings ("""...""" / '''...''') span multiple lines and the
915
+ // single-line table editor cannot rewrite or remove them without corrupting
916
+ // the document. Scope this guard to the target V2 table so an unrelated
917
+ // table carrying the same key does not block an otherwise safe edit.
918
+ const dedicatedEntry = findTomlAssignment(dedicatedStringBody, key);
919
+ if (dedicatedEntry !== null && isMultilineTomlString(dedicatedStringBody, dedicatedEntry.valueStart)) {
920
+ return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
921
+ }
922
+ const legacyDedicatedBody = tomlTableBody(content, "features.multi_agent_v2") ?? "";
923
+ if (dedicatedEntry !== null && findTomlAssignment(legacyDedicatedBody, key) === null) {
924
+ return { ok: false, error: `cannot edit ${key} after a header-shaped multiline value safely` };
925
+ }
926
+ const next = editScalarInTable(content, "features.multi_agent_v2", key, encoded);
716
927
  if (next === content) return { ok: true, changed: false };
717
928
  atomicWriteFile(path, next);
718
929
  return { ok: true, changed: true };
719
930
  }
720
931
 
932
+ if (featuresBody !== null && featuresV2Entry !== null && hasInlineMultilineTomlString(featuresBody, featuresV2Entry, key)) {
933
+ return { ok: false, error: `multi-line TOML string for ${key} is not editable; convert it to a single-line string first` };
934
+ }
935
+
721
936
  const eol = dominantEol(content);
722
937
  const lines = content.split(/\r?\n/);
723
938
  const featuresHeader = lines.findIndex(l => /^\s*\[features\]\s*(?:#.*)?$/.test(l));
@@ -733,7 +948,7 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
733
948
  const openIdx = inlineMatch[0].length - 1;
734
949
  const closeIdx = findInlineTableEnd(line, openIdx);
735
950
  if (closeIdx === -1) return { ok: false, error: "malformed multi_agent_v2 inline table" };
736
- const entry = findInlineEntry(line, openIdx + 1, closeIdx, "subagent_developer_instructions");
951
+ const entry = findInlineEntry(line, openIdx + 1, closeIdx, key);
737
952
  if (encoded === null) {
738
953
  if (!entry) return { ok: true, changed: false };
739
954
  let start = entry.keyStart;
@@ -760,8 +975,8 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
760
975
  while (insertPos > openIdx + 1 && line[insertPos - 1] === " ") insertPos--;
761
976
  const hasEntries = line.slice(openIdx + 1, insertPos).trim().length > 0;
762
977
  const insertion = hasEntries
763
- ? `, subagent_developer_instructions = ${encoded} `
764
- : ` subagent_developer_instructions = ${encoded} `;
978
+ ? `, ${key} = ${encoded} `
979
+ : ` ${key} = ${encoded} `;
765
980
  lines[i] = line.slice(0, insertPos) + insertion + line.slice(closeIdx);
766
981
  }
767
982
  atomicWriteFile(path, applyEol(lines.join("\n"), eol));
@@ -770,21 +985,212 @@ export function setSubagentDeveloperInstructions(value: string | null, configPat
770
985
  const boolMatch = line.match(/^(\s*)multi_agent_v2\s*=\s*(true|false)(\s*(?:#.*)?)$/);
771
986
  if (boolMatch) {
772
987
  if (encoded === null) return { ok: true, changed: false };
773
- lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, subagent_developer_instructions = ${encoded} }${boolMatch[3]}`;
988
+ lines[i] = `${boolMatch[1]}multi_agent_v2 = { enabled = ${boolMatch[2]}, ${key} = ${encoded} }${boolMatch[3]}`;
774
989
  atomicWriteFile(path, applyEol(lines.join("\n"), eol));
775
990
  return { ok: true, changed: true };
776
991
  }
777
992
  }
993
+ if (featuresV2Entry !== null) {
994
+ return { ok: false, error: "multi_agent_v2 inside a multiline [features] table is not editable safely" };
995
+ }
778
996
  }
779
997
 
780
998
  if (encoded === null) return { ok: true, changed: false };
781
999
  const suffix = content.endsWith("\n") || content.length === 0 ? "" : eol;
782
1000
  const separator = content.length > 0 && !content.endsWith(`${eol}${eol}`) ? eol : "";
783
- const tableText = `[features.multi_agent_v2]${eol}subagent_developer_instructions = ${encoded}${eol}`;
1001
+ const tableText = `[features.multi_agent_v2]${eol}${key} = ${encoded}${eol}`;
784
1002
  atomicWriteFile(path, `${content}${suffix}${separator}${tableText}`);
785
1003
  return { ok: true, changed: true };
786
1004
  }
787
1005
 
1006
+ export function setSubagentDeveloperInstructions(value: string | null, configPath?: string): ConfigEditResult {
1007
+ return setV2StringField("subagent_developer_instructions", value, configPath);
1008
+ }
1009
+
1010
+ export const MODE_HINT_UNSUPPORTED_ERROR =
1011
+ "installed Codex does not support multi_agent_mode_hint_text; update Codex first";
1012
+
1013
+ /**
1014
+ * Persist `features.multi_agent_v2.multi_agent_mode_hint_text`, or remove the key
1015
+ * when `value` is null. Same encoding coverage and upstream-name discipline as
1016
+ * `setSubagentDeveloperInstructions`; the upstream struct rejects unknown fields,
1017
+ * so the key spelling must match codex-rs exactly.
1018
+ */
1019
+ export function setMultiAgentModeHintText(value: string | null, configPath?: string): ConfigEditResult {
1020
+ // The upstream `multi_agent_mode_hint_text` key is newer than the v2 config
1021
+ // surface opencodex already manages; an older Codex build rejects the unknown
1022
+ // member (`#[serde(deny_unknown_fields)]`) and fails to start. Probe the
1023
+ // installed runtime binary for the key string and refuse the write when the
1024
+ // binary provably lacks it. A probe that cannot run (missing binary,
1025
+ // unreadable file) does not block: that is the test/hermetic path and the
1026
+ // headless runtime fallback.
1027
+ if (value !== null) {
1028
+ const probe = probeCodexSupportsModeHint();
1029
+ if (probe === false) {
1030
+ return {
1031
+ ok: false,
1032
+ error: MODE_HINT_UNSUPPORTED_ERROR,
1033
+ };
1034
+ }
1035
+ }
1036
+ return setV2StringField("multi_agent_mode_hint_text", value, configPath);
1037
+ }
1038
+
1039
+ const modeHintCapabilityCache = new Map<string, boolean | null>();
1040
+
1041
+ /**
1042
+ * True when the installed Codex runtime binary contains the
1043
+ * `multi_agent_mode_hint_text` config key, false when it provably does not, and
1044
+ * null when the probe could not run (missing binary, unreadable file).
1045
+ */
1046
+ export function probeCodexSupportsModeHint(): boolean | null {
1047
+ try {
1048
+ const runtime = resolveAndPersistCodexRuntime({ env: process.env }).runtime;
1049
+ const selectedPath = resolveSelectedCommandPath(runtime.command);
1050
+ let selectedIdentity = selectedPath ?? "";
1051
+ try { if (selectedPath) selectedIdentity = realpathSync(selectedPath); } catch { /* keep lexical path */ }
1052
+ const candidates = codexNativeBinaryCandidates(runtime.command);
1053
+ const binaryStatFingerprint = candidates.map(candidate => {
1054
+ try {
1055
+ const stat = statSync(candidate);
1056
+ return `${candidate}\0${stat.dev}\0${stat.ino}\0${stat.size}\0${stat.mtimeMs}\0${stat.ctimeMs}`;
1057
+ } catch {
1058
+ return `${candidate}\0missing`;
1059
+ }
1060
+ }).join("\0");
1061
+ const cacheKey = `${runtime.command}\0${runtime.version ?? ""}\0${selectedIdentity}\0${binaryStatFingerprint}`;
1062
+ const cached = modeHintCapabilityCache.get(cacheKey);
1063
+ if (cached !== undefined) return cached;
1064
+ let sawBinary = false;
1065
+ for (const candidate of candidates) {
1066
+ try {
1067
+ if (!existsSync(candidate)) continue;
1068
+ const buf = readFileSync(candidate);
1069
+ if (!isNativeExecutable(buf)) continue;
1070
+ sawBinary = true;
1071
+ if (buf.includes(Buffer.from("multi_agent_mode_hint_text", "utf8"))) {
1072
+ modeHintCapabilityCache.set(cacheKey, true);
1073
+ return true;
1074
+ }
1075
+ } catch {
1076
+ continue;
1077
+ }
1078
+ }
1079
+ // At least one real binary was inspected and none contained the key.
1080
+ const result = sawBinary ? false : null;
1081
+ modeHintCapabilityCache.set(cacheKey, result);
1082
+ return result;
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+
1088
+ const CODEX_PLATFORM_PACKAGES = [
1089
+ ["codex-darwin-arm64", "aarch64-apple-darwin", "codex"],
1090
+ ["codex-darwin-x64", "x86_64-apple-darwin", "codex"],
1091
+ ["codex-linux-x64", "x86_64-unknown-linux-musl", "codex"],
1092
+ ["codex-linux-arm64", "aarch64-unknown-linux-musl", "codex"],
1093
+ ["codex-win32-x64", "x86_64-pc-windows-msvc", "codex.exe"],
1094
+ ["codex-win32-arm64", "aarch64-pc-windows-msvc", "codex.exe"],
1095
+ ] as const;
1096
+
1097
+ function isNativeExecutable(buf: Buffer): boolean {
1098
+ if (buf.length < 4) return false;
1099
+ if (buf[0] === 0x4d && buf[1] === 0x5a) return true; // PE/COFF
1100
+ const magic = buf.readUInt32BE(0);
1101
+ return magic === 0x7f454c46 // ELF
1102
+ || magic === 0xfeedface || magic === 0xfeedfacf
1103
+ || magic === 0xcefaedfe || magic === 0xcffaedfe
1104
+ || magic === 0xcafebabe || magic === 0xbebafeca; // Mach-O/fat Mach-O
1105
+ }
1106
+
1107
+ /**
1108
+ * Candidate native codex binaries to probe. The resolved `command` may be the
1109
+ * opencodex shim or the npm JS wrapper (`codex.opencodex-real`), neither of
1110
+ * which embeds the Rust config schema. The real binary ships under the
1111
+ * platform package's `vendor/<triple>/bin/codex`; also try adjacent wrappers.
1112
+ */
1113
+ function codexNativeBinaryCandidates(command: string): string[] {
1114
+ const out = new Set<string>();
1115
+ const resolverBases = new Set<string>();
1116
+ const selectedPath = resolveSelectedCommandPath(command);
1117
+
1118
+ const addSelectedTarget = (target: string) => {
1119
+ try {
1120
+ const real = realpathSync(target);
1121
+ out.add(target);
1122
+ out.add(real);
1123
+ if (/[\\/]@openai[\\/]codex[\\/]bin[\\/]/.test(real)) resolverBases.add(real);
1124
+ } catch {
1125
+ // Missing backing paths cannot establish capability either way.
1126
+ }
1127
+ };
1128
+
1129
+ // Follow the resolved command to its real location. The opencodex shim and
1130
+ // the npm JS wrapper resolve to `@openai/codex/bin/codex.js`; the native
1131
+ // binary lives in the sibling platform package's vendor directory.
1132
+ if (selectedPath) {
1133
+ addSelectedTarget(selectedPath);
1134
+ for (const target of selectedShimBackingPaths(selectedPath)) addSelectedTarget(target);
1135
+ }
1136
+
1137
+ // npm's Windows wrappers are ordinary scripts. Resolve from both the global
1138
+ // prefix layout and a project-local node_modules/.bin layout; createRequire
1139
+ // then follows the selected installation's own dependency tree (including
1140
+ // pnpm/nested optional dependencies) without consulting unrelated PATH bins.
1141
+ const wrapperPath = selectedPath ?? command;
1142
+ if (/\.(?:cmd|ps1)$/i.test(wrapperPath)) {
1143
+ resolverBases.add(resolve(dirname(wrapperPath), "node_modules", "@openai", "codex", "bin", "codex.js"));
1144
+ resolverBases.add(resolve(dirname(wrapperPath), "..", "@openai", "codex", "bin", "codex.js"));
1145
+ }
1146
+
1147
+ for (const base of resolverBases) {
1148
+ const requireFromSelected = createRequire(base);
1149
+ for (const [pkg, triple, exe] of CODEX_PLATFORM_PACKAGES) {
1150
+ try {
1151
+ const manifest = requireFromSelected.resolve(`@openai/${pkg}/package.json`);
1152
+ out.add(join(dirname(manifest), "vendor", triple, "bin", exe));
1153
+ } catch {
1154
+ // Optional platform packages for other targets are normally absent.
1155
+ }
1156
+ }
1157
+ }
1158
+ return [...out];
1159
+ }
1160
+
1161
+ /** Backing paths tied to this exact OCX shim entry in codex-shim.json. */
1162
+ function selectedShimBackingPaths(commandPath: string): string[] {
1163
+ try {
1164
+ const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
1165
+ wrapperPath?: unknown;
1166
+ originalPath?: unknown;
1167
+ backupPath?: unknown;
1168
+ realPath?: unknown;
1169
+ wrappers?: Array<Record<string, unknown>>;
1170
+ };
1171
+ const entries = Array.isArray(state.wrappers) && state.wrappers.length > 0 ? state.wrappers : [state];
1172
+ const selected = resolve(commandPath);
1173
+ for (const entry of entries) {
1174
+ if (typeof entry.wrapperPath !== "string" || resolve(entry.wrapperPath) !== selected) continue;
1175
+ return [entry.backupPath, entry.realPath, entry.originalPath]
1176
+ .filter((value): value is string => typeof value === "string" && value.length > 0 && resolve(value) !== selected);
1177
+ }
1178
+ } catch {
1179
+ // Not an OCX-owned shim, or no readable state.
1180
+ }
1181
+ return [];
1182
+ }
1183
+
1184
+ /** Resolve only the selected bare command against PATH; never enumerate peers. */
1185
+ function resolveSelectedCommandPath(command: string): string | null {
1186
+ if (command.includes("/") || command.includes("\\")) return existsSync(command) ? resolve(command) : null;
1187
+ for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
1188
+ const candidate = join(dir, command);
1189
+ if (existsSync(candidate)) return candidate;
1190
+ }
1191
+ return null;
1192
+ }
1193
+
788
1194
  function editAgentsMaxThreads(value: number | null, configPath?: string, migratedComment?: string): ConfigEditResult {
789
1195
  const path = configPath ?? activeCodexConfigPath();
790
1196
  const content = readConfigText(path);
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { homedir } from "node:os";
21
- import { join } from "node:path";
22
21
 
23
22
  import type {
24
23
  CodexHistoryWorkerOperation,
@@ -26,19 +25,15 @@ import type {
26
25
  } from "./history-worker";
27
26
  import { historyBackupPathFor } from "./history-provider";
28
27
  import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider";
29
- import { getCodexHome } from "./paths";
30
-
31
- /** Where Codex keeps its resume history, and the manifest that shadows it. */
32
- const STATE_DB_FILE = "state_5.sqlite";
28
+ import { getCodexHome, resolveCodexStateDbPath } from "./paths";
33
29
 
34
30
  /**
35
31
  * Resolve the paths a history job needs, at CALL time.
36
32
  *
37
- * `history-provider.ts` resolves its equivalents at module load (`:16`, `:22`),
38
- * which is fine in one process and wrong for a Worker: the Worker does not
39
- * inherit them, so anything derived from those constants would address a
40
- * different home than the caller intended. Resolving here also means a test that
41
- * moves `CODEX_HOME` is honoured rather than ignored.
33
+ * The SQLite root can differ from CODEX_HOME and both environment/config inputs
34
+ * can change between invocations. The parent resolves one exact target and hands
35
+ * those canonical paths to the Worker rather than asking the Worker to infer a
36
+ * possibly different environment.
42
37
  */
43
38
  export function resolveCodexHistoryJobTarget(): {
44
39
  readonly canonicalCodexHome: string;
@@ -46,7 +41,7 @@ export function resolveCodexHistoryJobTarget(): {
46
41
  readonly canonicalBackupPath: string;
47
42
  } {
48
43
  const home = getCodexHome();
49
- const stateDb = join(home, STATE_DB_FILE);
44
+ const stateDb = resolveCodexStateDbPath({ codexHome: home });
50
45
  return {
51
46
  canonicalCodexHome: home,
52
47
  canonicalStateDbPath: stateDb,