@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
package/src/cli/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { serviceStayOutExitCode } from "../service/windows-wrapper-exit";
|
|
2
3
|
import { spawn } from "node:child_process";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -83,10 +84,12 @@ import { requestBoundSystemRestart } from "./system-restart-client";
|
|
|
83
84
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
84
85
|
import { SpendLedgerOwnerError } from "../lib/spend-ledger-owner";
|
|
85
86
|
import { redactUrlForLog } from "../lib/redact";
|
|
86
|
-
import { dispatchCommand, decideBusyPreferredPort, decideStartWithLiveOwner } from "./dispatch";
|
|
87
|
+
import { dispatchCommand, decideBusyPreferredPort, decideStartExitTeardown, decideStartWithLiveOwner, startupLeftCodexNativeLine } from "./dispatch";
|
|
88
|
+
import { probeOwnerPastRestartParent, takeRestartHandoffMarkers } from "./restart-handoff";
|
|
87
89
|
import { AuxiliaryListenerBindError, findAvailablePort, isAddrInUse, PortUnavailableError, shouldPersistSelectedPort, waitForPortAvailable } from "../server/ports";
|
|
88
90
|
import {
|
|
89
91
|
findLiveProxy,
|
|
92
|
+
proveLiveProxyOwnedByHome,
|
|
90
93
|
probeEndpointLiveness,
|
|
91
94
|
probeHostname,
|
|
92
95
|
probePortOwner,
|
|
@@ -127,13 +130,9 @@ import {
|
|
|
127
130
|
StartOwnershipRollbackUncertainError,
|
|
128
131
|
} from "./start-ownership-publication";
|
|
129
132
|
import { syncModelsToCodex } from "../codex/sync";
|
|
130
|
-
import {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
shouldSyncGrokOnStart,
|
|
134
|
-
syncCodexOnStartIfEnabled,
|
|
135
|
-
type LocalClientSkipReason,
|
|
136
|
-
} from "../codex/desired-state";
|
|
133
|
+
import { localClientSkipReason, shouldSyncGrokOnStart, syncCodexOnStartIfEnabled } from "../codex/desired-state";
|
|
134
|
+
import { honorSiblingMarker, markSiblingStart, siblingOfLivePort, siblingRuntimeField, siblingStopFoundOwner, withoutSiblingMarker } from "../codex/sibling-start";
|
|
135
|
+
import { consumeSiblingHandoff } from "../codex/sibling-handoff";
|
|
137
136
|
import {
|
|
138
137
|
reconcileClientStartupBeforeReady,
|
|
139
138
|
syncClaudeAgentDefsAtProxyStartup,
|
|
@@ -232,19 +231,6 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
|
232
231
|
return null;
|
|
233
232
|
}
|
|
234
233
|
|
|
235
|
-
/**
|
|
236
|
-
* The one startup line for "nothing was written to Codex".
|
|
237
|
-
*
|
|
238
|
-
* Two very different facts reached it: the user's own OFF switch, and a hub declining to
|
|
239
|
-
* rewrite its own local clients. Printing the toggle's wording for the gate is what made
|
|
240
|
-
* operators hunt for a switch they never set (#4236).
|
|
241
|
-
*/
|
|
242
|
-
function startupLeftCodexNativeLine(reason: LocalClientSkipReason): string {
|
|
243
|
-
return reason === "hub-gated"
|
|
244
|
-
? ` ${HUB_GATED_SKIP_MESSAGE} Startup left Codex native.`
|
|
245
|
-
: " Codex integration OFF; startup left Codex native.";
|
|
246
|
-
}
|
|
247
|
-
|
|
248
234
|
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
249
235
|
function startArgv(port?: number): string[] {
|
|
250
236
|
const args = ["start"];
|
|
@@ -330,10 +316,9 @@ async function chooseListenPort(
|
|
|
330
316
|
ocxService: process.env.OCX_SERVICE,
|
|
331
317
|
});
|
|
332
318
|
if (decision === "service-stay-out") {
|
|
333
|
-
//
|
|
334
|
-
// on a zero exit, and the port it was asked to serve is already served.
|
|
319
|
+
// Signal intentional stay-out to wrappers that support the exit protocol.
|
|
335
320
|
console.log(`Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}); service wrapper staying out of the way.`);
|
|
336
|
-
throw new StartCommandExit(
|
|
321
|
+
throw new StartCommandExit(serviceStayOutExitCode());
|
|
337
322
|
}
|
|
338
323
|
if (decision === "refuse-live-proxy") {
|
|
339
324
|
console.error(`⚠️ Proxy already running (PID ${holder?.pid ?? "unknown"}, port ${preferred}). Use 'ocx stop' first.`);
|
|
@@ -381,7 +366,8 @@ async function findProxyOwnerBeforeJournalRecovery(
|
|
|
381
366
|
// The probe established that the snapshotted owner is stale. Compare before
|
|
382
367
|
// deleting so a concurrent start that rewrote the PID file keeps its state.
|
|
383
368
|
removePidIfValueIs(pidSnapshot);
|
|
384
|
-
|
|
369
|
+
// A marked sibling's owner can be down mid-restart; its journal is still not ours to replay.
|
|
370
|
+
if (!currentExternalCodexModelProvider() && siblingOfLivePort() === null) {
|
|
385
371
|
const clientState = readClientConnectionState();
|
|
386
372
|
reconcileJournal(clientState.kind === "connected"
|
|
387
373
|
? { activeClientApiKeyId: clientState.value.apiKeyId }
|
|
@@ -427,42 +413,36 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
427
413
|
}
|
|
428
414
|
}
|
|
429
415
|
const requestedPort = startOpts.port;
|
|
430
|
-
//
|
|
431
|
-
//
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
|
|
438
|
-
let siblingStart = false;
|
|
416
|
+
// Probe the configured port even without state files: a fallback sibling can remove them,
|
|
417
|
+
// and an unprobed start could shadow the owner and reroute Codex to a short-lived port.
|
|
418
|
+
// Consume a sibling replacement's handoff before probing, even if its owner is momentarily down.
|
|
419
|
+
let siblingStart = honorSiblingMarker(process.env, consumeSiblingHandoff) !== null;
|
|
420
|
+
// A restart replacement waits out its draining parent instead of refusing it, and bounds its handoff log (restart-handoff.ts).
|
|
421
|
+
const restartParent = { restartParentPid: takeRestartHandoffMarkers(process.env), requestedPort, ocxService: process.env.OCX_SERVICE };
|
|
422
|
+
const owner = await probeOwnerPastRestartParent(() => findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true }), restartParent);
|
|
439
423
|
if (owner.live) {
|
|
440
424
|
// Rationale and the full decision table live on `decideStartWithLiveOwner`.
|
|
441
425
|
const decision = decideStartWithLiveOwner({
|
|
442
426
|
livePort: owner.live.port,
|
|
443
|
-
|
|
444
|
-
ocxService: process.env.OCX_SERVICE,
|
|
427
|
+
livePid: owner.live.pid, ...restartParent,
|
|
445
428
|
});
|
|
446
429
|
if (decision === "service-stay-out") {
|
|
447
|
-
//
|
|
448
|
-
// ANY source means the requested port is already served. Exit 0 so the wrapper's
|
|
449
|
-
// `if %ERRORLEVEL% NEQ 0` retry loop terminates instead of respawning every 5s
|
|
450
|
-
// against a listener it can never claim (observed as an endless
|
|
451
|
-
// "Proxy already running" service.log loop).
|
|
430
|
+
// A live owner is an intentional stay-out, not an unexpected child exit.
|
|
452
431
|
console.log(`Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}); service wrapper staying out of the way.`);
|
|
453
|
-
process.exit(
|
|
432
|
+
process.exit(serviceStayOutExitCode());
|
|
454
433
|
}
|
|
455
|
-
if (decision === "refuse") {
|
|
434
|
+
if (decision === "refuse" || decision === "await-parent") {
|
|
456
435
|
console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
|
|
457
436
|
process.exit(1);
|
|
458
437
|
}
|
|
459
|
-
// Sibling path.
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// to bake the service (
|
|
438
|
+
// Sibling path. The new instance takes over this home's ocx.pid / runtime-port.json while
|
|
439
|
+
// it runs, and nothing else: Codex, Grok, Claude and the system env keep pointing at the
|
|
440
|
+
// live proxy for this process's whole lifetime, its exit and `ocx stop` included, so the
|
|
441
|
+
// mark goes down before any client write (`src/codex/sibling-start.ts`). Nor may it persist
|
|
442
|
+
// its port into config.port: the configured-port proxy still owns this home, and a later
|
|
443
|
+
// `ocx service` reads config.port to bake the service (a probe on 10198 pinned it there).
|
|
465
444
|
siblingStart = true;
|
|
445
|
+
markSiblingStart(owner.live.port);
|
|
466
446
|
console.warn(
|
|
467
447
|
`Proxy already running on port ${owner.live.port}; requested a second instance on port ${requestedPort}. `
|
|
468
448
|
+ `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
|
|
@@ -485,8 +465,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
485
465
|
|
|
486
466
|
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
487
467
|
// PID: choosing "Update now" installs globally and exits, so we never want a
|
|
488
|
-
// live daemon holding resources while it overwrites its own binary.
|
|
489
|
-
|
|
468
|
+
// live daemon holding resources while it overwrites its own binary. Never from a
|
|
469
|
+
// sibling: replacing the global package makes the live proxy drain and restart.
|
|
470
|
+
if (!siblingStart) await maybeShowUpdatePrompt();
|
|
490
471
|
|
|
491
472
|
type StartServerModule = typeof import("../server");
|
|
492
473
|
type BoundStart = {
|
|
@@ -508,18 +489,20 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
508
489
|
if (fencedLive) {
|
|
509
490
|
const decision = decideStartWithLiveOwner({
|
|
510
491
|
livePort: fencedLive.port,
|
|
492
|
+
livePid: fencedLive.pid, restartParentPid: restartParent.restartParentPid,
|
|
511
493
|
requestedPort,
|
|
512
494
|
ocxService: process.env.OCX_SERVICE,
|
|
513
495
|
});
|
|
514
496
|
if (decision === "service-stay-out") {
|
|
515
497
|
console.log(`Proxy already running (PID ${fencedLive.pid ?? "unknown"}, port ${fencedLive.port}); service wrapper staying out of the way.`);
|
|
516
|
-
throw new StartCommandExit(
|
|
498
|
+
throw new StartCommandExit(serviceStayOutExitCode());
|
|
517
499
|
}
|
|
518
|
-
if (decision === "refuse") {
|
|
500
|
+
if (decision === "refuse" || decision === "await-parent") {
|
|
519
501
|
console.error(`⚠️ Proxy appeared before bind (PID ${fencedLive.pid ?? "unknown"}, port ${fencedLive.port}). Use 'ocx stop' first.`);
|
|
520
502
|
throw new StartCommandExit(1);
|
|
521
503
|
}
|
|
522
504
|
siblingStart = true;
|
|
505
|
+
markSiblingStart(fencedLive.port);
|
|
523
506
|
}
|
|
524
507
|
|
|
525
508
|
// Port selection is check-then-bind. The lease prevents every cooperating start or
|
|
@@ -529,6 +512,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
529
512
|
const readinessGate = createReadinessGate();
|
|
530
513
|
const localAttestationSecret = createLocalAttestationSecret();
|
|
531
514
|
const config = loadConfig();
|
|
515
|
+
await (await import("../plugins/loader")).loadAndReportOcxPlugins();
|
|
532
516
|
let server: ReturnType<typeof serverModule.startServer>;
|
|
533
517
|
for (let attempt = 0; ; attempt++) {
|
|
534
518
|
try {
|
|
@@ -566,6 +550,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
566
550
|
port: bound.port,
|
|
567
551
|
hostname: bound.config.hostname,
|
|
568
552
|
attestationSecret: bound.localAttestationSecret,
|
|
553
|
+
...siblingRuntimeField(),
|
|
569
554
|
}),
|
|
570
555
|
stopBound: bound => bound.server.stop(true),
|
|
571
556
|
removeRuntime: () => removeRuntimePortIfPidIs(process.pid),
|
|
@@ -592,24 +577,25 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
592
577
|
// background — the first `ocx start` after an update usually races the Codex app's DB lock.
|
|
593
578
|
// Loopback-only (legacy mode still forward-tags) and respects syncResumeHistory opt-out.
|
|
594
579
|
let historyGuardian: ReturnType<typeof startHistoryMigrationGuardian> | undefined;
|
|
580
|
+
let routingHealer: { stop(): void } | undefined; // routing-healer.ts; stopped first in syncCleanup
|
|
595
581
|
|
|
596
582
|
let cleaned = false;
|
|
597
583
|
let cleanupSucceeded = true;
|
|
598
584
|
const syncCleanup = () => {
|
|
599
585
|
if (cleaned) return cleanupSucceeded;
|
|
600
586
|
cleaned = true;
|
|
587
|
+
try { routingHealer?.stop(); } catch { /* best-effort */ }
|
|
601
588
|
try { guardian.stop(); } catch { /* best-effort */ }
|
|
602
589
|
try { historyGuardian?.stop(); } catch { /* best-effort */ }
|
|
603
590
|
// Dashboard drain-and-restart (#563) must not tear down injection: the replacement
|
|
604
|
-
// process expects Codex/Grok/env fences to still be in place.
|
|
605
|
-
const
|
|
606
|
-
if (
|
|
591
|
+
// process expects Codex/Grok/env fences to still be in place. A sibling owns none of them.
|
|
592
|
+
const teardown = decideStartExitTeardown({ sibling: siblingStart, recycling: isRecyclingForExit(), ocxService: process.env.OCX_SERVICE });
|
|
593
|
+
if (teardown.revertSystemEnv) {
|
|
607
594
|
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
608
595
|
}
|
|
609
596
|
removePid(process.pid);
|
|
610
597
|
removeRuntimePort(process.pid);
|
|
611
|
-
|
|
612
|
-
if (!recycling && !preserveRouting && !currentExternalCodexModelProvider()) {
|
|
598
|
+
if (teardown.restoreNativeCodex && !currentExternalCodexModelProvider()) {
|
|
613
599
|
try {
|
|
614
600
|
const restored = restoreNativeCodex();
|
|
615
601
|
if (!restored.success) {
|
|
@@ -625,7 +611,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
625
611
|
// Grok fence is shared state we must not remove — that service keeps running and would be
|
|
626
612
|
// left pointing nowhere. This guard also covers signal-driven exits, which is the path that
|
|
627
613
|
// would otherwise bypass handleStop's gate entirely.
|
|
628
|
-
if (
|
|
614
|
+
if (teardown.stripGrokConfig && serviceEnvironmentOwnedHere()) {
|
|
629
615
|
try { stripGrokConfig(); } catch { /* best-effort restore */ }
|
|
630
616
|
}
|
|
631
617
|
return cleanupSucceeded;
|
|
@@ -672,8 +658,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
672
658
|
// syncCleanup reverts even if injection itself or subsequent startup steps fail).
|
|
673
659
|
const systemEnv = await injectSystemEnv(port, config).catch(() => ({ injected: false }));
|
|
674
660
|
// The hook is useful only for an installed Claude Code CLI. Reconcile instead of
|
|
675
|
-
// appending unconditionally so stale OpenCodex-owned hooks are removed as well.
|
|
676
|
-
|
|
661
|
+
// appending unconditionally so stale OpenCodex-owned hooks are removed as well. A sibling
|
|
662
|
+
// skips it: reconciling to "not injected" would uninstall the live owner's ~/.zshrc hook.
|
|
663
|
+
if (!siblingStart) reportShellHookFailure(reconcileShellHook(systemEnv.injected));
|
|
677
664
|
await maybeShowStarPrompt(); // once-only Yes/No GitHub-star prompt on first interactive start
|
|
678
665
|
// Codex sync owns the ready/failed verdict, but its successful transition is
|
|
679
666
|
// deferred until the best-effort Claude roster and Desktop registry settle. This
|
|
@@ -709,7 +696,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
709
696
|
}
|
|
710
697
|
},
|
|
711
698
|
);
|
|
712
|
-
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config)));
|
|
699
|
+
if (!startupSync.ran) console.log(startupLeftCodexNativeLine(localClientSkipReason(config), server.port ?? port));
|
|
713
700
|
await refreshOwnedRaycastCatalog(config, port);
|
|
714
701
|
// #1046: one warning per startup, after BOTH writes. The server's cache
|
|
715
702
|
// invalidation happens first and the catalog sync second, so the mtime is only
|
|
@@ -720,9 +707,11 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
720
707
|
const { warnIfStaleCodexAppServersAfterStartupWrite } = await import("../codex/app-server-processes");
|
|
721
708
|
warnIfStaleCodexAppServersAfterStartupWrite({ log: console });
|
|
722
709
|
}
|
|
723
|
-
if (!currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
710
|
+
if (!siblingStart && !currentExternalCodexModelProvider() && !shouldInjectApiAuthHeader(config) && config.syncResumeHistory !== false) {
|
|
724
711
|
historyGuardian = startHistoryMigrationGuardian();
|
|
725
712
|
}
|
|
713
|
+
const routingHealerModule = siblingStart ? null : await import("../codex/routing-healer");
|
|
714
|
+
if (routingHealerModule && !cleaned) routingHealer = routingHealerModule.startCodexRoutingHealer({ port, config }); // `cleaned` read once the import settled
|
|
726
715
|
// Grok Build auto-registration: additive fenced block in ~/.grok/config.toml so an installed
|
|
727
716
|
// grok CLI can pick opencodex-routed models without manual config. No-op when ~/.grok is
|
|
728
717
|
// absent or the bind is non-loopback; removed again by stop/eject/uninstall/shutdown.
|
|
@@ -751,9 +740,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
751
740
|
}
|
|
752
741
|
|
|
753
742
|
function detachedStartEnvironment(): NodeJS.ProcessEnv {
|
|
754
|
-
const env: NodeJS.ProcessEnv =
|
|
755
|
-
// Only a real service wrapper may claim supervision. A detached ensure/tray child
|
|
756
|
-
//
|
|
743
|
+
const env: NodeJS.ProcessEnv = withoutSiblingMarker(process.env);
|
|
744
|
+
// Only a real service wrapper may claim supervision. A detached ensure/tray child is an
|
|
745
|
+
// ordinary owner, never a sibling: while live it maintains routing, and on exit restores it.
|
|
757
746
|
delete env.OCX_SERVICE;
|
|
758
747
|
return withProcessRuntimeProvenance(env);
|
|
759
748
|
}
|
|
@@ -1100,6 +1089,11 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1100
1089
|
// service — the exact failure this flag prevents. A plain stop failure is different: we
|
|
1101
1090
|
// tried, so local teardown still proceeds.
|
|
1102
1091
|
let ownershipBlocked = false;
|
|
1092
|
+
// A sibling instance's shared teardown is not this stop's to run: the client routing belongs
|
|
1093
|
+
// to the live proxy it ran beside (`src/codex/sibling-start.ts`). Read before any stop removes
|
|
1094
|
+
// the record; a record left behind by a hard-killed sibling still says so.
|
|
1095
|
+
const siblingOfPort = readRuntimePort()?.siblingOfPort;
|
|
1096
|
+
const stoppingSibling = siblingOfPort !== undefined;
|
|
1103
1097
|
// Structured twin of the human lines below, for `ocx stop --json`: one document the
|
|
1104
1098
|
// desktop shell can read across the process boundary (D4). Every field is assigned
|
|
1105
1099
|
// where the corresponding boolean already flips — the summarizer never re-decides.
|
|
@@ -1137,7 +1131,7 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1137
1131
|
.filter(read => isPendingTeardownAbandoned(read, teardownOwnerStillRunning));
|
|
1138
1132
|
let teardownNonce: string | undefined;
|
|
1139
1133
|
const claimTeardown = (endpoint: { hostname: string; port: number }, endpointSource: "exact" | "guessed") => {
|
|
1140
|
-
if (teardownNonce) return;
|
|
1134
|
+
if (teardownNonce || stoppingSibling) return;
|
|
1141
1135
|
try {
|
|
1142
1136
|
teardownNonce = claimPendingTeardown(endpoint, endpointSource).nonce;
|
|
1143
1137
|
} catch (err) {
|
|
@@ -1155,8 +1149,8 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1155
1149
|
* silently reopening the parent-crash window on the path where the stop is a hard kill
|
|
1156
1150
|
* and no child teardown runs at all.
|
|
1157
1151
|
*
|
|
1158
|
-
* So the caller supplies
|
|
1159
|
-
*
|
|
1152
|
+
* So the caller supplies the endpoint it discovered after the orphan listener proves
|
|
1153
|
+
* possession of this home's runtime record, even when the pid file is gone.
|
|
1160
1154
|
*
|
|
1161
1155
|
* When nothing resolves, the graceful request cannot be made at all — `stopProxy` goes
|
|
1162
1156
|
* straight to the kill ladder, no child teardown runs, and there is no receipt to leave
|
|
@@ -1201,7 +1195,9 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1201
1195
|
settle: () => settleApprovedTarget(snapshot.approval),
|
|
1202
1196
|
managerState: () => observeGuardedManagerStopped(snapshot.manager),
|
|
1203
1197
|
})).service
|
|
1204
|
-
:
|
|
1198
|
+
// A sibling never runs under a service manager: an installed one belongs to the live owner,
|
|
1199
|
+
// and its ownership check would fail this stop and skip the stale-record purge below.
|
|
1200
|
+
: stoppingSibling ? "absent" : stopServiceIfInstalledDetailed();
|
|
1205
1201
|
record.service = serviceStop;
|
|
1206
1202
|
stoppedService = serviceStop === "stopped" || serviceStop === "stopped-respawnable";
|
|
1207
1203
|
schedulerCanRespawn = serviceStop === "stopped-respawnable";
|
|
@@ -1288,14 +1284,22 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1288
1284
|
}
|
|
1289
1285
|
}
|
|
1290
1286
|
} else {
|
|
1291
|
-
// Snapshot
|
|
1292
|
-
// can write fresh records mid-probe, and the purge below must never delete those.
|
|
1287
|
+
// Snapshot stale state before probing; purge only exact values if another start races us.
|
|
1293
1288
|
const stalePidValue = readPidFileValue();
|
|
1294
1289
|
const staleRuntimePid = readRuntimePort()?.pid ?? null;
|
|
1295
1290
|
// Orphan recovery: a live proxy can outlive its pid file (crash, manual delete,
|
|
1296
1291
|
// corrupt file). Identity-checked liveness still finds it via the runtime record.
|
|
1297
1292
|
const live = await findLiveProxy({ acceptPackageTreeFenced: true });
|
|
1298
|
-
if (live
|
|
1293
|
+
if (siblingStopFoundOwner(siblingOfPort, live)) {
|
|
1294
|
+
record.proxy = "not-running";
|
|
1295
|
+
console.log(`The sibling instance is already gone; the proxy on port ${siblingOfPort} was left running.`);
|
|
1296
|
+
} else if (live?.pid && !(await proveLiveProxyOwnedByHome(live))) {
|
|
1297
|
+
stopFailed = true;
|
|
1298
|
+
ownershipBlocked = true;
|
|
1299
|
+
record.proxy = "ownership-refused";
|
|
1300
|
+
console.error(`❌ A proxy answers on port ${live.port}, but it has not proved it belongs to this home; refusing to stop it.`);
|
|
1301
|
+
console.error(" Skipping shared teardown (native Codex restore, Grok config) while that proxy is running.");
|
|
1302
|
+
} else if (live?.pid) {
|
|
1299
1303
|
try {
|
|
1300
1304
|
// The probe already found where it answers, and on this path the runtime record is
|
|
1301
1305
|
// typically what went missing in the first place.
|
|
@@ -1343,9 +1347,9 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1343
1347
|
}
|
|
1344
1348
|
}
|
|
1345
1349
|
}
|
|
1346
|
-
// Environment ownership is independent from service ownership.
|
|
1347
|
-
//
|
|
1348
|
-
try { revertSystemEnv(); } catch { /* best-effort */ }
|
|
1350
|
+
// Environment ownership is independent from service ownership. Roll back current-home
|
|
1351
|
+
// variables (the helper refuses foreign markers on its own) unless a sibling never set them.
|
|
1352
|
+
if (!stoppingSibling) { try { revertSystemEnv(); } catch { /* best-effort */ } }
|
|
1349
1353
|
// A stopped Windows scheduler is not a proven-down proxy. `killWindowsSchedulerWrappers`
|
|
1350
1354
|
// is explicitly best-effort and the `:loop` wrapper respawns its child after ~5s, so an
|
|
1351
1355
|
// immediate probe can see a dead interval and an update can start replacing files right
|
|
@@ -1432,8 +1436,9 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1432
1436
|
console.error(" The obligation is preserved; retry once the proxy is confirmed stopped.");
|
|
1433
1437
|
}
|
|
1434
1438
|
}
|
|
1435
|
-
if (nativeRestoreHandledByProxy) record.sharedTeardown = "performed-by-proxy";
|
|
1436
|
-
|
|
1439
|
+
if (nativeRestoreHandledByProxy && !stoppingSibling) record.sharedTeardown = "performed-by-proxy";
|
|
1440
|
+
if (stoppingSibling) console.log(`↩️ Client routing stays on the proxy at port ${siblingOfPort}; this sibling had no shared teardown to run.`);
|
|
1441
|
+
const restoreBlocked = ownershipBlocked || inheritedBlocks || nativeRestoreHandledByProxy || stoppingSibling;
|
|
1437
1442
|
if (!restoreBlocked) {
|
|
1438
1443
|
if (recoveredNonces.length > 0) {
|
|
1439
1444
|
// A previous deferred stop died before restoring, and the probe says its endpoint is
|
|
@@ -1974,7 +1979,7 @@ process.exit(await dispatchCommand(head, {
|
|
|
1974
1979
|
detached: true,
|
|
1975
1980
|
stdio: "ignore",
|
|
1976
1981
|
windowsHide: true,
|
|
1977
|
-
env: withProcessRuntimeProvenance(process.env),
|
|
1982
|
+
env: withProcessRuntimeProvenance(withoutSiblingMarker(process.env)),
|
|
1978
1983
|
});
|
|
1979
1984
|
child.unref();
|
|
1980
1985
|
},
|
package/src/cli/minimax.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { isLoopbackHostname } from "../server/auth-cors";
|
|
|
21
21
|
import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
|
|
22
22
|
import type { OcxConfig } from "../types";
|
|
23
23
|
import { opencodeProxyStartEnv } from "./opencode";
|
|
24
|
+
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
24
25
|
|
|
25
26
|
export interface MinimaxLaunchEnv {
|
|
26
27
|
[key: string]: string | undefined;
|
|
@@ -270,8 +271,9 @@ async function ensureProxy(config: OcxConfig): Promise<LiveProxy | null> {
|
|
|
270
271
|
stdio: "ignore",
|
|
271
272
|
windowsHide: true,
|
|
272
273
|
// Reuse the established service-token lookup so a detached start works
|
|
273
|
-
// when admission lives in the hardened token file rather than this shell.
|
|
274
|
-
|
|
274
|
+
// when admission lives in the hardened token file rather than this shell. An ordinary
|
|
275
|
+
// owner, so a stray sibling marker is dropped.
|
|
276
|
+
env: withProcessRuntimeProvenance(opencodeProxyStartEnv(withoutSiblingMarker(process.env)) as NodeJS.ProcessEnv),
|
|
275
277
|
});
|
|
276
278
|
child.on("error", () => { /* the bounded health poll reports failure */ });
|
|
277
279
|
child.unref();
|
package/src/cli/opencode.ts
CHANGED
|
@@ -50,6 +50,7 @@ import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-li
|
|
|
50
50
|
import type { OcxConfig } from "../types";
|
|
51
51
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
52
52
|
import { selfLaunchArgv } from "../lib/self-launch-argv";
|
|
53
|
+
import { withoutSiblingMarker } from "../codex/sibling-start";
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* The provider-block serializer, its constants, and the config-path helpers now live in
|
|
@@ -101,6 +102,8 @@ export interface OpencodeProxyModelRow {
|
|
|
101
102
|
displayName?: string;
|
|
102
103
|
displayNameSource?: "operator" | "provider" | "fallback";
|
|
103
104
|
contextWindow?: number;
|
|
105
|
+
/** Authoritative output limit (CatalogModel.maxOutputTokens); optional. */
|
|
106
|
+
maxOutputTokens?: number;
|
|
104
107
|
/** Declared input modalities from `/api/models`; carried into opencode model capabilities. */
|
|
105
108
|
inputModalities?: string[];
|
|
106
109
|
/** Declared effort ladder from `/api/models`; carried into opencode model variants. */
|
|
@@ -427,6 +430,7 @@ export function opencodeCatalogFromProxyRows(
|
|
|
427
430
|
provider: row.provider,
|
|
428
431
|
id: row.id,
|
|
429
432
|
contextWindow: row.contextWindow,
|
|
433
|
+
...(typeof row.maxOutputTokens === "number" ? { maxTokens: row.maxOutputTokens } : {}),
|
|
430
434
|
displayName: row.displayNameSource === "fallback" ? undefined : row.displayName,
|
|
431
435
|
...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
|
|
432
436
|
? { inputModalities: [...row.inputModalities] }
|
|
@@ -641,7 +645,8 @@ async function ensureProxyForOpencode(config: OcxConfig): Promise<LiveProxy | nu
|
|
|
641
645
|
detached: true,
|
|
642
646
|
stdio: "ignore",
|
|
643
647
|
windowsHide: true,
|
|
644
|
-
|
|
648
|
+
// An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
|
|
649
|
+
env: withProcessRuntimeProvenance(opencodeProxyStartEnv(withoutSiblingMarker(process.env)) as NodeJS.ProcessEnv),
|
|
645
650
|
});
|
|
646
651
|
// Without a listener an 'error' (bad argv[1], EMFILE, AV denial) throws synchronously
|
|
647
652
|
// and kills this process; the health poll below already reports the failure properly.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The replacement side of a restart handoff: a start that finds its own draining parent.
|
|
3
|
+
*
|
|
4
|
+
* A dashboard drain-and-restart, a join into a Child and the standalone recycle all spawn a
|
|
5
|
+
* detached `ocx start` and hand it `OCX_RESTART_PARENT_PID` (`src/server/restart-replacement.ts`).
|
|
6
|
+
* The deadline and listener-stop-fallback handoffs spawn before the old listener is certainly
|
|
7
|
+
* gone, so the replacement's owner probe can still reach the parent. Refusing that "proxy already
|
|
8
|
+
* running" left no proxy at all a moment later, when the parent exited.
|
|
9
|
+
*
|
|
10
|
+
* So the owner probe runs through {@link probeOwnerPastRestartParent}: while the live owner is
|
|
11
|
+
* exactly the restart parent (`decideStartWithLiveOwner` answers `"await-parent"`), it waits for
|
|
12
|
+
* that process to exit, re-probing once a second in case the parent stops answering first, and
|
|
13
|
+
* then hands back a fresh probe. The wait is bounded; a parent that outlives it is refused like
|
|
14
|
+
* any other live proxy. Nothing here signals the parent, and the wait costs nothing on an ordinary
|
|
15
|
+
* start, which carries no marker.
|
|
16
|
+
*
|
|
17
|
+
* {@link takeRestartHandoffMarkers} also consumes the handoff-log flag: a replacement whose output
|
|
18
|
+
* its parent sent to `restart-handoff.log` bounds that file itself from then on
|
|
19
|
+
* (`armRestartHandoffLogCap` in `src/server/restart-replacement.ts`). It consumes the desktop app's
|
|
20
|
+
* supervision marker too, so a later restart exits to the app instead of spawning past it.
|
|
21
|
+
*/
|
|
22
|
+
import { isProcessAlive } from "../lib/process-control";
|
|
23
|
+
import { takeDesktopSupervisedMarker, takeRestartParentMarker } from "../lib/system-restart-contract";
|
|
24
|
+
import { armRestartHandoffLogCap, type RestartHandoffLogCapIo } from "../server/restart-replacement";
|
|
25
|
+
import { decideStartWithLiveOwner } from "./dispatch";
|
|
26
|
+
|
|
27
|
+
export { takeRestartParentMarker };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Consume everything a parent handed this start, before its first probe: arm the handoff log's cap
|
|
31
|
+
* when the flag is set, record the desktop app's supervision, and return the restart-parent pid,
|
|
32
|
+
* honored only for this process's real parent. Every marker is removed from `env`, so no later child
|
|
33
|
+
* inherits one.
|
|
34
|
+
*/
|
|
35
|
+
export function takeRestartHandoffMarkers(
|
|
36
|
+
env: Record<string, string | undefined>,
|
|
37
|
+
logCap: RestartHandoffLogCapIo = {},
|
|
38
|
+
): number | null {
|
|
39
|
+
armRestartHandoffLogCap(env, logCap);
|
|
40
|
+
takeDesktopSupervisedMarker(env);
|
|
41
|
+
return takeRestartParentMarker(env);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** How long a replacement waits for its own draining parent before refusing it. */
|
|
45
|
+
export const RESTART_PARENT_EXIT_TIMEOUT_MS = 30_000;
|
|
46
|
+
const PARENT_EXIT_POLL_MS = 100;
|
|
47
|
+
const OWNER_REPROBE_MS = 1_000;
|
|
48
|
+
|
|
49
|
+
export interface OwnerProbeResult {
|
|
50
|
+
live: { pid: number | null; port: number } | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface RestartParentOwnerInput {
|
|
54
|
+
restartParentPid: number | null;
|
|
55
|
+
requestedPort: number | undefined;
|
|
56
|
+
ocxService: string | undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RestartParentWaitIo {
|
|
60
|
+
isAlive?: (pid: number) => boolean;
|
|
61
|
+
now?: () => number;
|
|
62
|
+
sleep?: (ms: number) => Promise<void>;
|
|
63
|
+
timeoutMs?: number;
|
|
64
|
+
log?: (line: string) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isRestartParent(owner: OwnerProbeResult, input: RestartParentOwnerInput): boolean {
|
|
68
|
+
if (owner.live === null) return false;
|
|
69
|
+
return decideStartWithLiveOwner({
|
|
70
|
+
livePort: owner.live.port,
|
|
71
|
+
livePid: owner.live.pid,
|
|
72
|
+
...input,
|
|
73
|
+
}) === "await-parent";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run the start's owner probe, waiting out this process's own restart parent when that is the
|
|
78
|
+
* owner it found. Returns the last probe result: after the parent exited or stopped answering,
|
|
79
|
+
* a fresh one; after the timeout, the one that still names the parent, which the caller refuses.
|
|
80
|
+
*/
|
|
81
|
+
export async function probeOwnerPastRestartParent<T extends OwnerProbeResult>(
|
|
82
|
+
probe: () => Promise<T>,
|
|
83
|
+
input: RestartParentOwnerInput,
|
|
84
|
+
io: RestartParentWaitIo = {},
|
|
85
|
+
): Promise<T> {
|
|
86
|
+
let owner = await probe();
|
|
87
|
+
if (input.restartParentPid === null || !isRestartParent(owner, input)) return owner;
|
|
88
|
+
const parentPid = input.restartParentPid;
|
|
89
|
+
const isAlive = io.isAlive ?? isProcessAlive;
|
|
90
|
+
const now = io.now ?? Date.now;
|
|
91
|
+
const sleep = io.sleep ?? Bun.sleep;
|
|
92
|
+
const log = io.log ?? ((line: string) => console.log(line));
|
|
93
|
+
const timeoutMs = io.timeoutMs ?? RESTART_PARENT_EXIT_TIMEOUT_MS;
|
|
94
|
+
log(`Waiting for the restarting OpenCodex process (PID ${parentPid}) to hand over its port...`);
|
|
95
|
+
const deadline = now() + timeoutMs;
|
|
96
|
+
let reprobeAt = now() + OWNER_REPROBE_MS;
|
|
97
|
+
while (now() < deadline) {
|
|
98
|
+
if (!isAlive(parentPid)) return probe();
|
|
99
|
+
if (now() >= reprobeAt) {
|
|
100
|
+
owner = await probe();
|
|
101
|
+
if (!isRestartParent(owner, input)) return owner;
|
|
102
|
+
reprobeAt = now() + OWNER_REPROBE_MS;
|
|
103
|
+
}
|
|
104
|
+
const remainingMs = deadline - now();
|
|
105
|
+
if (remainingMs <= 0) break;
|
|
106
|
+
await sleep(Math.min(PARENT_EXIT_POLL_MS, remainingMs));
|
|
107
|
+
}
|
|
108
|
+
log(`The restarting OpenCodex process (PID ${parentPid}) did not exit within ${Math.round(timeoutMs / 1000)}s.`);
|
|
109
|
+
return owner;
|
|
110
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
|
18
18
|
import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home";
|
|
19
19
|
import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
|
|
20
20
|
import { effectiveLoopbackListenerPort } from "../codex/loopback-target";
|
|
21
|
+
import { journaledInjectedOpenaiBaseUrl, journaledInjectedRealtimeWsBaseUrl } from "../codex/journal";
|
|
22
|
+
import { detectCodexRoutingDrift, type JournaledCodexRouting } from "../codex/routing-drift";
|
|
21
23
|
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
22
24
|
import { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy, type ClaudeDesktopPolicyHealth } from "../claude/desktop-policy";
|
|
23
25
|
import { collectClientConnectionStatus, type ClientConnectionStatus } from "./connect";
|
|
@@ -547,6 +549,36 @@ export function detectMissingCodexCatalogPath(): string | null {
|
|
|
547
549
|
}
|
|
548
550
|
}
|
|
549
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Owned Codex routing that names a local port the live proxy does not serve.
|
|
554
|
+
*
|
|
555
|
+
* `classifyCodexRouting` calls that "opencodex-local" whatever the port, so without this line the
|
|
556
|
+
* state behind a killed second instance was invisible here. Detection is `routing-drift.ts`, the
|
|
557
|
+
* same rule the owner's healer uses; this only reports and reads the journal without side effects.
|
|
558
|
+
* A sibling's report says nothing: its routing correctly names the owner it runs beside. Whether a
|
|
559
|
+
* healer runs, or is gated or paused, is not visible from here, so the line promises only `ocx sync`.
|
|
560
|
+
*/
|
|
561
|
+
export function codexRoutingDriftWarning(input: {
|
|
562
|
+
content: string | null;
|
|
563
|
+
livePort: number | undefined;
|
|
564
|
+
loopbackPort: number | null;
|
|
565
|
+
/** `siblingOfPort` from this home's runtime record. */
|
|
566
|
+
siblingOfPort: number | undefined;
|
|
567
|
+
journaled: () => JournaledCodexRouting;
|
|
568
|
+
}): string | null {
|
|
569
|
+
const { content, livePort, loopbackPort } = input;
|
|
570
|
+
if (content === null || livePort === undefined || input.siblingOfPort !== undefined) return null;
|
|
571
|
+
const drift = detectCodexRoutingDrift(content, {
|
|
572
|
+
ownPorts: loopbackPort === null ? [livePort] : [livePort, loopbackPort],
|
|
573
|
+
journaled: input.journaled,
|
|
574
|
+
});
|
|
575
|
+
if (drift.kind !== "foreign") return null;
|
|
576
|
+
const ports = [...new Set(drift.targets.map(target => target.port))].join(", ");
|
|
577
|
+
return `Codex routing points at port ${ports}, but the proxy is on ${livePort}; if nothing serves that port, `
|
|
578
|
+
+ "new Codex threads fail with \"Connection refused\". Run 'ocx sync' to re-point it now; a running "
|
|
579
|
+
+ "owner proxy may also do that on its own once the port stays dead.";
|
|
580
|
+
}
|
|
581
|
+
|
|
550
582
|
/**
|
|
551
583
|
* The one state in this report where Codex is broken independently of the proxy (#5261).
|
|
552
584
|
*
|
|
@@ -702,6 +734,25 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
702
734
|
+ `${grokDrift.livePort}; grok turns will retry against a closed port. Run 'ocx ensure' to repoint it.`,
|
|
703
735
|
);
|
|
704
736
|
}
|
|
737
|
+
const codexDrift = (() => {
|
|
738
|
+
if (!health.ok) return null;
|
|
739
|
+
try {
|
|
740
|
+
const siblingOfPort = readRuntimePort()?.siblingOfPort;
|
|
741
|
+
return codexRoutingDriftWarning({
|
|
742
|
+
content: siblingOfPort === undefined ? readFileSync(CODEX_CONFIG_PATH, "utf8") : null,
|
|
743
|
+
livePort: listen.port,
|
|
744
|
+
loopbackPort: effectiveLoopbackListenerPort(config, listen.port),
|
|
745
|
+
siblingOfPort,
|
|
746
|
+
journaled: () => ({
|
|
747
|
+
openaiBaseUrl: journaledInjectedOpenaiBaseUrl({ readOnly: true }),
|
|
748
|
+
realtimeWsBaseUrl: journaledInjectedRealtimeWsBaseUrl({ readOnly: true }),
|
|
749
|
+
}),
|
|
750
|
+
});
|
|
751
|
+
} catch {
|
|
752
|
+
return null; // an absent or unreadable config.toml must never break `ocx status`
|
|
753
|
+
}
|
|
754
|
+
})();
|
|
755
|
+
if (codexDrift) warningParts.push(codexDrift);
|
|
705
756
|
const codexRuntime = {
|
|
706
757
|
path: displayCodexRuntimePath(resolvedRuntime.runtime.command),
|
|
707
758
|
version: resolvedRuntime.runtime.version,
|