@bitkyc08/opencodex 2.58.0 → 2.59.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 +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstatSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { atomicWriteFile, getConfigDir, getConfigPath, mutatePersistedConfig, readConfigDiagnostics, withConfigMutationLockSync } from "../config";
|
|
5
|
+
import { readAlivePid, readRuntimePort } from "../config/process-state";
|
|
6
|
+
import { getCodexHome } from "./paths";
|
|
7
|
+
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
8
|
+
import { assertPlainLocalPath, ORCA_ACCOUNT_DIRECTORY, parseOrcaAuth, readBoundedLocalFile, readOrcaAuthSource, sameLocalPath } from "./orca-auth-source";
|
|
9
|
+
|
|
10
|
+
export class OrcaImportError extends Error {
|
|
11
|
+
constructor(message: string) { super(message); this.name = "OrcaImportError"; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OrcaImportResult {
|
|
15
|
+
mode: "preview" | "apply";
|
|
16
|
+
discovered: number;
|
|
17
|
+
eligible: number;
|
|
18
|
+
imported: number;
|
|
19
|
+
duplicates: number;
|
|
20
|
+
invalid: number;
|
|
21
|
+
invalidReasons: Partial<Record<OrcaImportInvalidReason, number>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type OrcaImportInvalidReason = "unsupported_entry" | "home_mismatch" | "source_invalid";
|
|
25
|
+
|
|
26
|
+
class OrcaImportEntryError extends Error {
|
|
27
|
+
constructor(readonly reason: OrcaImportInvalidReason) { super(reason); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function noteInvalid(result: OrcaImportResult, error: unknown): void {
|
|
31
|
+
const reason = error instanceof OrcaImportEntryError ? error.reason : "source_invalid";
|
|
32
|
+
result.invalid++;
|
|
33
|
+
result.invalidReasons[reason] = (result.invalidReasons[reason] ?? 0) + 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function optionalFile(path: string): string | undefined {
|
|
37
|
+
try { return readBoundedLocalFile(path, 4 * 1024 * 1024); }
|
|
38
|
+
catch (error) {
|
|
39
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
40
|
+
throw new OrcaImportError("A target or main credential file is unreadable; import refused.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function object(value: unknown): Record<string, unknown> {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new OrcaImportError("Invalid existing account state; import refused.");
|
|
46
|
+
return value as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertStopped(): void {
|
|
50
|
+
let running = readAlivePid() !== null;
|
|
51
|
+
const runtime = readRuntimePort();
|
|
52
|
+
if (runtime) {
|
|
53
|
+
try { process.kill(runtime.pid, 0); running = true; }
|
|
54
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") running = true; }
|
|
55
|
+
}
|
|
56
|
+
if (running) throw new OrcaImportError("Stop the opencodex proxy before applying an Orca import.");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function prepare(sourceDir: string, registryPath: string) {
|
|
60
|
+
const configRaw = optionalFile(getConfigPath());
|
|
61
|
+
if (!configRaw || readConfigDiagnostics().source !== "file") {
|
|
62
|
+
throw new OrcaImportError("Initialize a valid opencodex configuration before importing.");
|
|
63
|
+
}
|
|
64
|
+
const config = object(JSON.parse(configRaw));
|
|
65
|
+
const accounts = config.codexAccounts ?? [];
|
|
66
|
+
if (!Array.isArray(accounts)) throw new OrcaImportError("Invalid existing account configuration.");
|
|
67
|
+
const identities = new Set<string>();
|
|
68
|
+
const configuredIds = new Set<string>();
|
|
69
|
+
for (const entry of accounts) {
|
|
70
|
+
const row = object(entry);
|
|
71
|
+
if (typeof row.id === "string") configuredIds.add(row.id);
|
|
72
|
+
if (row.chatgptAccountId !== undefined && typeof row.chatgptAccountId !== "string") throw new OrcaImportError("Invalid existing account identity.");
|
|
73
|
+
if (typeof row.chatgptAccountId === "string" && row.chatgptAccountId) identities.add(row.chatgptAccountId);
|
|
74
|
+
}
|
|
75
|
+
const reservedIdentities = new Set(identities);
|
|
76
|
+
const storedIdentityCounts = new Map<string, number>();
|
|
77
|
+
const recoverable = new Map<string, { id: string; credential: Record<string, unknown> }>();
|
|
78
|
+
const storePath = join(getConfigDir(), "codex-accounts.json");
|
|
79
|
+
const storeRaw = optionalFile(storePath);
|
|
80
|
+
const store = storeRaw === undefined ? {} : object(JSON.parse(storeRaw));
|
|
81
|
+
for (const [id, entry] of Object.entries(store)) {
|
|
82
|
+
const record = object(entry);
|
|
83
|
+
// Retain orphan and tombstoned records; a credential is still evidence of identity.
|
|
84
|
+
if (record.credential === undefined && typeof record.generation === "number") continue;
|
|
85
|
+
const credential = object(record.credential ?? record);
|
|
86
|
+
if (typeof credential.chatgptAccountId !== "string" || !credential.chatgptAccountId
|
|
87
|
+
|| typeof credential.accessToken !== "string" || typeof credential.refreshToken !== "string"
|
|
88
|
+
|| typeof credential.expiresAt !== "number") throw new OrcaImportError("Invalid existing credential store.");
|
|
89
|
+
identities.add(credential.chatgptAccountId);
|
|
90
|
+
storedIdentityCounts.set(credential.chatgptAccountId, (storedIdentityCounts.get(credential.chatgptAccountId) ?? 0) + 1);
|
|
91
|
+
// A process can die after the credential rename but before config registration. Only
|
|
92
|
+
// the untouched initial record emitted by this importer may finish that registration.
|
|
93
|
+
// Older, validated, replaced, tombstoned, or independently owned orphans remain blockers.
|
|
94
|
+
if (id.startsWith("orca-") && ORCA_ACCOUNT_DIRECTORY.test(id.slice(5)) && !configuredIds.has(id)
|
|
95
|
+
&& record.generation === 1 && record.codexValidationPending === true
|
|
96
|
+
&& record.deletedAt === undefined && record.replacedAt === undefined
|
|
97
|
+
&& record.lastCodexValidatedAt === undefined && record.lastCodexValidationStatus === undefined
|
|
98
|
+
&& record.lastCodexValidationError === undefined && record.lastCodexValidationTerminal === undefined
|
|
99
|
+
&& credential.refreshToken === "" && typeof credential.sourceAuthPath === "string"
|
|
100
|
+
&& typeof credential.sourceSubject === "string" && credential.sourceSubject) {
|
|
101
|
+
recoverable.set(credential.chatgptAccountId, { id, credential });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const mainRaw = optionalFile(join(getCodexHome(), "auth.json"));
|
|
105
|
+
if (mainRaw !== undefined) {
|
|
106
|
+
const main = object(JSON.parse(mainRaw));
|
|
107
|
+
if (main.tokens !== undefined) {
|
|
108
|
+
const identity = parseOrcaAuth(mainRaw, false).chatgptAccountId;
|
|
109
|
+
identities.add(identity);
|
|
110
|
+
reservedIdentities.add(identity);
|
|
111
|
+
}
|
|
112
|
+
else if (typeof main.OPENAI_API_KEY !== "string") throw new OrcaImportError("Main Codex identity is unknown; import refused.");
|
|
113
|
+
}
|
|
114
|
+
const root = assertPlainLocalPath(resolve(sourceDir));
|
|
115
|
+
const directory = assertPlainLocalPath(join(root, "codex-accounts"));
|
|
116
|
+
if (!lstatSync(directory).isDirectory()) throw new OrcaImportError("Invalid Orca data directory.");
|
|
117
|
+
const registryRaw = readBoundedLocalFile(resolve(registryPath), 32 * 1024 * 1024);
|
|
118
|
+
const registry = object(JSON.parse(registryRaw));
|
|
119
|
+
const registered = object(registry.settings).codexManagedAccounts;
|
|
120
|
+
if (!Array.isArray(registered) || registered.length > 1024) {
|
|
121
|
+
throw new OrcaImportError("Invalid Orca account registry or account limit exceeded.");
|
|
122
|
+
}
|
|
123
|
+
const result: OrcaImportResult = {
|
|
124
|
+
mode: "preview", discovered: registered.length, eligible: 0, imported: 0,
|
|
125
|
+
duplicates: 0, invalid: 0, invalidReasons: {},
|
|
126
|
+
};
|
|
127
|
+
const candidates: { credential: ReturnType<typeof readOrcaAuthSource>; recoverId?: string }[] = [];
|
|
128
|
+
const sourceIds = new Set<string>();
|
|
129
|
+
for (const entry of registered) {
|
|
130
|
+
try {
|
|
131
|
+
const row = object(entry);
|
|
132
|
+
if (typeof row.id !== "string" || !ORCA_ACCOUNT_DIRECTORY.test(row.id)
|
|
133
|
+
|| typeof row.managedHomePath !== "string"
|
|
134
|
+
|| (row.managedHomeRuntime !== undefined && row.managedHomeRuntime !== "host")) {
|
|
135
|
+
throw new OrcaImportEntryError("unsupported_entry");
|
|
136
|
+
}
|
|
137
|
+
const home = join(directory, row.id, "home");
|
|
138
|
+
if (!sameLocalPath(assertPlainLocalPath(row.managedHomePath), assertPlainLocalPath(home))) {
|
|
139
|
+
throw new OrcaImportEntryError("home_mismatch");
|
|
140
|
+
}
|
|
141
|
+
if (sourceIds.has(row.id)) { result.duplicates++; continue; }
|
|
142
|
+
sourceIds.add(row.id);
|
|
143
|
+
const credential = readOrcaAuthSource(join(home, "auth.json"));
|
|
144
|
+
let recoverId: string | undefined;
|
|
145
|
+
if (identities.has(credential.chatgptAccountId)) {
|
|
146
|
+
const orphan = recoverable.get(credential.chatgptAccountId);
|
|
147
|
+
if (orphan && !reservedIdentities.has(credential.chatgptAccountId)
|
|
148
|
+
&& storedIdentityCounts.get(credential.chatgptAccountId) === 1
|
|
149
|
+
&& sameLocalPath(orphan.credential.sourceAuthPath as string, credential.sourceAuthPath)
|
|
150
|
+
&& orphan.credential.sourceSubject === credential.sourceSubject) {
|
|
151
|
+
recoverId = orphan.id;
|
|
152
|
+
recoverable.delete(credential.chatgptAccountId);
|
|
153
|
+
} else { result.duplicates++; continue; }
|
|
154
|
+
}
|
|
155
|
+
identities.add(credential.chatgptAccountId);
|
|
156
|
+
candidates.push({ credential, recoverId });
|
|
157
|
+
result.eligible++;
|
|
158
|
+
} catch (error) { noteInvalid(result, error); }
|
|
159
|
+
}
|
|
160
|
+
return { configRaw, registryRaw, storeRaw, storePath, store, candidates, result };
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Offline, explicit-source import. The returned report deliberately contains no identities or paths. */
|
|
165
|
+
export function importOrcaAccounts(options: { sourceDir: string; registryPath: string; apply?: boolean }): OrcaImportResult {
|
|
166
|
+
try {
|
|
167
|
+
if (!options.apply) return prepare(options.sourceDir, options.registryPath).result;
|
|
168
|
+
assertStopped();
|
|
169
|
+
return withConfigMutationLockSync(() => {
|
|
170
|
+
assertStopped();
|
|
171
|
+
const state = prepare(options.sourceDir, options.registryPath);
|
|
172
|
+
state.result.mode = "apply";
|
|
173
|
+
if (!state.candidates.length) return state.result;
|
|
174
|
+
const writesCredentials = state.candidates.some(candidate => candidate.recoverId === undefined);
|
|
175
|
+
const additions = state.candidates.map(({ credential, recoverId }) => {
|
|
176
|
+
let id = recoverId;
|
|
177
|
+
if (id === undefined) {
|
|
178
|
+
do { id = `orca-${randomUUID()}`; } while (state.store[id]);
|
|
179
|
+
state.store[id] = { credential, generation: 1, codexValidationPending: true };
|
|
180
|
+
}
|
|
181
|
+
return { id, email: "", alias: "Orca account", isMain: false, chatgptAccountId: credential.chatgptAccountId };
|
|
182
|
+
});
|
|
183
|
+
// Re-read registry and sources immediately before publishing either target file.
|
|
184
|
+
if (readBoundedLocalFile(resolve(options.registryPath), 32 * 1024 * 1024) !== state.registryRaw) {
|
|
185
|
+
throw new OrcaImportError("Orca registry changed during import; retry.");
|
|
186
|
+
}
|
|
187
|
+
for (const { credential } of state.candidates) {
|
|
188
|
+
if (JSON.stringify(readOrcaAuthSource(credential.sourceAuthPath)) !== JSON.stringify(credential)) {
|
|
189
|
+
throw new OrcaImportError("Orca credentials changed during import; retry.");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
assertStopped();
|
|
193
|
+
if (optionalFile(getConfigPath()) !== state.configRaw || optionalFile(state.storePath) !== state.storeRaw) {
|
|
194
|
+
throw new OrcaImportError("Target accounts changed during import; retry.");
|
|
195
|
+
}
|
|
196
|
+
if (writesCredentials) atomicWriteFile(state.storePath, JSON.stringify(state.store, null, 2) + "\n");
|
|
197
|
+
try {
|
|
198
|
+
const outcome = mutatePersistedConfig(config => {
|
|
199
|
+
if (optionalFile(getConfigPath()) !== state.configRaw) throw new OrcaImportError("Target configuration changed; retry.");
|
|
200
|
+
config.codexAccounts = [...(config.codexAccounts ?? []), ...additions];
|
|
201
|
+
return { changed: true, value: undefined };
|
|
202
|
+
});
|
|
203
|
+
if (outcome.status !== "committed") throw new OrcaImportError("Target configuration could not be committed.");
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (writesCredentials) {
|
|
206
|
+
if (state.storeRaw === undefined) unlinkSync(state.storePath);
|
|
207
|
+
else atomicWriteFile(state.storePath, state.storeRaw);
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
advanceCodexCredentialMutationEpoch();
|
|
212
|
+
state.result.imported = additions.length;
|
|
213
|
+
return state.result;
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error instanceof OrcaImportError) throw error;
|
|
217
|
+
throw new OrcaImportError("Orca import could not complete; verify source and target local account files.");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* What this does NOT cover, stated rather than implied:
|
|
10
10
|
*
|
|
11
|
-
* - `base-instructions` is absent
|
|
12
|
-
* discards `base_instructions`, so the base prompt never
|
|
11
|
+
* - `base-instructions` is absent from the probed output: Codex returns
|
|
12
|
+
* `prompt.input` and discards `base_instructions`, so the base prompt never
|
|
13
|
+
* appears there. It is read here from the configured sources instead - the
|
|
14
|
+
* `model_instructions_file` override when one is set, otherwise the selected
|
|
15
|
+
* model's catalog row - and reported separately as `base`.
|
|
13
16
|
* - World-state sections are DIFF-rendered (`add_section` registers state, it does
|
|
14
17
|
* not emit text). A section that renders nothing on a first turn is missing
|
|
15
18
|
* from this output even though its layer exists.
|
|
@@ -17,8 +20,10 @@
|
|
|
17
20
|
* universal prompt.
|
|
18
21
|
*/
|
|
19
22
|
import { spawn } from "node:child_process";
|
|
20
|
-
import { existsSync, statSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
23
|
+
import { closeSync, constants, existsSync, fstatSync, openSync, readSync, statSync } from "node:fs";
|
|
24
|
+
import { dirname, join, resolve } from "node:path";
|
|
25
|
+
import { expandUserPath } from "../config";
|
|
26
|
+
import { parseCatalogJson, readCodexCatalogPathForHome, type RawCatalog, type RawEntry } from "./catalog/parsing";
|
|
22
27
|
import { codexExecInvocation } from "./exec-invocation";
|
|
23
28
|
import { resolveCodexHomeDir } from "./home";
|
|
24
29
|
import {
|
|
@@ -82,10 +87,58 @@ export interface LayerText {
|
|
|
82
87
|
/** Why the text is absent, when it is. */
|
|
83
88
|
reason: "ok" | "empty-source" | "not-rendered" | "not-exposed" | "unavailable";
|
|
84
89
|
bytes: number;
|
|
85
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* `expanded` is text Codex sends as written. `template` is a catalog
|
|
92
|
+
* `instructions_template`, which Codex expands before sending. Absent for every
|
|
93
|
+
* layer read out of the probed output, whose text is rendered by definition.
|
|
94
|
+
*/
|
|
95
|
+
representation?: "expanded" | "template";
|
|
96
|
+
/**
|
|
97
|
+
* For `empty-source`: the file that exists but has nothing in it. For
|
|
98
|
+
* `base-instructions`: the configured file the base prompt was read from, or
|
|
99
|
+
* the one that could not be read.
|
|
100
|
+
*/
|
|
86
101
|
sourcePath?: string;
|
|
87
102
|
}
|
|
88
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Why the base prompt is or is not readable, at the granularity a reader can act
|
|
106
|
+
* on. `LayerText.reason` has five coarse values and cannot express any of this,
|
|
107
|
+
* which is why the detailed answer travels on its own record.
|
|
108
|
+
*/
|
|
109
|
+
export type BasePromptReason =
|
|
110
|
+
| "ok"
|
|
111
|
+
| "config-not-found"
|
|
112
|
+
| "config-unreadable"
|
|
113
|
+
| "config-too-large"
|
|
114
|
+
| "model-not-selected"
|
|
115
|
+
| "model-not-found"
|
|
116
|
+
| "catalog-not-found"
|
|
117
|
+
| "catalog-unreadable"
|
|
118
|
+
| "catalog-too-large"
|
|
119
|
+
| "not-published"
|
|
120
|
+
| "override-empty"
|
|
121
|
+
| "override-not-found"
|
|
122
|
+
| "override-too-large"
|
|
123
|
+
| "override-unreadable";
|
|
124
|
+
|
|
125
|
+
export interface BasePromptText {
|
|
126
|
+
/** The base prompt, when a configured source published one. */
|
|
127
|
+
text: string | null;
|
|
128
|
+
reason: BasePromptReason;
|
|
129
|
+
bytes: number;
|
|
130
|
+
/** The model selected in `config.toml`, when the config named one. */
|
|
131
|
+
model: string | null;
|
|
132
|
+
/** The file the answer came from, when a file was reached at all. */
|
|
133
|
+
sourcePath: string | null;
|
|
134
|
+
/**
|
|
135
|
+
* `expanded` is the text Codex sends. `template` is a catalog
|
|
136
|
+
* `instructions_template`: a template even when it currently carries no
|
|
137
|
+
* placeholder, so it is never presented as the text the model receives.
|
|
138
|
+
*/
|
|
139
|
+
representation: "expanded" | "template" | "unavailable";
|
|
140
|
+
}
|
|
141
|
+
|
|
89
142
|
/**
|
|
90
143
|
* Why a probe failed, as a stable token the caller can branch on. The prose
|
|
91
144
|
* `detail` string is kept for display, but matching on it was never a contract:
|
|
@@ -116,6 +169,12 @@ export interface PromptTextProbe {
|
|
|
116
169
|
/** The Codex home the probe reported on. */
|
|
117
170
|
codexHome: string;
|
|
118
171
|
layers: Record<string, LayerText>;
|
|
172
|
+
/**
|
|
173
|
+
* The base prompt, read from configuration rather than from the probed output.
|
|
174
|
+
* Present on every outcome: it is computed before the subprocess, so it answers
|
|
175
|
+
* even when Codex cannot be resolved, the probe fails, or the caller cancels.
|
|
176
|
+
*/
|
|
177
|
+
base: BasePromptText;
|
|
119
178
|
/** The runtime the probe resolved and tried, when resolution produced one. */
|
|
120
179
|
runtime?: { command: string; source: CodexRuntimeSource };
|
|
121
180
|
/** Stable failure classification; `detail` remains the display string. */
|
|
@@ -129,6 +188,208 @@ const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
|
129
188
|
/** stderr is captured only to classify the failure, never to echo back. */
|
|
130
189
|
const MAX_PROBE_STDERR_BYTES = 64 * 1024;
|
|
131
190
|
|
|
191
|
+
/** Read chunk for local prompt sources; the ceiling is MAX_PROBE_OUTPUT_BYTES. */
|
|
192
|
+
const PROMPT_SOURCE_CHUNK_BYTES = 64 * 1024;
|
|
193
|
+
|
|
194
|
+
function errorWithCode(message: string, code: string): NodeJS.ErrnoException {
|
|
195
|
+
const error = new Error(message) as NodeJS.ErrnoException;
|
|
196
|
+
error.code = code;
|
|
197
|
+
return error;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function errorCodeOf(error: unknown): string | undefined {
|
|
201
|
+
return (error as NodeJS.ErrnoException | null)?.code;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Read one configured prompt source, bounded, from a single descriptor.
|
|
206
|
+
*
|
|
207
|
+
* Three properties are load-bearing, and all three are about this running
|
|
208
|
+
* synchronously on the Bun request thread:
|
|
209
|
+
*
|
|
210
|
+
* - `O_NONBLOCK` is set BEFORE anything else. `openSync(path, "r")` on a FIFO with
|
|
211
|
+
* no writer blocks forever, and nothing can recover it: the probe timeout and
|
|
212
|
+
* request cancellation both need the event loop this call is holding. Windows
|
|
213
|
+
* has no such flag, so the constant is absent there and contributes nothing.
|
|
214
|
+
* - The regular-file check reads `fstatSync` on the OPENED descriptor. A path stat
|
|
215
|
+
* describes whatever the name pointed at a moment ago, not what got opened.
|
|
216
|
+
* - The ceiling is enforced while reading, not from a preflight size. A
|
|
217
|
+
* stat-then-read pair is a window in which the file can change, and it would let
|
|
218
|
+
* an unbounded body through into a management-API response.
|
|
219
|
+
*/
|
|
220
|
+
function readBoundedPromptSource(path: string): string {
|
|
221
|
+
// `O_NONBLOCK` is POSIX-only: Windows omits it from `fs.constants` even though the
|
|
222
|
+
// type declares it, so it is read defensively and contributes nothing there.
|
|
223
|
+
const nonBlocking = (constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0;
|
|
224
|
+
const descriptor = openSync(path, constants.O_RDONLY | nonBlocking);
|
|
225
|
+
try {
|
|
226
|
+
if (!fstatSync(descriptor).isFile()) {
|
|
227
|
+
throw errorWithCode(`prompt source is not a regular file: ${path}`, "EFTYPE");
|
|
228
|
+
}
|
|
229
|
+
const parts: Buffer[] = [];
|
|
230
|
+
const view = Buffer.allocUnsafe(PROMPT_SOURCE_CHUNK_BYTES);
|
|
231
|
+
let total = 0;
|
|
232
|
+
for (;;) {
|
|
233
|
+
const read = readSync(descriptor, view, 0, view.length, total);
|
|
234
|
+
if (read === 0) break;
|
|
235
|
+
total += read;
|
|
236
|
+
// One byte past the ceiling is the whole answer: refuse before the content
|
|
237
|
+
// is retained, so an oversized source costs no more than a bounded read.
|
|
238
|
+
if (total > MAX_PROBE_OUTPUT_BYTES) {
|
|
239
|
+
throw errorWithCode(`prompt source exceeds ${MAX_PROBE_OUTPUT_BYTES} bytes: ${path}`, "EFBIG");
|
|
240
|
+
}
|
|
241
|
+
parts.push(Buffer.from(view.subarray(0, read)));
|
|
242
|
+
}
|
|
243
|
+
return Buffer.concat(parts).toString("utf8");
|
|
244
|
+
} finally {
|
|
245
|
+
try { closeSync(descriptor); } catch { /* the descriptor is already unusable */ }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function unavailableBase(
|
|
250
|
+
reason: BasePromptReason,
|
|
251
|
+
model: string | null = null,
|
|
252
|
+
sourcePath: string | null = null,
|
|
253
|
+
): BasePromptText {
|
|
254
|
+
return { text: null, reason, bytes: 0, model, sourcePath, representation: "unavailable" };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function readableBase(
|
|
258
|
+
text: string,
|
|
259
|
+
representation: "expanded" | "template",
|
|
260
|
+
model: string,
|
|
261
|
+
sourcePath: string,
|
|
262
|
+
): BasePromptText {
|
|
263
|
+
return { text, reason: "ok", bytes: Buffer.byteLength(text, "utf8"), model, sourcePath, representation };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function publishedString(entry: RawEntry, key: string): string | null {
|
|
267
|
+
const value = entry[key];
|
|
268
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The base prompt for the configured model, read from the same files Codex reads.
|
|
273
|
+
*
|
|
274
|
+
* Precedence follows Codex: `model_instructions_file` replaces the base prompt
|
|
275
|
+
* outright, so an override that is set decides the answer whether or not it can be
|
|
276
|
+
* read. Without one, the selected catalog row's `base_instructions` is the
|
|
277
|
+
* published text, and `model_messages.instructions_template` is the fallback -
|
|
278
|
+
* reported as a template, because that is what it is.
|
|
279
|
+
*
|
|
280
|
+
* Every failure is a reason, never an exception: this feeds a GET that must
|
|
281
|
+
* degrade to "we could not read it" rather than to a 500.
|
|
282
|
+
*/
|
|
283
|
+
function readBasePrompt(codexHome: string): BasePromptText {
|
|
284
|
+
const configPath = join(codexHome, "config.toml");
|
|
285
|
+
let configText: string;
|
|
286
|
+
try {
|
|
287
|
+
configText = readBoundedPromptSource(configPath);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// ENOENT is "no config"; anything else is "the config we have could not be
|
|
290
|
+
// read". `existsSync` cannot separate those: it races the read, and it reports
|
|
291
|
+
// a permission failure as absence.
|
|
292
|
+
const code = errorCodeOf(error);
|
|
293
|
+
return unavailableBase(
|
|
294
|
+
code === "ENOENT" ? "config-not-found" : code === "EFBIG" ? "config-too-large" : "config-unreadable",
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
let root: Record<string, unknown>;
|
|
298
|
+
try {
|
|
299
|
+
const parsed = Bun.TOML.parse(configText);
|
|
300
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("config.toml is not a table");
|
|
301
|
+
root = parsed as Record<string, unknown>;
|
|
302
|
+
} catch {
|
|
303
|
+
// The WHOLE document has to parse before any key from it is trusted. Codex
|
|
304
|
+
// rejects a malformed config outright, so a model scraped out of the readable
|
|
305
|
+
// first lines would make this display a prompt that is never sent.
|
|
306
|
+
return unavailableBase("config-unreadable", null, configPath);
|
|
307
|
+
}
|
|
308
|
+
const configuredModel = typeof root.model === "string" ? root.model.trim() : "";
|
|
309
|
+
// A `model_instructions_file` with no model is out of scope here: the base
|
|
310
|
+
// prompt this reports on is the selected model's, and there is no selection.
|
|
311
|
+
if (!configuredModel) return unavailableBase("model-not-selected", null, configPath);
|
|
312
|
+
const model = configuredModel;
|
|
313
|
+
|
|
314
|
+
const override = typeof root.model_instructions_file === "string" ? root.model_instructions_file : null;
|
|
315
|
+
if (override !== null) {
|
|
316
|
+
// A blank value names no file. The key is still set, so Codex does not fall
|
|
317
|
+
// back to the catalog default and neither does this: reporting the catalog row
|
|
318
|
+
// would describe a base prompt this configuration never produces.
|
|
319
|
+
if (override.trim().length === 0) return unavailableBase("override-not-found", model);
|
|
320
|
+
// Relative to the config file's own directory, which is how the rest of this
|
|
321
|
+
// repository resolves the key.
|
|
322
|
+
const overridePath = resolve(dirname(configPath), expandUserPath(override));
|
|
323
|
+
let text: string;
|
|
324
|
+
try {
|
|
325
|
+
text = readBoundedPromptSource(overridePath);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
const code = errorCodeOf(error);
|
|
328
|
+
return unavailableBase(
|
|
329
|
+
code === "ENOENT" ? "override-not-found" : code === "EFBIG" ? "override-too-large" : "override-unreadable",
|
|
330
|
+
model,
|
|
331
|
+
overridePath,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
// Codex rejects this config with "model instructions file is empty", so
|
|
335
|
+
// reporting `ok` here would claim text that is never sent - and claim it for a
|
|
336
|
+
// configuration that does not start.
|
|
337
|
+
if (text.trim().length === 0) return unavailableBase("override-empty", model, overridePath);
|
|
338
|
+
return readableBase(text, "expanded", model, overridePath);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const catalogPath = readCodexCatalogPathForHome(codexHome, configText);
|
|
342
|
+
let catalog: RawCatalog | null;
|
|
343
|
+
try {
|
|
344
|
+
catalog = parseCatalogJson(readBoundedPromptSource(catalogPath));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
const code = errorCodeOf(error);
|
|
347
|
+
return unavailableBase(
|
|
348
|
+
code === "ENOENT" ? "catalog-not-found" : code === "EFBIG" ? "catalog-too-large" : "catalog-unreadable",
|
|
349
|
+
model,
|
|
350
|
+
catalogPath,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (!catalog) return unavailableBase("catalog-unreadable", model, catalogPath);
|
|
354
|
+
// Each candidate is guarded rather than trusted: `parseCatalogJson` validates
|
|
355
|
+
// only that `models` is an array, so a row of `null` would throw on property
|
|
356
|
+
// access and turn this read into a 500 on the management API.
|
|
357
|
+
const entry = catalog.models?.find(candidate => (
|
|
358
|
+
candidate !== null
|
|
359
|
+
&& typeof candidate === "object"
|
|
360
|
+
&& !Array.isArray(candidate)
|
|
361
|
+
&& (candidate.slug === model || candidate.id === model)
|
|
362
|
+
)) ?? null;
|
|
363
|
+
if (!entry) return unavailableBase("model-not-found", model, catalogPath);
|
|
364
|
+
const published = publishedString(entry, "base_instructions");
|
|
365
|
+
if (published) return readableBase(published, "expanded", model, catalogPath);
|
|
366
|
+
const messages = entry.model_messages;
|
|
367
|
+
const template = messages !== null && typeof messages === "object" && !Array.isArray(messages)
|
|
368
|
+
? publishedString(messages as RawEntry, "instructions_template")
|
|
369
|
+
: null;
|
|
370
|
+
if (template) return readableBase(template, "template", model, catalogPath);
|
|
371
|
+
return unavailableBase("not-published", model, catalogPath);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Project the base prompt onto the legacy `base-instructions` layer slot.
|
|
376
|
+
*
|
|
377
|
+
* The slot is lossy by construction - five coarse reasons, no renderer that reads
|
|
378
|
+
* `representation` - so it carries only what it can carry honestly: published text
|
|
379
|
+
* when the source is text Codex sends, and otherwise no text at all. A template is
|
|
380
|
+
* deliberately NOT `ok` here, because the dialog labels every `ok` layer "Text
|
|
381
|
+
* sent to the model" and an unexpanded template is not that. The detailed answer
|
|
382
|
+
* stays on `base`.
|
|
383
|
+
*/
|
|
384
|
+
function promptLayerForBase(base: BasePromptText): LayerText {
|
|
385
|
+
const source = base.sourcePath ? { sourcePath: base.sourcePath } : {};
|
|
386
|
+
if (base.reason === "ok" && base.text !== null && base.representation === "expanded") {
|
|
387
|
+
return { text: base.text, reason: "ok", bytes: base.bytes, representation: "expanded", ...source };
|
|
388
|
+
}
|
|
389
|
+
if (base.reason === "ok") return { text: null, reason: "not-exposed", bytes: 0, representation: "template", ...source };
|
|
390
|
+
return { text: null, reason: "unavailable", bytes: 0, ...source };
|
|
391
|
+
}
|
|
392
|
+
|
|
132
393
|
interface ProbeCommand {
|
|
133
394
|
binary: string;
|
|
134
395
|
args: string[];
|
|
@@ -557,8 +818,13 @@ export async function probePromptText(
|
|
|
557
818
|
// and it also described a prompt that depends on where Codex happened to run.
|
|
558
819
|
// The global home is the one context this page can honestly report on.
|
|
559
820
|
const codexHome = resolveCodexHomeDir();
|
|
821
|
+
// Read before anything is spawned. The base prompt comes from files this process
|
|
822
|
+
// reads itself, so binding it to the subprocess would lose the one layer the
|
|
823
|
+
// probed output never carries the moment Codex is missing, slow, or cancelled.
|
|
824
|
+
const base = readBasePrompt(codexHome);
|
|
825
|
+
const baseLayers = { "base-instructions": promptLayerForBase(base) };
|
|
560
826
|
if (signal?.aborted) {
|
|
561
|
-
return { ok: false, codexHome, layers: {}, detail: "prompt probe cancelled" };
|
|
827
|
+
return { ok: false, codexHome, layers: { ...baseLayers }, base, detail: "prompt probe cancelled" };
|
|
562
828
|
}
|
|
563
829
|
// Resolve through the shared runtime resolver, not a private path list: the
|
|
564
830
|
// old four-path POSIX check could never match the Codex App's Windows install
|
|
@@ -600,7 +866,8 @@ export async function probePromptText(
|
|
|
600
866
|
return {
|
|
601
867
|
ok: false,
|
|
602
868
|
codexHome,
|
|
603
|
-
layers: {},
|
|
869
|
+
layers: { ...baseLayers },
|
|
870
|
+
base,
|
|
604
871
|
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
605
872
|
failure,
|
|
606
873
|
detail: "codex binary not found",
|
|
@@ -618,7 +885,8 @@ export async function probePromptText(
|
|
|
618
885
|
return {
|
|
619
886
|
ok: false,
|
|
620
887
|
codexHome,
|
|
621
|
-
layers: {},
|
|
888
|
+
layers: { ...baseLayers },
|
|
889
|
+
base,
|
|
622
890
|
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
623
891
|
...(outcome.kind === "failed" && outcome.failure ? { failure: outcome.failure } : {}),
|
|
624
892
|
detail: signal?.aborted
|
|
@@ -636,7 +904,8 @@ export async function probePromptText(
|
|
|
636
904
|
return {
|
|
637
905
|
ok: false,
|
|
638
906
|
codexHome,
|
|
639
|
-
layers: {},
|
|
907
|
+
layers: { ...baseLayers },
|
|
908
|
+
base,
|
|
640
909
|
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
641
910
|
failure: probeFailure(
|
|
642
911
|
command,
|
|
@@ -669,8 +938,9 @@ export async function probePromptText(
|
|
|
669
938
|
// An unreadable file stays "not-rendered": we cannot claim it is empty.
|
|
670
939
|
}
|
|
671
940
|
}
|
|
672
|
-
//
|
|
673
|
-
|
|
941
|
+
// Read from configuration above, not from this output: Codex discards
|
|
942
|
+
// `base_instructions` before rendering `prompt.input`.
|
|
943
|
+
Object.assign(layers, baseLayers);
|
|
674
944
|
|
|
675
945
|
// Layers whose rendered tag we have not confirmed against live output. Leaving
|
|
676
946
|
// them absent made the GUI fall through to "unavailable", which claims the probe
|
|
@@ -678,7 +948,7 @@ export async function probePromptText(
|
|
|
678
948
|
for (const id of UNMAPPED_LAYER_IDS) {
|
|
679
949
|
layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
|
|
680
950
|
}
|
|
681
|
-
return { ok: true, codexHome, layers, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
|
|
951
|
+
return { ok: true, codexHome, layers, base, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
|
|
682
952
|
}
|
|
683
953
|
|
|
684
954
|
/** Test-only command seam; production always resolves the installed Codex binary. */
|
|
@@ -105,6 +105,18 @@ export function settleQuotaRecovery(
|
|
|
105
105
|
records.set(accountId, { state: "spent", lineage: fenced });
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Fence a same-grant alias that adopted the credential produced by another account's refresh.
|
|
110
|
+
*
|
|
111
|
+
* Propagation advances the alias without making it a separate credential lineage. Leaving its
|
|
112
|
+
* committed generation unspent would let a later quota poll rotate the same refresh grant again.
|
|
113
|
+
* This deliberately supersedes an old live claim for the alias: that claim targets the
|
|
114
|
+
* pre-propagation generation, and its eventual settlement must not erase this newer fence.
|
|
115
|
+
*/
|
|
116
|
+
export function fencePropagatedQuotaRecovery(accountId: string, lineage: number): void {
|
|
117
|
+
records.set(accountId, { state: "spent", lineage });
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
/**
|
|
109
121
|
* Record a refresh that failed with proof the grant itself is dead.
|
|
110
122
|
*
|