@yansigit/opencodex 2.36.1-dev.20260829.48 → 2.36.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/gui/dist/assets/{ApiKeys-LOOFiZfv.js → ApiKeys-CsmNyf4I.js} +1 -1
- package/gui/dist/assets/{Claude-BwKzpXe3.js → Claude-B4sHsGTD.js} +1 -1
- package/gui/dist/assets/{CodexSet-B5qi9KeE.js → CodexSet-BuAQ4YQn.js} +1 -1
- package/gui/dist/assets/{FileIntegrationPage-B0sUef6W.js → FileIntegrationPage-BC65sEaS.js} +1 -1
- package/gui/dist/assets/{Grok-bsb4n-f4.js → Grok-DNJWlISf.js} +1 -1
- package/gui/dist/assets/{Integrations-CSMwFDvM.js → Integrations-vGJshFZK.js} +2 -2
- package/gui/dist/assets/{IntegrationsOverview-CTORdKJA.js → IntegrationsOverview-DYMkDFHE.js} +1 -1
- package/gui/dist/assets/{Logs-BWYbfELf.js → Logs-Coc7_RPP.js} +1 -1
- package/gui/dist/assets/{Models-CexEtdT1.js → Models-CKVT8Meb.js} +1 -1
- package/gui/dist/assets/{NumberStepper-BapSFQnW.js → NumberStepper-Dug4wR8n.js} +1 -1
- package/gui/dist/assets/{Providers-BwUFeAgA.js → Providers-DyaCHRWL.js} +1 -1
- package/gui/dist/assets/{RestoreDialog-CD8piq90.js → RestoreDialog-BpuvVU2w.js} +1 -1
- package/gui/dist/assets/{Startup-BgX731C2.js → Startup-DAUQqNEN.js} +1 -1
- package/gui/dist/assets/{Storage-Bgq7HphP.js → Storage-DfP95SlJ.js} +1 -1
- package/gui/dist/assets/{Subagents-ChxItbeF.js → Subagents-CMNsZLfk.js} +1 -1
- package/gui/dist/assets/{Usage-BkylzP50.js → Usage-ESFRfBMO.js} +1 -1
- package/gui/dist/assets/{codex-stale-banner-D4gvSWnO.js → codex-stale-banner-W4FhikN7.js} +1 -1
- package/gui/dist/assets/{data-surface-BtO3lwam.js → data-surface-B8PX8rjm.js} +1 -1
- package/gui/dist/assets/{data-surface-pCXChiBf.js → data-surface-CETFXqiA.js} +1 -1
- package/gui/dist/assets/{index-CNopOid3.js → index-DHQHBRhb.js} +3 -3
- package/gui/dist/assets/{model-display-CiUpg8T9.js → model-display-OBTJhQwT.js} +1 -1
- package/gui/dist/assets/{provider-payload-b2jlS-On.js → provider-payload-CJRJUn7w.js} +1 -1
- package/gui/dist/assets/{section-tabs-BP7gEPK6.js → section-tabs-B8sLMiz6.js} +1 -1
- package/gui/dist/assets/shared-DI18-uF7.js +69 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/base.ts +26 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +34 -41
- package/src/adapters/cursor/envelope-echo.ts +128 -0
- package/src/adapters/cursor/request-builder.ts +19 -12
- package/src/adapters/cursor/tool-definitions.ts +2 -1
- package/src/adapters/cursor/tool-result-normalize.ts +23 -31
- package/src/adapters/cursor.ts +21 -2
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +71 -2
- package/src/adapters/google.ts +19 -4
- package/src/adapters/kiro-constants.ts +12 -0
- package/src/adapters/kiro.ts +128 -11
- package/src/adapters/openai-chat.ts +16 -2
- package/src/adapters/openai-responses.ts +15 -2
- package/src/adapters/tool-catalog-nudge.ts +2 -1
- package/src/adapters/xai-web-search.ts +10 -14
- package/src/claude/outbound.ts +14 -3
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +93 -16
- package/src/cli/account-extended.ts +262 -36
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +40 -10
- package/src/cli/agent.ts +8 -1
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +535 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/dispatch.ts +195 -27
- package/src/cli/doctor.ts +100 -1
- package/src/cli/help.ts +11 -2
- package/src/cli/index.ts +19 -3
- package/src/cli/inspect.ts +230 -0
- package/src/cli/observe.ts +11 -3
- package/src/cli/registry.ts +34 -2
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +16 -0
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/usage-report.ts +52 -2
- package/src/cli/version-skew.ts +46 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/catalog/provider-fetch.ts +4 -0
- package/src/codex/transition-state.ts +12 -3
- package/src/compatibility/openai-responses.ts +9 -1
- package/src/generated/compatibility-version.json +94 -58
- package/src/integrations/ownership-policy.ts +24 -5
- package/src/integrations/ownership.ts +36 -2
- package/src/integrations/state.ts +40 -7
- package/src/integrations/writer.ts +21 -3
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/errors.ts +25 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/oauth/store.ts +16 -6
- package/src/providers/label.ts +34 -1
- package/src/responses/turn-termination.ts +107 -0
- package/src/server/management/logs-usage-routes.ts +5 -27
- package/src/server/management/route-registry.ts +317 -0
- package/src/server/proxy-liveness.ts +27 -4
- package/src/server/request-log.ts +37 -17
- package/src/server/responses/core.ts +80 -4
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/service.ts +34 -0
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +79 -33
- package/src/usage/log.ts +32 -6
- package/src/usage/summary.ts +22 -34
- package/gui/dist/assets/shared-BdYGV-yJ.js +0 -69
|
@@ -43,6 +43,10 @@ import {
|
|
|
43
43
|
previousResponseScopeMismatch,
|
|
44
44
|
rememberResponseState,
|
|
45
45
|
} from "../../responses/state";
|
|
46
|
+
import {
|
|
47
|
+
bindTurnTerminationScope,
|
|
48
|
+
rememberDeliveredFinalAnswer,
|
|
49
|
+
} from "../../responses/turn-termination";
|
|
46
50
|
import {
|
|
47
51
|
isValidProviderContinuationOwner,
|
|
48
52
|
mergeProviderContinuationPayload,
|
|
@@ -122,6 +126,7 @@ import {
|
|
|
122
126
|
resolveAnthropicAccountForSession,
|
|
123
127
|
rotateAnthropicAccountOn429,
|
|
124
128
|
} from "../../oauth/anthropic-routing";
|
|
129
|
+
import { stampOAuthAccountLabel } from "../../providers/label";
|
|
125
130
|
import {
|
|
126
131
|
antigravitySessionKeyFromParts,
|
|
127
132
|
bindAntigravitySessionAffinity,
|
|
@@ -2508,6 +2513,10 @@ async function handleResponsesInner(
|
|
|
2508
2513
|
threadIdHeader: req.headers.get("thread-id"),
|
|
2509
2514
|
cursorConversationId: parsed._cursorConversationId,
|
|
2510
2515
|
});
|
|
2516
|
+
bindTurnTerminationScope(parsed, resolvedConversationId);
|
|
2517
|
+
const rememberKiroDeliveredFinalAnswer = (adapterName: string, response: unknown): void => {
|
|
2518
|
+
if (adapterName === "kiro") rememberDeliveredFinalAnswer(parsed, response);
|
|
2519
|
+
};
|
|
2511
2520
|
// _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
|
|
2512
2521
|
// a dedicated raw conversation namespace so mixed headers that carry the same identity still
|
|
2513
2522
|
// match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
|
|
@@ -3040,9 +3049,12 @@ async function handleResponsesInner(
|
|
|
3040
3049
|
accountId: snapshot.accountId,
|
|
3041
3050
|
generation: snapshot.generation,
|
|
3042
3051
|
};
|
|
3043
|
-
logCtx.accountLogLabel = snapshot.accountId ?? genericFailoverAccountId;
|
|
3044
3052
|
bindAntigravitySessionAffinity(antigravitySessionKey, snapshot.accountId ?? genericFailoverAccountId);
|
|
3045
3053
|
}
|
|
3054
|
+
// Re-stamp: a request that rotated accounts must be attributed to the account that actually
|
|
3055
|
+
// served it. All three rotation sites funnel through here, so this is the only re-stamp
|
|
3056
|
+
// needed -- and putting it anywhere else would let one of the three drift.
|
|
3057
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
|
|
3046
3058
|
return true;
|
|
3047
3059
|
};
|
|
3048
3060
|
const oauthSessionKeyParts = {
|
|
@@ -3151,9 +3163,10 @@ async function handleResponsesInner(
|
|
|
3151
3163
|
};
|
|
3152
3164
|
if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
|
|
3153
3165
|
route.provider = { ...route.provider, apiKey: resolved.accessToken };
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3166
|
+
// Attribution is independent of failover (#2699): stamped from the resolved snapshot
|
|
3167
|
+
// itself, not from inside the `isGenericFailoverProvider` branch below, so a future
|
|
3168
|
+
// narrowing of that predicate cannot silently switch attribution off.
|
|
3169
|
+
stampOAuthAccountLabel(logCtx, route.providerName, route.provider, resolved.accountId);
|
|
3157
3170
|
// Remember which account actually served this request so a 429 cools THAT one, not
|
|
3158
3171
|
// whichever account is active by the time the response comes back (#2568).
|
|
3159
3172
|
if (isGenericFailoverProvider(route.providerName, route.provider)) {
|
|
@@ -4810,6 +4823,7 @@ async function handleResponsesInner(
|
|
|
4810
4823
|
...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
|
|
4811
4824
|
onCompletedResponse: (response, providerState) => {
|
|
4812
4825
|
commitReasoningReplayServingRoute();
|
|
4826
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, response);
|
|
4813
4827
|
rememberResponseState(
|
|
4814
4828
|
parsed._rawBody,
|
|
4815
4829
|
response,
|
|
@@ -5135,6 +5149,7 @@ async function handleResponsesInner(
|
|
|
5135
5149
|
},
|
|
5136
5150
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
5137
5151
|
commitReasoningReplayServingRoute();
|
|
5152
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, response);
|
|
5138
5153
|
if (!routedCompaction) {
|
|
5139
5154
|
rememberResponseState(
|
|
5140
5155
|
parsed._rawBody,
|
|
@@ -5204,6 +5219,7 @@ async function handleResponsesInner(
|
|
|
5204
5219
|
},
|
|
5205
5220
|
});
|
|
5206
5221
|
if (!routedCompaction) {
|
|
5222
|
+
rememberKiroDeliveredFinalAnswer(adapter.name, json);
|
|
5207
5223
|
rememberResponseState(
|
|
5208
5224
|
parsed._rawBody,
|
|
5209
5225
|
json,
|
|
@@ -5238,6 +5254,64 @@ async function handleResponsesInner(
|
|
|
5238
5254
|
// reuse is safe, and releaseBodyObservation is idempotent per build.
|
|
5239
5255
|
let initialRequest: AdapterRequest | undefined;
|
|
5240
5256
|
let inputTokenEstimate: number | undefined;
|
|
5257
|
+
// An adapter may know the turn needs no inference at all — Kiro's replayed history ending in a
|
|
5258
|
+
// delivered final answer. Answer it locally: no build (so no token estimate), no send (so
|
|
5259
|
+
// sendCount stays 0), and crucially no empty-completion guard, which treats an outputless
|
|
5260
|
+
// terminal as a failed turn and re-invokes the identical request. Routing this through the
|
|
5261
|
+
// ordinary event path would therefore reinstate the loop it exists to end.
|
|
5262
|
+
const localTerminal = activeAdapter.localTerminal?.(parsed);
|
|
5263
|
+
if (localTerminal) {
|
|
5264
|
+
logCtx.localTerminalReason = localTerminal.reason;
|
|
5265
|
+
// Mark the physical attempt too, not just the parent row. `finishRequestAttempt` finalizes the
|
|
5266
|
+
// attempt through the same estimated-provider path, so without this the row reads exact while
|
|
5267
|
+
// its own attempt still claims an estimate — the detailed accounting a maintainer actually
|
|
5268
|
+
// reads for a zero-send turn.
|
|
5269
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.locallyAnswered = true;
|
|
5270
|
+
cleanupUpstreamAbort();
|
|
5271
|
+
upstream.abort();
|
|
5272
|
+
const terminalEvents: AdapterEvent[] = [{
|
|
5273
|
+
type: "done",
|
|
5274
|
+
endTurn: true,
|
|
5275
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5276
|
+
}];
|
|
5277
|
+
if (parsed.stream) {
|
|
5278
|
+
const localSse = bridgeToResponsesSSE(
|
|
5279
|
+
(async function* () { yield* terminalEvents; })(),
|
|
5280
|
+
parsed._responseModelId ?? parsed.modelId,
|
|
5281
|
+
toolBridgeMaps.toolNsMap,
|
|
5282
|
+
toolBridgeMaps.freeformToolNames,
|
|
5283
|
+
toolBridgeMaps.toolSearchToolNames,
|
|
5284
|
+
undefined,
|
|
5285
|
+
2_000,
|
|
5286
|
+
{
|
|
5287
|
+
translatorBudget,
|
|
5288
|
+
...(options.forceEmptyResponseId ? { responseId: "" } : {}),
|
|
5289
|
+
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
|
|
5290
|
+
},
|
|
5291
|
+
);
|
|
5292
|
+
// Same lifetime tracking as every other streaming return in this function: the turn
|
|
5293
|
+
// admission lease is released when the body finishes or the client disconnects. Returning
|
|
5294
|
+
// the raw stream would hold a lease for a turn that already has all of its output.
|
|
5295
|
+
const localTurnAc = new AbortController();
|
|
5296
|
+
return new Response(
|
|
5297
|
+
trackStreamLifetime(localSse, localTurnAc, undefined, options.turnAdmissionLease),
|
|
5298
|
+
{
|
|
5299
|
+
headers: {
|
|
5300
|
+
"Content-Type": "text/event-stream",
|
|
5301
|
+
"Cache-Control": "no-cache",
|
|
5302
|
+
"Connection": "keep-alive",
|
|
5303
|
+
"X-Accel-Buffering": "no",
|
|
5304
|
+
},
|
|
5305
|
+
},
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
return new Response(
|
|
5309
|
+
JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, {
|
|
5310
|
+
translatorBudget,
|
|
5311
|
+
})),
|
|
5312
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
5313
|
+
);
|
|
5314
|
+
}
|
|
5241
5315
|
try {
|
|
5242
5316
|
assertGoogleOptionsRoute(activeAdapter, route.provider);
|
|
5243
5317
|
initialRequest = await activeAdapter.buildRequest(parsed, {
|
|
@@ -6434,6 +6508,7 @@ async function handleResponsesInner(
|
|
|
6434
6508
|
},
|
|
6435
6509
|
onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
6436
6510
|
commitReasoningReplayServingRoute();
|
|
6511
|
+
rememberKiroDeliveredFinalAnswer(activeAdapter.name, response);
|
|
6437
6512
|
// Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
|
|
6438
6513
|
// PRE-compaction history, and a later previous_response_id expansion would rehydrate the
|
|
6439
6514
|
// giant stale chain Codex just replaced.
|
|
@@ -6511,6 +6586,7 @@ async function handleResponsesInner(
|
|
|
6511
6586
|
});
|
|
6512
6587
|
// See the streaming branch: compaction turns skip the continuation cache.
|
|
6513
6588
|
if (!routedCompaction) {
|
|
6589
|
+
rememberKiroDeliveredFinalAnswer(activeAdapter.name, json);
|
|
6514
6590
|
rememberResponseState(
|
|
6515
6591
|
parsed._rawBody,
|
|
6516
6592
|
json,
|
|
@@ -89,7 +89,7 @@ function finishFailedPolicyAttempt(logCtx: RequestLogContext, status: number): v
|
|
|
89
89
|
const attempt = logCtx.activeAttempt;
|
|
90
90
|
if (attempt) {
|
|
91
91
|
const startedAt = logCtx.activeAttemptStartedAt ?? Date.now();
|
|
92
|
-
finishRequestAttempt(attempt, status, Math.max(0, Date.now() - startedAt), attempt.usage ?? logCtx.usage
|
|
92
|
+
finishRequestAttempt(attempt, status, Math.max(0, Date.now() - startedAt), attempt.usage ?? logCtx.usage);
|
|
93
93
|
}
|
|
94
94
|
delete logCtx.activeAttempt;
|
|
95
95
|
delete logCtx.activeAttemptStartedAt;
|
package/src/service.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from
|
|
|
19
19
|
import type { BunRuntimeSource } from "./lib/bun-runtime";
|
|
20
20
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
21
21
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
22
|
+
import { tokenCollidesWithAdmin } from "./lib/admin-secrets";
|
|
22
23
|
import { PROXY_ENV_KEYS } from "./lib/proxy-env";
|
|
23
24
|
import { randomUUID } from "node:crypto";
|
|
24
25
|
import {
|
|
@@ -366,8 +367,38 @@ export function serviceRetryCommand(
|
|
|
366
367
|
return diag.installed && !diag.conflict ? "ocx service repair" : "ocx service install";
|
|
367
368
|
}
|
|
368
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Refuse a management (admin) token as the data-plane secret.
|
|
372
|
+
*
|
|
373
|
+
* The service exports the contents of the service token file as
|
|
374
|
+
* `OPENCODEX_API_AUTH_TOKEN` before starting the proxy. When that value is the admin
|
|
375
|
+
* token, the server treats the management credential as a data-plane admission secret
|
|
376
|
+
* and fails the ENTIRE management plane closed at boot, so every `/api/*` request
|
|
377
|
+
* returns 503 — even on a loopback install that never needed a data-plane secret.
|
|
378
|
+
* Exporting the admin token in the CLI cannot recover it, because the fence is decided
|
|
379
|
+
* server-side at startup (#2696).
|
|
380
|
+
*
|
|
381
|
+
* Nothing in this codebase puts an admin token in that env var; it arrives from the
|
|
382
|
+
* installing shell. This function is the chokepoint that should refuse it rather than
|
|
383
|
+
* writing a file that produces a broken service. Comparison is the same helper doctor
|
|
384
|
+
* uses: minted `ocx_admin_…` prefix, or byte-equal to configuredAdminToken (env or file).
|
|
385
|
+
*/
|
|
386
|
+
export function assertNotAdminToken(token: string, env: NodeJS.ProcessEnv = process.env): void {
|
|
387
|
+
if (!tokenCollidesWithAdmin(token, env)) return;
|
|
388
|
+
throw new Error(
|
|
389
|
+
"OPENCODEX_API_AUTH_TOKEN holds a management (admin) token. The service exports it "
|
|
390
|
+
+ "as the data-plane secret, which fences the whole management API closed and makes "
|
|
391
|
+
+ "every ocx management command fail with 503. Unset OPENCODEX_API_AUTH_TOKEN, or set "
|
|
392
|
+
+ "it to a distinct data-plane key, then rerun the install.",
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
369
396
|
export function assertServiceAuthEnvironment(): void {
|
|
370
397
|
const config = loadConfig();
|
|
398
|
+
// Check the collision before the loopback short-circuit: a loopback install writes
|
|
399
|
+
// the token file too, so returning early here is what let the broken state through.
|
|
400
|
+
const present = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
401
|
+
if (present) assertNotAdminToken(present);
|
|
371
402
|
if (isLoopbackHostname(config.hostname)) return;
|
|
372
403
|
if (process.env.OPENCODEX_API_AUTH_TOKEN?.trim()) return;
|
|
373
404
|
// Reached from `service repair` as well as `install`, so name a command that can
|
|
@@ -383,6 +414,9 @@ export function assertServiceAuthEnvironment(): void {
|
|
|
383
414
|
function writeServiceApiTokenFile(): string | null {
|
|
384
415
|
const token = process.env.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
385
416
|
if (!token) return null;
|
|
417
|
+
// Last line of defence: every install/repair path funnels through here, so a
|
|
418
|
+
// collision cannot reach disk regardless of which caller ran (#2696).
|
|
419
|
+
assertNotAdminToken(token);
|
|
386
420
|
const path = serviceApiTokenFilePath();
|
|
387
421
|
const dir = getConfigDir();
|
|
388
422
|
recordOwnedConfigPath(dir, path);
|
|
@@ -38,6 +38,7 @@ export interface PolicyJobOutcome {
|
|
|
38
38
|
freedBytes?: number;
|
|
39
39
|
removed?: number;
|
|
40
40
|
trashDir?: string;
|
|
41
|
+
metadataPersistenceError?: PolicyRunResult["metadataPersistenceError"];
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export interface PolicyJobState {
|
|
@@ -246,6 +247,7 @@ export async function abortStorageCleanupPolicyJobAsync(): Promise<void> {
|
|
|
246
247
|
}
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
/** Project a run result into the bounded management-API job outcome. */
|
|
249
251
|
function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
|
|
250
252
|
return {
|
|
251
253
|
ok: result.ok,
|
|
@@ -256,18 +258,26 @@ function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
|
|
|
256
258
|
...(result.freedBytes !== undefined ? { freedBytes: result.freedBytes } : {}),
|
|
257
259
|
...(result.removed !== undefined ? { removed: result.removed } : {}),
|
|
258
260
|
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
261
|
+
...(result.metadataPersistenceError
|
|
262
|
+
? { metadataPersistenceError: result.metadataPersistenceError }
|
|
263
|
+
: {}),
|
|
259
264
|
};
|
|
260
265
|
}
|
|
261
266
|
|
|
267
|
+
/** Publish one completed evaluation without losing successful cleanup effects. */
|
|
262
268
|
function applyFinished(result: PolicyRunResult): void {
|
|
263
269
|
// Prefer the latest persisted policy over `result.policy`. The worker (or
|
|
264
270
|
// in-process run) already merged run metadata into disk; a concurrent PUT
|
|
265
271
|
// may also have landed after that write. Re-reading avoids applying a stale
|
|
266
272
|
// start-of-job snapshot when the run skipped without saving.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
273
|
+
// A best-effort fallback policy may predate concurrent edits; keep the current
|
|
274
|
+
// live config untouched when the durable metadata write did not land.
|
|
275
|
+
if (!result.metadataPersistenceError) {
|
|
276
|
+
try {
|
|
277
|
+
livePolicyApply?.(readStorageCleanupPolicyFromConfig());
|
|
278
|
+
} catch {
|
|
279
|
+
livePolicyApply?.(result.policy);
|
|
280
|
+
}
|
|
271
281
|
}
|
|
272
282
|
state = {
|
|
273
283
|
status: "idle",
|
package/src/storage/policy.ts
CHANGED
|
@@ -53,6 +53,7 @@ function adoptStorageCleanupPolicy(policy: StorageCleanupPolicy): void {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
export type PolicyRunReason = "startup" | "schedule" | "manual";
|
|
56
|
+
export type PolicyMetadataPersistenceError = "missing" | "invalid" | "conflict" | "write_failed";
|
|
56
57
|
|
|
57
58
|
export type PolicySkipReason =
|
|
58
59
|
| "disabled"
|
|
@@ -69,6 +70,7 @@ export interface PolicyRunResult {
|
|
|
69
70
|
freedBytes?: number;
|
|
70
71
|
removed?: number;
|
|
71
72
|
trashDir?: string;
|
|
73
|
+
metadataPersistenceError?: PolicyMetadataPersistenceError;
|
|
72
74
|
policy: StorageCleanupPolicy;
|
|
73
75
|
}
|
|
74
76
|
|
|
@@ -230,14 +232,18 @@ export type PolicyRunMetadataPatch = {
|
|
|
230
232
|
lastRun?: StorageCleanupPolicy["lastRun"];
|
|
231
233
|
};
|
|
232
234
|
|
|
235
|
+
/** Apply only run-owned fields while preserving the supplied policy settings. */
|
|
233
236
|
function applyPolicyRunMetadata(
|
|
234
|
-
|
|
237
|
+
policy: StorageCleanupPolicy,
|
|
235
238
|
patch: PolicyRunMetadataPatch,
|
|
236
239
|
): StorageCleanupPolicy {
|
|
237
|
-
let next =
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
let next =
|
|
241
|
+
patch.nextRun === "defer_busy"
|
|
242
|
+
? deferBusy(policy, patch.now)
|
|
243
|
+
: advanceNextRun(policy, patch.now);
|
|
244
|
+
if (patch.lastRun) {
|
|
245
|
+
next = { ...next, lastRun: patch.lastRun };
|
|
246
|
+
}
|
|
241
247
|
return next;
|
|
242
248
|
}
|
|
243
249
|
|
|
@@ -257,16 +263,51 @@ export function commitPolicyRunMetadata(
|
|
|
257
263
|
return next;
|
|
258
264
|
}
|
|
259
265
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
266
|
+
type PolicyRunMetadataCommit = {
|
|
267
|
+
policy: StorageCleanupPolicy;
|
|
268
|
+
persistenceError?: PolicyMetadataPersistenceError;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** Attach the durable metadata outcome without replacing cleanup status or metrics. */
|
|
272
|
+
function withMetadataCommit(
|
|
273
|
+
result: Omit<PolicyRunResult, "policy" | "metadataPersistenceError">,
|
|
274
|
+
committed: PolicyRunMetadataCommit,
|
|
275
|
+
): PolicyRunResult {
|
|
276
|
+
return {
|
|
277
|
+
...result,
|
|
278
|
+
policy: committed.policy,
|
|
279
|
+
...(committed.persistenceError ? { metadataPersistenceError: committed.persistenceError } : {}),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Recompute run-owned metadata from the latest config inside the mutation lock. */
|
|
284
|
+
function commitPolicyRunMetadataToConfig(
|
|
285
|
+
patch: PolicyRunMetadataPatch,
|
|
286
|
+
fallbackPolicy: StorageCleanupPolicy,
|
|
287
|
+
): PolicyRunMetadataCommit {
|
|
288
|
+
const unavailable = (reason: PolicyMetadataPersistenceError): PolicyRunMetadataCommit => {
|
|
289
|
+
console.warn(`[storage-policy] metadata_persist_failed reason=${reason}`);
|
|
290
|
+
return {
|
|
291
|
+
policy: applyPolicyRunMetadata(fallbackPolicy, patch),
|
|
292
|
+
persistenceError: reason,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
try {
|
|
296
|
+
const outcome = mutatePersistedConfig(config => {
|
|
297
|
+
const before = JSON.stringify(config.storageCleanupPolicy);
|
|
298
|
+
const next = applyPolicyRunMetadata(
|
|
299
|
+
normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
|
|
300
|
+
patch,
|
|
301
|
+
);
|
|
302
|
+
config.storageCleanupPolicy = next;
|
|
303
|
+
return { changed: JSON.stringify(next) !== before, value: structuredClone(next) };
|
|
304
|
+
});
|
|
305
|
+
if (outcome.status === "unavailable") return unavailable(outcome.reason);
|
|
306
|
+
adoptStorageCleanupPolicy(outcome.value);
|
|
307
|
+
return { policy: outcome.value };
|
|
308
|
+
} catch {
|
|
309
|
+
return unavailable("write_failed");
|
|
310
|
+
}
|
|
270
311
|
}
|
|
271
312
|
|
|
272
313
|
function logPolicyEvent(message: string): void {
|
|
@@ -281,11 +322,14 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
281
322
|
const now = deps.now ?? Date.now();
|
|
282
323
|
const load = deps.loadPolicy ?? readStorageCleanupPolicyFromConfig;
|
|
283
324
|
const execute = deps.execute ?? executeArchivedCleanup;
|
|
284
|
-
const commit = deps.savePolicy
|
|
285
|
-
? (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadata(load, deps.savePolicy!, patch)
|
|
286
|
-
: commitPersistedPolicyRunMetadata;
|
|
287
|
-
|
|
288
325
|
const policy = normalizeStorageCleanupPolicy(load());
|
|
326
|
+
// Injected stores retain the existing load/save contract. The production path
|
|
327
|
+
// recomputes metadata from the latest persisted policy inside the config lock.
|
|
328
|
+
const commitMetadata = deps.savePolicy !== undefined
|
|
329
|
+
? (patch: PolicyRunMetadataPatch): PolicyRunMetadataCommit => ({
|
|
330
|
+
policy: commitPolicyRunMetadata(load, deps.savePolicy!, patch),
|
|
331
|
+
})
|
|
332
|
+
: (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadataToConfig(patch, policy);
|
|
289
333
|
deps.onPolicyLoaded?.();
|
|
290
334
|
|
|
291
335
|
if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
|
|
@@ -302,15 +346,15 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
302
346
|
|
|
303
347
|
const selection = selectPolicyPreview(policy, deps.codexHome);
|
|
304
348
|
if (selection.archivedBytes <= policy.trigger.archivedBytesOver) {
|
|
305
|
-
const
|
|
349
|
+
const committed = commitMetadata({ now, nextRun: "advance" });
|
|
306
350
|
logPolicyEvent("skip under_threshold");
|
|
307
|
-
return { ok: true, skipped: "under_threshold",
|
|
351
|
+
return withMetadataCommit({ ok: true, skipped: "under_threshold" }, committed);
|
|
308
352
|
}
|
|
309
353
|
|
|
310
354
|
if (selection.count === 0) {
|
|
311
|
-
const
|
|
355
|
+
const committed = commitMetadata({ now, nextRun: "advance" });
|
|
312
356
|
logPolicyEvent("skip nothing_selected");
|
|
313
|
-
return { ok: true, skipped: "nothing_selected",
|
|
357
|
+
return withMetadataCommit({ ok: true, skipped: "nothing_selected" }, committed);
|
|
314
358
|
}
|
|
315
359
|
|
|
316
360
|
const result = execute({
|
|
@@ -324,25 +368,28 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
324
368
|
});
|
|
325
369
|
|
|
326
370
|
if (!result.ok && result.error === "codex_busy") {
|
|
327
|
-
const
|
|
371
|
+
const committed = commitMetadata({ now, nextRun: "defer_busy" });
|
|
328
372
|
logPolicyEvent("defer codex_busy");
|
|
329
|
-
return {
|
|
373
|
+
return withMetadataCommit({
|
|
374
|
+
ok: false,
|
|
375
|
+
deferred: "codex_busy",
|
|
376
|
+
error: "codex_busy",
|
|
377
|
+
}, committed);
|
|
330
378
|
}
|
|
331
379
|
|
|
332
380
|
if (!result.ok) {
|
|
333
381
|
// Non-busy failure: still advance schedule so we do not tight-loop.
|
|
334
|
-
const
|
|
382
|
+
const committed = commitMetadata({ now, nextRun: "advance" });
|
|
335
383
|
logPolicyEvent(`fail ${result.error ?? "cleanup_failed"}`);
|
|
336
|
-
return {
|
|
384
|
+
return withMetadataCommit({
|
|
337
385
|
ok: false,
|
|
338
386
|
error: result.error,
|
|
339
387
|
mode: result.mode,
|
|
340
388
|
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
341
|
-
|
|
342
|
-
};
|
|
389
|
+
}, committed);
|
|
343
390
|
}
|
|
344
391
|
|
|
345
|
-
const
|
|
392
|
+
const committed = commitMetadata({
|
|
346
393
|
now,
|
|
347
394
|
nextRun: "advance",
|
|
348
395
|
lastRun: {
|
|
@@ -354,14 +401,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
|
|
|
354
401
|
logPolicyEvent(
|
|
355
402
|
`ok mode=${result.mode} removed=${result.count} freedBytes=${result.bytes}`,
|
|
356
403
|
);
|
|
357
|
-
return {
|
|
404
|
+
return withMetadataCommit({
|
|
358
405
|
ok: true,
|
|
359
406
|
mode: result.mode,
|
|
360
407
|
freedBytes: result.bytes,
|
|
361
408
|
removed: result.count,
|
|
362
409
|
...(result.trashDir ? { trashDir: result.trashDir } : {}),
|
|
363
|
-
|
|
364
|
-
};
|
|
410
|
+
}, committed);
|
|
365
411
|
}
|
|
366
412
|
|
|
367
413
|
/** Startup / schedule tick entry — swallows unexpected errors. */
|
package/src/usage/log.ts
CHANGED
|
@@ -8,22 +8,27 @@ import { sanitizeLogMetadataString } from "../lib/redact";
|
|
|
8
8
|
import { usageDisplayTotalTokens } from "./totals";
|
|
9
9
|
import type { AttemptTierOutcome, OcxUsage } from "../types";
|
|
10
10
|
import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
|
|
11
|
-
import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
11
|
+
import { ACCOUNT_LOG_LABEL_RE, CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
12
12
|
import type { AgentKind } from "../server/effort-policy";
|
|
13
13
|
|
|
14
14
|
export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
|
|
15
|
-
export type
|
|
15
|
+
export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}`;
|
|
16
|
+
export type CodexUsageAccountLogLabel = UsageAccountLogLabel;
|
|
16
17
|
|
|
17
18
|
export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^[a-f0-9]{8,64}(?:-\d+)?$/;
|
|
18
19
|
|
|
19
20
|
export function isPersistableAccountLogLabel(value: unknown): value is string {
|
|
20
21
|
return (
|
|
21
|
-
value === "
|
|
22
|
-
|
|
22
|
+
typeof value === "string"
|
|
23
|
+
&& (ACCOUNT_LOG_LABEL_RE.test(value) || OAUTH_ACCOUNT_LOG_LABEL_RE.test(value))
|
|
23
24
|
);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function isCodexUsageAccountLogLabel(value: unknown): value is
|
|
27
|
+
export function isCodexUsageAccountLogLabel(value: unknown): value is UsageAccountLogLabel {
|
|
28
|
+
return typeof value === "string" && ACCOUNT_LOG_LABEL_RE.test(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isCodexPoolAccountLogLabel(value: unknown): value is "main" | `p${string}` {
|
|
27
32
|
return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -63,6 +68,13 @@ export interface PersistedUsageAttempt {
|
|
|
63
68
|
sendCount: number;
|
|
64
69
|
recoveryKinds: AttemptRecoveryKind[];
|
|
65
70
|
usageStatus: UsageStatus;
|
|
71
|
+
/**
|
|
72
|
+
* True when the proxy answered this turn locally and issued no upstream request. It travels on
|
|
73
|
+
* the attempt itself rather than as a `finishRequestAttempt` argument because that function is
|
|
74
|
+
* called from six places, and a new parameter would silently default to the wrong answer at any
|
|
75
|
+
* one of them that was missed. Absent on ordinary attempts so old rows keep their exact shape.
|
|
76
|
+
*/
|
|
77
|
+
locallyAnswered?: boolean;
|
|
66
78
|
/** Stable non-PII identity for the account that served this attempt. */
|
|
67
79
|
accountLogLabel?: string;
|
|
68
80
|
inputTokenEstimate?: number;
|
|
@@ -200,8 +212,20 @@ function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
|
|
|
200
212
|
|| providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
|
|
201
213
|
}
|
|
202
214
|
|
|
203
|
-
export function usageForFinalLog(
|
|
215
|
+
export function usageForFinalLog(
|
|
216
|
+
provider: string,
|
|
217
|
+
usage: OcxUsage | undefined,
|
|
218
|
+
/**
|
|
219
|
+
* True when the proxy answered this turn locally and issued no upstream request. Such a turn's
|
|
220
|
+
* zero counts are EXACT, so the provider-wide estimated marking must not apply: Kiro and Cursor
|
|
221
|
+
* are marked estimated because their adapters can only guess a real inference's usage, and a
|
|
222
|
+
* turn with no inference has nothing to guess. Without this, a no-send turn is indistinguishable
|
|
223
|
+
* from a real one whose usage frame never arrived.
|
|
224
|
+
*/
|
|
225
|
+
locallyAnswered = false,
|
|
226
|
+
): OcxUsage | undefined {
|
|
204
227
|
if (!usage) return undefined;
|
|
228
|
+
if (locallyAnswered) return usage;
|
|
205
229
|
if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
|
|
206
230
|
return usage;
|
|
207
231
|
}
|
|
@@ -239,6 +263,8 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
|
|
|
239
263
|
"key-429",
|
|
240
264
|
"rate-limit-429",
|
|
241
265
|
"anthropic-oauth-429",
|
|
266
|
+
"cursor-oauth-auth",
|
|
267
|
+
"cursor-oauth-429",
|
|
242
268
|
"oauth-account-429",
|
|
243
269
|
"image-413",
|
|
244
270
|
"opaque-blob-rejection",
|