@bitkyc08/opencodex 2.34.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/gui/dist/assets/{index-C4TMRloX.js → index-DNdRKXK9.js} +11 -11
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -1
  4. package/src/adapters/base.ts +26 -0
  5. package/src/adapters/cursor/catalog.ts +541 -0
  6. package/src/adapters/cursor/cursor-errors.ts +15 -0
  7. package/src/adapters/cursor/discovery.ts +34 -41
  8. package/src/adapters/cursor/envelope-echo.ts +128 -0
  9. package/src/adapters/cursor/request-builder.ts +19 -12
  10. package/src/adapters/cursor/tool-definitions.ts +2 -1
  11. package/src/adapters/cursor/tool-result-normalize.ts +23 -31
  12. package/src/adapters/cursor.ts +21 -2
  13. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  14. package/src/adapters/google-antigravity-replay.ts +71 -2
  15. package/src/adapters/google-antigravity-wire.ts +5 -0
  16. package/src/adapters/google.ts +15 -1
  17. package/src/adapters/kiro-constants.ts +12 -0
  18. package/src/adapters/kiro.ts +128 -11
  19. package/src/adapters/openai-chat.ts +16 -2
  20. package/src/adapters/openai-responses.ts +15 -2
  21. package/src/adapters/run-turn-queue.ts +36 -1
  22. package/src/adapters/tool-catalog-nudge.ts +2 -1
  23. package/src/adapters/xai-web-search.ts +10 -14
  24. package/src/claude/outbound.ts +14 -3
  25. package/src/cli/access.ts +46 -3
  26. package/src/cli/account-api.ts +84 -15
  27. package/src/cli/account-extended.ts +261 -28
  28. package/src/cli/account-main.ts +12 -12
  29. package/src/cli/account.ts +40 -10
  30. package/src/cli/agent.ts +8 -1
  31. package/src/cli/capabilities-command.ts +94 -0
  32. package/src/cli/capabilities.ts +496 -0
  33. package/src/cli/claude-desktop.ts +31 -11
  34. package/src/cli/dispatch.ts +195 -27
  35. package/src/cli/doctor.ts +100 -1
  36. package/src/cli/help.ts +11 -2
  37. package/src/cli/index.ts +19 -3
  38. package/src/cli/inspect.ts +230 -0
  39. package/src/cli/observe.ts +11 -3
  40. package/src/cli/registry.ts +34 -2
  41. package/src/cli/runtime-api.ts +51 -7
  42. package/src/cli/status.ts +16 -0
  43. package/src/cli/storage.ts +234 -0
  44. package/src/cli/system-command.ts +16 -0
  45. package/src/cli/usage-report.ts +52 -2
  46. package/src/cli/version-skew.ts +46 -0
  47. package/src/codex/account-label.ts +21 -0
  48. package/src/codex/catalog/provider-fetch.ts +4 -0
  49. package/src/codex/transition-state.ts +12 -3
  50. package/src/compatibility/openai-responses.ts +9 -1
  51. package/src/generated/compatibility-version.json +95 -59
  52. package/src/integrations/ownership-policy.ts +24 -5
  53. package/src/integrations/ownership.ts +36 -2
  54. package/src/integrations/state.ts +40 -7
  55. package/src/integrations/writer.ts +21 -3
  56. package/src/lib/admin-secrets.ts +24 -0
  57. package/src/lib/errors.ts +25 -1
  58. package/src/lib/service-secrets.ts +15 -0
  59. package/src/oauth/store.ts +14 -5
  60. package/src/providers/label.ts +34 -1
  61. package/src/responses/turn-termination.ts +107 -0
  62. package/src/server/management/logs-usage-routes.ts +0 -16
  63. package/src/server/management/route-registry.ts +311 -0
  64. package/src/server/proxy-liveness.ts +27 -4
  65. package/src/server/request-log.ts +29 -1
  66. package/src/server/responses/core.ts +80 -0
  67. package/src/service.ts +34 -0
  68. package/src/storage/policy-job.ts +14 -4
  69. package/src/storage/policy.ts +88 -23
  70. package/src/usage/log.ts +44 -4
  71. package/src/usage/summary.ts +10 -0
@@ -40,6 +40,10 @@ import {
40
40
  previousResponseScopeMismatch,
41
41
  rememberResponseState,
42
42
  } from "../../responses/state";
43
+ import {
44
+ bindTurnTerminationScope,
45
+ rememberDeliveredFinalAnswer,
46
+ } from "../../responses/turn-termination";
43
47
  import {
44
48
  isValidProviderContinuationOwner,
45
49
  mergeProviderContinuationPayload,
@@ -115,6 +119,7 @@ import {
115
119
  resolveAnthropicAccountForSession,
116
120
  rotateAnthropicAccountOn429,
117
121
  } from "../../oauth/anthropic-routing";
122
+ import { stampOAuthAccountLabel } from "../../providers/label";
118
123
  import {
119
124
  failoverAccountSnapshot,
120
125
  GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST,
@@ -2380,6 +2385,10 @@ async function handleResponsesInner(
2380
2385
  threadIdHeader: req.headers.get("thread-id"),
2381
2386
  cursorConversationId: parsed._cursorConversationId,
2382
2387
  });
2388
+ bindTurnTerminationScope(parsed, resolvedConversationId);
2389
+ const rememberKiroDeliveredFinalAnswer = (adapterName: string, response: unknown): void => {
2390
+ if (adapterName === "kiro") rememberDeliveredFinalAnswer(parsed, response);
2391
+ };
2383
2392
  // _clientThreadId remains the routing/continuation identity supplied by Codex. Replay state uses
2384
2393
  // a dedicated raw conversation namespace so mixed headers that carry the same identity still
2385
2394
  // match, and a shared/synthetic session_id cannot coalesce distinct thread/Cursor conversations.
@@ -2841,6 +2850,10 @@ async function handleResponsesInner(
2841
2850
  if (snapshot.projectId) rotatedProvider = { ...rotatedProvider, project: snapshot.projectId };
2842
2851
  route.provider = rotatedProvider;
2843
2852
  if (route.providerName === "kiro") parsed._kiroAuthContext = { ...(snapshot.kiro ?? {}) };
2853
+ // Re-stamp: a request that rotated accounts must be attributed to the account that actually
2854
+ // served it. All three rotation sites funnel through here, so this is the only re-stamp
2855
+ // needed -- and putting it anywhere else would let one of the three drift.
2856
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, snapshot.accountId);
2844
2857
  return true;
2845
2858
  };
2846
2859
  const anthropicSessionKey = route.providerName === "anthropic" && route.provider.authMode === "oauth"
@@ -2882,6 +2895,10 @@ async function handleResponsesInner(
2882
2895
  };
2883
2896
  if (isOAuth401ReplayProvider) sentOAuthSnapshot = resolved;
2884
2897
  route.provider = { ...route.provider, apiKey: resolved.accessToken };
2898
+ // Attribution is independent of failover (#2699): stamped from the resolved snapshot
2899
+ // itself, not from inside the `isGenericFailoverProvider` branch below, so a future
2900
+ // narrowing of that predicate cannot silently switch attribution off.
2901
+ stampOAuthAccountLabel(logCtx, route.providerName, route.provider, resolved.accountId);
2885
2902
  // Remember which account actually served this request so a 429 cools THAT one, not
2886
2903
  // whichever account is active by the time the response comes back (#2568).
2887
2904
  if (isGenericFailoverProvider(route.providerName, route.provider)) {
@@ -4440,6 +4457,7 @@ async function handleResponsesInner(
4440
4457
  ...(options.forceEmptyResponseId ? { forceEmptyResponseId: true } : {}),
4441
4458
  onCompletedResponse: (response, providerState) => {
4442
4459
  commitReasoningReplayServingRoute();
4460
+ rememberKiroDeliveredFinalAnswer(adapter.name, response);
4443
4461
  rememberResponseState(
4444
4462
  parsed._rawBody,
4445
4463
  response,
@@ -4755,6 +4773,7 @@ async function handleResponsesInner(
4755
4773
  },
4756
4774
  onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
4757
4775
  commitReasoningReplayServingRoute();
4776
+ rememberKiroDeliveredFinalAnswer(adapter.name, response);
4758
4777
  if (!routedCompaction) {
4759
4778
  rememberResponseState(
4760
4779
  parsed._rawBody,
@@ -4824,6 +4843,7 @@ async function handleResponsesInner(
4824
4843
  },
4825
4844
  });
4826
4845
  if (!routedCompaction) {
4846
+ rememberKiroDeliveredFinalAnswer(adapter.name, json);
4827
4847
  rememberResponseState(
4828
4848
  parsed._rawBody,
4829
4849
  json,
@@ -4858,6 +4878,64 @@ async function handleResponsesInner(
4858
4878
  // reuse is safe, and releaseBodyObservation is idempotent per build.
4859
4879
  let initialRequest: AdapterRequest | undefined;
4860
4880
  let inputTokenEstimate: number | undefined;
4881
+ // An adapter may know the turn needs no inference at all — Kiro's replayed history ending in a
4882
+ // delivered final answer. Answer it locally: no build (so no token estimate), no send (so
4883
+ // sendCount stays 0), and crucially no empty-completion guard, which treats an outputless
4884
+ // terminal as a failed turn and re-invokes the identical request. Routing this through the
4885
+ // ordinary event path would therefore reinstate the loop it exists to end.
4886
+ const localTerminal = activeAdapter.localTerminal?.(parsed);
4887
+ if (localTerminal) {
4888
+ logCtx.localTerminalReason = localTerminal.reason;
4889
+ // Mark the physical attempt too, not just the parent row. `finishRequestAttempt` finalizes the
4890
+ // attempt through the same estimated-provider path, so without this the row reads exact while
4891
+ // its own attempt still claims an estimate — the detailed accounting a maintainer actually
4892
+ // reads for a zero-send turn.
4893
+ if (logCtx.activeAttempt) logCtx.activeAttempt.locallyAnswered = true;
4894
+ cleanupUpstreamAbort();
4895
+ upstream.abort();
4896
+ const terminalEvents: AdapterEvent[] = [{
4897
+ type: "done",
4898
+ endTurn: true,
4899
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
4900
+ }];
4901
+ if (parsed.stream) {
4902
+ const localSse = bridgeToResponsesSSE(
4903
+ (async function* () { yield* terminalEvents; })(),
4904
+ parsed._responseModelId ?? parsed.modelId,
4905
+ toolBridgeMaps.toolNsMap,
4906
+ toolBridgeMaps.freeformToolNames,
4907
+ toolBridgeMaps.toolSearchToolNames,
4908
+ undefined,
4909
+ 2_000,
4910
+ {
4911
+ translatorBudget,
4912
+ ...(options.forceEmptyResponseId ? { responseId: "" } : {}),
4913
+ ...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
4914
+ },
4915
+ );
4916
+ // Same lifetime tracking as every other streaming return in this function: the turn
4917
+ // admission lease is released when the body finishes or the client disconnects. Returning
4918
+ // the raw stream would hold a lease for a turn that already has all of its output.
4919
+ const localTurnAc = new AbortController();
4920
+ return new Response(
4921
+ trackStreamLifetime(localSse, localTurnAc, undefined, options.turnAdmissionLease),
4922
+ {
4923
+ headers: {
4924
+ "Content-Type": "text/event-stream",
4925
+ "Cache-Control": "no-cache",
4926
+ "Connection": "keep-alive",
4927
+ "X-Accel-Buffering": "no",
4928
+ },
4929
+ },
4930
+ );
4931
+ }
4932
+ return new Response(
4933
+ JSON.stringify(buildResponseJSON(terminalEvents, parsed._responseModelId ?? parsed.modelId, {
4934
+ translatorBudget,
4935
+ })),
4936
+ { headers: { "Content-Type": "application/json" } },
4937
+ );
4938
+ }
4861
4939
  try {
4862
4940
  initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
4863
4941
  refreshRoutedNamespaceToolAliases(initialRequest);
@@ -5706,6 +5784,7 @@ async function handleResponsesInner(
5706
5784
  },
5707
5785
  onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
5708
5786
  commitReasoningReplayServingRoute();
5787
+ rememberKiroDeliveredFinalAnswer(activeAdapter.name, response);
5709
5788
  // Compaction turns must NOT enter the continuation cache: _rawBody still holds the full
5710
5789
  // PRE-compaction history, and a later previous_response_id expansion would rehydrate the
5711
5790
  // giant stale chain Codex just replaced.
@@ -5783,6 +5862,7 @@ async function handleResponsesInner(
5783
5862
  });
5784
5863
  // See the streaming branch: compaction turns skip the continuation cache.
5785
5864
  if (!routedCompaction) {
5865
+ rememberKiroDeliveredFinalAnswer(activeAdapter.name, json);
5786
5866
  rememberResponseState(
5787
5867
  parsed._rawBody,
5788
5868
  json,
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 {
@@ -238,6 +239,7 @@ export async function abortStorageCleanupPolicyJobAsync(): Promise<void> {
238
239
  }
239
240
  }
240
241
 
242
+ /** Project a run result into the bounded management-API job outcome. */
241
243
  function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
242
244
  return {
243
245
  ok: result.ok,
@@ -248,18 +250,26 @@ function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
248
250
  ...(result.freedBytes !== undefined ? { freedBytes: result.freedBytes } : {}),
249
251
  ...(result.removed !== undefined ? { removed: result.removed } : {}),
250
252
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
253
+ ...(result.metadataPersistenceError
254
+ ? { metadataPersistenceError: result.metadataPersistenceError }
255
+ : {}),
251
256
  };
252
257
  }
253
258
 
259
+ /** Publish one completed evaluation without losing successful cleanup effects. */
254
260
  function applyFinished(result: PolicyRunResult): void {
255
261
  // Prefer the latest persisted policy over `result.policy`. The worker (or
256
262
  // in-process run) already merged run metadata into disk; a concurrent PUT
257
263
  // may also have landed after that write. Re-reading avoids applying a stale
258
264
  // start-of-job snapshot when the run skipped without saving.
259
- try {
260
- livePolicyApply?.(readStorageCleanupPolicyFromConfig());
261
- } catch {
262
- livePolicyApply?.(result.policy);
265
+ // A best-effort fallback policy may predate concurrent edits; keep the current
266
+ // live config untouched when the durable metadata write did not land.
267
+ if (!result.metadataPersistenceError) {
268
+ try {
269
+ livePolicyApply?.(readStorageCleanupPolicyFromConfig());
270
+ } catch {
271
+ livePolicyApply?.(result.policy);
272
+ }
263
273
  }
264
274
  state = {
265
275
  status: "idle",
@@ -8,7 +8,7 @@
8
8
  * Privacy: logs never include host paths, digests of file contents, or secrets.
9
9
  */
10
10
  import { resolveCodexHomeDir } from "../codex/home";
11
- import { loadConfig, saveConfigPreservingClaudeCode } from "../config";
11
+ import { loadConfig, mutatePersistedConfig, saveConfigPreservingClaudeCode } from "../config";
12
12
  import type { StorageCleanupPolicy } from "../types";
13
13
  import {
14
14
  computePreviewDigest,
@@ -38,6 +38,7 @@ export function setStorageCleanupPolicyLiveSink(
38
38
 
39
39
  export type PolicySchedule = StorageCleanupPolicy["schedule"];
40
40
  export type PolicyRunReason = "startup" | "schedule" | "manual";
41
+ export type PolicyMetadataPersistenceError = "missing" | "invalid" | "conflict" | "write_failed";
41
42
 
42
43
  export type PolicySkipReason =
43
44
  | "disabled"
@@ -54,6 +55,7 @@ export interface PolicyRunResult {
54
55
  freedBytes?: number;
55
56
  removed?: number;
56
57
  trashDir?: string;
58
+ metadataPersistenceError?: PolicyMetadataPersistenceError;
57
59
  policy: StorageCleanupPolicy;
58
60
  }
59
61
 
@@ -399,6 +401,21 @@ export type PolicyRunMetadataPatch = {
399
401
  lastRun?: StorageCleanupPolicy["lastRun"];
400
402
  };
401
403
 
404
+ /** Apply only run-owned fields while preserving the supplied policy settings. */
405
+ function applyPolicyRunMetadata(
406
+ policy: StorageCleanupPolicy,
407
+ patch: PolicyRunMetadataPatch,
408
+ ): StorageCleanupPolicy {
409
+ let next =
410
+ patch.nextRun === "defer_busy"
411
+ ? deferBusy(policy, patch.now)
412
+ : advanceNextRun(policy, patch.now);
413
+ if (patch.lastRun) {
414
+ next = { ...next, lastRun: patch.lastRun };
415
+ }
416
+ return next;
417
+ }
418
+
402
419
  /**
403
420
  * Reload the latest persisted policy and write only run-owned metadata
404
421
  * (`lastRun` / `nextRun`). Preserves concurrent edits to enabled, trigger,
@@ -410,17 +427,57 @@ export function commitPolicyRunMetadata(
410
427
  patch: PolicyRunMetadataPatch,
411
428
  ): StorageCleanupPolicy {
412
429
  const latest = normalizeStorageCleanupPolicy(load());
413
- let next =
414
- patch.nextRun === "defer_busy"
415
- ? deferBusy(latest, patch.now)
416
- : advanceNextRun(latest, patch.now);
417
- if (patch.lastRun) {
418
- next = { ...next, lastRun: patch.lastRun };
419
- }
430
+ const next = applyPolicyRunMetadata(latest, patch);
420
431
  save(next);
421
432
  return next;
422
433
  }
423
434
 
435
+ type PolicyRunMetadataCommit = {
436
+ policy: StorageCleanupPolicy;
437
+ persistenceError?: PolicyMetadataPersistenceError;
438
+ };
439
+
440
+ /** Attach the durable metadata outcome without replacing cleanup status or metrics. */
441
+ function withMetadataCommit(
442
+ result: Omit<PolicyRunResult, "policy" | "metadataPersistenceError">,
443
+ committed: PolicyRunMetadataCommit,
444
+ ): PolicyRunResult {
445
+ return {
446
+ ...result,
447
+ policy: committed.policy,
448
+ ...(committed.persistenceError ? { metadataPersistenceError: committed.persistenceError } : {}),
449
+ };
450
+ }
451
+
452
+ /** Recompute run-owned metadata from the latest config inside the mutation lock. */
453
+ function commitPolicyRunMetadataToConfig(
454
+ patch: PolicyRunMetadataPatch,
455
+ fallbackPolicy: StorageCleanupPolicy,
456
+ ): PolicyRunMetadataCommit {
457
+ const unavailable = (reason: PolicyMetadataPersistenceError): PolicyRunMetadataCommit => {
458
+ console.warn(`[storage-policy] metadata_persist_failed reason=${reason}`);
459
+ return {
460
+ policy: applyPolicyRunMetadata(fallbackPolicy, patch),
461
+ persistenceError: reason,
462
+ };
463
+ };
464
+ try {
465
+ const outcome = mutatePersistedConfig(config => {
466
+ const next = applyPolicyRunMetadata(
467
+ normalizeStorageCleanupPolicy(config.storageCleanupPolicy),
468
+ patch,
469
+ );
470
+ config.storageCleanupPolicy = next;
471
+ return { changed: true, value: next };
472
+ });
473
+ if (outcome.status === "unavailable") return unavailable(outcome.reason);
474
+ livePolicySink?.(outcome.value);
475
+ return { policy: outcome.value };
476
+ } catch {
477
+ return unavailable("write_failed");
478
+ }
479
+ }
480
+
424
481
  function logPolicyEvent(message: string): void {
425
482
  console.log(`[storage-policy] ${message}`);
426
483
  }
@@ -434,8 +491,14 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
434
491
  const load = deps.loadPolicy ?? readStorageCleanupPolicyFromConfig;
435
492
  const save = deps.savePolicy ?? writeStorageCleanupPolicyToConfig;
436
493
  const execute = deps.execute ?? executeArchivedCleanup;
437
-
438
494
  const policy = normalizeStorageCleanupPolicy(load());
495
+ // Injected stores retain the existing load/save contract. The production path
496
+ // recomputes metadata from the latest persisted policy inside the config lock.
497
+ const commitMetadata = deps.loadPolicy !== undefined || deps.savePolicy !== undefined
498
+ ? (patch: PolicyRunMetadataPatch): PolicyRunMetadataCommit => ({
499
+ policy: commitPolicyRunMetadata(load, save, patch),
500
+ })
501
+ : (patch: PolicyRunMetadataPatch) => commitPolicyRunMetadataToConfig(patch, policy);
439
502
 
440
503
  if (typeof deps.holdAfterLoadMs === "number" && Number.isFinite(deps.holdAfterLoadMs) && deps.holdAfterLoadMs > 0) {
441
504
  Bun.sleepSync(Math.floor(deps.holdAfterLoadMs));
@@ -451,15 +514,15 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
451
514
 
452
515
  const selection = selectPolicyPreview(policy, deps.codexHome);
453
516
  if (selection.archivedBytes <= policy.trigger.archivedBytesOver) {
454
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
517
+ const committed = commitMetadata({ now, nextRun: "advance" });
455
518
  logPolicyEvent("skip under_threshold");
456
- return { ok: true, skipped: "under_threshold", policy: saved };
519
+ return withMetadataCommit({ ok: true, skipped: "under_threshold" }, committed);
457
520
  }
458
521
 
459
522
  if (selection.count === 0) {
460
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
523
+ const committed = commitMetadata({ now, nextRun: "advance" });
461
524
  logPolicyEvent("skip nothing_selected");
462
- return { ok: true, skipped: "nothing_selected", policy: saved };
525
+ return withMetadataCommit({ ok: true, skipped: "nothing_selected" }, committed);
463
526
  }
464
527
 
465
528
  const result = execute({
@@ -473,25 +536,28 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
473
536
  });
474
537
 
475
538
  if (!result.ok && result.error === "codex_busy") {
476
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "defer_busy" });
539
+ const committed = commitMetadata({ now, nextRun: "defer_busy" });
477
540
  logPolicyEvent("defer codex_busy");
478
- return { ok: false, deferred: "codex_busy", error: "codex_busy", policy: saved };
541
+ return withMetadataCommit({
542
+ ok: false,
543
+ deferred: "codex_busy",
544
+ error: "codex_busy",
545
+ }, committed);
479
546
  }
480
547
 
481
548
  if (!result.ok) {
482
549
  // Non-busy failure: still advance schedule so we do not tight-loop.
483
- const saved = commitPolicyRunMetadata(load, save, { now, nextRun: "advance" });
550
+ const committed = commitMetadata({ now, nextRun: "advance" });
484
551
  logPolicyEvent(`fail ${result.error ?? "cleanup_failed"}`);
485
- return {
552
+ return withMetadataCommit({
486
553
  ok: false,
487
554
  error: result.error,
488
555
  mode: result.mode,
489
556
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
490
- policy: saved,
491
- };
557
+ }, committed);
492
558
  }
493
559
 
494
- const saved = commitPolicyRunMetadata(load, save, {
560
+ const committed = commitMetadata({
495
561
  now,
496
562
  nextRun: "advance",
497
563
  lastRun: {
@@ -503,14 +569,13 @@ export function runStorageCleanupPolicy(deps: PolicyRunDeps): PolicyRunResult {
503
569
  logPolicyEvent(
504
570
  `ok mode=${result.mode} removed=${result.count} freedBytes=${result.bytes}`,
505
571
  );
506
- return {
572
+ return withMetadataCommit({
507
573
  ok: true,
508
574
  mode: result.mode,
509
575
  freedBytes: result.bytes,
510
576
  removed: result.count,
511
577
  ...(result.trashDir ? { trashDir: result.trashDir } : {}),
512
- policy: saved,
513
- };
578
+ }, committed);
514
579
  }
515
580
 
516
581
  /** Startup / schedule tick entry — swallows unexpected errors. */
package/src/usage/log.ts CHANGED
@@ -8,12 +8,33 @@ 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
 
13
13
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
14
- export type CodexUsageAccountLogLabel = "main" | `p${string}`;
14
+ /**
15
+ * A persisted account label: a Codex pool account (`main`/`p<hex6>`) or a non-Codex OAuth
16
+ * provider account (`o<hex6>`, #2699).
17
+ *
18
+ * The old name `CodexUsageAccountLogLabel` is kept as an alias because it is exported and used
19
+ * across modules; the two predicates below are what callers should choose between.
20
+ */
21
+ export type UsageAccountLogLabel = "main" | `p${string}` | `o${string}`;
22
+ export type CodexUsageAccountLogLabel = UsageAccountLogLabel;
23
+
24
+ /**
25
+ * Accepts EITHER label family. This is the predicate the persistence writers use, so widening
26
+ * it here is what stops six separate call sites from silently dropping an `o`-label -- including
27
+ * two in the live request path (`request-log.ts:972` and `:1187`).
28
+ *
29
+ * The name is unchanged deliberately: renaming it would touch every call site for no behavior,
30
+ * and the widened contract is what every one of those sites wanted.
31
+ */
32
+ export function isCodexUsageAccountLogLabel(value: unknown): value is UsageAccountLogLabel {
33
+ return value === "main" || (typeof value === "string" && ACCOUNT_LOG_LABEL_RE.test(value));
34
+ }
15
35
 
16
- export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
36
+ /** Strictly a Codex pool label. Use when the Codex-only distinction actually matters. */
37
+ export function isCodexPoolAccountLogLabel(value: unknown): value is "main" | `p${string}` {
17
38
  return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
18
39
  }
19
40
 
@@ -51,6 +72,13 @@ export interface PersistedUsageAttempt {
51
72
  sendCount: number;
52
73
  recoveryKinds: AttemptRecoveryKind[];
53
74
  usageStatus: UsageStatus;
75
+ /**
76
+ * True when the proxy answered this turn locally and issued no upstream request. It travels on
77
+ * the attempt itself rather than as a `finishRequestAttempt` argument because that function is
78
+ * called from six places, and a new parameter would silently default to the wrong answer at any
79
+ * one of them that was missed. Absent on ordinary attempts so old rows keep their exact shape.
80
+ */
81
+ locallyAnswered?: boolean;
54
82
  /** Stable non-PII identity for the Codex pool account that served this attempt. */
55
83
  accountLogLabel?: CodexUsageAccountLogLabel;
56
84
  inputTokenEstimate?: number;
@@ -181,8 +209,20 @@ function isEstimatedUsageProvider(providerOrAdapter: string): boolean {
181
209
  || providerOrAdapter === "cursor" || providerOrAdapter.startsWith("cursor-");
182
210
  }
183
211
 
184
- export function usageForFinalLog(provider: string, usage: OcxUsage | undefined): OcxUsage | undefined {
212
+ export function usageForFinalLog(
213
+ provider: string,
214
+ usage: OcxUsage | undefined,
215
+ /**
216
+ * True when the proxy answered this turn locally and issued no upstream request. Such a turn's
217
+ * zero counts are EXACT, so the provider-wide estimated marking must not apply: Kiro and Cursor
218
+ * are marked estimated because their adapters can only guess a real inference's usage, and a
219
+ * turn with no inference has nothing to guess. Without this, a no-send turn is indistinguishable
220
+ * from a real one whose usage frame never arrived.
221
+ */
222
+ locallyAnswered = false,
223
+ ): OcxUsage | undefined {
185
224
  if (!usage) return undefined;
225
+ if (locallyAnswered) return usage;
186
226
  if (usage.estimated || isEstimatedUsageProvider(provider)) return { ...usage, estimated: true };
187
227
  return usage;
188
228
  }
@@ -684,6 +684,16 @@ function legacyCodexAccountLabel(provider: string): string | null {
684
684
  return suffix ?? LEGACY_AMBIGUOUS_ACCOUNT_LABEL;
685
685
  }
686
686
 
687
+ /**
688
+ * An explicitly stamped label of EITHER family is authoritative for any provider (#2699).
689
+ *
690
+ * No `o`-label branch is needed here: `isCodexUsageAccountLogLabel` now accepts both families,
691
+ * and adding a second predicate call would be a no-op guarded by a comment claiming otherwise.
692
+ *
693
+ * The legacy fallback stays openai-only on purpose. It infers an account from the PROVIDER
694
+ * string, and inferring for a non-Codex row would merge unrelated accounts under one label --
695
+ * so an unlabeled xai row is dropped from the account table rather than guessed at.
696
+ */
687
697
  function accountLabelForAttribution(provider: string, explicit: unknown): string | null {
688
698
  if (isCodexUsageAccountLogLabel(explicit)) return explicit;
689
699
  return legacyCodexAccountLabel(provider);