@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.
- package/gui/dist/assets/{index-DUCH59lJ.css → index-CQ7bIKee.css} +1 -1
- package/gui/dist/assets/{index-ta3-_hgj.js → index-D_JUZLEC.js} +16 -16
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/cursor/live-transport.ts +17 -5
- package/src/adapters/cursor/protobuf-events.ts +662 -20
- package/src/adapters/cursor/tool-definitions.ts +12 -6
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +22 -3
- package/src/bridge.ts +12 -2
- package/src/chat/inbound.ts +24 -1
- package/src/cli/index.ts +11 -0
- package/src/codex/app-server-processes.ts +3 -3
- package/src/codex/shim.ts +100 -5
- package/src/codex/user-identity.ts +36 -6
- package/src/config.ts +0 -2
- package/src/generated/compatibility-version.json +52 -44
- package/src/lib/errors.ts +27 -0
- package/src/lib/token-estimate.ts +19 -2
- package/src/lib/windows-elevation.ts +37 -0
- package/src/lib/windows-secret-acl.ts +7 -0
- package/src/lib/windows-text.ts +106 -0
- package/src/lib/windows-user-principal.ts +0 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/store.ts +32 -18
- package/src/providers/antigravity-models.ts +25 -5
- package/src/providers/free-directory.ts +1 -1
- package/src/providers/registry.ts +6 -3
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/server/chat-completions.ts +4 -2
- package/src/server/effort-policy.ts +18 -0
- package/src/server/index.ts +5 -1
- package/src/server/management/logs-usage-routes.ts +7 -22
- package/src/server/request-log.ts +48 -3
- package/src/server/responses/core.ts +59 -15
- package/src/server/responses/encrypted-payload.ts +58 -38
- package/src/server/responses/fetch-helpers.ts +12 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/policy-fallback.ts +13 -2
- package/src/server/responses/ws-upstream.ts +115 -6
- package/src/service-manager-probe.ts +23 -37
- package/src/service.ts +233 -25
- package/src/tray/windows.ts +0 -2
- package/src/types.ts +10 -2
- package/src/update/job.ts +2 -2
- package/src/usage/summary.ts +21 -4
- package/src/vision/index.ts +21 -4
- 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 {
|
|
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
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
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
|
-
|
|
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?: (
|
|
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
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
|
|
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
|
|
1982
|
-
|
|
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 (
|
|
2118
|
+
if (status() !== "nonexistent") {
|
|
1986
2119
|
console.log("🔁 Removing the native (WinSW) service before installing the Task Scheduler backend...");
|
|
1987
2120
|
try {
|
|
1988
|
-
|
|
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
|
-
|
|
1993
|
-
|
|
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
|
-
|
|
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
|
|
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; }
|
package/src/tray/windows.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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", "-
|
|
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",
|
|
1389
|
+
"-NoProfile", "-NoLogo", "-NonInteractive",
|
|
1390
1390
|
"-Command", ps,
|
|
1391
1391
|
], { stdio: "ignore", timeout: 5000, windowsHide: true });
|
|
1392
1392
|
} catch {
|
package/src/usage/summary.ts
CHANGED
|
@@ -136,9 +136,23 @@ export function parseUsageSurface(input: string | null | undefined): UsageSurfac
|
|
|
136
136
|
return "all";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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) {
|
package/src/vision/index.ts
CHANGED
|
@@ -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
|
-
|
|
249
|
-
|
|
265
|
+
modelId: string,
|
|
266
|
+
parsed: OcxParsedRequest,
|
|
250
267
|
): boolean {
|
|
251
|
-
if (!
|
|
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 (!
|
|
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;
|
package/src/web-search/index.ts
CHANGED
|
@@ -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 =
|
|
170
|
+
const describeImages = isModelTextOnly(provider, modelId);
|
|
170
171
|
const reasoning = cfg.reasoning ?? DEFAULT_SIDECAR_REASONING;
|
|
171
172
|
const streamRoutedModelOutput = cfg.streamRoutedModelOutput === true;
|
|
172
173
|
|