@bitkyc08/opencodex 2.17.0 → 2.18.2

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 (37) hide show
  1. package/gui/dist/assets/{index-DOKr6RBR.js → index-CXI1262_.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/client-fingerprint.ts +2 -2
  5. package/src/adapters/cursor/live-transport.ts +17 -5
  6. package/src/adapters/cursor/protobuf-events.ts +662 -20
  7. package/src/adapters/cursor/tool-definitions.ts +12 -6
  8. package/src/adapters/google.ts +21 -2
  9. package/src/bridge.ts +12 -2
  10. package/src/cli/index.ts +11 -0
  11. package/src/codex/app-server-processes.ts +3 -3
  12. package/src/codex/user-identity.ts +36 -6
  13. package/src/config.ts +0 -2
  14. package/src/generated/compatibility-version.json +37 -33
  15. package/src/lib/token-estimate.ts +19 -2
  16. package/src/lib/windows-elevation.ts +37 -0
  17. package/src/lib/windows-secret-acl.ts +7 -0
  18. package/src/lib/windows-text.ts +106 -0
  19. package/src/lib/windows-user-principal.ts +0 -2
  20. package/src/oauth/index.ts +1 -1
  21. package/src/oauth/store.ts +32 -18
  22. package/src/providers/antigravity-models.ts +25 -5
  23. package/src/providers/free-directory.ts +1 -1
  24. package/src/providers/registry.ts +4 -3
  25. package/src/server/index.ts +5 -1
  26. package/src/server/management/logs-usage-routes.ts +7 -22
  27. package/src/server/request-log.ts +48 -3
  28. package/src/server/responses/core.ts +38 -13
  29. package/src/server/responses/encrypted-payload.ts +58 -38
  30. package/src/server/responses/fetch-helpers.ts +12 -4
  31. package/src/server/responses/policy-fallback.ts +13 -2
  32. package/src/server/responses/ws-upstream.ts +115 -6
  33. package/src/service-manager-probe.ts +21 -34
  34. package/src/service.ts +233 -25
  35. package/src/tray/windows.ts +0 -2
  36. package/src/update/job.ts +2 -2
  37. package/src/usage/summary.ts +21 -4
@@ -200,6 +200,12 @@ export function cursorRequestHasShellAlias(tools: readonly Pick<OcxTool, "namesp
200
200
  return tools?.some(isBareCodexExecCommandTool) ?? false;
201
201
  }
202
202
 
203
+ function cursorRequestHasExecutionPath(
204
+ tools: readonly Pick<OcxTool, "namespace" | "name">[] | undefined,
205
+ ): boolean {
206
+ return tools?.some(isCursorExecutionPathTool) ?? false;
207
+ }
208
+
203
209
  export function cursorRequestAdvertisesApplyPatch(
204
210
  tools: readonly Pick<OcxTool, "namespace" | "name" | "freeform">[] | undefined,
205
211
  toolChoice?: OcxRequestOptions["toolChoice"],
@@ -249,14 +255,14 @@ export function cursorStructuredEditTools(
249
255
  name: CURSOR_EDIT_FILE_TOOL,
250
256
  cursorStructuredEdit: true,
251
257
  description:
252
- "Replace one block of exact text in a file. OpenCodex converts the replacement into a Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
258
+ "Replace one block of text in a file. OpenCodex converts the replacement into a Codex apply_patch change. Copy old_string and new_string with their exact leading whitespace — Codex may locate a line after trimming indent, but it writes new_string verbatim, so stripped indent silently corrupts the file. An empty old_string with a non-empty new_string creates a new file (Add File). If the same text appears more than once, the first match is updated. Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
253
259
  parameters: { ...CURSOR_EDIT_FILE_INPUT_SCHEMA },
254
260
  },
255
261
  {
256
262
  name: CURSOR_MULTI_EDIT_TOOL,
257
263
  cursorStructuredEdit: true,
258
264
  description:
259
- "Apply several exact-text replacements to one file. OpenCodex converts the edits into a single Codex apply_patch change, which the Codex client applies with its normal approval and sandbox policy. Each old_string must match the current file content exactly at exactly one location (apply_patch rejects ambiguous hunks). Edits are independent: every old_string is matched against the ORIGINAL file content, so a later edit must not rely on text introduced by an earlier one. Matching is line-based, so an edit cannot add or remove only the file's final newline, and old_string/new_string that are identical after line normalization are rejected as a no-op.",
265
+ "Apply several text replacements to one file. OpenCodex converts them into one Codex apply_patch change. Copy each old_string/new_string with exact leading whitespace. If a later edit's old_string is the text after an earlier replacement, OpenCodex folds those edits into one original-file hunk. Independent edits stay separate hunks. An empty old_string with a non-empty new_string creates a new file (Add File); do not mix that with an independent Update on the same path. If the same text appears more than once, the first match is updated. Matching is line-based, so an edit cannot add or remove only the file's final newline, and identical old/new after line normalization are rejected as a no-op.",
260
266
  parameters: { ...CURSOR_MULTI_EDIT_INPUT_SCHEMA },
261
267
  },
262
268
  ];
@@ -421,7 +427,7 @@ export function shouldUseNativeExecOnlyForGenericToolUse(
421
427
  text: string,
422
428
  ): boolean {
423
429
  const trimmed = text.trim();
424
- if (trimmed.length === 0 || !cursorRequestHasShellAlias(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false;
430
+ if (trimmed.length === 0 || !cursorRequestHasExecutionPath(tools) || !isGenericToolUseCountDemoPrompt(trimmed)) return false;
425
431
  return !/\b(?:mcp|resource|resources|tool_search|plugin|plugins|app connector|github)\b/i.test(trimmed)
426
432
  && !/(?:리소스|플러그인|깃허브|github)/i.test(trimmed);
427
433
  }
@@ -432,7 +438,7 @@ export function cursorToolsForActivePrompt<T extends Pick<OcxTool, "namespace" |
432
438
  toolChoice?: OcxRequestOptions["toolChoice"],
433
439
  ): readonly T[] | undefined {
434
440
  if (!shouldUseNativeExecOnlyForGenericToolUse(tools, activeText)) return tools;
435
- const execTools = tools?.filter(isBareCodexExecCommandTool);
441
+ const execTools = tools?.filter(isCursorExecutionPathTool);
436
442
  const catalog = tools ?? [];
437
443
  if (execTools?.length && !execTools.some(tool => cursorToolAllowedByChoice(tool, toolChoice, catalog))) return tools;
438
444
  return execTools && execTools.length > 0 ? execTools : tools;
@@ -558,7 +564,7 @@ export function buildCursorToolGuidanceSystemNote(
558
564
  // Host-shell-neutral: the Codex client executes bridge commands, and may differ from
559
565
  // the OpenCodex proxy OS (LAN/SSH remote-proxy). Always cover PowerShell 5.1 pitfalls.
560
566
  const hostShellNote = hasBareExec
561
- ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`<<EOF`); `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`. POSIX: use portable commands. After a shell failure, make at most one corrected bridge attempt, then report the error and stop — do not repeat equivalent failing commands."
567
+ ? "Match shell syntax to the Codex client host that runs the bridge (not only the proxy OS). Windows PowerShell 5.1: no CMD `cd /d`, no bash heredocs (`<<EOF`); `&&`/`||` are unsupported parser errors — prefer the bridge working-directory argument for directory changes, and use `if ($?) { ... }` for success-gated follow-up steps; do not treat `;` as a substitute for `&&`. POSIX: use portable commands (`cat`/`ls`/`rg`); never emit Get-Content or Get-ChildItem unless the host shell is PowerShell. After a shell failure, make at most one corrected bridge attempt, then report the error and stop — do not repeat equivalent failing commands."
562
568
  : undefined;
563
569
  const notes = [
564
570
  `Cursor tool calls: available tool names are exactly ${listedNames}.`,
@@ -582,7 +588,7 @@ export function buildCursorToolGuidanceSystemNote(
582
588
  : undefined,
583
589
  hasApplyPatch
584
590
  ? structuredEditNames.length > 0
585
- ? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take exact-match replacements that OpenCodex converts into Codex \`apply_patch\` changes for approval. Use \`apply_patch\` directly only when you can emit its exact freeform syntax (\`*** Begin Patch\` envelope with \`@@\` hunks and \`-\`/\`+\` line prefixes); never emit patch-like plain text as tool arguments.`
591
+ ? `For file edits, prefer the structured edit tools ${quotedNames(structuredEditNames)} — they take replacements that OpenCodex converts into Codex \`apply_patch\` changes. Include exact leading whitespace in old_string/new_string. Use \`apply_patch\` directly only with a \`*** Begin Patch\` envelope and bare \`@@\` hunks (never git-style \`@@ -n,m +n,m @@\`); never emit patch-like plain text as tool arguments.`
586
592
  : "For file edits, use the `apply_patch` tool, not built-in file write/delete tools."
587
593
  : undefined,
588
594
  hasBareExec
@@ -45,6 +45,25 @@ const GOOGLE_BREVITY_INSTRUCTION = [
45
45
  "- This applies only to intermediate progress text. Your final answer after the work is done is exempt: write it in full and at whatever length the task requires.",
46
46
  ].join("\n");
47
47
 
48
+ /**
49
+ * Google renamed the current Gemini Flash generations on the Generative Language API,
50
+ * appending a `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). The
51
+ * old `gemini-3.7-flash` path 404s, so a saved config or registry entry naming the base
52
+ * id must be resolved here before it reaches the URL. The user-facing id is deliberately
53
+ * left alone: the picker, the catalog, the usage log and the price overlays all stay
54
+ * keyed on the base id, and only the wire path learns the new spelling.
55
+ */
56
+ const GEMINI_DIRECT_WIRE_RENAMES: Record<string, string> = {
57
+ "gemini-3.7-flash": "gemini-3.7-flash-tiered",
58
+ "gemini-3.6-flash": "gemini-3.6-flash-tiered",
59
+ };
60
+
61
+ function resolveDirectGeminiWireModelId(modelId: string): string {
62
+ return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId)
63
+ ? GEMINI_DIRECT_WIRE_RENAMES[modelId]!
64
+ : modelId;
65
+ }
66
+
48
67
  /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
49
68
  function resolveVertexApiKey(optKey?: string): string | undefined {
50
69
  const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
@@ -353,7 +372,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
353
372
  parsed.modelId,
354
373
  mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
355
374
  ).wireModelId
356
- : parsed.modelId;
375
+ : resolveDirectGeminiWireModelId(parsed.modelId);
357
376
  const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId);
358
377
  const tools = toolsToGeminiFormat(parsed);
359
378
 
@@ -501,7 +520,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
501
520
  }
502
521
 
503
522
  // ai-studio (default): Generative Language API + x-goog-api-key.
504
- const url = `${provider.baseUrl}/v1beta/models/${parsed.modelId}:${method}${streamParam}`;
523
+ const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
505
524
  const apiKey = provider.apiKey?.trim();
506
525
  if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
507
526
  headers["x-goog-api-key"] = apiKey;
package/src/bridge.ts CHANGED
@@ -92,10 +92,11 @@ function responseError(status: number, type: string, message: string): OcxErrorP
92
92
  * non-stream adapters degrade a bad payload to `{}`.
93
93
  */
94
94
  function toolCallArgumentsUsable(args: string): boolean {
95
+ if (args.length === 0) return true;
95
96
  const trimmed = args.trim();
96
- if (!trimmed) return true;
97
+ if (!trimmed) return false;
97
98
  try {
98
- JSON.parse(trimmed);
99
+ JSON.parse(args);
99
100
  return true;
100
101
  } catch {
101
102
  return false;
@@ -1593,6 +1594,15 @@ function buildResponseJSONWithBudget(
1593
1594
  };
1594
1595
 
1595
1596
  for (const e of events) {
1597
+ if (errorEvent) {
1598
+ // Match streaming: once the turn fails, later parallel calls must not become executable
1599
+ // completed output. Still release every retained event in order and preserve terminal usage.
1600
+ if (e.type === "error" || e.type === "incomplete" || e.type === "done") {
1601
+ usage = e.usage ?? usage;
1602
+ }
1603
+ if (budget) releaseTranslatedEvent(e, budget);
1604
+ continue;
1605
+ }
1596
1606
  switch (e.type) {
1597
1607
  case "assistant_boundary":
1598
1608
  flushText("commentary");
package/src/cli/index.ts CHANGED
@@ -216,6 +216,17 @@ async function handleStart(options: { block?: boolean } = {}) {
216
216
  const requestedPort = parsePortOption();
217
217
  const owner = await findProxyOwnerBeforeJournalRecovery();
218
218
  if (owner.live) {
219
+ // Service-wrapper context (opencodex-service.cmd `:loop`): a healthy proxy from
220
+ // ANY source means the requested port is already served. Exit 0 so the wrapper's
221
+ // `if %ERRORLEVEL% NEQ 0` retry loop terminates instead of respawning every 5s
222
+ // against a listener it can never claim (observed as an endless
223
+ // "Proxy already running" service.log loop).
224
+ // Only the exact "1" sentinel takes this path — the same check syncCleanup
225
+ // uses — so an env value like "0" or "false" cannot bypass the conflict error.
226
+ if (process.env.OCX_SERVICE === "1") {
227
+ console.log(`Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}); service wrapper staying out of the way.`);
228
+ process.exit(0);
229
+ }
219
230
  console.error(`⚠️ Proxy already running (PID ${owner.live.pid ?? owner.pidSnapshot ?? "unknown"}, port ${owner.live.port}). Use 'ocx stop' first.`);
220
231
  process.exit(1);
221
232
  }
@@ -350,7 +350,7 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
350
350
  // executable resolves from the trusted System32 directory (never PATH), and
351
351
  // windowsHide keeps the enumeration console-less on desktop sessions (#1278).
352
352
  const output = execFileSync(resolveTrustedWindowsPowerShellExe(), [
353
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
353
+ "-NoProfile", "-NoLogo", "-NonInteractive",
354
354
  "-Command",
355
355
  psCommand,
356
356
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true });
@@ -461,7 +461,7 @@ function readDarwinProcStartMs(pid: number): number | null {
461
461
  function readWindowsProcStartMs(pid: number): number | null {
462
462
  try {
463
463
  const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
464
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
464
+ "-NoProfile", "-NoLogo", "-NonInteractive",
465
465
  "-Command",
466
466
  `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
467
467
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 8_000, windowsHide: true }).trim();
@@ -517,7 +517,7 @@ export function readProcessStartMsBatch(
517
517
  try {
518
518
  const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
519
519
  const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [
520
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
520
+ "-NoProfile", "-NoLogo", "-NonInteractive",
521
521
  "-Command",
522
522
  `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,
523
523
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 5_000, windowsHide: true });
@@ -53,15 +53,22 @@ function refuse(message: string, cause?: unknown): never {
53
53
  }
54
54
 
55
55
  function windowsIdentityPowerShellCommand(expression: string): string[] {
56
+ // PowerShell 5.1 can encode redirected native/host output with the active
57
+ // Windows code page. Base64 contains ASCII only, while the payload is
58
+ // explicitly UTF-16LE, so Korean and Western profile paths arrive unchanged.
59
+ const deterministicOutput = [
60
+ "$ErrorActionPreference = 'Stop'",
61
+ `$ocxValue = [string](${expression})`,
62
+ "$ocxBytes = [System.Text.Encoding]::Unicode.GetBytes($ocxValue)",
63
+ "[Console]::Out.Write([Convert]::ToBase64String($ocxBytes))",
64
+ ].join("; ");
56
65
  return [
57
66
  resolveTrustedWindowsPowerShellExe(),
58
67
  "-NoLogo",
59
68
  "-NoProfile",
60
69
  "-NonInteractive",
61
- "-WindowStyle",
62
- "Hidden",
63
70
  "-Command",
64
- expression,
71
+ deterministicOutput,
65
72
  ];
66
73
  }
67
74
 
@@ -93,6 +100,28 @@ export function windowsIdentityPowerShellSpawnOptionsForTests(): ReturnType<
93
100
  return windowsIdentityPowerShellSpawnOptions();
94
101
  }
95
102
 
103
+ function decodeWindowsIdentityPowerShellOutput(output: Uint8Array): string {
104
+ let encoded: string;
105
+ try {
106
+ encoded = new TextDecoder("utf-8", { fatal: true }).decode(output).trim();
107
+ } catch (cause) {
108
+ refuse("Windows effective-account lookup returned a malformed value.", cause);
109
+ }
110
+ if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded)) {
111
+ refuse("Windows effective-account lookup returned a malformed value.");
112
+ }
113
+ const bytes = Buffer.from(encoded, "base64");
114
+ if (bytes.length % 2 !== 0 || bytes.toString("base64") !== encoded) {
115
+ refuse("Windows effective-account lookup returned a malformed value.");
116
+ }
117
+ return bytes.toString("utf16le").trim();
118
+ }
119
+
120
+ /** Test-only decode seam for the deterministic PowerShell output contract. */
121
+ export function decodeWindowsIdentityPowerShellOutputForTests(output: Uint8Array): string {
122
+ return decodeWindowsIdentityPowerShellOutput(output);
123
+ }
124
+
96
125
  function powershellValue(expression: string): string {
97
126
  let command: string[];
98
127
  try {
@@ -105,15 +134,16 @@ function powershellValue(expression: string): string {
105
134
  // `windowsHide` is the popup fix (#1278): the desktop proxy parent runs
106
135
  // without a console, so a console-subsystem child spawned without
107
136
  // CREATE_NO_WINDOW gets a fresh visible console window at startup and on
108
- // every config write. `-WindowStyle Hidden` alone does not stop the
109
- // allocation; the flag behind `windowsHide` does.
137
+ // every config write. Do not add PowerShell's `-WindowStyle Hidden` here:
138
+ // Bun 1.3.14 can fail that direct CLI combination before the SID command
139
+ // executes (#1589); the process-level `windowsHide` flag is sufficient.
110
140
  result = Bun.spawnSync(command, windowsIdentityPowerShellSpawnOptions());
111
141
  } catch (cause) {
112
142
  refuse("Windows effective-account lookup could not start.", cause);
113
143
  }
114
144
  if (result.exitedDueToTimeout) refuse("Windows effective-account lookup timed out.");
115
145
  if (result.exitCode !== 0) refuse("Windows effective-account lookup failed.");
116
- const value = new TextDecoder().decode(result.stdout).trim();
146
+ const value = decodeWindowsIdentityPowerShellOutput(result.stdout ?? Buffer.alloc(0));
117
147
  if (!value) refuse("Windows effective-account lookup returned an empty value.");
118
148
  return value;
119
149
  }
package/src/config.ts CHANGED
@@ -3578,8 +3578,6 @@ function readProcessCommandLine(pid: number): string | undefined {
3578
3578
  "-NoProfile",
3579
3579
  "-NoLogo",
3580
3580
  "-NonInteractive",
3581
- "-WindowStyle",
3582
- "Hidden",
3583
3581
  "-Command",
3584
3582
  `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
3585
3583
  ], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"], timeout: 3000, windowsHide: true });
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "b8810736c02b3cf34dbd0119082924f56a48e435cd9a0768c68c428f8e128cda"
13
+ "sha256": "c49e32901436d0c5d16ce428689fc389ce9647256fa7a1143afa2d96b046a30d"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  {
48
48
  "path": "src/adapters/client-fingerprint.ts",
49
- "sha256": "6954cdf0885a7b55d179926117239c90e7f938b23ffd8c1b02392d183df4a253"
49
+ "sha256": "db85c96fb739ada2ddb2b811722d497cec0cbe692fa816920da6a97d10c202e4"
50
50
  },
51
51
  {
52
52
  "path": "src/adapters/command-code.ts",
@@ -102,7 +102,7 @@
102
102
  },
103
103
  {
104
104
  "path": "src/adapters/cursor/live-transport.ts",
105
- "sha256": "7cc03eea3bb6b8ebd99dd134546e997d3eaf10e7c430a1bd7a0e3e283607ccef"
105
+ "sha256": "247e48058e2d22e282d2d49652cde998c5d13f36197898562510d99746993711"
106
106
  },
107
107
  {
108
108
  "path": "src/adapters/cursor/mcp-config.ts",
@@ -150,7 +150,7 @@
150
150
  },
151
151
  {
152
152
  "path": "src/adapters/cursor/protobuf-events.ts",
153
- "sha256": "a5732084a5562242b7184321723ecf1a6e0009080d69f82fbea3ac66969d135f"
153
+ "sha256": "16faa16a8f5eeb5022a2f7332bd0739189bf3cd5b6dc1267730b14a78786bc08"
154
154
  },
155
155
  {
156
156
  "path": "src/adapters/cursor/protobuf-request.ts",
@@ -166,7 +166,7 @@
166
166
  },
167
167
  {
168
168
  "path": "src/adapters/cursor/tool-definitions.ts",
169
- "sha256": "be643963a3f36fe2c68bafb3f3a82ba5e9b59114d689f79cf83383c5852ff34f"
169
+ "sha256": "95b440c3456085df1216ed1eb2659025edfc49e6ba121bd8231b46f32fa88870"
170
170
  },
171
171
  {
172
172
  "path": "src/adapters/cursor/transport-retry.ts",
@@ -210,7 +210,7 @@
210
210
  },
211
211
  {
212
212
  "path": "src/adapters/google.ts",
213
- "sha256": "b2e57cd169f7b2a044624d64df557aff05424c06c026ffd055f0e009770325a4"
213
+ "sha256": "5fd50e67930ddac30951c2764b5dc8852252e75690fa4c7068a8f36d85932f82"
214
214
  },
215
215
  {
216
216
  "path": "src/adapters/identity.ts",
@@ -298,7 +298,7 @@
298
298
  },
299
299
  {
300
300
  "path": "src/bridge.ts",
301
- "sha256": "137c6d4b5cc3e7c932fed242b5200469171ae786b4835bec7f667f15a1d79a75"
301
+ "sha256": "47ccd936f3abd32d98ea9f044d35acd353b16ee1c9f0f0542f4b519f63f52221"
302
302
  },
303
303
  {
304
304
  "path": "src/chat/inbound.ts",
@@ -462,7 +462,7 @@
462
462
  },
463
463
  {
464
464
  "path": "src/cli/index.ts",
465
- "sha256": "63e8747afb606295340bd30598d7112b75efb2a89353234a874df42c5fc7e1a7"
465
+ "sha256": "d8c530cd151adc3ecbc243a63d5297c8c18001345aaaa708842d854e854faa6a"
466
466
  },
467
467
  {
468
468
  "path": "src/cli/init.ts",
@@ -606,7 +606,7 @@
606
606
  },
607
607
  {
608
608
  "path": "src/codex/app-server-processes.ts",
609
- "sha256": "66f866e499edc75d7530a1a9230e77e7d9f6553579c64141ba73630f6ea91a7d"
609
+ "sha256": "4c55315be736f3e8fb0e19569d96d1235592d07a10fd243aa16a9d6925227f5f"
610
610
  },
611
611
  {
612
612
  "path": "src/codex/auth-api.ts",
@@ -930,7 +930,7 @@
930
930
  },
931
931
  {
932
932
  "path": "src/codex/user-identity.ts",
933
- "sha256": "f16cc31184e7e00128545f9c6b59664ed17f0a5f08c606cf3731f752493a1f23"
933
+ "sha256": "e2cb675b208f7ad2fdd64c5d05d9c439511f786f3887ea5cb54d87b6fe7fe8a2"
934
934
  },
935
935
  {
936
936
  "path": "src/codex/warmup.ts",
@@ -966,7 +966,7 @@
966
966
  },
967
967
  {
968
968
  "path": "src/config.ts",
969
- "sha256": "d98507303396605a2f698448295572038d6f30f3593af58910d04ecec918e9b8"
969
+ "sha256": "142ca0d8026b63ddceb2b347b4f27dd6912b908ccf351016352e9423445fa2ef"
970
970
  },
971
971
  {
972
972
  "path": "src/generated/model-metadata.ts",
@@ -1666,7 +1666,7 @@
1666
1666
  },
1667
1667
  {
1668
1668
  "path": "src/lib/token-estimate.ts",
1669
- "sha256": "2c3cc4b8193166ca100197ca22202b06ac98c02547ffe32f3e0f8720190b9897"
1669
+ "sha256": "c90473299655c86ef0118f5a265a71c100bf92e52172d6464a16c8f6ea300ca5"
1670
1670
  },
1671
1671
  {
1672
1672
  "path": "src/lib/tool-argument-integers.ts",
@@ -1694,15 +1694,19 @@
1694
1694
  },
1695
1695
  {
1696
1696
  "path": "src/lib/windows-elevation.ts",
1697
- "sha256": "9ef8a94484a6c4a95d3270aa10302f8e747ce54efed926c640f0cbdac13e6c27"
1697
+ "sha256": "eeb579420c42318232ffdf924dba46d3ec172fb7dd8d83930a2985dfbc875d4a"
1698
1698
  },
1699
1699
  {
1700
1700
  "path": "src/lib/windows-secret-acl.ts",
1701
- "sha256": "beefa22c1aef684743785fecd75cad6cd84bf8acdf035a16e64930ac3402da4c"
1701
+ "sha256": "6cbb7d5a6d9e5a1d73058af79ac02611e5cebc01fc49c7daf39ab8b0f9b41f53"
1702
+ },
1703
+ {
1704
+ "path": "src/lib/windows-text.ts",
1705
+ "sha256": "b5b4505de8b0e99df6548e2c6359e57066378b3e1e7ef3fe210e8ecd4aec8367"
1702
1706
  },
1703
1707
  {
1704
1708
  "path": "src/lib/windows-user-principal.ts",
1705
- "sha256": "4830ed6cfeb040fb25603cd0f98079b56c46b8e325dc4bac0771ecd118a51cd4"
1709
+ "sha256": "c8974defc31bc73364c57ef906befd5142e32467655cefbcf8f764db6ef5c896"
1706
1710
  },
1707
1711
  {
1708
1712
  "path": "src/lib/winsw.ts",
@@ -1770,7 +1774,7 @@
1770
1774
  },
1771
1775
  {
1772
1776
  "path": "src/oauth/index.ts",
1773
- "sha256": "cbe370cd31412914b4cd2f565fd519eb45897ebd4ec7284503f68926017e127d"
1777
+ "sha256": "e42ad175a0eaf9a4ab524066ee825e81b4cd16c3671cb2051968f87a56c46cb7"
1774
1778
  },
1775
1779
  {
1776
1780
  "path": "src/oauth/key-providers.ts",
@@ -1810,7 +1814,7 @@
1810
1814
  },
1811
1815
  {
1812
1816
  "path": "src/oauth/store.ts",
1813
- "sha256": "dda6e0d1d63ef43291aba437fc63749808a85ca039bfbf3e98b82b0368718f4a"
1817
+ "sha256": "61c14ad05498f0146aec503f8618a4249d8064967377c6c782437276c9ab0ac5"
1814
1818
  },
1815
1819
  {
1816
1820
  "path": "src/oauth/token-guardian.ts",
@@ -1838,7 +1842,7 @@
1838
1842
  },
1839
1843
  {
1840
1844
  "path": "src/providers/antigravity-models.ts",
1841
- "sha256": "cbe65f34a127ad44fd8f6bd2214553b238449b4f10d948d5ed67f4b163964874"
1845
+ "sha256": "005e17707703842d91d9b18aa6f28375bda6b49efff07cb7642c597ee0bcdc8b"
1842
1846
  },
1843
1847
  {
1844
1848
  "path": "src/providers/api-keys.ts",
@@ -1866,7 +1870,7 @@
1866
1870
  },
1867
1871
  {
1868
1872
  "path": "src/providers/free-directory.ts",
1869
- "sha256": "cdcdb72b65bf2655bf9a8dd0a9b77d76ac35ebc8082c1065816a4fc9d785d2c4"
1873
+ "sha256": "e984f4a79b202e702f4e73386c479fbe96293e6e99ac97d91ddd410e15999087"
1870
1874
  },
1871
1875
  {
1872
1876
  "path": "src/providers/github-copilot-transport.ts",
@@ -1938,7 +1942,7 @@
1938
1942
  },
1939
1943
  {
1940
1944
  "path": "src/providers/registry.ts",
1941
- "sha256": "7d6a712c9ae027d409c2d367ba77c13ceba46ecaab8199a68c3984c9ea5c4c04"
1945
+ "sha256": "78f6cda9b3a114e72656e88645b062e96af413f9af7e0241246f37473d086b7f"
1942
1946
  },
1943
1947
  {
1944
1948
  "path": "src/providers/slug-codec.ts",
@@ -2138,7 +2142,7 @@
2138
2142
  },
2139
2143
  {
2140
2144
  "path": "src/server/index.ts",
2141
- "sha256": "cc6461e629f1f5e373d81ebee0720a9ba85c7deedded457bbd398ac962e0e404"
2145
+ "sha256": "e909c5777cac73894273ab6ce9a0e635dc55a74de726cc6a6f71c4409a695ba2"
2142
2146
  },
2143
2147
  {
2144
2148
  "path": "src/server/lifecycle.ts",
@@ -2206,7 +2210,7 @@
2206
2210
  },
2207
2211
  {
2208
2212
  "path": "src/server/management/logs-usage-routes.ts",
2209
- "sha256": "645c8902e0d3b589a5b20a7d607fb9b40197f34a4c73945a4bf9fd91336bf15e"
2213
+ "sha256": "0f4b36868f53fe7314ba5393f1270045eb8d260058620ab34f4aa343ce0b4b80"
2210
2214
  },
2211
2215
  {
2212
2216
  "path": "src/server/management/model-routes.ts",
@@ -2310,7 +2314,7 @@
2310
2314
  },
2311
2315
  {
2312
2316
  "path": "src/server/request-log.ts",
2313
- "sha256": "036af24421d796ba82cd52c8ed97dbead949a471c9a212fe74f1aed2b87edf8a"
2317
+ "sha256": "e7111d92152cdedf0e771d719bddee0150124d14bcac13610662dcbed3b75869"
2314
2318
  },
2315
2319
  {
2316
2320
  "path": "src/server/responses-custom-tool-repair.ts",
@@ -2362,15 +2366,15 @@
2362
2366
  },
2363
2367
  {
2364
2368
  "path": "src/server/responses/core.ts",
2365
- "sha256": "b8e42c1e0be113256d7318ee6dc3dc4cb7d045867e9e5412ccdfd64fa07111cb"
2369
+ "sha256": "1dde5ae551f6baadb2dd0cca790d43d6ce1ae223568ebbc8d9a0d68c41cf7ef6"
2366
2370
  },
2367
2371
  {
2368
2372
  "path": "src/server/responses/encrypted-payload.ts",
2369
- "sha256": "d0f83a3a8f5bdce327fe32e744f8bfbc8695e7cb2981a1dffc67daf8aa302d93"
2373
+ "sha256": "0e48bc0b453fc6b037d334d06a6454af46d8214f29b286a197d0bfdee138e215"
2370
2374
  },
2371
2375
  {
2372
2376
  "path": "src/server/responses/fetch-helpers.ts",
2373
- "sha256": "c0dd5255a485b23fcef54378cd1ea2eac40f7824952050f8e9db9c3d02f0e2d2"
2377
+ "sha256": "1043b5b66d5d8c43bd3600200d86cbd6bb5af5614bc4fa17bebc2134b854ca99"
2374
2378
  },
2375
2379
  {
2376
2380
  "path": "src/server/responses/passthrough-error.ts",
@@ -2378,7 +2382,7 @@
2378
2382
  },
2379
2383
  {
2380
2384
  "path": "src/server/responses/policy-fallback.ts",
2381
- "sha256": "64592e4c703941f9d860d0f575a7944f16dbca487b08fd8d2536e0b92f188fca"
2385
+ "sha256": "7f824ac3b25fa7b9716e81daec7fbfab75bd1c08098ab8195b22404922dae856"
2382
2386
  },
2383
2387
  {
2384
2388
  "path": "src/server/responses/terminal-guard.ts",
@@ -2390,7 +2394,7 @@
2390
2394
  },
2391
2395
  {
2392
2396
  "path": "src/server/responses/ws-upstream.ts",
2393
- "sha256": "f697ac29a3b1965c4d77531abd69ed382d938ed503c951834205cffe6d8e9757"
2397
+ "sha256": "9d76c39ae90cb8a2256a851fddd25c8d508816689a2768d9d578ff1ee908ae61"
2394
2398
  },
2395
2399
  {
2396
2400
  "path": "src/server/search.ts",
@@ -2430,11 +2434,11 @@
2430
2434
  },
2431
2435
  {
2432
2436
  "path": "src/service-manager-probe.ts",
2433
- "sha256": "ec12b537eccc4310ad07ea504da84e99331ded66e233bb0e54945f8b781bf932"
2437
+ "sha256": "6a9efca4e86bb940654475254c6600f377d1fa96a430f70c47fa0781145fca6f"
2434
2438
  },
2435
2439
  {
2436
2440
  "path": "src/service.ts",
2437
- "sha256": "1438465d669c54759319ed6a689517967ed8fc66463870dda547ac123d43c0df"
2441
+ "sha256": "da173595e82233c5b20b78026255027004f0eea388fc66a7bf073229dd69e45e"
2438
2442
  },
2439
2443
  {
2440
2444
  "path": "src/stall-timeout.ts",
@@ -2506,7 +2510,7 @@
2506
2510
  },
2507
2511
  {
2508
2512
  "path": "src/tray/windows.ts",
2509
- "sha256": "3ca49da1758a7d82be178f4370bed3f1a62bfd052d83b6f79575c19b1d69c1bd"
2513
+ "sha256": "d571ce0ea715feeb5aebd1f623a06b604f7b1e7f3445fca6ea755bd871415746"
2510
2514
  },
2511
2515
  {
2512
2516
  "path": "src/types.ts",
@@ -2522,7 +2526,7 @@
2522
2526
  },
2523
2527
  {
2524
2528
  "path": "src/update/job.ts",
2525
- "sha256": "af0aeb7c8c3f24fd93306d0def7695482b1a0792268c2e67dcb76b586e4ffa85"
2529
+ "sha256": "60bb9f289105ff55fbf96d15de28e3305ad881f1fcd1f0b2a216eff50e7ff498"
2526
2530
  },
2527
2531
  {
2528
2532
  "path": "src/update/notify.ts",
@@ -2570,7 +2574,7 @@
2570
2574
  },
2571
2575
  {
2572
2576
  "path": "src/usage/summary.ts",
2573
- "sha256": "393dd15c9ef4aa7e597bdb0005658595bb567271b559414f203d2a4f81223842"
2577
+ "sha256": "7c73078f67bb6b14f8448b794cd1e622f47517f5686efe1cd3553580afe98122"
2574
2578
  },
2575
2579
  {
2576
2580
  "path": "src/usage/totals.ts",
@@ -55,15 +55,32 @@ function cjkRatio(text: string): number {
55
55
  return sampled === 0 ? 0 : cjk / sampled;
56
56
  }
57
57
 
58
+ /**
59
+ * Cap an estimated token count at a model's context window (codex-router PR #140).
60
+ *
61
+ * A request the provider answered cannot have exceeded the window, so the estimate is never
62
+ * allowed to claim it did: the estimate only ever substitutes a reported-zero (or missing)
63
+ * input count, and a value above the window would be provably false. Positive or missing
64
+ * reported counts are never rewritten here — this bounds the heuristic estimate itself.
65
+ * Non-positive or non-integer windows (unknown model) leave the estimate untouched.
66
+ */
67
+ export function capEstimateAtContextWindow(estimate: number, contextWindow: number | undefined): number {
68
+ return typeof contextWindow === "number" && Number.isInteger(contextWindow) && contextWindow > 0
69
+ ? Math.min(estimate, contextWindow)
70
+ : estimate;
71
+ }
72
+
58
73
  /**
59
74
  * Estimate the token count of a text blob. Pure and deterministic.
60
75
  * Returns 0 for empty/whitespace-free-empty input; otherwise ceil(length / ratio), min 1.
76
+ * When `contextWindow` is a positive integer the estimate is capped at it: a request the
77
+ * provider answered cannot have exceeded the window, so the estimate must not claim it did.
61
78
  */
62
- export function estimateTokens(text: string, modelId?: string): number {
79
+ export function estimateTokens(text: string, modelId?: string, contextWindow?: number): number {
63
80
  if (!text) return 0;
64
81
  const len = text.length;
65
82
  if (len === 0) return 0;
66
83
  let ratio = charsPerToken(modelId);
67
84
  if (cjkRatio(text) > CJK_RATIO_THRESHOLD) ratio = Math.min(ratio, CJK_CHARS_PER_TOKEN);
68
- return Math.max(1, Math.ceil(len / ratio));
85
+ return capEstimateAtContextWindow(Math.max(1, Math.ceil(len / ratio)), contextWindow);
69
86
  }
@@ -612,6 +612,43 @@ export function runWindowsElevated(file: string, args: string[]): Promise<number
612
612
  return startPowerShellCommand(script).completion.then(result => result.exitCode);
613
613
  }
614
614
 
615
+ /**
616
+ * Register one scheduled-task definition without exposing a mutable XML pathname to
617
+ * the elevated process. The XML bytes are fixed in the encoded PowerShell command
618
+ * before UAC; Register-ScheduledTask receives that string directly after elevation.
619
+ */
620
+ export function runWindowsElevatedScheduledTaskRegistration(
621
+ taskName: string,
622
+ xml: string,
623
+ ): Promise<number> {
624
+ const xmlBase64 = Buffer.from(xml, "utf16le").toString("base64");
625
+ const inner = [
626
+ `$taskName = ${psSingleQuote(taskName)}`,
627
+ `$xmlBase64 = ${psSingleQuote(xmlBase64)}`,
628
+ "$xml = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($xmlBase64))",
629
+ "Register-ScheduledTask -TaskName $taskName -Xml $xml -Force -ErrorAction Stop | Out-Null",
630
+ ].join("; ");
631
+ const encodedCommand = Buffer.from(inner, "utf16le").toString("base64");
632
+ const script = [
633
+ `$p = Start-Process -FilePath ${psSingleQuote(windowsPowerShell())}`,
634
+ ` -ArgumentList ${psSingleQuote(buildWindowsElevatedArgumentList([
635
+ "-NoProfile",
636
+ "-NonInteractive",
637
+ "-ExecutionPolicy",
638
+ "Bypass",
639
+ "-EncodedCommand",
640
+ encodedCommand,
641
+ ]))}`,
642
+ " -Verb RunAs -WindowStyle Hidden -PassThru -Wait",
643
+ `if ($null -eq $p) { exit ${OCX_ELEVATED_UAC_CANCELLED} }`,
644
+ "$null = $p.Handle",
645
+ `if ($null -eq $p.ExitCode) { exit ${OCX_ELEVATED_PROTOCOL_FAILED} }`,
646
+ "exit $p.ExitCode",
647
+ ].join("; ");
648
+
649
+ return startPowerShellCommand(script).completion.then(result => result.exitCode);
650
+ }
651
+
615
652
  /**
616
653
  * Build the elevated (post-UAC) script: create → run → optional delete rollback.
617
654
  * Returns only OpenCodex protocol exit codes (never raw schtasks codes, never 1223).
@@ -388,6 +388,13 @@ export function forgetEphemeralSecretPath(tempPath: string): void {
388
388
  timedOutPaths.delete(`optional:${tempPath}`);
389
389
  }
390
390
 
391
+ /** Directory counterpart for a proven-absent ephemeral staging root. */
392
+ export function forgetEphemeralSecretDir(tempPath: string): void {
393
+ hardenedDirectories.delete(tempPath);
394
+ timedOutPaths.delete(`required:${tempPath}`);
395
+ timedOutPaths.delete(`optional:${tempPath}`);
396
+ }
397
+
391
398
  /** Test seam: timeout memo sets return to baseline after ephemeral cleanup. */
392
399
  export function timedOutSecretPathCountForTests(): number {
393
400
  return timedOutPaths.size;