diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c6fbc6e3..2088779ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ ### πŸ› Bug Fixes +- **fix(startup):** best-effort self-heal for a corrupted Turbopack dev cache on Windows ([#6289](https://github.com/diegosouzapw/OmniRoute/issues/6289)). On Windows, `pnpm dev` can fail at startup when Turbopack `mmap`s a persistent-cache SST file and the OS refuses the mapping (`os error 1455` β€” "paging file too small"), which Turbopack surfaces as a misleading `Module not found: Can't resolve '@/shared/utils/machine'`. This is a **known upstream Turbopack cache-corruption bug β€” not our code**. The dev launcher (`scripts/dev/run-next.mjs`) now wraps `nextApp.prepare()` and, when it rejects with that signature (`isTurbopackCacheCorruption` in the new `scripts/dev/turbopackCacheHeal.mjs`), purges `.build/next/**/cache/turbopack` and retries **once** with a clear log. **Caveat β€” best-effort only:** the corruption often surfaces as a runtime overlay rather than a `prepare()` rejection, so this cannot always intercept it; the reliable remedy remains manually deleting the Turbopack cache dir. Regression guard: `tests/unit/turbopack-cache-heal-6289.test.ts`. (thanks @chirag127) + - **fix(providers):** qodercli PAT auth no longer fails with `spawn qodercli ENOENT` on Windows ([#6263](https://github.com/diegosouzapw/OmniRoute/issues/6263)) β€” `spawnQoderCli` spawned the bare `qodercli` name with `shell:false` and an unenriched env, so the npm `.cmd` wrapper under `%APPDATA%\npm` (a user-PATH directory) was never resolved. It now resolves the absolute `.cmd`/`.exe` path through the existing `getCliRuntimeStatus("qoder")` resolver in `src/shared/services/cliRuntime.ts` (memoized), spawns with `shell` when the target is a `.cmd`/`.bat`, and uses the cliRuntime-enriched env (PATH + PATHEXT + APPDATA); the ENOENT error now lists the searched paths plus the `CLI_QODER_BIN` override. End-to-end spawn on a real Windows host is host-only (Hard Rule #18); the path-resolution logic is unit-tested. Regression guard: `tests/unit/qodercli-windows-resolve-6263.test.ts`. (thanks @chirag127) - **fix(sse):** the reasoning-token buffer no longer inflates **probe-sized `max_tokens`** ([#6274](https://github.com/diegosouzapw/OmniRoute/issues/6274)) β€” Claude Code's `/model` capability check sends `max_tokens: 1`, but for a thinking-capable model with a large output cap (e.g. `glm-5.2`) the #3587 headroom heuristic (`max(current + 1000, ceil(current * 1.5))`) rewrote it to `1001` and forwarded that upstream, wasting tokens on a request that was never a genuine reasoning budget. `resolveReasoningBufferedMaxTokens()` (`open-sse/services/reasoningTokenBuffer.ts`) now short-circuits and returns the caller's value verbatim when it is below the new `REASONING_BUFFER_MIN_TRIGGER` (256) threshold β€” a tiny explicit limit is a probe, not a reasoning request. Real budgets still receive the #3587 headroom unchanged, and the guard runs after the existing capability checks so unknown / non-reasoning models keep returning `null`. Regression guard: `tests/unit/reasoning-token-buffer-6274.test.ts`. (thanks @brightfiscalband) diff --git a/scripts/dev/run-next.mjs b/scripts/dev/run-next.mjs index 38f914d2e3..4a402650a8 100644 --- a/scripts/dev/run-next.mjs +++ b/scripts/dev/run-next.mjs @@ -11,6 +11,10 @@ import { createResponsesWsProxy } from "./responses-ws-proxy.mjs"; import { ensurePeerStampToken, stampPeerIp } from "./peer-stamp.mjs"; import methodGuard from "./http-method-guard.cjs"; import { ensureNativeSqlite } from "./ensure-native-sqlite.mjs"; +import { + isTurbopackCacheCorruption, + purgeAllTurbopackCaches, +} from "./turbopackCacheHeal.mjs"; import { randomUUID } from "node:crypto"; const { maybeHandleDisallowedMethod } = methodGuard; @@ -88,17 +92,46 @@ ensurePeerStampToken(); if (!useTurbopack) { delete process.env.TURBOPACK; } -const nextApp = next({ - dev, - dir: process.cwd(), - hostname, - port: dashboardPort, - turbopack: useTurbopack, - webpack: !useTurbopack, -}); +function createNextApp() { + return next({ + dev, + dir: process.cwd(), + hostname, + port: dashboardPort, + turbopack: useTurbopack, + webpack: !useTurbopack, + }); +} + +let nextApp = createNextApp(); + +// Best-effort self-heal for a corrupted Turbopack persistent dev cache (#6289): +// on Windows an mmap of an SST cache file can fail ("os error 1455" / paging +// file too small), which Turbopack surfaces as a misleading module-resolve +// error. This is an UPSTREAM Turbopack cache-corruption bug β€” not our code. +// When `prepare()` rejects with that signature we purge the cache and retry +// ONCE. Caveat: the failure often surfaces as a runtime overlay rather than a +// `prepare()` rejection, so this cannot always intercept it β€” the reliable +// remedy remains manually deleting `.build/next/**/cache/turbopack`. +async function prepareWithHeal() { + try { + await nextApp.prepare(); + } catch (error) { + const detail = error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error); + if (!useTurbopack || !isTurbopackCacheCorruption(detail)) throw error; + console.warn( + "[Next] Turbopack dev cache looks corrupted (Windows mmap / os error 1455 β€” known upstream bug). Purging and retrying once…" + ); + const removed = purgeAllTurbopackCaches(); + for (const dir of removed) console.warn(`[Next] purged Turbopack cache: ${dir}`); + nextApp = createNextApp(); + await nextApp.prepare(); + console.warn("[Next] Turbopack dev cache purged; startup retry succeeded."); + } +} async function start() { - await nextApp.prepare(); + await prepareWithHeal(); const requestHandler = nextApp.getRequestHandler(); const upgradeHandler = nextApp.getUpgradeHandler(); diff --git a/scripts/dev/turbopackCacheHeal.mjs b/scripts/dev/turbopackCacheHeal.mjs new file mode 100644 index 0000000000..7b6c2a110f --- /dev/null +++ b/scripts/dev/turbopackCacheHeal.mjs @@ -0,0 +1,74 @@ +// Best-effort self-heal for a corrupted Turbopack persistent dev cache. +// +// Context (#6289): on Windows, `pnpm dev` can fail at startup when Turbopack +// mmaps a persistent-cache SST file and the OS refuses the mapping +// ("os error 1455" β€” "paging file too small"). Turbopack then surfaces a +// misleading `Module not found: Can't resolve '@/shared/utils/machine'`. +// This is a known UPSTREAM Turbopack cache-corruption bug β€” NOT our code. +// The reliable remedy is deleting the Turbopack cache dir; this module lets +// the dev launcher attempt that automatically once before giving up. +// +// Pure + side-effect-isolated so it can be unit-tested without booting Next. + +import fs from "node:fs"; +import path from "node:path"; + +// Signature of the corrupted-cache failure. Kept intentionally broad because +// the same corruption surfaces through several messages (the raw mmap/SST +// error, the Windows paging-file error code, and the misleading module-resolve +// error emitted by Turbopack's "restore task data" step). +const CORRUPTION_SIGNATURE = /restore task data|mmap .*SST|os error 1455|paging file/i; + +/** + * True when an error message looks like a corrupted Turbopack persistent cache. + * @param {unknown} message + * @returns {boolean} + */ +export function isTurbopackCacheCorruption(message) { + if (message == null) return false; + return CORRUPTION_SIGNATURE.test(String(message)); +} + +/** + * Candidate Turbopack cache directories for a given Next dist dir. Next has + * placed the persistent cache under both `/cache/turbopack` and + * `/dev/cache/turbopack` across versions, so purge both. + * @param {string} [distDir] + * @param {string} [cwd] + * @returns {string[]} + */ +export function turbopackCacheDirs( + distDir = process.env.NEXT_DIST_DIR || ".build/next", + cwd = process.cwd() +) { + const base = path.isAbsolute(distDir) ? distDir : path.join(cwd, distDir); + return [ + path.join(base, "cache", "turbopack"), + path.join(base, "dev", "cache", "turbopack"), + ]; +} + +/** + * Recursively remove a single Turbopack cache directory. + * @param {string} dir + * @returns {boolean} true if the directory existed and was removed + */ +export function purgeTurbopackCache(dir) { + if (!dir || !fs.existsSync(dir)) return false; + fs.rmSync(dir, { recursive: true, force: true }); + return true; +} + +/** + * Purge every candidate Turbopack cache directory for the given dist dir. + * @param {string} [distDir] + * @param {string} [cwd] + * @returns {string[]} the directories that existed and were removed + */ +export function purgeAllTurbopackCaches(distDir, cwd) { + const removed = []; + for (const dir of turbopackCacheDirs(distDir, cwd)) { + if (purgeTurbopackCache(dir)) removed.push(dir); + } + return removed; +} diff --git a/tests/unit/turbopack-cache-heal-6289.test.ts b/tests/unit/turbopack-cache-heal-6289.test.ts new file mode 100644 index 0000000000..0a30882fbe --- /dev/null +++ b/tests/unit/turbopack-cache-heal-6289.test.ts @@ -0,0 +1,76 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +import { + isTurbopackCacheCorruption, + purgeTurbopackCache, + turbopackCacheDirs, +} from "../../scripts/dev/turbopackCacheHeal.mjs"; + +// Regression for #6289: on Windows, `pnpm dev` fails when Turbopack mmaps a +// corrupted persistent-cache SST file ("os error 1455" / paging file too +// small), surfaced as a misleading `Module not found: Can't resolve +// '@/shared/utils/machine'`. The launcher (scripts/dev/run-next.mjs) uses these +// pure helpers to detect that signature and purge the Turbopack dev cache +// before retrying once. This test covers ONLY the pure logic β€” reproducing the +// real mmap failure is host-only and out of scope. + +test("isTurbopackCacheCorruption matches the corrupted-cache signatures", () => { + const matching = [ + "failed to mmap SST file .build/next/cache/turbopack/data.sst", + "Backend error: Storage restore task data failed", + "memory map failed: os error 1455", + "The paging file is too small for this operation to complete. (os error 1455)", + "Module not found: os error 1455 while trying to restore task data", + ]; + for (const msg of matching) { + assert.equal(isTurbopackCacheCorruption(msg), true, `should match: ${msg}`); + } +}); + +test("isTurbopackCacheCorruption returns false for unrelated errors", () => { + const unrelated = [ + "Module not found: Can't resolve '@/shared/utils/machine'", + "EADDRINUSE: address already in use :::20128", + "TypeError: Cannot read properties of undefined", + "", + null, + undefined, + ]; + for (const msg of unrelated) { + assert.equal(isTurbopackCacheCorruption(msg), false, `should NOT match: ${String(msg)}`); + } +}); + +test("turbopackCacheDirs resolves both known cache layouts under the dist dir", () => { + const cwd = "/tmp/omniroute-test"; + const dirs = turbopackCacheDirs(".build/next", cwd); + assert.deepEqual(dirs, [ + path.join(cwd, ".build/next", "cache", "turbopack"), + path.join(cwd, ".build/next", "dev", "cache", "turbopack"), + ]); +}); + +test("purgeTurbopackCache removes an existing cache/turbopack dir", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tp-cache-heal-")); + const cacheDir = path.join(tmp, "cache", "turbopack"); + fs.mkdirSync(cacheDir, { recursive: true }); + fs.writeFileSync(path.join(cacheDir, "data.sst"), "corrupt"); + assert.equal(fs.existsSync(cacheDir), true); + + const removed = purgeTurbopackCache(cacheDir); + + assert.equal(removed, true); + assert.equal(fs.existsSync(cacheDir), false); + + fs.rmSync(tmp, { recursive: true, force: true }); +}); + +test("purgeTurbopackCache is a no-op (returns false) when the dir is absent", () => { + const missing = path.join(os.tmpdir(), "tp-cache-heal-missing-does-not-exist"); + assert.equal(fs.existsSync(missing), false); + assert.equal(purgeTurbopackCache(missing), false); +});