@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
@@ -26,6 +26,7 @@ import {
26
26
  resolveTrustedWindowsSchtasksExe,
27
27
  resolveTrustedWindowsSystemDirectory,
28
28
  } from "./lib/windows-elevation";
29
+ import { decodeWindowsTextBytes } from "./lib/windows-text";
29
30
  import { WINSW_SERVICE_ID } from "./lib/winsw";
30
31
 
31
32
  /** Short: this runs inside admission, and a slow answer is the same as none. */
@@ -119,6 +120,8 @@ export interface ProbeDeps {
119
120
  readonly configDir?: string;
120
121
  /** Test seam for WinSW SCM status. Production uses bounded trusted `sc.exe query`. */
121
122
  readonly winswStatus?: () => "started" | "stopped" | "nonexistent" | "unknown";
123
+ /** Test seam for redirected Windows legacy-codepage output. */
124
+ readonly windowsLocale?: string;
122
125
  }
123
126
 
124
127
  const LABEL = "com.opencodex.proxy";
@@ -338,29 +341,6 @@ function windowsConfigDirPath(deps: { home: string; configDir?: string }): strin
338
341
  return join(deps.home, ".opencodex");
339
342
  }
340
343
 
341
- /** Decode an on-disk Windows text asset (task XML, VBS), which is UTF-16LE (often BOM-prefixed). */
342
- function decodeWindowsText(buffer: Buffer): string {
343
- if (buffer.length === 0) return "";
344
- const bomUtf16Le = buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe;
345
- const bomUtf16Be = buffer.length >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff;
346
- const looksUtf16Le = buffer.length >= 4
347
- && buffer[1] === 0x00
348
- && buffer[3] === 0x00
349
- && buffer[0] !== 0x00;
350
- if (bomUtf16Le || looksUtf16Le) {
351
- return buffer.toString("utf16le").replace(/^\uFEFF/, "").trim();
352
- }
353
- if (bomUtf16Be) {
354
- const swapped = Buffer.alloc(buffer.length - 2);
355
- for (let i = 2; i + 1 < buffer.length; i += 2) {
356
- swapped[i - 2] = buffer[i + 1]!;
357
- swapped[i - 1] = buffer[i]!;
358
- }
359
- return swapped.toString("utf16le").trim();
360
- }
361
- return buffer.toString("utf8").replace(/^\uFEFF/, "").trim();
362
- }
363
-
364
344
  /** Decode the XML entities emitted by the service-definition writers. */
365
345
  function decodeXmlEntities(value: string): string {
366
346
  return value
@@ -478,7 +458,9 @@ const SCHTASKS_TASK_NOT_FOUND_EN = /cannot find the file specified/i;
478
458
  * other nonzero responses use a bounded full listing as the locale-neutral
479
459
  * fallback, and only a successful list without our task proves absence.
480
460
  */
481
- function probeWindowsTaskRegistration(deps: Required<Pick<ProbeDeps, "runRaw">>): {
461
+ function probeWindowsTaskRegistration(
462
+ deps: Required<Pick<ProbeDeps, "runRaw">> & Pick<ProbeDeps, "windowsLocale">,
463
+ ): {
482
464
  registered: "present" | "absent" | "unknown";
483
465
  registeredXml: string;
484
466
  } {
@@ -492,13 +474,14 @@ function probeWindowsTaskRegistration(deps: Required<Pick<ProbeDeps, "runRaw">>)
492
474
  const queried = deps.runRaw(schtasks, ["/query", "/tn", windowsTaskName(), "/xml"]);
493
475
  if (queried.spawnFailed || queried.timedOut) return { registered: "unknown", registeredXml: "" };
494
476
  if (queried.status === 0) {
495
- const registeredXml = decodeWindowsText(queried.stdout) || decodeWindowsText(queried.stderr);
477
+ const registeredXml = decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })
478
+ || decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale });
496
479
  return registeredXml
497
480
  ? { registered: "present", registeredXml }
498
481
  : { registered: "unknown", registeredXml: "" };
499
482
  }
500
483
 
501
- const queryText = `${decodeWindowsText(queried.stdout)}\n${decodeWindowsText(queried.stderr)}`;
484
+ const queryText = `${decodeWindowsTextBytes(queried.stdout, { locale: deps.windowsLocale })}\n${decodeWindowsTextBytes(queried.stderr, { locale: deps.windowsLocale })}`;
502
485
  if (queried.status !== null && SCHTASKS_TASK_NOT_FOUND_EN.test(queryText)) {
503
486
  return { registered: "absent", registeredXml: "" };
504
487
  }
@@ -507,7 +490,8 @@ function probeWindowsTaskRegistration(deps: Required<Pick<ProbeDeps, "runRaw">>)
507
490
  if (listed.spawnFailed || listed.timedOut || listed.status !== 0) {
508
491
  return { registered: "unknown", registeredXml: "" };
509
492
  }
510
- const listing = decodeWindowsText(listed.stdout) || decodeWindowsText(listed.stderr);
493
+ const listing = decodeWindowsTextBytes(listed.stdout, { locale: deps.windowsLocale })
494
+ || decodeWindowsTextBytes(listed.stderr, { locale: deps.windowsLocale });
511
495
  return windowsTaskListContains(listing, windowsTaskName())
512
496
  ? { registered: "unknown", registeredXml: "" }
513
497
  : { registered: "absent", registeredXml: "" };
@@ -545,7 +529,8 @@ function probeWinswRegistration(
545
529
  }
546
530
 
547
531
  function inspectWindows(
548
- deps: Required<Pick<ProbeDeps, "runRaw" | "home">> & Pick<ProbeDeps, "configDir" | "winswStatus">,
532
+ deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
533
+ & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
549
534
  ): ServiceManagerInstallation {
550
535
  const configDir = windowsConfigDirPath(deps);
551
536
  const taskXmlPath = join(configDir, "opencodex-service-task.xml");
@@ -557,7 +542,7 @@ function inspectWindows(
557
542
  let xml = "";
558
543
  if (task !== "absent") {
559
544
  try {
560
- xml = decodeWindowsText(readFileSync(taskXmlPath));
545
+ xml = decodeWindowsTextBytes(readFileSync(taskXmlPath), { locale: deps.windowsLocale });
561
546
  } catch (error) {
562
547
  return unknown(`the scheduled-task XML exists but could not be read: ${String(error)}`);
563
548
  }
@@ -659,7 +644,7 @@ function homesEqual(
659
644
  * generated service-asset directory.
660
645
  */
661
646
  function walkWindowsChain(
662
- deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir">,
647
+ deps: Required<Pick<ProbeDeps, "home">> & Pick<ProbeDeps, "configDir" | "windowsLocale">,
663
648
  xml: string,
664
649
  definitionPath: string,
665
650
  ): ServiceManagerInstallation {
@@ -683,7 +668,7 @@ function walkWindowsChain(
683
668
  }
684
669
  let launcherBody: string;
685
670
  try {
686
- launcherBody = decodeWindowsText(readFileSync(launcherPath));
671
+ launcherBody = decodeWindowsTextBytes(readFileSync(launcherPath), { locale: deps.windowsLocale });
687
672
  } catch (error) {
688
673
  return unknown(`the scheduled-task launcher could not be read: ${String(error)}`);
689
674
  }
@@ -702,7 +687,7 @@ function walkWindowsChain(
702
687
  }
703
688
  let wrapperBody: string;
704
689
  try {
705
- wrapperBody = decodeWindowsText(readFileSync(wrapperPath));
690
+ wrapperBody = decodeWindowsTextBytes(readFileSync(wrapperPath), { locale: deps.windowsLocale });
706
691
  } catch (error) {
707
692
  return unknown(`the launcher wrapper could not be read: ${String(error)}`);
708
693
  }
@@ -734,7 +719,8 @@ function walkWindowsChain(
734
719
  * this read-only ownership probe.
735
720
  */
736
721
  function walkWinswChain(
737
- deps: Required<Pick<ProbeDeps, "runRaw" | "home">> & Pick<ProbeDeps, "configDir" | "winswStatus">,
722
+ deps: Required<Pick<ProbeDeps, "runRaw" | "home">>
723
+ & Pick<ProbeDeps, "configDir" | "winswStatus" | "windowsLocale">,
738
724
  ): ServiceManagerInstallation {
739
725
  const configDir = windowsConfigDirPath(deps);
740
726
  const exePath = join(configDir, "winsw", `${WINSW_SERVICE_ID}.exe`);
@@ -753,7 +739,7 @@ function walkWinswChain(
753
739
 
754
740
  let body: string;
755
741
  try {
756
- body = decodeWindowsText(readFileSync(xmlPath));
742
+ body = decodeWindowsTextBytes(readFileSync(xmlPath), { locale: deps.windowsLocale });
757
743
  } catch (error) {
758
744
  return unknown(`the WinSW XML could not be read: ${String(error)}`);
759
745
  }
@@ -801,6 +787,7 @@ export function inspectServiceManagerInstallation(deps: ProbeDeps = {}): Service
801
787
  home,
802
788
  configDir: deps.configDir,
803
789
  winswStatus: deps.winswStatus,
790
+ windowsLocale: deps.windowsLocale,
804
791
  });
805
792
  }
806
793
  return unknown(`no service manager probe for platform ${platform}`);
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/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) {