@bitkyc08/opencodex 2.8.0 → 2.9.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/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
package/src/service.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* restore it via the command.
|
|
7
7
|
*/
|
|
8
8
|
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
import { findLiveProxy, SERVICE_STOP_LIVENESS } from "./server/proxy-liveness";
|
|
9
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
10
11
|
import { homedir } from "node:os";
|
|
11
12
|
import { dirname, join, resolve } from "node:path";
|
|
12
|
-
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
|
+
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort, verifyPidIdentity } from "./config";
|
|
13
14
|
import { loadConfig } from "./config";
|
|
14
15
|
import { restoreNativeCodex } from "./codex/inject";
|
|
15
16
|
import { stripGrokConfig } from "./grok/inject";
|
|
@@ -17,7 +18,6 @@ import { isWslRuntime } from "./codex/home";
|
|
|
17
18
|
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
|
|
18
19
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
19
20
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
20
|
-
import { findLiveProxy } from "./server/proxy-liveness";
|
|
21
21
|
import { randomUUID } from "node:crypto";
|
|
22
22
|
import {
|
|
23
23
|
ELEVATION_REQUEST_TIMEOUT_MS,
|
|
@@ -1428,7 +1428,38 @@ export async function repairService(deps: RepairServiceDeps = {}): Promise<void>
|
|
|
1428
1428
|
* scheduler backend first; on failure the machine is left with NO service (explicitly
|
|
1429
1429
|
* reported) — never a silent fallback to the scheduler.
|
|
1430
1430
|
*/
|
|
1431
|
+
/** Refuse WinSW when the interactive user is a Microsoft account (SCM cannot authenticate it). */
|
|
1432
|
+
export function assertWindowsNativeServiceAccountSupported(): void {
|
|
1433
|
+
if (process.platform !== "win32") return;
|
|
1434
|
+
const source = readWindowsPrincipalSource();
|
|
1435
|
+
if (source?.toLowerCase() === "microsoftaccount") {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
"The native (WinSW) service backend cannot run under a Microsoft-account Windows login. "
|
|
1438
|
+
+ "Keep the Task Scheduler backend (`ocx service install`) or sign in with a local/domain account before `ocx service install --native`.",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function readWindowsPrincipalSource(): string | null {
|
|
1444
|
+
if (process.platform !== "win32") return null;
|
|
1445
|
+
const ps = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
1446
|
+
if (!existsSync(ps)) return null;
|
|
1447
|
+
try {
|
|
1448
|
+
const out = execFileSync(ps, [
|
|
1449
|
+
"-NoLogo",
|
|
1450
|
+
"-NoProfile",
|
|
1451
|
+
"-NonInteractive",
|
|
1452
|
+
"-Command",
|
|
1453
|
+
"(Get-LocalUser -Name $env:USERNAME -ErrorAction SilentlyContinue).PrincipalSource",
|
|
1454
|
+
], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }).trim();
|
|
1455
|
+
return out || null;
|
|
1456
|
+
} catch {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1431
1461
|
async function installWindowsNative(): Promise<void> {
|
|
1462
|
+
assertWindowsNativeServiceAccountSupported();
|
|
1432
1463
|
recordOwnedConfigPath(getConfigDir(), serviceStatePath());
|
|
1433
1464
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
1434
1465
|
writeServiceApiTokenFile();
|
|
@@ -1463,11 +1494,49 @@ async function installWindowsNative(): Promise<void> {
|
|
|
1463
1494
|
writeServiceInstallState("native");
|
|
1464
1495
|
}
|
|
1465
1496
|
function startWindows(): void { schtasks(["/run", "/tn", TASK]); }
|
|
1466
|
-
|
|
1497
|
+
|
|
1498
|
+
export function isWindowsSchedulerEndBenign(error: unknown): boolean {
|
|
1499
|
+
const detail = schtasksErrorDetail(error).toLowerCase();
|
|
1500
|
+
return detail.includes("no running instance")
|
|
1501
|
+
|| detail.includes("not currently running")
|
|
1502
|
+
|| detail.includes("0x41330");
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* End the scheduler task. "Already stopped" is success; other `/end` failures are
|
|
1507
|
+
* swallowed so callers can still run tracked-proxy + live-proxy cleanup.
|
|
1508
|
+
*
|
|
1509
|
+
* Do not key a restart-window wait on `/end` failure: the #764 case is an `/end`
|
|
1510
|
+
* that *succeeds* while the wrapper survives and respawns. That verification lives
|
|
1511
|
+
* on the stop-verification path (poll across the restart window), not here.
|
|
1512
|
+
*/
|
|
1513
|
+
export function stopWindows(): void {
|
|
1514
|
+
try {
|
|
1515
|
+
schtasks(["/end", "/tn", TASK]);
|
|
1516
|
+
} catch (error) {
|
|
1517
|
+
if (isWindowsSchedulerEndBenign(error)) return;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1467
1520
|
function statusWindows(): string { try { return schtasks(["/query", "/tn", TASK]); } catch { return ""; } }
|
|
1468
1521
|
function statusWindowsXml(): string { try { return schtasks(["/query", "/tn", TASK, "/xml"]); } catch { return ""; } }
|
|
1469
1522
|
function uninstallWindows(): void {
|
|
1470
|
-
|
|
1523
|
+
const probe = probeWindowsSchedulerTask(TASK);
|
|
1524
|
+
if (probe.status === "present") {
|
|
1525
|
+
try {
|
|
1526
|
+
schtasks(["/delete", "/tn", TASK, "/f"]);
|
|
1527
|
+
} catch (error) {
|
|
1528
|
+
throw new Error(`Failed to delete Task Scheduler task ${TASK}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1529
|
+
}
|
|
1530
|
+
const afterDelete = probeWindowsSchedulerTask(TASK);
|
|
1531
|
+
if (afterDelete.status === "present") {
|
|
1532
|
+
throw new Error(`Task Scheduler task ${TASK} is still present after delete — refusing to remove service assets. Retry from an elevated shell.`);
|
|
1533
|
+
}
|
|
1534
|
+
if (afterDelete.status === "unknown") {
|
|
1535
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified after delete — refusing to remove service assets.`);
|
|
1536
|
+
}
|
|
1537
|
+
} else if (probe.status === "unknown") {
|
|
1538
|
+
throw new Error(`Task Scheduler task ${TASK} presence could not be verified — refusing to remove service assets.`);
|
|
1539
|
+
}
|
|
1471
1540
|
if (existsSync(windowsServiceScriptPath())) unlinkSync(windowsServiceScriptPath());
|
|
1472
1541
|
if (existsSync(windowsLauncherVbsPath())) unlinkSync(windowsLauncherVbsPath());
|
|
1473
1542
|
if (existsSync(windowsTaskXmlPath())) unlinkSync(windowsTaskXmlPath());
|
|
@@ -1626,18 +1695,88 @@ function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
|
|
|
1626
1695
|
|
|
1627
1696
|
type TrackedProxyCleanupResult = "none" | "stale" | "stopped";
|
|
1628
1697
|
|
|
1698
|
+
function verifiedKillTarget(pid: number | null | undefined): number | null {
|
|
1699
|
+
if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0) return null;
|
|
1700
|
+
const verified = verifyPidIdentity(pid);
|
|
1701
|
+
return verified === pid ? verified : null;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/**
|
|
1705
|
+
* Whether a proxy is still answering after the service manager claimed to stop it.
|
|
1706
|
+
*
|
|
1707
|
+
* `ops.stop()` reports the outcome of the STOP COMMAND, not of the process. A Windows scheduler
|
|
1708
|
+
* task whose wrapper survives `schtasks /end` respawns its child a few seconds later, so a stop
|
|
1709
|
+
* that returned success can still leave a live proxy — and `ocx service stop` then restored
|
|
1710
|
+
* native Codex on top of a running one (#764). The tracked-pid cleanup does not catch it either:
|
|
1711
|
+
* the respawned child writes a different pid, or none this process knows about.
|
|
1712
|
+
*
|
|
1713
|
+
* Probed rather than assumed, and bounded. The respawn risk is specific to a supervisor that can
|
|
1714
|
+
* restart its child — the Windows scheduler wrapper — so only that case pays the restart window.
|
|
1715
|
+
* Everywhere else a single probe answers the question, because nothing is going to bring the
|
|
1716
|
+
* proxy back after `launchctl unload` or `systemctl stop`. Making every platform wait 7s on a
|
|
1717
|
+
* stop that already succeeded would trade one bug for a worse everyday one.
|
|
1718
|
+
*/
|
|
1719
|
+
export async function proxyStillLiveAfterStop(deps: {
|
|
1720
|
+
findProxy?: () => Promise<{ port: number } | null>;
|
|
1721
|
+
sleep?: (ms: number) => Promise<void>;
|
|
1722
|
+
now?: () => number;
|
|
1723
|
+
/** Whether the stopped supervisor can respawn its child; only then is polling worth the wait. */
|
|
1724
|
+
canRespawn?: boolean;
|
|
1725
|
+
} = {}): Promise<{ port: number } | null> {
|
|
1726
|
+
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
1727
|
+
const now = deps.now ?? Date.now;
|
|
1728
|
+
const canRespawn = deps.canRespawn ?? process.platform === "win32";
|
|
1729
|
+
const deadline = now() + (canRespawn ? 7000 : 0);
|
|
1730
|
+
// Single-shot (non-respawn) still needs one full SERVICE_STOP_LIVENESS budget; respawn
|
|
1731
|
+
// polling shares the outer deadline so multi-candidate discovery cannot overrun it.
|
|
1732
|
+
const findProxy = deps.findProxy ?? (() => {
|
|
1733
|
+
const probeDeadline = canRespawn
|
|
1734
|
+
? deadline
|
|
1735
|
+
: now() + (SERVICE_STOP_LIVENESS.timeoutMs! * SERVICE_STOP_LIVENESS.attempts! + 250);
|
|
1736
|
+
return findLiveProxy({ ...SERVICE_STOP_LIVENESS, deadlineAt: probeDeadline, nowFn: now });
|
|
1737
|
+
});
|
|
1738
|
+
for (;;) {
|
|
1739
|
+
try {
|
|
1740
|
+
const live = await findProxy();
|
|
1741
|
+
if (live) return live;
|
|
1742
|
+
} catch {
|
|
1743
|
+
// A probe failure is not proof the proxy is gone; keep polling until the deadline.
|
|
1744
|
+
}
|
|
1745
|
+
if (now() >= deadline) return null;
|
|
1746
|
+
await sleep(1000);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1629
1750
|
async function stopTrackedProxyIfRunning(): Promise<TrackedProxyCleanupResult> {
|
|
1751
|
+
let stopped = false;
|
|
1630
1752
|
const pid = readPid();
|
|
1631
|
-
|
|
1632
|
-
if (
|
|
1753
|
+
const trackedKillPid = verifiedKillTarget(pid);
|
|
1754
|
+
if (trackedKillPid !== null && isProcessAlive(trackedKillPid)) {
|
|
1755
|
+
await stopProxy(trackedKillPid);
|
|
1756
|
+
removePid(trackedKillPid);
|
|
1757
|
+
removeRuntimePort(trackedKillPid);
|
|
1758
|
+
stopped = true;
|
|
1759
|
+
} else if (pid) {
|
|
1633
1760
|
removePid(pid);
|
|
1634
1761
|
removeRuntimePort(pid);
|
|
1635
|
-
return "stale";
|
|
1636
1762
|
}
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1763
|
+
// Orphan recovery: the pid file can be missing/stale while the service wrapper keeps
|
|
1764
|
+
// a live proxy running — mirror `ocx stop`'s identity-checked findLiveProxy fallback.
|
|
1765
|
+
// Cap multi-candidate discovery so stop cleanup cannot hang for three full retry budgets.
|
|
1766
|
+
const live = await findLiveProxy({
|
|
1767
|
+
...SERVICE_STOP_LIVENESS,
|
|
1768
|
+
deadlineAt: Date.now() + 7000,
|
|
1769
|
+
});
|
|
1770
|
+
const liveKillPid = verifiedKillTarget(live?.pid);
|
|
1771
|
+
if (liveKillPid !== null) {
|
|
1772
|
+
await stopProxy(liveKillPid);
|
|
1773
|
+
removePid(liveKillPid);
|
|
1774
|
+
removeRuntimePort(liveKillPid);
|
|
1775
|
+
stopped = true;
|
|
1776
|
+
}
|
|
1777
|
+
if (stopped) return "stopped";
|
|
1778
|
+
if (pid) return "stale";
|
|
1779
|
+
return "none";
|
|
1641
1780
|
}
|
|
1642
1781
|
|
|
1643
1782
|
async function stopTrackedProxyForServiceCommand(): Promise<TrackedProxyCleanupResult> {
|
|
@@ -1723,6 +1862,15 @@ export function isServiceInstalled(): boolean {
|
|
|
1723
1862
|
return diagnoseService().installed;
|
|
1724
1863
|
}
|
|
1725
1864
|
|
|
1865
|
+
/**
|
|
1866
|
+
* True when an installed background service can actually supervise the proxy.
|
|
1867
|
+
* Presence alone is not enough: stale/missing assets, conflicts, and disabled
|
|
1868
|
+
* registrations report `installed` but will not bring the proxy back after exit.
|
|
1869
|
+
*/
|
|
1870
|
+
export function isServiceViable(): boolean {
|
|
1871
|
+
return diagnoseService().viable;
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1726
1874
|
export interface ServiceDiagnostic {
|
|
1727
1875
|
supported: boolean;
|
|
1728
1876
|
installed: boolean;
|
|
@@ -1945,13 +2093,29 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1945
2093
|
ops.start();
|
|
1946
2094
|
console.log("✅ service started.");
|
|
1947
2095
|
break;
|
|
1948
|
-
case "stop":
|
|
2096
|
+
case "stop": {
|
|
1949
2097
|
assertServiceEnvironmentMatchesInstall();
|
|
1950
2098
|
// Only stop what is actually installed. The unguarded call ran a real `launchctl unload`
|
|
1951
2099
|
// (and its Windows/Linux twins) even with nothing installed.
|
|
1952
|
-
if (ops.status() !== null || isServiceInstalled())
|
|
2100
|
+
if (ops.status() !== null || isServiceInstalled()) {
|
|
2101
|
+
ops.stop();
|
|
2102
|
+
}
|
|
1953
2103
|
await stopTrackedProxyForServiceCommand();
|
|
1954
2104
|
{
|
|
2105
|
+
// Verify rather than trust the stop command: a surviving wrapper respawns its child
|
|
2106
|
+
// seconds later, and restoring native Codex on top of a live proxy is the failure #764
|
|
2107
|
+
// reports as "stop reports success without stopping the proxy".
|
|
2108
|
+
const survivor = await proxyStillLiveAfterStop();
|
|
2109
|
+
if (survivor) {
|
|
2110
|
+
console.error(
|
|
2111
|
+
`❌ service stop did not take effect: a proxy is still listening on port ${survivor.port}.`
|
|
2112
|
+
+ "\nNative Codex was NOT restored, because doing so while the proxy is running leaves"
|
|
2113
|
+
+ " both pointing at each other. Check for a second service backend (`ocx service status`)"
|
|
2114
|
+
+ " or a manually started proxy, then re-run `ocx service stop`.",
|
|
2115
|
+
);
|
|
2116
|
+
process.exitCode = 1;
|
|
2117
|
+
break;
|
|
2118
|
+
}
|
|
1955
2119
|
const restore = restoreNativeCodex();
|
|
1956
2120
|
if (restore.success) console.log("✅ service stopped + native Codex restored.");
|
|
1957
2121
|
else console.error(`⚠️ service stopped, but native Codex restore FAILED: ${restore.message}\nRun \`ocx restore\` (or check $CODEX_HOME/config.toml) before using native Codex.`);
|
|
@@ -1962,6 +2126,7 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
1962
2126
|
else if (!grok.ok) console.error(`⚠️ ${grok.message}`);
|
|
1963
2127
|
}
|
|
1964
2128
|
break;
|
|
2129
|
+
}
|
|
1965
2130
|
case "status": {
|
|
1966
2131
|
if (process.platform === "win32" && backend === "scheduler") {
|
|
1967
2132
|
console.log(await inspectWindowsSchedulerServiceStatus());
|
|
@@ -2007,3 +2172,4 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
|
|
|
2007
2172
|
process.exit(1);
|
|
2008
2173
|
}
|
|
2009
2174
|
}
|
|
2175
|
+
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import type { CleanupMode, CleanupResult } from "./cleanup";
|
|
9
9
|
import { resolveCodexHomeDir } from "../codex/home";
|
|
10
10
|
import {
|
|
11
|
-
endStorageMutation,
|
|
12
11
|
tryBeginStorageMutation,
|
|
13
12
|
} from "./storage-mutation-coordinator";
|
|
13
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
14
14
|
import {
|
|
15
15
|
isPolicyDue,
|
|
16
16
|
readStorageCleanupPolicyFromConfig,
|
|
@@ -19,9 +19,12 @@ import {
|
|
|
19
19
|
type PolicySkipReason,
|
|
20
20
|
} from "./policy";
|
|
21
21
|
import {
|
|
22
|
+
cancelQueuedStorageWorkerSpawns,
|
|
22
23
|
drainStorageWorkers,
|
|
23
|
-
|
|
24
|
+
StorageWorkerAdmissionBusyError,
|
|
24
25
|
terminateStorageWorker,
|
|
26
|
+
tryReserveStorageWorker,
|
|
27
|
+
withStorageWorkerSpawnGate,
|
|
25
28
|
} from "./worker-lifecycle";
|
|
26
29
|
|
|
27
30
|
export type PolicyJobStatus = "idle" | "running";
|
|
@@ -82,12 +85,11 @@ let cancelActiveRun: (() => void) | null = null;
|
|
|
82
85
|
/** Bumped on abort/reset so a late worker completion cannot clobber newer job state. */
|
|
83
86
|
let runGeneration = 0;
|
|
84
87
|
/** CODEX_HOME whose mutation slot this job holds (parent thread only). */
|
|
85
|
-
let
|
|
88
|
+
let heldMutationLease: AdmissionLease | undefined;
|
|
86
89
|
|
|
87
90
|
function releaseHeldMutationSlot(): void {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
heldMutationHome = undefined;
|
|
91
|
+
heldMutationLease?.release();
|
|
92
|
+
heldMutationLease = undefined;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
export function setStorageCleanupPolicyJobLiveApply(
|
|
@@ -129,8 +131,16 @@ function disownActiveRun(): void {
|
|
|
129
131
|
cancel?.();
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Fire-and-forget reset. Prefer {@link resetStorageCleanupPolicyJobForTestsAsync}
|
|
136
|
+
* from test beforeEach/afterEach — sync terminate races Windows
|
|
137
|
+
* `bun test --isolate` reclaim when the next case spawns immediately.
|
|
138
|
+
*/
|
|
132
139
|
export function resetStorageCleanupPolicyJobForTests(): void {
|
|
133
140
|
disownActiveRun();
|
|
141
|
+
// Invalidate spawnGate callbacks that have not created a Worker yet — otherwise
|
|
142
|
+
// they can still spawn after this sync path releases the mutation slot.
|
|
143
|
+
cancelQueuedStorageWorkerSpawns();
|
|
134
144
|
if (activeWorker) {
|
|
135
145
|
void terminateStorageWorker(activeWorker);
|
|
136
146
|
activeWorker = null;
|
|
@@ -148,19 +158,39 @@ export function resetStorageCleanupPolicyJobForTests(): void {
|
|
|
148
158
|
* worker still exiting at that moment trips a Bun-internal assertion on Windows
|
|
149
159
|
* and takes the whole run down, so a suite that spawns workers must be able to
|
|
150
160
|
* wait for them rather than fire-and-forget.
|
|
161
|
+
*
|
|
162
|
+
* Do not route through the sync reset's `void terminate(...)`: that used to
|
|
163
|
+
* deregister the worker before the thread exited, so a follow-up drain saw an
|
|
164
|
+
* empty registry and returned while Windows was still reclaiming the thread.
|
|
151
165
|
*/
|
|
152
166
|
export async function resetStorageCleanupPolicyJobForTestsAsync(): Promise<void> {
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
disownActiveRun();
|
|
168
|
+
cancelQueuedStorageWorkerSpawns();
|
|
169
|
+
const worker = activeWorker;
|
|
170
|
+
activeWorker = null;
|
|
171
|
+
inflight = null;
|
|
172
|
+
testHooks = null;
|
|
173
|
+
state = { status: "idle" };
|
|
174
|
+
// Join the worker before releasing the mutation slot so a concurrent run
|
|
175
|
+
// cannot acquire CODEX_HOME while the aborted thread is still mutating.
|
|
176
|
+
try {
|
|
177
|
+
if (worker) await terminateStorageWorker(worker);
|
|
178
|
+
await drainStorageWorkers();
|
|
179
|
+
} finally {
|
|
180
|
+
releaseHeldMutationSlot();
|
|
181
|
+
}
|
|
155
182
|
}
|
|
156
183
|
|
|
157
184
|
/** Terminate an in-flight worker during process shutdown. */
|
|
158
185
|
export function abortStorageCleanupPolicyJob(): void {
|
|
159
186
|
disownActiveRun();
|
|
187
|
+
cancelQueuedStorageWorkerSpawns();
|
|
160
188
|
if (activeWorker) {
|
|
161
189
|
void terminateStorageWorker(activeWorker);
|
|
162
190
|
activeWorker = null;
|
|
163
191
|
}
|
|
192
|
+
// Sync path cannot join; prefer abortStorageCleanupPolicyJobAsync when the
|
|
193
|
+
// caller can await so the mutation slot stays held until the thread exits.
|
|
164
194
|
releaseHeldMutationSlot();
|
|
165
195
|
if (state.status === "running") {
|
|
166
196
|
state = {
|
|
@@ -177,6 +207,37 @@ export function abortStorageCleanupPolicyJob(): void {
|
|
|
177
207
|
inflight = null;
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Shutdown path that joins worker threads. Prefer this over the sync abort when
|
|
212
|
+
* the caller can await (server drain, test teardown) so Windows isolate reclaim
|
|
213
|
+
* does not race a still-exiting storage worker.
|
|
214
|
+
*/
|
|
215
|
+
export async function abortStorageCleanupPolicyJobAsync(): Promise<void> {
|
|
216
|
+
disownActiveRun();
|
|
217
|
+
cancelQueuedStorageWorkerSpawns();
|
|
218
|
+
const worker = activeWorker;
|
|
219
|
+
activeWorker = null;
|
|
220
|
+
if (state.status === "running") {
|
|
221
|
+
state = {
|
|
222
|
+
...state,
|
|
223
|
+
status: "idle",
|
|
224
|
+
finishedAt: Date.now(),
|
|
225
|
+
lastError: "aborted",
|
|
226
|
+
lastOutcome: {
|
|
227
|
+
ok: false,
|
|
228
|
+
error: "evaluation_failed",
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
inflight = null;
|
|
233
|
+
try {
|
|
234
|
+
if (worker) await terminateStorageWorker(worker);
|
|
235
|
+
await drainStorageWorkers();
|
|
236
|
+
} finally {
|
|
237
|
+
releaseHeldMutationSlot();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
180
241
|
function outcomeFromResult(result: PolicyRunResult): PolicyJobOutcome {
|
|
181
242
|
return {
|
|
182
243
|
ok: result.ok,
|
|
@@ -232,22 +293,22 @@ function applyMutationBusy(): void {
|
|
|
232
293
|
}
|
|
233
294
|
|
|
234
295
|
function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Promise<PolicyRunResult> {
|
|
235
|
-
|
|
296
|
+
const reservation = tryReserveStorageWorker();
|
|
297
|
+
if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
|
|
298
|
+
return withStorageWorkerSpawnGate(() => new Promise<PolicyRunResult>((resolve, reject) => {
|
|
236
299
|
const requestId = crypto.randomUUID();
|
|
237
300
|
let settled = false;
|
|
238
|
-
|
|
239
|
-
|
|
301
|
+
let worker: Worker;
|
|
302
|
+
try {
|
|
303
|
+
worker = new Worker(new URL("./policy-worker.ts", import.meta.url).href);
|
|
304
|
+
reservation.bind(worker);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
reservation.release();
|
|
307
|
+
reject(error);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
240
310
|
activeWorker = worker;
|
|
241
311
|
|
|
242
|
-
const timer = setTimeout(() => {
|
|
243
|
-
if (settled) return;
|
|
244
|
-
settled = true;
|
|
245
|
-
cancelActiveRun = null;
|
|
246
|
-
void terminateStorageWorker(worker);
|
|
247
|
-
if (activeWorker === worker) activeWorker = null;
|
|
248
|
-
reject(new Error("storage_cleanup_worker_timeout"));
|
|
249
|
-
}, WORKER_TIMEOUT_MS);
|
|
250
|
-
|
|
251
312
|
const finish = (fn: () => void) => {
|
|
252
313
|
if (settled) return;
|
|
253
314
|
settled = true;
|
|
@@ -256,10 +317,15 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
256
317
|
if (activeWorker === worker) activeWorker = null;
|
|
257
318
|
// Settle the caller only after the thread is actually gone, so a suite
|
|
258
319
|
// that awaits its request cannot reach the next test file with a worker
|
|
259
|
-
// still exiting behind it.
|
|
320
|
+
// still exiting behind it. Also keeps executeJob's finally from releasing
|
|
321
|
+
// the mutation slot while the thread may still mutate CODEX_HOME.
|
|
260
322
|
void terminateStorageWorker(worker).then(fn, fn);
|
|
261
323
|
};
|
|
262
324
|
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
finish(() => reject(new Error("storage_cleanup_worker_timeout")));
|
|
327
|
+
}, WORKER_TIMEOUT_MS);
|
|
328
|
+
|
|
263
329
|
cancelActiveRun = () => {
|
|
264
330
|
finish(() => reject(new Error("aborted")));
|
|
265
331
|
};
|
|
@@ -296,6 +362,9 @@ function runInWorker(opts: RequestPolicyRunOptions & { blockMs?: number }): Prom
|
|
|
296
362
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
297
363
|
},
|
|
298
364
|
});
|
|
365
|
+
})).catch(error => {
|
|
366
|
+
reservation.release();
|
|
367
|
+
throw error;
|
|
299
368
|
});
|
|
300
369
|
}
|
|
301
370
|
|
|
@@ -309,7 +378,7 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
|
|
|
309
378
|
}
|
|
310
379
|
return;
|
|
311
380
|
}
|
|
312
|
-
|
|
381
|
+
heldMutationLease = gate.lease;
|
|
313
382
|
try {
|
|
314
383
|
const blockMs = testHooks?.blockMs;
|
|
315
384
|
let result: PolicyRunResult;
|
|
@@ -336,7 +405,8 @@ async function executeJob(opts: RequestPolicyRunOptions): Promise<void> {
|
|
|
336
405
|
applyFinished(result);
|
|
337
406
|
} catch (err) {
|
|
338
407
|
if (generation !== runGeneration) return;
|
|
339
|
-
|
|
408
|
+
if (err instanceof StorageWorkerAdmissionBusyError) applyMutationBusy();
|
|
409
|
+
else applyFailed(err instanceof Error ? err.message : "worker_failed");
|
|
340
410
|
} finally {
|
|
341
411
|
releaseHeldMutationSlot();
|
|
342
412
|
}
|
|
@@ -49,5 +49,11 @@ self.onmessage = (event: MessageEvent<unknown>) => {
|
|
|
49
49
|
requestId,
|
|
50
50
|
message: err instanceof Error ? err.message : "worker_failed",
|
|
51
51
|
});
|
|
52
|
+
} finally {
|
|
53
|
+
// Close from inside the worker so the thread begins exiting before the
|
|
54
|
+
// parent’s terminate() races isolate realm reclaim on Windows.
|
|
55
|
+
try {
|
|
56
|
+
(self as unknown as { close?: () => void }).close?.();
|
|
57
|
+
} catch { /* already closing */ }
|
|
52
58
|
}
|
|
53
59
|
};
|
|
@@ -17,9 +17,12 @@ import {
|
|
|
17
17
|
type StorageMutationCoordinatorTestHooks,
|
|
18
18
|
} from "./storage-mutation-coordinator";
|
|
19
19
|
import {
|
|
20
|
+
cancelQueuedStorageWorkerSpawns,
|
|
20
21
|
drainStorageWorkers,
|
|
21
|
-
|
|
22
|
+
StorageWorkerAdmissionBusyError,
|
|
22
23
|
terminateStorageWorker,
|
|
24
|
+
tryReserveStorageWorker,
|
|
25
|
+
withStorageWorkerSpawnGate,
|
|
23
26
|
} from "./worker-lifecycle";
|
|
24
27
|
|
|
25
28
|
export interface RestoreJobTestHooks extends StorageMutationCoordinatorTestHooks {
|
|
@@ -72,7 +75,12 @@ export function setRestoreTrashJobTestHooks(hooks: RestoreJobTestHooks | null):
|
|
|
72
75
|
setStorageMutationCoordinatorTestHooks(hooks);
|
|
73
76
|
}
|
|
74
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Fire-and-forget reset. Prefer {@link resetRestoreTrashJobForTestsAsync} from
|
|
80
|
+
* test beforeEach/afterEach under `bun test --isolate` on Windows.
|
|
81
|
+
*/
|
|
75
82
|
export function resetRestoreTrashJobForTests(): void {
|
|
83
|
+
cancelQueuedStorageWorkerSpawns();
|
|
76
84
|
if (activeWorker) {
|
|
77
85
|
void terminateStorageWorker(activeWorker);
|
|
78
86
|
activeWorker = null;
|
|
@@ -83,14 +91,31 @@ export function resetRestoreTrashJobForTests(): void {
|
|
|
83
91
|
resetStorageMutationCoordinatorForTests();
|
|
84
92
|
}
|
|
85
93
|
|
|
86
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Await-able reset for test teardown; see policy-job's equivalent for why.
|
|
96
|
+
* Awaits the active worker directly so drain cannot race a fire-and-forget
|
|
97
|
+
* deregister from the sync reset.
|
|
98
|
+
*/
|
|
87
99
|
export async function resetRestoreTrashJobForTestsAsync(): Promise<void> {
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
cancelQueuedStorageWorkerSpawns();
|
|
101
|
+
const worker = activeWorker;
|
|
102
|
+
activeWorker = null;
|
|
103
|
+
cancelActiveRun?.();
|
|
104
|
+
cancelActiveRun = null;
|
|
105
|
+
testHooks = null;
|
|
106
|
+
// Join the worker before clearing the mutation coordinator so a concurrent
|
|
107
|
+
// run cannot acquire CODEX_HOME while the aborted thread is still mutating.
|
|
108
|
+
try {
|
|
109
|
+
if (worker) await terminateStorageWorker(worker);
|
|
110
|
+
await drainStorageWorkers();
|
|
111
|
+
} finally {
|
|
112
|
+
resetStorageMutationCoordinatorForTests();
|
|
113
|
+
}
|
|
90
114
|
}
|
|
91
115
|
|
|
92
116
|
/** Terminate an in-flight worker during process shutdown. */
|
|
93
117
|
export function abortRestoreTrashJob(): void {
|
|
118
|
+
cancelQueuedStorageWorkerSpawns();
|
|
94
119
|
if (activeWorker) {
|
|
95
120
|
void terminateStorageWorker(activeWorker);
|
|
96
121
|
activeWorker = null;
|
|
@@ -99,6 +124,17 @@ export function abortRestoreTrashJob(): void {
|
|
|
99
124
|
cancelActiveRun = null;
|
|
100
125
|
}
|
|
101
126
|
|
|
127
|
+
/** Await-able shutdown sibling; joins the restore worker thread before returning. */
|
|
128
|
+
export async function abortRestoreTrashJobAsync(): Promise<void> {
|
|
129
|
+
cancelQueuedStorageWorkerSpawns();
|
|
130
|
+
const worker = activeWorker;
|
|
131
|
+
activeWorker = null;
|
|
132
|
+
cancelActiveRun?.();
|
|
133
|
+
cancelActiveRun = null;
|
|
134
|
+
if (worker) await terminateStorageWorker(worker);
|
|
135
|
+
await drainStorageWorkers();
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
/** Test-only SSE/text stream served from the proxy while a worker is blocked. */
|
|
103
139
|
export function getRestoreTrashTestStreamResponse(): Response | null {
|
|
104
140
|
if (!testHooks?.enableTestStream) return null;
|
|
@@ -124,31 +160,37 @@ function runInWorker(opts: {
|
|
|
124
160
|
blockMs?: number;
|
|
125
161
|
restoreTest?: RestoreTestHooks;
|
|
126
162
|
}): Promise<RestoreResult> {
|
|
127
|
-
|
|
163
|
+
const reservation = tryReserveStorageWorker();
|
|
164
|
+
if (!reservation) return Promise.reject(new StorageWorkerAdmissionBusyError());
|
|
165
|
+
return withStorageWorkerSpawnGate(() => new Promise<RestoreResult>((resolve, reject) => {
|
|
128
166
|
const requestId = crypto.randomUUID();
|
|
129
167
|
let settled = false;
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
let worker: Worker;
|
|
169
|
+
try {
|
|
170
|
+
worker = new Worker(new URL("./restore-worker.ts", import.meta.url).href);
|
|
171
|
+
reservation.bind(worker);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
reservation.release();
|
|
174
|
+
reject(error);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
132
177
|
activeWorker = worker;
|
|
133
178
|
|
|
134
|
-
const timer = setTimeout(() => {
|
|
135
|
-
if (settled) return;
|
|
136
|
-
settled = true;
|
|
137
|
-
cancelActiveRun = null;
|
|
138
|
-
void terminateStorageWorker(worker);
|
|
139
|
-
if (activeWorker === worker) activeWorker = null;
|
|
140
|
-
reject(new Error("restore_worker_timeout"));
|
|
141
|
-
}, WORKER_TIMEOUT_MS);
|
|
142
|
-
|
|
143
179
|
const finish = (fn: () => void) => {
|
|
144
180
|
if (settled) return;
|
|
145
181
|
settled = true;
|
|
146
182
|
cancelActiveRun = null;
|
|
147
183
|
clearTimeout(timer);
|
|
148
184
|
if (activeWorker === worker) activeWorker = null;
|
|
185
|
+
// Join before settle so mutation coordination inside the restore worker
|
|
186
|
+
// cannot overlap a follow-on run after a watchdog timeout.
|
|
149
187
|
void terminateStorageWorker(worker).then(fn, fn);
|
|
150
188
|
};
|
|
151
189
|
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
finish(() => reject(new Error("restore_worker_timeout")));
|
|
192
|
+
}, WORKER_TIMEOUT_MS);
|
|
193
|
+
|
|
152
194
|
cancelActiveRun = () => {
|
|
153
195
|
finish(() => reject(new Error("aborted")));
|
|
154
196
|
};
|
|
@@ -185,6 +227,9 @@ function runInWorker(opts: {
|
|
|
185
227
|
...(process.env.OPENCODEX_HOME ? { OPENCODEX_HOME: process.env.OPENCODEX_HOME } : {}),
|
|
186
228
|
},
|
|
187
229
|
});
|
|
230
|
+
})).catch(error => {
|
|
231
|
+
reservation.release();
|
|
232
|
+
throw error;
|
|
188
233
|
});
|
|
189
234
|
}
|
|
190
235
|
|
|
@@ -215,6 +260,7 @@ async function executeRestore(opts: {
|
|
|
215
260
|
...(restoreTest ? { restoreTest } : {}),
|
|
216
261
|
});
|
|
217
262
|
} catch (err) {
|
|
263
|
+
if (err instanceof StorageWorkerAdmissionBusyError) return busyRestoreResult();
|
|
218
264
|
return restoreResultFromWorkerRejection(err, opts.trashId);
|
|
219
265
|
}
|
|
220
266
|
}
|
|
@@ -46,6 +46,12 @@ self.onmessage = async (event: MessageEvent<unknown>) => {
|
|
|
46
46
|
requestId,
|
|
47
47
|
message: err instanceof Error ? err.message : "worker_failed",
|
|
48
48
|
});
|
|
49
|
+
} finally {
|
|
50
|
+
// Close from inside the worker so the thread begins exiting before the
|
|
51
|
+
// parent’s terminate() races isolate realm reclaim on Windows.
|
|
52
|
+
try {
|
|
53
|
+
(self as unknown as { close?: () => void }).close?.();
|
|
54
|
+
} catch { /* already closing */ }
|
|
49
55
|
}
|
|
50
56
|
};
|
|
51
57
|
|