@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
package/src/cli/doctor.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
import { getConfigDir, getConfigPath, readConfigDiagnostics
|
|
13
|
+
import { getConfigDir, getConfigPath, readConfigDiagnostics } from "../config";
|
|
14
14
|
import { readPid } from "../config/process-state";
|
|
15
|
+
import { probeUncleanExitState } from "./status";
|
|
15
16
|
import { findLiveProxy, type LiveProxy } from "../server/proxy-liveness";
|
|
16
17
|
import { BUN_RUNTIME_SOURCES } from "../lib/bun-runtime";
|
|
17
18
|
import type { BunRuntimeSource } from "../lib/bun-runtime";
|
|
@@ -26,7 +27,8 @@ import { probeNativeProfileRecoveryState, resolveNativeProfileContext } from "..
|
|
|
26
27
|
import { NativeProfileError } from "../codex/native-profile-types";
|
|
27
28
|
import { collectOrcaCodexHomeDiagnostic, resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
28
29
|
import { scanCodexAgentRolesWithTomlModelFallback } from "../codex/subagent-model-fallback";
|
|
29
|
-
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
30
|
+
import { diagnoseCodexShim, findCodexOnPath, isWindowsInteropDir, type CodexShimDiagnostic } from "../codex/shim";
|
|
31
|
+
import { providerTableString, rootTomlString } from "../codex/injected-marker";
|
|
30
32
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
31
33
|
import {
|
|
32
34
|
inspectCodexCoordinator,
|
|
@@ -403,6 +405,10 @@ export function collectWslDualInstall(deps: WslDualInstallDeps = {}): WslDualIns
|
|
|
403
405
|
export type ProxyEnvRow = { key: string; present: boolean };
|
|
404
406
|
export type EnvMap = Record<string, string | undefined>;
|
|
405
407
|
|
|
408
|
+
function ownEnvValue(env: EnvMap, name: string): string | undefined {
|
|
409
|
+
return Object.hasOwn(env, name) ? env[name] : undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
406
412
|
/** Report only presence/absence of proxy env vars - never the value (it may
|
|
407
413
|
* embed credentials). Checks both upper- and lower-case forms. */
|
|
408
414
|
export function collectProxyEnv(env: EnvMap = process.env): ProxyEnvRow[] {
|
|
@@ -438,11 +444,6 @@ export function collectProviderApiKeyDiagnostics(
|
|
|
438
444
|
providers: Record<string, { authMode?: string; apiKey?: string }> = readConfigDiagnostics().config.providers ?? {},
|
|
439
445
|
env: EnvMap = process.env,
|
|
440
446
|
): ProviderApiKeyDiagnostic[] {
|
|
441
|
-
const resolveInEnv = (value: string): string | undefined => {
|
|
442
|
-
const name = envReferenceName(value);
|
|
443
|
-
if (!name) return value;
|
|
444
|
-
return env[name];
|
|
445
|
-
};
|
|
446
447
|
const rows: ProviderApiKeyDiagnostic[] = [];
|
|
447
448
|
for (const [provider, config] of Object.entries(providers)) {
|
|
448
449
|
if (config.authMode !== "key") continue;
|
|
@@ -450,7 +451,7 @@ export function collectProviderApiKeyDiagnostics(
|
|
|
450
451
|
if (!raw) continue;
|
|
451
452
|
const envName = envReferenceName(raw);
|
|
452
453
|
if (!envName) continue;
|
|
453
|
-
const resolved =
|
|
454
|
+
const resolved = ownEnvValue(env, envName);
|
|
454
455
|
if (resolved?.trim()) continue;
|
|
455
456
|
rows.push({
|
|
456
457
|
provider,
|
|
@@ -461,6 +462,33 @@ export function collectProviderApiKeyDiagnostics(
|
|
|
461
462
|
return rows;
|
|
462
463
|
}
|
|
463
464
|
|
|
465
|
+
export type CodexEnvKeyReadinessDiagnostic = {
|
|
466
|
+
envName: string;
|
|
467
|
+
shimState: "missing" | "unhealthy";
|
|
468
|
+
detail: string;
|
|
469
|
+
action: string;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/** Warn when routed Codex cannot obtain its configured admission token at launch. */
|
|
473
|
+
export function collectCodexEnvKeyReadiness(
|
|
474
|
+
configText: string | null,
|
|
475
|
+
env: EnvMap,
|
|
476
|
+
shim: CodexShimDiagnostic,
|
|
477
|
+
serviceTokenPresent: boolean,
|
|
478
|
+
): CodexEnvKeyReadinessDiagnostic | null {
|
|
479
|
+
if (!configText || rootTomlString(configText, "model_provider") !== "opencodex") return null;
|
|
480
|
+
const envName = providerTableString(configText, "opencodex", "env_key")?.trim();
|
|
481
|
+
const envValue = envName ? ownEnvValue(env, envName) : undefined;
|
|
482
|
+
if (!envName || envValue?.trim() || shim.healthy || !serviceTokenPresent) return null;
|
|
483
|
+
const shimState = shim.installed ? "unhealthy" : "missing";
|
|
484
|
+
return {
|
|
485
|
+
envName,
|
|
486
|
+
shimState,
|
|
487
|
+
detail: `Codex uses env_key ${envName}, but that variable is unset and the OpenCodex shim is ${shimState}; the service token file exists but plain Codex does not load it`,
|
|
488
|
+
action: `Run 'ocx codex-shim install' to repair launch-time token injection, or export ${envName} in the process that starts Codex`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
464
492
|
export function collectConfiguredProxy(): ConfiguredProxyDiagnostic {
|
|
465
493
|
const diagnostics = readConfigDiagnostics();
|
|
466
494
|
const rawProxy = typeof diagnostics.config.proxy === "string" ? diagnostics.config.proxy.trim() : "";
|
|
@@ -484,7 +512,9 @@ export function collectConfiguredProxy(): ConfiguredProxyDiagnostic {
|
|
|
484
512
|
}
|
|
485
513
|
|
|
486
514
|
const envName = envReferenceName(rawProxy);
|
|
487
|
-
const resolved =
|
|
515
|
+
const resolved = rawProxy.startsWith("$")
|
|
516
|
+
? ownEnvValue(process.env, envName ?? rawProxy.slice(1))
|
|
517
|
+
: rawProxy;
|
|
488
518
|
if (resolved?.trim()) {
|
|
489
519
|
return {
|
|
490
520
|
key: "config.proxy",
|
|
@@ -505,7 +535,7 @@ export function collectConfiguredProxy(): ConfiguredProxyDiagnostic {
|
|
|
505
535
|
}
|
|
506
536
|
|
|
507
537
|
export function parseProcessEnvBlock(content: string): EnvMap {
|
|
508
|
-
const env: EnvMap =
|
|
538
|
+
const env: EnvMap = Object.create(null);
|
|
509
539
|
for (const entry of content.split("\0")) {
|
|
510
540
|
if (!entry) continue;
|
|
511
541
|
const separator = entry.indexOf("=");
|
|
@@ -943,6 +973,11 @@ export function proxyDownRestartHint(input: {
|
|
|
943
973
|
/** Absent means "unknown"; the hint then keeps its pre-repair wording. */
|
|
944
974
|
serviceInstalled?: boolean;
|
|
945
975
|
serviceConflict?: boolean;
|
|
976
|
+
/**
|
|
977
|
+
* Persisted owner records outlived their process (#1419). Cause-neutral: what is on
|
|
978
|
+
* disk proves an unclean exit, not which signal caused it.
|
|
979
|
+
*/
|
|
980
|
+
staleProcessState?: boolean;
|
|
946
981
|
}): string | null {
|
|
947
982
|
if (input.proxyRunning) return null;
|
|
948
983
|
// `serviceViable` alone conflates "no service at all" with "registered but stale or
|
|
@@ -955,7 +990,10 @@ export function proxyDownRestartHint(input: {
|
|
|
955
990
|
: installedButBroken
|
|
956
991
|
? "Restart it with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
957
992
|
: "Restart it with 'ocx start', or install the persistent service: 'ocx service install'.";
|
|
958
|
-
|
|
993
|
+
const uncleanExit = input.staleProcessState === true
|
|
994
|
+
? "Stale process records remain, so the previous run may have exited unexpectedly. "
|
|
995
|
+
: "";
|
|
996
|
+
return `The ocx proxy is not running. ${uncleanExit}Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
|
|
959
997
|
}
|
|
960
998
|
|
|
961
999
|
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
@@ -1059,6 +1097,17 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1059
1097
|
}
|
|
1060
1098
|
|
|
1061
1099
|
const doctorConfig = readConfigDiagnostics().config;
|
|
1100
|
+
const codexConfigPath = join(resolveCodexHomeDirImpl(), "config.toml");
|
|
1101
|
+
const codexConfigText = (() => {
|
|
1102
|
+
try { return readFileSync(codexConfigPath, "utf8"); } catch { return null; }
|
|
1103
|
+
})();
|
|
1104
|
+
const serviceTokenPresent = Boolean(readInstalledServiceToken()?.trim());
|
|
1105
|
+
const codexEnvKeyReadiness = collectCodexEnvKeyReadiness(
|
|
1106
|
+
codexConfigText,
|
|
1107
|
+
process.env,
|
|
1108
|
+
diagnoseCodexShim(),
|
|
1109
|
+
serviceTokenPresent,
|
|
1110
|
+
);
|
|
1062
1111
|
const startup = collectStartupHealth(doctorConfig);
|
|
1063
1112
|
console.log("\nCodex restart safety");
|
|
1064
1113
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
@@ -1137,6 +1186,14 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1137
1186
|
}
|
|
1138
1187
|
}
|
|
1139
1188
|
|
|
1189
|
+
console.log("\nCodex env_key launch readiness");
|
|
1190
|
+
if (codexEnvKeyReadiness) {
|
|
1191
|
+
console.log(` !! ${codexEnvKeyReadiness.detail}`);
|
|
1192
|
+
console.log(` Action: ${codexEnvKeyReadiness.action}`);
|
|
1193
|
+
} else {
|
|
1194
|
+
console.log(" ok no broken OpenCodex env_key launch path detected");
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1140
1197
|
console.log("\nRunning proxy process proxy env (presence only)");
|
|
1141
1198
|
if (runningProxyEnv.status === "not_running") {
|
|
1142
1199
|
console.log(" -- no running ocx proxy process found");
|
|
@@ -1269,11 +1326,20 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1269
1326
|
serviceViable: startup.serviceViable,
|
|
1270
1327
|
serviceInstalled: startup.serviceInstalled,
|
|
1271
1328
|
serviceConflict: startup.serviceConflict,
|
|
1329
|
+
// Threaded through the same decision helper `ocx status` uses, so the two
|
|
1330
|
+
// diagnostics cannot drift. A helper-only change would satisfy a unit test while
|
|
1331
|
+
// real `ocx doctor` output never mentioned the crash (#1419).
|
|
1332
|
+
staleProcessState: await probeUncleanExitState({
|
|
1333
|
+
live: Boolean(live),
|
|
1334
|
+
port: doctorConfig.port,
|
|
1335
|
+
hostname: doctorConfig.hostname,
|
|
1336
|
+
}),
|
|
1272
1337
|
});
|
|
1273
1338
|
if (proxyDown) hints.push(proxyDown);
|
|
1274
1339
|
for (const row of providerApiKeys) {
|
|
1275
1340
|
hints.push(`${row.detail}. Set ${row.envName} in the shell that starts the proxy, or store a literal key in config (value hidden here).`);
|
|
1276
1341
|
}
|
|
1342
|
+
if (codexEnvKeyReadiness) hints.push(`${codexEnvKeyReadiness.detail}. ${codexEnvKeyReadiness.action}.`);
|
|
1277
1343
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
1278
1344
|
const noProxy = currentProxyEnv.every(p => !p.present) && !configuredProxy.present;
|
|
1279
1345
|
if (!startup.rebootSafe) {
|
package/src/cli/help.ts
CHANGED
|
@@ -61,7 +61,7 @@ Usage:
|
|
|
61
61
|
ocx account <sub> Accounts, login/reauth, key pools, and quota controls
|
|
62
62
|
ocx models <sub> Live/custom models, visibility, context, and shadow calls
|
|
63
63
|
ocx alias <sub> Short names for providers and models (list, set, rm, defaults)
|
|
64
|
-
ocx combo <sub> Combo failover
|
|
64
|
+
ocx combo <sub> Combo routing strategies and failover
|
|
65
65
|
ocx agent <sub> Subagents, injection, effort caps, and sidecars
|
|
66
66
|
ocx observe <sub> Logs, usage, storage, memory, and debug data
|
|
67
67
|
ocx inspect <sub> Effective config, catalog, analytics, pacing, client-config
|
package/src/cli/index.ts
CHANGED
|
@@ -858,6 +858,11 @@ async function handleStatus() {
|
|
|
858
858
|
console.log(`❌ Proxy: ${status.proxyLabel}`);
|
|
859
859
|
}
|
|
860
860
|
console.log(` Health: ${status.healthLabel}`);
|
|
861
|
+
if (status.json.claudeDesktop.desiredEnabled && !status.json.claudeDesktop.policy.ok) {
|
|
862
|
+
console.log(` ⚠️ Claude Desktop 3P health: ${status.json.claudeDesktop.policy.status}`);
|
|
863
|
+
console.log(` ${status.json.claudeDesktop.policy.message}`);
|
|
864
|
+
console.log(` Action: ${status.json.claudeDesktop.policy.action}`);
|
|
865
|
+
}
|
|
861
866
|
// Printed here, not only in --json: a stale ocx on PATH is exactly the situation where
|
|
862
867
|
// the operator is reading human output and wondering why the CLI disagrees with the
|
|
863
868
|
// dashboard. Adding the JSON field alone would satisfy a test and help nobody (#2701).
|
|
@@ -877,6 +882,17 @@ async function handleStatus() {
|
|
|
877
882
|
// contradicted it in the same report, and install re-registers: UAC on Windows and a
|
|
878
883
|
// possible WinSW-to-scheduler switch for someone who already has a service.
|
|
879
884
|
const installed = status.json.startup.serviceInstalled && !status.json.startup.serviceConflict;
|
|
885
|
+
// #1419: the records outliving the process is the only evidence the user gets that a
|
|
886
|
+
// previous run ended without cleanup. Deliberately hedged and cause-neutral — cleanup
|
|
887
|
+
// ignores unlink failures and the records carry no session provenance, so this cannot
|
|
888
|
+
// prove a crash, only that the last run left state behind. The restart advice below is
|
|
889
|
+
// not repeated here; one recommendation per report.
|
|
890
|
+
if (status.json.proxy.staleProcessState) {
|
|
891
|
+
console.log(" Stale process records remain, so the previous run may have exited unexpectedly.");
|
|
892
|
+
if (!installed) {
|
|
893
|
+
console.log(" No background service was available to restart it.");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
880
896
|
console.log(installed
|
|
881
897
|
? " Restart with 'ocx start', or refresh the installed service: 'ocx service repair'."
|
|
882
898
|
: " Restart with 'ocx start', or install the persistent service: 'ocx service install'.");
|
package/src/cli/models.ts
CHANGED
|
@@ -283,10 +283,27 @@ async function handleCustomRemove(args: string[]): Promise<void> {
|
|
|
283
283
|
// that row while the dash form matched both — the two relations disagreed on the same
|
|
284
284
|
// config. Removal stays exact-or-refuse: an ambiguous selector still aborts below, which is
|
|
285
285
|
// the right default for a destructive command.
|
|
286
|
+
const separator = target.indexOf("/");
|
|
287
|
+
const selectedProvider = separator >= 0 ? target.slice(0, separator) : undefined;
|
|
288
|
+
// Resolve ONCE against the provider's whole roster, then map the decision back onto rows.
|
|
289
|
+
// Calling the resolver per row with a singleton roster hid every cross-row fact it needs:
|
|
290
|
+
// a self-namespaced `acme/turbo` and a sibling `turbo` each matched their own singleton,
|
|
291
|
+
// so the command saw two matches and aborted as ambiguous even though the selector names
|
|
292
|
+
// one row exactly.
|
|
293
|
+
const rosterMatched = selectedProvider === undefined
|
|
294
|
+
? undefined
|
|
295
|
+
: resolveSlugSelection(
|
|
296
|
+
selectedProvider,
|
|
297
|
+
target,
|
|
298
|
+
existing.filter(model => model.provider === selectedProvider).map(model => model.modelId),
|
|
299
|
+
);
|
|
300
|
+
// Deliberately admit the whole matched set rather than narrowing to `exact`: an encoded
|
|
301
|
+
// selector that spans a real collision must still abort below. Removal stays exact-or-refuse.
|
|
302
|
+
const admitted = new Set(rosterMatched?.matched ?? []);
|
|
286
303
|
const matchingIndexes = existing.flatMap((model, index) => {
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
return
|
|
304
|
+
if (selectedProvider === undefined) return model.id === target ? [index] : [];
|
|
305
|
+
if (model.provider !== selectedProvider) return [];
|
|
306
|
+
return admitted.has(model.modelId) ? [index] : [];
|
|
290
307
|
});
|
|
291
308
|
if (matchingIndexes.length === 0) fail(`custom model "${target}" not found`);
|
|
292
309
|
if (matchingIndexes.length > 1) {
|
package/src/cli/registry.ts
CHANGED
|
@@ -194,7 +194,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
194
194
|
{
|
|
195
195
|
name: "combo",
|
|
196
196
|
usage: "ocx combo <list|show|set|remove> ...",
|
|
197
|
-
summary: "Manage combo
|
|
197
|
+
summary: "Manage combo virtual models and routing strategies.",
|
|
198
198
|
details: ["Alias hierarchy: ocx route combo ...", "Use --targets provider/model[:weight],provider/model[:weight]."],
|
|
199
199
|
},
|
|
200
200
|
{
|
|
@@ -291,6 +291,7 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
291
291
|
"Claude Desktop profile:",
|
|
292
292
|
" ocx claude desktop [apply] Save and apply the four-family profile",
|
|
293
293
|
" ocx claude desktop show [--json] Show routes, families, and defaults",
|
|
294
|
+
" ocx claude desktop status [--json] Show applied state, drift, and health",
|
|
294
295
|
" ocx claude desktop move <route> <family> [--default]",
|
|
295
296
|
" ocx claude desktop default <family> <route|none>",
|
|
296
297
|
" ocx claude desktop export <path|-> Export versioned JSON (`-` = stdout)",
|
package/src/cli/status.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { durableBunRuntime } from "../lib/bun-runtime";
|
|
2
2
|
import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
|
|
3
|
-
import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
3
|
+
import { getPidPath, readPid, readPidFileValue, readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
4
4
|
import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
|
|
5
5
|
import { findLiveProxy, isOpencodexHealthz, probeHostname } from "../server/proxy-liveness";
|
|
6
6
|
import { directLocalHttpFetch } from "../server/direct-local-http";
|
|
7
7
|
import type { OcxConfig } from "../types";
|
|
8
8
|
import { diagnoseService, serviceLogPath } from "../service";
|
|
9
9
|
import { collectStartupHealth, type StartupHealth } from "../codex/autostart-health";
|
|
10
|
+
import { isProcessAlive } from "../lib/process-control";
|
|
10
11
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
11
12
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
12
13
|
import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime";
|
|
@@ -15,12 +16,16 @@ import { computeVersionSkew, type VersionSkew } from "./version-skew";
|
|
|
15
16
|
import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
16
17
|
import { collectOrcaCodexHomeDiagnostic, type OrcaCodexHomeDiagnostic } from "../codex/home";
|
|
17
18
|
import { grokFenceEndpointDrift, readGrokStatus } from "../grok/status";
|
|
19
|
+
import { claudeDesktopIntegrationEnabled } from "../codex/desired-state";
|
|
20
|
+
import { claudeDesktopPolicyHealth, probeClaudeDesktopPolicy, type ClaudeDesktopPolicyHealth } from "../claude/desktop-policy";
|
|
18
21
|
|
|
19
22
|
type HealthCheck = {
|
|
20
23
|
ok: boolean;
|
|
21
24
|
url: string;
|
|
22
25
|
message: string;
|
|
23
26
|
label: string;
|
|
27
|
+
/** True only for a connect-phase refusal: proof that nothing holds the port. */
|
|
28
|
+
refused?: boolean;
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export type CliStatusJson = {
|
|
@@ -28,6 +33,8 @@ export type CliStatusJson = {
|
|
|
28
33
|
proxy: {
|
|
29
34
|
running: boolean;
|
|
30
35
|
pid: number | null;
|
|
36
|
+
/** Persisted owner records outlived their process: the last proxy did not exit cleanly. */
|
|
37
|
+
staleProcessState: boolean;
|
|
31
38
|
health: {
|
|
32
39
|
ok: boolean;
|
|
33
40
|
url: string;
|
|
@@ -72,6 +79,10 @@ export type CliStatusJson = {
|
|
|
72
79
|
};
|
|
73
80
|
};
|
|
74
81
|
codexHome: OrcaCodexHomeDiagnostic;
|
|
82
|
+
claudeDesktop: {
|
|
83
|
+
desiredEnabled: boolean;
|
|
84
|
+
policy: ClaudeDesktopPolicyHealth;
|
|
85
|
+
};
|
|
75
86
|
/**
|
|
76
87
|
* This CLI's version against the running proxy's (#2701).
|
|
77
88
|
*
|
|
@@ -127,6 +138,69 @@ export function proxyHealthFailureReason(error: unknown, signal: AbortSignal): "
|
|
|
127
138
|
: "unreachable";
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
/**
|
|
142
|
+
* "Nothing is listening" is narrower than "the probe failed". `unreachable` covers every
|
|
143
|
+
* non-abort failure, including a socket that was ACCEPTED and then reset — which is what
|
|
144
|
+
* an in-flight start looks like mid-bind. Only a connect-phase refusal proves the port is
|
|
145
|
+
* free, so this reads the underlying errno instead of the display string.
|
|
146
|
+
*/
|
|
147
|
+
export function isConnectionRefused(error: unknown): boolean {
|
|
148
|
+
for (let current: unknown = error, depth = 0; current instanceof Error && depth < 4; depth++) {
|
|
149
|
+
const code = (current as { code?: unknown }).code;
|
|
150
|
+
if (code === "ECONNREFUSED" || code === "ConnectionRefused") return true;
|
|
151
|
+
// Bun surfaces the refusal as a plain message on some platforms; the errno name is
|
|
152
|
+
// still the discriminator, not a substring of arbitrary prose.
|
|
153
|
+
if (typeof code === "string" && code.endsWith("ECONNREFUSED")) return true;
|
|
154
|
+
current = (current as { cause?: unknown }).cause;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A proxy killed by a native trap or SIGKILL never runs the exit cleanup that removes
|
|
161
|
+
* `ocx.pid` and `runtime-port.json` (only SIGINT/SIGTERM/SIGHUP and normal exit are
|
|
162
|
+
* wired to it), so both records outlive it. That makes "crashed" and "never started"
|
|
163
|
+
* distinguishable — and #1419 is what it costs when we discard the distinction: the
|
|
164
|
+
* reporter's unsupervised `ocx gui` proxy died and every later command said only
|
|
165
|
+
* "not running", never that a previous process had exited or that a service would
|
|
166
|
+
* have restarted it.
|
|
167
|
+
*
|
|
168
|
+
* Two races have to stay closed, because a false "it crashed" is worse than a missing
|
|
169
|
+
* hint. `handleStart` binds the port BEFORE it publishes either record, so:
|
|
170
|
+
*
|
|
171
|
+
* - a start that publishes between two reads is caught by comparing the raw records
|
|
172
|
+
* observed before and after the probes (the same snapshot discipline
|
|
173
|
+
* `removePidIfValueIs` uses for deletion);
|
|
174
|
+
* - a start that has bound but not yet published leaves both snapshots identical, so
|
|
175
|
+
* records alone cannot see it. That one is excluded on the port instead: the probe
|
|
176
|
+
* must have been REFUSED at connect, which is the only outcome proving nothing holds
|
|
177
|
+
* the port. A socket that is accepted and then reset — an in-flight bind — is not a
|
|
178
|
+
* refusal, so review caught `unreachable` being too broad for this job.
|
|
179
|
+
*
|
|
180
|
+
* What this can and cannot prove: the records outliving their process establish that the
|
|
181
|
+
* previous run did not complete its cleanup. It does not establish a cause, and it cannot
|
|
182
|
+
* fully exclude a clean exit whose `unlinkSync` failed, because cleanup ignores that
|
|
183
|
+
* error (`src/cli/index.ts:324-325`) and the records carry no session provenance. The
|
|
184
|
+
* wording therefore says the records remain and the run MAY have exited unexpectedly.
|
|
185
|
+
*/
|
|
186
|
+
export function isUncleanExitEvidence(input: {
|
|
187
|
+
live: boolean;
|
|
188
|
+
healthOk: boolean;
|
|
189
|
+
healthRefused: boolean;
|
|
190
|
+
ownerPidAlive: boolean;
|
|
191
|
+
pidRecordBefore: number | null;
|
|
192
|
+
pidRecordAfter: number | null;
|
|
193
|
+
runtimePidBefore: number | null;
|
|
194
|
+
runtimePidAfter: number | null;
|
|
195
|
+
}): boolean {
|
|
196
|
+
if (input.live || input.healthOk) return false;
|
|
197
|
+
if (!input.healthRefused) return false;
|
|
198
|
+
if (input.ownerPidAlive) return false;
|
|
199
|
+
if (input.pidRecordBefore !== input.pidRecordAfter) return false;
|
|
200
|
+
if (input.runtimePidBefore !== input.runtimePidAfter) return false;
|
|
201
|
+
return input.pidRecordAfter !== null || input.runtimePidAfter !== null;
|
|
202
|
+
}
|
|
203
|
+
|
|
130
204
|
/**
|
|
131
205
|
* `ocx status` greens on process liveness alone, so a proxy that answers
|
|
132
206
|
* /healthz reads healthy even when Codex is not pointed at it and every routed
|
|
@@ -171,15 +245,70 @@ async function checkProxyHealth(target: ListenTarget): Promise<HealthCheck> {
|
|
|
171
245
|
return { ok: true, url, message, label: `${url} ${message}` };
|
|
172
246
|
} catch (error) {
|
|
173
247
|
const reason = proxyHealthFailureReason(error, controller.signal);
|
|
174
|
-
return { ok: false, url, message: reason, label: `${url} ${reason}
|
|
248
|
+
return { ok: false, url, message: reason, label: `${url} ${reason}`, refused: isConnectionRefused(error) };
|
|
175
249
|
} finally {
|
|
176
250
|
clearTimeout(timer);
|
|
177
251
|
}
|
|
178
252
|
}
|
|
179
253
|
|
|
254
|
+
/**
|
|
255
|
+
* The ONE evidence gatherer for stale-process state, shared by `ocx status` and
|
|
256
|
+
* `ocx doctor`.
|
|
257
|
+
*
|
|
258
|
+
* It deliberately probes the port named by the STALE RECORD, not the configured display
|
|
259
|
+
* port. Review found the two commands disagreeing precisely here: a proxy that hopped to
|
|
260
|
+
* a fallback port, or a config whose port changed after the crash, left status probing
|
|
261
|
+
* the configured port while doctor probed the recorded one, so one reported a crash and
|
|
262
|
+
* the other did not. The question being asked is "is the process that wrote this record
|
|
263
|
+
* gone?", and only that record's own port can answer it.
|
|
264
|
+
*
|
|
265
|
+
* `live` short-circuits before the probe so a healthy install pays nothing.
|
|
266
|
+
*/
|
|
267
|
+
export async function probeUncleanExitState(input: {
|
|
268
|
+
live: boolean;
|
|
269
|
+
port?: number;
|
|
270
|
+
hostname?: string | null;
|
|
271
|
+
}): Promise<boolean> {
|
|
272
|
+
if (input.live) return false;
|
|
273
|
+
const pidRecordBefore = readPidFileValue();
|
|
274
|
+
const runtimeBefore = readRuntimePort();
|
|
275
|
+
const runtimePidBefore = runtimeBefore?.pid ?? null;
|
|
276
|
+
if (pidRecordBefore === null && runtimePidBefore === null) return false;
|
|
277
|
+
const ownerPid = pidRecordBefore ?? runtimePidBefore;
|
|
278
|
+
if (ownerPid !== null && isProcessAlive(ownerPid)) return false;
|
|
279
|
+
// The recorded port is the evidence target. Fall back to the configured port only when
|
|
280
|
+
// no runtime record exists, which is the pid-file-only case.
|
|
281
|
+
const port = runtimeBefore?.port ?? input.port ?? 10100;
|
|
282
|
+
const hostname = runtimeBefore?.hostname ?? input.hostname ?? undefined;
|
|
283
|
+
const health = await checkProxyHealth({
|
|
284
|
+
port,
|
|
285
|
+
hostname,
|
|
286
|
+
source: runtimeBefore ? "runtime" : "config",
|
|
287
|
+
healthUrl: `http://${probeHostname(hostname)}:${port}/healthz`,
|
|
288
|
+
dashboardUrl: `http://localhost:${port}/`,
|
|
289
|
+
});
|
|
290
|
+
const pidRecordAfter = readPidFileValue();
|
|
291
|
+
const runtimePidAfter = readRuntimePort()?.pid ?? null;
|
|
292
|
+
const ownerPidAfter = pidRecordAfter ?? runtimePidAfter;
|
|
293
|
+
return isUncleanExitEvidence({
|
|
294
|
+
live: false,
|
|
295
|
+
healthOk: health.ok,
|
|
296
|
+
healthRefused: health.refused === true,
|
|
297
|
+
ownerPidAlive: ownerPidAfter !== null && isProcessAlive(ownerPidAfter),
|
|
298
|
+
pidRecordBefore,
|
|
299
|
+
pidRecordAfter,
|
|
300
|
+
runtimePidBefore,
|
|
301
|
+
runtimePidAfter,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
180
305
|
export async function collectStatus(): Promise<CliStatusView> {
|
|
181
306
|
const configDiagnostics = readConfigDiagnostics();
|
|
182
307
|
const config = configDiagnostics.config;
|
|
308
|
+
const claudeDesktop = {
|
|
309
|
+
desiredEnabled: claudeDesktopIntegrationEnabled(config),
|
|
310
|
+
policy: claudeDesktopPolicyHealth(probeClaudeDesktopPolicy()),
|
|
311
|
+
};
|
|
183
312
|
// Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
|
|
184
313
|
// Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
|
|
185
314
|
// warn on malformed config.json (status --json must stay stderr-clean).
|
|
@@ -210,6 +339,13 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
210
339
|
label: `${listen.healthUrl} ok (live)`,
|
|
211
340
|
}
|
|
212
341
|
: await checkProxyHealth(listen);
|
|
342
|
+
// Same gatherer `ocx doctor` uses, so the two commands cannot reach different verdicts
|
|
343
|
+
// about the same on-disk state (review found them diverging on fallback ports).
|
|
344
|
+
const staleProcessState = await probeUncleanExitState({
|
|
345
|
+
live: Boolean(live),
|
|
346
|
+
port: config.port,
|
|
347
|
+
hostname: config.hostname,
|
|
348
|
+
});
|
|
213
349
|
const bunRuntime = durableBunRuntime();
|
|
214
350
|
const service = diagnoseService();
|
|
215
351
|
// A service can be registered and still not serve: the manager reports the job
|
|
@@ -326,6 +462,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
326
462
|
proxy: {
|
|
327
463
|
running: Boolean(live) || Boolean(pid && health.ok),
|
|
328
464
|
pid: live?.pid ?? pid,
|
|
465
|
+
staleProcessState,
|
|
329
466
|
health: {
|
|
330
467
|
ok: health.ok,
|
|
331
468
|
url: health.url,
|
|
@@ -359,6 +496,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
359
496
|
codexPlugins,
|
|
360
497
|
codexRuntime,
|
|
361
498
|
codexHome,
|
|
499
|
+
claudeDesktop,
|
|
362
500
|
// Own field rather than a line in `codexRuntime.warning`: a stale ocx on PATH is a
|
|
363
501
|
// fact about this install, not about the Codex runtime, and filing it there would
|
|
364
502
|
// print it under the wrong heading (#2701).
|
package/src/cli/storage.ts
CHANGED
|
@@ -173,7 +173,16 @@ async function policy(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
173
173
|
}
|
|
174
174
|
const body: Record<string, unknown> = {};
|
|
175
175
|
if (enabled !== undefined) body.enabled = enabled === "true";
|
|
176
|
-
|
|
176
|
+
// The policy target is nested. A top-level `percent` is not part of the PUT contract:
|
|
177
|
+
// `normalizeStorageCleanupPolicy` reads only `target`, so the field was dropped and the
|
|
178
|
+
// previously stored target survived. `--percent 10` on a policy still holding the
|
|
179
|
+
// default 25% therefore reported success while leaving cleanup authorized to delete
|
|
180
|
+
// more than the operator asked for.
|
|
181
|
+
//
|
|
182
|
+
// An out-of-range value is deliberately still sent: the server owns the 1-100
|
|
183
|
+
// vocabulary and answers with a named 400, which is a rejected write rather than the
|
|
184
|
+
// silent wrong write this replaces.
|
|
185
|
+
if (percent !== undefined) body.target = { removeOldestPercent: percent };
|
|
177
186
|
if (mode !== undefined) body.mode = mode;
|
|
178
187
|
if (schedule !== undefined) body.schedule = schedule;
|
|
179
188
|
if (Object.keys(body).length === 0) {
|
|
@@ -1,18 +1,43 @@
|
|
|
1
1
|
import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
|
|
2
|
+
import { isCodexAccountGenerationLive } from "./account-store";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Accounts quarantined for reauthentication, each remembering WHICH credential produced the
|
|
6
|
+
* evidence (#2892 gap 4).
|
|
7
|
+
*
|
|
8
|
+
* A 401 describes one credential, not an account. Recording only the id let a 401 raced by a
|
|
9
|
+
* cross-process credential replacement quarantine the replacement: the flag outlived the credential
|
|
10
|
+
* it was evidence about, and routing then refused a perfectly good credential until a restart. A
|
|
11
|
+
* post-write re-read cannot fix that — the replacement may land at any point after the write — so
|
|
12
|
+
* the generation travels WITH the flag and is checked when the flag is read.
|
|
13
|
+
*
|
|
14
|
+
* `undefined` means "no credential generation was supplied", which stays account-wide: callers such
|
|
15
|
+
* as a login flow have no specific credential in hand, and their quarantine must not silently expire.
|
|
16
|
+
*/
|
|
17
|
+
const reauthAccounts = new Map<string, number | undefined>();
|
|
4
18
|
let lastReconciledGeneration = 0;
|
|
5
19
|
let liveAccountIds = new Set<string>();
|
|
6
20
|
|
|
7
|
-
export function markAccountNeedsReauth(
|
|
21
|
+
export function markAccountNeedsReauth(
|
|
22
|
+
id: string,
|
|
23
|
+
writerGeneration = captureConfigGeneration(),
|
|
24
|
+
credentialGeneration?: number,
|
|
25
|
+
): void {
|
|
8
26
|
if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
|
|
9
|
-
|
|
27
|
+
// An account-wide mark supersedes a generation-scoped one: it is the stronger claim.
|
|
28
|
+
if (credentialGeneration === undefined || !reauthAccounts.has(id)) {
|
|
29
|
+
reauthAccounts.set(id, credentialGeneration);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const existing = reauthAccounts.get(id);
|
|
33
|
+
if (existing === undefined) return;
|
|
34
|
+
reauthAccounts.set(id, Math.max(existing, credentialGeneration));
|
|
10
35
|
}
|
|
11
36
|
|
|
12
37
|
export function reconcileCodexReauthState(context: GenerationContext): number {
|
|
13
38
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
14
39
|
let removed = 0;
|
|
15
|
-
for (const id of reauthAccounts) {
|
|
40
|
+
for (const id of [...reauthAccounts.keys()]) {
|
|
16
41
|
if (context.codexAccountIds.has(id)) continue;
|
|
17
42
|
reauthAccounts.delete(id);
|
|
18
43
|
removed += 1;
|
|
@@ -23,7 +48,16 @@ export function reconcileCodexReauthState(context: GenerationContext): number {
|
|
|
23
48
|
}
|
|
24
49
|
|
|
25
50
|
export function isAccountNeedsReauth(id: string): boolean {
|
|
26
|
-
|
|
51
|
+
if (!reauthAccounts.has(id)) return false;
|
|
52
|
+
const credentialGeneration = reauthAccounts.get(id);
|
|
53
|
+
if (credentialGeneration === undefined) return true;
|
|
54
|
+
// The credential this evidence describes is gone, so the evidence is spent. Drop it rather than
|
|
55
|
+
// re-deriving the same answer on every read.
|
|
56
|
+
if (!isCodexAccountGenerationLive(id, credentialGeneration)) {
|
|
57
|
+
reauthAccounts.delete(id);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
27
61
|
}
|
|
28
62
|
|
|
29
63
|
export function clearAccountNeedsReauth(id: string): void {
|