@bitkyc08/opencodex 2.14.0 → 2.14.1
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 +55 -0
- package/gui/dist/assets/index-DWhX3yMp.css +1 -0
- package/gui/dist/assets/index-DuaUVm_d.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +31 -2
- package/src/adapters/openai-chat-url.ts +11 -0
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses-url.ts +14 -0
- package/src/adapters/openai-responses.ts +2 -2
- package/src/codex/auth-api.ts +2 -74
- package/src/codex/catalog/parsing.ts +10 -6
- package/src/codex/catalog/sync.ts +10 -1
- package/src/codex/features.ts +14 -3
- package/src/codex/model-cache.ts +7 -1
- package/src/codex/native-main-claim.ts +13 -2
- package/src/generated/compatibility-version.json +35 -19
- package/src/lab/ledger/store.ts +0 -18
- package/src/lab/subject/installation-salt.ts +13 -2
- package/src/providers/registry.ts +5 -5
- package/src/router.ts +12 -1
- package/src/server/index.ts +1 -1
- package/src/server/management/config-routes.ts +51 -16
- package/src/server/responses/core.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/ws-upstream.ts +199 -0
- package/src/vision/index.ts +25 -4
- package/src/vision/timeout-bounds.ts +9 -0
- package/gui/dist/assets/index-BNVYzdn0.css +0 -1
- package/gui/dist/assets/index-Co12XTT-.js +0 -76
package/gui/dist/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
20
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-DuaUVm_d.js"></script>
|
|
20
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DWhX3yMp.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -200,6 +200,8 @@ const MAX_RECENT_COMMIT_LENGTH = 512;
|
|
|
200
200
|
const MAX_GIT_STATUS_LENGTH = 2048;
|
|
201
201
|
/** Keep collected workspace/git metadata fresh for this long (ms) so repeated requests reuse it. */
|
|
202
202
|
const WORKSPACE_METADATA_TTL_MS = 30_000;
|
|
203
|
+
/** Hard cap on cached workspace metadata entries to prevent unbounded growth across distinct cwds. */
|
|
204
|
+
export const MAX_WORKSPACE_METADATA_ENTRIES = 128;
|
|
203
205
|
|
|
204
206
|
/** Derive a bounded project slug from the working directory for the `x-project-slug` header. */
|
|
205
207
|
function projectSlug(cwd: string): string {
|
|
@@ -214,7 +216,32 @@ interface GitWorkspaceInfo {
|
|
|
214
216
|
recentCommits: string[];
|
|
215
217
|
}
|
|
216
218
|
|
|
217
|
-
const workspaceMetadataCache = new Map<string, { collectedAt: number; value: GitWorkspaceInfo }>();
|
|
219
|
+
export const workspaceMetadataCache = new Map<string, { collectedAt: number; value: GitWorkspaceInfo }>();
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Evict expired entries first, then the oldest live entry if at capacity.
|
|
223
|
+
* Called before inserting a new key so the cache never exceeds the cap.
|
|
224
|
+
*/
|
|
225
|
+
export function pruneWorkspaceMetadataCache(now: number): void {
|
|
226
|
+
// Pass 1: remove expired entries.
|
|
227
|
+
for (const [key, entry] of workspaceMetadataCache) {
|
|
228
|
+
if (now - entry.collectedAt >= WORKSPACE_METADATA_TTL_MS) {
|
|
229
|
+
workspaceMetadataCache.delete(key);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Pass 2: if still at capacity, evict the oldest live entry.
|
|
233
|
+
if (workspaceMetadataCache.size >= MAX_WORKSPACE_METADATA_ENTRIES) {
|
|
234
|
+
let oldestKey: string | null = null;
|
|
235
|
+
let oldestAt = Infinity;
|
|
236
|
+
for (const [key, entry] of workspaceMetadataCache) {
|
|
237
|
+
if (entry.collectedAt < oldestAt) {
|
|
238
|
+
oldestAt = entry.collectedAt;
|
|
239
|
+
oldestKey = key;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (oldestKey !== null) workspaceMetadataCache.delete(oldestKey);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
218
245
|
|
|
219
246
|
const execFile = promisify(execFileCallback);
|
|
220
247
|
|
|
@@ -246,7 +273,9 @@ async function gitWorkspaceInfo(cwd: string | undefined): Promise<GitWorkspaceIn
|
|
|
246
273
|
.map(commit => commit.slice(0, MAX_RECENT_COMMIT_LENGTH)),
|
|
247
274
|
}
|
|
248
275
|
: fallback;
|
|
249
|
-
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
if (!workspaceMetadataCache.has(cwd)) pruneWorkspaceMetadataCache(now);
|
|
278
|
+
workspaceMetadataCache.set(cwd, { collectedAt: now, value });
|
|
250
279
|
return value;
|
|
251
280
|
}
|
|
252
281
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
2
|
+
const TRAILING_CHAT_COMPLETIONS = /\/chat\/completions\/?$/;
|
|
3
|
+
|
|
4
|
+
/** Build the openai-chat send URL from a configured baseUrl.
|
|
5
|
+
* Accepts /v1, /v1/, /v1/chat/completions, and /v1/chat/completions/.
|
|
6
|
+
*/
|
|
7
|
+
export function openaiChatCompletionsUrl(baseUrl: string): string {
|
|
8
|
+
const trimmed = baseUrl.trim().replace(TRAILING_SLASHES, "");
|
|
9
|
+
const withoutEndpoint = trimmed.replace(TRAILING_CHAT_COMPLETIONS, "");
|
|
10
|
+
return `${withoutEndpoint}/chat/completions`;
|
|
11
|
+
}
|
|
@@ -12,6 +12,7 @@ import { identifyRoutedModel } from "./identity";
|
|
|
12
12
|
import { peekReasoningForCall } from "../responses/reasoning-replay-cache";
|
|
13
13
|
import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "./tool-catalog-nudge";
|
|
14
14
|
import { openRouterProviderPayload, resolveOpenRouterRouting } from "../providers/openrouter-routing";
|
|
15
|
+
import { openaiChatCompletionsUrl } from "./openai-chat-url";
|
|
15
16
|
import {
|
|
16
17
|
isTranslatorBudgetExceededError,
|
|
17
18
|
retainTranslatedEventBatch,
|
|
@@ -975,7 +976,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
975
976
|
}
|
|
976
977
|
if (parsed.stream) body.stream_options = { include_usage: true };
|
|
977
978
|
|
|
978
|
-
const url =
|
|
979
|
+
const url = openaiChatCompletionsUrl(provider.baseUrl);
|
|
979
980
|
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
980
981
|
if (hasCredential) headers["Authorization"] = `Bearer ${provider.apiKey}`;
|
|
981
982
|
if (provider.headers) Object.assign(headers, provider.headers);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
2
|
+
const TRAILING_RESPONSES = /\/responses\/?$/;
|
|
3
|
+
const TRAILING_V1 = /\/v1\/?$/;
|
|
4
|
+
|
|
5
|
+
/** Build the default key-auth openai-responses send URL.
|
|
6
|
+
* Accepts /v1, /v1/, /v1/responses, and /v1/responses/.
|
|
7
|
+
* Custom `responsesPath` stays on the adapter; this helper is only the legacy /v1/responses branch.
|
|
8
|
+
*/
|
|
9
|
+
export function openaiResponsesUrl(baseUrl: string): string {
|
|
10
|
+
const trimmed = baseUrl.trim().replace(TRAILING_SLASHES, "");
|
|
11
|
+
const withoutEndpoint = trimmed.replace(TRAILING_RESPONSES, "");
|
|
12
|
+
const withoutV1 = withoutEndpoint.replace(TRAILING_V1, "");
|
|
13
|
+
return `${withoutV1}/v1/responses`;
|
|
14
|
+
}
|
|
@@ -11,6 +11,7 @@ import { OCX_REASONING_PREFIX } from "../responses/reasoning-envelope";
|
|
|
11
11
|
import { modelRecordValue } from "../reasoning-effort";
|
|
12
12
|
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
13
13
|
import { rewriteRoutedCustomToolsForUpstream } from "../responses/custom-tool-compat";
|
|
14
|
+
import { openaiResponsesUrl } from "./openai-responses-url";
|
|
14
15
|
|
|
15
16
|
// Headers relayed verbatim from the caller in OAuth-passthrough ("forward") mode.
|
|
16
17
|
// Exported so the web-search sidecar reuses the exact same forwarded-auth set for its ChatGPT call.
|
|
@@ -1244,8 +1245,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
|
|
|
1244
1245
|
}
|
|
1245
1246
|
} else {
|
|
1246
1247
|
if (provider.responsesPath === undefined) {
|
|
1247
|
-
|
|
1248
|
-
url = `${base}/v1/responses`;
|
|
1248
|
+
url = openaiResponsesUrl(provider.baseUrl);
|
|
1249
1249
|
} else {
|
|
1250
1250
|
const base = provider.baseUrl.replace(/\/$/, "");
|
|
1251
1251
|
url = `${base}${provider.responsesPath}`;
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -82,7 +82,7 @@ export {
|
|
|
82
82
|
setAccountQuotaFromParsed,
|
|
83
83
|
updateAccountQuota,
|
|
84
84
|
} from "./quota";
|
|
85
|
-
import { extractAccountId
|
|
85
|
+
import { extractAccountId } from "../oauth/chatgpt";
|
|
86
86
|
import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
87
87
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
88
88
|
import { reconcileLiveStateStores } from "../lib/state-store-registrations";
|
|
@@ -147,7 +147,6 @@ function nativeMainProfileBusyResponse(): Response {
|
|
|
147
147
|
return response;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
|
|
151
150
|
const CODEX_CREDENTIAL_PERSISTENCE_ERROR = "Account was saved, but credential setup did not complete. Reauthenticate or remove the account.";
|
|
152
151
|
const CODEX_CREDENTIAL_PERSISTENCE_CODE = "codex_credential_persistence_failed";
|
|
153
152
|
|
|
@@ -375,10 +374,6 @@ async function readResetCreditJson(
|
|
|
375
374
|
}
|
|
376
375
|
}
|
|
377
376
|
|
|
378
|
-
export function isUnverifiedCodexImportEnabled(): boolean {
|
|
379
|
-
return process.env[MANUAL_IMPORT_ENV] === "1";
|
|
380
|
-
}
|
|
381
|
-
|
|
382
377
|
function manualImportDisabledResponse(): Response {
|
|
383
378
|
return jsonResponse({
|
|
384
379
|
error: "Manual Codex account import is disabled. Use OAuth login to add a pool account.",
|
|
@@ -1368,74 +1363,7 @@ export async function handleCodexAuthAPI(
|
|
|
1368
1363
|
}
|
|
1369
1364
|
|
|
1370
1365
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
let body: { id: string; email: string; plan?: unknown; accessToken: string; refreshToken: string; chatgptAccountId: string };
|
|
1374
|
-
try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
1375
|
-
if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
|
|
1376
|
-
return jsonResponse({ error: "Missing required fields" }, 400);
|
|
1377
|
-
}
|
|
1378
|
-
if (!isValidCodexAccountId(body.id)) {
|
|
1379
|
-
return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
1380
|
-
}
|
|
1381
|
-
if (body.accessToken.length > 10_000 || body.refreshToken.length > 10_000) {
|
|
1382
|
-
return jsonResponse({ error: "Input too large" }, 400);
|
|
1383
|
-
}
|
|
1384
|
-
const runtimeConfig = getRuntimeConfig(config);
|
|
1385
|
-
const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
|
|
1386
|
-
if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
|
|
1387
|
-
// 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
|
|
1388
|
-
const plan = codexPlanValue(body.plan);
|
|
1389
|
-
const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
|
|
1390
|
-
const collision = checkAccountIdCollision(derivedAccountId, body.email, plan);
|
|
1391
|
-
if (collision.collision) {
|
|
1392
|
-
return jsonResponse({ error: collision.reason }, 400);
|
|
1393
|
-
}
|
|
1394
|
-
// 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
|
|
1395
|
-
const payload = decodeJwtPayload(body.accessToken);
|
|
1396
|
-
const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
|
|
1397
|
-
const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
|
|
1398
|
-
if (!warmup.ok) return warmup.response;
|
|
1399
|
-
const latestConfig = getRuntimeConfig(config);
|
|
1400
|
-
const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
|
|
1401
|
-
if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
|
|
1402
|
-
const addedAccount = withCodexAccountLogLabel(
|
|
1403
|
-
{
|
|
1404
|
-
id: body.id,
|
|
1405
|
-
email: body.email,
|
|
1406
|
-
...(plan !== undefined ? { plan } : {}),
|
|
1407
|
-
isMain: false,
|
|
1408
|
-
},
|
|
1409
|
-
latestConfig.codexAccounts ?? [],
|
|
1410
|
-
);
|
|
1411
|
-
const persistence = persistNewCodexAccount(
|
|
1412
|
-
config,
|
|
1413
|
-
latestConfig,
|
|
1414
|
-
addedAccount,
|
|
1415
|
-
{
|
|
1416
|
-
credential: {
|
|
1417
|
-
accessToken: body.accessToken,
|
|
1418
|
-
refreshToken: body.refreshToken,
|
|
1419
|
-
expiresAt: exp,
|
|
1420
|
-
chatgptAccountId: derivedAccountId,
|
|
1421
|
-
},
|
|
1422
|
-
validatedAt: warmup.validatedAt,
|
|
1423
|
-
},
|
|
1424
|
-
);
|
|
1425
|
-
reconcileLiveStateStores();
|
|
1426
|
-
if (persistence.status === "publication-failed") markAccountNeedsReauth(body.id);
|
|
1427
|
-
const catalogRefresh = await convergeAccountNamespaceCatalog(
|
|
1428
|
-
latestConfig,
|
|
1429
|
-
persistence.pickerVisibilityChanged,
|
|
1430
|
-
convergeCodexCatalog,
|
|
1431
|
-
);
|
|
1432
|
-
if (persistence.status === "publication-failed") {
|
|
1433
|
-
return jsonResponse({
|
|
1434
|
-
ok: false,
|
|
1435
|
-
...codexCredentialPersistenceFailure(body.id, catalogRefresh.catalogRefreshPending === true),
|
|
1436
|
-
}, 500);
|
|
1437
|
-
}
|
|
1438
|
-
return jsonResponse({ ok: true, ...catalogRefresh });
|
|
1366
|
+
return manualImportDisabledResponse();
|
|
1439
1367
|
}
|
|
1440
1368
|
|
|
1441
1369
|
if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
|
|
@@ -394,17 +394,21 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
|
|
|
394
394
|
delete entry.supports_reasoning_summaries;
|
|
395
395
|
const isCursorEntry = typeof entry.slug === "string" && entry.slug.startsWith("cursor/");
|
|
396
396
|
// `supports_search_tool` selects Codex's deferred tool-discovery surface; it is not the hosted
|
|
397
|
-
// web-search capability.
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
397
|
+
// web-search capability. Routed rows also carry tool_mode=code_mode_only (below), and under code
|
|
398
|
+
// mode DEFERRED MCP tools remain callable through exec's `tools` global / ALL_TOOLS without any
|
|
399
|
+
// tool_search round-trip (upstream codex-rs code_mode suite; live canary 2026-08-13: routed
|
|
400
|
+
// kimi/k3 called tools.mcp__node_repl__js → isError:false). Stamping false here instead forces
|
|
401
|
+
// every MCP declaration into exec.description — a measured 2.7x turn-1 payload regression
|
|
402
|
+
// (96,699 → 258,929 chars; devlog/_plan/260813_tool_catalog_deferral/010). So non-Cursor routed
|
|
403
|
+
// rows advertise deferred discovery; the #1522 reachability concern is covered by the code-mode
|
|
404
|
+
// path, not by paying the full-catalog tax. Cursor stays false: its runTurn transport bypasses
|
|
405
|
+
// the web-search sidecar and has no proven deferred path.
|
|
402
406
|
if (isCursorEntry) {
|
|
403
407
|
delete entry.web_search_tool_type;
|
|
404
408
|
} else {
|
|
405
409
|
entry.web_search_tool_type = "text_and_image";
|
|
406
410
|
}
|
|
407
|
-
entry.supports_search_tool =
|
|
411
|
+
entry.supports_search_tool = !isCursorEntry;
|
|
408
412
|
// Cursor's transport already serializes overlapping tool calls into atomic Responses tool events.
|
|
409
413
|
// Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI.
|
|
410
414
|
// Opt-in providers (OcxProviderConfig.parallelToolCalls, e.g. xAI) advertise it too: the
|
|
@@ -310,11 +310,20 @@ export function deriveEntry(
|
|
|
310
310
|
});
|
|
311
311
|
}
|
|
312
312
|
// Fallback when no template is available (best-effort; strict parser may need more).
|
|
313
|
+
// Cursor fallback rows mirror normalizeRoutedCatalogEntry: no deferred discovery, no hosted
|
|
314
|
+
// web-search metadata (runTurn transport bypasses the sidecar). Non-Cursor routed fallbacks
|
|
315
|
+
// advertise deferred discovery — code mode keeps deferred MCP callable (devlog
|
|
316
|
+
// 260813_tool_catalog_deferral/010+020); search=false costs a measured 2.7x turn-1 payload.
|
|
317
|
+
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
313
318
|
const entry: RawEntry = {
|
|
314
319
|
slug, display_name: routedDisplayName(slug), description: desc,
|
|
315
320
|
shell_type: "shell_command", visibility: "list", supported_in_api: true,
|
|
316
321
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
317
|
-
...(isRouted
|
|
322
|
+
...(isRouted
|
|
323
|
+
? isCursorFallback
|
|
324
|
+
? { supports_search_tool: false }
|
|
325
|
+
: { web_search_tool_type: "text_and_image", supports_search_tool: true }
|
|
326
|
+
: {}),
|
|
318
327
|
};
|
|
319
328
|
if (isRouted) {
|
|
320
329
|
applyRoutedCodexToolMode(entry);
|
package/src/codex/features.ts
CHANGED
|
@@ -1036,7 +1036,18 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str
|
|
|
1036
1036
|
return setV2StringField("multi_agent_mode_hint_text", value, configPath);
|
|
1037
1037
|
}
|
|
1038
1038
|
|
|
1039
|
-
const
|
|
1039
|
+
export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8;
|
|
1040
|
+
export const modeHintCapabilityCache = new Map<string, boolean | null>();
|
|
1041
|
+
|
|
1042
|
+
export function rememberModeHintCapability(cacheKey: string, capability: boolean | null): void {
|
|
1043
|
+
modeHintCapabilityCache.delete(cacheKey);
|
|
1044
|
+
modeHintCapabilityCache.set(cacheKey, capability);
|
|
1045
|
+
while (modeHintCapabilityCache.size > MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES) {
|
|
1046
|
+
const oldest = modeHintCapabilityCache.keys().next().value;
|
|
1047
|
+
if (oldest === undefined) break;
|
|
1048
|
+
modeHintCapabilityCache.delete(oldest);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1040
1051
|
|
|
1041
1052
|
/**
|
|
1042
1053
|
* True when the installed Codex runtime binary contains the
|
|
@@ -1069,7 +1080,7 @@ export function probeCodexSupportsModeHint(): boolean | null {
|
|
|
1069
1080
|
if (!isNativeExecutable(buf)) continue;
|
|
1070
1081
|
sawBinary = true;
|
|
1071
1082
|
if (buf.includes(Buffer.from("multi_agent_mode_hint_text", "utf8"))) {
|
|
1072
|
-
|
|
1083
|
+
rememberModeHintCapability(cacheKey, true);
|
|
1073
1084
|
return true;
|
|
1074
1085
|
}
|
|
1075
1086
|
} catch {
|
|
@@ -1078,7 +1089,7 @@ export function probeCodexSupportsModeHint(): boolean | null {
|
|
|
1078
1089
|
}
|
|
1079
1090
|
// At least one real binary was inspected and none contained the key.
|
|
1080
1091
|
const result = sawBinary ? false : null;
|
|
1081
|
-
|
|
1092
|
+
rememberModeHintCapability(cacheKey, result);
|
|
1082
1093
|
return result;
|
|
1083
1094
|
} catch {
|
|
1084
1095
|
return null;
|
package/src/codex/model-cache.ts
CHANGED
|
@@ -47,7 +47,7 @@ export type ModelCacheClearReason = "authority" | "eviction";
|
|
|
47
47
|
|
|
48
48
|
const cache = new Map<string, CacheEntry>();
|
|
49
49
|
let globalCacheGeneration = 0;
|
|
50
|
-
const providerCacheGenerations = new Map<string, number>();
|
|
50
|
+
export const providerCacheGenerations = new Map<string, number>();
|
|
51
51
|
let cacheBytes = 0;
|
|
52
52
|
let oldestCachedProvider: string | undefined;
|
|
53
53
|
let oldestCachedAt: number | null = null;
|
|
@@ -235,9 +235,15 @@ export function reconcileModelCacheProviders(
|
|
|
235
235
|
...liveModelCounts.keys(),
|
|
236
236
|
...cache.keys(),
|
|
237
237
|
]);
|
|
238
|
+
let revokedRemovedProviderAuthority = false;
|
|
238
239
|
for (const provider of trackedProviders) {
|
|
239
240
|
if (validProviders.has(provider)) continue;
|
|
241
|
+
if (!revokedRemovedProviderAuthority) {
|
|
242
|
+
globalCacheGeneration += 1;
|
|
243
|
+
revokedRemovedProviderAuthority = true;
|
|
244
|
+
}
|
|
240
245
|
providerCacheGenerations.set(provider, (providerCacheGenerations.get(provider) ?? 0) + 1);
|
|
246
|
+
providerCacheGenerations.delete(provider);
|
|
241
247
|
deleteCachedProvider(provider);
|
|
242
248
|
failureAt.delete(provider);
|
|
243
249
|
discoveryStatus.delete(provider);
|
|
@@ -22,7 +22,18 @@ export interface NativeMainClaimOptions {
|
|
|
22
22
|
env?: NodeJS.ProcessEnv;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const
|
|
25
|
+
export const NATIVE_MAIN_HARDENED_IDENTITY_MAX_ENTRIES = 32;
|
|
26
|
+
export const hardenedIdentities = new Map<string, string>();
|
|
27
|
+
|
|
28
|
+
export function rememberHardenedIdentity(path: string, identity: string): void {
|
|
29
|
+
hardenedIdentities.delete(path);
|
|
30
|
+
hardenedIdentities.set(path, identity);
|
|
31
|
+
while (hardenedIdentities.size > NATIVE_MAIN_HARDENED_IDENTITY_MAX_ENTRIES) {
|
|
32
|
+
const oldest = hardenedIdentities.keys().next().value;
|
|
33
|
+
if (oldest === undefined) break;
|
|
34
|
+
hardenedIdentities.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
26
37
|
|
|
27
38
|
export function nativeMainClaimPath(context: NativeProfileContext): string {
|
|
28
39
|
return join(context.codexHome, NATIVE_MAIN_CLAIM_DB);
|
|
@@ -86,7 +97,7 @@ async function openClaimDatabase(
|
|
|
86
97
|
// tests stayed green, because nearly every claim test injects `hardenPath`.
|
|
87
98
|
await (options.hardenPath ?? ((target: string) => hardenStableLockFile(target, platform)))(path);
|
|
88
99
|
assertStableLockFile(path, file);
|
|
89
|
-
|
|
100
|
+
rememberHardenedIdentity(path, identity);
|
|
90
101
|
}
|
|
91
102
|
database = new Database(path, { create: true });
|
|
92
103
|
// Journal-mode negotiation itself may need a database lock. Disable
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "package.json",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "c2501a020b2b8a7c01f0199dc7a1d7763d82181fe7a329bd2b265fd7474ec994"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "scripts/model-metadata.source.json",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"path": "src/adapters/command-code.ts",
|
|
53
|
-
"sha256": "
|
|
53
|
+
"sha256": "7b969b84c92664588d5b427676743b05169ccc9b26b140e244279186b6c08b4a"
|
|
54
54
|
},
|
|
55
55
|
{
|
|
56
56
|
"path": "src/adapters/cursor.ts",
|
|
@@ -268,13 +268,21 @@
|
|
|
268
268
|
"path": "src/adapters/mimo-free.ts",
|
|
269
269
|
"sha256": "d267d57747f6208850c64b695ee0028748ad1e352a1fd488799e6b4abdb736ef"
|
|
270
270
|
},
|
|
271
|
+
{
|
|
272
|
+
"path": "src/adapters/openai-chat-url.ts",
|
|
273
|
+
"sha256": "3309bcebcf21f35e5a395187a06db326173cd0a7a652a5ecb8abc20713eb8f96"
|
|
274
|
+
},
|
|
271
275
|
{
|
|
272
276
|
"path": "src/adapters/openai-chat.ts",
|
|
273
|
-
"sha256": "
|
|
277
|
+
"sha256": "7700ec687a4cb7516c9cc975ac6726ab1ad858fe4b39a342aea155bd2be291c4"
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"path": "src/adapters/openai-responses-url.ts",
|
|
281
|
+
"sha256": "b3acd48b0b8fcda9f2ebedb51ebfd619e1c1d1adaeca2f6965db853e6ffdc6e4"
|
|
274
282
|
},
|
|
275
283
|
{
|
|
276
284
|
"path": "src/adapters/openai-responses.ts",
|
|
277
|
-
"sha256": "
|
|
285
|
+
"sha256": "78013f261ce7f7b5f06ad8fa02f49c8d5c58ab4a2718d8571d41812cfe71b605"
|
|
278
286
|
},
|
|
279
287
|
{
|
|
280
288
|
"path": "src/adapters/run-turn-queue.ts",
|
|
@@ -602,7 +610,7 @@
|
|
|
602
610
|
},
|
|
603
611
|
{
|
|
604
612
|
"path": "src/codex/auth-api.ts",
|
|
605
|
-
"sha256": "
|
|
613
|
+
"sha256": "074bef369e3d1e300c051076088e01408183d5e95db1b405a0c0f5301e3a37d7"
|
|
606
614
|
},
|
|
607
615
|
{
|
|
608
616
|
"path": "src/codex/auth-collision.ts",
|
|
@@ -666,7 +674,7 @@
|
|
|
666
674
|
},
|
|
667
675
|
{
|
|
668
676
|
"path": "src/codex/catalog/parsing.ts",
|
|
669
|
-
"sha256": "
|
|
677
|
+
"sha256": "b73918da5145d0422484d29bcaf05d00cd044ef1866187ff36168cfcb1aa16d9"
|
|
670
678
|
},
|
|
671
679
|
{
|
|
672
680
|
"path": "src/codex/catalog/provider-fetch.ts",
|
|
@@ -674,7 +682,7 @@
|
|
|
674
682
|
},
|
|
675
683
|
{
|
|
676
684
|
"path": "src/codex/catalog/sync.ts",
|
|
677
|
-
"sha256": "
|
|
685
|
+
"sha256": "c285b373e030af62e33976ccb7e8b0f6ec2741783e24853404fdf1dfc4c19283"
|
|
678
686
|
},
|
|
679
687
|
{
|
|
680
688
|
"path": "src/codex/codex-write-lock.ts",
|
|
@@ -706,7 +714,7 @@
|
|
|
706
714
|
},
|
|
707
715
|
{
|
|
708
716
|
"path": "src/codex/features.ts",
|
|
709
|
-
"sha256": "
|
|
717
|
+
"sha256": "f6e64aa07c4ac8330df679a2b613fac84029d2fe1b8f0d5e8ace27ea664dda16"
|
|
710
718
|
},
|
|
711
719
|
{
|
|
712
720
|
"path": "src/codex/generation.ts",
|
|
@@ -782,7 +790,7 @@
|
|
|
782
790
|
},
|
|
783
791
|
{
|
|
784
792
|
"path": "src/codex/model-cache.ts",
|
|
785
|
-
"sha256": "
|
|
793
|
+
"sha256": "f49cb78b3af7487d1da6f9bbb578615a75644f1a176e5ca89096186219456e94"
|
|
786
794
|
},
|
|
787
795
|
{
|
|
788
796
|
"path": "src/codex/native-main-admission.ts",
|
|
@@ -794,7 +802,7 @@
|
|
|
794
802
|
},
|
|
795
803
|
{
|
|
796
804
|
"path": "src/codex/native-main-claim.ts",
|
|
797
|
-
"sha256": "
|
|
805
|
+
"sha256": "64f5c342243e8420859ba013d987f6d1514b79f57dde662ae721ff9a8da2cf38"
|
|
798
806
|
},
|
|
799
807
|
{
|
|
800
808
|
"path": "src/codex/native-main-lock-file.ts",
|
|
@@ -1314,7 +1322,7 @@
|
|
|
1314
1322
|
},
|
|
1315
1323
|
{
|
|
1316
1324
|
"path": "src/lab/ledger/store.ts",
|
|
1317
|
-
"sha256": "
|
|
1325
|
+
"sha256": "75d8b2581e5766aa5d2b66432f396456a6b12e2a4a2286745b2ba32bc67bf3a7"
|
|
1318
1326
|
},
|
|
1319
1327
|
{
|
|
1320
1328
|
"path": "src/lab/live/credential-lease.ts",
|
|
@@ -1442,7 +1450,7 @@
|
|
|
1442
1450
|
},
|
|
1443
1451
|
{
|
|
1444
1452
|
"path": "src/lab/subject/installation-salt.ts",
|
|
1445
|
-
"sha256": "
|
|
1453
|
+
"sha256": "292ae399517d544d12d1d009510baa43fb150b24e4dcccadc6b5d4db151c2911"
|
|
1446
1454
|
},
|
|
1447
1455
|
{
|
|
1448
1456
|
"path": "src/lab/subject/protocol-subject.ts",
|
|
@@ -1902,7 +1910,7 @@
|
|
|
1902
1910
|
},
|
|
1903
1911
|
{
|
|
1904
1912
|
"path": "src/providers/registry.ts",
|
|
1905
|
-
"sha256": "
|
|
1913
|
+
"sha256": "f2a1b6f538bc7939bb98d9d0187c3350c6c46d9c844a3019a9f56582aa816c12"
|
|
1906
1914
|
},
|
|
1907
1915
|
{
|
|
1908
1916
|
"path": "src/providers/slug-codec.ts",
|
|
@@ -1958,7 +1966,7 @@
|
|
|
1958
1966
|
},
|
|
1959
1967
|
{
|
|
1960
1968
|
"path": "src/router.ts",
|
|
1961
|
-
"sha256": "
|
|
1969
|
+
"sha256": "8d555a3112f6f5aee67f4768880638f729baa145885bce9bd4e4cd5fcce34f7b"
|
|
1962
1970
|
},
|
|
1963
1971
|
{
|
|
1964
1972
|
"path": "src/routing/analytics.ts",
|
|
@@ -2094,7 +2102,7 @@
|
|
|
2094
2102
|
},
|
|
2095
2103
|
{
|
|
2096
2104
|
"path": "src/server/index.ts",
|
|
2097
|
-
"sha256": "
|
|
2105
|
+
"sha256": "cdf8ee40eeed9a69cfcb7ab95730b131ba57dad547349f74d585ffca97f171b9"
|
|
2098
2106
|
},
|
|
2099
2107
|
{
|
|
2100
2108
|
"path": "src/server/lifecycle.ts",
|
|
@@ -2142,7 +2150,7 @@
|
|
|
2142
2150
|
},
|
|
2143
2151
|
{
|
|
2144
2152
|
"path": "src/server/management/config-routes.ts",
|
|
2145
|
-
"sha256": "
|
|
2153
|
+
"sha256": "621ca958cebfdf9e4486af305f31b950768b045a666b5dbf4da5ea4c7153a805"
|
|
2146
2154
|
},
|
|
2147
2155
|
{
|
|
2148
2156
|
"path": "src/server/management/context.ts",
|
|
@@ -2314,7 +2322,7 @@
|
|
|
2314
2322
|
},
|
|
2315
2323
|
{
|
|
2316
2324
|
"path": "src/server/responses/core.ts",
|
|
2317
|
-
"sha256": "
|
|
2325
|
+
"sha256": "e115d89db4e94c910ea74976c259260b6dd9f6d0238a82ba16a3fe49b12bfa86"
|
|
2318
2326
|
},
|
|
2319
2327
|
{
|
|
2320
2328
|
"path": "src/server/responses/encrypted-payload.ts",
|
|
@@ -2322,7 +2330,7 @@
|
|
|
2322
2330
|
},
|
|
2323
2331
|
{
|
|
2324
2332
|
"path": "src/server/responses/fetch-helpers.ts",
|
|
2325
|
-
"sha256": "
|
|
2333
|
+
"sha256": "c0dd5255a485b23fcef54378cd1ea2eac40f7824952050f8e9db9c3d02f0e2d2"
|
|
2326
2334
|
},
|
|
2327
2335
|
{
|
|
2328
2336
|
"path": "src/server/responses/passthrough-error.ts",
|
|
@@ -2340,6 +2348,10 @@
|
|
|
2340
2348
|
"path": "src/server/responses/upstream-error.ts",
|
|
2341
2349
|
"sha256": "e74ff94134f82d594891242b49d03619771f938bde3dc4b6c96a527510da2546"
|
|
2342
2350
|
},
|
|
2351
|
+
{
|
|
2352
|
+
"path": "src/server/responses/ws-upstream.ts",
|
|
2353
|
+
"sha256": "f697ac29a3b1965c4d77531abd69ed382d938ed503c951834205cffe6d8e9757"
|
|
2354
|
+
},
|
|
2343
2355
|
{
|
|
2344
2356
|
"path": "src/server/search.ts",
|
|
2345
2357
|
"sha256": "62a6c940ca5d967ea2a766e6d864c6df77e74130c99d0cacf4f4380cb01fbb5a"
|
|
@@ -2546,12 +2558,16 @@
|
|
|
2546
2558
|
},
|
|
2547
2559
|
{
|
|
2548
2560
|
"path": "src/vision/index.ts",
|
|
2549
|
-
"sha256": "
|
|
2561
|
+
"sha256": "b3c27892a17ea2176c2b6361830800772a6efea5681fff570b3ca5da77626bbc"
|
|
2550
2562
|
},
|
|
2551
2563
|
{
|
|
2552
2564
|
"path": "src/vision/reasoning.ts",
|
|
2553
2565
|
"sha256": "7b5ba571d9e727304f9cc053baaf82a5023fd2dad92f7fbf01af7edb8cfca21a"
|
|
2554
2566
|
},
|
|
2567
|
+
{
|
|
2568
|
+
"path": "src/vision/timeout-bounds.ts",
|
|
2569
|
+
"sha256": "65a3124c6817cda51eb529520652576ffb1e92db433112162c0065104954eaba"
|
|
2570
|
+
},
|
|
2555
2571
|
{
|
|
2556
2572
|
"path": "src/web-search/anthropic-executor.ts",
|
|
2557
2573
|
"sha256": "dc7be63143d4ba70cb5e8127fe2d5231bbabb724ee4715a091bfcc336d48eb32"
|
package/src/lab/ledger/store.ts
CHANGED
|
@@ -25,7 +25,6 @@ export interface LedgerStore {
|
|
|
25
25
|
replay(): ReplayResult;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const eventIdIndexByLedger = new Map<string, Set<string>>();
|
|
29
28
|
const LEDGER_LOCK_STALE_MS = 60_000;
|
|
30
29
|
const LEDGER_LOCK_WAIT_MS = 5_000;
|
|
31
30
|
|
|
@@ -157,21 +156,6 @@ function withLedgerLock<T>(ledgerPath: string, fn: () => T): T {
|
|
|
157
156
|
}
|
|
158
157
|
}
|
|
159
158
|
|
|
160
|
-
/** Load or build the in-memory event-id index for a ledger path. */
|
|
161
|
-
function loadEventIdIndex(ledgerPath: string): Set<string> {
|
|
162
|
-
let index = eventIdIndexByLedger.get(ledgerPath);
|
|
163
|
-
if (!index) {
|
|
164
|
-
index = new Set();
|
|
165
|
-
if (existsSync(ledgerPath)) {
|
|
166
|
-
for (const event of replayLabLedger(ledgerPath).events) {
|
|
167
|
-
index.add(event.eventId);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
eventIdIndexByLedger.set(ledgerPath, index);
|
|
171
|
-
}
|
|
172
|
-
return index;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
159
|
/** Durable append of one validated event as a single JSONL line + fsync. */
|
|
176
160
|
export function appendLabEvent(ledgerPath: string, event: LabEvent): void {
|
|
177
161
|
const validated = validateLabEvent(event);
|
|
@@ -192,7 +176,6 @@ export function appendLabEvent(ledgerPath: string, event: LabEvent): void {
|
|
|
192
176
|
} finally {
|
|
193
177
|
closeSync(fd);
|
|
194
178
|
}
|
|
195
|
-
loadEventIdIndex(ledgerPath).add(validated.eventId);
|
|
196
179
|
}
|
|
197
180
|
|
|
198
181
|
/**
|
|
@@ -209,7 +192,6 @@ export function appendLabEventIfAbsent(ledgerPath: string, event: LabEvent): boo
|
|
|
209
192
|
fresh.add(row.eventId);
|
|
210
193
|
}
|
|
211
194
|
}
|
|
212
|
-
eventIdIndexByLedger.set(ledgerPath, fresh);
|
|
213
195
|
if (fresh.has(validated.eventId)) return false;
|
|
214
196
|
appendLabEvent(ledgerPath, validated);
|
|
215
197
|
return true;
|
|
@@ -4,9 +4,20 @@ import { dirname } from "node:path";
|
|
|
4
4
|
import { labInstallationSaltPath, labRoot } from "../paths";
|
|
5
5
|
|
|
6
6
|
const SALT_BYTES = 32;
|
|
7
|
-
const
|
|
7
|
+
export const INSTALLATION_SALT_CACHE_MAX_ENTRIES = 16;
|
|
8
|
+
export const installationSaltCache = new Map<string, Uint8Array>();
|
|
8
9
|
const UNSUPPORTED_DIRECTORY_FSYNC_CODES = new Set(["EINVAL", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
|
|
9
10
|
|
|
11
|
+
export function rememberInstallationSalt(path: string, salt: Uint8Array): void {
|
|
12
|
+
installationSaltCache.delete(path);
|
|
13
|
+
installationSaltCache.set(path, salt);
|
|
14
|
+
while (installationSaltCache.size > INSTALLATION_SALT_CACHE_MAX_ENTRIES) {
|
|
15
|
+
const oldest = installationSaltCache.keys().next().value;
|
|
16
|
+
if (oldest === undefined) break;
|
|
17
|
+
installationSaltCache.delete(oldest);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
function readSaltFile(path: string): Uint8Array {
|
|
11
22
|
const bytes = readFileSync(path);
|
|
12
23
|
if (bytes.byteLength !== SALT_BYTES) {
|
|
@@ -17,7 +28,7 @@ function readSaltFile(path: string): Uint8Array {
|
|
|
17
28
|
|
|
18
29
|
function cacheSalt(path: string, salt: Uint8Array): Uint8Array {
|
|
19
30
|
const cached = new Uint8Array(salt);
|
|
20
|
-
|
|
31
|
+
rememberInstallationSalt(path, cached);
|
|
21
32
|
return new Uint8Array(cached);
|
|
22
33
|
}
|
|
23
34
|
|