@bitkyc08/opencodex 2.8.0 → 2.9.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 (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -1,5 +1,6 @@
1
1
  import { collectResponsesToolGroups } from "../responses/tool-groups";
2
2
  import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
3
+ import type { TranslatorBudget } from "../lib/translator-budget";
3
4
 
4
5
  interface NamespacedTool {
5
6
  namespace: string;
@@ -22,12 +23,24 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
22
23
  export function imageGenToolCallAliases(
23
24
  toolNsMap: ReadonlyMap<string, NamespacedTool>,
24
25
  requestBody?: unknown,
26
+ translatorBudget?: TranslatorBudget,
25
27
  ): Map<string, NamespacedTool> {
26
28
  const aliases = new Map<string, NamespacedTool>();
29
+ const retainAlias = (key: string, tool: NamespacedTool) => {
30
+ if (aliases.has(key)) {
31
+ aliases.set(key, tool);
32
+ return;
33
+ }
34
+ translatorBudget?.chargeRetained(
35
+ Buffer.byteLength(key) + Buffer.byteLength(tool.namespace) + Buffer.byteLength(tool.name),
36
+ { kind: "request_copies" },
37
+ );
38
+ aliases.set(key, tool);
39
+ };
27
40
  for (const [wireName, tool] of toolNsMap) {
28
41
  if (tool.namespace !== IMAGE_GEN_NAMESPACE) continue;
29
- aliases.set(wireName, tool);
30
- aliases.set(`${tool.namespace}.${tool.name}`, tool);
42
+ retainAlias(wireName, tool);
43
+ retainAlias(`${tool.namespace}.${tool.name}`, tool);
31
44
  }
32
45
  for (const group of collectResponsesToolGroups(requestBody)) {
33
46
  for (const tool of group) {
@@ -40,8 +53,8 @@ export function imageGenToolCallAliases(
40
53
  ) continue;
41
54
  const localName = tool.name.slice(IMAGE_GEN_DOTTED_PREFIX.length);
42
55
  const target = { namespace: IMAGE_GEN_NAMESPACE, name: localName };
43
- aliases.set(`${IMAGE_GEN_NAMESPACE}__${localName}`, target);
44
- aliases.set(tool.name, target);
56
+ retainAlias(`${IMAGE_GEN_NAMESPACE}__${localName}`, target);
57
+ retainAlias(tool.name, target);
45
58
  }
46
59
  }
47
60
  return aliases;
@@ -112,7 +125,8 @@ export function createImageGenCallRestoreRewrite(
112
125
  export function relaySseWithImageGenCallRestore(
113
126
  body: ReadableStream<Uint8Array>,
114
127
  aliases: ReadonlyMap<string, NamespacedTool>,
128
+ translatorBudget: TranslatorBudget,
115
129
  ): ReadableStream<Uint8Array> {
116
130
  const rewrite = createImageGenCallRestoreRewrite(aliases);
117
- return rewrite ? relaySseWithPayloadRewrite(body, rewrite) : body;
131
+ return rewrite ? relaySseWithPayloadRewrite(body, rewrite, translatorBudget) : body;
118
132
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { ResponsesItemIdRepairConfig } from "../types";
3
3
  import { relaySseWithPayloadRewrite, type SsePayloadRewrite } from "./sse-payload-rewrite";
4
+ import type { TranslatorBudget } from "../lib/translator-budget";
4
5
 
5
6
  type RepairableItemType = "message" | "reasoning";
6
7
 
@@ -9,6 +10,7 @@ interface ResponsesItemIdRepairState {
9
10
  readonly placeholders: Record<RepairableItemType, ReadonlySet<string>>;
10
11
  readonly outputIds: Record<RepairableItemType, Map<number, string>>;
11
12
  readonly scope: string;
13
+ readonly budget?: TranslatorBudget;
12
14
  }
13
15
 
14
16
  const REPAIRABLE_PREFIXES: Record<RepairableItemType, string> = {
@@ -48,8 +50,8 @@ function mintCanonicalId(type: RepairableItemType, scope: string, outputIndex: n
48
50
  return `${REPAIRABLE_PREFIXES[type]}ocx_${scope}_${outputIndex}`;
49
51
  }
50
52
 
51
- function createRepairState(config: ResponsesItemIdRepairConfig): ResponsesItemIdRepairState {
52
- return {
53
+ function createRepairState(config: ResponsesItemIdRepairConfig, budget?: TranslatorBudget): ResponsesItemIdRepairState {
54
+ const state = {
53
55
  repairMissingTerminalIds: config.repairMissingTerminalIds === true,
54
56
  placeholders: {
55
57
  message: new Set(config.message ?? []),
@@ -60,7 +62,14 @@ function createRepairState(config: ResponsesItemIdRepairConfig): ResponsesItemId
60
62
  reasoning: new Map<number, string>(),
61
63
  },
62
64
  scope: randomUUID().replace(/-/g, ""),
65
+ budget,
63
66
  };
67
+ budget?.chargeRetained(new TextEncoder().encode(JSON.stringify({
68
+ message: [...state.placeholders.message],
69
+ reasoning: [...state.placeholders.reasoning],
70
+ scope: state.scope,
71
+ })).byteLength, { kind: "item_ids" });
72
+ return state;
64
73
  }
65
74
 
66
75
  function rememberMappedId(
@@ -80,6 +89,7 @@ function rememberMappedId(
80
89
  ? rawId
81
90
  : null;
82
91
  if (!mapped) return null;
92
+ state.budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([outputIndex, rawId, mapped])).byteLength, { kind: "item_ids" });
83
93
  state.outputIds[type].set(outputIndex, mapped);
84
94
  return mapped;
85
95
  }
@@ -163,7 +173,13 @@ function repairEventPayload(
163
173
  changed = true;
164
174
  }
165
175
  }
166
- return changed ? JSON.stringify(nextEvent) : payload;
176
+ if (!changed) return payload;
177
+ const rewritten = JSON.stringify(nextEvent);
178
+ const bytes = new TextEncoder().encode(rewritten).byteLength;
179
+ const reservation = state.budget?.reserveTransient(bytes, { kind: "item_ids" });
180
+ reservation?.commitRetained();
181
+ if (state.budget) queueMicrotask(() => state.budget?.releaseRetained(bytes, { kind: "item_ids" }));
182
+ return rewritten;
167
183
  }
168
184
 
169
185
  /**
@@ -187,16 +203,18 @@ function repairEventPayload(
187
203
  /** Stateful payload rewrite for composition with other client-facing SSE transforms. */
188
204
  export function createResponsesItemIdPayloadRewrite(
189
205
  config: ResponsesItemIdRepairConfig,
206
+ budget?: TranslatorBudget,
190
207
  ): SsePayloadRewrite {
191
- const state = createRepairState(config);
208
+ const state = createRepairState(config, budget);
192
209
  return (payload) => repairEventPayload(payload, state);
193
210
  }
194
211
 
195
212
  export function relaySseWithResponsesItemIdRepair(
196
213
  body: ReadableStream<Uint8Array>,
197
214
  config: ResponsesItemIdRepairConfig,
215
+ budget: TranslatorBudget,
198
216
  ): ReadableStream<Uint8Array> {
199
- return relaySseWithPayloadRewrite(body, createResponsesItemIdPayloadRewrite(config));
217
+ return relaySseWithPayloadRewrite(body, createResponsesItemIdPayloadRewrite(config, budget), budget);
200
218
  }
201
219
 
202
220
  export function hasResponsesItemIdRepair(config: ResponsesItemIdRepairConfig | undefined): boolean {
@@ -1,3 +1,5 @@
1
+ import type { TranslatorBudget } from "../lib/translator-budget";
2
+
1
3
  /**
2
4
  * Shared client-facing SSE payload rewrite shell.
3
5
  *
@@ -66,11 +68,60 @@ export function composeSsePayloadRewrites(...rewrites: SsePayloadRewrite[]): Sse
66
68
  export function relaySseWithPayloadRewrite(
67
69
  body: ReadableStream<Uint8Array>,
68
70
  rewrite: SsePayloadRewrite,
71
+ translatorBudget: TranslatorBudget,
69
72
  ): ReadableStream<Uint8Array> {
70
73
  const reader = body.getReader();
71
74
  const decoder = new TextDecoder();
72
75
  const encoder = new TextEncoder();
73
76
  let buffer = "";
77
+ let bufferBytes = 0;
78
+
79
+ const appendBuffer = (fragment: string): void => {
80
+ if (!fragment) return;
81
+ const nextBytes = bufferBytes + encoder.encode(fragment).byteLength;
82
+ const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
83
+ try {
84
+ buffer += fragment;
85
+ reservation.commitRetained();
86
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
87
+ bufferBytes = nextBytes;
88
+ } catch (error) {
89
+ reservation.release();
90
+ throw error;
91
+ }
92
+ };
93
+
94
+ const replaceBuffer = (next: string): void => {
95
+ const nextBytes = encoder.encode(next).byteLength;
96
+ const reservation = translatorBudget.reserveTransient(nextBytes, { kind: "live_transient" });
97
+ reservation.commitRetained();
98
+ buffer = next;
99
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
100
+ bufferBytes = nextBytes;
101
+ };
102
+
103
+ const enqueueText = (
104
+ controller: ReadableStreamDefaultController<Uint8Array>,
105
+ text: string,
106
+ ): void => {
107
+ const bytes = encoder.encode(text).byteLength;
108
+ const reservation = translatorBudget.reserveTransient(bytes, { kind: "live_transient" });
109
+ try {
110
+ const encoded = encoder.encode(text);
111
+ reservation.commitRetained();
112
+ controller.enqueue(encoded);
113
+ translatorBudget.releaseRetained(bytes, { kind: "live_transient" });
114
+ } catch (error) {
115
+ reservation.release();
116
+ throw error;
117
+ }
118
+ };
119
+
120
+ const releaseBuffer = (): void => {
121
+ translatorBudget.releaseRetained(bufferBytes, { kind: "live_transient" });
122
+ buffer = "";
123
+ bufferBytes = 0;
124
+ };
74
125
 
75
126
  const emitProcessedBlocks = (
76
127
  controller: ReadableStreamDefaultController<Uint8Array>,
@@ -78,13 +129,13 @@ export function relaySseWithPayloadRewrite(
78
129
  ): void => {
79
130
  let next: { block: string; delimiter: string; rest: string } | null;
80
131
  while ((next = nextSseBlock(buffer))) {
81
- buffer = next.rest;
132
+ replaceBuffer(next.rest);
82
133
  const payload = sseDataPayload(next.block);
83
134
  const rewrittenPayload = payload ? rewrite(payload) : undefined;
84
135
  const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
85
136
  ? replaceSseDataPayload(next.block, rewrittenPayload)
86
137
  : next.block;
87
- controller.enqueue(encoder.encode(block + next.delimiter));
138
+ enqueueText(controller, block + next.delimiter);
88
139
  }
89
140
  if (flushFinal && buffer.length > 0) {
90
141
  const payload = sseDataPayload(buffer);
@@ -92,24 +143,32 @@ export function relaySseWithPayloadRewrite(
92
143
  const block = payload && rewrittenPayload !== undefined && rewrittenPayload !== payload
93
144
  ? replaceSseDataPayload(buffer, rewrittenPayload)
94
145
  : buffer;
95
- controller.enqueue(encoder.encode(block));
96
- buffer = "";
146
+ enqueueText(controller, block);
147
+ releaseBuffer();
97
148
  }
98
149
  };
99
150
 
100
151
  return new ReadableStream<Uint8Array>({
101
152
  async pull(controller) {
102
- const { done, value } = await reader.read();
103
- if (done) {
104
- buffer += decoder.decode();
105
- emitProcessedBlocks(controller, true);
106
- controller.close();
107
- return;
153
+ try {
154
+ const { done, value } = await reader.read();
155
+ if (done) {
156
+ appendBuffer(decoder.decode());
157
+ emitProcessedBlocks(controller, true);
158
+ releaseBuffer();
159
+ controller.close();
160
+ return;
161
+ }
162
+ appendBuffer(decoder.decode(value, { stream: true }));
163
+ emitProcessedBlocks(controller);
164
+ } catch (error) {
165
+ releaseBuffer();
166
+ try { await reader.cancel(error); } catch { /* already closed */ }
167
+ controller.error(error);
108
168
  }
109
- buffer += decoder.decode(value, { stream: true });
110
- emitProcessedBlocks(controller);
111
169
  },
112
170
  cancel(reason) {
171
+ releaseBuffer();
113
172
  reader.cancel(reason).catch(() => {});
114
173
  },
115
174
  });
@@ -6,9 +6,11 @@ import { getCodexRoutingKind } from "../codex/inject";
6
6
  import { diagnoseCodexShim } from "../codex/shim";
7
7
  import { durableBunPath } from "../lib/bun-runtime";
8
8
  import type { OcxConfig } from "../types";
9
+ import { truncateRetainedUtf8 } from "../lib/admission";
9
10
 
10
11
  const CACHE_TTL_MS = 30_000;
11
12
  const PROBE_TIMEOUT_MS = 5_000;
13
+ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
12
14
  let cached: { timestamp: number; value: StartupHealth } | null = null;
13
15
  let inflight: Promise<StartupHealth> | null = null;
14
16
  let generation = 0;
@@ -63,7 +65,18 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
63
65
  try {
64
66
  const parsed = JSON.parse(lines[index]) as StartupHealth;
65
67
  if (["native", "protected", "at-risk"].includes(parsed.status) && typeof parsed.rebootSafe === "boolean") {
66
- resolve({ ...parsed, diagnosticStale: false });
68
+ resolve({
69
+ ...parsed,
70
+ recommendedCommand: parsed.recommendedCommand === null
71
+ ? null
72
+ : truncateRetainedUtf8(parsed.recommendedCommand, MAX_DIAGNOSTIC_VALUE_BYTES),
73
+ commands: {
74
+ installService: truncateRetainedUtf8(parsed.commands.installService, MAX_DIAGNOSTIC_VALUE_BYTES),
75
+ installShim: truncateRetainedUtf8(parsed.commands.installShim, MAX_DIAGNOSTIC_VALUE_BYTES),
76
+ restoreNative: truncateRetainedUtf8(parsed.commands.restoreNative, MAX_DIAGNOSTIC_VALUE_BYTES),
77
+ },
78
+ diagnosticStale: false,
79
+ });
67
80
  return;
68
81
  }
69
82
  } catch { /* scan earlier output; config repair messages may precede JSON */ }
@@ -236,7 +236,14 @@ async function computeEffectiveModelEnv(config: OcxConfig, auto?: AutoContextMod
236
236
  const { boundedContextWindows, buildClaudeContextWindows, effectiveModelEnv } = await import("../claude/context-windows");
237
237
  const windows = await boundedContextWindows(async () => {
238
238
  const { gatherRoutedModels, visibleNativeSlugs } = await import("../codex/catalog");
239
- return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config));
239
+ try {
240
+ return buildClaudeContextWindows([...visibleNativeSlugs(config)], await gatherRoutedModels(config));
241
+ } catch (error) {
242
+ if (error && typeof error === "object" && (error as { code?: unknown }).code === "catalog_busy") {
243
+ return buildClaudeContextWindows([...visibleNativeSlugs(config)], []);
244
+ }
245
+ throw error;
246
+ }
240
247
  });
241
248
  return { modelEnv: effectiveModelEnv(config.claudeCode, windows ?? {}, auto), windows: windows ?? {} };
242
249
  }
@@ -79,6 +79,12 @@ export type WindowsTcpDropResult = {
79
79
  dropped: number;
80
80
  /** IPv6 (or unparseable) rows skipped — never coerced into IPv4 wildcards. */
81
81
  skippedIpv6: number;
82
+ /**
83
+ * SetTcpEntry returned ERROR_ACCESS_DENIED (5) or the UAC-non-elevated code (317).
84
+ * On a normal (non-admin) update worker this is expected — ghost LISTEN rows must
85
+ * clear by waiting for the OS, not by SetTcpEntry.
86
+ */
87
+ accessDenied: number;
82
88
  };
83
89
 
84
90
  function htons(port: number): number {
@@ -133,21 +139,22 @@ function readNetstatAno(): string {
133
139
  */
134
140
  export function dropWindowsTcpRowsForLocalPort(port: number): WindowsTcpDropResult {
135
141
  if (process.platform !== "win32" || !Number.isFinite(port) || port <= 0) {
136
- return { dropped: 0, skippedIpv6: 0 };
142
+ return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
137
143
  }
138
144
  const setTcpEntry = loadSetTcpEntry();
139
- if (!setTcpEntry) return { dropped: 0, skippedIpv6: 0 };
145
+ if (!setTcpEntry) return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
140
146
 
141
147
  let output = "";
142
148
  try {
143
149
  output = readNetstatAno();
144
150
  } catch {
145
- return { dropped: 0, skippedIpv6: 0 };
151
+ return { dropped: 0, skippedIpv6: 0, accessDenied: 0 };
146
152
  }
147
153
 
148
154
  const rows = parseTcpQuadsForLocalPort(output, Math.trunc(port));
149
155
  let dropped = 0;
150
156
  let skippedIpv6 = 0;
157
+ let accessDenied = 0;
151
158
  for (const row of rows) {
152
159
  const localDw = ipv4ToWinUint32(row.localAddr);
153
160
  const remoteDw = ipv4ToWinUint32(row.remoteAddr);
@@ -165,10 +172,13 @@ export function dropWindowsTcpRowsForLocalPort(port: number): WindowsTcpDropResu
165
172
  view.setUint32(12, remoteDw, true);
166
173
  view.setUint32(16, htons(row.remotePort), true);
167
174
  try {
168
- if (setTcpEntry(ptr(buf)) === 0) dropped += 1;
175
+ const rc = setTcpEntry(ptr(buf));
176
+ if (rc === 0) dropped += 1;
177
+ // 5 = ERROR_ACCESS_DENIED, 317 = non-elevated SetTcpEntry (MSDN).
178
+ else if (rc === 5 || rc === 317) accessDenied += 1;
169
179
  } catch {
170
180
  /* keep going */
171
181
  }
172
182
  }
173
- return { dropped, skippedIpv6 };
183
+ return { dropped, skippedIpv6, accessDenied };
174
184
  }
@@ -3,6 +3,8 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
3
3
  import type { CodexAuthContext } from "../codex/auth-context";
4
4
  import { headersForCodexAuthContext } from "../codex/auth-context";
5
5
  import type { ResponsesTerminalStatus } from "../bridge";
6
+ import type { DataPlaneAdmission } from "./auth-cors";
7
+ import type { AdmissionReservation } from "../lib/admission";
6
8
 
7
9
  const OPEN = 1;
8
10
  type ResponsesTerminalReporter = (status: ResponsesTerminalStatus) => void;
@@ -19,6 +21,14 @@ const SAFE_RESPONSE_HEADER_EXACT = new Set([
19
21
 
20
22
  export interface WsData {
21
23
  headers?: Headers; // base inbound forward headers only; per-turn auth refresh injects current pool tokens
24
+ /**
25
+ * Resolved once at the handshake. Auth is handshake-time only on this path, so
26
+ * the per-frame log contexts have no request headers left to re-resolve from.
27
+ * Optional like every other member here: a socket object can exist before the
28
+ * handshake fills it, and an unattributed frame is preferable to a fabricated
29
+ * attribution.
30
+ */
31
+ admission?: DataPlaneAdmission;
22
32
  authContext?: CodexAuthContext; // last resolved account decision for observability/registry cleanup
23
33
  cancel?: () => void; // cancels the in-flight stream reader/fetch
24
34
  turnId?: number; // monotonically increasing per socket; prevents stale frames after replacement turns
@@ -29,6 +39,21 @@ export interface WsData {
29
39
  liveUpstreamHeaders?: Record<string, string>;
30
40
  livePending?: Array<string | Buffer>;
31
41
  liveOpened?: boolean;
42
+ admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>;
43
+ }
44
+
45
+ /**
46
+ * Build the Responses WebSocket upgrade payload.
47
+ *
48
+ * Extracted so the handshake's contract is testable: `server.upgrade` hands its
49
+ * `data` straight to the socket, and a client has no way to read `ws.data` back.
50
+ * A test that only asserts "the socket opened" would still pass if the admission
51
+ * were dropped from the payload, so the payload itself is what gets asserted.
52
+ */
53
+ export function buildResponsesWsData(headers: Headers, admission: DataPlaneAdmission, admissionLease?: AdmissionReservation<ServerWebSocket<WsData>>): WsData {
54
+ // Auth is handshake-time only on this path: the per-frame contexts have no
55
+ // request headers left to re-resolve from, so the decision rides along here.
56
+ return { headers, admission, ...(admissionLease ? { admissionLease } : {}) };
32
57
  }
33
58
 
34
59
  export class WsSendDroppedError extends Error {