@bitkyc08/opencodex 2.48.0-preview.20260908 → 2.49.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 (97) hide show
  1. package/README.md +8 -5
  2. package/SPONSORS.md +1 -1
  3. package/assets/sponsors/orcarouter.png +0 -0
  4. package/assets/sponsors/packycode.png +0 -0
  5. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  6. package/gui/dist/assets/index-BtyONQrZ.js +115 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/packycode.svg +19 -0
  9. package/gui/dist/provider-icons/qoder.svg +5 -0
  10. package/package.json +5 -3
  11. package/src/adapters/anthropic.ts +31 -16
  12. package/src/adapters/codebuddy/adapter.ts +85 -0
  13. package/src/adapters/codebuddy/profiles.ts +52 -0
  14. package/src/adapters/coding-agent/profile.ts +100 -0
  15. package/src/adapters/coding-agent/protocol.ts +463 -0
  16. package/src/adapters/coding-agent/turn.ts +353 -0
  17. package/src/adapters/google.ts +15 -11
  18. package/src/adapters/mimo-free.ts +3 -0
  19. package/src/adapters/openai-chat.ts +2 -2
  20. package/src/adapters/openai-responses.ts +18 -11
  21. package/src/adapters/qoder/adapter.ts +70 -0
  22. package/src/adapters/qoder/live-models.ts +89 -0
  23. package/src/adapters/qoder/profiles.ts +36 -0
  24. package/src/adapters/registry.ts +12 -0
  25. package/src/adapters/responses-tool-schema.ts +113 -8
  26. package/src/cli/dispatch.ts +2 -0
  27. package/src/cli/doctor.ts +40 -0
  28. package/src/cli/effort.ts +24 -8
  29. package/src/cli/help.ts +2 -0
  30. package/src/cli/index.ts +24 -1
  31. package/src/cli/provider-runtime.ts +2 -1
  32. package/src/cli/registry.ts +2 -2
  33. package/src/cli/system-command.ts +10 -3
  34. package/src/cli/usage-report.ts +9 -5
  35. package/src/codex/account-lifecycle.ts +35 -2
  36. package/src/codex/auth-api.ts +6 -3
  37. package/src/codex/auth-collision.ts +12 -2
  38. package/src/codex/auth-context.ts +75 -7
  39. package/src/codex/catalog/provider-fetch.ts +73 -9
  40. package/src/codex/inject.ts +127 -30
  41. package/src/codex/internal/catalog-writer.ts +3 -0
  42. package/src/codex/journal.ts +61 -12
  43. package/src/codex/model-cache.ts +11 -4
  44. package/src/codex/native-profile-startup.ts +72 -5
  45. package/src/codex/native-profile-store.ts +2 -2
  46. package/src/codex/ocx-compaction-history.ts +226 -0
  47. package/src/codex/project-config-warnings.ts +3 -1
  48. package/src/codex/quota.ts +18 -8
  49. package/src/codex/reserve-availability.ts +21 -5
  50. package/src/codex/runtime.ts +45 -1
  51. package/src/codex/sync.ts +5 -0
  52. package/src/config.ts +1 -0
  53. package/src/generated/compatibility-version.json +126 -74
  54. package/src/images/loop.ts +1 -0
  55. package/src/images/xai-video-client.ts +2 -0
  56. package/src/integrations/registry.ts +1 -0
  57. package/src/lib/process-control.ts +52 -8
  58. package/src/lib/upstream-retry.ts +1 -0
  59. package/src/oauth/chatgpt.ts +83 -0
  60. package/src/oauth/xai.ts +151 -8
  61. package/src/providers/api-key-selection-capture.ts +10 -0
  62. package/src/providers/api-key-selection.ts +2 -7
  63. package/src/providers/caller-authorization.ts +36 -0
  64. package/src/providers/codebuddy-models.ts +184 -0
  65. package/src/providers/derive.ts +5 -0
  66. package/src/providers/free-directory.ts +26 -2
  67. package/src/providers/openai-sidecar.ts +35 -11
  68. package/src/providers/qoder-models.ts +25 -0
  69. package/src/providers/registry.ts +139 -0
  70. package/src/responses/compaction.ts +4 -0
  71. package/src/responses/task-input.ts +21 -1
  72. package/src/router.ts +1 -1
  73. package/src/server/chat-completions.ts +26 -9
  74. package/src/server/claude-messages.ts +12 -2
  75. package/src/server/images.ts +1 -0
  76. package/src/server/management/account-selection-stream.ts +13 -4
  77. package/src/server/management/config-routes.ts +24 -5
  78. package/src/server/management/logs-usage-routes.ts +1 -0
  79. package/src/server/management/native-integration-routes.ts +2 -1
  80. package/src/server/management/provider-routes.ts +24 -0
  81. package/src/server/management-api.ts +14 -0
  82. package/src/server/port-reclaim.ts +11 -26
  83. package/src/server/request-log.ts +6 -0
  84. package/src/server/responses/collaboration.ts +4 -9
  85. package/src/server/responses/core.ts +141 -19
  86. package/src/server/responses/fetch-helpers.ts +18 -7
  87. package/src/server/responses/policy-fallback.ts +12 -0
  88. package/src/service.ts +36 -2
  89. package/src/storage/cleanup.ts +77 -45
  90. package/src/types/config.ts +7 -1
  91. package/src/usage/log.ts +24 -0
  92. package/src/vision/anthropic-describe.ts +1 -0
  93. package/src/web-search/anthropic-executor.ts +1 -0
  94. package/src/web-search/loop.ts +1 -0
  95. package/src/web-search/progress-stream.ts +4 -0
  96. package/gui/dist/assets/index-B6EFoatP.js +0 -115
  97. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
@@ -0,0 +1,36 @@
1
+ import { clearCodingAgentBinaryCache, resolveProfileByBaseUrl, type CodingAgentProviderProfile } from "../coding-agent/profile";
2
+
3
+ /** Official Qoder CLI profile. Region variants are separate profiles and credentials. */
4
+ export interface QoderProfile extends CodingAgentProviderProfile {
5
+ family: "qoder";
6
+ }
7
+
8
+ export const QODER_GLOBAL_PROFILE: QoderProfile = {
9
+ providerId: "qoder",
10
+ family: "qoder",
11
+ region: "global",
12
+ label: "Qoder",
13
+ canonicalBaseUrl: "https://qoder.com",
14
+ binaryCandidates: ["qoder", "qodercli"],
15
+ tokenEnv: "QODER_PERSONAL_ACCESS_TOKEN",
16
+ installHint: "npm install -g @qoder-ai/qodercli",
17
+ documentationUrl: "https://docs.qoder.com/cli/authentication",
18
+ };
19
+
20
+ export const QODER_CN_PROFILE: QoderProfile = {
21
+ providerId: "qoder-cn",
22
+ family: "qoder",
23
+ region: "cn",
24
+ label: "Qoder CN",
25
+ canonicalBaseUrl: "https://qoder.cn",
26
+ binaryCandidates: ["qodercn", "qoderclicn"],
27
+ tokenEnv: "QODERCN_PERSONAL_ACCESS_TOKEN",
28
+ installHint: "npm install -g @qodercn-ai/qoderclicn",
29
+ documentationUrl: "https://docs.qoder.cn/en/cli/authentication",
30
+ };
31
+
32
+ export const QODER_PROFILES: readonly QoderProfile[] = [QODER_GLOBAL_PROFILE, QODER_CN_PROFILE];
33
+ export function resolveQoderProfile(baseUrl: string | undefined): QoderProfile | undefined {
34
+ return resolveProfileByBaseUrl(QODER_PROFILES, baseUrl) as QoderProfile | undefined;
35
+ }
36
+ export const clearQoderBinaryCache = clearCodingAgentBinaryCache;
@@ -2,6 +2,8 @@ import { createAnthropicAdapter } from "./anthropic";
2
2
  import { createAzureAdapter } from "./azure";
3
3
  import type { ProviderAdapter } from "./base";
4
4
  import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
5
+ import { createCodeBuddyAdapter } from "./codebuddy/adapter";
6
+ import { createQoderAdapter } from "./qoder/adapter";
5
7
  import { createCommandCodeAdapter } from "./command-code";
6
8
  import { createCursorAdapter } from "./cursor";
7
9
  import { createGoogleAdapter } from "./google";
@@ -20,6 +22,7 @@ export interface AdapterFactoryContext {
20
22
  }
21
23
 
22
24
  export type AdapterWire =
25
+ | "codebuddy"
23
26
  | "command-code"
24
27
  | "openai-chat"
25
28
  | "ollama-native"
@@ -53,6 +56,11 @@ type InheritedAdapterDefinition = {
53
56
  type AdapterDefinition = DirectAdapterDefinition | InheritedAdapterDefinition;
54
57
 
55
58
  export const ADAPTER_REGISTRY = {
59
+ codebuddy: {
60
+ wire: "codebuddy",
61
+ mutation: "codex-owned",
62
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCodeBuddyAdapter(provider),
63
+ },
56
64
  "command-code": {
57
65
  wire: "command-code",
58
66
  mutation: "codex-owned",
@@ -108,6 +116,10 @@ export const ADAPTER_REGISTRY = {
108
116
  contractParent: "openai-chat",
109
117
  create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
110
118
  },
119
+ qoder: {
120
+ contractParent: "codebuddy",
121
+ create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createQoderAdapter(provider),
122
+ },
111
123
  } as const satisfies Record<string, AdapterDefinition>;
112
124
 
113
125
  export type AdapterId = keyof typeof ADAPTER_REGISTRY;
@@ -1,8 +1,7 @@
1
- // Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on
2
- // collaboration tool schemas (openai/codex 5f4d06ef; issue #85). It is an
3
- // annotation for the ChatGPT backend only, so translated provider schemas must
4
- // drop it without removing properties or definitions literally named `encrypted`.
5
- const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
1
+ // Keys whose *children's names* are caller-chosen rather than schema keywords, and keys whose
2
+ // values are literal payloads rather than schemas. Shared by both strippers below: each one has
3
+ // to tell "the keyword `x`" apart from "a property someone named `x`".
4
+ const SCHEMA_NAME_BAG_KEYS = new Set([
6
5
  "properties",
7
6
  "patternProperties",
8
7
  "$defs",
@@ -11,9 +10,21 @@ const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
11
10
  "dependentSchemas",
12
11
  "dependentRequired",
13
12
  ]);
14
- const ENCRYPTED_MARKER_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
13
+ const SCHEMA_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
14
+
15
+ // These subtrees depend on property evaluation, polarity, branch selection or references.
16
+ // Preserve their complete argument contract rather than guessing whether a local relaxation
17
+ // remains a relaxation in the containing schema. The destination reports unsupported regexes.
18
+ const PRESERVED_PATTERN_SUBTREES = new Set([
19
+ "patternProperties", "not", "oneOf", "if", "contains", "$defs", "definitions",
20
+ ]);
15
21
 
16
22
  /**
23
+ * Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on collaboration tool
24
+ * schemas (openai/codex 5f4d06ef; issue #85). It is an annotation for the ChatGPT backend only,
25
+ * so translated provider schemas must drop it without removing properties or definitions
26
+ * literally named `encrypted`.
27
+ *
17
28
  * The schema is caller-supplied, so its nesting depth is attacker-influenced. Native recursion
18
29
  * would turn a deep schema into a stack overflow that takes down the request path, so this walks
19
30
  * an explicit stack instead: depth costs heap, which is bounded and recoverable.
@@ -52,11 +63,11 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
52
63
  // Inside a name bag every key is a caller-chosen name, so `encrypted` here is data.
53
64
  stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
54
65
  } else if (key !== "encrypted") {
55
- if (ENCRYPTED_MARKER_LITERAL_VALUE_KEYS.has(key)) {
66
+ if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
56
67
  // Literal payloads are values, not schemas: an `encrypted` key inside them is data.
57
68
  out[key] = value;
58
69
  } else {
59
- const childInNameBag = ENCRYPTED_MARKER_NAME_BAG_KEYS.has(key);
70
+ const childInNameBag = SCHEMA_NAME_BAG_KEYS.has(key);
60
71
  stack.push({ node: value, inNameBag: childInNameBag, assign: v => { out[key] = v; } });
61
72
  }
62
73
  }
@@ -65,3 +76,97 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
65
76
 
66
77
  return result;
67
78
  }
79
+
80
+ /**
81
+ * `\p{…}` is an escape only when the backslash introducing it is itself unescaped: in `\\p{2}`
82
+ * the pair is a literal backslash and the `p{2}` that follows is an ordinary quantified `p`,
83
+ * which Python compiles fine. Scanning for the raw substring would misread that as a property
84
+ * escape and discard a working pattern.
85
+ */
86
+ function usesUnicodePropertyEscape(pattern: string): boolean {
87
+ for (let i = 0; i < pattern.length; i++) {
88
+ if (pattern[i] !== "\\") continue;
89
+ const next = pattern[i + 1];
90
+ if (next === "\\") {
91
+ i++;
92
+ continue;
93
+ }
94
+ if ((next === "p" || next === "P") && pattern[i + 2] === "{") return true;
95
+ }
96
+ return false;
97
+ }
98
+
99
+ /**
100
+ * Remove unsupported Unicode property escapes from scalar `pattern` constraints in ordinary
101
+ * positive schema positions. This keeps built-in Artifact tools usable on Python-re backends;
102
+ * the omitted constraint is not enforced by this proxy and tools must validate their inputs.
103
+ *
104
+ * Regex-keyed objects are preserved. Removing a matcher can lose evaluated-property annotations
105
+ * needed by an ancestor's unevaluatedProperties, even when the local object appears open.
106
+ * Negation, exclusive alternatives, conditions, contains and reusable definitions are also
107
+ * preserved: loosening a nested constraint can instead reject an input in those contexts.
108
+ * Unsupported patterns there remain the destination's validation responsibility.
109
+ *
110
+ * Returns `node` itself when nothing was dropped. Uses an explicit stack for caller-controlled
111
+ * nesting depth; the separate Responses-only encrypted-marker normalization is unchanged.
112
+ */
113
+ export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown {
114
+ type Assign = (value: unknown) => void;
115
+ interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
116
+
117
+ let result: unknown;
118
+ let dropped = 0;
119
+ const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
120
+
121
+ while (stack.length > 0) {
122
+ const frame = stack.pop()!;
123
+ const current = frame.node;
124
+
125
+ if (Array.isArray(current)) {
126
+ const out: unknown[] = new Array(current.length);
127
+ frame.assign(out);
128
+ // Array items are schemas in their own right, never a name bag.
129
+ for (let i = current.length - 1; i >= 0; i--) {
130
+ stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
131
+ }
132
+ continue;
133
+ }
134
+ if (!current || typeof current !== "object") {
135
+ frame.assign(current);
136
+ continue;
137
+ }
138
+
139
+ // A schema name may be `__proto__`; a null-prototype record keeps it as data.
140
+ const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
141
+ frame.assign(out);
142
+
143
+ for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
144
+ if (frame.inNameBag) {
145
+ // Inside a name bag every key is a caller-chosen name, so `pattern` here is a property
146
+ // name; its value is still a schema and is walked as one.
147
+ stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
148
+ continue;
149
+ }
150
+ if (PRESERVED_PATTERN_SUBTREES.has(key)) {
151
+ out[key] = value;
152
+ continue;
153
+ }
154
+ if (key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
155
+ dropped++;
156
+ continue;
157
+ }
158
+ if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
159
+ // Literal payloads are values, not schemas: a `pattern` key inside them is data.
160
+ out[key] = value;
161
+ continue;
162
+ }
163
+ stack.push({
164
+ node: value,
165
+ inNameBag: SCHEMA_NAME_BAG_KEYS.has(key),
166
+ assign: v => { out[key] = v; },
167
+ });
168
+ }
169
+ }
170
+
171
+ return dropped === 0 ? node : result;
172
+ }
@@ -25,6 +25,7 @@ import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes
25
25
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
26
26
  import { isJsonOption, takeFlag } from "./runtime-api";
27
27
  import type { ClientConnectionState } from "../client/state";
28
+ import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
28
29
 
29
30
  export interface CliDispatchDeps {
30
31
  args: string[];
@@ -199,6 +200,7 @@ const commandRunners: Record<string, CommandRunner> = {
199
200
  }
200
201
  if (r.success) {
201
202
  console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
203
+ console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
202
204
  } else {
203
205
  console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
204
206
  }
package/src/cli/doctor.ts CHANGED
@@ -45,6 +45,9 @@ import {
45
45
  probeCodexCoordinatorNamespace,
46
46
  resolveEffectiveUserIdentity,
47
47
  } from "../codex/user-identity";
48
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
49
+ import type { OcxProviderConfig } from "../types/provider";
50
+ import { routedProviderConfig } from "../router";
48
51
  import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
49
52
  import {
50
53
  collectLegacyCodexConfigKeyDiagnostics,
@@ -1000,6 +1003,41 @@ export function proxyDownRestartHint(input: {
1000
1003
  return `The ocx proxy is not running. ${uncleanExit}Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
1001
1004
  }
1002
1005
 
1006
+ /** Explain the expected channel and latency trade-off for native ChatGPT routing. */
1007
+ export function chatgptPublicEndpointHint(
1008
+ providers: Record<string, unknown> | undefined,
1009
+ ): string | null {
1010
+ const openai = providers?.openai;
1011
+ if (!openai || typeof openai !== "object") {
1012
+ return null;
1013
+ }
1014
+ // A disabled row never routes, so it must not be described as the route in use.
1015
+ const configured = openai as OcxProviderConfig;
1016
+ if (configured.disabled === true) {
1017
+ return null;
1018
+ }
1019
+ // Classify the destination the router resolves, not the raw config text. Two things follow
1020
+ // from the registry entry for the built-in `openai` id: a row that omits `authMode` still
1021
+ // forwards, and a row carrying some other `baseUrl` has it discarded in favour of the
1022
+ // canonical ChatGPT endpoint. Both keep using the public endpoint, so both want this hint;
1023
+ // reading the raw row would have suppressed the first and misjudged the second.
1024
+ //
1025
+ // `routedProviderConfig` throws for an unresolved URL only when the registry entry allows a
1026
+ // baseUrl override, which this entry does not, so no input reaches that path today. The guard
1027
+ // is here because doctor is read-only diagnostics: a later registry change must not turn a
1028
+ // diagnostic into a crash.
1029
+ let routed: OcxProviderConfig;
1030
+ try {
1031
+ routed = routedProviderConfig("openai", configured);
1032
+ } catch {
1033
+ return null;
1034
+ }
1035
+ if (!isCanonicalOpenAiForwardProvider(routed)) {
1036
+ return null;
1037
+ }
1038
+ return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier.";
1039
+ }
1040
+
1003
1041
  export async function runDoctor(args: string[] = []): Promise<void> {
1004
1042
  if (args.includes("--fix-codex-runtime")) {
1005
1043
  const resolved = resolveCodexRuntime();
@@ -1330,6 +1368,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
1330
1368
 
1331
1369
  // Hints, not fixes.
1332
1370
  const hints: string[] = [];
1371
+ const chatgptHint = chatgptPublicEndpointHint(doctorConfig.providers);
1372
+ if (chatgptHint) hints.push(chatgptHint);
1333
1373
  const proxyDown = proxyDownRestartHint({
1334
1374
  proxyRunning: Boolean(live),
1335
1375
  port: live?.port ?? doctorConfig.port ?? 10100,
package/src/cli/effort.ts CHANGED
@@ -2,6 +2,7 @@ import { loadConfig, saveConfig } from "../config";
2
2
  import {
3
3
  CODEX_REASONING_LEVELS,
4
4
  configuredReasoningEfforts,
5
+ isCodexReasoningEffort,
5
6
  isDeclaredReasoningEffort,
6
7
  mapReasoningEffort,
7
8
  reasoningEffortMapFor,
@@ -21,7 +22,7 @@ import {
21
22
 
22
23
  export const EFFORT_USAGE = `Usage:
23
24
  ocx effort [status] [--json]
24
- ocx effort <low|medium|high|xhigh|max|ultra|none|minimal|-> [--json]
25
+ ocx effort <low|medium|high|xhigh|max|ultra|-> [--json]
25
26
  ocx effort set [--main <level|->] [--subagent <level|->] [--injection <level|->] [--json]
26
27
  ocx effort clear [--json]
27
28
  ocx effort model <provider/model|model> [--json]
@@ -33,13 +34,18 @@ function clearable(value: string | undefined): string | null | undefined {
33
34
  return value === "-" ? null : value;
34
35
  }
35
36
 
36
- function validateEffortLevel(level: string | null | undefined, label: string): string | null | undefined {
37
+ function validateEffortLevel(
38
+ level: string | null | undefined,
39
+ label: string,
40
+ kind: "cap" | "injection",
41
+ ): string | null | undefined {
37
42
  if (level === undefined || level === null) return level;
38
43
  const trimmed = level.trim();
39
44
  if (trimmed === "-" || trimmed === "") return null;
40
- if (!isDeclaredReasoningEffort(trimmed)) {
45
+ const valid = kind === "cap" ? isCodexReasoningEffort(trimmed) : isDeclaredReasoningEffort(trimmed);
46
+ if (!valid) {
41
47
  throw new CliUsageError(
42
- `unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}, none, minimal, -)`,
48
+ `unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}${kind === "injection" ? ", none, minimal" : ""}, -)`,
43
49
  EFFORT_USAGE,
44
50
  );
45
51
  }
@@ -114,6 +120,15 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
114
120
  data = getOfflineStatus();
115
121
  }
116
122
 
123
+ // Report the stored/runtime value exactly as the enforcement layer evaluates it.
124
+ // An ignored subagent field does not disable a valid main cap on that child.
125
+ const warnings = ([ ["effortCap", "--main"], ["subagentEffortCap", "--subagent"] ] as const)
126
+ .flatMap(([key, flag]) => {
127
+ const value = data[key];
128
+ if (value === null || isCodexReasoningEffort(value)) return [];
129
+ return [`${key}=${JSON.stringify(value)} is invalid and is not applied. Use: ocx effort set ${flag} <${CODEX_REASONING_LEVELS.map(l => l.effort).join("|")}|->.`];
130
+ });
131
+
117
132
  const lines = [
118
133
  `Reasoning effort status (${data.source === "runtime" ? "live proxy" : "offline config"}):`,
119
134
  ` Main agent effort cap: ${data.effortCap ?? "(unset — no cap)"}`,
@@ -122,9 +137,10 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
122
137
  "",
123
138
  "Supported Codex reasoning effort ladder:",
124
139
  ...CODEX_REASONING_LEVELS.map(l => ` - ${l.effort.padEnd(8)} ${l.description}`),
140
+ ...(warnings.length ? ["", "Warnings:", ...warnings.map(warning => ` ${warning}`)] : []),
125
141
  ];
126
142
 
127
- printData(data, wantsJson, lines);
143
+ printData({ ...data, warnings }, wantsJson, lines);
128
144
  }
129
145
 
130
146
  async function setEffort(
@@ -136,9 +152,9 @@ async function setEffort(
136
152
  wantsJson: boolean,
137
153
  deps: RuntimeApiDeps,
138
154
  ): Promise<void> {
139
- const validatedMain = validateEffortLevel(options.main, "--main");
140
- const validatedSubagent = validateEffortLevel(options.subagent, "--subagent");
141
- const validatedInjection = validateEffortLevel(options.injection, "--injection");
155
+ const validatedMain = validateEffortLevel(options.main, "--main", "cap");
156
+ const validatedSubagent = validateEffortLevel(options.subagent, "--subagent", "cap");
157
+ const validatedInjection = validateEffortLevel(options.injection, "--injection", "injection");
142
158
 
143
159
  if (validatedMain === undefined && validatedSubagent === undefined && validatedInjection === undefined) {
144
160
  throw new CliUsageError("at least one effort option (--main, --subagent, or --injection) is required", EFFORT_USAGE);
package/src/cli/help.ts CHANGED
@@ -33,6 +33,8 @@ Usage:
33
33
  ocx restore back Re-point codex at the running proxy (undo restore)
34
34
  ocx recover-history --legacy-openai --yes
35
35
  Force all user-message opencodex rows to OpenAI (legacy recovery)
36
+ ocx recover-history --ocx-compaction <thread-id> --yes
37
+ Back up and make one ocx1-compacted thread replayable by native Codex
36
38
  ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
37
39
  ocx service [sub] Run as a background service (default: install/update/start)
38
40
  ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
package/src/cli/index.ts CHANGED
@@ -1442,8 +1442,31 @@ async function handleStatus() {
1442
1442
  }
1443
1443
 
1444
1444
  async function handleRecoverHistory() {
1445
+ if (args[1] === "--ocx-compaction") {
1446
+ const threadId = args[2];
1447
+ if (args.length !== 4 || !threadId || args[3] !== "--yes") {
1448
+ console.error("Usage: ocx recover-history --ocx-compaction <thread-id> --yes");
1449
+ console.error("This rewrites one rollout after saving a private byte-for-byte backup. Close that Codex thread before retrying.");
1450
+ process.exit(1);
1451
+ }
1452
+ console.error("WARNING: this converts OpenCodeX-owned ocx1 compaction state into a plain summary for native Codex replay.");
1453
+ try {
1454
+ const { recoverOcxCompactionHistory } = await import("../codex/ocx-compaction-history");
1455
+ const result = recoverOcxCompactionHistory({ threadId });
1456
+ if (result.replaced === 0) {
1457
+ console.log(`Thread ${threadId} has no repairable ocx1 compaction history; no files changed.`);
1458
+ return;
1459
+ }
1460
+ console.log(`Recovered ${result.replaced} ocx1 compaction item(s) in thread ${threadId}.`);
1461
+ console.log(`Backup: ${result.backupPath}`);
1462
+ return;
1463
+ } catch (error) {
1464
+ console.error(`Recovery failed: ${error instanceof Error ? error.message : String(error)}`);
1465
+ process.exit(1);
1466
+ }
1467
+ }
1445
1468
  if (args[1] !== "--legacy-openai") {
1446
- console.error("Usage: ocx recover-history --legacy-openai --yes");
1469
+ console.error("Usage: ocx recover-history (--legacy-openai | --ocx-compaction <thread-id>) --yes");
1447
1470
  console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
1448
1471
  process.exit(1);
1449
1472
  }
@@ -204,7 +204,8 @@ async function presets(argv: string[], deps: RuntimeApiDeps): Promise<void> {
204
204
  const rows = Array.isArray(result) ? result : result.providers ?? [];
205
205
  printData(result, wantsJson, rows.map(row => {
206
206
  const record = row as Record<string, unknown>;
207
- return `${String(record.id ?? record.name ?? "?")} ${String(record.label ?? record.adapter ?? "")}`.trimEnd();
207
+ const sponsor = record.sponsor ? ` (sponsor: ${String(record.sponsor)})` : "";
208
+ return `${String(record.id ?? record.name ?? "?")} ${String(record.label ?? record.adapter ?? "")}${sponsor}`.trimEnd();
208
209
  }));
209
210
  }
210
211
 
@@ -36,8 +36,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
36
36
  },
37
37
  {
38
38
  name: "recover-history",
39
- usage: "ocx recover-history --legacy-openai --yes",
40
- summary: "Force all user-message opencodex rows to OpenAI for legacy recovery.",
39
+ usage: "ocx recover-history (--legacy-openai | --ocx-compaction <thread-id>) --yes",
40
+ summary: "Recover legacy provider metadata or one OpenCodeX-compacted thread for native replay.",
41
41
  },
42
42
  {
43
43
  name: "uninstall",
@@ -14,7 +14,7 @@ import {
14
14
  const USAGE = `Usage:
15
15
  ocx system [status] [--json]
16
16
  ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
17
- [--desktop-authless <on|off>] [--json]
17
+ [--desktop-authless <on|off>] [--client-compaction <on|off>] [--json]
18
18
  ocx system startup <health|install-service|install-shim> [--json]
19
19
  ocx system diagnostics [--json]
20
20
  ocx system sync [--json]
@@ -23,7 +23,11 @@ const USAGE = `Usage:
23
23
  ocx system codex-cli-update check [--json]
24
24
  ocx system update check [--channel <latest|preview>] [--json]
25
25
  ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
26
- ocx system update status <job-id> [--json]`;
26
+ ocx system update status <job-id> [--json]
27
+
28
+ --client-compaction favors native replay portability for future compactions while
29
+ keeping OpenCodeX routing active; the configured provider may process summaries
30
+ and consume its quota.`;
27
31
 
28
32
  async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
29
33
  const args = [...argv];
@@ -44,8 +48,10 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
44
48
  const autoStart = takeBooleanOption(args, "--auto-start");
45
49
  const streamMode = takeOption(args, "--stream-mode");
46
50
  const desktopAuthless = takeBooleanOption(args, "--desktop-authless");
51
+ const clientCompaction = takeBooleanOption(args, "--client-compaction");
47
52
  rejectArgs(args, USAGE);
48
- if (autoStart === undefined && streamMode === undefined && desktopAuthless === undefined) {
53
+ if (autoStart === undefined && streamMode === undefined
54
+ && desktopAuthless === undefined && clientCompaction === undefined) {
49
55
  const result = await runtimeRequest("/api/settings", {}, deps);
50
56
  printData(result, wantsJson, summaryLines(result));
51
57
  return;
@@ -54,6 +60,7 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
54
60
  ...(autoStart !== undefined ? { codexAutoStart: autoStart } : {}),
55
61
  ...(streamMode !== undefined ? { streamMode } : {}),
56
62
  ...(desktopAuthless !== undefined ? { codexDesktopAuthless: desktopAuthless } : {}),
63
+ ...(clientCompaction !== undefined ? { codexClientCompaction: clientCompaction } : {}),
57
64
  };
58
65
  const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps);
59
66
  printData(result, wantsJson, ["System settings updated."]);
@@ -59,8 +59,11 @@ interface UsageReportInput {
59
59
 
60
60
  const MAX_MODEL_ROWS = 10;
61
61
 
62
- function terminalText(value: string): string {
63
- return value.replace(/[\x00-\x1f\x7f-\x9f]/g, character => {
62
+ function terminalText(value: unknown): string {
63
+ const text = typeof value === "string" ? value
64
+ : value === null || value === undefined ? ""
65
+ : typeof value === "number" || typeof value === "boolean" ? String(value) : "[invalid]";
66
+ return text.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, character => {
64
67
  const code = character.charCodeAt(0);
65
68
  return code <= 0x7f
66
69
  ? `\\x${code.toString(16).padStart(2, "0")}`
@@ -69,7 +72,8 @@ function terminalText(value: string): string {
69
72
  }
70
73
 
71
74
  function count(value: number | undefined): string {
72
- return (value ?? 0).toLocaleString("en-US");
75
+ if (value === undefined || value === null) return "0";
76
+ return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "—";
73
77
  }
74
78
 
75
79
  /**
@@ -111,7 +115,7 @@ export function formatUsageReport(data: UsageReportInput): string[] {
111
115
  .filter(Boolean).join(" and ");
112
116
  lines.push(`No usage recorded for ${terminalText(what)} in this range.`);
113
117
  lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
114
- return lines;
118
+ return lines.map(terminalText);
115
119
  }
116
120
 
117
121
  const tokenSplit = [
@@ -185,5 +189,5 @@ export function formatUsageReport(data: UsageReportInput): string[] {
185
189
 
186
190
  lines.push("");
187
191
  lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
188
- return lines;
192
+ return lines.map(terminalText);
189
193
  }
@@ -7,12 +7,13 @@ import {
7
7
  } from "../config";
8
8
  import { removeCodexAccountCredential } from "./account-store";
9
9
  import { clearAccountNeedsReauth } from "./account-runtime-state";
10
- import { getMainChatgptAccountId } from "./auth-collision";
10
+ import { getMainChatgptAccountId, readCodexTokensResult } from "./auth-collision";
11
11
  import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
12
12
  import { clearAccountQuota } from "./quota";
13
13
  import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
14
14
  import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
15
- import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaIdentity } from "./main-account-cache";
15
+ import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaCredential, observeMainQuotaIdentity } from "./main-account-cache";
16
+ import { extractAccountIdClaims } from "../oauth/chatgpt";
16
17
  import { forgetCodexAccountPause } from "./account-pause";
17
18
  import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
18
19
  import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
@@ -75,6 +76,38 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
75
76
  return true;
76
77
  }
77
78
 
79
+ /**
80
+ * Rebuild the memory-only policy binding from a startup-owned, recovered auth path.
81
+ * The caller holds the native owner and exclusive claim; an incoming bearer is never evidence.
82
+ * A failed read creates no binding and cannot revoke a prior verified observation or its block.
83
+ * Only a valid replacement observation or confirmed account transition supersedes that evidence.
84
+ */
85
+ export function initializeMainAccountPolicyBinding(authPath: string): boolean {
86
+ // Startup observes the pinned owned path inside the exclusive claim: bound the read so a
87
+ // replaced non-regular or oversized file cannot stall startup inside that claim.
88
+ const result = readCodexTokensResult(authPath, { bounded: true });
89
+ if (result.status !== "ok") return false;
90
+ const { tokens } = result;
91
+ if (typeof tokens.access_token !== "string" || !tokens.access_token
92
+ || typeof tokens.account_id !== "string" || !tokens.account_id) return false;
93
+ if (tokens.id_token != null && typeof tokens.id_token !== "string") return false;
94
+ const accountId = tokens.account_id;
95
+ // An owned file may contain an opaque bearer, but every decoded identity must agree —
96
+ // including the two account-id encodings within a single token.
97
+ const idTokenClaims = extractAccountIdClaims(tokens.id_token);
98
+ const accessTokenClaims = extractAccountIdClaims(tokens.access_token);
99
+ if (idTokenClaims.conflict || accessTokenClaims.conflict) return false;
100
+ const idTokenAccountId = idTokenClaims.accountId;
101
+ const accessTokenAccountId = accessTokenClaims.accountId;
102
+ if ((idTokenAccountId !== undefined && idTokenAccountId !== accountId)
103
+ || (accessTokenAccountId !== undefined && accessTokenAccountId !== accountId)) return false;
104
+ const previousAccountId = observedMainChatgptAccountId;
105
+ observedMainChatgptAccountId = accountId;
106
+ if (previousAccountId !== undefined && previousAccountId !== accountId) purgeMainCodexAccountRuntimeState();
107
+ observeMainQuotaIdentity(accountId);
108
+ return observeMainQuotaCredential(tokens.access_token, accountId) !== undefined;
109
+ }
110
+
78
111
  /**
79
112
  * Apply a transaction-confirmed physical native-login change without waiting for
80
113
  * a later auth.json observation. The caller owns credential commit/rollback.
@@ -241,12 +241,15 @@ function codexAccountPersistenceConflict(
241
241
  }
242
242
 
243
243
  /**
244
- * The exact label `parseUsageQuota` emits for the Codex Spark window (quota.ts).
244
+ * The exact labels `parseUsageQuota` emits for the Codex Spark windows (quota.ts).
245
245
  * Matching on the label rather than on "is a custom window" is load-bearing: the same array
246
246
  * carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet,
247
247
  * Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters.
248
248
  */
249
- const CODEX_SPARK_WINDOW_LABEL = "GPT-5.3-Codex-Spark Weekly";
249
+ const CODEX_SPARK_WINDOW_LABELS = new Set([
250
+ "GPT-5.3-Codex-Spark 5h",
251
+ "GPT-5.3-Codex-Spark Weekly",
252
+ ]);
250
253
 
251
254
  /**
252
255
  * Drop the Spark window unless the operator asked for it (default hidden).
@@ -264,7 +267,7 @@ export function withSparkVisibility<T extends Omit<StoredAccountQuota, "updatedA
264
267
  ): T {
265
268
  if (!quota?.customWindows?.length) return quota;
266
269
  if (loadConfig().showCodexSparkQuota === true) return quota;
267
- const kept = quota.customWindows.filter(window => window.label !== CODEX_SPARK_WINDOW_LABEL);
270
+ const kept = quota.customWindows.filter(window => !CODEX_SPARK_WINDOW_LABELS.has(window.label));
268
271
  if (kept.length === quota.customWindows.length) return quota;
269
272
  // An empty list is dropped rather than serialized: an absent field and an empty array should
270
273
  // not be two different ways of saying "no custom windows" on the wire.
@@ -6,6 +6,7 @@ import { resolveCodexHomeDir } from "./home";
6
6
  import { extractAccountId } from "../oauth/chatgpt";
7
7
  import { isSelectableCodexPoolAccount } from "./account-id";
8
8
  import { codexPlanKey } from "./plan";
9
+ import { MAX_AUTH_BYTES, readBounded } from "./native-profile-store";
9
10
 
10
11
  export interface CodexTokens {
11
12
  access_token: string;
@@ -31,12 +32,21 @@ function hasErrnoCode(error: unknown, code: string): boolean {
31
32
  /**
32
33
  * Reads the Codex CLI credential file and classifies the outcome. Reads once instead of doing an
33
34
  * `existsSync` pre-check, so a file replaced between check and read cannot be misread as absent.
35
+ * An already-owned lifecycle may supply its pinned auth path instead of resolving ambient home.
36
+ * `bounded` opts into the native-profile bounded reader (regular file, size-capped, no-follow,
37
+ * non-blocking) for startup observation paths that run inside the owner claim; bounded violations
38
+ * classify as `unreadable`. Legacy callers keep the unbounded read.
34
39
  * Never returns or logs the raw error or any token material.
35
40
  */
36
- export function readCodexTokensResult(): CodexTokenReadResult {
41
+ export function readCodexTokensResult(
42
+ authPath = join(resolveCodexHomeDir(), "auth.json"),
43
+ options?: { bounded?: boolean },
44
+ ): CodexTokenReadResult {
37
45
  let raw: string;
38
46
  try {
39
- raw = readFileSync(join(resolveCodexHomeDir(), "auth.json"), "utf-8");
47
+ raw = options?.bounded === true
48
+ ? readBounded(authPath, MAX_AUTH_BYTES).toString("utf-8")
49
+ : readFileSync(authPath, "utf-8");
40
50
  } catch (error) {
41
51
  return { status: hasErrnoCode(error, "ENOENT") ? "missing" : "unreadable" };
42
52
  }