@bitkyc08/opencodex 2.17.1-preview.20260814 → 2.19.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 (50) hide show
  1. package/gui/dist/assets/{index-DUCH59lJ.css → index-CQ7bIKee.css} +1 -1
  2. package/gui/dist/assets/{index-ta3-_hgj.js → index-D_JUZLEC.js} +16 -16
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/client-fingerprint.ts +14 -10
  6. package/src/adapters/cursor/live-transport.ts +17 -5
  7. package/src/adapters/cursor/protobuf-events.ts +662 -20
  8. package/src/adapters/cursor/tool-definitions.ts +12 -6
  9. package/src/adapters/google-antigravity-wire.ts +4 -3
  10. package/src/adapters/google.ts +22 -3
  11. package/src/bridge.ts +12 -2
  12. package/src/chat/inbound.ts +24 -1
  13. package/src/cli/index.ts +11 -0
  14. package/src/codex/app-server-processes.ts +3 -3
  15. package/src/codex/shim.ts +100 -5
  16. package/src/codex/user-identity.ts +36 -6
  17. package/src/config.ts +0 -2
  18. package/src/generated/compatibility-version.json +52 -44
  19. package/src/lib/errors.ts +27 -0
  20. package/src/lib/token-estimate.ts +19 -2
  21. package/src/lib/windows-elevation.ts +37 -0
  22. package/src/lib/windows-secret-acl.ts +7 -0
  23. package/src/lib/windows-text.ts +106 -0
  24. package/src/lib/windows-user-principal.ts +0 -2
  25. package/src/oauth/index.ts +1 -1
  26. package/src/oauth/store.ts +32 -18
  27. package/src/providers/antigravity-models.ts +25 -5
  28. package/src/providers/free-directory.ts +1 -1
  29. package/src/providers/registry.ts +6 -3
  30. package/src/responses/spill-store.ts +20 -1
  31. package/src/responses/state.ts +159 -3
  32. package/src/server/chat-completions.ts +4 -2
  33. package/src/server/effort-policy.ts +18 -0
  34. package/src/server/index.ts +5 -1
  35. package/src/server/management/logs-usage-routes.ts +7 -22
  36. package/src/server/request-log.ts +48 -3
  37. package/src/server/responses/core.ts +59 -15
  38. package/src/server/responses/encrypted-payload.ts +58 -38
  39. package/src/server/responses/fetch-helpers.ts +12 -4
  40. package/src/server/responses/input-admission.ts +169 -0
  41. package/src/server/responses/policy-fallback.ts +13 -2
  42. package/src/server/responses/ws-upstream.ts +115 -6
  43. package/src/service-manager-probe.ts +23 -37
  44. package/src/service.ts +233 -25
  45. package/src/tray/windows.ts +0 -2
  46. package/src/types.ts +10 -2
  47. package/src/update/job.ts +2 -2
  48. package/src/usage/summary.ts +21 -4
  49. package/src/vision/index.ts +21 -4
  50. package/src/web-search/index.ts +2 -1
package/src/service.ts CHANGED
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import { execFileSync, execSync, spawnSync } from "node:child_process";
9
9
  import { findLiveProxy, proxyIdentityAt, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
10
- import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
- import { homedir } from "node:os";
10
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { homedir, tmpdir } from "node:os";
12
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";
@@ -24,9 +24,11 @@ import {
24
24
  ELEVATION_REQUEST_TIMEOUT_MS,
25
25
  OCX_ELEVATED_PROTOCOL_FAILED,
26
26
  raceWithTimeout,
27
+ resolveTrustedWindowsPowerShellExe,
27
28
  resolveTrustedWindowsSchtasksExe,
28
29
  startElevatedSchtasksCreateAndRun,
29
30
  runWindowsElevated,
31
+ runWindowsElevatedScheduledTaskRegistration,
30
32
  toWindowsSchtasksError,
31
33
  WindowsElevationError,
32
34
  WindowsSchtasksError,
@@ -35,7 +37,12 @@ import {
35
37
  type ElevatedSchtasksCreateAndRunResult,
36
38
  } from "./lib/windows-elevation";
37
39
  import { defaultWinswEntry, installWinswService, startWinswService, stopWinswService, statusWinswRaw, uninstallWinswService, winswStatusSummary, winswXmlPath, WINSW_SERVICE_ID, WINSW_SHA256, WINSW_VERSION, type WinswStatus } from "./lib/winsw";
38
- import { hardenSecretDir, hardenSecretPath } from "./lib/windows-secret-acl";
40
+ import {
41
+ forgetEphemeralSecretDir,
42
+ forgetEphemeralSecretPath,
43
+ hardenSecretDir,
44
+ hardenSecretPath,
45
+ } from "./lib/windows-secret-acl";
39
46
  import { windowsEnvIndirectBatchPathList, windowsEnvIndirectBatchValue } from "./lib/win-paths";
40
47
  import { recordOwnedConfigPath } from "./lib/config-ownership";
41
48
  import { maybeShowStarPrompt } from "./cli/star-prompt";
@@ -1896,22 +1903,115 @@ function writeWindowsSchedulerAssets(): void {
1896
1903
  writeServiceAssetWithRetry(windowsTaskXmlPath(), `\uFEFF${buildWindowsTaskXml(script)}`, "utf16le");
1897
1904
  }
1898
1905
 
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",
1906
+ const WINDOWS_SCHEDULER_STAGE_PREFIX = "opencodex-service-stage-";
1907
+ const ownedWindowsSchedulerStages = new Set<string>();
1908
+
1909
+ export interface WindowsSchedulerRegistrationStageDeps {
1910
+ createStageDir?: () => string;
1911
+ hardenDir?: (path: string) => void;
1912
+ writeXml?: (path: string, contents: string) => void;
1913
+ hardenPath?: (path: string) => void;
1914
+ removeStageDir?: (path: string) => void;
1915
+ }
1916
+
1917
+ function cleanupWindowsSchedulerStage(
1918
+ stageDir: string,
1919
+ xmlPath: string,
1920
+ removeStageDir: (path: string) => void,
1921
+ ): void {
1922
+ let cleanupError: unknown;
1923
+ try {
1924
+ unlinkSync(xmlPath);
1925
+ forgetEphemeralSecretPath(xmlPath);
1926
+ } catch (error) {
1927
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
1928
+ forgetEphemeralSecretPath(xmlPath);
1929
+ } else {
1930
+ cleanupError = error;
1931
+ }
1932
+ }
1933
+ try {
1934
+ removeStageDir(stageDir);
1935
+ forgetEphemeralSecretDir(stageDir);
1936
+ } catch (error) {
1937
+ if ((error as NodeJS.ErrnoException | undefined)?.code === "ENOENT") {
1938
+ forgetEphemeralSecretDir(stageDir);
1939
+ } else if (cleanupError) {
1940
+ throw new AggregateError([cleanupError, error], "Task Scheduler staging cleanup failed.");
1941
+ } else {
1942
+ cleanupError = error;
1943
+ }
1944
+ }
1945
+ if (cleanupError) throw cleanupError;
1946
+ }
1947
+
1948
+ export function stageWindowsSchedulerRegistrationXml(
1949
+ attemptNonce: string,
1950
+ deps: WindowsSchedulerRegistrationStageDeps = {},
1951
+ ): string {
1952
+ const createStageDir = deps.createStageDir
1953
+ ?? (() => mkdtempSync(join(tmpdir(), WINDOWS_SCHEDULER_STAGE_PREFIX)));
1954
+ const hardenDir = deps.hardenDir
1955
+ ?? ((path: string) => { hardenSecretDir(path, { required: true }); });
1956
+ const writeXml = deps.writeXml ?? ((path: string, contents: string) => {
1957
+ writeFileSync(path, contents, { encoding: "utf16le", flag: "wx", mode: 0o600 });
1958
+ });
1959
+ const hardenPath = deps.hardenPath
1960
+ ?? ((path: string) => { hardenSecretPath(path, { required: true }); });
1961
+ const removeStageDir = deps.removeStageDir
1962
+ ?? ((path: string) => { rmdirSync(path); });
1963
+
1964
+ let stageDir: string | null = null;
1965
+ let xmlPath: string | null = null;
1966
+ try {
1967
+ stageDir = createStageDir();
1968
+ try { chmodSync(stageDir, 0o700); } catch { /* required Windows ACL is authoritative */ }
1969
+ hardenDir(stageDir);
1970
+ xmlPath = join(stageDir, "task.xml");
1971
+ // This document points at the canonical launcher but does not publish or rewrite it.
1972
+ // The hardened private directory prevents another local account from replacing the
1973
+ // document while UAC is pending; the file harden independently proves its identity.
1974
+ writeXml(
1975
+ xmlPath,
1976
+ `\uFEFF${buildWindowsTaskXml(windowsServiceScriptPath(), windowsLauncherVbsPath(), attemptNonce)}`,
1977
+ );
1978
+ hardenPath(xmlPath);
1979
+ ownedWindowsSchedulerStages.add(xmlPath);
1980
+ return xmlPath;
1981
+ } catch (error) {
1982
+ if (stageDir) {
1983
+ try {
1984
+ cleanupWindowsSchedulerStage(stageDir, xmlPath ?? join(stageDir, "task.xml"), removeStageDir);
1985
+ } catch (cleanupError) {
1986
+ throw new AggregateError(
1987
+ [error, cleanupError],
1988
+ "Task Scheduler staging failed and its private temporary directory could not be removed.",
1989
+ );
1990
+ }
1991
+ }
1992
+ throw error;
1993
+ }
1994
+ }
1995
+
1996
+ function removeWindowsSchedulerRegistrationStage(xmlPath: string): void {
1997
+ if (!ownedWindowsSchedulerStages.has(xmlPath)) {
1998
+ throw new Error("Refusing to remove an unrecognized Task Scheduler staging path.");
1999
+ }
2000
+ const stageDir = dirname(xmlPath);
2001
+ cleanupWindowsSchedulerStage(
2002
+ stageDir,
2003
+ xmlPath,
2004
+ path => { rmdirSync(path); },
1908
2005
  );
1909
- return path;
2006
+ if (existsSync(stageDir)) {
2007
+ throw new Error("The private Task Scheduler staging directory still exists after cleanup.");
2008
+ }
2009
+ ownedWindowsSchedulerStages.delete(xmlPath);
1910
2010
  }
1911
2011
 
1912
2012
  export interface FreshWindowsSchedulerRegistrationDeps {
1913
2013
  create?: (args: string[]) => void;
1914
- elevate?: (args: string[]) => Promise<void>;
2014
+ elevate?: (taskName: string, xml: string) => Promise<void>;
1915
2015
  probe?: () => WindowsSchedulerTaskProbe;
1916
2016
  queryXml?: () => string;
1917
2017
  rollback?: () => Promise<string | null>;
@@ -1923,6 +2023,16 @@ export async function registerFreshWindowsSchedulerTask(
1923
2023
  deps: FreshWindowsSchedulerRegistrationDeps = {},
1924
2024
  ): Promise<void> {
1925
2025
  const args = buildWindowsSchtasksCreateArgsForXml(xmlPath);
2026
+ // Capture and validate the exact definition before an access-denied attempt can
2027
+ // cross the UAC boundary. The elevated fallback receives these immutable bytes,
2028
+ // never the caller-writable staging pathname.
2029
+ const expectedXml = decodeSchtasksOutput(readFileSync(xmlPath));
2030
+ if (
2031
+ !windowsTaskRegistrationHealthy(expectedXml)
2032
+ || !windowsTaskRegistrationOwnedByAttempt(expectedXml, attemptNonce)
2033
+ ) {
2034
+ throw new Error("The staged Task Scheduler registration failed OpenCodex ownership or shape validation.");
2035
+ }
1926
2036
  try {
1927
2037
  (deps.create ?? schtasks)(args);
1928
2038
  } catch (error) {
@@ -1933,9 +2043,13 @@ export async function registerFreshWindowsSchedulerTask(
1933
2043
  ) {
1934
2044
  throw error;
1935
2045
  }
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);
2046
+ // Register from the captured XML string inside the elevated process. Another
2047
+ // same-user process can mutate its own temp files, but cannot change this command.
2048
+ const elevate = deps.elevate ?? (async (taskName: string, xml: string) => {
2049
+ const exitCode = await runWindowsElevatedScheduledTaskRegistration(taskName, xml);
2050
+ if (exitCode !== 0) throw new Error(`Background service install failed with exit code ${exitCode}.`);
2051
+ });
2052
+ await elevate(TASK, expectedXml);
1939
2053
  }
1940
2054
 
1941
2055
  const rollbackTask = deps.rollback ?? (() => rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK));
@@ -1978,21 +2092,47 @@ export async function registerFreshWindowsSchedulerTask(
1978
2092
  }
1979
2093
  }
1980
2094
 
1981
- function installWindows(): void {
1982
- recordOwnedConfigPath(getConfigDir(), serviceStatePath());
2095
+ function recordWindowsSchedulerOwnership(): boolean {
2096
+ // Ownership claiming is deliberately conservative: a legacy non-empty config root
2097
+ // without metadata stays unclaimed, but that must not turn a service reinstall into
2098
+ // an outage after prepareServiceInstall has stopped the previous manager.
2099
+ return recordOwnedConfigPath(getConfigDir(), serviceStatePath());
2100
+ }
2101
+
2102
+ export interface RemoveNativeWindowsServiceDeps {
2103
+ status?: () => WinswStatus;
2104
+ uninstall?: () => void;
2105
+ sleep?: (ms: number) => void;
2106
+ settleChecks?: number;
2107
+ }
2108
+
2109
+ export function removeNativeWindowsServiceForScheduler(
2110
+ deps: RemoveNativeWindowsServiceDeps = {},
2111
+ ): void {
2112
+ const status = deps.status ?? statusWinswRaw;
2113
+ const uninstall = deps.uninstall ?? uninstallWinswService;
2114
+ const sleep = deps.sleep ?? Bun.sleepSync;
2115
+ const settleChecks = Math.max(1, deps.settleChecks ?? 20);
1983
2116
  // Transactional backend switch: installing the scheduler backend removes a native
1984
2117
  // service first — two live managers would both respawn the proxy (conflict).
1985
- if (statusWinswRaw() !== "nonexistent") {
2118
+ if (status() !== "nonexistent") {
1986
2119
  console.log("🔁 Removing the native (WinSW) service before installing the Task Scheduler backend...");
1987
2120
  try {
1988
- uninstallWinswService();
2121
+ uninstall();
1989
2122
  } catch (err) {
1990
2123
  throw new Error(`Cannot remove the native service before switching to Task Scheduler: ${err instanceof Error ? err.message : String(err)}. Remove it manually with 'sc delete ${WINSW_SERVICE_ID}' or retry.`);
1991
2124
  }
1992
- if (statusWinswRaw() !== "nonexistent") {
1993
- throw new Error(`Native service registration could not be re-verified after the removal attempt — aborting switch. Check 'sc.exe query ${WINSW_SERVICE_ID}' and remove it manually if present.`);
2125
+ for (let check = 0; check < settleChecks; check++) {
2126
+ if (status() === "nonexistent") return;
2127
+ if (check + 1 < settleChecks) sleep(250);
1994
2128
  }
2129
+ throw new Error(`Native service registration could not be re-verified after the removal attempt — aborting switch. Check 'sc.exe query ${WINSW_SERVICE_ID}' and remove it manually if present.`);
1995
2130
  }
2131
+ }
2132
+
2133
+ function installWindows(): void {
2134
+ recordWindowsSchedulerOwnership();
2135
+ removeNativeWindowsServiceForScheduler();
1996
2136
  // End a running task BEFORE rewriting the assets it is executing — cmd.exe reading the
1997
2137
  // script mid-rewrite runs a torn batch file, and its open handle can fail the write.
1998
2138
  try { stopWindows(); } catch { /* not running */ }
@@ -2164,6 +2304,50 @@ export function stopWindows(): void {
2164
2304
  }
2165
2305
  function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
2166
2306
  function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
2307
+
2308
+ /**
2309
+ * Best-effort termination of surviving Windows scheduler launcher/wrapper processes.
2310
+ * `schtasks /end` ends the task instance but often leaves wscript/cmd running the
2311
+ * `:loop` batch, which brings the proxy back during a stop or restart. Same killer
2312
+ * the update job uses, so both teardown paths share the guarantee.
2313
+ *
2314
+ * Matching is scoped to the CANONICAL paths of THIS installation (opencodex-service.cmd
2315
+ * and opencodex-service-launcher.vbs under the current config dir), never a bare
2316
+ * filename: a wrapper from another OpenCodex home — or an unrelated process whose
2317
+ * command line merely contains the filename — must not be force-terminated.
2318
+ * The path must appear as a COMPLETE command-line token (wscript.exe spawns the
2319
+ * .vbs as an argument; cmd.exe /c runs the .cmd), so a substring-only match is
2320
+ * excluded.
2321
+ */
2322
+ function killWindowsServiceWrapperProcesses(): void {
2323
+ if (process.platform !== "win32") return;
2324
+ try {
2325
+ const script = windowsServiceScriptPath();
2326
+ const launcher = windowsLauncherVbsPath();
2327
+ // Quote for PowerShell: single-quote the value and double any embedded quote.
2328
+ const quote = (value: string) => `'${value.replace(/'/g, "''")}'`;
2329
+ const ps = [
2330
+ `$pats = @(${quote(script)}, ${quote(launcher)});`,
2331
+ "Get-CimInstance Win32_Process | Where-Object {",
2332
+ " if ($_.ProcessId -eq $PID) { return $false };",
2333
+ " $c = $_.CommandLine; if (-not $c) { return $false };",
2334
+ " foreach ($p in $pats) {",
2335
+ " $i = $c.IndexOf($p, [System.StringComparison]::OrdinalIgnoreCase);",
2336
+ " if ($i -lt 0) { continue };",
2337
+ " $before = if ($i -gt 0) { $c.Substring($i - 1, 1) } else { ' ' };",
2338
+ " $end = $i + $p.Length;",
2339
+ " $after = if ($end -lt $c.Length) { $c.Substring($end, 1) } else { ' ' };",
2340
+ " if ($before -match '[\\s\"'']' -and $after -match '[\\s\"'']') { return $true };",
2341
+ " };",
2342
+ " $false",
2343
+ "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
2344
+ ].join(" ");
2345
+ spawnSync(resolveTrustedWindowsPowerShellExe(), [
2346
+ "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
2347
+ "-Command", ps,
2348
+ ], { stdio: "ignore", timeout: 5000, windowsHide: true });
2349
+ } catch { /* best-effort */ }
2350
+ }
2167
2351
  function uninstallWindows(): void {
2168
2352
  const probe = probeWindowsSchedulerTask(TASK);
2169
2353
  if (probe.status === "present") {
@@ -2595,7 +2779,9 @@ export async function installServiceSafely(
2595
2779
  export interface FreshWindowsSchedulerInstallDeps {
2596
2780
  stageRegistrationXml?: (attemptNonce: string) => string;
2597
2781
  register?: (xmlPath: string, attemptNonce: string) => Promise<void>;
2782
+ recordOwnership?: () => boolean;
2598
2783
  prepare?: () => Promise<void>;
2784
+ removeNativeService?: () => void;
2599
2785
  publishAssets?: () => void;
2600
2786
  runTask?: () => void;
2601
2787
  writeState?: () => void;
@@ -2616,7 +2802,9 @@ export async function installFreshWindowsSchedulerSafely(
2616
2802
  ): Promise<void> {
2617
2803
  const stage = deps.stageRegistrationXml ?? stageWindowsSchedulerRegistrationXml;
2618
2804
  const register = deps.register ?? registerFreshWindowsSchedulerTask;
2805
+ const recordOwnership = deps.recordOwnership ?? recordWindowsSchedulerOwnership;
2619
2806
  const prepare = deps.prepare ?? (() => prepareServiceInstall("scheduler"));
2807
+ const removeNativeService = deps.removeNativeService ?? removeNativeWindowsServiceForScheduler;
2620
2808
  const publishAssets = deps.publishAssets ?? writeWindowsSchedulerAssets;
2621
2809
  const runTask = deps.runTask ?? startWindows;
2622
2810
  const writeState = deps.writeState ?? (() => writeServiceInstallState("scheduler"));
@@ -2624,11 +2812,12 @@ export async function installFreshWindowsSchedulerSafely(
2624
2812
  rollbackWindowsSchedulerTaskOwnedByAttempt(attemptNonce, TASK)
2625
2813
  ));
2626
2814
  const removeStagedXml = deps.removeStagedXml ?? ((path: string) => {
2627
- if (existsSync(path)) unlinkSync(path);
2815
+ removeWindowsSchedulerRegistrationStage(path);
2628
2816
  });
2629
2817
 
2630
2818
  let stagedXml: string | null = null;
2631
2819
  const attemptNonce = randomUUID();
2820
+ const configRootWasAbsent = !existsSync(getConfigDir());
2632
2821
  let registered = false;
2633
2822
  let started = false;
2634
2823
  try {
@@ -2637,7 +2826,19 @@ export async function installFreshWindowsSchedulerSafely(
2637
2826
  registered = true;
2638
2827
 
2639
2828
  // The destructive boundary begins only after Task Scheduler accepted the definition.
2829
+ // The registration has consumed its temporary XML. Remove it before claiming a newly
2830
+ // created config root, because ownership initialization intentionally requires emptiness.
2831
+ removeStagedXml(stagedXml);
2832
+ stagedXml = null;
2833
+ const ownershipRecorded = recordOwnership();
2834
+ if (!ownershipRecorded && configRootWasAbsent) {
2835
+ throw new Error(
2836
+ "The fresh OpenCodex config root could not be claimed for safe uninstall; "
2837
+ + "aborting before service-manager cleanup or asset publication.",
2838
+ );
2839
+ }
2640
2840
  await prepare();
2841
+ removeNativeService();
2641
2842
  publishAssets();
2642
2843
  runTask();
2643
2844
  started = true;
@@ -2663,8 +2864,11 @@ export async function installFreshWindowsSchedulerSafely(
2663
2864
  } finally {
2664
2865
  if (stagedXml) {
2665
2866
  try { removeStagedXml(stagedXml); } catch (error) {
2867
+ const code = error && typeof error === "object" && "code" in error
2868
+ ? String((error as NodeJS.ErrnoException).code)
2869
+ : "";
2666
2870
  console.error(
2667
- `⚠️ Failed to remove temporary Task Scheduler XML ${stagedXml}: ${error instanceof Error ? error.message : String(error)}`,
2871
+ `⚠️ Failed to remove the private Task Scheduler staging directory${code ? ` (${code})` : ""}.`,
2668
2872
  );
2669
2873
  }
2670
2874
  }
@@ -2692,6 +2896,10 @@ export function stopServiceIfInstalled(): boolean {
2692
2896
  if (statusWinswRaw() !== "nonexistent") {
2693
2897
  try { stopWinswService(); stopped = true; } catch { /* best-effort */ }
2694
2898
  }
2899
+ // `schtasks /end` ends the task instance but the cmd `:loop` wrapper survives and
2900
+ // respawns its child seconds later (issue #764), resurrecting the proxy during a
2901
+ // stop or a tray restart. Kill the launcher/wrapper processes outright.
2902
+ killWindowsServiceWrapperProcesses();
2695
2903
  if (stopped) return true;
2696
2904
  } else if (process.platform === "linux" && isSystemd() && existsSync(unitPath())) {
2697
2905
  try { stopSystemd(); return true; } catch { return false; }
@@ -138,7 +138,6 @@ export function windowsTrayProcessArgs(entry: WindowsTrayEntry, mode: "Run" | "S
138
138
  "-NonInteractive",
139
139
  "-STA",
140
140
  "-ExecutionPolicy", "Bypass",
141
- "-WindowStyle", "Hidden",
142
141
  "-File", safePath(entry.script),
143
142
  "-BunPath", safePath(entry.bun),
144
143
  "-BunRuntimeSource", entry.bunRuntimeSource,
@@ -173,7 +172,6 @@ export function buildWindowsTrayPowerShellCommand(entry: WindowsTrayEntry, power
173
172
  "-NonInteractive",
174
173
  "-STA",
175
174
  "-ExecutionPolicy", "Bypass",
176
- "-WindowStyle", "Hidden",
177
175
  "-File", quoteRunValue(entry.script),
178
176
  "-BunPath", quoteRunValue(entry.bun),
179
177
  "-BunRuntimeSource", entry.bunRuntimeSource,
package/src/types.ts CHANGED
@@ -32,11 +32,19 @@ export interface OcxParsedRequest {
32
32
  stream: boolean;
33
33
  options: OcxRequestOptions;
34
34
  _rawBody?: unknown;
35
- /** Number of leading raw input items restored from local previous_response_id state. */
35
+ /**
36
+ * Boundary between replayed history and this turn's newly appended input. Usually the
37
+ * items the proxy restored from local previous_response_id state; also set when the
38
+ * CLIENT already carried that history verbatim and the proxy skipped the prepend.
39
+ */
36
40
  _replayPrefixLen?: number;
37
41
  /** Parsed-message index before the first conversational item in a continuation's current delta. */
38
42
  _continuationConversationMessageIndex?: number;
39
- /** True when the proxy expanded a previous_response_id request into a full input replay. */
43
+ /**
44
+ * True when the full history for a previous_response_id request is present in the input —
45
+ * whether the proxy expanded it or the client already sent it. Consumers read this as
46
+ * "this request is self-contained", never as "the proxy mutated it".
47
+ */
40
48
  _previousResponseInputExpanded?: boolean;
41
49
  /** Provider-private stable Cursor conversation id resolved from the Responses previous_response_id chain. */
42
50
  _cursorConversationId?: string;
package/src/update/job.ts CHANGED
@@ -578,7 +578,7 @@ export function spawnGuiUpdateWorker(
578
578
  ].join("; ");
579
579
  const launched = spawnSync(
580
580
  resolveTrustedWindowsPowerShellExe(),
581
- ["-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", ps],
581
+ ["-NoProfile", "-NoLogo", "-NonInteractive", "-Command", ps],
582
582
  { encoding: "utf8", windowsHide: true, timeout: 15_000 },
583
583
  );
584
584
  const pid = Number(String(launched.stdout ?? "").trim().split(/\r?\n/).pop());
@@ -1386,7 +1386,7 @@ function killWindowsServiceWrapperProcesses(): void {
1386
1386
  "} | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }",
1387
1387
  ].join(" ");
1388
1388
  spawnSync(resolveTrustedWindowsPowerShellExe(), [
1389
- "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
1389
+ "-NoProfile", "-NoLogo", "-NonInteractive",
1390
1390
  "-Command", ps,
1391
1391
  ], { stdio: "ignore", timeout: 5000, windowsHide: true });
1392
1392
  } catch {
@@ -136,9 +136,23 @@ export function parseUsageSurface(input: string | null | undefined): UsageSurfac
136
136
  return "all";
137
137
  }
138
138
 
139
- function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
140
- if (range === "7d") return { since: now - 7 * DAY_MS, days: 7 };
141
- if (range === "30d") return { since: now - 30 * DAY_MS, days: 30 };
139
+ function startOfLocalDay(ts: number): number {
140
+ const d = new Date(ts);
141
+ d.setHours(0, 0, 0, 0);
142
+ return d.getTime();
143
+ }
144
+
145
+ export function rangeWindow(range: UsageRange, now: number): { since: number | null; days: number } {
146
+ if (range === "7d") {
147
+ const start = new Date(startOfLocalDay(now));
148
+ start.setDate(start.getDate() - 6);
149
+ return { since: start.getTime(), days: 7 };
150
+ }
151
+ if (range === "30d") {
152
+ const start = new Date(startOfLocalDay(now));
153
+ start.setDate(start.getDate() - 29);
154
+ return { since: start.getTime(), days: 30 };
155
+ }
142
156
  return { since: null, days: 0 };
143
157
  }
144
158
 
@@ -347,8 +361,11 @@ function buildDayGrid(range: UsageRange, since: number | null, now: number, entr
347
361
  m.attemptCount += 1;
348
362
  m.totalTokens += usageDisplayTotalTokens(attribution.usage, attribution.totalTokens) ?? 0;
349
363
  };
364
+ const startOfToday = startOfLocalDay(now);
350
365
  for (let i = days - 1; i >= 0; i--) {
351
- const key = localDateKey(now - i * DAY_MS);
366
+ const d = new Date(startOfToday);
367
+ d.setDate(d.getDate() - i);
368
+ const key = localDateKey(d.getTime());
352
369
  grid.set(key, { date: key, requests: 0, measuredRequests: 0, reportedRequests: 0, totalTokens: 0, models: [] });
353
370
  }
354
371
  for (const entry of entries) {
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { OcxConfig, OcxContentPart, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxTextContent } from "../types";
3
3
  import { modelInList } from "../types";
4
+ import { modelRecordValue } from "../reasoning-effort";
4
5
  import type { VisionReasoningEffort } from "../reasoning-effort";
5
6
  import { describeImage, type DescribeOutcome, type VisionSettings } from "./describe";
6
7
  import { describeImageAnthropic } from "./anthropic-describe";
@@ -18,6 +19,22 @@ import {
18
19
  } from "./timeout-bounds";
19
20
 
20
21
  export { describeImage } from "./describe";
22
+
23
+ /**
24
+ * True when the model is explicitly known to be text-only — either listed in
25
+ * `noVisionModels` or declared with `modelInputModalities` that exclude "image".
26
+ * Returns false for unknown models (no evidence either way) so they fall through
27
+ * to native image passthrough, which is the safe default for an unclassified model.
28
+ */
29
+ export function isModelTextOnly(
30
+ provider: OcxProviderConfig,
31
+ modelId: string,
32
+ ): boolean {
33
+ if (modelInList(provider.noVisionModels, modelId)) return true;
34
+ const modalities = modelRecordValue(provider.modelInputModalities, modelId);
35
+ if (Array.isArray(modalities) && modalities.length > 0 && !modalities.includes("image")) return true;
36
+ return false;
37
+ }
21
38
  export { describeImageAnthropic, parseAnthropicVisionSSE } from "./anthropic-describe";
22
39
  export {
23
40
  BASELINE_VISION_MODELS,
@@ -245,10 +262,10 @@ function messagesHaveImage(parsed: OcxParsedRequest): boolean {
245
262
  export function shouldResolveOpenAiVisionSidecar(
246
263
  config: OcxConfig,
247
264
  provider: OcxProviderConfig,
248
- modelId: string,
249
- parsed: OcxParsedRequest,
265
+ modelId: string,
266
+ parsed: OcxParsedRequest,
250
267
  ): boolean {
251
- if (!modelInList(provider.noVisionModels, modelId) || !messagesHaveImage(parsed)) return false;
268
+ if (!isModelTextOnly(provider, modelId) || !messagesHaveImage(parsed)) return false;
252
269
  const cfg = config.visionSidecar ?? {};
253
270
  if (cfg.enabled === false) return false;
254
271
  return resolveVisionBackend(cfg.backend, findAnthropicVisionProvider(config)) === "openai";
@@ -275,7 +292,7 @@ export function planVisionSidecar(
275
292
  parsed: OcxParsedRequest,
276
293
  openAiSidecar?: ResolvedOpenAiForwardSidecar,
277
294
  ): VisionPlan | undefined {
278
- if (!modelInList(provider.noVisionModels, modelId)) return undefined;
295
+ if (!isModelTextOnly(provider, modelId)) return undefined;
279
296
  if (!messagesHaveImage(parsed)) return undefined;
280
297
  const cfg = config.visionSidecar ?? {};
281
298
  if (cfg.enabled === false) return undefined;
@@ -1,5 +1,6 @@
1
1
  import type { OcxConfig, OcxParsedRequest, OcxProviderConfig } from "../types";
2
2
  import { modelInList, toolChoiceToolPredicate } from "../types";
3
+ import { isModelTextOnly } from "../vision";
3
4
  import type { SidecarSettings } from "./executor";
4
5
  import type { ResolvedOpenAiForwardSidecar } from "../providers/openai-sidecar";
5
6
  import { getAccountSet } from "../oauth/store";
@@ -166,7 +167,7 @@ export function planWebSearch(
166
167
  timeoutMs,
167
168
  );
168
169
  // The routed model being text-only means the search model must verbalize image results (either backend).
169
- const describeImages = modelInList(provider.noVisionModels, modelId);
170
+ const describeImages = isModelTextOnly(provider, modelId);
170
171
  const reasoning = cfg.reasoning ?? DEFAULT_SIDECAR_REASONING;
171
172
  const streamRoutedModelOutput = cfg.streamRoutedModelOutput === true;
172
173