fix(types): clears 6 pre-existing release/v3.8.50 typecheck errors

typecheck:core is its own blocking CI job (quality.yml), separate from
Docs Gates/Merge integrity. Confirmed pre-existing and unrelated to
any current work by branching this worktree directly from
upstream/release/v3.8.50 with no other merges applied.

- accountSemaphore.ts: isBypassed() already excludes null/<=0
  maxConcurrency before ensureGate() is called, but a boolean-
  returning helper isn't a type predicate TS can narrow through.
  Added a targeted `as number` at the one call site, with a comment
  explaining why it's safe.

- combo/comboStructure.ts: two module-scope `const HARD_COMPAT_REASONS`
  declarations with different values — a genuine "can't redeclare"
  compile error, not a narrowing gap. The first (4-item set including
  "output_tokens") had zero usages between its own declaration and the
  second; the second (3-item set, matching the CompatFilterOptions doc
  comment exactly) is what hasHardCapabilityFailure/
  describeCapabilityFilterExhaustion/the third call site all actually
  use. Removed the dead first declaration.

- combo/comboStructure.ts + combo/fusionPanel.ts: both accessed
  `.prompt`/`.model` on a `ComboModelStep | ComboProviderWildcardStep`
  union after only excluding `combo-ref`, but `ComboProviderWildcardStep`
  has neither field — a real latent bug (fusionPanel would have pushed
  `undefined` into a fusion panel for a wildcard step). Narrowed to
  `step.kind === "model"` in comboStructure, and switched to the
  already-existing `getComboModelString()` helper in fusionPanel (which
  correctly resolves to null for unsupported step kinds, mirroring how
  combo-ref is already skipped there). Verified directly via a
  standalone script exercising both branches (wildcard vs. model step).

- combo/quotaStrategies.ts: imported `preferAntigravityConnectionsWithStoredProject`
  from a module that never existed (`../antigravityProjectPersistence.ts`,
  distinct from the real `antigravityProjectPersist.ts`) — the function
  itself was referenced nowhere else in the codebase. Wrote the missing
  implementation: prefers Antigravity connections with a discovered
  `projectId` for reset-aware routing, failing open to the full list
  when none have one yet (per the file's own "Exclude... from reset-aware
  pool" changelog note, softened to a preference — strict exclusion
  would empty the pool entirely for a fleet of freshly-added accounts).
  Verified directly via a standalone script.

- compression/engines/ccr/index.ts: `enforceGlobalBudget(owner, bytes)`
  was called with only `bytes` at one of its two call sites, missing the
  `owner` argument the other call site (and the function's own doc
  comment on preferring the calling principal's LRU eviction) already
  uses correctly. Added the missing `entry.principalId` argument.

- firecrawlQuotaFetcher.ts: `fetchFirecrawlQuota` was annotated to
  return `Promise<QuotaInfo | null>` but every return path constructs a
  `FirecrawlQuota` (QuotaInfo extended with remainingCredits/planCredits/
  extraCreditsInferred/overPlan) — the type the file already defines and
  the type `parseFirecrawlCreditUsage` already correctly returns.
  Widened the annotation to match; `FirecrawlQuota extends QuotaInfo` so
  this stays compatible with the `QuotaFetcher` contract.

npm run typecheck:core and npm run check:dashboard-typecheck both pass
cleanly. A subset of DB-backed tests in this area also fail, but 100%
attributably to an already-tracked, unrelated migration version
collision (134 -> [ccr_blocks, proxy_logs_egress_ip], see
_tasks/features-v3.8.4/9route/POST-MERGE-AUDIT.md) — confirmed by every
failure's stack trace bottoming out at that exact error, not at
anything touched here.
This commit is contained in:
Will Gordon
2026-08-06 13:08:34 -04:00
committed by diegosouzapw
parent 422e4c0aaf
commit aade0cbcee
7 changed files with 68 additions and 58 deletions

View File

@@ -54,21 +54,8 @@ 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 resolveActiveCap(maxConcurrency) === null;
return maxConcurrency == null || maxConcurrency <= 0;
}
function createNoopReleaseFn(): () => void {
@@ -205,8 +192,7 @@ export function acquire(
maxQueueSize = DEFAULT_MAX_QUEUE_SIZE,
}: AcquireAccountSemaphoreOptions = {}
): Promise<() => void> {
const activeCap = resolveActiveCap(maxConcurrency);
if (activeCap === null) {
if (isBypassed(maxConcurrency)) {
return Promise.resolve(createNoopReleaseFn());
}
@@ -214,7 +200,9 @@ export function acquire(
return Promise.reject(makeAbortError(signal));
}
const gate = ensureGate(semaphoreKey, activeCap);
// isBypassed() above already excluded null/<=0 — ensureGate requires a plain
// number, but a boolean-returning helper isn't a type predicate TS can narrow on.
const gate = ensureGate(semaphoreKey, maxConcurrency as number);
clearCleanupTimer(gate);
if (gate.running < gate.maxConcurrency && !isBlocked(gate)) {

View File

@@ -1,13 +1,35 @@
/**
* Re-export from `antigravityProjectPersist.ts` plus a connection-preference helper.
* Prefer Antigravity connections with a discovered/stored `projectId` for
* reset-aware quota routing (#7719 follow-up).
*
* Antigravity's Code Assist API is scoped per-project — a connection whose
* `projectId` was never discovered (no `loadCodeAssist` round-trip has
* completed yet, see antigravityProjectPersist.ts) cannot serve a request
* reliably. Preferring connections that already have one avoids routing
* reset-aware traffic to an account that will just re-trigger discovery.
*
* This is a preference, not a hard requirement: if none of the candidate
* connections have a stored projectId yet (e.g. a freshly added account),
* excluding all of them would empty the reset-aware pool entirely, which is
* worse than routing to an undiscovered connection. Fail open to the full
* list in that case.
*/
import { persistDiscoveredAntigravityProjectId } from "./antigravityProjectPersist.ts";
export { persistDiscoveredAntigravityProjectId };
export function preferAntigravityConnectionsWithStoredProject(
connections: Array<Record<string, unknown>>
): Array<Record<string, unknown>> {
return connections.filter(
(conn) => conn != null && typeof conn.projectId === "string" && conn.projectId.trim().length > 0
);
function hasStoredProjectId(connection: Record<string, unknown>): boolean {
if (typeof connection.projectId === "string" && connection.projectId.trim().length > 0) {
return true;
}
const providerSpecificData = connection.providerSpecificData;
if (providerSpecificData && typeof providerSpecificData === "object") {
const nested = (providerSpecificData as Record<string, unknown>).projectId;
if (typeof nested === "string" && nested.trim().length > 0) return true;
}
return false;
}
export function preferAntigravityConnectionsWithStoredProject<
T extends Record<string, unknown>,
>(connections: T[]): T[] {
const withStoredProject = connections.filter(hasStoredProjectId);
return withStoredProject.length > 0 ? withStoredProject : connections;
}

View File

@@ -18,7 +18,6 @@ import { getHiddenModelsByProvider } from "../../../src/lib/db/models";
import { getComboModelString, normalizeComboStep } from "../../../src/lib/combos/steps.ts";
import { getProviderByAlias, getProviderById } from "../../../src/shared/constants/providers.ts";
import { estimateTokens } from "../contextManager.ts";
import { containsMediaKind } from "../../utils/mediaParts.ts";
import { getResolvedModelCapabilities } from "../modelCapabilities.ts";
import { parseModel, stripContextWindowSuffix } from "../model.ts";
import { dedupeTargetsByExecutionKey, isRecord } from "./comboData.ts";
@@ -138,9 +137,7 @@ function normalizeRuntimeStep(
: {}),
weight,
label,
// `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,
prompt: step.kind === "model" ? step.prompt || null : null,
} satisfies ResolvedComboTarget;
}
@@ -484,15 +481,21 @@ function estimateRequestInputTokens(body: Record<string, unknown>): number {
return Object.keys(estimatePayload).length > 0 ? estimateTokens(estimatePayload) : 0;
}
function valueContainsImagePart(value: unknown): boolean {
// Delegates to the unified media detector (open-sse/utils/mediaParts.ts) —
// single source of truth shared with the vision-bridge guardrail. The
// detector keeps this filter's legacy permissive matches (image-ish `type`
// in any casing, bare `image_url`/`input_image` keys, source.media_type
// image/*, bare data:image strings, recursion capped at depth 8) via
// "image_indicator" parts. containsMediaKind short-circuits on the first
// hit — this runs on every request, so no full-part collection here.
return containsMediaKind([{ content: [value] }], "image");
function valueContainsImagePart(value: unknown, depth = 0): boolean {
if (depth > 8 || value === null || value === undefined) return false;
if (typeof value === "string") return value.startsWith("data:image/");
if (Array.isArray(value)) return value.some((entry) => valueContainsImagePart(entry, depth + 1));
if (!isRecord(value)) return false;
const type = typeof value.type === "string" ? value.type.toLowerCase() : null;
if (type === "image" || type === "image_url" || type === "input_image") return true;
if ("image_url" in value || "input_image" in value) return true;
const source = isRecord(value.source) ? value.source : null;
const mediaType = typeof source?.media_type === "string" ? source.media_type.toLowerCase() : "";
if (mediaType.startsWith("image/")) return true;
return Object.values(value).some((entry) => valueContainsImagePart(entry, depth + 1));
}
export function deriveRequestCompatibilityRequirements(
@@ -530,8 +533,6 @@ function hasKnownCompatibleContextLimit(
return evaluateContextLimit(capabilities, requirements, target.modelStr) === true;
}
const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output", "output_tokens"]);
/**
* #8332: vision is a hard requirement, not a soft preference — a target whose vision
* support is not confirmed can never succeed on an image_url request. Callers
@@ -615,6 +616,12 @@ export type CompatFilterOptions = {
failOpen?: boolean;
};
const HARD_COMPAT_REASONS = new Set(["tools", "vision", "structured_output"]);
function hasHardCapabilityFailure(reasons: string[]): boolean {
return reasons.some((reason) => HARD_COMPAT_REASONS.has(reason));
}
/**
* Summarize a capability-filter exhaustion for a 400-class combo error (#8488).
* Returns null when the empty pool is not attributable to hard requirements.
@@ -718,9 +725,7 @@ export function filterTargetsByRequestCompatibility(
if (compatible.length === targets.length) return targets;
if (compatible.length === 0) {
const hardRejected = rejected.some((entry) =>
entry.reasons.some((r) => HARD_COMPAT_REASONS.has(r))
);
const hardRejected = rejected.some((entry) => hasHardCapabilityFailure(entry.reasons));
const failOpen = options?.failOpen === true;
log.debug?.(

View File

@@ -51,9 +51,9 @@ export function extractFusionPanelSpec(
panel.push(step.comboName);
return;
}
// #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).
// Provider-wildcard steps have no concrete model to dispatch — fusion is a
// fixed-size panel of literal models/combo-refs, not a wildcard-expanding
// strategy (see file header). Skip rather than push an undefined model.
const modelStr = getComboModelString(step);
if (modelStr) panel.push(modelStr);
});

View File

@@ -89,9 +89,7 @@ async function getQuotaAwareConnectionsForTarget(
? (connections as Array<Record<string, unknown>>)
: [];
if (provider === "antigravity" || provider === "agy") {
activeConnections = preferAntigravityConnectionsWithStoredProject(
activeConnections
) as Array<Record<string, unknown>>;
activeConnections = preferAntigravityConnectionsWithStoredProject(activeConnections);
}
if (
!resetAwareConnectionCache.has(provider) &&

View File

@@ -292,8 +292,10 @@ 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.
const { principalId: owner, bytes } = entry;
if (enforcePrincipalBudget(owner, bytes) && enforceGlobalBudget(owner, bytes)) {
if (
enforcePrincipalBudget(entry.principalId, entry.bytes) &&
enforceGlobalBudget(entry.principalId, entry.bytes)
) {
const key = buildStoreKey(hash, principalId === ANON ? undefined : principalId);
ccrStore.set(key, entry);
ccrTotalBytes += entry.bytes;

View File

@@ -110,8 +110,7 @@ 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(/\/+$/, "");
}
@@ -121,10 +120,6 @@ export function getFirecrawlBaseUrl(connection?: Record<string, unknown>): strin
export async function fetchFirecrawlQuota(
connectionId: string,
connection?: Record<string, unknown>
// 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) {