@iislee/opencodex 2.31.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the prompt text Codex actually assembles.
|
|
3
|
+
*
|
|
4
|
+
* The read-only dialog used to say Codex "does not expose" a layer's text. That
|
|
5
|
+
* was wrong: Codex is open source and ships `codex debug prompt-input`, which
|
|
6
|
+
* renders the model-visible input list as JSON. Reading it is the difference
|
|
7
|
+
* between describing a layer and showing it.
|
|
8
|
+
*
|
|
9
|
+
* What this does NOT cover, stated rather than implied:
|
|
10
|
+
*
|
|
11
|
+
* - `base-instructions` is absent. `prompt_debug.rs` returns `prompt.input` and
|
|
12
|
+
* discards `base_instructions`, so the base prompt never appears here.
|
|
13
|
+
* - World-state sections are DIFF-rendered (`add_section` registers state, it does
|
|
14
|
+
* not emit text). A section that renders nothing on a first turn is missing
|
|
15
|
+
* from this output even though its layer exists.
|
|
16
|
+
* - The output reflects the invoking directory and the current config, not a
|
|
17
|
+
* universal prompt.
|
|
18
|
+
*/
|
|
19
|
+
import { spawn } from "node:child_process";
|
|
20
|
+
import { existsSync, statSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { resolveCodexHomeDir } from "./home";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Layer id -> the tag Codex actually renders it under.
|
|
27
|
+
*
|
|
28
|
+
* Every entry here was read off live `codex debug prompt-input` output across the
|
|
29
|
+
* feature flags that enable each section. None is inferred from the Rust section
|
|
30
|
+
* `ID` constants: those are identifiers for diffing, not wrapper tags, and an
|
|
31
|
+
* earlier guess from them mapped `permissions` to the wrong name while the real
|
|
32
|
+
* tag - `permissions instructions`, with a space - went unmatched.
|
|
33
|
+
*
|
|
34
|
+
* A layer absent from this map is reported as unsupported, never as silent.
|
|
35
|
+
*/
|
|
36
|
+
const LAYER_SECTION_TAGS: Record<string, string> = {
|
|
37
|
+
skills: "skills_instructions",
|
|
38
|
+
apps: "apps_instructions",
|
|
39
|
+
plugins: "plugins_instructions",
|
|
40
|
+
environment: "environment_context",
|
|
41
|
+
permissions: "permissions instructions",
|
|
42
|
+
// Synthetic: the project doc carries no tag of its own (see extractSections).
|
|
43
|
+
"agents-md": "__agents_md",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Inventory ids with no confirmed tag in the rendered output. They are listed
|
|
48
|
+
* explicitly rather than left missing: an absent entry made the dialog report a
|
|
49
|
+
* successful probe as unavailable.
|
|
50
|
+
*/
|
|
51
|
+
const UNMAPPED_LAYER_IDS = [
|
|
52
|
+
"model-switch",
|
|
53
|
+
"context-window-guidance",
|
|
54
|
+
"environments-instructions",
|
|
55
|
+
"tools",
|
|
56
|
+
"multi-agent-mode",
|
|
57
|
+
// Confirmed absent from live output under their own feature flags, so the
|
|
58
|
+
// previous `personality` / `realtime` guesses reported these as silent when the
|
|
59
|
+
// truth is that this extractor has no verified tag for them.
|
|
60
|
+
"personality",
|
|
61
|
+
"realtime",
|
|
62
|
+
"collaboration",
|
|
63
|
+
// The Rust source names a <git_attribution> marker pair, but a world-state section is
|
|
64
|
+
// DIFF-rendered: it emits nothing on a turn where its state has not changed. Live
|
|
65
|
+
// `codex debug prompt-input` (codex-cli 0.145.0, 32978 bytes) showed no such block and
|
|
66
|
+
// no attribution text. Listing the id here reports "not exposed" honestly instead of
|
|
67
|
+
// claiming a tag this extractor has never actually matched - the same mistake the
|
|
68
|
+
// header above records for permissions.
|
|
69
|
+
"git-attribution",
|
|
70
|
+
] as const;
|
|
71
|
+
|
|
72
|
+
export interface LayerText {
|
|
73
|
+
/** Rendered text, when this layer produced a section on the probed turn. */
|
|
74
|
+
text: string | null;
|
|
75
|
+
/** Why the text is absent, when it is. */
|
|
76
|
+
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
|
|
77
|
+
bytes: number;
|
|
78
|
+
/** For `empty-source`: the file that exists but has nothing in it. */
|
|
79
|
+
sourcePath?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PromptTextProbe {
|
|
83
|
+
ok: boolean;
|
|
84
|
+
/** The Codex home the probe reported on. */
|
|
85
|
+
codexHome: string;
|
|
86
|
+
layers: Record<string, LayerText>;
|
|
87
|
+
detail?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function resolveCodexBinary(): string | null {
|
|
91
|
+
const candidates = [
|
|
92
|
+
join(homedir(), ".codex/packages/standalone/current/bin/codex"),
|
|
93
|
+
join(homedir(), ".local/bin/codex"),
|
|
94
|
+
"/usr/local/bin/codex",
|
|
95
|
+
"/opt/homebrew/bin/codex",
|
|
96
|
+
];
|
|
97
|
+
return candidates.find(path => existsSync(path)) ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 8 MiB is far above any real prompt and far below anything that hurts the server. */
|
|
101
|
+
const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
102
|
+
|
|
103
|
+
function runProbe(binary: string, cwd: string, timeoutMs: number): Promise<string | null> {
|
|
104
|
+
return new Promise(resolve => {
|
|
105
|
+
// A probe must never hang OR balloon the management API: it is bounded in
|
|
106
|
+
// time AND in bytes, and every failure degrades to "unavailable" rather than
|
|
107
|
+
// an error page.
|
|
108
|
+
const child = spawn(binary, ["debug", "prompt-input"], {
|
|
109
|
+
cwd,
|
|
110
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
111
|
+
});
|
|
112
|
+
const chunks: Buffer[] = [];
|
|
113
|
+
let size = 0;
|
|
114
|
+
let settled = false;
|
|
115
|
+
// One settlement path: a timeout that resolved before `close` used to leave
|
|
116
|
+
// the child streaming into a buffer nobody would ever read.
|
|
117
|
+
const settle = (value: string | null) => {
|
|
118
|
+
if (settled) return;
|
|
119
|
+
settled = true;
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
child.stdout?.destroy();
|
|
122
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
123
|
+
resolve(value);
|
|
124
|
+
};
|
|
125
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
126
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
127
|
+
size += chunk.length;
|
|
128
|
+
if (size > MAX_PROBE_OUTPUT_BYTES) { settle(null); return; }
|
|
129
|
+
chunks.push(chunk);
|
|
130
|
+
});
|
|
131
|
+
child.on("error", () => settle(null));
|
|
132
|
+
child.on("close", code => {
|
|
133
|
+
// Decode once, at the end: `String(chunk)` per chunk corrupts any UTF-8
|
|
134
|
+
// character that straddles a chunk boundary.
|
|
135
|
+
settle(code === 0 ? Buffer.concat(chunks).toString("utf8") : null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Pull every `<tag>...</tag>` section out of the rendered developer messages. */
|
|
141
|
+
function extractSections(raw: string): Map<string, string> {
|
|
142
|
+
const sections = new Map<string, string>();
|
|
143
|
+
let parsed: unknown;
|
|
144
|
+
try {
|
|
145
|
+
parsed = JSON.parse(raw);
|
|
146
|
+
} catch {
|
|
147
|
+
return sections;
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(parsed)) return sections;
|
|
150
|
+
for (const item of parsed) {
|
|
151
|
+
const content = (item as { content?: unknown }).content;
|
|
152
|
+
if (!Array.isArray(content)) continue;
|
|
153
|
+
const text = content.map(part => String((part as { text?: unknown }).text ?? "")).join("");
|
|
154
|
+
// Tag names are NOT all snake_case: Codex renders `<permissions instructions>`
|
|
155
|
+
// with a space. A `[a-z_]+` pattern silently skipped it, and the layer was
|
|
156
|
+
// reported as "sent nothing" while its text sat right there.
|
|
157
|
+
for (const match of text.matchAll(/<([a-zA-Z_][a-zA-Z0-9_ -]*)>([\s\S]*?)<\/\1>/g)) {
|
|
158
|
+
sections.set(match[1]!, match[2]!.trim());
|
|
159
|
+
}
|
|
160
|
+
// AGENTS.md is NOT tagged: it arrives as a plain `# AGENTS.md instructions
|
|
161
|
+
// for <path>` block among the tagged sections. Matching only on tags would
|
|
162
|
+
// report the layer as unrendered while its text sits in the same message.
|
|
163
|
+
//
|
|
164
|
+
// Bounded at both ends. Capturing to end-of-message swept up any unrelated
|
|
165
|
+
// untagged prose that happened to follow, and stripping tag-shaped blocks
|
|
166
|
+
// first also deleted XML-like text the user had written INSIDE their own
|
|
167
|
+
// AGENTS.md. Codex wraps the body in <INSTRUCTIONS>, so that is the boundary.
|
|
168
|
+
// No line anchor: the block is concatenated directly onto the previous
|
|
169
|
+
// section's closing tag, so requiring a newline before it never matched.
|
|
170
|
+
const projectDoc = /# AGENTS\.md instructions for [^\n]*\n+<INSTRUCTIONS>\n?([\s\S]*?)\n?<\/INSTRUCTIONS>/.exec(text);
|
|
171
|
+
if (projectDoc) sections.set("__agents_md", projectDoc[1]!.trim());
|
|
172
|
+
}
|
|
173
|
+
return sections;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Test seam: the extraction is the part worth pinning, not the spawn. */
|
|
177
|
+
export const extractSectionsForTests = extractSections;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Probe once and map every known layer to its rendered text.
|
|
181
|
+
*
|
|
182
|
+
* `cwd` matters: AGENTS.md and environment context are directory-dependent, so a
|
|
183
|
+
* probe from the wrong place would describe a prompt the user never sees.
|
|
184
|
+
*/
|
|
185
|
+
export async function probePromptText(timeoutMs = 15_000): Promise<PromptTextProbe> {
|
|
186
|
+
// The probe runs in CODEX_HOME, never in a caller-supplied directory. A `cwd`
|
|
187
|
+
// parameter let an authenticated request read any readable folder's AGENTS.md,
|
|
188
|
+
// and it also described a prompt that depends on where Codex happened to run.
|
|
189
|
+
// The global home is the one context this page can honestly report on.
|
|
190
|
+
const codexHome = resolveCodexHomeDir();
|
|
191
|
+
const binary = resolveCodexBinary();
|
|
192
|
+
if (!binary) {
|
|
193
|
+
return { ok: false, codexHome, layers: {}, detail: "codex binary not found" };
|
|
194
|
+
}
|
|
195
|
+
const raw = await runProbe(binary, codexHome, timeoutMs);
|
|
196
|
+
if (raw === null) {
|
|
197
|
+
return { ok: false, codexHome, layers: {}, detail: "codex debug prompt-input failed" };
|
|
198
|
+
}
|
|
199
|
+
const sections = extractSections(raw);
|
|
200
|
+
if (sections.size === 0) {
|
|
201
|
+
// Zero sections from a zero-exit probe means the output did not parse, which
|
|
202
|
+
// is a failed read - not fifteen layers that each chose to send nothing.
|
|
203
|
+
return { ok: false, codexHome, layers: {}, detail: "prompt output could not be parsed" };
|
|
204
|
+
}
|
|
205
|
+
const layers: Record<string, LayerText> = {};
|
|
206
|
+
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
|
|
207
|
+
const text = sections.get(tag) ?? null;
|
|
208
|
+
layers[layerId] = text === null
|
|
209
|
+
// Registered but not rendered on this turn, which is an ordinary state for
|
|
210
|
+
// a diff-rendered section rather than an error.
|
|
211
|
+
? { text: null, reason: "not-rendered", bytes: 0 }
|
|
212
|
+
: { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8") };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// A file that exists and is empty is not the same as a layer that chose to send
|
|
216
|
+
// nothing. Reporting "sent nothing" for an empty AGENTS.md tells the user their
|
|
217
|
+
// layer is idle when the real answer is that the file they wrote is blank.
|
|
218
|
+
const agentsMdPath = join(codexHome, "AGENTS.md");
|
|
219
|
+
if (layers["agents-md"]?.reason === "not-rendered" && existsSync(agentsMdPath)) {
|
|
220
|
+
try {
|
|
221
|
+
if (statSync(agentsMdPath).size === 0) {
|
|
222
|
+
layers["agents-md"] = { text: null, reason: "empty-source", bytes: 0, sourcePath: agentsMdPath };
|
|
223
|
+
}
|
|
224
|
+
} catch {
|
|
225
|
+
// An unreadable file stays "not-rendered": we cannot claim it is empty.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// The base prompt travels outside prompt.input and cannot be read this way.
|
|
229
|
+
layers["base-instructions"] = { text: null, reason: "not-exposed", bytes: 0 };
|
|
230
|
+
|
|
231
|
+
// Layers whose rendered tag we have not confirmed against live output. Leaving
|
|
232
|
+
// them absent made the GUI fall through to "unavailable", which claims the probe
|
|
233
|
+
// failed when it succeeded. Saying we have no mapping is the smaller claim.
|
|
234
|
+
for (const id of UNMAPPED_LAYER_IDS) {
|
|
235
|
+
layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, codexHome, layers };
|
|
238
|
+
}
|
package/src/codex/quota.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type StoredAccountQuota = {
|
|
|
23
23
|
shortPercent?: number;
|
|
24
24
|
shortResetAt?: number;
|
|
25
25
|
shortWindowSeconds?: number;
|
|
26
|
+
customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
|
|
26
27
|
resetCredits?: number;
|
|
27
28
|
/**
|
|
28
29
|
* True when `monthlyPercent` came from an explicitly-monthly PRIMARY window —
|
|
@@ -61,6 +62,16 @@ export type WhamUsageResponse = {
|
|
|
61
62
|
rate_limit_reset_credits?: {
|
|
62
63
|
available_count: number;
|
|
63
64
|
} | null;
|
|
65
|
+
additional_rate_limits?: WhamAdditionalRateLimit[] | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type WhamAdditionalRateLimit = {
|
|
69
|
+
limit_name?: unknown;
|
|
70
|
+
metered_feature?: unknown;
|
|
71
|
+
rate_limit?: {
|
|
72
|
+
primary_window?: WhamUsageWindow | null;
|
|
73
|
+
secondary_window?: WhamUsageWindow | null;
|
|
74
|
+
} | null;
|
|
64
75
|
};
|
|
65
76
|
|
|
66
77
|
type WhamUsageWindow = {
|
|
@@ -84,6 +95,9 @@ const MONTHLY_WINDOW_MIN_SECONDS = 28 * 24 * 60 * 60;
|
|
|
84
95
|
*/
|
|
85
96
|
const WEEKLY_WINDOW_MIN_SECONDS = 24 * 60 * 60;
|
|
86
97
|
const MONTHLY_WINDOW_MIN_MINUTES = MONTHLY_WINDOW_MIN_SECONDS / 60;
|
|
98
|
+
// Derived, never written as a literal: the header parser and the WHAM parser must not be able
|
|
99
|
+
// to drift to different thresholds, which is the class of defect this pair exists to prevent.
|
|
100
|
+
const WEEKLY_WINDOW_MIN_MINUTES = WEEKLY_WINDOW_MIN_SECONDS / 60;
|
|
87
101
|
|
|
88
102
|
const accountQuota = new Map<string, StoredAccountQuota>();
|
|
89
103
|
let lastReconciledGeneration = 0;
|
|
@@ -187,7 +201,8 @@ function normalizeResetAt(value: unknown): number | undefined {
|
|
|
187
201
|
|
|
188
202
|
function hasKnownQuotaValue(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
189
203
|
return [quota.weeklyPercent, quota.monthlyPercent, quota.shortPercent]
|
|
190
|
-
.some(value => typeof value === "number" && Number.isFinite(value))
|
|
204
|
+
.some(value => typeof value === "number" && Number.isFinite(value))
|
|
205
|
+
|| !!quota.customWindows?.some(window => Number.isFinite(window.percent));
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
/** True only for a window that DECLARES a duration shorter than a day. */
|
|
@@ -207,14 +222,24 @@ function isExplicitMonthlyWindow(window: WhamUsageWindow | null | undefined): bo
|
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
function isExplicitMonthlyWindowMinutes(windowMinutes: unknown): boolean {
|
|
210
|
-
const minutes =
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
225
|
+
const minutes = windowMinutes_(windowMinutes);
|
|
226
|
+
return minutes !== undefined && minutes >= MONTHLY_WINDOW_MIN_MINUTES;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The header wire reports a window duration in MINUTES; WHAM reports it in seconds. */
|
|
230
|
+
function windowMinutes_(value: unknown): number | undefined {
|
|
231
|
+
const minutes = typeof value === "number"
|
|
232
|
+
? value
|
|
233
|
+
: typeof value === "string" && value.trim() !== ""
|
|
234
|
+
? Number(value)
|
|
214
235
|
: undefined;
|
|
215
|
-
return typeof minutes === "number"
|
|
216
|
-
|
|
217
|
-
|
|
236
|
+
return typeof minutes === "number" && Number.isFinite(minutes) ? minutes : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Minutes-domain twin of isExplicitShortWindow. Same strict `<`, same 24h discriminator. */
|
|
240
|
+
function isExplicitShortWindowMinutes(value: unknown): boolean {
|
|
241
|
+
const minutes = windowMinutes_(value);
|
|
242
|
+
return minutes !== undefined && minutes > 0 && minutes < WEEKLY_WINDOW_MIN_MINUTES;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
|
|
@@ -232,8 +257,12 @@ function snapshotHasShort(quota: Omit<StoredAccountQuota, "updatedAt">): boolean
|
|
|
232
257
|
|| quota.shortWindowSeconds !== undefined;
|
|
233
258
|
}
|
|
234
259
|
|
|
260
|
+
function snapshotHasCustom(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
261
|
+
return quota.customWindows !== undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
235
264
|
function snapshotHasUsage(quota: Omit<StoredAccountQuota, "updatedAt">): boolean {
|
|
236
|
-
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota);
|
|
265
|
+
return snapshotHasWeekly(quota) || snapshotHasMonthly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota);
|
|
237
266
|
}
|
|
238
267
|
export function setAccountQuotaFromParsed(
|
|
239
268
|
accountId: string,
|
|
@@ -255,6 +284,7 @@ export function setAccountQuotaFromParsed(
|
|
|
255
284
|
if (existing?.shortPercent !== undefined) next.shortPercent = existing.shortPercent;
|
|
256
285
|
if (existing?.shortResetAt !== undefined) next.shortResetAt = existing.shortResetAt;
|
|
257
286
|
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
287
|
+
if (existing?.customWindows !== undefined) next.customWindows = existing.customWindows;
|
|
258
288
|
next.resetCredits = quota.resetCredits;
|
|
259
289
|
accountQuota.set(accountId, next);
|
|
260
290
|
schedulePersistAccountQuotas();
|
|
@@ -279,7 +309,7 @@ export function setAccountQuotaFromParsed(
|
|
|
279
309
|
// while silently dropping `monthlyIsPrimaryWindow` would look like tertiary-only data to
|
|
280
310
|
// any future reader, and that failure would be invisible.
|
|
281
311
|
if (quota.monthlyIsPrimaryWindow === true) next.monthlyIsPrimaryWindow = true;
|
|
282
|
-
} else if ((snapshotHasWeekly(quota) || snapshotHasShort(quota))
|
|
312
|
+
} else if ((snapshotHasWeekly(quota) || snapshotHasShort(quota) || snapshotHasCustom(quota))
|
|
283
313
|
&& existing?.monthlyPercent !== undefined) {
|
|
284
314
|
next.monthlyPercent = existing.monthlyPercent;
|
|
285
315
|
if (existing.monthlyResetAt !== undefined) next.monthlyResetAt = existing.monthlyResetAt;
|
|
@@ -298,6 +328,8 @@ export function setAccountQuotaFromParsed(
|
|
|
298
328
|
if (existing?.shortWindowSeconds !== undefined) next.shortWindowSeconds = existing.shortWindowSeconds;
|
|
299
329
|
}
|
|
300
330
|
|
|
331
|
+
if (snapshotHasCustom(quota)) next.customWindows = quota.customWindows;
|
|
332
|
+
|
|
301
333
|
if (quota.resetCredits !== undefined) next.resetCredits = quota.resetCredits;
|
|
302
334
|
else if (existing?.resetCredits !== undefined) next.resetCredits = existing.resetCredits;
|
|
303
335
|
|
|
@@ -323,6 +355,12 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
323
355
|
const secondaryResetAt = normalizeResetAt(secondaryResetRaw);
|
|
324
356
|
const tertiaryResetAt = normalizeResetAt(tertiaryResetRaw);
|
|
325
357
|
const primaryIsMonthly = primaryRaw !== null && isExplicitMonthlyWindowMinutes(primaryWindowMinutes);
|
|
358
|
+
// Codex removed the 5-hour window and has now restored it for Plus and Team (Pro stays
|
|
359
|
+
// weekly-only). A primary window that DECLARES a sub-day duration is a burst window: folding
|
|
360
|
+
// it into weeklyPercent both discards the real weekly reading and leaves the account looking
|
|
361
|
+
// exhausted long after the burst window resets. Duration decides, exactly as parseUsageQuota
|
|
362
|
+
// already does for the WHAM payload — the two parsers must not disagree about the same data.
|
|
363
|
+
const primaryIsShort = primaryRaw !== null && isExplicitShortWindowMinutes(primaryWindowMinutes);
|
|
326
364
|
|
|
327
365
|
if (primaryIsMonthly) {
|
|
328
366
|
if (primaryPercent !== undefined) {
|
|
@@ -337,6 +375,19 @@ export function parseUpstreamQuotaHeaders(headers: Headers): Omit<StoredAccountQ
|
|
|
337
375
|
quota.weeklyPercent = secondaryPercent;
|
|
338
376
|
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
339
377
|
}
|
|
378
|
+
} else if (primaryIsShort) {
|
|
379
|
+
if (primaryPercent !== undefined) {
|
|
380
|
+
quota.shortPercent = primaryPercent;
|
|
381
|
+
if (primaryResetAt !== undefined) quota.shortResetAt = primaryResetAt;
|
|
382
|
+
const minutes = windowMinutes_(primaryWindowMinutes);
|
|
383
|
+
if (minutes !== undefined) quota.shortWindowSeconds = Math.round(minutes * 60);
|
|
384
|
+
}
|
|
385
|
+
// The burst window vacates the primary slot, so the weekly reading is the secondary — which
|
|
386
|
+
// is where it was all along. Without this the true weekly value is silently dropped.
|
|
387
|
+
if (secondaryPercent !== undefined) {
|
|
388
|
+
quota.weeklyPercent = secondaryPercent;
|
|
389
|
+
if (secondaryResetAt !== undefined) quota.weeklyResetAt = secondaryResetAt;
|
|
390
|
+
}
|
|
340
391
|
} else {
|
|
341
392
|
const weeklyPercent = primaryPercent ?? secondaryPercent;
|
|
342
393
|
const weeklyResetAt = primaryPercent !== undefined
|
|
@@ -394,6 +445,7 @@ export function updateAccountQuota(
|
|
|
394
445
|
...(existing?.shortPercent !== undefined ? { shortPercent: existing.shortPercent } : {}),
|
|
395
446
|
...(existing?.shortResetAt !== undefined ? { shortResetAt: existing.shortResetAt } : {}),
|
|
396
447
|
...(existing?.shortWindowSeconds !== undefined ? { shortWindowSeconds: existing.shortWindowSeconds } : {}),
|
|
448
|
+
...(existing?.customWindows !== undefined ? { customWindows: existing.customWindows } : {}),
|
|
397
449
|
...(existing?.resetCredits !== undefined ? { resetCredits: existing.resetCredits } : {}),
|
|
398
450
|
updatedAt: Date.now(),
|
|
399
451
|
};
|
|
@@ -506,6 +558,9 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
506
558
|
: undefined;
|
|
507
559
|
|
|
508
560
|
if (!data.rate_limit) {
|
|
561
|
+
if (data.additional_rate_limits?.length) {
|
|
562
|
+
return parseUsageQuota({ ...data, rate_limit: {} });
|
|
563
|
+
}
|
|
509
564
|
return resetCredits !== undefined ? { resetCredits } : null;
|
|
510
565
|
}
|
|
511
566
|
|
|
@@ -567,6 +622,32 @@ export function parseUsageQuota(data: WhamUsageResponse): Omit<StoredAccountQuot
|
|
|
567
622
|
// different period, so recovery must not treat the two as interchangeable.
|
|
568
623
|
if (primaryIsMonthly && primaryPercent !== undefined) quota.monthlyIsPrimaryWindow = true;
|
|
569
624
|
}
|
|
625
|
+
|
|
626
|
+
const spark = data.additional_rate_limits?.find(additional => {
|
|
627
|
+
const name = String(additional.limit_name ?? "").toLowerCase();
|
|
628
|
+
const feature = String(additional.metered_feature ?? "").toLowerCase();
|
|
629
|
+
return feature === "codex_bengalfox" || name.includes("gpt-5.3-codex-spark");
|
|
630
|
+
});
|
|
631
|
+
const sparkWindows = [spark?.rate_limit?.primary_window, spark?.rate_limit?.secondary_window]
|
|
632
|
+
.filter((window): window is WhamUsageWindow => !!window);
|
|
633
|
+
const sparkWeekly = sparkWindows.find(window => {
|
|
634
|
+
const percent = normalizeUsagePercent(window.used_percent);
|
|
635
|
+
const seconds = window.limit_window_seconds;
|
|
636
|
+
return percent !== undefined
|
|
637
|
+
&& !isExplicitShortWindow(window)
|
|
638
|
+
&& !isExplicitMonthlyWindow(window)
|
|
639
|
+
&& (seconds === undefined || seconds >= WEEKLY_WINDOW_MIN_SECONDS);
|
|
640
|
+
});
|
|
641
|
+
const sparkPercent = normalizeUsagePercent(sparkWeekly?.used_percent);
|
|
642
|
+
if (sparkPercent !== undefined) {
|
|
643
|
+
const sparkWindow: { label: string; percent: number; resetAt?: number } = {
|
|
644
|
+
label: "GPT-5.3-Codex-Spark Weekly",
|
|
645
|
+
percent: sparkPercent,
|
|
646
|
+
};
|
|
647
|
+
const resetAt = normalizeResetAt(sparkWeekly?.reset_at);
|
|
648
|
+
if (resetAt !== undefined) sparkWindow.resetAt = resetAt;
|
|
649
|
+
quota.customWindows = [sparkWindow];
|
|
650
|
+
}
|
|
570
651
|
if (resetCredits !== undefined) quota.resetCredits = resetCredits;
|
|
571
652
|
|
|
572
653
|
return hasKnownQuotaValue(quota) || resetCredits !== undefined ? quota : null;
|
package/src/codex/routing.ts
CHANGED
|
@@ -612,6 +612,15 @@ export function tryAcquireCodexQuotaScopeProbeLease(
|
|
|
612
612
|
return probeLeaseId;
|
|
613
613
|
}
|
|
614
614
|
|
|
615
|
+
/** Side-effect-free check for a confirmed model-specific quota probe. */
|
|
616
|
+
export function canAcquireCodexQuotaScopeProbeLease(
|
|
617
|
+
accountId: string,
|
|
618
|
+
scope: CodexQuotaScope,
|
|
619
|
+
now = Date.now(),
|
|
620
|
+
): boolean {
|
|
621
|
+
return canAcquireQuotaProbeLease(scopedHealthFor(accountId, scope), now);
|
|
622
|
+
}
|
|
623
|
+
|
|
615
624
|
/**
|
|
616
625
|
* Hand a probe lease back without recording an upstream outcome. Used by paths
|
|
617
626
|
* that take a lease and then fail before any request reaches upstream.
|
package/src/codex/shim.ts
CHANGED
|
@@ -603,6 +603,47 @@ function backupPathFor(path: string): string {
|
|
|
603
603
|
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
/**
|
|
607
|
+
* True when a Codex binary lives inside a version manager's install tree.
|
|
608
|
+
*
|
|
609
|
+
* These trees are rewritten in place on upgrade, which destroys both the shim
|
|
610
|
+
* and the sibling .opencodex-real backup it restores from (#2412). The tempting
|
|
611
|
+
* repair — adopt the newly installed binary as a fresh original — is wrong
|
|
612
|
+
* twice: it records a provenance that never happened, and the next upgrade wipes
|
|
613
|
+
* it again, so the repair silently un-repairs on the version manager's schedule.
|
|
614
|
+
*
|
|
615
|
+
* Scope is the three managers named in the report. nvm/fnm/npm-prefix are
|
|
616
|
+
* deliberately excluded: a false positive here refuses a restore that would
|
|
617
|
+
* otherwise be correct.
|
|
618
|
+
*/
|
|
619
|
+
export function isVersionManagerOwnedCodexPath(path: string): boolean {
|
|
620
|
+
const normalized = path.replace(/\\/g, "/").toLowerCase();
|
|
621
|
+
return normalized.includes("/mise/installs/")
|
|
622
|
+
|| normalized.includes("/mise/shims/")
|
|
623
|
+
|| normalized.includes("/.asdf/installs/")
|
|
624
|
+
|| normalized.includes("/.asdf/shims/")
|
|
625
|
+
|| normalized.includes("/.volta/");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Why auto-restore refused, in the operator's own terms. Auto-restore used to
|
|
630
|
+
* return a bare `{ status: "ineligible" }`, and the CLI warns only when a
|
|
631
|
+
* message is present, so `ocx start`, `ocx ensure`, and `ocx service repair` all
|
|
632
|
+
* reported success while routing quietly stayed native (#2412, the cause behind
|
|
633
|
+
* the misleading green status in #2411).
|
|
634
|
+
*/
|
|
635
|
+
function destroyedShimMessage(file: ShimFileState): string {
|
|
636
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
637
|
+
? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
|
|
638
|
+
: "missing";
|
|
639
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
640
|
+
const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
641
|
+
if (!isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
642
|
+
return `${base} Re-run 'ocx codex-shim install' once the Codex binary is stable.`;
|
|
643
|
+
}
|
|
644
|
+
return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
|
|
645
|
+
}
|
|
646
|
+
|
|
606
647
|
function shQuote(value: string): string {
|
|
607
648
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
608
649
|
}
|
|
@@ -2039,14 +2080,20 @@ export function autoRestoreCodexShim(options: {
|
|
|
2039
2080
|
if (seen.has(file.wrapperPath)) return { status: "ineligible" };
|
|
2040
2081
|
seen.add(file.wrapperPath);
|
|
2041
2082
|
if (file.preserveOnly) {
|
|
2042
|
-
if (!existsSync(file.backupPath) || existsSync(file.originalPath))
|
|
2083
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) {
|
|
2084
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2085
|
+
}
|
|
2043
2086
|
continue;
|
|
2044
2087
|
}
|
|
2045
|
-
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file))
|
|
2088
|
+
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) {
|
|
2089
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2090
|
+
}
|
|
2046
2091
|
const probe = stableShimPathProbe(file.wrapperPath);
|
|
2047
2092
|
if (!probe) return { status: "deferred" };
|
|
2048
2093
|
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
2049
|
-
if (!isHealthyShimProbe(probe, state.platform))
|
|
2094
|
+
if (!isHealthyShimProbe(probe, state.platform)) {
|
|
2095
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2096
|
+
}
|
|
2050
2097
|
if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
|
|
2051
2098
|
obsoleteShimProbes.set(file.wrapperPath, probe);
|
|
2052
2099
|
continue;
|
|
@@ -2054,6 +2101,12 @@ export function autoRestoreCodexShim(options: {
|
|
|
2054
2101
|
healthyCount += 1;
|
|
2055
2102
|
continue;
|
|
2056
2103
|
}
|
|
2104
|
+
// A surviving backup would otherwise let the replacement path below wrap the
|
|
2105
|
+
// version manager's NEW binary as a fresh original — the same adoption the
|
|
2106
|
+
// missing-backup case refuses, arriving through the back door.
|
|
2107
|
+
if (isVersionManagerOwnedCodexPath(file.wrapperPath)) {
|
|
2108
|
+
return { status: "ineligible", message: destroyedShimMessage(file) };
|
|
2109
|
+
}
|
|
2057
2110
|
replacementProbes.set(file.wrapperPath, probe);
|
|
2058
2111
|
}
|
|
2059
2112
|
|