@bitkyc08/opencodex 2.58.0 → 2.60.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 (303) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-BTuCbqQd.css +1 -0
  3. package/gui/dist/assets/index-DoBVdPHP.js +134 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +4 -1
  8. package/src/adapters/anthropic-image-codec.ts +16 -2
  9. package/src/adapters/anthropic-image-normalize.ts +49 -2
  10. package/src/adapters/anthropic.ts +4 -1
  11. package/src/adapters/base.ts +34 -1
  12. package/src/adapters/coding-agent/turn.ts +22 -2
  13. package/src/adapters/command-code.ts +50 -3
  14. package/src/adapters/cursor/catalog.ts +11 -0
  15. package/src/adapters/cursor/checkpoint-store.ts +3 -0
  16. package/src/adapters/cursor/discovery.ts +11 -8
  17. package/src/adapters/cursor/effort-map.ts +16 -2
  18. package/src/adapters/cursor/envelope-echo.ts +55 -2
  19. package/src/adapters/cursor/live-transport.ts +26 -9
  20. package/src/adapters/cursor/message-mapper.ts +3 -2
  21. package/src/adapters/cursor/protobuf-request.ts +8 -5
  22. package/src/adapters/cursor/request-builder.ts +21 -4
  23. package/src/adapters/cursor/thread-continuity.ts +105 -31
  24. package/src/adapters/cursor/tool-guidance.ts +5 -4
  25. package/src/adapters/cursor/transport.ts +19 -0
  26. package/src/adapters/cursor.ts +45 -2
  27. package/src/adapters/devin/cloud-direct/chat.ts +14 -3
  28. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  29. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
  30. package/src/adapters/devin.ts +125 -26
  31. package/src/adapters/google-antigravity-replay.ts +1 -1
  32. package/src/adapters/google-antigravity-wire.ts +55 -4
  33. package/src/adapters/google-http.ts +57 -11
  34. package/src/adapters/google-tool-schema.ts +595 -31
  35. package/src/adapters/google-wire-compiler.ts +93 -10
  36. package/src/adapters/google-wire-shape.ts +461 -0
  37. package/src/adapters/google.ts +60 -10
  38. package/src/adapters/openai-chat/response-events.ts +61 -0
  39. package/src/adapters/openai-chat-images.ts +3 -1
  40. package/src/adapters/openai-chat.ts +10 -11
  41. package/src/adapters/openai-responses/image-gen.ts +8 -6
  42. package/src/adapters/openai-responses/passthrough.ts +25 -4
  43. package/src/adapters/openai-responses/reasoning.ts +7 -0
  44. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  45. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  46. package/src/adapters/opencode-go-additional-tools.ts +12 -2
  47. package/src/adapters/responses-tool-schema.ts +76 -46
  48. package/src/adapters/run-turn-queue.ts +17 -4
  49. package/src/bridge/response-json.ts +1 -1
  50. package/src/bridge/sse.ts +28 -35
  51. package/src/claude/context-windows.ts +22 -0
  52. package/src/claude/outbound.ts +35 -4
  53. package/src/cli/account-api.ts +4 -3
  54. package/src/cli/account-extended.ts +26 -6
  55. package/src/cli/account-orca-import.ts +63 -0
  56. package/src/cli/account.ts +32 -4
  57. package/src/cli/capabilities.ts +40 -0
  58. package/src/cli/claude.ts +29 -1
  59. package/src/cli/codex-cli-update.ts +97 -2
  60. package/src/cli/dispatch.ts +57 -3
  61. package/src/cli/doctor.ts +218 -4
  62. package/src/cli/help.ts +4 -1
  63. package/src/cli/hub.ts +3 -2
  64. package/src/cli/index.ts +95 -22
  65. package/src/cli/models-runtime.ts +33 -4
  66. package/src/cli/opencode.ts +2 -2
  67. package/src/cli/provider.ts +13 -1
  68. package/src/cli/registry.ts +11 -1
  69. package/src/cli/runtime-api.ts +44 -0
  70. package/src/cli/start-args.ts +94 -0
  71. package/src/cli/system-command.ts +2 -0
  72. package/src/client/machine-api.ts +6 -5
  73. package/src/client/machine-listener.ts +16 -3
  74. package/src/client/runtime.ts +26 -2
  75. package/src/clients/config-export/constants.ts +2 -3
  76. package/src/clients/config-export.ts +5 -5
  77. package/src/codex/account-store.ts +146 -5
  78. package/src/codex/auth-api/account-list.ts +19 -11
  79. package/src/codex/auth-api/pool-quota-probe.ts +44 -10
  80. package/src/codex/auth-api/routes.ts +17 -2
  81. package/src/codex/auth-context.ts +16 -12
  82. package/src/codex/catalog/build-entries.ts +25 -4
  83. package/src/codex/catalog/derive-entry.ts +8 -1
  84. package/src/codex/catalog/effort.ts +10 -6
  85. package/src/codex/catalog/gather-capture.ts +22 -2
  86. package/src/codex/catalog/model-hints.ts +66 -33
  87. package/src/codex/catalog/parsing.ts +90 -5
  88. package/src/codex/catalog/provider-models.ts +19 -2
  89. package/src/codex/catalog/reserve-warn.ts +96 -0
  90. package/src/codex/catalog/retained-sync.ts +41 -26
  91. package/src/codex/catalog/routed-gather.ts +61 -3
  92. package/src/codex/cli-installation-identity.ts +210 -0
  93. package/src/codex/cli-installation-targets.ts +158 -0
  94. package/src/codex/context-compat.ts +5 -2
  95. package/src/codex/convergence.ts +5 -0
  96. package/src/codex/desired-state.ts +4 -1
  97. package/src/codex/history-job.ts +6 -6
  98. package/src/codex/history-provider.ts +24 -167
  99. package/src/codex/history-rollout-read.ts +174 -0
  100. package/src/codex/history-state-open.ts +105 -0
  101. package/src/codex/inject/config-toml.ts +44 -2
  102. package/src/codex/inject.ts +3 -2
  103. package/src/codex/internal/catalog-writer.ts +33 -1
  104. package/src/codex/lineage.ts +83 -32
  105. package/src/codex/loopback-target.ts +31 -0
  106. package/src/codex/main-account-hard-lock.ts +2 -1
  107. package/src/codex/main-account.ts +10 -3
  108. package/src/codex/main-device-reauth.ts +17 -9
  109. package/src/codex/model-cache.ts +47 -0
  110. package/src/codex/model-entitlements.ts +80 -2
  111. package/src/codex/observed-model-denials.ts +230 -0
  112. package/src/codex/orca-auth-source.ts +94 -0
  113. package/src/codex/orca-import.ts +219 -0
  114. package/src/codex/prompt-text-probe.ts +289 -16
  115. package/src/codex/quota-401-recovery.ts +12 -0
  116. package/src/codex/quota-types.ts +65 -0
  117. package/src/codex/quota.ts +24 -19
  118. package/src/codex/routing/cooldown-math.ts +8 -47
  119. package/src/codex/routing/pin-drain.ts +57 -0
  120. package/src/codex/routing.ts +20 -16
  121. package/src/codex/shim.ts +1 -1
  122. package/src/codex/subagent-model-fallback.ts +114 -2
  123. package/src/codex/windows-installation-files.ts +224 -0
  124. package/src/combos/failover.ts +125 -5
  125. package/src/config/admitted-identity.ts +222 -0
  126. package/src/config/diagnostics.ts +43 -1
  127. package/src/config/feature-flags.ts +5 -0
  128. package/src/config/load-degrade.ts +33 -0
  129. package/src/config/pending-teardown.ts +8 -0
  130. package/src/config/process-state.ts +36 -3
  131. package/src/config/provider-relative-send-path.ts +16 -0
  132. package/src/config/proxy-env.ts +31 -7
  133. package/src/config/schema/compaction-triggers.ts +11 -0
  134. package/src/config/schema/config-schema.ts +25 -0
  135. package/src/config/schema/leaf-validators.ts +76 -17
  136. package/src/config.ts +2 -2
  137. package/src/generated/compatibility-version.json +410 -258
  138. package/src/generated/model-metadata.ts +1 -1
  139. package/src/grok/reset-coupons.ts +38 -19
  140. package/src/images/loop.ts +6 -1
  141. package/src/integrations/aside-profile-context.ts +37 -3
  142. package/src/integrations/aside-profile-journal.ts +68 -3
  143. package/src/integrations/aside-profiles.ts +128 -3
  144. package/src/integrations/mutation-plan.ts +815 -0
  145. package/src/integrations/writer.ts +85 -99
  146. package/src/lab/live/transport.ts +4 -0
  147. package/src/lab/live/types.ts +5 -0
  148. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  149. package/src/lib/admin-secrets.ts +9 -1
  150. package/src/lib/bounded-body.ts +4 -2
  151. package/src/lib/debug-log-buffer.ts +6 -1
  152. package/src/lib/debug.ts +23 -0
  153. package/src/lib/destination-policy.ts +48 -6
  154. package/src/lib/errors.ts +82 -15
  155. package/src/lib/http-response-semantics.ts +57 -0
  156. package/src/lib/lab-live-pinned-sender.ts +26 -12
  157. package/src/lib/local-destinations.ts +32 -5
  158. package/src/lib/pinned-http.ts +142 -2
  159. package/src/lib/plain-data.ts +103 -0
  160. package/src/lib/process-control.ts +13 -5
  161. package/src/lib/provider-outbound.ts +53 -5
  162. package/src/lib/proxy-env.ts +70 -3
  163. package/src/lib/request-execution-budget.ts +11 -3
  164. package/src/lib/response-body-inactivity.ts +193 -0
  165. package/src/lib/retry-delay.ts +69 -0
  166. package/src/lib/socks5-fetch.ts +741 -0
  167. package/src/lib/spend-ledger-owner.ts +364 -0
  168. package/src/lib/spend-reservation-ledger.ts +332 -35
  169. package/src/lib/windows-system-proxy.ts +16 -11
  170. package/src/lib/workflow-budget.ts +145 -8
  171. package/src/oauth/account-quota-rank.ts +72 -15
  172. package/src/oauth/callback-server.ts +4 -3
  173. package/src/oauth/generic-account-failover.ts +41 -27
  174. package/src/oauth/health.ts +12 -1
  175. package/src/oauth/index.ts +3 -107
  176. package/src/oauth/login-flow-state.ts +127 -0
  177. package/src/oauth/orcarouter.ts +15 -2
  178. package/src/oauth/store.ts +8 -0
  179. package/src/providers/codex-capacity.ts +9 -0
  180. package/src/providers/derive.ts +34 -17
  181. package/src/providers/devin-cli-authmode-migration.ts +14 -10
  182. package/src/providers/devin-provider-merge-migration.ts +33 -12
  183. package/src/providers/free-directory.ts +20 -2
  184. package/src/providers/key-failover.ts +327 -25
  185. package/src/providers/model-rename-migration.ts +56 -1
  186. package/src/providers/model-rename-startup.ts +7 -5
  187. package/src/providers/openai-sidecar.ts +4 -0
  188. package/src/providers/openai-virtual-models.ts +42 -2
  189. package/src/providers/opencode-go-transport.ts +14 -5
  190. package/src/providers/quota/antigravity.ts +22 -2
  191. package/src/providers/quota/report-cache.ts +3 -0
  192. package/src/providers/quota/vendor-probes-key.ts +1 -1
  193. package/src/providers/registry/entries-core.ts +39 -17
  194. package/src/providers/registry/entries-extended.ts +131 -1
  195. package/src/providers/registry/model-ids.ts +168 -0
  196. package/src/providers/registry/model-seeds.ts +87 -21
  197. package/src/providers/registry/types.ts +2 -0
  198. package/src/providers/resolved-model-policy-merge.ts +167 -0
  199. package/src/providers/resolved-model-policy.ts +406 -0
  200. package/src/providers/stale-vision-classification-migration.ts +137 -0
  201. package/src/responses/apply-patch-envelope.ts +32 -11
  202. package/src/responses/bridge-search-replay-cache.ts +152 -0
  203. package/src/responses/code-mode-helper-compat.ts +26 -16
  204. package/src/responses/custom-tool-compat.ts +1 -1
  205. package/src/responses/freeform-wrapper-scan.ts +279 -0
  206. package/src/responses/hosted-tool-policy.ts +85 -2
  207. package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
  208. package/src/responses/progressive-freeform-input.ts +130 -0
  209. package/src/responses/reasoning-envelope.ts +30 -0
  210. package/src/responses/schema.ts +9 -2
  211. package/src/responses/state.ts +5 -12
  212. package/src/responses/tool-name-aliases.ts +15 -1
  213. package/src/router.ts +91 -115
  214. package/src/routing/compatibility/behavior.ts +9 -0
  215. package/src/routing/compatibility/subject.ts +16 -1
  216. package/src/server/adapter-resolve.ts +9 -0
  217. package/src/server/auth-cors.ts +29 -0
  218. package/src/server/chat-completions.ts +13 -5
  219. package/src/server/chat-native-sse.ts +26 -9
  220. package/src/server/chat-native.ts +10 -4
  221. package/src/server/claude-messages.ts +30 -5
  222. package/src/server/effort-row.ts +11 -3
  223. package/src/server/grok-responses-control-frame.ts +160 -1
  224. package/src/server/gui-static.ts +36 -2
  225. package/src/server/inbound-body-admission.ts +187 -0
  226. package/src/server/index/serve-options.ts +86 -29
  227. package/src/server/index/spend-ledger-lifecycle.ts +66 -0
  228. package/src/server/index/websocket-handler.ts +6 -1
  229. package/src/server/index.ts +24 -25
  230. package/src/server/management/api-access.ts +3 -4
  231. package/src/server/management/aside-profile-routes.ts +266 -7
  232. package/src/server/management/config-routes.ts +48 -7
  233. package/src/server/management/context.ts +3 -0
  234. package/src/server/management/integration-routes.ts +287 -5
  235. package/src/server/management/metrics-routes.ts +20 -0
  236. package/src/server/management/model-rows.ts +224 -12
  237. package/src/server/management/provider-capability-config.ts +35 -7
  238. package/src/server/management/provider-routes.ts +70 -18
  239. package/src/server/management/route-registry.ts +14 -0
  240. package/src/server/management/shared.ts +10 -3
  241. package/src/server/management/system-restart.ts +7 -2
  242. package/src/server/management/system-routes.ts +2 -0
  243. package/src/server/management/usage-aggregate-cache.ts +4 -0
  244. package/src/server/management-api.ts +2 -0
  245. package/src/server/management-auth.ts +15 -1
  246. package/src/server/proxy-liveness.ts +97 -2
  247. package/src/server/readiness.ts +29 -10
  248. package/src/server/relay-eager.ts +24 -2
  249. package/src/server/relay.ts +136 -34
  250. package/src/server/request-log.ts +80 -3
  251. package/src/server/request-metrics.ts +236 -0
  252. package/src/server/responses/adapter-continuation.ts +74 -30
  253. package/src/server/responses/adapter-delivery.ts +39 -8
  254. package/src/server/responses/adapter-dispatch.ts +63 -30
  255. package/src/server/responses/compact.ts +89 -18
  256. package/src/server/responses/compaction-routing.ts +111 -0
  257. package/src/server/responses/core-codex-account.ts +90 -24
  258. package/src/server/responses/core-combo.ts +7 -7
  259. package/src/server/responses/core-normalize.ts +16 -15
  260. package/src/server/responses/core-opaque-recovery.ts +1 -0
  261. package/src/server/responses/core-options.ts +4 -0
  262. package/src/server/responses/encrypted-payload.ts +20 -2
  263. package/src/server/responses/fetch-helpers.ts +68 -2
  264. package/src/server/responses/passthrough-delivery.ts +41 -15
  265. package/src/server/responses/passthrough-dispatch.ts +145 -53
  266. package/src/server/responses/passthrough-execution.ts +11 -1
  267. package/src/server/responses/policy-fallback.ts +5 -13
  268. package/src/server/responses/request-prepare.ts +96 -17
  269. package/src/server/responses/request-send-budget.ts +89 -8
  270. package/src/server/responses/request-sidecar-auth.ts +17 -9
  271. package/src/server/responses/request-spend.ts +38 -9
  272. package/src/server/responses/request-transport.ts +15 -12
  273. package/src/server/responses/run-turn-execution.ts +45 -8
  274. package/src/server/responses/sidecar-execution.ts +19 -2
  275. package/src/server/responses/ws-upstream.ts +16 -28
  276. package/src/server/responses-custom-tool-repair.ts +29 -56
  277. package/src/server/responses-undeclared-tool-guard.ts +31 -1
  278. package/src/server/sse-frame-buffer.ts +12 -10
  279. package/src/server/sse-payload-rewrite.ts +37 -10
  280. package/src/server/system-env-shell.ts +5 -1
  281. package/src/server/system-env.ts +7 -1
  282. package/src/server/workflow-refusal.ts +56 -2
  283. package/src/service/cli.ts +16 -6
  284. package/src/service/guards.ts +10 -0
  285. package/src/service/health.ts +43 -0
  286. package/src/service/state.ts +7 -2
  287. package/src/tray/windows-tray.ps1 +155 -3
  288. package/src/types/accounts.ts +4 -0
  289. package/src/types/config.ts +111 -6
  290. package/src/types/provider.ts +37 -0
  291. package/src/types/request.ts +9 -1
  292. package/src/types/tools.ts +14 -0
  293. package/src/types/wire.ts +9 -1
  294. package/src/types.ts +1 -0
  295. package/src/usage/expected-prices.ts +28 -0
  296. package/src/usage/log.ts +87 -4
  297. package/src/vision/eligibility.ts +88 -9
  298. package/src/vision/plan.ts +34 -10
  299. package/src/web-search/executor.ts +41 -2
  300. package/src/web-search/loop.ts +6 -1
  301. package/src/web-search/passthrough-bridge.ts +39 -5
  302. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
  303. package/gui/dist/assets/index-C5-RdDmD.css +0 -1
@@ -0,0 +1,219 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstatSync, unlinkSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { atomicWriteFile, getConfigDir, getConfigPath, mutatePersistedConfig, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
5
+ import { readAlivePid, readRuntimePort } from "../config/process-state";
6
+ import { getCodexHome } from "./paths";
7
+ import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
8
+ import { assertPlainLocalPath, ORCA_ACCOUNT_DIRECTORY, parseOrcaAuth, readBoundedLocalFile, readOrcaAuthSource, sameLocalPath } from "./orca-auth-source";
9
+
10
+ export class OrcaImportError extends Error {
11
+ constructor(message: string) { super(message); this.name = "OrcaImportError"; }
12
+ }
13
+
14
+ export interface OrcaImportResult {
15
+ mode: "preview" | "apply";
16
+ discovered: number;
17
+ eligible: number;
18
+ imported: number;
19
+ duplicates: number;
20
+ invalid: number;
21
+ invalidReasons: Partial<Record<OrcaImportInvalidReason, number>>;
22
+ }
23
+
24
+ export type OrcaImportInvalidReason = "unsupported_entry" | "home_mismatch" | "source_invalid";
25
+
26
+ class OrcaImportEntryError extends Error {
27
+ constructor(readonly reason: OrcaImportInvalidReason) { super(reason); }
28
+ }
29
+
30
+ function noteInvalid(result: OrcaImportResult, error: unknown): void {
31
+ const reason = error instanceof OrcaImportEntryError ? error.reason : "source_invalid";
32
+ result.invalid++;
33
+ result.invalidReasons[reason] = (result.invalidReasons[reason] ?? 0) + 1;
34
+ }
35
+
36
+ function optionalFile(path: string): string | undefined {
37
+ try { return readBoundedLocalFile(path, 4 * 1024 * 1024); }
38
+ catch (error) {
39
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
40
+ throw new OrcaImportError("A target or main credential file is unreadable; import refused.");
41
+ }
42
+ }
43
+
44
+ function object(value: unknown): Record<string, unknown> {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new OrcaImportError("Invalid existing account state; import refused.");
46
+ return value as Record<string, unknown>;
47
+ }
48
+
49
+ function assertStopped(): void {
50
+ let running = readAlivePid() !== null;
51
+ const runtime = readRuntimePort();
52
+ if (runtime) {
53
+ try { process.kill(runtime.pid, 0); running = true; }
54
+ catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") running = true; }
55
+ }
56
+ if (running) throw new OrcaImportError("Stop the opencodex proxy before applying an Orca import.");
57
+ }
58
+
59
+ function prepare(sourceDir: string, registryPath: string) {
60
+ const configRaw = optionalFile(getConfigPath());
61
+ if (!configRaw || readConfigDiagnostics().source !== "file") {
62
+ throw new OrcaImportError("Initialize a valid opencodex configuration before importing.");
63
+ }
64
+ const config = object(JSON.parse(configRaw));
65
+ const accounts = config.codexAccounts ?? [];
66
+ if (!Array.isArray(accounts)) throw new OrcaImportError("Invalid existing account configuration.");
67
+ const identities = new Set<string>();
68
+ const configuredIds = new Set<string>();
69
+ for (const entry of accounts) {
70
+ const row = object(entry);
71
+ if (typeof row.id === "string") configuredIds.add(row.id);
72
+ if (row.chatgptAccountId !== undefined && typeof row.chatgptAccountId !== "string") throw new OrcaImportError("Invalid existing account identity.");
73
+ if (typeof row.chatgptAccountId === "string" && row.chatgptAccountId) identities.add(row.chatgptAccountId);
74
+ }
75
+ const reservedIdentities = new Set(identities);
76
+ const storedIdentityCounts = new Map<string, number>();
77
+ const recoverable = new Map<string, { id: string; credential: Record<string, unknown> }>();
78
+ const storePath = join(getConfigDir(), "codex-accounts.json");
79
+ const storeRaw = optionalFile(storePath);
80
+ const store = storeRaw === undefined ? {} : object(JSON.parse(storeRaw));
81
+ for (const [id, entry] of Object.entries(store)) {
82
+ const record = object(entry);
83
+ // Retain orphan and tombstoned records; a credential is still evidence of identity.
84
+ if (record.credential === undefined && typeof record.generation === "number") continue;
85
+ const credential = object(record.credential ?? record);
86
+ if (typeof credential.chatgptAccountId !== "string" || !credential.chatgptAccountId
87
+ || typeof credential.accessToken !== "string" || typeof credential.refreshToken !== "string"
88
+ || typeof credential.expiresAt !== "number") throw new OrcaImportError("Invalid existing credential store.");
89
+ identities.add(credential.chatgptAccountId);
90
+ storedIdentityCounts.set(credential.chatgptAccountId, (storedIdentityCounts.get(credential.chatgptAccountId) ?? 0) + 1);
91
+ // A process can die after the credential rename but before config registration. Only
92
+ // the untouched initial record emitted by this importer may finish that registration.
93
+ // Older, validated, replaced, tombstoned, or independently owned orphans remain blockers.
94
+ if (id.startsWith("orca-") && ORCA_ACCOUNT_DIRECTORY.test(id.slice(5)) && !configuredIds.has(id)
95
+ && record.generation === 1 && record.codexValidationPending === true
96
+ && record.deletedAt === undefined && record.replacedAt === undefined
97
+ && record.lastCodexValidatedAt === undefined && record.lastCodexValidationStatus === undefined
98
+ && record.lastCodexValidationError === undefined && record.lastCodexValidationTerminal === undefined
99
+ && credential.refreshToken === "" && typeof credential.sourceAuthPath === "string"
100
+ && typeof credential.sourceSubject === "string" && credential.sourceSubject) {
101
+ recoverable.set(credential.chatgptAccountId, { id, credential });
102
+ }
103
+ }
104
+ const mainRaw = optionalFile(join(getCodexHome(), "auth.json"));
105
+ if (mainRaw !== undefined) {
106
+ const main = object(JSON.parse(mainRaw));
107
+ if (main.tokens !== undefined) {
108
+ const identity = parseOrcaAuth(mainRaw, false).chatgptAccountId;
109
+ identities.add(identity);
110
+ reservedIdentities.add(identity);
111
+ }
112
+ else if (typeof main.OPENAI_API_KEY !== "string") throw new OrcaImportError("Main Codex identity is unknown; import refused.");
113
+ }
114
+ const root = assertPlainLocalPath(resolve(sourceDir));
115
+ const directory = assertPlainLocalPath(join(root, "codex-accounts"));
116
+ if (!lstatSync(directory).isDirectory()) throw new OrcaImportError("Invalid Orca data directory.");
117
+ const registryRaw = readBoundedLocalFile(resolve(registryPath), 32 * 1024 * 1024);
118
+ const registry = object(JSON.parse(registryRaw));
119
+ const registered = object(registry.settings).codexManagedAccounts;
120
+ if (!Array.isArray(registered) || registered.length > 1024) {
121
+ throw new OrcaImportError("Invalid Orca account registry or account limit exceeded.");
122
+ }
123
+ const result: OrcaImportResult = {
124
+ mode: "preview", discovered: registered.length, eligible: 0, imported: 0,
125
+ duplicates: 0, invalid: 0, invalidReasons: {},
126
+ };
127
+ const candidates: { credential: ReturnType<typeof readOrcaAuthSource>; recoverId?: string }[] = [];
128
+ const sourceIds = new Set<string>();
129
+ for (const entry of registered) {
130
+ try {
131
+ const row = object(entry);
132
+ if (typeof row.id !== "string" || !ORCA_ACCOUNT_DIRECTORY.test(row.id)
133
+ || typeof row.managedHomePath !== "string"
134
+ || (row.managedHomeRuntime !== undefined && row.managedHomeRuntime !== "host")) {
135
+ throw new OrcaImportEntryError("unsupported_entry");
136
+ }
137
+ const home = join(directory, row.id, "home");
138
+ if (!sameLocalPath(assertPlainLocalPath(row.managedHomePath), assertPlainLocalPath(home))) {
139
+ throw new OrcaImportEntryError("home_mismatch");
140
+ }
141
+ if (sourceIds.has(row.id)) { result.duplicates++; continue; }
142
+ sourceIds.add(row.id);
143
+ const credential = readOrcaAuthSource(join(home, "auth.json"));
144
+ let recoverId: string | undefined;
145
+ if (identities.has(credential.chatgptAccountId)) {
146
+ const orphan = recoverable.get(credential.chatgptAccountId);
147
+ if (orphan && !reservedIdentities.has(credential.chatgptAccountId)
148
+ && storedIdentityCounts.get(credential.chatgptAccountId) === 1
149
+ && sameLocalPath(orphan.credential.sourceAuthPath as string, credential.sourceAuthPath)
150
+ && orphan.credential.sourceSubject === credential.sourceSubject) {
151
+ recoverId = orphan.id;
152
+ recoverable.delete(credential.chatgptAccountId);
153
+ } else { result.duplicates++; continue; }
154
+ }
155
+ identities.add(credential.chatgptAccountId);
156
+ candidates.push({ credential, recoverId });
157
+ result.eligible++;
158
+ } catch (error) { noteInvalid(result, error); }
159
+ }
160
+ return { configRaw, registryRaw, storeRaw, storePath, store, candidates, result };
161
+
162
+ }
163
+
164
+ /** Offline, explicit-source import. The returned report deliberately contains no identities or paths. */
165
+ export function importOrcaAccounts(options: { sourceDir: string; registryPath: string; apply?: boolean }): OrcaImportResult {
166
+ try {
167
+ if (!options.apply) return prepare(options.sourceDir, options.registryPath).result;
168
+ assertStopped();
169
+ return withConfigMutationLockSync(() => {
170
+ assertStopped();
171
+ const state = prepare(options.sourceDir, options.registryPath);
172
+ state.result.mode = "apply";
173
+ if (!state.candidates.length) return state.result;
174
+ const writesCredentials = state.candidates.some(candidate => candidate.recoverId === undefined);
175
+ const additions = state.candidates.map(({ credential, recoverId }) => {
176
+ let id = recoverId;
177
+ if (id === undefined) {
178
+ do { id = `orca-${randomUUID()}`; } while (state.store[id]);
179
+ state.store[id] = { credential, generation: 1, codexValidationPending: true };
180
+ }
181
+ return { id, email: "", alias: "Orca account", isMain: false, chatgptAccountId: credential.chatgptAccountId };
182
+ });
183
+ // Re-read registry and sources immediately before publishing either target file.
184
+ if (readBoundedLocalFile(resolve(options.registryPath), 32 * 1024 * 1024) !== state.registryRaw) {
185
+ throw new OrcaImportError("Orca registry changed during import; retry.");
186
+ }
187
+ for (const { credential } of state.candidates) {
188
+ if (JSON.stringify(readOrcaAuthSource(credential.sourceAuthPath)) !== JSON.stringify(credential)) {
189
+ throw new OrcaImportError("Orca credentials changed during import; retry.");
190
+ }
191
+ }
192
+ assertStopped();
193
+ if (optionalFile(getConfigPath()) !== state.configRaw || optionalFile(state.storePath) !== state.storeRaw) {
194
+ throw new OrcaImportError("Target accounts changed during import; retry.");
195
+ }
196
+ if (writesCredentials) atomicWriteFile(state.storePath, JSON.stringify(state.store, null, 2) + "\n");
197
+ try {
198
+ const outcome = mutatePersistedConfig(config => {
199
+ if (optionalFile(getConfigPath()) !== state.configRaw) throw new OrcaImportError("Target configuration changed; retry.");
200
+ config.codexAccounts = [...(config.codexAccounts ?? []), ...additions];
201
+ return { changed: true, value: undefined };
202
+ });
203
+ if (outcome.status !== "committed") throw new OrcaImportError("Target configuration could not be committed.");
204
+ } catch (error) {
205
+ if (writesCredentials) {
206
+ if (state.storeRaw === undefined) unlinkSync(state.storePath);
207
+ else atomicWriteFile(state.storePath, state.storeRaw);
208
+ }
209
+ throw error;
210
+ }
211
+ advanceCodexCredentialMutationEpoch();
212
+ state.result.imported = additions.length;
213
+ return state.result;
214
+ });
215
+ } catch (error) {
216
+ if (error instanceof OrcaImportError) throw error;
217
+ throw new OrcaImportError("Orca import could not complete; verify source and target local account files.");
218
+ }
219
+ }
@@ -8,8 +8,11 @@
8
8
  *
9
9
  * What this does NOT cover, stated rather than implied:
10
10
  *
11
- * - `base-instructions` is absent. `prompt_debug.rs` returns `prompt.input` and
12
- * discards `base_instructions`, so the base prompt never appears here.
11
+ * - `base-instructions` is absent from the probed output: Codex returns
12
+ * `prompt.input` and discards `base_instructions`, so the base prompt never
13
+ * appears there. It is read here from the configured sources instead - the
14
+ * `model_instructions_file` override when one is set, otherwise the selected
15
+ * model's catalog row - and reported separately as `base`.
13
16
  * - World-state sections are DIFF-rendered (`add_section` registers state, it does
14
17
  * not emit text). A section that renders nothing on a first turn is missing
15
18
  * from this output even though its layer exists.
@@ -17,8 +20,10 @@
17
20
  * universal prompt.
18
21
  */
19
22
  import { spawn } from "node:child_process";
20
- import { existsSync, statSync } from "node:fs";
21
- import { join } from "node:path";
23
+ import { closeSync, constants, existsSync, fstatSync, openSync, readSync, statSync } from "node:fs";
24
+ import { dirname, join, resolve } from "node:path";
25
+ import { expandUserPath } from "../config";
26
+ import { parseCatalogJson, readCodexCatalogPathForHome, type RawCatalog, type RawEntry } from "./catalog/parsing";
22
27
  import { codexExecInvocation } from "./exec-invocation";
23
28
  import { resolveCodexHomeDir } from "./home";
24
29
  import {
@@ -70,7 +75,7 @@ const UNMAPPED_LAYER_IDS = [
70
75
  // The Rust source names a <git_attribution> marker pair, but a world-state section is
71
76
  // DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
72
77
  // `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
73
- // no attribution text. Listing the id here reports "not exposed" honestly instead of
78
+ // no attribution text. Listing the id here reports "unmapped" honestly instead of
74
79
  // claiming a tag this extractor has never actually matched - the same mistake the
75
80
  // header above records for permissions.
76
81
  "git-attribution",
@@ -80,12 +85,60 @@ export interface LayerText {
80
85
  /** Rendered text, when this layer produced a section on the probed turn. */
81
86
  text: string | null;
82
87
  /** Why the text is absent, when it is. */
83
- reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
88
+ reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unmapped" | "unavailable";
84
89
  bytes: number;
85
- /** For `empty-source`: the file that exists but has nothing in it. */
90
+ /**
91
+ * `expanded` is text Codex sends as written. `template` is a catalog
92
+ * `instructions_template`, which Codex expands before sending. Absent for every
93
+ * layer read out of the probed output, whose text is rendered by definition.
94
+ */
95
+ representation?: "expanded" | "template";
96
+ /**
97
+ * For `empty-source`: the file that exists but has nothing in it. For
98
+ * `base-instructions`: the configured file the base prompt was read from, or
99
+ * the one that could not be read.
100
+ */
86
101
  sourcePath?: string;
87
102
  }
88
103
 
104
+ /**
105
+ * Why the base prompt is or is not readable, at the granularity a reader can act
106
+ * on. `LayerText.reason` has six coarse values and cannot express any of this,
107
+ * which is why the detailed answer travels on its own record.
108
+ */
109
+ export type BasePromptReason =
110
+ | "ok"
111
+ | "config-not-found"
112
+ | "config-unreadable"
113
+ | "config-too-large"
114
+ | "model-not-selected"
115
+ | "model-not-found"
116
+ | "catalog-not-found"
117
+ | "catalog-unreadable"
118
+ | "catalog-too-large"
119
+ | "not-published"
120
+ | "override-empty"
121
+ | "override-not-found"
122
+ | "override-too-large"
123
+ | "override-unreadable";
124
+
125
+ export interface BasePromptText {
126
+ /** The base prompt, when a configured source published one. */
127
+ text: string | null;
128
+ reason: BasePromptReason;
129
+ bytes: number;
130
+ /** The model selected in `config.toml`, when the config named one. */
131
+ model: string | null;
132
+ /** The file the answer came from, when a file was reached at all. */
133
+ sourcePath: string | null;
134
+ /**
135
+ * `expanded` is the text Codex sends. `template` is a catalog
136
+ * `instructions_template`: a template even when it currently carries no
137
+ * placeholder, so it is never presented as the text the model receives.
138
+ */
139
+ representation: "expanded" | "template" | "unavailable";
140
+ }
141
+
89
142
  /**
90
143
  * Why a probe failed, as a stable token the caller can branch on. The prose
91
144
  * `detail` string is kept for display, but matching on it was never a contract:
@@ -116,6 +169,12 @@ export interface PromptTextProbe {
116
169
  /** The Codex home the probe reported on. */
117
170
  codexHome: string;
118
171
  layers: Record<string, LayerText>;
172
+ /**
173
+ * The base prompt, read from configuration rather than from the probed output.
174
+ * Present on every outcome: it is computed before the subprocess, so it answers
175
+ * even when Codex cannot be resolved, the probe fails, or the caller cancels.
176
+ */
177
+ base: BasePromptText;
119
178
  /** The runtime the probe resolved and tried, when resolution produced one. */
120
179
  runtime?: { command: string; source: CodexRuntimeSource };
121
180
  /** Stable failure classification; `detail` remains the display string. */
@@ -129,6 +188,208 @@ const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024;
129
188
  /** stderr is captured only to classify the failure, never to echo back. */
130
189
  const MAX_PROBE_STDERR_BYTES = 64 * 1024;
131
190
 
191
+ /** Read chunk for local prompt sources; the ceiling is MAX_PROBE_OUTPUT_BYTES. */
192
+ const PROMPT_SOURCE_CHUNK_BYTES = 64 * 1024;
193
+
194
+ function errorWithCode(message: string, code: string): NodeJS.ErrnoException {
195
+ const error = new Error(message) as NodeJS.ErrnoException;
196
+ error.code = code;
197
+ return error;
198
+ }
199
+
200
+ function errorCodeOf(error: unknown): string | undefined {
201
+ return (error as NodeJS.ErrnoException | null)?.code;
202
+ }
203
+
204
+ /**
205
+ * Read one configured prompt source, bounded, from a single descriptor.
206
+ *
207
+ * Three properties are load-bearing, and all three are about this running
208
+ * synchronously on the Bun request thread:
209
+ *
210
+ * - `O_NONBLOCK` is set BEFORE anything else. `openSync(path, "r")` on a FIFO with
211
+ * no writer blocks forever, and nothing can recover it: the probe timeout and
212
+ * request cancellation both need the event loop this call is holding. Windows
213
+ * has no such flag, so the constant is absent there and contributes nothing.
214
+ * - The regular-file check reads `fstatSync` on the OPENED descriptor. A path stat
215
+ * describes whatever the name pointed at a moment ago, not what got opened.
216
+ * - The ceiling is enforced while reading, not from a preflight size. A
217
+ * stat-then-read pair is a window in which the file can change, and it would let
218
+ * an unbounded body through into a management-API response.
219
+ */
220
+ function readBoundedPromptSource(path: string): string {
221
+ // `O_NONBLOCK` is POSIX-only: Windows omits it from `fs.constants` even though the
222
+ // type declares it, so it is read defensively and contributes nothing there.
223
+ const nonBlocking = (constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0;
224
+ const descriptor = openSync(path, constants.O_RDONLY | nonBlocking);
225
+ try {
226
+ if (!fstatSync(descriptor).isFile()) {
227
+ throw errorWithCode(`prompt source is not a regular file: ${path}`, "EFTYPE");
228
+ }
229
+ const parts: Buffer[] = [];
230
+ const view = Buffer.allocUnsafe(PROMPT_SOURCE_CHUNK_BYTES);
231
+ let total = 0;
232
+ for (;;) {
233
+ const read = readSync(descriptor, view, 0, view.length, total);
234
+ if (read === 0) break;
235
+ total += read;
236
+ // One byte past the ceiling is the whole answer: refuse before the content
237
+ // is retained, so an oversized source costs no more than a bounded read.
238
+ if (total > MAX_PROBE_OUTPUT_BYTES) {
239
+ throw errorWithCode(`prompt source exceeds ${MAX_PROBE_OUTPUT_BYTES} bytes: ${path}`, "EFBIG");
240
+ }
241
+ parts.push(Buffer.from(view.subarray(0, read)));
242
+ }
243
+ return Buffer.concat(parts).toString("utf8");
244
+ } finally {
245
+ try { closeSync(descriptor); } catch { /* the descriptor is already unusable */ }
246
+ }
247
+ }
248
+
249
+ function unavailableBase(
250
+ reason: BasePromptReason,
251
+ model: string | null = null,
252
+ sourcePath: string | null = null,
253
+ ): BasePromptText {
254
+ return { text: null, reason, bytes: 0, model, sourcePath, representation: "unavailable" };
255
+ }
256
+
257
+ function readableBase(
258
+ text: string,
259
+ representation: "expanded" | "template",
260
+ model: string,
261
+ sourcePath: string,
262
+ ): BasePromptText {
263
+ return { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8"), model, sourcePath, representation };
264
+ }
265
+
266
+ function publishedString(entry: RawEntry, key: string): string | null {
267
+ const value = entry[key];
268
+ return typeof value === "string" && value.length > 0 ? value : null;
269
+ }
270
+
271
+ /**
272
+ * The base prompt for the configured model, read from the same files Codex reads.
273
+ *
274
+ * Precedence follows Codex: `model_instructions_file` replaces the base prompt
275
+ * outright, so an override that is set decides the answer whether or not it can be
276
+ * read. Without one, the selected catalog row's `base_instructions` is the
277
+ * published text, and `model_messages.instructions_template` is the fallback -
278
+ * reported as a template, because that is what it is.
279
+ *
280
+ * Every failure is a reason, never an exception: this feeds a GET that must
281
+ * degrade to "we could not read it" rather than to a 500.
282
+ */
283
+ function readBasePrompt(codexHome: string): BasePromptText {
284
+ const configPath = join(codexHome, "config.toml");
285
+ let configText: string;
286
+ try {
287
+ configText = readBoundedPromptSource(configPath);
288
+ } catch (error) {
289
+ // ENOENT is "no config"; anything else is "the config we have could not be
290
+ // read". `existsSync` cannot separate those: it races the read, and it reports
291
+ // a permission failure as absence.
292
+ const code = errorCodeOf(error);
293
+ return unavailableBase(
294
+ code === "ENOENT" ? "config-not-found" : code === "EFBIG" ? "config-too-large" : "config-unreadable",
295
+ );
296
+ }
297
+ let root: Record<string, unknown>;
298
+ try {
299
+ const parsed = Bun.TOML.parse(configText);
300
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("config.toml is not a table");
301
+ root = parsed as Record<string, unknown>;
302
+ } catch {
303
+ // The WHOLE document has to parse before any key from it is trusted. Codex
304
+ // rejects a malformed config outright, so a model scraped out of the readable
305
+ // first lines would make this display a prompt that is never sent.
306
+ return unavailableBase("config-unreadable", null, configPath);
307
+ }
308
+ const configuredModel = typeof root.model === "string" ? root.model.trim() : "";
309
+ // A `model_instructions_file` with no model is out of scope here: the base
310
+ // prompt this reports on is the selected model's, and there is no selection.
311
+ if (!configuredModel) return unavailableBase("model-not-selected", null, configPath);
312
+ const model = configuredModel;
313
+
314
+ const override = typeof root.model_instructions_file === "string" ? root.model_instructions_file : null;
315
+ if (override !== null) {
316
+ // A blank value names no file. The key is still set, so Codex does not fall
317
+ // back to the catalog default and neither does this: reporting the catalog row
318
+ // would describe a base prompt this configuration never produces.
319
+ if (override.trim().length === 0) return unavailableBase("override-not-found", model);
320
+ // Relative to the config file's own directory, which is how the rest of this
321
+ // repository resolves the key.
322
+ const overridePath = resolve(dirname(configPath), expandUserPath(override));
323
+ let text: string;
324
+ try {
325
+ text = readBoundedPromptSource(overridePath);
326
+ } catch (error) {
327
+ const code = errorCodeOf(error);
328
+ return unavailableBase(
329
+ code === "ENOENT" ? "override-not-found" : code === "EFBIG" ? "override-too-large" : "override-unreadable",
330
+ model,
331
+ overridePath,
332
+ );
333
+ }
334
+ // Codex rejects this config with "model instructions file is empty", so
335
+ // reporting `ok` here would claim text that is never sent - and claim it for a
336
+ // configuration that does not start.
337
+ if (text.trim().length === 0) return unavailableBase("override-empty", model, overridePath);
338
+ return readableBase(text, "expanded", model, overridePath);
339
+ }
340
+
341
+ const catalogPath = readCodexCatalogPathForHome(codexHome, configText);
342
+ let catalog: RawCatalog | null;
343
+ try {
344
+ catalog = parseCatalogJson(readBoundedPromptSource(catalogPath));
345
+ } catch (error) {
346
+ const code = errorCodeOf(error);
347
+ return unavailableBase(
348
+ code === "ENOENT" ? "catalog-not-found" : code === "EFBIG" ? "catalog-too-large" : "catalog-unreadable",
349
+ model,
350
+ catalogPath,
351
+ );
352
+ }
353
+ if (!catalog) return unavailableBase("catalog-unreadable", model, catalogPath);
354
+ // Each candidate is guarded rather than trusted: `parseCatalogJson` validates
355
+ // only that `models` is an array, so a row of `null` would throw on property
356
+ // access and turn this read into a 500 on the management API.
357
+ const entry = catalog.models?.find(candidate => (
358
+ candidate !== null
359
+ && typeof candidate === "object"
360
+ && !Array.isArray(candidate)
361
+ && (candidate.slug === model || candidate.id === model)
362
+ )) ?? null;
363
+ if (!entry) return unavailableBase("model-not-found", model, catalogPath);
364
+ const published = publishedString(entry, "base_instructions");
365
+ if (published) return readableBase(published, "expanded", model, catalogPath);
366
+ const messages = entry.model_messages;
367
+ const template = messages !== null && typeof messages === "object" && !Array.isArray(messages)
368
+ ? publishedString(messages as RawEntry, "instructions_template")
369
+ : null;
370
+ if (template) return readableBase(template, "template", model, catalogPath);
371
+ return unavailableBase("not-published", model, catalogPath);
372
+ }
373
+
374
+ /**
375
+ * Project the base prompt onto the legacy `base-instructions` layer slot.
376
+ *
377
+ * The slot is lossy by construction - six coarse reasons, no renderer that reads
378
+ * `representation` - so it carries only what it can carry honestly: published text
379
+ * when the source is text Codex sends, and otherwise no text at all. A template is
380
+ * deliberately NOT `ok` here, because the dialog labels every `ok` layer "Text
381
+ * sent to the model" and an unexpanded template is not that. The detailed answer
382
+ * stays on `base`.
383
+ */
384
+ function promptLayerForBase(base: BasePromptText): LayerText {
385
+ const source = base.sourcePath ? { sourcePath: base.sourcePath } : {};
386
+ if (base.reason === "ok" && base.text !== null && base.representation === "expanded") {
387
+ return { text: base.text, reason: "ok", bytes: base.bytes, representation: "expanded", ...source };
388
+ }
389
+ if (base.reason === "ok") return { text: null, reason: "not-exposed", bytes: 0, representation: "template", ...source };
390
+ return { text: null, reason: "unavailable", bytes: 0, ...source };
391
+ }
392
+
132
393
  interface ProbeCommand {
133
394
  binary: string;
134
395
  args: string[];
@@ -557,8 +818,13 @@ export async function probePromptText(
557
818
  // and it also described a prompt that depends on where Codex happened to run.
558
819
  // The global home is the one context this page can honestly report on.
559
820
  const codexHome = resolveCodexHomeDir();
821
+ // Read before anything is spawned. The base prompt comes from files this process
822
+ // reads itself, so binding it to the subprocess would lose the one layer the
823
+ // probed output never carries the moment Codex is missing, slow, or cancelled.
824
+ const base = readBasePrompt(codexHome);
825
+ const baseLayers = { "base-instructions": promptLayerForBase(base) };
560
826
  if (signal?.aborted) {
561
- return { ok: false, codexHome, layers: {}, detail: "prompt probe cancelled" };
827
+ return { ok: false, codexHome, layers: { ...baseLayers }, base, detail: "prompt probe cancelled" };
562
828
  }
563
829
  // Resolve through the shared runtime resolver, not a private path list: the
564
830
  // old four-path POSIX check could never match the Codex App's Windows install
@@ -600,7 +866,8 @@ export async function probePromptText(
600
866
  return {
601
867
  ok: false,
602
868
  codexHome,
603
- layers: {},
869
+ layers: { ...baseLayers },
870
+ base,
604
871
  ...(reportedRuntime ? { runtime: reportedRuntime } : {}),
605
872
  failure,
606
873
  detail: "codex binary not found",
@@ -618,7 +885,8 @@ export async function probePromptText(
618
885
  return {
619
886
  ok: false,
620
887
  codexHome,
621
- layers: {},
888
+ layers: { ...baseLayers },
889
+ base,
622
890
  ...(reportedRuntime ? { runtime: reportedRuntime } : {}),
623
891
  ...(outcome.kind === "failed" && outcome.failure ? { failure: outcome.failure } : {}),
624
892
  detail: signal?.aborted
@@ -636,7 +904,8 @@ export async function probePromptText(
636
904
  return {
637
905
  ok: false,
638
906
  codexHome,
639
- layers: {},
907
+ layers: { ...baseLayers },
908
+ base,
640
909
  ...(reportedRuntime ? { runtime: reportedRuntime } : {}),
641
910
  failure: probeFailure(
642
911
  command,
@@ -669,16 +938,20 @@ export async function probePromptText(
669
938
  // An unreadable file stays "not-rendered": we cannot claim it is empty.
670
939
  }
671
940
  }
672
- // The base prompt travels outside prompt.input and cannot be read this way.
673
- layers["base-instructions"] = { text: null, reason: "not-exposed", bytes: 0 };
941
+ // Read from configuration above, not from this output: Codex discards
942
+ // `base_instructions` before rendering `prompt.input`.
943
+ Object.assign(layers, baseLayers);
674
944
 
675
945
  // Layers whose rendered tag we have not confirmed against live output. Leaving
676
946
  // them absent made the GUI fall through to "unavailable", which claims the probe
677
- // failed when it succeeded. Saying we have no mapping is the smaller claim.
947
+ // failed when it succeeded. Keep this distinct from the base prompt's
948
+ // "not-exposed", which is confirmed to travel outside the printable message
949
+ // list - reusing it showed a base-prompt-specific explanation for unrelated
950
+ // layers.
678
951
  for (const id of UNMAPPED_LAYER_IDS) {
679
- layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
952
+ layers[id] ??= { text: null, reason: "unmapped", bytes: 0 };
680
953
  }
681
- return { ok: true, codexHome, layers, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
954
+ return { ok: true, codexHome, layers, base, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
682
955
  }
683
956
 
684
957
  /** Test-only command seam; production always resolves the installed Codex binary. */
@@ -105,6 +105,18 @@ export function settleQuotaRecovery(
105
105
  records.set(accountId, { state: "spent", lineage: fenced });
106
106
  }
107
107
 
108
+ /**
109
+ * Fence a same-grant alias that adopted the credential produced by another account's refresh.
110
+ *
111
+ * Propagation advances the alias without making it a separate credential lineage. Leaving its
112
+ * committed generation unspent would let a later quota poll rotate the same refresh grant again.
113
+ * This deliberately supersedes an old live claim for the alias: that claim targets the
114
+ * pre-propagation generation, and its eventual settlement must not erase this newer fence.
115
+ */
116
+ export function fencePropagatedQuotaRecovery(accountId: string, lineage: number): void {
117
+ records.set(accountId, { state: "spent", lineage });
118
+ }
119
+
108
120
  /**
109
121
  * Record a refresh that failed with proof the grant itself is dead.
110
122
  *