@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
package/src/service.ts CHANGED
@@ -9,12 +9,12 @@ import { execFileSync, execSync, spawnSync } from "node:child_process";
9
9
  import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
10
10
  import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
- import { dirname, join, resolve } from "node:path";
12
+ import { dirname, join, resolve, win32 } from "node:path";
13
13
  import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
14
14
  import { loadConfig } from "./config";
15
15
  import { restoreNativeCodex, restoreNativeCodexAsync } from "./codex/inject";
16
16
  import { stripGrokConfig } from "./grok/inject";
17
- import { isWslRuntime } from "./codex/home";
17
+ import { isWslRuntime, resolveCodexHomeDir, type CodexHomeDeps } from "./codex/home";
18
18
  import { BUN_RUNTIME_PATH_ENV, BUN_RUNTIME_SOURCE_ENV, durableBunRuntime } from "./lib/bun-runtime";
19
19
  import type { BunRuntimeSource } from "./lib/bun-runtime";
20
20
  import { isProcessAlive, stopProxy } from "./lib/process-control";
@@ -29,6 +29,7 @@ import {
29
29
  runWindowsElevated,
30
30
  toWindowsSchtasksError,
31
31
  WindowsElevationError,
32
+ WindowsSchtasksError,
32
33
  type ElevatedSchedulerOutcome,
33
34
  type ElevatedSchtasksCreateAndRunExecution,
34
35
  type ElevatedSchtasksCreateAndRunResult,
@@ -94,9 +95,23 @@ function serviceStatePaths(): string[] {
94
95
  return paths;
95
96
  }
96
97
 
97
- function currentCodexHome(): string {
98
- const raw = process.env.CODEX_HOME?.trim();
99
- return raw ? resolve(expandUserPath(raw)) : join(homedir(), ".codex");
98
+ function currentCodexHome(deps: CodexHomeDeps = {}): string {
99
+ // Service ownership must identify the same home as the runtime. In WSL an
100
+ // unset CODEX_HOME can resolve to the single Windows Desktop home rather than
101
+ // Linux ~/.codex; recording the fallback here creates a false foreign owner.
102
+ return resolveCodexHomeDir(deps);
103
+ }
104
+
105
+ function currentCodexSqliteHomeAbsolute(target: "native" | "windows" = "native"): string | undefined {
106
+ const raw = process.env.CODEX_SQLITE_HOME?.trim();
107
+ if (!raw) return undefined;
108
+ const expanded = expandUserPath(raw);
109
+ // Windows service artifacts can be rendered by cross-platform tests and
110
+ // repair tooling. Preserve an already-absolute drive/UNC path instead of
111
+ // anchoring it beneath the current POSIX worktree.
112
+ return target === "windows" && win32.isAbsolute(expanded)
113
+ ? win32.normalize(expanded)
114
+ : resolve(expanded);
100
115
  }
101
116
 
102
117
  function currentOpenCodexHome(): string {
@@ -218,8 +233,8 @@ export function inspectServiceStateEvidence(
218
233
  }
219
234
 
220
235
  /** The homes this process is actually using, for comparison against a claim. */
221
- export function currentServiceHomes(): { codexHome: string; opencodexHome: string } {
222
- return { codexHome: currentCodexHome(), opencodexHome: currentOpenCodexHome() };
236
+ export function currentServiceHomes(deps: CodexHomeDeps = {}): { codexHome: string; opencodexHome: string } {
237
+ return { codexHome: currentCodexHome(deps), opencodexHome: currentOpenCodexHome() };
223
238
  }
224
239
 
225
240
  export function serviceHomeMatches(a: string, b: string): boolean {
@@ -283,11 +298,12 @@ export function serviceEnvironmentOwnedHere(): boolean {
283
298
  export function assertServiceEnvironmentMatchesInstall(): void {
284
299
  const state = readServiceInstallState();
285
300
  if (!state) return;
301
+ const actualCodexHome = currentCodexHome();
286
302
  const expected = normalizePathForCompare(state.codexHome);
287
- const actual = normalizePathForCompare(currentCodexHome());
303
+ const actual = normalizePathForCompare(actualCodexHome);
288
304
  if (expected !== actual) {
289
305
  throw new ServiceOwnershipError(
290
- `Service was installed with CODEX_HOME=${state.codexHome}, but current CODEX_HOME=${currentCodexHome()}. ` +
306
+ `Service was installed with CODEX_HOME=${state.codexHome}, but current CODEX_HOME=${actualCodexHome}. ` +
291
307
  "Run the service command from the same Codex home so native Codex restore updates the correct config.",
292
308
  );
293
309
  }
@@ -363,6 +379,7 @@ export function buildPlist(): string {
363
379
  const log = logPath();
364
380
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
365
381
  const codexHome = process.env.CODEX_HOME?.trim();
382
+ const codexSqliteHome = currentCodexSqliteHomeAbsolute();
366
383
  const opencodexHome = process.env.OPENCODEX_HOME?.trim();
367
384
  const envLines = [
368
385
  ` <key>OCX_SERVICE</key><string>1</string>`,
@@ -370,6 +387,7 @@ export function buildPlist(): string {
370
387
  ` <key>${BUN_RUNTIME_PATH_ENV}</key><string>${plistString(bun)}</string>`,
371
388
  ` <key>PATH</key><string>${plistString(path)}</string>`,
372
389
  codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
390
+ codexSqliteHome ? ` <key>CODEX_SQLITE_HOME</key><string>${plistString(codexSqliteHome)}</string>` : null,
373
391
  opencodexHome ? ` <key>OPENCODEX_HOME</key><string>${plistString(opencodexHome)}</string>` : null,
374
392
  ].filter((line): line is string => Boolean(line)).join("\n");
375
393
  const command = buildServiceShellCommand(bun, cli);
@@ -981,6 +999,48 @@ async function elevateSchtasks(args: string[]): Promise<void> {
981
999
  }
982
1000
  }
983
1001
 
1002
+ export interface WindowsSchedulerRollbackDeps {
1003
+ queryXml?: () => string;
1004
+ deleteTask?: () => Promise<void>;
1005
+ probe?: () => WindowsSchedulerTaskProbe;
1006
+ }
1007
+
1008
+ export async function rollbackWindowsSchedulerTaskOwnedByAttempt(
1009
+ attemptNonce: string,
1010
+ taskName = TASK,
1011
+ deps: WindowsSchedulerRollbackDeps = {},
1012
+ ): Promise<string | null> {
1013
+ let registeredXml = "";
1014
+ try {
1015
+ registeredXml = (deps.queryXml ?? (() => querySchtasks(["/query", "/tn", taskName, "/xml"])))();
1016
+ } catch (error) {
1017
+ const detail = error instanceof Error ? error.message : String(error);
1018
+ return `Task Scheduler task ${taskName} ownership could not be proven: ${detail}. Residual scheduler state: task ${taskName} presence is unknown; no rollback deletion was attempted.`;
1019
+ }
1020
+ if (!registeredXml.trim()) {
1021
+ return `Task Scheduler task ${taskName} ownership could not be proven because its live XML was empty. Residual scheduler state: task ${taskName} presence is unknown; no rollback deletion was attempted.`;
1022
+ }
1023
+ if (!windowsTaskRegistrationOwnedByAttempt(registeredXml, attemptNonce)) {
1024
+ return `Task Scheduler task ${taskName} ownership could not be proven because its attempt nonce does not match. Residual scheduler state: task ${taskName} remains registered; no rollback deletion was attempted.`;
1025
+ }
1026
+
1027
+ try {
1028
+ await (deps.deleteTask ?? (() => elevateSchtasks(["/delete", "/tn", taskName, "/f"])))();
1029
+ } catch (error) {
1030
+ const detail = error instanceof Error ? error.message : String(error);
1031
+ return `Rollback deletion failed: ${detail}. Residual scheduler state: task ${taskName} may remain registered.`;
1032
+ }
1033
+ const probe = (deps.probe ?? (() => resolveWindowsSchedulerTaskProbe(taskName)))();
1034
+ if (probe.status === "absent") return null;
1035
+ if (probe.status === "unknown") {
1036
+ return `Task Scheduler task ${taskName} presence could not be verified after rollback: ${probe.detail}. Residual scheduler state: task presence is unknown.`;
1037
+ }
1038
+ return `Residual scheduler state: task ${taskName} is still present after rollback.`;
1039
+ }
1040
+
1041
+ // Legacy dashboard finalization creates and runs in one elevated child, whose protocol
1042
+ // performs its own rollback before returning. This fallback remains for indeterminate
1043
+ // protocol outcomes that predate the staged CLI transaction.
984
1044
  async function rollbackElevatedSchedulerTask(taskName = TASK): Promise<string | null> {
985
1045
  try {
986
1046
  await elevateSchtasks(["/delete", "/tn", taskName, "/f"]);
@@ -1454,6 +1514,7 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
1454
1514
  windowsBatchSet(BUN_RUNTIME_PATH_ENV, bun, "path"),
1455
1515
  windowsBatchSet("PATH", path, "pathList"),
1456
1516
  windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim(), "path"),
1517
+ windowsBatchSet("CODEX_SQLITE_HOME", currentCodexSqliteHomeAbsolute("windows"), "path"),
1457
1518
  windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim(), "path"),
1458
1519
  windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath(), "path"),
1459
1520
  windowsBatchSet("OCX_SERVICE_LOG", serviceLogPath(), "path"),
@@ -1488,6 +1549,11 @@ export function buildWindowsSchtasksCreateArgs(script = windowsServiceScriptPath
1488
1549
  return ["/create", "/tn", TASK, "/xml", xml, "/f"];
1489
1550
  }
1490
1551
 
1552
+ /** Build the fixed scheduler-create command from an explicit staged XML document. */
1553
+ export function buildWindowsSchtasksCreateArgsForXml(xml: string): string[] {
1554
+ return ["/create", "/tn", TASK, "/xml", xml, "/f"];
1555
+ }
1556
+
1491
1557
  /**
1492
1558
  * VBS launcher that starts the batch wrapper with a hidden window (style 0).
1493
1559
  * bWaitOnReturn=True keeps wscript.exe resident for the wrapper's lifetime so the
@@ -1508,7 +1574,17 @@ export function buildWindowsLauncherVbs(script = windowsServiceScriptPath()): st
1508
1574
  return `${lines.join("\r\n")}\r\n`;
1509
1575
  }
1510
1576
 
1511
- export function buildWindowsTaskXml(script = windowsServiceScriptPath(), launcher = windowsLauncherVbsPath()): string {
1577
+ function windowsTaskDescription(attemptNonce?: string): string {
1578
+ return attemptNonce
1579
+ ? `OpenCodex proxy service wrapper; install-attempt=${attemptNonce}`
1580
+ : "OpenCodex proxy service wrapper";
1581
+ }
1582
+
1583
+ export function buildWindowsTaskXml(
1584
+ script = windowsServiceScriptPath(),
1585
+ launcher = windowsLauncherVbsPath(),
1586
+ attemptNonce?: string,
1587
+ ): string {
1512
1588
  const escapedWscript = taskXmlString(windowsWscript());
1513
1589
  // Escape the launcher path independently for the <Arguments> element; quoting it
1514
1590
  // keeps spaces intact, and /b (batch mode) suppresses script error popups.
@@ -1516,7 +1592,7 @@ export function buildWindowsTaskXml(script = windowsServiceScriptPath(), launche
1516
1592
  return `<?xml version="1.0" encoding="UTF-16"?>
1517
1593
  <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
1518
1594
  <RegistrationInfo>
1519
- <Description>OpenCodex proxy service wrapper</Description>
1595
+ <Description>${taskXmlString(windowsTaskDescription(attemptNonce))}</Description>
1520
1596
  </RegistrationInfo>
1521
1597
  <Triggers>
1522
1598
  <LogonTrigger>
@@ -1636,6 +1712,21 @@ function taskXmlOptionalValueEquals(xml: string, tag: string, expected: string):
1636
1712
  return value?.trim().toLowerCase() === expected.toLowerCase();
1637
1713
  }
1638
1714
 
1715
+ /** True only when the exported live task carries this install attempt's nonce. */
1716
+ export function windowsTaskRegistrationOwnedByAttempt(xml: string, attemptNonce: string): boolean {
1717
+ if (!attemptNonce) return false;
1718
+ const scrubbed = taskXmlWithoutCommentsAndCdata(xml);
1719
+ if (taskXmlElementCount(scrubbed, "Data") > 0 || taskXmlHasPrefixedTag(scrubbed, "Data")) return false;
1720
+ if (taskXmlHasPrefixedTag(scrubbed, "RegistrationInfo")) return false;
1721
+ if (taskXmlElementCount(scrubbed, "RegistrationInfo") !== 1) return false;
1722
+ const registrationInfo = taskXmlSection(scrubbed, "RegistrationInfo");
1723
+ return taskXmlDecodedValueEquals(
1724
+ registrationInfo,
1725
+ "Description",
1726
+ windowsTaskDescription(attemptNonce),
1727
+ );
1728
+ }
1729
+
1639
1730
  /** Validate the security/lifecycle-critical fields of the registered scheduler task. */
1640
1731
  export function windowsTaskRegistrationHealthy(
1641
1732
  xml: string,
@@ -1805,6 +1896,88 @@ function writeWindowsSchedulerAssets(): void {
1805
1896
  writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
1806
1897
  }
1807
1898
 
1899
+ function stageWindowsSchedulerRegistrationXml(attemptNonce: string): string {
1900
+ if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true, mode: 0o700 });
1901
+ const path = join(getConfigDir(), `.opencodex-service-task.${randomUUID()}.xml`);
1902
+ // This document points at the canonical launcher but does not publish or rewrite that
1903
+ // launcher. UAC can therefore be refused while the current proxy still owns its port.
1904
+ writeServiceAssetWithRetry(
1905
+ path,
1906
+ `\uFEFF${buildWindowsTaskXml(windowsServiceScriptPath(), windowsLauncherVbsPath(), attemptNonce)}`,
1907
+ "utf16le",
1908
+ );
1909
+ return path;
1910
+ }
1911
+
1912
+ export interface FreshWindowsSchedulerRegistrationDeps {
1913
+ create?: (args: string[]) => void;
1914
+ elevate?: (args: string[]) => Promise<void>;
1915
+ probe?: () => WindowsSchedulerTaskProbe;
1916
+ queryXml?: () => string;
1917
+ rollback?: () => Promise<string | null>;
1918
+ }
1919
+
1920
+ export async function registerFreshWindowsSchedulerTask(
1921
+ xmlPath: string,
1922
+ attemptNonce: string,
1923
+ deps: FreshWindowsSchedulerRegistrationDeps = {},
1924
+ ): Promise<void> {
1925
+ const args = buildWindowsSchtasksCreateArgsForXml(xmlPath);
1926
+ try {
1927
+ (deps.create ?? schtasks)(args);
1928
+ } catch (error) {
1929
+ if (
1930
+ !(error instanceof WindowsSchtasksError)
1931
+ || error.operation !== "create"
1932
+ || error.reason !== "access-denied"
1933
+ ) {
1934
+ throw error;
1935
+ }
1936
+ // The elevated command is still the fixed trusted schtasks executable plus the
1937
+ // owned create shape. It registers only; the task is not run until cleanup commits.
1938
+ await (deps.elevate ?? elevateSchtasks)(args);
1939
+ }
1940
+
1941
+ const rollbackTask = deps.rollback ?? (() => rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK));
1942
+ const probe = (deps.probe ?? (() => probeWindowsSchedulerTask(TASK)))();
1943
+ if (probe.status === "absent") {
1944
+ throw new Error("Task Scheduler reported success, but the new registration is absent; no service cleanup was started.");
1945
+ }
1946
+ if (probe.status === "unknown") {
1947
+ const rollback = await rollbackTask();
1948
+ throw new Error(
1949
+ `Task Scheduler registration was not verifiably present after create (${probe.detail}).`
1950
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The unverified registration was rolled back."),
1951
+ );
1952
+ }
1953
+
1954
+ let registeredXml = "";
1955
+ let queryDetail: string | null = null;
1956
+ try {
1957
+ registeredXml = (deps.queryXml ?? (() => querySchtasks(["/query", "/tn", TASK, "/xml"])))();
1958
+ } catch (error) {
1959
+ queryDetail = error instanceof Error ? error.message : String(error);
1960
+ }
1961
+ if (!registeredXml.trim()) {
1962
+ const rollback = await rollbackTask();
1963
+ throw new Error(
1964
+ "Task Scheduler registration was created, but its live XML could not be verified."
1965
+ + (queryDetail ? ` Query failed: ${queryDetail}` : " The query returned an empty document.")
1966
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The unverified registration was rolled back."),
1967
+ );
1968
+ }
1969
+ if (
1970
+ !windowsTaskRegistrationHealthy(registeredXml)
1971
+ || !windowsTaskRegistrationOwnedByAttempt(registeredXml, attemptNonce)
1972
+ ) {
1973
+ const rollback = await rollbackTask();
1974
+ throw new Error(
1975
+ "Task Scheduler registration was created but failed the OpenCodex action/trigger or attempt-ownership verification."
1976
+ + (rollback ? ` Cleanup also failed: ${rollback}` : " The invalid registration was rolled back."),
1977
+ );
1978
+ }
1979
+ }
1980
+
1808
1981
  function installWindows(): void {
1809
1982
  recordOwnedConfigPath(getConfigDir(), serviceStatePath());
1810
1983
  // Transactional backend switch: installing the scheduler backend removes a native
@@ -2046,6 +2219,7 @@ export function buildUnit(): string {
2046
2219
  const log = logPath();
2047
2220
  const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
2048
2221
  const codexHome = systemdEnvironmentAssignment("CODEX_HOME", process.env.CODEX_HOME?.trim());
2222
+ const codexSqliteHome = systemdEnvironmentAssignment("CODEX_SQLITE_HOME", currentCodexSqliteHomeAbsolute());
2049
2223
  const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
2050
2224
  const envLines = [
2051
2225
  systemdEnvironmentAssignment("OCX_SERVICE", "1"),
@@ -2053,6 +2227,7 @@ export function buildUnit(): string {
2053
2227
  systemdEnvironmentAssignment(BUN_RUNTIME_PATH_ENV, bun),
2054
2228
  systemdEnvironmentAssignment("PATH", path),
2055
2229
  codexHome,
2230
+ codexSqliteHome,
2056
2231
  opencodexHome,
2057
2232
  ].filter((line): line is string => Boolean(line)).join("\n");
2058
2233
  return `[Unit]
@@ -2417,6 +2592,85 @@ export async function installServiceSafely(
2417
2592
  await install();
2418
2593
  }
2419
2594
 
2595
+ export interface FreshWindowsSchedulerInstallDeps {
2596
+ stageRegistrationXml?: (attemptNonce: string) => string;
2597
+ register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
2598
+ prepare?: () => Promise<void>;
2599
+ publishAssets?: () => void;
2600
+ runTask?: () => void;
2601
+ writeState?: () => void;
2602
+ rollbackTask?: (attemptNonce: string) => Promise<string | null>;
2603
+ removeStagedXml?: (xmlPath: string) => void;
2604
+ }
2605
+
2606
+ /**
2607
+ * Fresh Windows scheduler install with UAC before the destructive commit.
2608
+ *
2609
+ * The registration is created but never run before `prepare`: UAC cancellation and
2610
+ * create failure therefore cannot stop the existing proxy or trigger its native-routing
2611
+ * cleanup. Rollback proves ownership from the live registration's attempt nonce before
2612
+ * deleting, because the fixed task name can be replaced by another process at any time.
2613
+ */
2614
+ export async function installFreshWindowsSchedulerSafely(
2615
+ deps: FreshWindowsSchedulerInstallDeps = {},
2616
+ ): Promise<void> {
2617
+ const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml;
2618
+ const register = deps.register ?? registerFreshWindowsSchedulerTask;
2619
+ const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler"));
2620
+ const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets;
2621
+ const runTask = deps.runTask ?? startWindows;
2622
+ const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler"));
2623
+ const rollbackTask = deps.rollbackTask ?? ((attemptNonce: string) => (
2624
+ rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK)
2625
+ ));
2626
+ const removeStagedXml = deps.removeStagedXml ?? ((path: string) => {
2627
+ if (existsSync(path)) unlinkSync(path);
2628
+ });
2629
+
2630
+ let stagedXml: string | null = null;
2631
+ const attemptNonce = randomUUID();
2632
+ let registered = false;
2633
+ let started = false;
2634
+ try {
2635
+ stagedXml = stage(attemptNonce);
2636
+ await register(stagedXml, attemptNonce);
2637
+ registered = true;
2638
+
2639
+ // The destructive boundary begins only after Task Scheduler accepted the definition.
2640
+ await prepare();
2641
+ publishAssets();
2642
+ runTask();
2643
+ started = true;
2644
+ writeState();
2645
+ } catch (error) {
2646
+ const detail = error instanceof Error ? error.message : String(error);
2647
+ if (registered && !started) {
2648
+ const rollback = await rollbackTask(attemptNonce);
2649
+ throw new Error(
2650
+ `${detail}\n`
2651
+ + (rollback
2652
+ ? `The new Task Scheduler registration may remain: ${rollback}`
2653
+ : "The new Task Scheduler registration was rolled back. The previous proxy/routing state was not assumed restored."),
2654
+ );
2655
+ }
2656
+ if (started) {
2657
+ throw new Error(
2658
+ `${detail}\nThe scheduler task started, but install state was not published. `
2659
+ + "The task was left in place; inspect `ocx service status` before retrying.",
2660
+ );
2661
+ }
2662
+ throw error;
2663
+ } finally {
2664
+ if (stagedXml) {
2665
+ try { removeStagedXml(stagedXml); } catch (error) {
2666
+ console.error(
2667
+ `⚠️ Failed to remove temporary Task Scheduler XML ${stagedXml}: ${error instanceof Error ? error.message : String(error)}`,
2668
+ );
2669
+ }
2670
+ }
2671
+ }
2672
+ }
2673
+
2420
2674
  /**
2421
2675
  * If a service is installed, stop it so the process manager doesn't respawn after `ocx stop`.
2422
2676
  * Returns true if a service was found and stopped.
@@ -2792,7 +3046,19 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
2792
3046
  // restart-loops on EADDRINUSE, and the old standalone process makes the install
2793
3047
  // verification report a false success.
2794
3048
  try {
2795
- await installServiceSafely(backend, ops.install);
3049
+ if (process.platform === "win32" && backend === "scheduler") {
3050
+ const scheduler = probeWindowsSchedulerTask(TASK);
3051
+ if (scheduler.status === "unknown") {
3052
+ throw new Error(`Task Scheduler state could not be verified before install: ${scheduler.detail}`);
3053
+ }
3054
+ if (scheduler.status === "absent") {
3055
+ await installFreshWindowsSchedulerSafely();
3056
+ } else {
3057
+ await installServiceSafely(backend, ops.install);
3058
+ }
3059
+ } else {
3060
+ await installServiceSafely(backend, ops.install);
3061
+ }
2796
3062
  } catch (error) {
2797
3063
  console.error(`❌ Service install cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
2798
3064
  process.exitCode = 1;
package/src/types.ts CHANGED
@@ -1,5 +1,26 @@
1
1
  import type { KiroOAuthMetadata } from "./oauth/types";
2
2
 
3
+ /** Exact provider/credential namespace for process-local reasoning replay. */
4
+ export interface OcxReasoningReplayIdentity {
5
+ providerName: string;
6
+ /** Opaque process-local digest of the exact upstream destination. */
7
+ providerDestinationIdentity: string;
8
+ adapterName: string;
9
+ modelId: string;
10
+ /** Opaque process-local credential identity; never a raw token or API key. */
11
+ credentialIdentity: string;
12
+ }
13
+
14
+ /**
15
+ * Stable holder shared by parsed-request copies and already-created bridges.
16
+ * Credential/provider rotation replaces `current` atomically without replacing
17
+ * the holder, so late tool-call cache writes see the active physical identity.
18
+ */
19
+ export interface OcxReasoningReplayScopeRef {
20
+ readonly clientThreadId: string;
21
+ current?: Readonly<OcxReasoningReplayIdentity>;
22
+ }
23
+
3
24
  export interface OcxParsedRequest {
4
25
  modelId: string;
5
26
  /** Client-facing model selector retained for Anthropic routes after wire-model normalization. */
@@ -13,12 +34,16 @@ export interface OcxParsedRequest {
13
34
  _rawBody?: unknown;
14
35
  /** Number of leading raw input items restored from local previous_response_id state. */
15
36
  _replayPrefixLen?: number;
37
+ /** Parsed-message index before the first conversational item in a continuation's current delta. */
38
+ _continuationConversationMessageIndex?: number;
16
39
  /** True when the proxy expanded a previous_response_id request into a full input replay. */
17
40
  _previousResponseInputExpanded?: boolean;
18
41
  /** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
19
42
  _cursorConversationId?: string;
20
43
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
21
44
  _clientThreadId?: string;
45
+ /** Provider/account/model-bound namespace for process-local raw-reasoning replay. */
46
+ _reasoningReplayScope?: OcxReasoningReplayScopeRef;
22
47
  /**
23
48
  * Optional authenticated tenant/operator namespace for Cursor thread→conversation derivation.
24
49
  * When absent (single-operator local proxy), derivation stays local-scoped.
@@ -995,6 +1020,19 @@ export interface OcxRoutingProfileUnknownEvidence {
995
1020
  cost?: OcxRoutingUnknownEvidenceMode;
996
1021
  }
997
1022
 
1023
+ export interface OcxRoutingProfileCompatibilitySuite {
1024
+ suiteId: string;
1025
+ evidenceLayer: "protocol_conformance" | "live_route_compatibility";
1026
+ }
1027
+
1028
+ export interface OcxRoutingProfileCompatibility {
1029
+ requiredSuites?: OcxRoutingProfileCompatibilitySuite[];
1030
+ minStatus?: "PROBED" | "VERIFIED";
1031
+ maxEvidenceAgeMs?: number;
1032
+ unknownEvidence?: OcxRoutingUnknownEvidenceMode;
1033
+ degradedEvidence?: OcxRoutingUnknownEvidenceMode;
1034
+ }
1035
+
998
1036
  export interface OcxRoutingProfileConfig {
999
1037
  /**
1000
1038
  * Explicit candidate allowlist (`provider/model` refs). No implicit
@@ -1010,6 +1048,8 @@ export interface OcxRoutingProfileConfig {
1010
1048
  limits?: OcxRoutingProfileLimits;
1011
1049
  /** How unknown evidence is handled per dimension. */
1012
1050
  unknownEvidence?: OcxRoutingProfileUnknownEvidence;
1051
+ /** Optional Compatibility Lab policy (CL-06). */
1052
+ compatibility?: OcxRoutingProfileCompatibility;
1013
1053
  }
1014
1054
 
1015
1055
  /**
@@ -1210,9 +1250,9 @@ export interface OcxProviderConfig {
1210
1250
  */
1211
1251
  statelessResponses?: boolean;
1212
1252
  /**
1213
- * Responses upstream whose parser requires each tool result to immediately follow
1214
- * its matching call. When enabled, only unambiguous matched pairs are reordered;
1215
- * intervening messages are preserved after the result instead of being dropped.
1253
+ * Responses upstream whose parser requires an unambiguous call batch and its matched
1254
+ * result batch to remain contiguous. Hook-injected context that splits the batch is
1255
+ * preserved after it, and parallel calls stay together with the reasoning turn that produced them.
1216
1256
  */
1217
1257
  requiresAdjacentResponsesToolResults?: boolean;
1218
1258
  /**
@@ -1378,6 +1418,12 @@ export interface OcxProviderConfig {
1378
1418
  noTopPModels?: string[];
1379
1419
  /** Model ids that reject caller-specified presence/frequency penalty values. */
1380
1420
  noPenaltyModels?: string[];
1421
+ /**
1422
+ * Model ids whose Chat Completions endpoint rejects `response_format`.
1423
+ * Structured-output translation remains enabled by default; this is a narrow
1424
+ * per-model compatibility escape hatch for mixed-capability gateways.
1425
+ */
1426
+ noStructuredOutputModels?: string[];
1381
1427
  /**
1382
1428
  * Allow multiple tool calls per completion. DEFAULT-ON for openai-chat providers (the
1383
1429
  * buffered stream parser assembles interleaved/fragmented multi-call turns safely);
@@ -40,6 +40,15 @@ export interface ExpectedPriceOverlay {
40
40
 
41
41
  const GEMINI_31_PRO: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 };
42
42
  const GPT56_SOL: Cost4 = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 };
43
+ /**
44
+ * Daybreak aliases. `daybreak-*-latest` never appears in the pricing table itself — only its
45
+ * current snapshot does — so these tuples are the snapshot's published rates and carry
46
+ * `verified-derived`. That status also keeps the `estimated` marker on, which matters more
47
+ * here than for a normal row: OpenAI can repoint an alias at a differently-priced model.
48
+ * Red = gpt-5.6-cyber (12.50 / 1.25 / 15.625 / 75, cache write = 1.25x uncached input).
49
+ * Blue = gpt-5.6-sol. Verified 2026-08-11 against the pricing page's Cyber models table.
50
+ */
51
+ const DAYBREAK_RED: Cost4 = { input: 12.5, output: 75, cacheRead: 1.25, cacheWrite: 15.625 };
43
52
  const GPT56_TERRA: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 };
44
53
  const GPT56_LUNA: Cost4 = { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 };
45
54
  const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 0 };
@@ -103,6 +112,11 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
103
112
  { provider: "openai-apikey", modelId: "gpt-5.6-sol-pro", cost4: GPT56_SOL, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
104
113
  { provider: "openai-apikey", modelId: "gpt-5.6-terra-pro", cost4: GPT56_TERRA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
105
114
  { provider: "openai-apikey", modelId: "gpt-5.6-luna-pro", cost4: GPT56_LUNA, source: `collapsed base ID ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-03", status: "verified-derived" },
115
+ // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber,
116
+ // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence
117
+ // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple.
118
+ { provider: "openai-apikey", modelId: "daybreak-red-latest", cost4: DAYBREAK_RED, source: `alias of gpt-5.6-cyber ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" },
119
+ { provider: "openai-apikey", modelId: "daybreak-blue-latest", cost4: GPT56_SOL, source: `alias of gpt-5.6-sol ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" },
106
120
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-low", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
107
121
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-high", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
108
122
  { provider: "google-antigravity", modelId: "gemini-pro-agent", cost4: GEMINI_31_PRO, source: `wire id for gemini-3.1-pro high ${GEMINI_PRICING}`, verifiedAt: "2026-07-23", status: "verified-derived" },
@@ -263,6 +277,22 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
263
277
  source: "https://docs.x.ai/developers/pricing",
264
278
  verifiedAt: "2026-08-03",
265
279
  },
280
+ {
281
+ // daybreak-blue-latest aliases gpt-5.6-sol, which publishes the full long-context row
282
+ // ($10 / $1 / $12.50 / $45). Scoped to openai-apikey ON PURPOSE: Daybreak is not
283
+ // routable on the Codex-login `openai` provider, so adding the alias to the shared
284
+ // OPENAI_GPT56_CONTEXT_MODELS list (expanded across both providers above) would mint a
285
+ // tier for a provider/model pair that cannot exist.
286
+ // daybreak-red-latest has NO tier row: the cyber snapshot's four long-context cells are
287
+ // all "-" on the pricing page (verified 2026-08-11).
288
+ provider: "openai-apikey",
289
+ modelId: "daybreak-blue-latest",
290
+ thresholdInputTokens: 272_000,
291
+ inclusive: false,
292
+ multiplier: OPENAI_LONG_CONTEXT,
293
+ source: OPENAI_PRICING_DOC,
294
+ verifiedAt: "2026-08-11",
295
+ },
266
296
  ...["minimax", "minimax-cn"].map((provider): ContextTier => ({
267
297
  provider,
268
298
  modelId: "MiniMax-M3",
package/src/usage/log.ts CHANGED
@@ -5,8 +5,14 @@ import { recordOwnedConfigPath } from "../lib/config-ownership";
5
5
  import { usageDisplayTotalTokens } from "./totals";
6
6
  import type { OcxUsage } from "../types";
7
7
  import { normalizeRouteDecisionTrace, type RouteDecisionTraceV1 } from "../routing/trace";
8
+ import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
8
9
 
9
10
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
11
+ export type CodexUsageAccountLogLabel = "main" | `p${string}`;
12
+
13
+ export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
14
+ return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
15
+ }
10
16
 
11
17
  /**
12
18
  * Recovery kinds recorded per attempt in the usage log; the GUI renders localized labels
@@ -33,10 +39,14 @@ export interface PersistedUsageAttempt {
33
39
  sendCount: number;
34
40
  recoveryKinds: AttemptRecoveryKind[];
35
41
  usageStatus: UsageStatus;
42
+ /** Stable non-PII identity for the Codex pool account that served this attempt. */
43
+ accountLogLabel?: CodexUsageAccountLogLabel;
36
44
  inputTokenEstimate?: number;
37
45
  usage?: OcxUsage;
38
46
  totalTokens?: number;
39
47
  errorCode?: string;
48
+ /** Installation-local exact Compatibility Lab route-subject digest for this attempt. */
49
+ labRouteSubjectId?: string;
40
50
  /** Target-specific reasoning intent and exact adapter-normalized wire parameter. */
41
51
  requestedEffort?: string;
42
52
  effectiveEffort?: string;
@@ -56,6 +66,8 @@ export interface PersistedUsageEntry {
56
66
  admissionKind?: "configured" | "environment" | "loopback";
57
67
  /** The inbound wire, not the client product — see `surface`. */
58
68
  inboundProtocol?: "responses" | "chat" | "messages";
69
+ /** Stable non-PII identity for Codex Pool usage; absent for Direct/non-Codex traffic. */
70
+ accountLogLabel?: CodexUsageAccountLogLabel;
59
71
  /** Best-effort chat/session correlation for Logs grouping (#330). */
60
72
  conversationId?: string;
61
73
  resolvedModel?: string;
@@ -130,8 +142,8 @@ export function isKnownInboundProtocol(value: unknown): value is NonNullable<Per
130
142
  return typeof value === "string" && KNOWN_INBOUND_PROTOCOLS.has(value as NonNullable<PersistedUsageEntry["inboundProtocol"]>);
131
143
  }
132
144
 
133
- export function usageLogPath(): string {
134
- return join(getConfigDir(), "usage.jsonl");
145
+ export function usageLogPath(configDir?: string): string {
146
+ return join(configDir ?? getConfigDir(), "usage.jsonl");
135
147
  }
136
148
 
137
149
  export function usageTotalTokens(usage: OcxUsage | undefined): number | undefined {
@@ -195,6 +207,11 @@ const USAGE_STATUSES = new Set<UsageStatus>([
195
207
  "unsupported",
196
208
  "estimated",
197
209
  ]);
210
+ const LAB_ROUTE_SUBJECT_ID_RE = /^[0-9a-f]{64}$/;
211
+
212
+ export function isLabRouteSubjectId(value: unknown): value is string {
213
+ return typeof value === "string" && LAB_ROUTE_SUBJECT_ID_RE.test(value);
214
+ }
198
215
 
199
216
  function isNonNegativeFiniteNumber(value: unknown): value is number {
200
217
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
@@ -264,6 +281,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
264
281
  sendCount: attempt.sendCount as number,
265
282
  recoveryKinds,
266
283
  usageStatus: attempt.usageStatus as UsageStatus,
284
+ ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
285
+ ? { accountLogLabel: attempt.accountLogLabel }
286
+ : {}),
267
287
  ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
268
288
  ? { inputTokenEstimate: attempt.inputTokenEstimate }
269
289
  : {}),
@@ -272,6 +292,9 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
272
292
  ? { totalTokens: attempt.totalTokens }
273
293
  : {}),
274
294
  ...(typeof attempt.errorCode === "string" ? { errorCode: attempt.errorCode } : {}),
295
+ ...(isLabRouteSubjectId(attempt.labRouteSubjectId)
296
+ ? { labRouteSubjectId: attempt.labRouteSubjectId }
297
+ : {}),
275
298
  ...(typeof attempt.requestedEffort === "string" && attempt.requestedEffort
276
299
  ? { requestedEffort: capMetadataString(attempt.requestedEffort) }
277
300
  : {}),
@@ -340,6 +363,9 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
340
363
  : {}),
341
364
  ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
342
365
  ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
366
+ ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
367
+ ? { accountLogLabel: entry.accountLogLabel }
368
+ : {}),
343
369
  ...(typeof entry.conversationId === "string" && entry.conversationId.trim()
344
370
  ? { conversationId: entry.conversationId.trim().slice(0, 128) }
345
371
  : {}),
@@ -647,9 +673,9 @@ function parseUsageLines(lines: string[]): PersistedUsageEntry[] {
647
673
  * Read only the newest `limit` usage.jsonl rows without loading the whole append-only
648
674
  * file into memory. Used by request-log hydration on `ocx start`.
649
675
  */
650
- export function readRecentUsageEntries(limit: number): PersistedUsageEntry[] {
676
+ export function readRecentUsageEntries(limit: number, configDir?: string): PersistedUsageEntry[] {
651
677
  if (!Number.isFinite(limit) || limit <= 0) return [];
652
- const path = usageLogPath();
678
+ const path = usageLogPath(configDir);
653
679
  if (!existsSync(path)) return [];
654
680
  let fd: number | undefined;
655
681
  try {