@bitkyc08/opencodex 2.69.0 → 2.70.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/gui/dist/assets/App-_MrbTrBe.js +51 -0
- package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
- package/gui/dist/assets/index-DKrO2eF3.js +86 -0
- package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +119 -0
- package/src/adapters/anthropic.ts +14 -83
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +542 -0
- package/src/adapters/command-code-tool-text.ts +99 -67
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
- package/src/adapters/devin/cloud-direct/chat.ts +78 -48
- package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
- package/src/adapters/devin/context-overflow.ts +75 -0
- package/src/adapters/devin/live-models.ts +174 -13
- package/src/adapters/devin/reasoning-signature.ts +100 -0
- package/src/adapters/devin.ts +310 -117
- package/src/adapters/kiro/payload.ts +10 -10
- package/src/adapters/kiro-images.ts +58 -7
- package/src/adapters/openai-chat/passthrough.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/bridge/response-json.ts +15 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/cli/account-extended.ts +31 -1
- package/src/cli/account.ts +3 -0
- package/src/cli/capabilities.ts +13 -0
- package/src/cli/client-start-fence.ts +36 -0
- package/src/cli/dispatch.ts +3 -2
- package/src/cli/index.ts +98 -117
- package/src/cli/provider-runtime.ts +23 -2
- package/src/cli/provider.ts +3 -0
- package/src/cli/resolve.ts +18 -0
- package/src/cli/start-owner-fence.ts +30 -0
- package/src/cli/stop-approval.ts +13 -1
- package/src/cli/stop-restore.ts +73 -0
- package/src/cli/tray-proxy.ts +221 -18
- package/src/cli/version-skew.ts +25 -23
- package/src/client/runtime.ts +8 -4
- package/src/codex/auth-api/account-list.ts +11 -5
- package/src/codex/auth-api/login-flow.ts +5 -3
- package/src/codex/auth-api/main-account-probe.ts +186 -110
- package/src/codex/auth-api/pool-mode-gate.ts +61 -7
- package/src/codex/auth-api/pool-quota-probe.ts +142 -90
- package/src/codex/auth-api/reset-credit-service.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +3 -3
- package/src/codex/catalog/provider-models.ts +2 -0
- package/src/codex/low-quota-events.ts +28 -0
- package/src/codex/low-quota-observer.ts +23 -0
- package/src/codex/low-quota-protection.ts +272 -0
- package/src/codex/quota-query-backoff.ts +148 -0
- package/src/codex/quota.ts +7 -2
- package/src/config/diagnostics.ts +10 -1
- package/src/config/load-degrade.ts +17 -0
- package/src/config/mutation-lock.ts +3 -4
- package/src/config/process-state.ts +1 -1
- package/src/config/provider-validation.ts +22 -0
- package/src/config/schema/blocked-model-redirects.ts +17 -0
- package/src/config/schema/config-schema.ts +4 -1
- package/src/config/schema/leaf-validators.ts +33 -2
- package/src/config/serving-runtimes.ts +412 -0
- package/src/generated/compatibility-version.json +211 -115
- package/src/generated/model-metadata.ts +3 -3
- package/src/images/loop.ts +3 -0
- package/src/lib/errors.ts +2 -1
- package/src/lib/handled-signal-exit.ts +23 -0
- package/src/lib/request-failure-model.ts +1 -0
- package/src/lib/self-launch-argv.ts +9 -0
- package/src/lib/shadow-call.ts +5 -2
- package/src/lib/strict-semver.ts +26 -0
- package/src/lib/transient-service-path.ts +16 -0
- package/src/lib/windows-service-wrappers.ts +10 -6
- package/src/lib/winsw.ts +3 -1
- package/src/oauth/anthropic-model-routes.ts +105 -0
- package/src/oauth/anthropic-routing.ts +85 -48
- package/src/oauth/devin.ts +135 -5
- package/src/oauth/index.ts +20 -3
- package/src/oauth/kiro-terminal-failover.ts +23 -9
- package/src/oauth/pool-settings-capability.ts +10 -2
- package/src/oauth/store.ts +1 -1
- package/src/protocols/encoders/adapter-events.ts +3 -1
- package/src/providers/github-copilot-context.ts +27 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/quota/account-cache.ts +8 -3
- package/src/providers/quota/devin.ts +286 -0
- package/src/providers/quota/vendor-probes-oauth.ts +6 -2
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +1 -1
- package/src/providers/registry/model-seeds.ts +5 -2
- package/src/responses/compaction-images.ts +27 -0
- package/src/router.ts +104 -16
- package/src/routing/analytics.ts +1 -0
- package/src/server/auth-cors.ts +9 -1
- package/src/server/background-lifecycle.ts +21 -10
- package/src/server/chat-completions.ts +2 -1
- package/src/server/chat-native.ts +1 -0
- package/src/server/index.ts +4 -8
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/management/context.ts +3 -0
- package/src/server/management/low-quota-routes.ts +14 -0
- package/src/server/management/oauth-account-routes.ts +21 -1
- package/src/server/management/provider-overwrite-carry.ts +79 -6
- package/src/server/management/provider-routes.ts +41 -28
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management-api.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +3 -0
- package/src/server/responses/adapter-delivery.ts +3 -0
- package/src/server/responses/adapter-dispatch.ts +20 -4
- package/src/server/responses/antigravity-validation-refusal.ts +20 -0
- package/src/server/responses/core-auth.ts +1 -0
- package/src/server/responses/core-codex-account.ts +1 -0
- package/src/server/responses/core-normalize.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +8 -1
- package/src/server/responses/request-sidecar-auth.ts +2 -0
- package/src/server/responses/request-transport.ts +27 -10
- package/src/server/responses/response-effects.ts +4 -1
- package/src/server/responses/run-turn-execution.ts +126 -42
- package/src/server/responses/sidecar-execution.ts +4 -1
- package/src/service/diagnostics.ts +3 -1
- package/src/service/guarded-manager-target.ts +39 -12
- package/src/service/launchd.ts +14 -6
- package/src/service/repair.ts +6 -1
- package/src/service/service-child-ownership.ts +168 -0
- package/src/service/state.ts +22 -4
- package/src/service/systemd.ts +2 -2
- package/src/service/windows-guarded-manager.ts +530 -0
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service.ts +2 -0
- package/src/types/config.ts +26 -0
- package/src/types/provider.ts +12 -0
- package/src/types/request.ts +2 -0
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/vision/anthropic-describe.ts +2 -1
- package/src/web-search/anthropic-executor.ts +2 -1
- package/src/web-search/loop.ts +1 -0
- package/gui/dist/assets/App-DDbgOTh6.js +0 -51
- package/gui/dist/assets/index-CuaBJkG4.js +0 -86
- package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
package/src/cli/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { serviceStayOutExitCode } from "../service/windows-wrapper-exit";
|
|
2
|
+
import { serviceStayOutExitCode, WINDOWS_WRAPPER_PROTOCOL_ENV } from "../service/windows-wrapper-exit";
|
|
3
|
+
import { isSupervisedServiceChild, serviceChildOwnershipDecisionForClassifiedChild } from "../service/service-child-ownership";
|
|
4
|
+
import { SERVICE_MANAGED_ENV } from "../service/state";
|
|
3
5
|
import { spawn } from "node:child_process";
|
|
4
6
|
import { homedir } from "node:os";
|
|
5
7
|
import { join } from "node:path";
|
|
8
|
+
import { currentServingCommand, deferServiceChildToNewerRuntime, markDelegatedServiceReady, recordServingRuntime } from "../config/serving-runtimes";
|
|
9
|
+
import { packageVersion } from "../lib/package-version";
|
|
6
10
|
import { findGuiDist } from "../server/gui-static";
|
|
7
11
|
import { inspectGuiBundleFreshness, staleGuiBundleLines } from "../server/gui-freshness";
|
|
8
12
|
|
|
@@ -74,11 +78,14 @@ import { parseStartOptions, StartArgsError } from "./start-args";
|
|
|
74
78
|
|
|
75
79
|
import {
|
|
76
80
|
discoverStableProxyForRestart,
|
|
77
|
-
|
|
81
|
+
recheckRestartFailedStart, reobserveRestartReplacement,
|
|
82
|
+
restartStartOutcome,
|
|
83
|
+
waitForProxyReplacement,
|
|
78
84
|
runProxyRestart,
|
|
79
85
|
runTrayProxyStart,
|
|
80
86
|
type ProxyRestartLive,
|
|
81
87
|
type ProxyRestartResult,
|
|
88
|
+
type ProxyRestartStartOutcome,
|
|
82
89
|
} from "./tray-proxy";
|
|
83
90
|
import { requestBoundSystemRestart } from "./system-restart-client";
|
|
84
91
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
@@ -146,8 +153,12 @@ import { loadExportModels } from "../server/management/model-rows";
|
|
|
146
153
|
|
|
147
154
|
import { removeOwnedConfigAfterDesktopCleanup } from "./uninstall-client-state";
|
|
148
155
|
import { withProcessRuntimeProvenance } from "../lib/bun-runtime";
|
|
149
|
-
import {
|
|
156
|
+
import { startArgv } from "../lib/self-launch-argv";
|
|
150
157
|
import { initializeNodeLauncherContext } from "./launcher-context";
|
|
158
|
+
import { restoreSharedClientStateAfterStop } from "./stop-restore";
|
|
159
|
+
import { startClientRuntimeUnderOwnershipLease } from "./client-start-fence";
|
|
160
|
+
import { recoverStartStateUnderOwnershipLease } from "./start-owner-fence";
|
|
161
|
+
import { handledSignalExitCode } from "../lib/handled-signal-exit";
|
|
151
162
|
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
152
163
|
import { MEMORY_DRAIN_RESTART_MS, REPLACEMENT_READY_TIMEOUT_MS } from "../lib/system-restart-contract";
|
|
153
164
|
|
|
@@ -231,15 +242,6 @@ async function waitForProxy(timeoutMs = 8_000): Promise<LiveProxy | null> {
|
|
|
231
242
|
return null;
|
|
232
243
|
}
|
|
233
244
|
|
|
234
|
-
/** Argv for detached `start`, optionally hard-pinning the listen port. */
|
|
235
|
-
function startArgv(port?: number): string[] {
|
|
236
|
-
const args = ["start"];
|
|
237
|
-
if (typeof port === "number" && Number.isFinite(port) && port > 0 && port <= 65535) {
|
|
238
|
-
args.push("--port", String(Math.trunc(port)));
|
|
239
|
-
}
|
|
240
|
-
return selfLaunchArgv(args);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
245
|
class StartCommandExit extends Error {
|
|
244
246
|
constructor(readonly exitCode: number) {
|
|
245
247
|
super(`start command exited with code ${exitCode}`);
|
|
@@ -350,24 +352,37 @@ async function chooseListenPort(
|
|
|
350
352
|
}
|
|
351
353
|
|
|
352
354
|
async function findProxyOwnerBeforeJournalRecovery(
|
|
353
|
-
options: { probeConfiguredPort?: boolean } = {},
|
|
355
|
+
options: { probeConfiguredPort?: boolean; deferPidCleanup?: boolean } = {},
|
|
354
356
|
): Promise<{ live: LiveProxy | null; pidSnapshot: number | null }> {
|
|
355
357
|
const pidSnapshot = readPidFileValue();
|
|
356
358
|
const hasRuntimeOwner = readRuntimePort() !== null;
|
|
357
359
|
const shouldProbe = pidSnapshot !== null || hasRuntimeOwner || options.probeConfiguredPort === true;
|
|
358
360
|
// A negative answer lets the caller walk past a proxy it was supposed to find and
|
|
359
|
-
// deletes this home's stale pid record.
|
|
361
|
+
// deletes this home's stale pid record. Supervised starts defer that write to the
|
|
362
|
+
// lease-held owner recheck; journal recovery follows cross-home discovery.
|
|
360
363
|
// One 750ms probe is not enough evidence for that (#5004) — a transport
|
|
361
364
|
// failure is indistinguishable from an empty port, and the reported Windows duplicate
|
|
362
365
|
// came from exactly that answer on a proxy the previous command had just found healthy.
|
|
363
366
|
const live = shouldProbe ? await findLiveProxy(START_OWNERSHIP_LIVENESS) : null;
|
|
364
367
|
if (live) return { live, pidSnapshot };
|
|
365
368
|
|
|
366
|
-
removePidIfValueIs(pidSnapshot);
|
|
369
|
+
if (!options.deferPidCleanup) removePidIfValueIs(pidSnapshot);
|
|
367
370
|
return { live: null, pidSnapshot };
|
|
368
371
|
}
|
|
369
372
|
|
|
370
373
|
async function handleStart(options: { block?: boolean } = {}) {
|
|
374
|
+
// A supervised service child defers to a foreign recorded owner before doing
|
|
375
|
+
// anything else. 'ocx service start' refuses this activation path already, but
|
|
376
|
+
// the process managers below it — the Windows boot wrapper's restart loop and
|
|
377
|
+
// the launchd/systemd units — spawn 'start' directly, which let an npm service
|
|
378
|
+
// resurrect beside a desktop-owned runtime. The stay-out exit is the wrapper's
|
|
379
|
+
// intentional-stop protocol, so a refusal does not read as a crash to respawn.
|
|
380
|
+
const supervisedServiceChild = isSupervisedServiceChild(process.env);
|
|
381
|
+
const childOwnership = serviceChildOwnershipDecisionForClassifiedChild(supervisedServiceChild);
|
|
382
|
+
if (childOwnership.kind === "stay-out") {
|
|
383
|
+
console.error("❌ " + childOwnership.refusal);
|
|
384
|
+
process.exit(serviceStayOutExitCode());
|
|
385
|
+
}
|
|
371
386
|
// Native (WinSW) service mode has no batch wrapper to read the service token file into
|
|
372
387
|
// the environment, and a FOREGROUND `ocx start` has no wrapper at all — so the app loads
|
|
373
388
|
// the token here, before the server binds, with the same precedence the launchd plist and
|
|
@@ -409,7 +424,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
409
424
|
let siblingStart = honorSiblingMarker(process.env, consumeSiblingHandoff) !== null;
|
|
410
425
|
// A restart replacement waits out its draining parent instead of refusing it, and bounds its handoff log (restart-handoff.ts).
|
|
411
426
|
const restartParent = { restartParentPid: takeRestartHandoffMarkers(process.env), requestedPort, ocxService: process.env.OCX_SERVICE };
|
|
412
|
-
const owner = await probeOwnerPastRestartParent(() => findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true }), restartParent);
|
|
427
|
+
const owner = await probeOwnerPastRestartParent(() => findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true, deferPidCleanup: supervisedServiceChild }), restartParent);
|
|
413
428
|
if (owner.live) {
|
|
414
429
|
// Rationale and the full decision table live on `decideStartWithLiveOwner`.
|
|
415
430
|
const decision = decideStartWithLiveOwner({
|
|
@@ -438,8 +453,18 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
438
453
|
+ `Startup continues only for an independent OPENCODEX_HOME; one state directory has one spend-ledger writer.`,
|
|
439
454
|
);
|
|
440
455
|
}
|
|
441
|
-
|
|
442
|
-
|
|
456
|
+
siblingStart = await recoverStartStateUnderOwnershipLease({
|
|
457
|
+
supervised: supervisedServiceChild,
|
|
458
|
+
acquireLease: () => acquireOwnershipMutationLease(serviceStatePaths()),
|
|
459
|
+
decide: () => serviceChildOwnershipDecisionForClassifiedChild(supervisedServiceChild),
|
|
460
|
+
stayOut: refusal => { console.error("❌ " + refusal); process.exit(serviceStayOutExitCode()); },
|
|
461
|
+
recover: async () => {
|
|
462
|
+
if (!owner.live && supervisedServiceChild) removePidIfValueIs(owner.pidSnapshot);
|
|
463
|
+
if (!owner.live && !siblingStart) siblingStart = await markCrossHomeSibling();
|
|
464
|
+
if (!siblingStart) reconcileStartupJournal();
|
|
465
|
+
return siblingStart;
|
|
466
|
+
},
|
|
467
|
+
});
|
|
443
468
|
|
|
444
469
|
const clientState = readClientConnectionState();
|
|
445
470
|
if (clientState.kind === "invalid" || clientState.kind === "mismatched") {
|
|
@@ -451,10 +476,20 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
451
476
|
throw new Error(`client startup refused: ${rotationGate.reason}`);
|
|
452
477
|
}
|
|
453
478
|
const { startClientRuntime } = await import("../client/runtime");
|
|
454
|
-
|
|
479
|
+
// Same lease-held owner recheck as the server path below: this branch binds and publishes too.
|
|
480
|
+
await startClientRuntimeUnderOwnershipLease({
|
|
481
|
+
acquireLease: () => acquireOwnershipMutationLease(serviceStatePaths()),
|
|
482
|
+
decide: () => serviceChildOwnershipDecisionForClassifiedChild(supervisedServiceChild),
|
|
483
|
+
stayOut: refusal => { console.error("❌ " + refusal); process.exit(serviceStayOutExitCode()); },
|
|
484
|
+
start: afterPublish => startClientRuntime({ port: requestedPort, block: options.block, afterPublish }),
|
|
485
|
+
});
|
|
455
486
|
return;
|
|
456
487
|
}
|
|
457
488
|
|
|
489
|
+
// Managed service children may defer before binding (serving-runtimes.ts).
|
|
490
|
+
const deferredExit = await deferServiceChildToNewerRuntime({ sibling: siblingStart, env: process.env, selfVersion: packageVersion(), selfCommand: currentServingCommand(), port: requestedPort });
|
|
491
|
+
if (deferredExit !== null) process.exit(deferredExit);
|
|
492
|
+
|
|
458
493
|
// Interactive-only update prompt. Must run BEFORE we bind a port / write a
|
|
459
494
|
// PID: choosing "Update now" installs globally and exits, so we never want a
|
|
460
495
|
// live daemon holding resources while it overwrites its own binary. Never from a
|
|
@@ -475,6 +510,11 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
475
510
|
boundStart = await bindAndPublishStartOwnership({
|
|
476
511
|
acquireLease: () => acquireOwnershipMutationLease(serviceStatePaths()),
|
|
477
512
|
bind: async () => {
|
|
513
|
+
const leasedChildOwnership = serviceChildOwnershipDecisionForClassifiedChild(supervisedServiceChild);
|
|
514
|
+
if (leasedChildOwnership.kind === "stay-out") {
|
|
515
|
+
console.error("❌ " + leasedChildOwnership.refusal);
|
|
516
|
+
throw new StartCommandExit(serviceStayOutExitCode());
|
|
517
|
+
}
|
|
478
518
|
// The earlier probe owned journal cleanup. This one owns the bind decision: an
|
|
479
519
|
// updater may have stopped the old runtime and acquired this lease for replacement.
|
|
480
520
|
const fencedLive = await findLiveProxy(START_OWNERSHIP_LIVENESS);
|
|
@@ -556,6 +596,9 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
556
596
|
|
|
557
597
|
const { server, serverModule, port, readinessGate, config } = boundStart;
|
|
558
598
|
const { drainAndShutdown, isRecyclingForExit, noteExplicitShutdownRequested } = serverModule;
|
|
599
|
+
markDelegatedServiceReady();
|
|
600
|
+
// Register the relaunch command the service-child deferral consults on a later takeover.
|
|
601
|
+
recordServingRuntime({ command: currentServingCommand(), version: packageVersion(), servedAt: new Date().toISOString() });
|
|
559
602
|
// Records are visible now; background work may observe this runtime without a gap.
|
|
560
603
|
scheduleCatalogPrewarm();
|
|
561
604
|
installCrashGuards();
|
|
@@ -617,7 +660,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
617
660
|
// this window as the same Ctrl-C (one graceful drain); a deliberate later press
|
|
618
661
|
// escalates to an immediate force-exit ("gradual kill").
|
|
619
662
|
const FORCE_AFTER_MS = 500;
|
|
620
|
-
const shutdown = () => {
|
|
663
|
+
const shutdown = (signal?: NodeJS.Signals) => {
|
|
621
664
|
const now = Date.now();
|
|
622
665
|
if (shuttingDown) {
|
|
623
666
|
if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
|
|
@@ -635,7 +678,7 @@ async function handleStart(options: { block?: boolean } = {}) {
|
|
|
635
678
|
shutdownSucceeded = await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
|
|
636
679
|
} finally {
|
|
637
680
|
const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
|
|
638
|
-
process.exit(restored && shutdownSucceeded ?
|
|
681
|
+
process.exit(restored && shutdownSucceeded ? handledSignalExitCode(signal) : 1);
|
|
639
682
|
}
|
|
640
683
|
})();
|
|
641
684
|
};
|
|
@@ -737,17 +780,22 @@ function detachedStartEnvironment(): NodeJS.ProcessEnv {
|
|
|
737
780
|
const env: NodeJS.ProcessEnv = withoutSiblingMarker(process.env);
|
|
738
781
|
// Only a real service wrapper may claim supervision. A detached ensure/tray child is an
|
|
739
782
|
// ordinary owner, never a sibling: while live it maintains routing, and on exit restores it.
|
|
783
|
+
// The supervisor markers ride along whenever this runs inside a service child's
|
|
784
|
+
// environment, so they must leave with OCX_SERVICE — otherwise the child would
|
|
785
|
+
// answer the ownership gate as a managed job it is not.
|
|
740
786
|
delete env.OCX_SERVICE;
|
|
787
|
+
delete env[SERVICE_MANAGED_ENV];
|
|
788
|
+
delete env[WINDOWS_WRAPPER_PROTOCOL_ENV];
|
|
741
789
|
return withProcessRuntimeProvenance(env);
|
|
742
790
|
}
|
|
743
791
|
|
|
744
|
-
async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Promise<boolean> {
|
|
792
|
+
async function handleEnsure(options: { existingIsSuccess?: boolean; forceStart?: boolean; onSpawn?: (child: ReturnType<typeof spawn>) => void } = {}): Promise<boolean> {
|
|
745
793
|
const owner = await findProxyOwnerBeforeJournalRecovery({ probeConfiguredPort: true });
|
|
746
794
|
if (!owner.live && !(await markCrossHomeSibling())) reconcileStartupJournal();
|
|
747
795
|
// A later ensure can find this home's sibling alive; the other port still owns shared clients.
|
|
748
796
|
if (owner.live) await markLiveHomeSibling(owner.live);
|
|
749
797
|
const config = loadConfig();
|
|
750
|
-
if (!codexAutoStartEnabled(config)) {
|
|
798
|
+
if (!options.forceStart && !codexAutoStartEnabled(config)) {
|
|
751
799
|
console.log("Codex autostart is disabled.");
|
|
752
800
|
return false;
|
|
753
801
|
}
|
|
@@ -790,6 +838,7 @@ async function handleEnsure(options: { existingIsSuccess?: boolean } = {}): Prom
|
|
|
790
838
|
windowsHide: true,
|
|
791
839
|
env: detachedStartEnvironment(),
|
|
792
840
|
});
|
|
841
|
+
options.onSpawn?.(child);
|
|
793
842
|
child.unref();
|
|
794
843
|
|
|
795
844
|
const port = (await waitForProxy())?.port;
|
|
@@ -861,24 +910,8 @@ async function handleTrayProxyStart(existingIsSuccess = true): Promise<boolean>
|
|
|
861
910
|
|
|
862
911
|
const PROXY_RESTART_OBSERVE_MS = MEMORY_DRAIN_RESTART_MS + REPLACEMENT_READY_TIMEOUT_MS + 15_000;
|
|
863
912
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
deadlineAt: number,
|
|
867
|
-
): Promise<ProxyRestartLive | null> {
|
|
868
|
-
while (Date.now() < deadlineAt) {
|
|
869
|
-
const live = await findLiveProxy({ deadlineAt });
|
|
870
|
-
if (Date.now() >= deadlineAt) return null;
|
|
871
|
-
// Modern /healthz publishes a PID. Require a different, identity-verified process;
|
|
872
|
-
// merely seeing the old port online again is not proof that restart completed.
|
|
873
|
-
if (isProxyReplacement(previous, live)) {
|
|
874
|
-
return live;
|
|
875
|
-
}
|
|
876
|
-
const remainingMs = deadlineAt - Date.now();
|
|
877
|
-
if (remainingMs > 0) await Bun.sleep(Math.min(250, remainingMs));
|
|
878
|
-
}
|
|
879
|
-
return null;
|
|
880
|
-
}
|
|
881
|
-
|
|
913
|
+
/** Reserve confirmation time within the shared restart deadline. */
|
|
914
|
+
const RESTART_REOBSERVE_RESERVE_MS = 10_000;
|
|
882
915
|
function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>): void {
|
|
883
916
|
if (result.phase === "identity") {
|
|
884
917
|
console.error("❌ Refusing to restart because the running proxy identity could not be attested.");
|
|
@@ -901,9 +934,8 @@ function reportRestartFailure(result: Extract<ProxyRestartResult, { ok: false }>
|
|
|
901
934
|
console.error("❌ Proxy was not running and the fallback start did not become healthy.");
|
|
902
935
|
}
|
|
903
936
|
}
|
|
904
|
-
|
|
905
937
|
async function handleProxyRestart(
|
|
906
|
-
startWhenStopped: () => Promise<
|
|
938
|
+
startWhenStopped: (recoveringLiveRestart: boolean) => Promise<ProxyRestartStartOutcome>,
|
|
907
939
|
): Promise<boolean> {
|
|
908
940
|
const deadlineAt = Date.now() + PROXY_RESTART_OBSERVE_MS;
|
|
909
941
|
const result = await runProxyRestart({
|
|
@@ -913,93 +945,42 @@ async function handleProxyRestart(
|
|
|
913
945
|
}),
|
|
914
946
|
startWhenStopped,
|
|
915
947
|
requestInPlaceRestart: previous => requestBoundSystemRestart(previous, deadlineAt),
|
|
916
|
-
waitForReplacement: previous => waitForProxyReplacement(previous, deadlineAt),
|
|
948
|
+
waitForReplacement: previous => waitForProxyReplacement(previous, deadlineAt - RESTART_REOBSERVE_RESERVE_MS, end => findLiveProxy({ deadlineAt: end })),
|
|
949
|
+
reobserveAfterReplacement: previous => reobserveRestartReplacement(previous, deadlineAt, end =>
|
|
950
|
+
discoverStableProxyForRestart({
|
|
951
|
+
findLive: () => findLiveProxy({ deadlineAt: end, attempts: 2, acceptPackageTreeFenced: true }),
|
|
952
|
+
waitBetweenChecks: () => Bun.sleep(250),
|
|
953
|
+
expired: () => Date.now() >= end,
|
|
954
|
+
})),
|
|
955
|
+
recheckAfterFailedStart: () => recheckRestartFailedStart(end => discoverStableProxyForRestart({
|
|
956
|
+
findLive: () => findLiveProxy({ deadlineAt: end, attempts: 2, acceptPackageTreeFenced: true }), expired: () => Date.now() >= end,
|
|
957
|
+
})),
|
|
917
958
|
});
|
|
918
959
|
if (!result.ok) reportRestartFailure(result);
|
|
919
960
|
process.exitCode = result.ok ? 0 : 1;
|
|
920
961
|
return result.ok;
|
|
921
962
|
}
|
|
922
|
-
|
|
923
963
|
async function handleTrayProxyRestart(): Promise<void> {
|
|
924
|
-
|
|
964
|
+
// A service start has no child handle here; its failed result is ambiguous.
|
|
965
|
+
await handleProxyRestart(async () => (await handleTrayProxyStart(false))
|
|
966
|
+
? { status: "started" }
|
|
967
|
+
: { status: "failed", launch: "unknown" });
|
|
925
968
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
if (!codexAutoStartEnabled(loadConfig())) {
|
|
969
|
+
async function handleRestartStartWhenStopped(recoveringLiveRestart = false): Promise<ProxyRestartStartOutcome> {
|
|
970
|
+
if (!recoveringLiveRestart && !codexAutoStartEnabled(loadConfig())) {
|
|
929
971
|
console.log("Codex autostart is disabled; no proxy was started.");
|
|
930
|
-
return "skipped";
|
|
972
|
+
return { status: "skipped" };
|
|
931
973
|
}
|
|
932
|
-
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
/**
|
|
936
|
-
* Restore shared client state after a stop.
|
|
937
|
-
*
|
|
938
|
-
* Returns the two failure kinds separately. `historyOnly` means teardown succeeded and
|
|
939
|
-
* only Codex history metadata could not be finalized: the proxy is down, the service is
|
|
940
|
-
* stopped, and a manifest is waiting for review. `other` means something that actually
|
|
941
|
-
* removes state a client depends on.
|
|
942
|
-
*
|
|
943
|
-
* The distinction exists because `ocx update` must proceed for the first and abort for the
|
|
944
|
-
* second, and it can only see an exit code (#3008).
|
|
945
|
-
*
|
|
946
|
-
* `historyDeferred` is the third kind (#4718). The Codex history preflight refuses BEFORE
|
|
947
|
-
* the config half runs, so nothing was restored at all: config, catalog, history and
|
|
948
|
-
* provenance are untouched and the client is still routed at the proxy that just stopped.
|
|
949
|
-
* Like `historyOnly` the proxy is genuinely down, so an update may replace package files.
|
|
950
|
-
* Unlike `historyOnly` the obligation was not performed, so the receipt must survive.
|
|
951
|
-
*/
|
|
952
|
-
async function restoreSharedClientStateAfterStop(): Promise<{ historyOnly: boolean; historyDeferred: boolean; other: boolean }> {
|
|
953
|
-
let historyOnly = false;
|
|
954
|
-
let historyDeferred = false;
|
|
955
|
-
let other = false;
|
|
974
|
+
let child: ReturnType<typeof spawn> | null = null;
|
|
956
975
|
try {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
console.log(`↩️ ${result.message}`);
|
|
960
|
-
if (result.retainedCodexProviderTable) {
|
|
961
|
-
reportRetainedCodexProviderTable(result.retainedCodexProviderTable);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
else {
|
|
965
|
-
// Codex history is the one restore whose failure leaves the runtime consistent: the
|
|
966
|
-
// manifest is retained and the routed metadata is untouched. Config and catalog are
|
|
967
|
-
// not — a client reads those, so their failure is a real teardown failure.
|
|
968
|
-
const artifacts = result.artifacts;
|
|
969
|
-
const configOrCatalogFailed = artifacts.config.state === "failed" || artifacts.catalog.state === "failed";
|
|
970
|
-
// A preflight refusal reports every artifact as `skipped` because none of them were
|
|
971
|
-
// attempted. Reading the states alone cannot tell that apart from an ownership
|
|
972
|
-
// refusal, so the structured reason carries it and the states are still required to
|
|
973
|
-
// agree — a refusal that somehow reports a failed artifact is not this case.
|
|
974
|
-
// A degraded restore has no refusal reason and reports config as partial, so it cannot
|
|
975
|
-
// enter this branch: its config obligation was discharged and the stop receipt must be
|
|
976
|
-
// released rather than preserved.
|
|
977
|
-
const preflightRefused = result.historyPreflightRefusal !== undefined
|
|
978
|
-
&& artifacts.config.state === "skipped"
|
|
979
|
-
&& artifacts.catalog.state === "skipped"
|
|
980
|
-
&& artifacts.history.state === "skipped";
|
|
981
|
-
if (preflightRefused) historyDeferred = true;
|
|
982
|
-
else if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
|
|
983
|
-
else other = true;
|
|
984
|
-
console.error(`⚠️ ${result.message}`);
|
|
985
|
-
}
|
|
976
|
+
const started = await handleEnsure({ existingIsSuccess: false, forceStart: recoveringLiveRestart, onSpawn: spawned => { child = spawned; } });
|
|
977
|
+
return restartStartOutcome(started, child);
|
|
986
978
|
} catch (error) {
|
|
987
|
-
|
|
988
|
-
console.error(`⚠️ Native Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
979
|
+
return restartStartOutcome(false, child, error);
|
|
989
980
|
}
|
|
990
|
-
|
|
991
|
-
// A refused or thrown Grok strip is actionable because it would point Grok at a dead proxy.
|
|
992
|
-
try {
|
|
993
|
-
const grok = stripGrokConfig();
|
|
994
|
-
if (grok.changed) console.log(`↩️ ${grok.message}`);
|
|
995
|
-
else if (!grok.ok) { other = true; console.error(`⚠️ ${grok.message}`); }
|
|
996
|
-
} catch (error) {
|
|
997
|
-
other = true;
|
|
998
|
-
console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
999
|
-
}
|
|
1000
|
-
return { historyOnly, historyDeferred, other };
|
|
1001
981
|
}
|
|
1002
982
|
|
|
983
|
+
|
|
1003
984
|
async function handleStop(approval?: StopApproval) {
|
|
1004
985
|
const lease = acquireOwnershipMutationLease(serviceStatePaths());
|
|
1005
986
|
try {
|
|
@@ -1442,7 +1423,7 @@ async function handleStopUnlocked(snapshot?: GuardedStopSnapshot) {
|
|
|
1442
1423
|
// not answering. That is the whole point of leaving the receipt behind.
|
|
1443
1424
|
console.log("↩️ Finishing a shared teardown left unfinished by an earlier stop.");
|
|
1444
1425
|
}
|
|
1445
|
-
const restore = await restoreSharedClientStateAfterStop();
|
|
1426
|
+
const restore = await restoreSharedClientStateAfterStop(reportRetainedCodexProviderTable);
|
|
1446
1427
|
record.sharedTeardown = restore.historyDeferred ? "refused" : restore.other ? "failed" : "restored";
|
|
1447
1428
|
if (restore.other) stopFailed = true;
|
|
1448
1429
|
else if (restore.historyDeferred) historyDeferredNonces = teardownNonce ? [teardownNonce, ...recoveredNonces] : recoveredNonces;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
1
|
+
import { contextTierRecordConfigError, modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
2
2
|
import {
|
|
3
3
|
CliUsageError,
|
|
4
4
|
csv,
|
|
@@ -42,7 +42,7 @@ const USAGE = `Usage:
|
|
|
42
42
|
[--headers <json>] [--enabled <on|off>] [--live-models <on|off>]
|
|
43
43
|
[--retain-models <id,id|->] [--model <id> --text-only]
|
|
44
44
|
[--xai-chat <on|off>]
|
|
45
|
-
[--allow-private-network <on|off>] [--json]
|
|
45
|
+
[--allow-private-network <on|off>] [--model-context-tier <model=default|long_context>] [--json]
|
|
46
46
|
ocx provider test <name> [--json]
|
|
47
47
|
ocx provider quota [--refresh] [--json]
|
|
48
48
|
ocx provider resets [--limit <n>] [--json]
|
|
@@ -73,6 +73,12 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
73
73
|
const liveModels = takeBooleanOption(args, "--live-models");
|
|
74
74
|
const allowPrivateNetwork = takeBooleanOption(args, "--allow-private-network");
|
|
75
75
|
const xaiChat = takeBooleanOption(args, "--xai-chat");
|
|
76
|
+
const contextTierValues: string[] = [];
|
|
77
|
+
for (;;) {
|
|
78
|
+
const value = takeOption(args, "--model-context-tier");
|
|
79
|
+
if (value === undefined) break;
|
|
80
|
+
contextTierValues.push(value);
|
|
81
|
+
}
|
|
76
82
|
const textOnly = takeFlag(args, "--text-only");
|
|
77
83
|
const capabilityModel = takeOption(args, "--model");
|
|
78
84
|
rejectArgs(args, USAGE);
|
|
@@ -83,6 +89,21 @@ async function edit(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
83
89
|
if (error) throw new CliUsageError(error, USAGE);
|
|
84
90
|
patch.modelCapabilities = declaration;
|
|
85
91
|
}
|
|
92
|
+
if (contextTierValues.length) {
|
|
93
|
+
if (name !== "github-copilot") throw new CliUsageError("--model-context-tier is valid only for provider github-copilot", USAGE);
|
|
94
|
+
const tiers: Record<string, "default" | "long_context"> = Object.create(null);
|
|
95
|
+
for (const value of contextTierValues) {
|
|
96
|
+
const separator = value.indexOf("=");
|
|
97
|
+
const model = value.slice(0, separator);
|
|
98
|
+
const tier = value.slice(separator + 1);
|
|
99
|
+
if (separator < 1 || (tier !== "default" && tier !== "long_context"))
|
|
100
|
+
throw new CliUsageError("--model-context-tier must use model=default or model=long_context", USAGE);
|
|
101
|
+
tiers[model] = tier;
|
|
102
|
+
}
|
|
103
|
+
const error = contextTierRecordConfigError(tiers);
|
|
104
|
+
if (error) throw new CliUsageError(error, USAGE);
|
|
105
|
+
patch.modelContextTiers = tiers;
|
|
106
|
+
}
|
|
86
107
|
if (xaiChat !== undefined) {
|
|
87
108
|
if (name !== "xai") throw new CliUsageError("--xai-chat is valid only for provider xai", USAGE);
|
|
88
109
|
patch.xaiResponsesOptIn = !xaiChat;
|
package/src/cli/provider.ts
CHANGED
|
@@ -245,6 +245,9 @@ async function handleAdd(args: string[]): Promise<void> {
|
|
|
245
245
|
if (existingProvider?.modelCapabilities !== undefined && provConfig.modelCapabilities === undefined) {
|
|
246
246
|
provConfig.modelCapabilities = structuredClone(existingProvider.modelCapabilities);
|
|
247
247
|
}
|
|
248
|
+
if (existingProvider?.modelContextTiers !== undefined && provConfig.modelContextTiers === undefined) {
|
|
249
|
+
provConfig.modelContextTiers = structuredClone(existingProvider.modelContextTiers);
|
|
250
|
+
}
|
|
248
251
|
if (textOnly) {
|
|
249
252
|
const modelId = capabilityModel ?? defaultModel ?? provConfig.defaultModel;
|
|
250
253
|
if (!modelId) {
|
package/src/cli/resolve.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
} from "../service/ownership-compatibility";
|
|
60
60
|
import { observeManagingClis } from "../service/managing-cli";
|
|
61
61
|
import { SERVICE_OWNERSHIP_MINIMUM_CLI_VERSION } from "../service/install-state-contract.mjs";
|
|
62
|
+
import { computeVersionSkew, type VersionSkew } from "./version-skew";
|
|
62
63
|
|
|
63
64
|
/** Wire version of the resolve document. Bump only on an incompatible shape change. */
|
|
64
65
|
export const RESOLVE_SCHEMA = "ocx-resolve/1";
|
|
@@ -114,6 +115,14 @@ export interface ResolveJson {
|
|
|
114
115
|
* member of `ServiceTakeoverCompatibility`'s union.
|
|
115
116
|
*/
|
|
116
117
|
takeover: ResolveTakeover;
|
|
118
|
+
/**
|
|
119
|
+
* The bundled CLI's version compared against the live proxy's, present only when
|
|
120
|
+
* something is live. A shell reads `relation` to decide whether attaching or a
|
|
121
|
+
* takeover moves versions: `"proxy-newer"` means this CLI's bundled runtime would
|
|
122
|
+
* DOWNGRADE the listener, which the desktop uses to stay a guest instead of offering
|
|
123
|
+
* takeover. Additive within ocx-resolve/1; older shells ignore it and read as unknown.
|
|
124
|
+
*/
|
|
125
|
+
versionSkew?: VersionSkew;
|
|
117
126
|
}
|
|
118
127
|
|
|
119
128
|
export type ResolveTakeover =
|
|
@@ -192,6 +201,9 @@ export function buildResolveJson(
|
|
|
192
201
|
liveness: livenessJson(live),
|
|
193
202
|
ownership,
|
|
194
203
|
takeover,
|
|
204
|
+
// A proven absence has nothing to skew against; the field is omitted there rather
|
|
205
|
+
// than emitted as an "unknown" row, matching the liveness identity fields.
|
|
206
|
+
...(live ? { versionSkew: computeVersionSkew(cliVersion, live.version) } : {}),
|
|
195
207
|
};
|
|
196
208
|
}
|
|
197
209
|
|
|
@@ -221,6 +233,12 @@ function reportHuman(json: ResolveJson, stdout: { log: (s: string) => void }): v
|
|
|
221
233
|
? "Takeover: supported"
|
|
222
234
|
: `Takeover: blocked (${json.takeover.reason}: ${json.takeover.detail})`,
|
|
223
235
|
);
|
|
236
|
+
// The skew warning is the same sentence `ocx status` and `doctor` surface; an
|
|
237
|
+
// operator comparing a live proxy against this CLI gets it here too instead of
|
|
238
|
+
// discovering the mismatch only inside the desktop shell.
|
|
239
|
+
if (json.versionSkew?.warning) {
|
|
240
|
+
stdout.log(`Version skew: ${json.versionSkew.warning}`);
|
|
241
|
+
}
|
|
224
242
|
}
|
|
225
243
|
|
|
226
244
|
/**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ServiceChildOwnershipDecision } from "../service/service-child-ownership";
|
|
2
|
+
|
|
3
|
+
/** Fence shared client-state recovery against a claim committed after the early owner probe. */
|
|
4
|
+
export async function recoverStartStateUnderOwnershipLease(deps: {
|
|
5
|
+
supervised: boolean;
|
|
6
|
+
acquireLease: () => { release(): void };
|
|
7
|
+
decide: () => ServiceChildOwnershipDecision;
|
|
8
|
+
stayOut: (refusal: string) => never;
|
|
9
|
+
recover: () => Promise<boolean>;
|
|
10
|
+
}): Promise<boolean> {
|
|
11
|
+
if (!deps.supervised) return deps.recover();
|
|
12
|
+
const lease = deps.acquireLease();
|
|
13
|
+
let released = false;
|
|
14
|
+
const release = () => {
|
|
15
|
+
if (released) return;
|
|
16
|
+
released = true;
|
|
17
|
+
lease.release();
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
const decision = deps.decide();
|
|
21
|
+
if (decision.kind === "stay-out") {
|
|
22
|
+
// stayOut exits the process, so a finally block would never release the lease.
|
|
23
|
+
release();
|
|
24
|
+
deps.stayOut(decision.refusal);
|
|
25
|
+
}
|
|
26
|
+
return await deps.recover();
|
|
27
|
+
} finally {
|
|
28
|
+
release();
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/cli/stop-approval.ts
CHANGED
|
@@ -158,8 +158,20 @@ export async function runGuardedManagerStep(
|
|
|
158
158
|
return { service: "absent", effect: "approval-changed", proxy: "unknown", handledByProxy: false };
|
|
159
159
|
}
|
|
160
160
|
const service = snapshot.manager.kind === "absent" ? "absent" : io.stopManager();
|
|
161
|
+
if (snapshot.manager.kind === "bound"
|
|
162
|
+
&& service !== "stopped" && service !== "stopped-respawnable") {
|
|
163
|
+
return { service, effect: "manager-still-active", proxy: "unknown", handledByProxy: false };
|
|
164
|
+
}
|
|
161
165
|
let handledByProxy = false;
|
|
162
|
-
|
|
166
|
+
// A bound manager whose stop cascades to the child needs no signal: launchd and
|
|
167
|
+
// systemd end the job's whole process tree. A bound Task Scheduler or WinSW
|
|
168
|
+
// manager does not — schtasks /end can leave both the wrapper and the approved
|
|
169
|
+
// PID alive (#764) — so those get the same graceful signal the absent path uses,
|
|
170
|
+
// run AFTER the manager stop so the proxy's respawn-refusal sees the manager
|
|
171
|
+
// already ended (the deferral receipt still bypasses it as before).
|
|
172
|
+
const needsApprovedSignal = snapshot.manager.kind === "absent"
|
|
173
|
+
|| snapshot.manager.childNeedsSeparateStop;
|
|
174
|
+
if (needsApprovedSignal) {
|
|
163
175
|
try { handledByProxy = await io.signalApproved(); }
|
|
164
176
|
catch { return { service, effect: "failed", proxy: "unknown", handledByProxy }; }
|
|
165
177
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { restoreNativeCodexAsync } from "../codex/inject";
|
|
2
|
+
import type { RetainedCodexProviderTable } from "../codex/inject/restore";
|
|
3
|
+
import { stripGrokConfig } from "../grok/inject";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Restore shared client state after a stop.
|
|
7
|
+
*
|
|
8
|
+
* Returns the two failure kinds separately. `historyOnly` means teardown succeeded and
|
|
9
|
+
* only Codex history metadata could not be finalized: the proxy is down, the service is
|
|
10
|
+
* stopped, and a manifest is waiting for review. `other` means something that actually
|
|
11
|
+
* removes state a client depends on.
|
|
12
|
+
*
|
|
13
|
+
* The distinction exists because `ocx update` must proceed for the first and abort for the
|
|
14
|
+
* second, and it can only see an exit code (#3008).
|
|
15
|
+
*
|
|
16
|
+
* `historyDeferred` is the third kind (#4718). The Codex history preflight refuses BEFORE
|
|
17
|
+
* the config half runs, so nothing was restored at all: config, catalog, history and
|
|
18
|
+
* provenance are untouched and the client is still routed at the proxy that just stopped.
|
|
19
|
+
* Like `historyOnly` the proxy is genuinely down, so an update may replace package files.
|
|
20
|
+
* Unlike `historyOnly` the obligation was not performed, so the receipt must survive.
|
|
21
|
+
*/
|
|
22
|
+
export async function restoreSharedClientStateAfterStop(
|
|
23
|
+
reportRetainedCodexProviderTable: (retained: RetainedCodexProviderTable) => void,
|
|
24
|
+
): Promise<{ historyOnly: boolean; historyDeferred: boolean; other: boolean }> {
|
|
25
|
+
let historyOnly = false;
|
|
26
|
+
let historyDeferred = false;
|
|
27
|
+
let other = false;
|
|
28
|
+
try {
|
|
29
|
+
const result = await restoreNativeCodexAsync();
|
|
30
|
+
if (result.success) {
|
|
31
|
+
console.log(`↩️ ${result.message}`);
|
|
32
|
+
if (result.retainedCodexProviderTable) {
|
|
33
|
+
reportRetainedCodexProviderTable(result.retainedCodexProviderTable);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Codex history is the one restore whose failure leaves the runtime consistent: the
|
|
38
|
+
// manifest is retained and the routed metadata is untouched. Config and catalog are
|
|
39
|
+
// not — a client reads those, so their failure is a real teardown failure.
|
|
40
|
+
const artifacts = result.artifacts;
|
|
41
|
+
const configOrCatalogFailed = artifacts.config.state === "failed" || artifacts.catalog.state === "failed";
|
|
42
|
+
// A preflight refusal reports every artifact as `skipped` because none of them were
|
|
43
|
+
// attempted. Reading the states alone cannot tell that apart from an ownership
|
|
44
|
+
// refusal, so the structured reason carries it and the states are still required to
|
|
45
|
+
// agree — a refusal that somehow reports a failed artifact is not this case.
|
|
46
|
+
// A degraded restore has no refusal reason and reports config as partial, so it cannot
|
|
47
|
+
// enter this branch: its config obligation was discharged and the stop receipt must be
|
|
48
|
+
// released rather than preserved.
|
|
49
|
+
const preflightRefused = result.historyPreflightRefusal !== undefined
|
|
50
|
+
&& artifacts.config.state === "skipped"
|
|
51
|
+
&& artifacts.catalog.state === "skipped"
|
|
52
|
+
&& artifacts.history.state === "skipped";
|
|
53
|
+
if (preflightRefused) historyDeferred = true;
|
|
54
|
+
else if (!configOrCatalogFailed && artifacts.history.state === "failed") historyOnly = true;
|
|
55
|
+
else other = true;
|
|
56
|
+
console.error(`⚠️ ${result.message}`);
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
other = true;
|
|
60
|
+
console.error(`⚠️ Native Codex restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A refused or thrown Grok strip is actionable because it would point Grok at a dead proxy.
|
|
64
|
+
try {
|
|
65
|
+
const grok = stripGrokConfig();
|
|
66
|
+
if (grok.changed) console.log(`↩️ ${grok.message}`);
|
|
67
|
+
else if (!grok.ok) { other = true; console.error(`⚠️ ${grok.message}`); }
|
|
68
|
+
} catch (error) {
|
|
69
|
+
other = true;
|
|
70
|
+
console.error(`⚠️ Grok config restore failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
71
|
+
}
|
|
72
|
+
return { historyOnly, historyDeferred, other };
|
|
73
|
+
}
|