fix(types,docs): clear the 5 typecheck errors and the fabricated env vars on the base

Third pass over the base-reds, from the 2026-08-06T22:51Z verdict on #9298 —
it reported "Typecheck (core)" with only the FIRST error; there are five, all on
the pure tip 9995bc4893. Two are real production defects.

**Real bugs**

- open-sse/services/compression/engines/ccr/index.ts:295 called
  enforceGlobalBudget(entry.bytes) against an (owner, bytes) signature. The
  `bytes` argument arrived undefined, so `ccrTotalBytes + undefined` is NaN,
  `NaN > MAX` is false (the eviction loop exits immediately) and `NaN <= MAX` is
  false (the re-admit is refused). The #9061 durable tier therefore NEVER
  repopulated its in-memory map: every retrieve after a restart or an eviction
  re-read from SQLite forever, and evictions could not prefer the owning
  principal. Fixed and pinned by a new case in
  tests/unit/ccr-durable-store-9061.test.ts (11/11) — verified failing against
  the buggy call and passing against the fix.
- open-sse/services/combo/fusionPanel.ts:54 read `step.model` after #8894
  widened ComboStep with ComboProviderWildcardStep (which carries modelPattern,
  not model), so a wildcard step in a fusion panel pushed `undefined` onto the
  panel. Now resolved through getComboModelString(), which already handles every
  step shape and returns null for the ones without a concrete model id.

**Type-only**

- accountSemaphore.ts:203 — isBypassed() returns a plain boolean and cannot
  narrow `number | null` (an `x is null | undefined` predicate would be unsound:
  0 bypasses too). Added resolveActiveCap(), the narrowing companion isBypassed
  is now defined in terms of; the acquire path uses the narrowed value.
- comboStructure.ts:140 — same #8894 widening: `prompt` only exists on a model
  step, so it is now read under a kind check.
- firecrawlQuotaFetcher.ts:136 — the function returns full FirecrawlQuota
  objects but was annotated Promise<QuotaInfo | null>, which made the
  custom-base literal an excess-property error. Widened to the accurate type
  (FirecrawlQuota extends QuotaInfo, so callers are unaffected).

**Fabricated docs (the "Docs sync + fabricated-docs (strict)" HARD failure)**

docs/ops/VM_DEPLOYMENT_GUIDE.md recommended OMNIROUTE_MAX_POOL_SIZE and
OMNIROUTE_DB_POOL_SIZE (#9471). Neither is read anywhere in the codebase.
Replaced with the two knobs that do exist and are already documented in
ENVIRONMENT.md: OMNIROUTE_MEMORY_MB and OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT.

typecheck:core 5 errors -> 0. check:fabricated-docs + check:env-doc-sync OK.
accountSemaphore 6/6, ccr-durable-store 11/11, ccr-protocol 9/9,
combo-fusion-strategy 10/10, combo-fusion-comboref 5/5, combo-fusion-warn 4/4,
firecrawl-executor 7/7, executor-firecrawl-fetch 4/4.

Refs #9298
This commit is contained in:
diegosouzapw
2026-08-07 04:34:58 -03:00
parent b80987e4d4
commit db2c3a4753
7 changed files with 63 additions and 10 deletions

View File

@@ -429,6 +429,7 @@ For deployments on small VPS instances (1 GB RAM or less):
- **Disable background services** — set `OMNIROUTE_DISABLE_BACKGROUND_SERVICES=1` to skip scheduler, MCP server, and periodic maintenance tasks. See `docs/reference/ENVIRONMENT.md`.
- **Use SQLite WAL mode** — enabled by default, reduces peak memory during concurrent reads.
- **Limit connection concurrency** — reduce `OMNIROUTE_MAX_POOL_SIZE` and `OMNIROUTE_DB_POOL_SIZE` in your environment.
- **Cap the V8 heap** — set `OMNIROUTE_MEMORY_MB` (e.g. `512`) so the runtime does not calibrate a ceiling larger than the VM. See `docs/reference/ENVIRONMENT.md`.
- **Limit concurrent heavy requests** — lower `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT` (default `1`); excess requests get a retryable `503` with `Retry-After` instead of competing for memory.
- **Avoid `next build` on the VPS** — build locally and deploy the standalone output (`.next/standalone/`).
- **Monitor with `top` / `free -m`** — OmniRoute typically uses 200-400 MB RSS at idle on a 1 GB VM.

View File

@@ -54,8 +54,21 @@ export function buildAccountSemaphoreKey({
return `${String(provider)}:${String(accountKey)}`;
}
/**
* Effective positive cap, or null when the semaphore is bypassed (unset/<=0).
*
* Narrowing companion of {@link isBypassed}: that one returns a plain boolean, so
* TypeScript cannot narrow `number | null` to `number` in its else-branch (a
* `x is null | undefined` predicate would be unsound — 0 bypasses too). Callers
* that need the VALUE after the guard go through here instead of casting.
*/
function resolveActiveCap(maxConcurrency?: number | null): number | null {
if (maxConcurrency == null || maxConcurrency <= 0) return null;
return maxConcurrency;
}
function isBypassed(maxConcurrency?: number | null): boolean {
return maxConcurrency == null || maxConcurrency <= 0;
return resolveActiveCap(maxConcurrency) === null;
}
function createNoopReleaseFn(): () => void {
@@ -192,7 +205,8 @@ export function acquire(
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
}: AcquireAccountSemaphoreOptions = {}
): Promise<() => void> {
if (isBypassed(maxConcurrency)) {
const activeCap = resolveActiveCap(maxConcurrency);
if (activeCap === null) {
return Promise.resolve(createNoopReleaseFn());
}
@@ -200,7 +214,7 @@ export function acquire(
return Promise.reject(makeAbortError(signal));
}
const gate = ensureGate(semaphoreKey, maxConcurrency);
const gate = ensureGate(semaphoreKey, activeCap);
clearCleanupTimer(gate);
if (gate.running < gate.maxConcurrency && !isBlocked(gate)) {

View File

@@ -137,7 +137,9 @@ function normalizeRuntimeStep(
: {}),
weight,
label,
prompt: step.prompt || null,
// `prompt` is a per-step pipeline input and only exists on a model step —
// #8894 widened the union with ComboProviderWildcardStep, which has no prompt.
prompt: (step.kind === "model" ? step.prompt : null) || null,
} satisfies ResolvedComboTarget;
}

View File

@@ -10,7 +10,7 @@
* literal `auto/*` string panel member already behaves via the single-
* dispatch safety net in src/sse/handlers/chat.ts.
*/
import { normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { executeComboRefUnit } from "./runtimeUnits.ts";
import type {
ComboCollectionLike,
@@ -51,7 +51,11 @@ export function extractFusionPanelSpec(
panel.push(step.comboName);
return;
}
panel.push(step.model);
// #8894 widened ComboStep with ComboProviderWildcardStep, which carries a
// modelPattern instead of a model. getComboModelString() already resolves any
// step shape (and returns null for the ones with no concrete model id).
const modelStr = getComboModelString(step);
if (modelStr) panel.push(modelStr);
});
return { panel, comboRefUnits };
}

View File

@@ -292,7 +292,8 @@ function rehydrateEntry(hash: string, principalId: string, now: number): CcrEntr
// Re-admit through the same budgets a fresh store would face. If the block no longer
// fits, it stays on disk and is served straight from the row instead of being cached.
if (enforcePrincipalBudget(entry.principalId, entry.bytes) && enforceGlobalBudget(entry.bytes)) {
const { principalId: owner, bytes } = entry;
if (enforcePrincipalBudget(owner, bytes) && enforceGlobalBudget(owner, bytes)) {
const key = buildStoreKey(hash, principalId === ANON ? undefined : principalId);
ccrStore.set(key, entry);
ccrTotalBytes += entry.bytes;

View File

@@ -110,7 +110,8 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
return envBase.replace(/\/+$/, "");
}
const providerData = toRecord(connection?.providerSpecificData);
const connBase = typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
const connBase =
typeof connection?.baseUrl === "string" ? connection.baseUrl : providerData?.baseUrl;
if (typeof connBase === "string" && connBase.trim() && !connBase.includes("api.firecrawl.dev")) {
return connBase.trim().replace(/\/+$/, "");
}
@@ -120,7 +121,11 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
export async function fetchFirecrawlQuota(
connectionId: string,
connection?: Record<string, unknown>
): Promise<QuotaInfo | null> {
// FirecrawlQuota, not the base QuotaInfo: every return here is a full credit
// breakdown (remainingCredits / planCredits / extraCreditsInferred / overPlan),
// and the narrower annotation made the custom-base literal below an excess-
// property error. FirecrawlQuota extends QuotaInfo, so callers are unaffected.
): Promise<FirecrawlQuota | null> {
const cached = quotaCache.get(connectionId);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
return cached.quota;

View File

@@ -119,6 +119,32 @@ describe("CCR engine survives losing its in-memory map (#9061)", () => {
);
});
it("re-admits the disk row into the fresh map (the enforceGlobalBudget arity bug)", async () => {
// The restart path re-admits a disk-served block through the same budgets a fresh
// store would face. #9061 called enforceGlobalBudget(entry.bytes) with ONE argument
// against a (owner, bytes) signature: `bytes` arrived undefined, `ccrTotalBytes +
// undefined` is NaN, and `NaN <= MAX` is false — so the re-admit never happened and
// the map stayed empty, re-reading from disk on every single retrieve. Typecheck
// caught the arity; this pins the observable behaviour.
const text = "z".repeat(2_000);
const stored = ccr.tryStoreBlock(text, "principal-readmit");
assert.equal(stored.stored, true);
await ccr.flushCcrDurableWrites();
const restarted = await import(`${ccrPath}?restart=9061-readmit`);
assert.equal(
restarted.getCcrStoreStats("principal-readmit").entries,
0,
"a fresh instance starts with an empty map"
);
assert.equal(restarted.retrieveBlock(stored.hash, "principal-readmit"), text);
assert.equal(
restarted.getCcrStoreStats("principal-readmit").entries,
1,
"the disk-served block must be re-admitted into the map, not re-read every time"
);
});
it("keeps the principal boundary across the restart", async () => {
const text = "y".repeat(2_000);
const stored = ccr.tryStoreBlock(text, "principal-a");