@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -32,6 +32,7 @@ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV } from "./lib/bun-runtime"
|
|
|
32
32
|
import { WINDOWS_WRAPPER_PROTOCOL_ENV, WINDOWS_WRAPPER_STAY_OUT_EXIT_CODE } from "./service/windows-wrapper-exit";
|
|
33
33
|
import { buildWindowsServiceScript, windowsTaskActionMatches } from "./service/windows-taskxml";
|
|
34
34
|
import { inspectServiceStateEvidence, serviceStatePathsForOpenCodexHome } from "./service/state";
|
|
35
|
+
import { parseSystemdUnitHomes } from "./service/systemd-env";
|
|
35
36
|
|
|
36
37
|
/** Short: this runs inside admission, and a slow answer is the same as none. */
|
|
37
38
|
export const SERVICE_PROBE_TIMEOUT_MS = 2_000;
|
|
@@ -238,21 +239,23 @@ function unknown(reason: string): ServiceManagerInstallation {
|
|
|
238
239
|
return { kind: "unknown", reason };
|
|
239
240
|
}
|
|
240
241
|
|
|
242
|
+
/** Decode the entities buildPlist's plistString applies. `&` goes last so a
|
|
243
|
+
* literal `&lt;` written by a double-escaped value stays `<`. */
|
|
244
|
+
function plistStringValue(raw: string): string {
|
|
245
|
+
return raw
|
|
246
|
+
.replace(/</g, "<")
|
|
247
|
+
.replace(/>/g, ">")
|
|
248
|
+
.replace(/"/g, "\"")
|
|
249
|
+
.replace(/'/g, "'")
|
|
250
|
+
.replace(/&/g, "&");
|
|
251
|
+
}
|
|
252
|
+
|
|
241
253
|
/** Pull `<key>NAME</key><string>VALUE</string>` out of a plist body. */
|
|
242
254
|
function plistEnvValue(body: string, key: string): string | null {
|
|
243
255
|
const match = body.match(
|
|
244
256
|
new RegExp(`<key>\\s*${key}\\s*</key>\\s*<string>([^<]*)</string>`),
|
|
245
257
|
);
|
|
246
|
-
return match ? match[1] : null;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/** Pull `Environment="NAME=VALUE"` (quoted or bare) out of a systemd unit. */
|
|
250
|
-
function unitEnvValue(body: string, key: string): string | null {
|
|
251
|
-
for (const line of body.split("\n")) {
|
|
252
|
-
const match = line.match(new RegExp(`^\\s*Environment=\\s*"?${key}=([^"\\n]*)"?\\s*$`));
|
|
253
|
-
if (match) return match[1];
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
258
|
+
return match ? plistStringValue(match[1]) : null;
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
/**
|
|
@@ -321,15 +324,14 @@ function inspectSystemdOffline(home: string): ServiceManagerInstallation {
|
|
|
321
324
|
} catch (error) {
|
|
322
325
|
return unknown(`the session bus is unreachable and the systemd unit could not be read: ${String(error)}`);
|
|
323
326
|
}
|
|
327
|
+
const parsed = parseSystemdUnitHomes(body);
|
|
328
|
+
if (parsed.kind === "invalid") return unknown("the systemd unit has unsupported or ambiguous environment assignments");
|
|
324
329
|
return {
|
|
325
330
|
kind: "present",
|
|
326
331
|
claims: [{
|
|
327
332
|
backend: "systemd",
|
|
328
333
|
definitionPath,
|
|
329
|
-
homes:
|
|
330
|
-
codexHome: unitEnvValue(body, "CODEX_HOME"),
|
|
331
|
-
opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
|
|
332
|
-
},
|
|
334
|
+
homes: parsed.homes,
|
|
333
335
|
registration: "absent",
|
|
334
336
|
}],
|
|
335
337
|
};
|
|
@@ -463,16 +465,14 @@ function inspectSystemd(deps: Required<Pick<ProbeDeps, "run" | "home">>): Servic
|
|
|
463
465
|
} catch (error) {
|
|
464
466
|
return unknown(`the systemd unit exists but could not be read: ${String(error)}`);
|
|
465
467
|
}
|
|
466
|
-
|
|
468
|
+
const parsed = parseSystemdUnitHomes(body);
|
|
469
|
+
if (parsed.kind === "invalid") return unknown("the systemd unit has unsupported or ambiguous environment assignments");
|
|
467
470
|
return {
|
|
468
471
|
kind: "present",
|
|
469
472
|
claims: [{
|
|
470
473
|
backend: "systemd",
|
|
471
474
|
definitionPath,
|
|
472
|
-
homes:
|
|
473
|
-
codexHome: unitEnvValue(body, "CODEX_HOME"),
|
|
474
|
-
opencodexHome: unitEnvValue(body, "OPENCODEX_HOME"),
|
|
475
|
-
},
|
|
475
|
+
homes: parsed.homes,
|
|
476
476
|
registration,
|
|
477
477
|
}],
|
|
478
478
|
};
|
|
@@ -801,6 +801,59 @@ function probeWinswRegistration(
|
|
|
801
801
|
return /\b1060\b/.test(text) ? "absent" : "unknown";
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
+
/**
|
|
805
|
+
* The `BINARY_PATH_NAME` value from `sc.exe qc` output, or null when the line is missing.
|
|
806
|
+
* `sc qc` field names are not localized; the value may be quoted and carry arguments.
|
|
807
|
+
*/
|
|
808
|
+
export function parseScQcBinaryPathName(output: string): string | null {
|
|
809
|
+
const match = /^[ \t]*BINARY_PATH_NAME[ \t]*:[ \t]*([^\r\n]*)$/m.exec(output);
|
|
810
|
+
const value = match?.[1]?.trim();
|
|
811
|
+
return value ? value : null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function normalizeWindowsExecutablePath(path: string): string {
|
|
815
|
+
const stripped = path.startsWith("\\\\?\\") ? path.slice(4) : path;
|
|
816
|
+
return win32Path.normalize(stripped).toLowerCase();
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Whether an SCM `BINARY_PATH_NAME` launches exactly `exePath`. A quoted value names the
|
|
821
|
+
* text between its quotes; an unquoted value names the text through the first `.exe`
|
|
822
|
+
* followed by whitespace or the end. Comparison is Windows path-normalized and case-insensitive.
|
|
823
|
+
*/
|
|
824
|
+
export function scBinaryPathNamesExecutable(binaryPathName: string, exePath: string): boolean {
|
|
825
|
+
const value = binaryPathName.trim();
|
|
826
|
+
let executable: string | null;
|
|
827
|
+
if (value.startsWith("\"")) {
|
|
828
|
+
const end = value.indexOf("\"", 1);
|
|
829
|
+
executable = end > 1 ? value.slice(1, end) : null;
|
|
830
|
+
} else {
|
|
831
|
+
executable = /^(.*?\.exe)(?=\s|$)/i.exec(value)?.[1] ?? null;
|
|
832
|
+
}
|
|
833
|
+
if (!executable || !exePath.trim()) return false;
|
|
834
|
+
return normalizeWindowsExecutablePath(executable) === normalizeWindowsExecutablePath(exePath.trim());
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* The registered WinSW service's `BINARY_PATH_NAME` through trusted System32 `sc.exe qc`,
|
|
839
|
+
* bounded like every other probe query. Null on any failure; the WinSW executable is never run.
|
|
840
|
+
*/
|
|
841
|
+
export function queryWinswBinaryPathName(
|
|
842
|
+
deps: Pick<ProbeDeps, "runRaw" | "windowsLocale"> = {},
|
|
843
|
+
): string | null {
|
|
844
|
+
let sc: string;
|
|
845
|
+
try {
|
|
846
|
+
sc = join(resolveTrustedWindowsSystemDirectory(), "sc.exe");
|
|
847
|
+
if (artifactPresence(sc) !== "present") return null;
|
|
848
|
+
} catch {
|
|
849
|
+
return null;
|
|
850
|
+
}
|
|
851
|
+
const runRaw = deps.runRaw ?? defaultRawProbeRunner;
|
|
852
|
+
const queried = runRaw(sc, ["qc", WINSW_SERVICE_ID]);
|
|
853
|
+
if (queried.spawnFailed || queried.timedOut || queried.status !== 0) return null;
|
|
854
|
+
return parseScQcBinaryPathName(decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale }));
|
|
855
|
+
}
|
|
856
|
+
|
|
804
857
|
function inspectWindows(
|
|
805
858
|
deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
|
|
806
859
|
& Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale" | "windowsTaskListingCache" | "statePaths">,
|
package/src/types/config.ts
CHANGED
|
@@ -495,6 +495,8 @@ export interface OcxClientConnectionConfig {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
export interface OcxConfig {
|
|
498
|
+
/** Experimental macOS app-server stdout shim; absent or false disables it. */
|
|
499
|
+
chatgptDesktop?: { appServerShim?: boolean };
|
|
498
500
|
port: number;
|
|
499
501
|
/** Runtime topology role. Absence preserves the historical standalone behavior. */
|
|
500
502
|
runtimeRole?: OcxRuntimeRole;
|
|
@@ -576,13 +578,15 @@ export interface OcxConfig {
|
|
|
576
578
|
*/
|
|
577
579
|
ultraFastTier?: boolean;
|
|
578
580
|
/**
|
|
579
|
-
* Stop new identity-matched main-account requests at
|
|
581
|
+
* Stop new identity-matched main-account requests at configured window usage (#5694).
|
|
580
582
|
*
|
|
581
583
|
* On by default: an absent key and `true` both enable it, and only an explicit `false`
|
|
582
584
|
* opts out. While it blocks, the main account's Luna Reserve cannot activate, so an operator
|
|
583
585
|
* who wants Reserve has to turn the setting off rather than delete the key.
|
|
584
586
|
*/
|
|
585
587
|
codexMainAccountHardLock?: boolean;
|
|
588
|
+
/** Per-window percentages: short defaults to 90, long to 98; integers 80..100, short <= long. */
|
|
589
|
+
codexMainAccountHardLockThresholds?: { short?: number; long?: number };
|
|
586
590
|
/** Explicit top-level deletion intent used by stale whole-config rebases. */
|
|
587
591
|
configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
|
|
588
592
|
/** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
|
|
@@ -651,6 +655,11 @@ export interface OcxConfig {
|
|
|
651
655
|
* reject the whole role file as an unknown field (#1190).
|
|
652
656
|
*/
|
|
653
657
|
subagentModelFallbackByModel?: Record<string, string[]>;
|
|
658
|
+
/**
|
|
659
|
+
* Capability tier of named models for Codex role auto-assign, overriding its price-rank
|
|
660
|
+
* classification. Models no list names and no price covers are never proposed.
|
|
661
|
+
*/
|
|
662
|
+
codexRoleTiers?: { fast?: string[]; standard?: string[]; frontier?: string[] };
|
|
654
663
|
/**
|
|
655
664
|
* TTL (ms) for cached sub-agent model availability probes. Default 60_000.
|
|
656
665
|
*/
|
|
@@ -1015,6 +1024,12 @@ export interface OcxConfig {
|
|
|
1015
1024
|
codexAccounts?: CodexAccount[];
|
|
1016
1025
|
/** Account ids administratively excluded from future pool selection until resumed. */
|
|
1017
1026
|
pausedCodexAccountIds?: string[];
|
|
1027
|
+
/**
|
|
1028
|
+
* Account ids, `__main__` included, allowed to keep serving from ChatGPT credits once one of
|
|
1029
|
+
* their usage windows is full. Every other account is skipped by selection at 100% until its
|
|
1030
|
+
* window resets, so spending credits is opt-in.
|
|
1031
|
+
*/
|
|
1032
|
+
creditCodexAccountIds?: string[];
|
|
1018
1033
|
/**
|
|
1019
1034
|
* Codex pool selection policy. Absent means no policy, so an existing install rotates exactly
|
|
1020
1035
|
* as before.
|
|
@@ -1342,6 +1357,21 @@ export interface OcxComboConfig {
|
|
|
1342
1357
|
nativeAlias?: boolean;
|
|
1343
1358
|
/** Display-only label for the public catalog row. Required for native aliases. */
|
|
1344
1359
|
displayName?: string;
|
|
1360
|
+
/**
|
|
1361
|
+
* `strategy: "jev"` only: provider id of the decision service. Omitted or `"jev"` uses the
|
|
1362
|
+
* canonical TypeSafe endpoint; any other id must name a configured `adapter: "jev-decision"`
|
|
1363
|
+
* row, such as a self-hosted Ollama `tev1` endpoint. That row's baseUrl is the full decision
|
|
1364
|
+
* endpoint and only its own apiKey is sent there.
|
|
1365
|
+
*/
|
|
1366
|
+
decisionProvider?: string | null;
|
|
1367
|
+
/** JEV only: an ordinary model route for decisions; null explicitly clears management input. */
|
|
1368
|
+
decisionModel?: string | null;
|
|
1369
|
+
/**
|
|
1370
|
+
* `strategy: "jev"` only: decision deadline in milliseconds before failing open to the first
|
|
1371
|
+
* eligible target. Default 4000; range 1000..120000. Raise it for a self-hosted decision model
|
|
1372
|
+
* whose first call may include a cold model load.
|
|
1373
|
+
*/
|
|
1374
|
+
decisionTimeoutMs?: number;
|
|
1345
1375
|
}
|
|
1346
1376
|
|
|
1347
1377
|
export type OcxRoutingUnknownEvidenceMode = "allow" | "penalize" | "exclude";
|
package/src/types/provider.ts
CHANGED
|
@@ -274,6 +274,8 @@ export interface ModelCapabilities {
|
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
export interface OcxProviderConfig {
|
|
277
|
+
/** Optional browser-compatible outbound TLS profile; disabled by default. */
|
|
278
|
+
tlsProfile?: "antigravity-browser";
|
|
277
279
|
/** Optional short provider namespace used only at request/catalog presentation time. */
|
|
278
280
|
alias?: string;
|
|
279
281
|
/** Native model id -> short, slash-free request alias. */
|
package/src/types/request.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface OcxParsedRequest {
|
|
|
116
116
|
_cursorIsolateConversation?: boolean;
|
|
117
117
|
/** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
|
|
118
118
|
_kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
|
|
119
|
+
/** Account-scoped Zed user identity paired with the long-lived access token. */
|
|
120
|
+
_zedAuthContext?: { userId: string };
|
|
119
121
|
/** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
|
|
120
122
|
_providerContinuation?: OcxProviderContinuationState;
|
|
121
123
|
/** Persisted continuation considered only after the final physical route is known. */
|
package/src/update/index.ts
CHANGED
|
@@ -570,12 +570,23 @@ export async function runUpdate(): Promise<void> {
|
|
|
570
570
|
};
|
|
571
571
|
const recoverStoppedRuntime = async (reason: string): Promise<void> => {
|
|
572
572
|
try {
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
573
|
+
const planRecovery = async () => {
|
|
574
|
+
const current = await resolvedRuntimeOwnership();
|
|
575
|
+
return planStoppedRuntimeRecovery({
|
|
576
|
+
stopAttempted, ...current, sameOwner: sameOwner(current),
|
|
577
|
+
liveness: currentPackageRuntimeLiveness(), serviceInstalled: serviceWasInstalled,
|
|
578
|
+
launcherUsable: postUpdateLauncherUsable && existsSync(postUpdateLauncher), hadRuntimeState,
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
let recovery = await planRecovery();
|
|
582
|
+
if (recovery.action === "service") {
|
|
583
|
+
// The service manager starts the proxy outside this process tree, so it
|
|
584
|
+
// cannot join this lease, and holding it through the repair's health wait
|
|
585
|
+
// keeps that proxy from starting (#5760). Release it, then decide again —
|
|
586
|
+
// a claim could have landed while the repair ran unleased.
|
|
587
|
+
mutation.release();
|
|
588
|
+
recovery = await planRecovery();
|
|
589
|
+
}
|
|
579
590
|
if (recovery.action === "manual") {
|
|
580
591
|
console.warn(`⚠️ ${reason}; runtime recovery requires manual review (${recovery.reason}).`);
|
|
581
592
|
} else if (recovery.action === "service") {
|
|
@@ -585,6 +596,9 @@ export async function runUpdate(): Promise<void> {
|
|
|
585
596
|
env: mutation.controlEnvironment({ ...process.env, OCX_BAKE_PORT: String(capturedListen.port) }),
|
|
586
597
|
});
|
|
587
598
|
if (service.status !== 0 || !isServiceViable()) {
|
|
599
|
+
// The refresh ran unleased; a claim could have landed meanwhile, so take
|
|
600
|
+
// the lease back before re-reading ownership and authorizing a direct start.
|
|
601
|
+
mutation.reacquire();
|
|
588
602
|
const nowOwned = await resolvedRuntimeOwnership();
|
|
589
603
|
const fallback = planStoppedRuntimeRecovery({
|
|
590
604
|
stopAttempted, ...nowOwned, sameOwner: sameOwner(nowOwned),
|
|
@@ -595,6 +609,8 @@ export async function runUpdate(): Promise<void> {
|
|
|
595
609
|
else console.warn("⚠️ Service recovery was not confirmed; no second proxy was started.");
|
|
596
610
|
}
|
|
597
611
|
} else if (recovery.action === "direct") {
|
|
612
|
+
// No-op while the lease is held; re-locks if the service branch released it.
|
|
613
|
+
mutation.reacquire();
|
|
598
614
|
await startProxyDirectly();
|
|
599
615
|
}
|
|
600
616
|
} catch {
|
|
@@ -815,6 +831,11 @@ export async function runUpdate(): Promise<void> {
|
|
|
815
831
|
if (!freed) {
|
|
816
832
|
console.warn(`⚠️ Port ${capturedListen.port} still busy after 30s; repairing service with pinned --port ${capturedListen.port} anyway (refusing to hop).`);
|
|
817
833
|
}
|
|
834
|
+
// `service repair` re-activates a manager whose `ocx start` child is not a
|
|
835
|
+
// process descendant and cannot join this lease; held through its serving
|
|
836
|
+
// wait, the lease makes that child die at the acquire deadline (#5760).
|
|
837
|
+
// The kill-authorizing reclaim above already ran under the lease.
|
|
838
|
+
mutation.release();
|
|
818
839
|
const prevBake = process.env.OCX_BAKE_PORT;
|
|
819
840
|
process.env.OCX_BAKE_PORT = String(capturedListen.port);
|
|
820
841
|
try {
|
|
@@ -855,6 +876,18 @@ export async function runUpdate(): Promise<void> {
|
|
|
855
876
|
// Re-read rather than reuse the plan from before the package install: the app can
|
|
856
877
|
// claim the runtime during an update that takes minutes, and the refusal the
|
|
857
878
|
// repair above just returned is indistinguishable from any other failure here.
|
|
879
|
+
// The refresh above ran outside the lease — take it back so this re-read and
|
|
880
|
+
// the direct start stay serialized with a claim that landed in that window.
|
|
881
|
+
// A claim that outlasts the wait is reported here: the package is already
|
|
882
|
+
// swapped, so the unexpected-failure recovery below would run a second,
|
|
883
|
+
// unleased repair and surface the raw lock error instead.
|
|
884
|
+
try {
|
|
885
|
+
mutation.reacquire();
|
|
886
|
+
} catch {
|
|
887
|
+
console.warn("⚠️ Updated, but another process kept the runtime ownership lease claimed, so no proxy was started.");
|
|
888
|
+
console.warn(` Run 'ocx service repair', then 'ocx start --port ${capturedListen.port}'.`);
|
|
889
|
+
return 1;
|
|
890
|
+
}
|
|
858
891
|
const nowOwned = planUpdateRuntimeHandling({
|
|
859
892
|
...(await resolvedRuntimeOwnership()),
|
|
860
893
|
serviceInstalled: true,
|
package/src/update/job.ts
CHANGED
|
@@ -238,9 +238,8 @@ async function waitForGhostListenClear(
|
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
function packageLauncherPath(): string {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
// tree (`@bitkyc08/.opencodex-*`). Prefer the live package path when that happens.
|
|
241
|
+
// The launcher is <pkg-root>/bin/ocx.mjs. After `npm install -g`, import.meta.url
|
|
242
|
+
// can still point at npm's renamed temp tree — prefer the live package path then.
|
|
244
243
|
const fromMeta = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "bin", "ocx.mjs");
|
|
245
244
|
if (!/[\\/]\.opencodex-/i.test(fromMeta) && existsSync(fromMeta)) return fromMeta;
|
|
246
245
|
const live = fromMeta.replace(/[\\/]@bitkyc08[\\/]\.opencodex-[^\\/]+/i, `${sep}@bitkyc08${sep}opencodex`);
|
|
@@ -470,17 +469,16 @@ export function restartCommand(
|
|
|
470
469
|
const startArgs = pinPort
|
|
471
470
|
? [launcher, "start", "--port", String(Math.trunc(port))]
|
|
472
471
|
: [launcher, "start"];
|
|
473
|
-
// Default to
|
|
474
|
-
// Windows scheduler definition and re-registers only when the live definition is stale.
|
|
472
|
+
// Default to repair, which reuses a healthy registration and re-registers only a stale one.
|
|
475
473
|
const svcArgs = serviceInstalled ? [launcher, ...(serviceArgs ?? ["service", "repair"])] : startArgs;
|
|
476
474
|
if (usesNodeLauncher(installer)) {
|
|
477
475
|
const bin = nodeBin();
|
|
478
476
|
const args = svcArgs;
|
|
479
477
|
return { mode, bin, args, display: formatCommand(bin, args) };
|
|
480
478
|
}
|
|
481
|
-
// bun/source installs
|
|
482
|
-
// .exe files),
|
|
483
|
-
//
|
|
479
|
+
// bun/source installs restart via the runtime executable + package launcher (real
|
|
480
|
+
// .exe files), not `ocx.cmd` — shell-less `.cmd` spawn throws EINVAL on Windows
|
|
481
|
+
// (CVE-2024-27980 hardening), the same class the npm path avoids.
|
|
484
482
|
const bin = process.execPath;
|
|
485
483
|
const args = svcArgs;
|
|
486
484
|
return { mode, bin, args, display: formatCommand(bin, args) };
|
|
@@ -873,9 +871,8 @@ export function summarizeCommandOutput(
|
|
|
873
871
|
const parts: string[] = [];
|
|
874
872
|
parts.push(signal ? `terminated by ${signal}` : `exit ${status ?? "null"}`);
|
|
875
873
|
|
|
876
|
-
// Read npm's own named fields rather than reproducing its text
|
|
877
|
-
//
|
|
878
|
-
// a user exactly what happened, and none of it can be a local path.
|
|
874
|
+
// Read npm's own named fields rather than reproducing its text — `code: E404 ·
|
|
875
|
+
// 404: ...` tells a user exactly what happened and none of it can be a local path.
|
|
879
876
|
const fields = npmDiagnosticFields(`${stderr}\n${stdout}`);
|
|
880
877
|
if (fields.length > 0) parts.push(...fields);
|
|
881
878
|
|
|
@@ -1001,6 +998,13 @@ export interface RestartIo {
|
|
|
1001
998
|
* to a direct proxy start so dashboard updates never leave /healthz dead.
|
|
1002
999
|
*/
|
|
1003
1000
|
serviceViableFn?: () => boolean;
|
|
1001
|
+
/**
|
|
1002
|
+
* Free the ownership-mutation lease before a service-manager-mediated start that
|
|
1003
|
+
* cannot join it (#5760); wired by runUpdateRestartWithOwnershipLease.
|
|
1004
|
+
*/
|
|
1005
|
+
releaseForServiceManagerFn?: () => void;
|
|
1006
|
+
/** Re-acquire the lease for the direct-start fallthrough, then re-run the veto; a notice stops the restart. */
|
|
1007
|
+
reacquireForDirectStartFn?: () => { readonly notice: string; readonly failed: boolean } | null;
|
|
1004
1008
|
probeProxy?: (port: number, hostname?: string) => Promise<boolean>;
|
|
1005
1009
|
/** Richer /healthz read for update-correlated restart evidence (pid + version). */
|
|
1006
1010
|
probeProxyIdentity?: (port: number, hostname?: string) => Promise<RestartProxyIdentity | null>;
|
|
@@ -1031,7 +1035,7 @@ export interface RestartIo {
|
|
|
1031
1035
|
job: UpdateJobState,
|
|
1032
1036
|
captured?: { port: number; hostname: string; oldPid?: number },
|
|
1033
1037
|
io?: RestartIo,
|
|
1034
|
-
) => Promise<void>;
|
|
1038
|
+
) => Promise<false | void>;
|
|
1035
1039
|
/**
|
|
1036
1040
|
* PIDs currently LISTENing on the captured port. Used to widen the post-update
|
|
1037
1041
|
* kill allowlist beyond the pre-update PID (Windows often leaves a respawned
|
|
@@ -1055,9 +1059,8 @@ export interface RestartIo {
|
|
|
1055
1059
|
* window: being wrong here costs one extra start attempt; being wrong the other way
|
|
1056
1060
|
* leaves the user with no proxy at all.
|
|
1057
1061
|
*
|
|
1058
|
-
* It runs AFTER the child's own 20s install probe
|
|
1059
|
-
*
|
|
1060
|
-
* the fallback — inside RESTART_TIMEOUT_MS of 60s. That is why this is 25s, not more.
|
|
1062
|
+
* It runs AFTER the child's own 20s install probe, so a reinstall that exits 0 but
|
|
1063
|
+
* never serves spends up to 45s before the fallback — inside the 60s restart budget.
|
|
1061
1064
|
*/
|
|
1062
1065
|
export const SERVICE_RECOVERY_HEALTH_MS = 25_000;
|
|
1063
1066
|
|
|
@@ -1095,12 +1098,11 @@ async function restartAfterUpdate(
|
|
|
1095
1098
|
job: UpdateJobState,
|
|
1096
1099
|
captured?: { port: number; hostname: string; oldPid?: number },
|
|
1097
1100
|
io: RestartIo = {},
|
|
1098
|
-
): Promise<void> {
|
|
1101
|
+
): Promise<false | void> {
|
|
1099
1102
|
const serviceInstalled = (io.serviceInstalledFn ?? isServiceInstalled)();
|
|
1100
1103
|
const config = loadConfig();
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
// port to wait on; config is only the cold-start fallback.
|
|
1104
|
+
// Stop-first update already cleared pid/runtime state, so the pre-update capture is
|
|
1105
|
+
// the authoritative port to wait on; config is only the cold-start fallback.
|
|
1104
1106
|
const port = captured?.port ?? config.port ?? 10100;
|
|
1105
1107
|
const hostname = captured?.hostname ?? config.hostname ?? "127.0.0.1";
|
|
1106
1108
|
const oldPid = typeof captured?.oldPid === "number" && captured.oldPid > 0
|
|
@@ -1119,10 +1121,9 @@ async function restartAfterUpdate(
|
|
|
1119
1121
|
const listPids = io.listListenPidsFn ?? listListenPids;
|
|
1120
1122
|
const verifyOcx = io.verifyOcxFn ?? verifyPidIdentity;
|
|
1121
1123
|
const aliveFn = io.isAliveFn ?? isProcessAlive;
|
|
1122
|
-
// Pre-update PID plus any ocx still LISTENing on the captured port
|
|
1123
|
-
//
|
|
1124
|
-
//
|
|
1125
|
-
// the direct-start fallback never binds.
|
|
1124
|
+
// Pre-update PID plus any ocx still LISTENing on the captured port: a self-update
|
|
1125
|
+
// often leaves a respawned bun/node child that is not the captured PID, and
|
|
1126
|
+
// treating it as protected blocks reclaim so the direct start never binds.
|
|
1126
1127
|
const reclaimKillAllowlist = (): number[] => {
|
|
1127
1128
|
const allow = new Set<number>();
|
|
1128
1129
|
if (oldPid != null) allow.add(oldPid);
|
|
@@ -1137,30 +1138,24 @@ async function restartAfterUpdate(
|
|
|
1137
1138
|
intervalMs: 100,
|
|
1138
1139
|
scanIntervalMs: 500,
|
|
1139
1140
|
killOcxHolders: true,
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
// npm rename trees under `@bitkyc08/.opencodex-*` are classified as ocx by
|
|
1143
|
-
// isOcxStartCommandLine — never kill unknown foreign claimants on this port.
|
|
1141
|
+
// Scheduler wrappers mint new ocx PIDs during the wait — kill every ocx
|
|
1142
|
+
// listener, not only the pre-wait snapshot; foreign claimants stay protected.
|
|
1144
1143
|
killAllOcxOnPort: true,
|
|
1145
1144
|
onlyKillPids,
|
|
1146
1145
|
});
|
|
1147
1146
|
|
|
1148
1147
|
if (serviceInstalled) {
|
|
1149
1148
|
// schtasks /end often leaves the hidden cmd/wscript wrapper alive; its :loop
|
|
1150
|
-
// respawns `ocx start` a few seconds later and races port reclaim
|
|
1151
|
-
// task again and
|
|
1149
|
+
// respawns `ocx start` a few seconds later and races port reclaim, so end the
|
|
1150
|
+
// task again and kill those wrappers before touching the socket, then reclaim it
|
|
1151
|
+
// for wrappers that bake `--port`.
|
|
1152
1152
|
stopWindowsServiceWrappersBestEffort();
|
|
1153
|
-
// Stop-first update already unloaded the service; reclaim the socket, then
|
|
1154
|
-
// reinstall wrappers that bake `--port`.
|
|
1155
1153
|
const preServiceAllow = reclaimKillAllowlist();
|
|
1156
1154
|
const freed = await waitFn(port, hostname, reclaimOptsFor(preServiceAllow));
|
|
1157
1155
|
let skipServiceInstall = false;
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
//
|
|
1161
|
-
// applies and would leave the common dashboard update with stale service assets.
|
|
1162
|
-
//
|
|
1163
|
-
// Only a caller that still passes install argv keeps the old behavior.
|
|
1156
|
+
// `service repair` reuses the live task and refreshes a stale definition through
|
|
1157
|
+
// its guarded create/elevation path, so the old install-only skip no longer
|
|
1158
|
+
// applies; only a caller that still passes install argv keeps that behavior.
|
|
1164
1159
|
const refreshRegisters = (svcArgs ?? []).includes("install");
|
|
1165
1160
|
if ((io.platform ?? process.platform) === "win32" && process.env.OCX_SERVICE === "1" && refreshRegisters) {
|
|
1166
1161
|
updateJob(job, {}, "Skipping service re-registration from the non-elevated update worker; falling back to a direct proxy start.");
|
|
@@ -1183,13 +1178,17 @@ async function restartAfterUpdate(
|
|
|
1183
1178
|
? liveScan.pids.filter(pid => pid !== process.pid && aliveFn(pid))
|
|
1184
1179
|
: null;
|
|
1185
1180
|
if (liveAfter !== null && liveAfter.length === 0) {
|
|
1186
|
-
// Non-elevated `service install` will UAC-fail anyway;
|
|
1187
|
-
//
|
|
1181
|
+
// Non-elevated `service install` will UAC-fail anyway; go straight to the
|
|
1182
|
+
// direct-start fallthrough instead of burning another minute on it.
|
|
1188
1183
|
updateJob(job, {}, "Skipping service reinstall after reclaim timeout with no live holders; falling back to a direct proxy start.");
|
|
1189
1184
|
skipServiceInstall = true;
|
|
1190
1185
|
}
|
|
1191
1186
|
}
|
|
1192
1187
|
if (!skipServiceInstall) {
|
|
1188
|
+
// `service repair` re-activates a manager whose `ocx start` child is not a
|
|
1189
|
+
// process descendant and cannot join this lease; held through its serving
|
|
1190
|
+
// wait, the lease makes that child die at the acquire deadline (#5760).
|
|
1191
|
+
io.releaseForServiceManagerFn?.();
|
|
1193
1192
|
const prevBake = process.env.OCX_BAKE_PORT;
|
|
1194
1193
|
process.env.OCX_BAKE_PORT = String(Math.trunc(port));
|
|
1195
1194
|
let serviceOk = false;
|
|
@@ -1211,13 +1210,9 @@ async function restartAfterUpdate(
|
|
|
1211
1210
|
+ "Run 'ocx service status', then 'ocx service repair' by hand.",
|
|
1212
1211
|
);
|
|
1213
1212
|
}
|
|
1214
|
-
// The refresh that just failed was `ocx service repair`
|
|
1215
|
-
//
|
|
1216
|
-
//
|
|
1217
|
-
// send the user to re-registration — a UAC prompt on
|
|
1218
|
-
// Windows and a possible WinSW-to-scheduler backend switch — to fix a service
|
|
1219
|
-
// that is already registered. Point at the same command that failed so its
|
|
1220
|
-
// output explains why, on every platform.
|
|
1213
|
+
// The refresh that just failed was `ocx service repair`. Advising `install`
|
|
1214
|
+
// here would send the user to re-registration (UAC, possible WinSW backend
|
|
1215
|
+
// switch) for a service that is already registered; name the failed command.
|
|
1221
1216
|
updateJob(
|
|
1222
1217
|
job,
|
|
1223
1218
|
{},
|
|
@@ -1230,13 +1225,10 @@ async function restartAfterUpdate(
|
|
|
1230
1225
|
else process.env.OCX_BAKE_PORT = prevBake;
|
|
1231
1226
|
}
|
|
1232
1227
|
if (serviceOk) {
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
//
|
|
1237
|
-
// macOS/Linux, but Windows still reports success from registration alone, a
|
|
1238
|
-
// flapping supervisor can satisfy one probe, and the child may be an older CLI.
|
|
1239
|
-
// Ask the port before skipping the fallback this branch exists to protect.
|
|
1228
|
+
// Registration state cannot distinguish a serving supervisor from one that
|
|
1229
|
+
// registered and bound nothing — `launchctl list` reports both, `schtasks`
|
|
1230
|
+
// reports a task whose child exited immediately, a flapping supervisor can
|
|
1231
|
+
// satisfy one probe — so ask the port before skipping the fallback.
|
|
1240
1232
|
const viable = (io.serviceViableFn ?? isServiceViable)();
|
|
1241
1233
|
if (viable) {
|
|
1242
1234
|
if (await serviceRestartServed(job, port, hostname, io)) return;
|
|
@@ -1256,9 +1248,13 @@ async function restartAfterUpdate(
|
|
|
1256
1248
|
}
|
|
1257
1249
|
}
|
|
1258
1250
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// The service refresh ran outside the lease; take it back so the veto and the kills/start below stay serialized.
|
|
1254
|
+
const restartVeto = io.reacquireForDirectStartFn?.();
|
|
1255
|
+
if (restartVeto) {
|
|
1256
|
+
updateJob(job, restartVeto.failed ? { status: "failed", restarted: false, error: restartVeto.notice } : { status: "succeeded", restarted: false }, restartVeto.notice);
|
|
1257
|
+
return false;
|
|
1262
1258
|
}
|
|
1263
1259
|
|
|
1264
1260
|
const pid = readPid();
|
|
@@ -1272,9 +1268,8 @@ async function restartAfterUpdate(
|
|
|
1272
1268
|
}
|
|
1273
1269
|
}
|
|
1274
1270
|
if (serviceInstalled) stopWindowsServiceWrappersBestEffort();
|
|
1275
|
-
// Reclaim
|
|
1276
|
-
//
|
|
1277
|
-
// killAllOcxOnPort covers wrapper-respawned bun PIDs minted during the wait.
|
|
1271
|
+
// Reclaim before the pinned start: spawning `--port` while the old socket is busy
|
|
1272
|
+
// is how updates used to fail health checks or hop ports.
|
|
1278
1273
|
const directAllow = reclaimKillAllowlist();
|
|
1279
1274
|
const freed = await waitFn(port, hostname, reclaimOptsFor(directAllow));
|
|
1280
1275
|
if (!freed) {
|
|
@@ -1459,7 +1454,7 @@ export function restartAfterUpdateForTests(
|
|
|
1459
1454
|
job: UpdateJobState,
|
|
1460
1455
|
captured: { port: number; hostname: string; oldPid?: number },
|
|
1461
1456
|
io: RestartIo,
|
|
1462
|
-
): Promise<void> {
|
|
1457
|
+
): Promise<false | void> {
|
|
1463
1458
|
return restartAfterUpdate(job, captured, io);
|
|
1464
1459
|
}
|
|
1465
1460
|
|
|
@@ -1734,7 +1729,7 @@ export async function finishGuiUpdateRestart(
|
|
|
1734
1729
|
}
|
|
1735
1730
|
}
|
|
1736
1731
|
const restartFn = io.restartAfterUpdateFn ?? restartAfterUpdate;
|
|
1737
|
-
await restartFn(job, captured, io);
|
|
1732
|
+
if ((await restartFn(job, captured, io)) === false) return false;
|
|
1738
1733
|
if (!usesNodeLauncher(installer)) {
|
|
1739
1734
|
// Bun/source: health alone remains enough unless a richer identity probe is supplied.
|
|
1740
1735
|
if (!io.probeProxyIdentity) return confirmRestartedProxy(job, captured, io);
|
|
@@ -1975,9 +1970,14 @@ export async function runGuiUpdateWorker(
|
|
|
1975
1970
|
}
|
|
1976
1971
|
|
|
1977
1972
|
if (restart) {
|
|
1978
|
-
const outcome = await runUpdateRestartWithOwnershipLease(io.resolveOwnershipFn, async
|
|
1973
|
+
const outcome = await runUpdateRestartWithOwnershipLease(io.resolveOwnershipFn, async lease => {
|
|
1979
1974
|
job = updateJob(job!, { status: "restarting" }, "Update installed. Restarting proxy...");
|
|
1980
|
-
return finishGuiUpdateRestart(job!, captured, check.installer, {
|
|
1975
|
+
return finishGuiUpdateRestart(job!, captured, check.installer, {
|
|
1976
|
+
...io.restartIo,
|
|
1977
|
+
releaseForServiceManagerFn: lease.releaseForServiceManager,
|
|
1978
|
+
reacquireForDirectStartFn: lease.reacquireForDirectStart,
|
|
1979
|
+
packageLauncherPathFn: () => activeLauncher,
|
|
1980
|
+
});
|
|
1981
1981
|
});
|
|
1982
1982
|
if (outcome.kind === "veto") { updateJob(job, { status: "succeeded", restarted: false }, outcome.notice); return; }
|
|
1983
1983
|
if (!outcome.value) return;
|
|
@@ -5,10 +5,11 @@ export type NpmCachePreflightReason =
|
|
|
5
5
|
| "cache_entry_foreign_owner"
|
|
6
6
|
| "cache_entry_inaccessible"
|
|
7
7
|
| "cache_path_malformed"
|
|
8
|
+
| "cache_root_dangling_link"
|
|
9
|
+
| "cache_root_not_directory"
|
|
8
10
|
| "inspection_incomplete"
|
|
9
11
|
| "npm_config_failed"
|
|
10
12
|
| "npm_unavailable"
|
|
11
|
-
| "windows_skip"
|
|
12
13
|
| "worker_failed"
|
|
13
14
|
| "worker_output_malformed"
|
|
14
15
|
| "worker_timeout";
|
|
@@ -20,17 +21,23 @@ export interface NpmCachePreflightResult {
|
|
|
20
21
|
|
|
21
22
|
export interface NpmCacheInspectionOptions {
|
|
22
23
|
expectedUid?: number;
|
|
24
|
+
/** Test seam: lstat used by the cache-root check. Defaults to lstatSync. */
|
|
25
|
+
lstatFn?: (path: string) => { isSymbolicLink(): boolean; isDirectory(): boolean };
|
|
23
26
|
maxDepth?: number;
|
|
24
27
|
maxEntries?: number;
|
|
25
28
|
nowMs?: () => number;
|
|
26
29
|
/** Test seam: resolve a symlinked cache root. Defaults to realpathSync. */
|
|
27
30
|
realpathFn?: (path: string) => string;
|
|
31
|
+
/** Test seam: stat (following links) used by the cache-root check. Defaults to statSync. */
|
|
32
|
+
statFn?: (path: string) => { isDirectory(): boolean };
|
|
28
33
|
/** Test seam: resolve an entry's owner uid. Defaults to the lstat result. */
|
|
29
34
|
uidOf?: (path: string, stat: { uid: number }) => number;
|
|
30
35
|
timeoutMs?: number;
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
export interface NpmCachePreflightOptions {
|
|
39
|
+
/** Inspect this resolved cache root instead of letting the worker resolve npm's cache. */
|
|
40
|
+
cachePath?: string;
|
|
34
41
|
env?: NodeJS.ProcessEnv;
|
|
35
42
|
execPath?: string;
|
|
36
43
|
platform?: NodeJS.Platform;
|
|
@@ -38,6 +45,31 @@ export interface NpmCachePreflightOptions {
|
|
|
38
45
|
timeoutMs?: number;
|
|
39
46
|
}
|
|
40
47
|
|
|
48
|
+
export type NpmCachePathResolution =
|
|
49
|
+
| { ok: true; path: string }
|
|
50
|
+
| { ok: false; reason: "npm_unavailable" | "npm_config_failed" | "cache_path_malformed" };
|
|
51
|
+
|
|
52
|
+
export interface NpmCachePathOptions {
|
|
53
|
+
/** Working directory for `npm config get`; defaults to the home directory. */
|
|
54
|
+
cwd?: string;
|
|
55
|
+
env?: NodeJS.ProcessEnv;
|
|
56
|
+
invocationFn?: (
|
|
57
|
+
args: string[],
|
|
58
|
+
platform: NodeJS.Platform,
|
|
59
|
+
env: NodeJS.ProcessEnv,
|
|
60
|
+
) => { file: string; args: string[]; options?: Record<string, unknown> } | null;
|
|
61
|
+
platform?: NodeJS.Platform;
|
|
62
|
+
spawnSyncFn?: typeof spawnSync;
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function inspectNpmCacheRoot(
|
|
67
|
+
cachePath: string,
|
|
68
|
+
options?: Pick<NpmCacheInspectionOptions, "lstatFn" | "statFn">,
|
|
69
|
+
): NpmCachePreflightResult;
|
|
70
|
+
|
|
71
|
+
export function resolveNpmCachePath(options?: NpmCachePathOptions): NpmCachePathResolution;
|
|
72
|
+
|
|
41
73
|
export function inspectNpmCacheDirectory(
|
|
42
74
|
cachePath: string,
|
|
43
75
|
options?: NpmCacheInspectionOptions,
|