@bitkyc08/opencodex 2.10.2 → 2.11.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.
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -0
- package/gui/dist/assets/index-Bk-PN-70.css +1 -0
- package/gui/dist/assets/index-BynIEIV-.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +4 -2
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/live-transport.ts +11 -0
- package/src/adapters/cursor/native-exec-fs.ts +9 -6
- package/src/adapters/cursor/native-exec.ts +4 -2
- package/src/adapters/cursor/protobuf-events.ts +176 -4
- package/src/adapters/cursor/request-builder.ts +15 -4
- package/src/adapters/cursor/tool-definitions.ts +118 -2
- package/src/adapters/google.ts +15 -5
- package/src/adapters/openai-chat.ts +24 -2
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +9 -5
- package/src/chat/outbound.ts +4 -3
- package/src/claude/desktop-3p.ts +222 -2
- package/src/claude/outbound.ts +15 -6
- package/src/cli/account-api.ts +4 -0
- package/src/cli/account-extended.ts +112 -0
- package/src/cli/account.ts +23 -6
- package/src/cli/claude-desktop.ts +26 -3
- package/src/cli/config-command.ts +9 -0
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +277 -55
- package/src/cli/models.ts +5 -1
- package/src/cli/provider.ts +8 -2
- package/src/cli/ready.ts +301 -0
- package/src/cli/system-restart-client.ts +146 -0
- package/src/cli/tray-proxy.ts +153 -6
- package/src/clients/config-export.ts +12 -19
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-namespaces.ts +49 -3
- package/src/codex/account-priority.ts +83 -0
- package/src/codex/auth-api.ts +83 -0
- package/src/codex/auth-context.ts +5 -2
- package/src/codex/catalog/provider-fetch.ts +11 -0
- package/src/codex/catalog/sync.ts +23 -1
- package/src/codex/codex-write-lock.ts +16 -4
- package/src/codex/desired-state.ts +37 -4
- package/src/codex/history-job.ts +15 -5
- package/src/codex/history-provider.ts +31 -14
- package/src/codex/history-worker.ts +28 -4
- package/src/codex/inject-coordination.ts +13 -1
- package/src/codex/inject.ts +360 -66
- package/src/codex/internal/history-writer.ts +1 -1
- package/src/codex/native-main-lock-file.ts +5 -1
- package/src/codex/native-main-owner.ts +17 -3
- package/src/codex/native-profile-manager.ts +19 -0
- package/src/codex/native-profile-startup.ts +8 -0
- package/src/codex/native-residue.ts +140 -27
- package/src/codex/pool-rotation.ts +74 -4
- package/src/codex/refresh.ts +7 -0
- package/src/codex/routing.ts +177 -36
- package/src/codex/subagent-model-fallback.ts +34 -4
- package/src/codex/sync.ts +61 -0
- package/src/codex/upstream-host-health.ts +329 -31
- package/src/combos/request.ts +2 -0
- package/src/config.ts +221 -2
- package/src/images/loop.ts +1 -1
- package/src/integrations/native/ownership-preflight.ts +39 -2
- package/src/lib/bun-stream-caps.ts +3 -3
- package/src/lib/sse-decoder.ts +41 -0
- package/src/lib/system-restart-contract.ts +73 -0
- package/src/lib/windows-secret-acl.ts +141 -39
- package/src/lib/windows-user-principal.ts +283 -0
- package/src/lib/winsw.ts +18 -2
- package/src/oauth/key-providers.ts +12 -0
- package/src/providers/derive.ts +54 -2
- package/src/providers/free-directory.ts +6 -5
- package/src/providers/model-discovery.ts +9 -3
- package/src/providers/quota.ts +592 -0
- package/src/providers/registry.ts +316 -13
- package/src/responses/parser.ts +26 -10
- package/src/responses/reasoning-replay-cache.ts +1 -0
- package/src/routing/profile-namespace.ts +15 -0
- package/src/routing/profile.ts +2 -1
- package/src/server/auth-cors.ts +44 -13
- package/src/server/chat-completions.ts +0 -4
- package/src/server/claude-messages.ts +73 -15
- package/src/server/github-copilot-responses-repair.ts +338 -0
- package/src/server/index.ts +328 -111
- package/src/server/lifecycle.ts +36 -0
- package/src/server/management/agent-settings-routes.ts +147 -56
- package/src/server/management/config-routes.ts +7 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +199 -20
- package/src/server/management/provider-routes.ts +41 -0
- package/src/server/management/routing-profile-routes.ts +234 -5
- package/src/server/management/system-restart.ts +12 -10
- package/src/server/management/system-routes.ts +20 -0
- package/src/server/management-auth.ts +51 -3
- package/src/server/ports.ts +41 -1
- package/src/server/proxy-liveness.ts +129 -4
- package/src/server/readiness.ts +99 -0
- package/src/server/relay.ts +113 -97
- package/src/server/request-log.ts +10 -4
- package/src/server/responses/compact.ts +107 -12
- package/src/server/responses/core.ts +220 -39
- package/src/server/responses-item-id-repair.ts +22 -3
- package/src/server/responses-model-rewrite.ts +29 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/sse-payload-rewrite.ts +25 -14
- package/src/server/ws-bridge.ts +27 -22
- package/src/service-manager-probe.ts +520 -10
- package/src/service.ts +134 -2
- package/src/storage/worker-lifecycle.ts +14 -14
- package/src/tray/windows-tray.ps1 +74 -9
- package/src/types.ts +68 -2
- package/src/update/index.ts +12 -0
- package/src/update/job.ts +392 -18
- package/src/update/npm-cache-preflight.d.mts +47 -0
- package/src/update/npm-cache-preflight.mjs +201 -0
- package/src/usage/log.ts +1 -1
- package/src/vision/index.ts +77 -2
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/parse.ts +4 -1
- package/gui/dist/assets/index-BKVqyYqT.js +0 -70
- package/gui/dist/assets/index-Ca_3269W.css +0 -1
package/src/service.ts
CHANGED
|
@@ -2177,6 +2177,11 @@ type ServiceOps = {
|
|
|
2177
2177
|
status: () => string; uninstall: () => void;
|
|
2178
2178
|
};
|
|
2179
2179
|
|
|
2180
|
+
type ServiceInstallCleanupOps = {
|
|
2181
|
+
status: () => string | null;
|
|
2182
|
+
stop: () => void;
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2180
2185
|
function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
2181
2186
|
if (process.platform === "darwin")
|
|
2182
2187
|
return { install: installLaunchd, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
|
|
@@ -2202,6 +2207,67 @@ function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
|
2202
2207
|
return null;
|
|
2203
2208
|
}
|
|
2204
2209
|
|
|
2210
|
+
/**
|
|
2211
|
+
* Install-only manager operations. Unlike the ordinary status/stop helpers, these
|
|
2212
|
+
* distinguish confirmed absence from a failed manager query and propagate every
|
|
2213
|
+
* non-benign stop failure. Installing new assets is unsafe while either answer is
|
|
2214
|
+
* unknown because an old manager may still respawn a listener on the target port.
|
|
2215
|
+
*/
|
|
2216
|
+
function platformServiceInstallCleanupOps(backend: ServiceBackend): ServiceInstallCleanupOps | null {
|
|
2217
|
+
if (process.platform === "darwin") {
|
|
2218
|
+
return {
|
|
2219
|
+
status: () => {
|
|
2220
|
+
const listing = sh("launchctl list");
|
|
2221
|
+
return listing.split("\n").some(line => line.includes(LABEL)) ? listing : null;
|
|
2222
|
+
},
|
|
2223
|
+
stop: () => { sh(`launchctl unload "${plistPath()}"`); },
|
|
2224
|
+
};
|
|
2225
|
+
}
|
|
2226
|
+
if (process.platform === "win32") {
|
|
2227
|
+
if (backend === "native") {
|
|
2228
|
+
return {
|
|
2229
|
+
status: () => {
|
|
2230
|
+
const status = statusWinswRaw();
|
|
2231
|
+
if (status === "unknown") throw new Error("Native service status could not be verified.");
|
|
2232
|
+
return status === "nonexistent" ? null : status;
|
|
2233
|
+
},
|
|
2234
|
+
stop: stopWinswService,
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
return {
|
|
2238
|
+
status: () => {
|
|
2239
|
+
const probe = probeWindowsSchedulerTask(TASK);
|
|
2240
|
+
if (probe.status === "unknown") throw new Error(`Task Scheduler status could not be verified: ${probe.detail}`);
|
|
2241
|
+
return probe.status === "present" ? "present" : null;
|
|
2242
|
+
},
|
|
2243
|
+
stop: () => {
|
|
2244
|
+
try {
|
|
2245
|
+
schtasks(["/end", "/tn", TASK]);
|
|
2246
|
+
} catch (error) {
|
|
2247
|
+
if (!isWindowsSchedulerEndBenign(error)) throw error;
|
|
2248
|
+
}
|
|
2249
|
+
},
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
if (process.platform === "linux") {
|
|
2253
|
+
return {
|
|
2254
|
+
status: () => {
|
|
2255
|
+
// `list-unit-files <name>` exits non-zero when the unit has never been
|
|
2256
|
+
// installed, which made a clean first install look like an unknown manager
|
|
2257
|
+
// failure. `show LoadState` gives us the tri-state we actually need: a
|
|
2258
|
+
// healthy user manager returns `not-found` for a missing unit, while an
|
|
2259
|
+
// unreachable/permission-denied manager still makes `sh()` throw and the
|
|
2260
|
+
// caller therefore fails closed.
|
|
2261
|
+
const loadState = sh(`systemctl --user show ${TASK} --property=LoadState --value`).trim().toLowerCase();
|
|
2262
|
+
if (!loadState) throw new Error("systemd service status could not be verified.");
|
|
2263
|
+
return loadState === "not-found" ? null : loadState;
|
|
2264
|
+
},
|
|
2265
|
+
stop: () => { sh(`systemctl --user stop ${TASK}`); },
|
|
2266
|
+
};
|
|
2267
|
+
}
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2205
2271
|
type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
|
|
2206
2272
|
|
|
2207
2273
|
function verifiedKillTarget(pid: number | null | undefined): number | null {
|
|
@@ -2297,6 +2363,60 @@ async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupR
|
|
|
2297
2363
|
}
|
|
2298
2364
|
}
|
|
2299
2365
|
|
|
2366
|
+
export interface ServiceInstallPreparationDeps {
|
|
2367
|
+
diagnose?: () => ServiceDiagnostic;
|
|
2368
|
+
managerOps?: (backend: ServiceBackend) => ServiceInstallCleanupOps | null;
|
|
2369
|
+
stopTrackedProxy?: () => Promise<unknown>;
|
|
2370
|
+
platform?: NodeJS.Platform;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
/**
|
|
2374
|
+
* Stop every manager that could own the install port, then stop the tracked
|
|
2375
|
+
* standalone listener. Any unknown status or cleanup failure rejects, so callers
|
|
2376
|
+
* cannot write assets or report success over a surviving old listener.
|
|
2377
|
+
*/
|
|
2378
|
+
export async function prepareServiceInstall(
|
|
2379
|
+
requestedBackend: ServiceBackend,
|
|
2380
|
+
deps: ServiceInstallPreparationDeps = {},
|
|
2381
|
+
): Promise<void> {
|
|
2382
|
+
const diagnostic = (deps.diagnose ?? diagnoseService)();
|
|
2383
|
+
const platform = deps.platform ?? process.platform;
|
|
2384
|
+
const resolveOps = deps.managerOps ?? platformServiceInstallCleanupOps;
|
|
2385
|
+
const backends: ServiceBackend[] = [];
|
|
2386
|
+
const addBackend = (backend: ServiceBackend) => {
|
|
2387
|
+
if (!backends.includes(backend)) backends.push(backend);
|
|
2388
|
+
};
|
|
2389
|
+
|
|
2390
|
+
if (platform === "win32") {
|
|
2391
|
+
// The recorded backend owns the old installation and must be stopped first.
|
|
2392
|
+
// A conflicting diagnostic means both managers exist, so stop both even when
|
|
2393
|
+
// the requested backend happens to match the recorded one.
|
|
2394
|
+
if (diagnostic.backend === "scheduler" || diagnostic.backend === "native") {
|
|
2395
|
+
addBackend(diagnostic.backend);
|
|
2396
|
+
if (diagnostic.conflict) addBackend(diagnostic.backend === "scheduler" ? "native" : "scheduler");
|
|
2397
|
+
}
|
|
2398
|
+
addBackend(requestedBackend);
|
|
2399
|
+
} else {
|
|
2400
|
+
addBackend(requestedBackend);
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
for (const backend of backends) {
|
|
2404
|
+
const manager = resolveOps(backend);
|
|
2405
|
+
if (!manager) throw new Error(`Background service manager is unavailable for ${backend}.`);
|
|
2406
|
+
if (manager.status() !== null) manager.stop();
|
|
2407
|
+
}
|
|
2408
|
+
await (deps.stopTrackedProxy ?? stopTrackedProxyIfRunning)();
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
export async function installServiceSafely(
|
|
2412
|
+
requestedBackend: ServiceBackend,
|
|
2413
|
+
install: () => void | Promise<void>,
|
|
2414
|
+
deps: ServiceInstallPreparationDeps = {},
|
|
2415
|
+
): Promise<void> {
|
|
2416
|
+
await prepareServiceInstall(requestedBackend, deps);
|
|
2417
|
+
await install();
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2300
2420
|
/**
|
|
2301
2421
|
* If a service is installed, stop it so the process manager doesn't respawn after `ocx stop`.
|
|
2302
2422
|
* Returns true if a service was found and stopped.
|
|
@@ -2645,7 +2765,19 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2645
2765
|
case "install":
|
|
2646
2766
|
assertServiceEnvironmentMatchesInstall();
|
|
2647
2767
|
assertServiceAuthEnvironment();
|
|
2648
|
-
|
|
2768
|
+
// A manually started proxy can still own the configured port while the service
|
|
2769
|
+
// registration is absent or unloaded. Stop both the registered manager and any
|
|
2770
|
+
// tracked standalone listener before loading the freshly written service assets.
|
|
2771
|
+
// Otherwise launchd/Task Scheduler can register successfully while its child
|
|
2772
|
+
// restart-loops on EADDRINUSE, and the old standalone process makes the install
|
|
2773
|
+
// verification report a false success.
|
|
2774
|
+
try {
|
|
2775
|
+
await installServiceSafely(backend, ops.install);
|
|
2776
|
+
} catch (error) {
|
|
2777
|
+
console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2778
|
+
process.exitCode = 1;
|
|
2779
|
+
break;
|
|
2780
|
+
}
|
|
2649
2781
|
// The wrapper was written moments ago in this process, so the configured port
|
|
2650
2782
|
// and the baked one cannot have diverged yet — unlike `start`, which reads the
|
|
2651
2783
|
// installed artifact instead.
|
|
@@ -2741,4 +2873,4 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2741
2873
|
console.error(" --native (Windows only): register a real SCM service via WinSW instead of Task Scheduler.");
|
|
2742
2874
|
process.exit(1);
|
|
2743
2875
|
}
|
|
2744
|
-
}
|
|
2876
|
+
}
|
|
@@ -11,10 +11,9 @@
|
|
|
11
11
|
* time (so we cannot miss `self.close()` / early exit), stays in `liveWorkers`
|
|
12
12
|
* until that close settles, and `drainStorageWorkers()` joins every in-flight
|
|
13
13
|
* terminate. Spawns are serialized through `withStorageWorkerSpawnGate` so the
|
|
14
|
-
* next Worker cannot be created until prior threads have exited.
|
|
15
|
-
*
|
|
16
|
-
* (
|
|
17
|
-
* `bun test --isolate`).
|
|
14
|
+
* next Worker cannot be created until prior threads have exited. A post-close
|
|
15
|
+
* settle covers the OS/runtime join gap Bun does not expose on Windows, macOS,
|
|
16
|
+
* and Linux (including Bun 1.3.14 isolate crashes and Linux `epoll_ctl` reuse).
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
import { createAdmissionGate, type AdmissionMetrics, type AdmissionReservation } from "../lib/admission";
|
|
@@ -52,15 +51,15 @@ let spawnCancelEpoch = 0;
|
|
|
52
51
|
/**
|
|
53
52
|
* OS-join gap after the `close` event on platforms where Bun's Worker reclaim
|
|
54
53
|
* 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
|
|
57
|
-
*
|
|
58
|
-
*
|
|
54
|
+
* Windows GHA at 250ms and 750ms still left `workers_spawned(N)`
|
|
55
|
+
* `workers_terminated(N-1)` panics under isolate, so Windows keeps 1500ms.
|
|
56
|
+
* Darwin and Linux use a shorter settle for the balanced-count/epoll reclaim
|
|
57
|
+
* window seen on Bun 1.3.14.
|
|
59
58
|
*/
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return
|
|
59
|
+
export function storageWorkerOsJoinSettleMs(platform = process.platform): number {
|
|
60
|
+
if (platform === "win32") return 1_500;
|
|
61
|
+
if (platform === "darwin" || platform === "linux") return 250;
|
|
62
|
+
return 0;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
/** Invalidate spawn callbacks still waiting on the gate (reset / server drain). */
|
|
@@ -181,9 +180,10 @@ export function terminateStorageWorker(worker: Worker, timeoutMs = 5_000): Promi
|
|
|
181
180
|
// only forces `closed`, it does not prove the OS thread has exited.
|
|
182
181
|
// Callers that catch and continue (e.g. drainAndShutdown) still need
|
|
183
182
|
// that gap before the next isolate reclaim or server.stop.
|
|
184
|
-
|
|
183
|
+
const settleMs = storageWorkerOsJoinSettleMs();
|
|
184
|
+
if (settleMs > 0) {
|
|
185
185
|
await Bun.sleep(0);
|
|
186
|
-
await Bun.sleep(
|
|
186
|
+
await Bun.sleep(settleMs);
|
|
187
187
|
}
|
|
188
188
|
if (timedOut) {
|
|
189
189
|
throw new Error(`storage worker did not exit within ${timeoutMs}ms`);
|
|
@@ -84,7 +84,7 @@ function ConvertTo-NativeArgument([string]$Value) {
|
|
|
84
84
|
return '"' + $Value + '"'
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function Start-OcxCommand([string[]]$CommandArgs) {
|
|
87
|
+
function Start-OcxCommand([string[]]$CommandArgs, [switch]$TrackExit) {
|
|
88
88
|
try {
|
|
89
89
|
$allArgs = @($CliPath) + $CommandArgs
|
|
90
90
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
@@ -102,8 +102,10 @@ function Start-OcxCommand([string[]]$CommandArgs) {
|
|
|
102
102
|
$psi.EnvironmentVariables["OCX_BUN_RUNTIME_PATH"] = $BunPath
|
|
103
103
|
}
|
|
104
104
|
$process = [System.Diagnostics.Process]::Start($psi)
|
|
105
|
-
if ($null -
|
|
105
|
+
if ($null -eq $process) { throw "Process did not start" }
|
|
106
106
|
Write-ActionLog "dispatched $($CommandArgs -join ' ')"
|
|
107
|
+
if ($TrackExit) { return $process }
|
|
108
|
+
$process.Dispose()
|
|
107
109
|
return $true
|
|
108
110
|
} catch {
|
|
109
111
|
Write-ActionLog "launch failed: $($_.Exception.GetType().Name)"
|
|
@@ -172,18 +174,40 @@ $script:pendingAction = $null
|
|
|
172
174
|
$script:pendingStarted = 0L
|
|
173
175
|
$script:pendingDeadline = 0L
|
|
174
176
|
$script:pendingOldProxyPid = $null
|
|
177
|
+
$script:pendingProcess = $null
|
|
175
178
|
|
|
176
179
|
function Set-PendingAction([string]$Action, [int]$TimeoutSeconds) {
|
|
180
|
+
if ($null -ne $script:pendingAction) {
|
|
181
|
+
Write-ActionLog "$Action ignored because $($script:pendingAction) is still pending"
|
|
182
|
+
return $false
|
|
183
|
+
}
|
|
184
|
+
if ($null -ne $script:pendingProcess) {
|
|
185
|
+
try {
|
|
186
|
+
$script:pendingProcess.Dispose()
|
|
187
|
+
} catch {
|
|
188
|
+
Write-ActionLog "pending process dispose failed: $($_.Exception.GetType().Name)"
|
|
189
|
+
}
|
|
190
|
+
$script:pendingProcess = $null
|
|
191
|
+
}
|
|
177
192
|
$script:pendingAction = $Action
|
|
178
193
|
$script:pendingStarted = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
179
194
|
$script:pendingDeadline = $script:pendingStarted + ($TimeoutSeconds * 1000)
|
|
180
195
|
$script:pendingOldProxyPid = $script:proxyPid
|
|
196
|
+
return $true
|
|
181
197
|
}
|
|
182
198
|
|
|
183
199
|
function Complete-PendingAction([bool]$Success) {
|
|
184
200
|
if ($null -eq $script:pendingAction) { return }
|
|
185
201
|
$action = $script:pendingAction
|
|
186
202
|
$script:pendingAction = $null
|
|
203
|
+
if ($null -ne $script:pendingProcess) {
|
|
204
|
+
try {
|
|
205
|
+
$script:pendingProcess.Dispose()
|
|
206
|
+
} catch {
|
|
207
|
+
Write-ActionLog "pending process dispose failed: $($_.Exception.GetType().Name)"
|
|
208
|
+
}
|
|
209
|
+
$script:pendingProcess = $null
|
|
210
|
+
}
|
|
187
211
|
if ($Success) {
|
|
188
212
|
Write-ActionLog "$action completed (port=$($script:port), pid=$($script:proxyPid))"
|
|
189
213
|
$notify.ShowBalloonTip(2500, "opencodex", "$action completed.", [System.Windows.Forms.ToolTipIcon]::Info)
|
|
@@ -226,6 +250,11 @@ function Update-TrayState {
|
|
|
226
250
|
$stopItem.Enabled = $false
|
|
227
251
|
$restartItem.Enabled = $false
|
|
228
252
|
}
|
|
253
|
+
if ($null -ne $script:pendingAction) {
|
|
254
|
+
$startItem.Enabled = $false
|
|
255
|
+
$stopItem.Enabled = $false
|
|
256
|
+
$restartItem.Enabled = $false
|
|
257
|
+
}
|
|
229
258
|
$heartbeat = @{ pid = $PID; timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() }
|
|
230
259
|
if ($HostPid -gt 0) { $heartbeat.hostPid = $HostPid }
|
|
231
260
|
$heartbeatJson = $heartbeat | ConvertTo-Json -Compress
|
|
@@ -237,26 +266,59 @@ function Update-TrayState {
|
|
|
237
266
|
$reached = ($script:pendingAction -eq "Start Proxy" -and $script:online) -or
|
|
238
267
|
($script:pendingAction -eq "Stop Proxy" -and -not $script:online) -or
|
|
239
268
|
($script:pendingAction -eq "Restart Proxy" -and $elapsed -gt 3000 -and $script:online -and $script:proxyPid -ne $script:pendingOldProxyPid)
|
|
240
|
-
|
|
269
|
+
$commandFailed = $false
|
|
270
|
+
if ($null -ne $script:pendingProcess) {
|
|
271
|
+
try {
|
|
272
|
+
$commandFailed = $script:pendingProcess.HasExited -and $script:pendingProcess.ExitCode -ne 0
|
|
273
|
+
} catch {
|
|
274
|
+
Write-ActionLog "pending process result inspection failed: $($_.Exception.GetType().Name)"
|
|
275
|
+
# If we cannot inspect the tracked command, we cannot prove it is still
|
|
276
|
+
# healthy. Fail the pending action instead of silently waiting for a later
|
|
277
|
+
# timeout and presenting an indeterminate process as success-capable.
|
|
278
|
+
$commandFailed = $true
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if ($commandFailed) { Complete-PendingAction $false }
|
|
282
|
+
elseif ($reached) { Complete-PendingAction $true }
|
|
241
283
|
elseif ($now -gt $script:pendingDeadline) { Complete-PendingAction $false }
|
|
242
284
|
}
|
|
243
285
|
}
|
|
244
286
|
|
|
245
287
|
$openItem.add_Click({ Start-OcxCommand @("gui") })
|
|
246
288
|
$startItem.add_Click({
|
|
289
|
+
if (-not (Set-PendingAction "Start Proxy" 75)) { return }
|
|
247
290
|
$statusItem.Text = "Proxy: Starting..."
|
|
248
|
-
|
|
249
|
-
|
|
291
|
+
# service start can spend 20s and the CLI then observes health for another 40s.
|
|
292
|
+
$startProcess = Start-OcxCommand @("__tray-start") -TrackExit
|
|
293
|
+
if ($startProcess -is [System.Diagnostics.Process]) {
|
|
294
|
+
$script:pendingProcess = $startProcess
|
|
295
|
+
} else {
|
|
296
|
+
Complete-PendingAction $false
|
|
297
|
+
}
|
|
250
298
|
})
|
|
251
299
|
$stopItem.add_Click({
|
|
300
|
+
if (-not (Set-PendingAction "Stop Proxy" 15)) { return }
|
|
252
301
|
$statusItem.Text = "Proxy: Stopping..."
|
|
253
|
-
|
|
254
|
-
if (
|
|
302
|
+
$stopProcess = Start-OcxCommand @("stop") -TrackExit
|
|
303
|
+
if ($stopProcess -is [System.Diagnostics.Process]) {
|
|
304
|
+
$script:pendingProcess = $stopProcess
|
|
305
|
+
} else {
|
|
306
|
+
Complete-PendingAction $false
|
|
307
|
+
}
|
|
255
308
|
})
|
|
256
309
|
$restartItem.add_Click({
|
|
310
|
+
if (-not (Set-PendingAction "Restart Proxy" 160)) { return }
|
|
257
311
|
$statusItem.Text = "Proxy: Restarting..."
|
|
258
|
-
|
|
259
|
-
|
|
312
|
+
# /api/system/restart may drain active work for 60s and then spend up to 70s
|
|
313
|
+
# handing off to an identity-verified replacement. The tray observes health/PID
|
|
314
|
+
# rather than the detached CLI exit, so keep a watchdog margin around that shared
|
|
315
|
+
# lifecycle budget. The CLI remains the lifecycle owner; the tray never kills.
|
|
316
|
+
$restartProcess = Start-OcxCommand @("__tray-restart") -TrackExit
|
|
317
|
+
if ($restartProcess -is [System.Diagnostics.Process]) {
|
|
318
|
+
$script:pendingProcess = $restartProcess
|
|
319
|
+
} else {
|
|
320
|
+
Complete-PendingAction $false
|
|
321
|
+
}
|
|
260
322
|
})
|
|
261
323
|
$logsItem.add_Click({
|
|
262
324
|
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
|
@@ -289,6 +351,9 @@ try {
|
|
|
289
351
|
$timer.Stop()
|
|
290
352
|
$timer.Dispose()
|
|
291
353
|
$notify.Visible = $false
|
|
354
|
+
if ($null -ne $script:pendingProcess) {
|
|
355
|
+
try { $script:pendingProcess.Dispose() } catch { $null = $_ }
|
|
356
|
+
}
|
|
292
357
|
$notify.Dispose()
|
|
293
358
|
foreach ($icon in $script:ownedIcons) { $icon.Dispose() }
|
|
294
359
|
$menu.Dispose()
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { KiroOAuthMetadata } from "./oauth/types";
|
|
|
2
2
|
|
|
3
3
|
export interface OcxParsedRequest {
|
|
4
4
|
modelId: string;
|
|
5
|
+
/** Client-facing model selector retained for Anthropic routes after wire-model normalization. */
|
|
6
|
+
_responseModelId?: string;
|
|
5
7
|
/** Selected OpenAI API virtual-model id retained after it rewrites the upstream wire model. */
|
|
6
8
|
_openAiVirtualSelectedModelId?: string;
|
|
7
9
|
previousResponseId?: string;
|
|
@@ -164,6 +166,8 @@ export interface OcxTool {
|
|
|
164
166
|
toolSearch?: boolean;
|
|
165
167
|
/** Tool definition restored from a prior tool_search output; transports may prioritize it when catalogs are bounded. */
|
|
166
168
|
loadedFromToolSearch?: boolean;
|
|
169
|
+
/** Cursor-only synthetic exact-match edit tool; never inferred from the wire name. */
|
|
170
|
+
cursorStructuredEdit?: true;
|
|
167
171
|
/** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
|
|
168
172
|
webSearch?: boolean;
|
|
169
173
|
/** Synthetic image_gen tool: the model's call is executed by the xAI image bridge sidecar, not relayed to Codex. */
|
|
@@ -233,6 +237,20 @@ export interface OcxRequestOptions {
|
|
|
233
237
|
frequencyPenalty?: number;
|
|
234
238
|
/** Responses prompt-cache affinity key. Passthrough preserves it via _rawBody; routed adapters do not consume it unless their upstream wire supports it. */
|
|
235
239
|
promptCacheKey?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Responses `text.format` (json_schema / json_object), preserved for adapters whose
|
|
242
|
+
* upstream wire has an equivalent. The openai-chat adapter re-nests it as chat
|
|
243
|
+
* `response_format`, the exact inverse of responseFormatToText in src/chat/inbound.ts.
|
|
244
|
+
* The native passthrough ignores it (it forwards `_rawBody.text` verbatim) and Kiro
|
|
245
|
+
* keeps rejecting structured output via `_structuredOutput`.
|
|
246
|
+
*/
|
|
247
|
+
textFormat?: {
|
|
248
|
+
type: "json_schema" | "json_object";
|
|
249
|
+
name?: string;
|
|
250
|
+
description?: string;
|
|
251
|
+
schema?: Record<string, unknown>;
|
|
252
|
+
strict?: boolean;
|
|
253
|
+
};
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
export type OcxMessagePhase = "commentary" | "final_answer";
|
|
@@ -553,6 +571,8 @@ export interface OcxClientIntegrationsConfig {
|
|
|
553
571
|
codex?: boolean;
|
|
554
572
|
/** Durable desired state for Grok Build. MISSING MEANS ON. */
|
|
555
573
|
grok?: boolean;
|
|
574
|
+
/** Durable desired state for Claude Desktop. MISSING MEANS ON. */
|
|
575
|
+
"claude-desktop"?: boolean;
|
|
556
576
|
}
|
|
557
577
|
|
|
558
578
|
export interface OcxConfig {
|
|
@@ -708,6 +728,30 @@ export interface OcxConfig {
|
|
|
708
728
|
contextCapValue?: number;
|
|
709
729
|
/** Bind hostname. Default "127.0.0.1" (loopback only). Set "0.0.0.0" to expose on all interfaces. */
|
|
710
730
|
hostname?: string;
|
|
731
|
+
/**
|
|
732
|
+
* Optional second listener bound to 127.0.0.1 that admits data-plane requests without a
|
|
733
|
+
* credential (issue #1102).
|
|
734
|
+
*
|
|
735
|
+
* Why a separate listener rather than an exemption on the main one: when `hostname` is a
|
|
736
|
+
* wildcard, every caller needs `x-opencodex-api-key`, but a `codex app-server` spawned
|
|
737
|
+
* directly from the resolved entrypoint never goes through the generated shim and so never
|
|
738
|
+
* inherits the token. Exempting "loopback-looking peers" on the public listener would be
|
|
739
|
+
* unsound — `requestIP()` only proves the last transport hop, and Docker Desktop port
|
|
740
|
+
* forwarding, host-network containers, WSL mirrored networking and tunnels all terminate
|
|
741
|
+
* remote connections locally. Binding a second socket to 127.0.0.1 makes the kernel refuse
|
|
742
|
+
* remote connections outright, so there is no address to judge.
|
|
743
|
+
*
|
|
744
|
+
* The public listener's admission policy is unchanged. This adds an explicit local trust
|
|
745
|
+
* surface: every process on the machine can reach it, spend account quota, and consume paid
|
|
746
|
+
* provider credentials. Off by default; not for multi-tenant hosts.
|
|
747
|
+
*
|
|
748
|
+
* The port is required when enabled and must differ from the proxy port. An OS-assigned port
|
|
749
|
+
* would change across restarts, which would break already-running app-servers holding the
|
|
750
|
+
* previous `base_url` — the exact symptom #1102 reported and we disproved for token rotation.
|
|
751
|
+
*/
|
|
752
|
+
unauthenticatedLoopbackListener?:
|
|
753
|
+
| { enabled: false }
|
|
754
|
+
| { enabled: true; port: number };
|
|
711
755
|
/**
|
|
712
756
|
* Outbound HTTP(S) proxy URL for provider requests (e.g. "http://user:pass@proxy:8080", or
|
|
713
757
|
* "${HTTPS_PROXY}"-style env reference). Mirrored into HTTP_PROXY/HTTPS_PROXY at startup when
|
|
@@ -762,12 +806,29 @@ export interface OcxConfig {
|
|
|
762
806
|
codexAccounts?: CodexAccount[];
|
|
763
807
|
/** Account ids administratively excluded from future pool selection until resumed. */
|
|
764
808
|
pausedCodexAccountIds?: string[];
|
|
809
|
+
/**
|
|
810
|
+
* Selection order per account id, higher used earlier; absent = 0. Keyed by id
|
|
811
|
+
* rather than stored on `codexAccounts` rows so the Desktop login (`__main__`),
|
|
812
|
+
* which has no row, can be ordered too. Range -100..100.
|
|
813
|
+
*/
|
|
814
|
+
codexAccountPriorities?: Record<string, number>;
|
|
815
|
+
/**
|
|
816
|
+
* Account id the operator last selected by hand. Suppresses upward priority
|
|
817
|
+
* preemption until that account crosses the auto-switch threshold. Stores the
|
|
818
|
+
* id (not a flag) so a stale pin cannot outlive the selection it described.
|
|
819
|
+
*/
|
|
820
|
+
activeCodexAccountPinned?: string;
|
|
765
821
|
/**
|
|
766
822
|
* Public model-selector namespaces bound to one Codex account. Values are stored account ids;
|
|
767
823
|
* `"@main"` selects the Codex Desktop/main auth.json account. Account display aliases
|
|
768
824
|
* are intentionally separate from these selectors.
|
|
769
825
|
*/
|
|
770
826
|
codexAccountNamespaces?: Record<string, string>;
|
|
827
|
+
/**
|
|
828
|
+
* Picker visibility override for account-qualified native models. When omitted, a non-empty
|
|
829
|
+
* selector map remains visible for compatibility with hand-written configurations.
|
|
830
|
+
*/
|
|
831
|
+
codexAccountPickerEnabled?: boolean;
|
|
771
832
|
/** Active pool account id for next session. undefined = main (passthrough as-is). */
|
|
772
833
|
activeCodexAccountId?: string;
|
|
773
834
|
/** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
|
|
@@ -778,6 +839,11 @@ export interface OcxConfig {
|
|
|
778
839
|
accountPoolStickyLimit?: number;
|
|
779
840
|
/** Consecutive non-2xx upstream responses before switching future new threads. Default 3. 0 = disabled. */
|
|
780
841
|
upstreamFailoverThreshold?: number;
|
|
842
|
+
/**
|
|
843
|
+
* Opt-in provider-origin circuit threshold for proven pre-connection reachability failures.
|
|
844
|
+
* Default 0 (disabled); range 0..20. The circuit never counts timeouts or HTTP responses.
|
|
845
|
+
*/
|
|
846
|
+
upstreamHostCircuitThreshold?: number;
|
|
781
847
|
/**
|
|
782
848
|
* Opt-in Anthropic OAuth account pool (#294). Default OFF.
|
|
783
849
|
* Failover on 429 + sticky affinity; new sessions may pick lowest known 5h usage.
|
|
@@ -1139,9 +1205,9 @@ export interface OcxProviderConfig {
|
|
|
1139
1205
|
* full set so the user can pick). See devlog issue_052_provider-model-allowlist.
|
|
1140
1206
|
*/
|
|
1141
1207
|
selectedModels?: string[];
|
|
1142
|
-
/** Provider-wide
|
|
1208
|
+
/** Provider-wide fallback when context metadata is absent; otherwise caps the reported window. */
|
|
1143
1209
|
contextWindow?: number;
|
|
1144
|
-
/**
|
|
1210
|
+
/** Per-model fallback when context metadata is absent; otherwise caps the reported window. */
|
|
1145
1211
|
modelContextWindows?: Record<string, number>;
|
|
1146
1212
|
/** Model-specific Codex catalog input modalities, e.g. ["text"] or ["text", "image"]. */
|
|
1147
1213
|
modelInputModalities?: Record<string, string[]>;
|
package/src/update/index.ts
CHANGED
|
@@ -4,6 +4,10 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
6
|
import { npmInvocation } from "./npm-invocation.mjs";
|
|
7
|
+
import {
|
|
8
|
+
npmCachePreflightFailureMessage,
|
|
9
|
+
runNpmCachePreflight,
|
|
10
|
+
} from "./npm-cache-preflight.mjs";
|
|
7
11
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
8
12
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
9
13
|
|
|
@@ -178,6 +182,14 @@ export async function runUpdate(): Promise<void> {
|
|
|
178
182
|
console.log(`Verified ${PKG}@${latest} integrity metadata ${integrity.integrity.slice(0, 24)}…`);
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
if (installer === "npm") {
|
|
186
|
+
const cachePreflight = runNpmCachePreflight();
|
|
187
|
+
if (!cachePreflight.ok) {
|
|
188
|
+
console.error(`⚠️ ${npmCachePreflightFailureMessage(cachePreflight.reason)}. Aborting before stopping the proxy.`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
182
194
|
const target = updateSpawnTarget(bin, cmdArgs);
|
|
183
195
|
if (!target) {
|