@bitkyc08/opencodex 2.8.2-preview.20260731 → 2.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) 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-CuVjugeE.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 +86 -5
  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 +89 -14
  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/doctor.ts +12 -0
  42. package/src/cli/export-command.ts +187 -0
  43. package/src/cli/help.ts +11 -0
  44. package/src/cli/index.ts +13 -3
  45. package/src/cli/opencode.ts +36 -151
  46. package/src/cli/star-prompt.ts +13 -4
  47. package/src/cli/status-oauth.ts +12 -2
  48. package/src/clients/config-export.ts +377 -0
  49. package/src/codex/account-runtime-state.ts +19 -1
  50. package/src/codex/account-store.ts +162 -82
  51. package/src/codex/auth-api.ts +467 -159
  52. package/src/codex/auth-context.ts +15 -2
  53. package/src/codex/catalog/aggregation.ts +15 -0
  54. package/src/codex/catalog/provider-fetch.ts +29 -0
  55. package/src/codex/catalog/sync.ts +7 -1
  56. package/src/codex/inject.ts +5 -5
  57. package/src/codex/main-account-cache.ts +8 -1
  58. package/src/codex/model-cache.ts +81 -2
  59. package/src/codex/pool-rotation.ts +39 -0
  60. package/src/codex/project-config-warnings.ts +12 -1
  61. package/src/codex/quota.ts +35 -3
  62. package/src/codex/routing.ts +46 -1
  63. package/src/codex/shim.ts +10 -4
  64. package/src/codex/subagent-model-fallback.ts +12 -0
  65. package/src/codex/websocket-registry.ts +27 -0
  66. package/src/combos/failover.ts +31 -1
  67. package/src/combos/request.ts +9 -0
  68. package/src/combos/resolve.ts +60 -4
  69. package/src/combos/types.ts +12 -0
  70. package/src/config.ts +510 -55
  71. package/src/github/star-state.ts +13 -1
  72. package/src/images/fulfill.ts +39 -1
  73. package/src/images/loop.ts +52 -12
  74. package/src/lib/admission.ts +83 -0
  75. package/src/lib/app-owned-memory-stores.ts +173 -0
  76. package/src/lib/app-owned-memory.ts +265 -0
  77. package/src/lib/bun-stream-caps.ts +31 -7
  78. package/src/lib/config-ownership.ts +33 -0
  79. package/src/lib/crash-guard.ts +65 -5
  80. package/src/lib/debug-log-buffer.ts +47 -6
  81. package/src/lib/errors.ts +3 -0
  82. package/src/lib/gcp-adc.ts +40 -2
  83. package/src/lib/injection-debug-log.ts +26 -2
  84. package/src/lib/sidecar-tracker.ts +5 -2
  85. package/src/lib/sse-decoder.ts +257 -37
  86. package/src/lib/state-store-registrations.ts +109 -0
  87. package/src/lib/state-store-sweeper.ts +184 -0
  88. package/src/lib/translator-budget.ts +356 -0
  89. package/src/lib/windows-secret-acl.ts +33 -12
  90. package/src/lib/winsw.ts +8 -1
  91. package/src/oauth/anthropic-routing.ts +31 -7
  92. package/src/oauth/google-antigravity.ts +2 -1
  93. package/src/oauth/health.ts +30 -12
  94. package/src/oauth/index.ts +127 -23
  95. package/src/oauth/kiro.ts +10 -2
  96. package/src/oauth/store.ts +165 -18
  97. package/src/oauth/token-guardian.ts +43 -4
  98. package/src/oauth/types.ts +2 -1
  99. package/src/providers/base-url-choices.ts +10 -0
  100. package/src/providers/derive.ts +12 -0
  101. package/src/providers/key-failover.ts +12 -0
  102. package/src/providers/openai-sidecar.ts +4 -1
  103. package/src/providers/quota.ts +68 -7
  104. package/src/providers/registry.ts +279 -3
  105. package/src/responses/parser.ts +5 -1
  106. package/src/responses/spill-store.ts +394 -0
  107. package/src/responses/state.ts +520 -102
  108. package/src/router.ts +18 -1
  109. package/src/server/adapter-resolve.ts +20 -3
  110. package/src/server/auth-cors.ts +121 -28
  111. package/src/server/chat-completions.ts +57 -12
  112. package/src/server/claude-messages.ts +85 -13
  113. package/src/server/index.ts +239 -97
  114. package/src/server/lifecycle.ts +155 -25
  115. package/src/server/management/agent-settings-routes.ts +75 -32
  116. package/src/server/management/api-key-usage.ts +167 -0
  117. package/src/server/management/body.ts +35 -0
  118. package/src/server/management/combo-routes.ts +5 -1
  119. package/src/server/management/config-routes.ts +37 -12
  120. package/src/server/management/logs-usage-routes.ts +41 -21
  121. package/src/server/management/model-routes.ts +173 -53
  122. package/src/server/management/oauth-account-routes.ts +115 -26
  123. package/src/server/management/provider-routes.ts +56 -6
  124. package/src/server/management/sidebar-routes.ts +50 -1
  125. package/src/server/management/system-restart.ts +13 -6
  126. package/src/server/management/system-routes.ts +15 -3
  127. package/src/server/management/usage-summary-cache.ts +86 -0
  128. package/src/server/management-api.ts +39 -5
  129. package/src/server/management-auth.ts +65 -14
  130. package/src/server/port-reclaim.ts +58 -12
  131. package/src/server/ports.ts +2 -0
  132. package/src/server/proxy-liveness.ts +51 -12
  133. package/src/server/relay-eager.ts +20 -4
  134. package/src/server/relay.ts +548 -154
  135. package/src/server/request-decompress.ts +51 -4
  136. package/src/server/request-log.ts +134 -15
  137. package/src/server/responses/collaboration.ts +15 -4
  138. package/src/server/responses/compact.ts +3 -0
  139. package/src/server/responses/core.ts +241 -66
  140. package/src/server/responses-image-gen-repair.ts +19 -5
  141. package/src/server/responses-item-id-repair.ts +23 -5
  142. package/src/server/sse-payload-rewrite.ts +71 -12
  143. package/src/server/startup-health-cache.ts +14 -1
  144. package/src/server/system-env.ts +8 -1
  145. package/src/server/windows-tcp-drop.ts +15 -5
  146. package/src/server/ws-bridge.ts +25 -0
  147. package/src/service.ts +23 -3
  148. package/src/storage/policy-job.ts +93 -23
  149. package/src/storage/policy-worker.ts +6 -0
  150. package/src/storage/restore-job.ts +62 -16
  151. package/src/storage/restore-worker.ts +6 -0
  152. package/src/storage/storage-mutation-coordinator.ts +36 -6
  153. package/src/storage/worker-lifecycle.ts +181 -47
  154. package/src/tray/windows.ts +45 -18
  155. package/src/types.ts +34 -6
  156. package/src/update/index.ts +24 -5
  157. package/src/update/job.ts +598 -73
  158. package/src/usage/log.ts +115 -17
  159. package/src/usage/summary.ts +67 -2
  160. package/src/vision/index.ts +112 -22
  161. package/src/web-search/loop.ts +38 -6
  162. package/src/web-search/progress-stream.ts +14 -3
  163. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
  164. package/gui/dist/assets/index-GC0Vlu1Z.js +0 -67
@@ -2,77 +2,211 @@
2
2
  * Deterministic teardown for the storage Bun Workers.
3
3
  *
4
4
  * `Worker.terminate()` returns void and does NOT wait for the thread to be
5
- * reclaimed. Every caller here used to fire it and move on, which is fine for
6
- * the proxy but not for `bun test --isolate`: the harness tears a test file's
7
- * realm down at the file boundary, and on Windows a worker that has not
8
- * finished exiting by then trips a Bun-internal assertion and kills the whole
9
- * run.
5
+ * reclaimed. Callers that fire-and-forget leave a window where
6
+ * `bun test --isolate` reclaims the file realm while a Windows worker thread
7
+ * is still exiting Bun then panics with
8
+ * `workers_spawned(N) workers_terminated(N-1)` / Internal assertion failure.
10
9
  *
11
- * The crash header names the shape exactly `workers_spawned(9)
12
- * workers_terminated(8)`, one worker still alive and it lands right at the
13
- * `api-storage-policy` `api-storage` file boundary, the only suite that
14
- * spawns policy workers. See `devlog/_plan/260730_remote_issue_merge_round/150`,
15
- * which reproduced the panic twice against an unchanged tree and left this
16
- * defence as the follow-up if it ever recurred. It recurred.
17
- *
18
- * So: keep a registry of live workers, and give shutdown/reset paths something
19
- * they can actually await. `close` is Bun's post-exit event for a worker
20
- * thread, so awaiting it is the real "the thread is gone" signal rather than a
21
- * timer we hope is long enough. The timeout only exists so a wedged worker
22
- * cannot hang a test teardown forever.
10
+ * Invariant: every registered worker has a `close` listener attached at spawn
11
+ * time (so we cannot miss `self.close()` / early exit), stays in `liveWorkers`
12
+ * until that close settles, and `drainStorageWorkers()` joins every in-flight
13
+ * terminate. Spawns are serialized through `withStorageWorkerSpawnGate` so the
14
+ * next Worker cannot be created until prior threads have exited. On Windows and
15
+ * macOS, a post-close settle covers the OS join gap Bun does not expose
16
+ * (Windows unbalanced join panic; macOS Silicon balanced-count segfault under
17
+ * `bun test --isolate`).
18
+ */
19
+
20
+ import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
21
+
22
+ type TrackedWorker = {
23
+ worker: Worker;
24
+ closed: Promise<void>;
25
+ resolveClosed: () => void;
26
+ terminatePromise?: Promise<void>;
27
+ reservation?: AdmissionReservation<Worker>;
28
+ };
29
+
30
+ export const MAX_RESERVED_STORAGE_WORKER_SPAWNS = 16;
31
+ export class StorageWorkerAdmissionBusyError extends Error {
32
+ readonly code = "storage_mutation_busy";
33
+
34
+ constructor() {
35
+ super("storage worker spawn queue is busy");
36
+ this.name = "StorageWorkerAdmissionBusyError";
37
+ }
38
+ }
39
+
40
+ const liveWorkers = new Map<Worker, TrackedWorker>();
41
+ const workerGate = createAdmissionGate("storage_worker_reservations", MAX_RESERVED_STORAGE_WORKER_SPAWNS);
42
+
43
+ /** Serialize spawns so a new Worker never overlaps a still-exiting predecessor. */
44
+ let spawnGate: Promise<void> = Promise.resolve();
45
+
46
+ /**
47
+ * Bumped by teardown so a spawn still queued on `spawnGate` (not yet in
48
+ * `liveWorkers`) cannot create a Worker after reset/shutdown reported idle.
49
+ */
50
+ let spawnCancelEpoch = 0;
51
+
52
+ /**
53
+ * OS-join gap after the `close` event on platforms where Bun's Worker reclaim
54
+ * races the isolate/file boundary (not a CI job-timeout bump).
55
+ * Windows GHA at 250ms and 750ms still left `workers_spawned(N)
56
+ * workers_terminated(N-1)` panics under isolate (seen mid
57
+ * `storage-mutation-race` with 11/10). 1500ms covers deferred reclaim under
58
+ * stacked policy/restore workers. Darwin uses 250ms.
23
59
  */
60
+ const WORKER_OS_JOIN_MS = process.platform === "win32" ? 1_500 : 250;
24
61
 
25
- const liveWorkers = new Set<Worker>();
62
+ function needsWorkerOsJoinSettle(): boolean {
63
+ return process.platform === "win32" || process.platform === "darwin";
64
+ }
65
+
66
+ /** Invalidate spawn callbacks still waiting on the gate (reset / server drain). */
67
+ export function cancelQueuedStorageWorkerSpawns(): void {
68
+ spawnCancelEpoch += 1;
69
+ }
26
70
 
27
71
  /** Track a freshly spawned worker so teardown can wait for it later. */
28
72
  export function registerStorageWorker(worker: Worker): void {
29
- liveWorkers.add(worker);
73
+ if (liveWorkers.has(worker)) return;
74
+
75
+ let resolveClosed!: () => void;
76
+ const closed = new Promise<void>(resolve => {
77
+ resolveClosed = resolve;
78
+ });
79
+
80
+ const tracked: TrackedWorker = { worker, closed, resolveClosed };
81
+ liveWorkers.set(worker, tracked);
82
+
83
+ try {
84
+ worker.addEventListener("close", () => {
85
+ resolveClosed();
86
+ }, { once: true });
87
+ } catch {
88
+ // No close event — terminate path will still resolve via timeout/settle.
89
+ }
90
+ }
91
+
92
+ export function tryReserveStorageWorker(): AdmissionReservation<Worker> | null {
93
+ const gateLease = workerGate.tryAcquire();
94
+ if (!gateLease) return null;
95
+ let active = true;
96
+ let bound: Worker | undefined;
97
+ const reservation: AdmissionReservation<Worker> = {
98
+ bind(worker) {
99
+ if (!active) return;
100
+ bound = worker;
101
+ registerStorageWorker(worker);
102
+ const tracked = liveWorkers.get(worker);
103
+ if (tracked) tracked.reservation = reservation;
104
+ },
105
+ release() {
106
+ if (!active) return;
107
+ active = false;
108
+ if (bound) {
109
+ const tracked = liveWorkers.get(bound);
110
+ if (tracked?.reservation === reservation) tracked.reservation = undefined;
111
+ }
112
+ bound = undefined;
113
+ gateLease.release();
114
+ },
115
+ };
116
+ return reservation;
117
+ }
118
+
119
+ export function storageWorkerAdmissionMetrics(): AdmissionMetrics {
120
+ return workerGate.metrics();
121
+ }
122
+
123
+ /**
124
+ * Run `fn` only after every previously gated spawn has finished tearing down.
125
+ * Used around `new Worker(...)` so tests cannot overlap Windows thread exit.
126
+ */
127
+ export function withStorageWorkerSpawnGate<T>(fn: () => Promise<T>): Promise<T> {
128
+ const epochAtEnqueue = spawnCancelEpoch;
129
+ const run = spawnGate.then(async () => {
130
+ if (epochAtEnqueue !== spawnCancelEpoch) {
131
+ throw new Error("storage_worker_spawn_cancelled");
132
+ }
133
+ await drainStorageWorkers();
134
+ if (epochAtEnqueue !== spawnCancelEpoch) {
135
+ throw new Error("storage_worker_spawn_cancelled");
136
+ }
137
+ return fn();
138
+ });
139
+ spawnGate = run.then(
140
+ () => undefined,
141
+ () => undefined,
142
+ );
143
+ return run;
144
+ }
145
+
146
+ export function queuedStorageWorkerSpawnCount(): number {
147
+ return Math.max(0, workerGate.metrics().active - liveWorkers.size);
30
148
  }
31
149
 
32
150
  /**
33
151
  * Terminate a worker and resolve once its thread has actually exited.
34
152
  *
35
- * Safe to call twice: the second call finds the worker already deregistered and
36
- * resolves immediately.
153
+ * Safe to call twice: the second call joins the in-flight terminate promise.
37
154
  */
38
155
  export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promise<void> {
39
- if (!liveWorkers.has(worker)) {
156
+ const tracked = liveWorkers.get(worker);
157
+ if (!tracked) {
40
158
  try { worker.terminate(); } catch { /* already gone */ }
41
159
  return Promise.resolve();
42
160
  }
43
- liveWorkers.delete(worker);
161
+ if (tracked.terminatePromise) return tracked.terminatePromise;
162
+
163
+ tracked.terminatePromise = (async () => {
164
+ let timedOut = false;
165
+ const timer = setTimeout(() => {
166
+ timedOut = true;
167
+ tracked.resolveClosed();
168
+ }, timeoutMs);
44
169
 
45
- return new Promise<void>(resolve => {
46
- let done = false;
47
- const settle = (): void => {
48
- if (done) return;
49
- done = true;
50
- clearTimeout(timer);
51
- resolve();
52
- };
53
- // A worker that refuses to exit must not wedge teardown; the proxy path
54
- // never awaits this, and a test teardown would rather continue than hang.
55
- const timer = setTimeout(settle, timeoutMs);
56
- try {
57
- worker.addEventListener("close", settle, { once: true });
58
- } catch {
59
- // No close event available — fall back to the timeout above.
60
- }
61
170
  try {
62
- worker.terminate();
63
- } catch {
64
- settle();
171
+ try {
172
+ worker.terminate();
173
+ } catch {
174
+ tracked.resolveClosed();
175
+ }
176
+ await tracked.closed;
177
+ // Disarm before the OS-join settle: a late timer firing during the sleep
178
+ // would set timedOut after close already won and throw a false timeout.
179
+ clearTimeout(timer);
180
+ // Always run the OS-join settle before throwing on timeout: the timer
181
+ // only forces `closed`, it does not prove the OS thread has exited.
182
+ // Callers that catch and continue (e.g. drainAndShutdown) still need
183
+ // that gap before the next isolate reclaim or server.stop.
184
+ if (needsWorkerOsJoinSettle()) {
185
+ await Bun.sleep(0);
186
+ await Bun.sleep(WORKER_OS_JOIN_MS);
187
+ }
188
+ if (timedOut) {
189
+ throw new Error(`storage worker did not exit within ${timeoutMs}ms`);
190
+ }
191
+ } finally {
192
+ clearTimeout(timer);
193
+ liveWorkers.delete(worker);
194
+ tracked.reservation?.release();
195
+ tracked.reservation = undefined;
65
196
  }
66
- });
197
+ })();
198
+
199
+ return tracked.terminatePromise;
67
200
  }
68
201
 
69
202
  /**
70
- * Await every worker this module still tracks. Used by test resets so no
71
- * storage worker outlives the file that spawned it.
203
+ * Await every worker this module still tracks (including terminations already
204
+ * in flight). Used by test resets so no storage worker outlives the file that
205
+ * spawned it.
72
206
  */
73
207
  export async function drainStorageWorkers(timeoutMs = 5_000): Promise<void> {
74
- const pending = [...liveWorkers];
75
- await Promise.all(pending.map(worker => terminateStorageWorker(worker, timeoutMs)));
208
+ const pending = [...liveWorkers.keys()].map(worker => terminateStorageWorker(worker, timeoutMs));
209
+ await Promise.all(pending);
76
210
  }
77
211
 
78
212
  /** Live worker count — exported so a regression test can assert the invariant. */
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
5
5
  import { join, resolve } from "node:path";
6
6
  import { expandUserPath, getConfigDir } from "../config";
7
7
  import { durableBunPath } from "../lib/bun-runtime";
8
- import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
8
+ import { forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
9
9
  import { recordOwnedConfigPath } from "../lib/config-ownership";
10
10
 
11
11
  const RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
@@ -218,18 +218,45 @@ function readState(): WindowsTrayState | null {
218
218
  }
219
219
  }
220
220
 
221
- function replaceOwnedFile(path: string, contents: string | Buffer): void {
221
+ export interface WindowsTrayOwnedFileIO {
222
+ write(path: string, contents: string | Buffer): void;
223
+ harden(path: string): void;
224
+ rename(source: string, destination: string): void;
225
+ unlink(path: string): void;
226
+ }
227
+
228
+ export function replaceWindowsTrayOwnedFile(
229
+ path: string,
230
+ contents: string | Buffer,
231
+ io: WindowsTrayOwnedFileIO = {
232
+ write: (target, value) => writeFileSync(target, value, { mode: 0o600 }),
233
+ harden: target => {
234
+ try { chmodSync(target, 0o600); } catch { /* best-effort */ }
235
+ if (process.platform !== "win32") return;
236
+ const hardened = hardenSecretPath(target, { required: true });
237
+ if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
238
+ },
239
+ rename: renameSync,
240
+ unlink: unlinkSync,
241
+ },
242
+ ): void {
222
243
  const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
223
- writeFileSync(temporary, contents, { mode: 0o600 });
244
+ io.write(temporary, contents);
245
+ let renamed = false;
224
246
  try {
225
- try { chmodSync(temporary, 0o600); } catch { /* best-effort */ }
226
- if (process.platform === "win32") {
227
- const hardened = hardenSecretPath(temporary, { required: true });
228
- if (!hardened.ok) throw new Error("Windows tray ACL hardening did not complete; refusing to persist executable state.");
229
- }
230
- renameSync(temporary, path);
247
+ io.harden(temporary);
248
+ io.rename(temporary, path);
249
+ renamed = true;
250
+ forgetHardenedSecretPath(temporary);
231
251
  } finally {
232
- try { if (existsSync(temporary)) unlinkSync(temporary); } catch { /* best-effort */ }
252
+ if (!renamed) {
253
+ try {
254
+ io.unlink(temporary);
255
+ forgetHardenedSecretPath(temporary);
256
+ } catch (error) {
257
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") forgetHardenedSecretPath(temporary);
258
+ }
259
+ }
233
260
  }
234
261
  }
235
262
 
@@ -239,7 +266,7 @@ function writeState(
239
266
  runCommand: string,
240
267
  ): void {
241
268
  const path = trayStatePath();
242
- replaceOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
269
+ replaceWindowsTrayOwnedFile(path, JSON.stringify({ version: TRAY_STATE_VERSION, ...entry, runValue, runCommand }, null, 2) + "\n");
243
270
  }
244
271
 
245
272
  export function parseWindowsTrayRunValue(output: string, runValue: string): string | null {
@@ -580,21 +607,21 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
580
607
  ]));
581
608
  const restorePreviousInstall = () => {
582
609
  try {
583
- if (previousScriptBytes) replaceOwnedFile(entry.script, previousScriptBytes);
610
+ if (previousScriptBytes) replaceWindowsTrayOwnedFile(entry.script, previousScriptBytes);
584
611
  else if (existsSync(entry.script)) unlinkSync(entry.script);
585
612
  } catch { /* rollback best-effort */ }
586
613
  try {
587
- if (previousLauncherBytes) replaceOwnedFile(launcherPath, previousLauncherBytes);
614
+ if (previousLauncherBytes) replaceWindowsTrayOwnedFile(launcherPath, previousLauncherBytes);
588
615
  else if (existsSync(launcherPath)) unlinkSync(launcherPath);
589
616
  } catch { /* rollback best-effort */ }
590
617
  for (const [path, contents] of previousIconBytes) {
591
618
  try {
592
- if (contents) replaceOwnedFile(path, contents);
619
+ if (contents) replaceWindowsTrayOwnedFile(path, contents);
593
620
  else if (existsSync(path)) unlinkSync(path);
594
621
  } catch { /* rollback best-effort */ }
595
622
  }
596
623
  try {
597
- if (previousStateBytes) replaceOwnedFile(trayStatePath(), previousStateBytes);
624
+ if (previousStateBytes) replaceWindowsTrayOwnedFile(trayStatePath(), previousStateBytes);
598
625
  else if (existsSync(trayStatePath())) unlinkSync(trayStatePath());
599
626
  } catch { /* rollback best-effort */ }
600
627
  try {
@@ -612,9 +639,9 @@ export function installWindowsTray(startNow = true): WindowsTrayStatus {
612
639
  try {
613
640
  const hardenedDir = hardenSecretDir(getConfigDir(), { required: true });
614
641
  if (!hardenedDir.ok) throw new Error("Windows tray directory ACL hardening did not complete; refusing to install persistence.");
615
- replaceOwnedFile(entry.script, readFileSync(sourceScript));
616
- for (const pair of iconPairs) replaceOwnedFile(pair.installed, readFileSync(pair.source));
617
- replaceOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
642
+ replaceWindowsTrayOwnedFile(entry.script, readFileSync(sourceScript));
643
+ for (const pair of iconPairs) replaceWindowsTrayOwnedFile(pair.installed, readFileSync(pair.source));
644
+ replaceWindowsTrayOwnedFile(launcherPath, Buffer.from("\uFEFF" + buildWindowsTrayLauncherScript(entry), "utf16le"));
618
645
  runRegistry(["add", RUN_KEY, "/v", runValue, "/t", "REG_SZ", "/d", runCommand, "/f", "/reg:64"]);
619
646
  writeState(entryWithLauncher, runValue, runCommand);
620
647
  } catch (error) {
package/src/types.ts CHANGED
@@ -516,8 +516,22 @@ export interface OcxCustomModel {
516
516
  addedAt?: string;
517
517
  }
518
518
 
519
+ /**
520
+ * A generated `ocx_` data-plane key. `key` is the secret itself and never leaves
521
+ * the server except in the one-time POST /api/keys response; every other surface
522
+ * sees only the masked prefix.
523
+ */
524
+ export interface OcxApiKeyEntry {
525
+ id: string;
526
+ name: string;
527
+ key: string;
528
+ createdAt: string;
529
+ }
530
+
519
531
  export interface OcxConfig {
520
532
  port: number;
533
+ /** Maximum usage-log bytes read for one management snapshot. */
534
+ managementUsageMaxReadBytes?: number;
521
535
  providers: Record<string, OcxProviderConfig>;
522
536
  defaultProvider: string;
523
537
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
@@ -579,11 +593,12 @@ export interface OcxConfig {
579
593
  */
580
594
  fastMode?: boolean;
581
595
  /**
582
- * Windows SSE passthrough stream shape (#314 mitigation).
583
- * "auto" (default): eager bounded relay only on runtimes proven to carry the
584
- * Bun#32111 fix (none today legacy tee). "eager-relay": force the new relay
585
- * (accepts #32111 crash risk on Bun 1.3.14). "legacy-tee": pin the tee path.
586
- * Persisted in config.json because Windows services do not inherit shell env.
596
+ * Windows/macOS SSE passthrough stream shape (#314 mitigation).
597
+ * On Windows, "auto" (default) selects eager relay only on a runtime proven
598
+ * to carry the Bun#32111 fix. On macOS, "auto" always stays on legacy tee and
599
+ * eager relay is explicit-only. "eager-relay" opts into the new relay (and
600
+ * accepts #32111 crash risk on Bun 1.3.14); "legacy-tee" pins the tee path.
601
+ * Persisted in config.json so service users can select the stream shape.
587
602
  * See src/lib/bun-stream-caps.ts.
588
603
  */
589
604
  streamMode?: "auto" | "legacy-tee" | "eager-relay";
@@ -675,7 +690,7 @@ export interface OcxConfig {
675
690
  */
676
691
  storageCleanupPolicy?: StorageCleanupPolicy;
677
692
  /** Generated API keys for external access to the proxy's /v1/responses endpoint. */
678
- apiKeys?: Array<{ id: string; name: string; key: string; createdAt: string }>;
693
+ apiKeys?: OcxApiKeyEntry[];
679
694
  /** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
680
695
  codexAutoStart?: boolean;
681
696
  /** Restore an installed shim after a stable external Codex update replaces it. Default true. */
@@ -689,6 +704,8 @@ export interface OcxConfig {
689
704
  syncResumeHistory?: boolean;
690
705
  /** Freshness window (ms) for the per-provider live `/models` cache. Defaults to 5 min. */
691
706
  modelCacheTtlMs?: number;
707
+ /** Evictable retained app-state budget in MiB. Default 256; valid 64..4096. */
708
+ appOwnedMemoryBudgetMb?: number;
692
709
  /** Anthropic prompt-cache retention: "short" = 5-min ephemeral (default), "long" = 1-hour extended, "none" = disabled. */
693
710
  cacheRetention?: "none" | "short" | "long";
694
711
  /** Web-search sidecar: route web_search for non-OpenAI models through a gpt-mini via ChatGPT passthrough. */
@@ -890,6 +907,10 @@ export interface ResponsesItemIdRepairConfig {
890
907
 
891
908
  export interface OcxProviderConfig {
892
909
  adapter: string;
910
+ /** Cursor MCP compatibility bounds; positive integers when configured. */
911
+ mcpMaxTools?: number;
912
+ mcpMaxSchemaBytes?: number;
913
+ mcpMaxResultBytes?: number;
893
914
  /**
894
915
  * Per-model wire override, keyed by the upstream native model id (after namespace
895
916
  * and combo resolution). A single gateway can front models that speak different
@@ -908,6 +929,13 @@ export interface OcxProviderConfig {
908
929
  * the legacy `/v1/responses` construction.
909
930
  */
910
931
  responsesPath?: string;
932
+ /**
933
+ * Responses upstream that stores nothing server-side (DeepSeek documents "the API
934
+ * is stateless"). Stateful request parameters are dropped, `store` is pinned false,
935
+ * and orphaned tool results left by a replay miss are repaired rather than
936
+ * forwarded to an upstream that cannot resolve their pair.
937
+ */
938
+ statelessResponses?: boolean;
911
939
  /**
912
940
  * Explicit opt-in for non-registry private-network destinations such as localhost, RFC1918,
913
941
  * link-local, or unique-local upstreams. Metadata endpoints remain blocked.
@@ -321,16 +321,35 @@ export async function runUpdate(): Promise<void> {
321
321
  windowsHide: true,
322
322
  });
323
323
  if (svcStdio === "pipe") logSpawnOutput("", svc);
324
- if (svc.status !== 0) {
324
+ const serviceRefreshed = svc.status === 0;
325
+ let serviceViable = serviceRefreshed;
326
+ if (serviceRefreshed) {
327
+ try {
328
+ const { isServiceViable } = await import("../service");
329
+ serviceViable = isServiceViable();
330
+ } catch {
331
+ serviceViable = false;
332
+ }
333
+ }
334
+ if (!serviceRefreshed || !serviceViable) {
325
335
  // On Windows, schtasks /create requires elevation. The CLI inherits the
326
336
  // user's (non-admin) token, so the service reinstall can fail with access
327
- // denied. Fall back to a direct detached proxy start so the update never
328
- // leaves the user without a running proxy but only when the port is free.
337
+ // denied or exit 0 while leaving stale/missing assets that never start
338
+ // the proxy. Fall back to a direct detached proxy start so the update
339
+ // never leaves the user without a running proxy — but only when the port is free.
329
340
  if (!freed) {
330
- console.warn("⚠️ Service refresh failed and the captured port is still busy; not starting on another port.");
341
+ console.warn(
342
+ serviceRefreshed
343
+ ? "⚠️ Service refresh left a non-viable manager and the captured port is still busy; not starting on another port."
344
+ : "⚠️ Service refresh failed and the captured port is still busy; not starting on another port.",
345
+ );
331
346
  console.warn(` Run 'ocx service install' as administrator, then 'ocx start --port ${capturedListen.port}'.`);
332
347
  } else {
333
- console.warn("⚠️ Service refresh failed — starting the proxy directly instead.");
348
+ console.warn(
349
+ serviceRefreshed
350
+ ? "⚠️ Service refresh left a non-viable manager (stale or missing assets) — starting the proxy directly instead."
351
+ : "⚠️ Service refresh failed — starting the proxy directly instead.",
352
+ );
334
353
  console.warn(" Run 'ocx service install' as administrator to refresh the background service.");
335
354
  const env = { ...process.env };
336
355
  delete env.OCX_SERVICE;