Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"_rebaseline_2026_06_24_combo_cooldown_wait_quota_share": "Feature quota-share combo cooldown-aware retry (Variante A) own growth: open-sse/services/combo.ts 3225->3293 (+68 = the cooldown-wait wrap inside handleComboChat's quota-share path. The existing setTry loop body is LEFT at its original indentation: instead of an outer `while (true)` (which would re-indent ~1600 lines and bloat the review), the setTry loop is hoisted into a small recursive closure `dispatchWithCooldownRetry`, and a wait+redispatch is a tail `return dispatchWithCooldownRetry()` — re-running ONLY the set loop (exactly the prior continue-to-top-of-set-loop semantics) while selection/shadow-routing/setup above stay untouched. At the 429 crystallization point the lock reason is resolved via getModelLockoutInfo, the decision via the new pure resolveComboCooldownWaitDecision, then await waitForCooldownAwareRetry (499 on abort), decrement the budget, recurse. Gated to strategy==='quota-share' && comboCooldownWait.enabled. `git diff` == `git diff -w` for combo.ts (zero re-indentation noise). The gating policy + reason resolution are extracted to the new pure leaf open-sse/services/combo/comboCooldownRetry.ts (<cap, unit-tested) to keep combo.ts thin; only the wait orchestration (one getModelLockoutInfo lookup + one await) stays at the chokepoint, not extractable. The quota_exhausted/auth/not-found exclusions defend against isRetryableModelLockoutReason (auth.ts:533) treating quota_exhausted as retryable (locked-until-midnight). Covered by tests/unit/combo-cooldown-retry.test.ts (pure helper, all branches) + tests/unit/combo-quota-share-cooldown-wait.test.ts (integration: rate_limit waits+recovers, quota_exhausted no-wait, abort=499, non-quota-share unchanged) + tests/unit/resilience-settings-combo-cooldown-wait.test.ts (settings round-trip/clamp). Also src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx 983->1098 (+115 = the ComboCooldownWaitCard exposing enabled/maxWaitMs/maxAttempts/budgetMs in Settings > Resilience, mirroring WaitForCooldownCard; wired through GET/PATCH in src/app/api/resilience/route.ts + comboCooldownWaitSettingsSchema in src/shared/validation/schemas/settings.ts; new UI labels use the t(key)||English-fallback pattern, en-only). Structural shrink of combo.ts + ResilienceTab tracked in #3501.",
"_rebaseline_2026_06_24_quota_share_concurrency_limit": "Feature FASE 2.1 (per-connection concurrency limit for quota-share combos) own growth. The quota-share gating in selectQuotaShareTarget is FAIL-OPEN (an at-cap connection is only deprioritized, never hard-blocked), so with a single-connection subscription-account pool concurrent requests still flood the account — empirically proven on the .15 deploy: 3 concurrent share-key calls to one minimax connection with max_concurrent=1 were all dispatched within 94ms. This adds a per-CONNECTION semaphore around the quota-share dispatch so excess concurrent requests WAIT in the queue instead of flooding (key = qsconn:<connectionId>, cap = the connection's max_concurrent; fail-open on a saturated queue/timeout to never worsen availability). open-sse/services/combo.ts 3306->3340 (+34 = the irreducible chokepoint wiring: the quotaShareConcurrencyEnabled gate, the acquire (lookupPositiveCap + acquireQuotaShareConcurrencySlot) around dispatchWithCooldownRetry, the release in the outer finally, and the updated cooldown-wait comment). ALL extractable logic lives in the new pure leaf open-sse/services/combo/quotaShareConcurrency.ts (<cap, unit-tested: key stability, no-cap/empty no-op, genuine serialization, fail-open). src/lib/resilience/settings.ts ->841 (+41 = QuotaShareConcurrencyLimitSettings interface + default {enabled:true} + normalizeQuotaShareConcurrencyLimitSettings + resolve/merge/legacy-fallback wiring; crossed the 800 new-file cap, mirrors the existing comboCooldownWait block). src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx 1098->1183 (+85 = QuotaShareConcurrencyLimitCard, a kill-switch toggle mirroring ComboCooldownWaitCard; wired through GET/PATCH in src/app/api/resilience/route.ts + quotaShareConcurrencyLimitSettingsSchema in src/shared/validation/schemas/settings.ts; t(key)||English-fallback, en-only). Covered by tests/unit/combo/quota-share-concurrency.test.ts + tests/unit/resilience-settings-quota-share-concurrency.test.ts. Structural shrink of combo.ts + ResilienceTab tracked in #3501.",
"_rebaseline_2026_06_23_4774_combo_legacy_strip": "PR #4774 (KooshaPari, #4382 round-trip) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4434->4456 (+22 = the client-side LEGACY_COMBO_RESILIENCE_KEYS Set gains queueTimeoutMs + the 12 v3.8.31-era removed keys (queueDepth/fallbackDelayMs/handoffProviders/maxComboDepth/manifestRouting/complexityAwareRouting/pipeline_enabled/pipelineConcurrency/shadowRouting/evalRouting/resetAwareEnabled/resetAwareWindow) with explanatory comments, mirroring the server-side strip list in src/app/api/combos/[id]/route.ts so the modal never re-introduces removed keys on Save). Functional strip list, not a movable block; combos/page.tsx structural shrink tracked in #3501. Covered by tests/unit/combo-config.test.ts (auto-promote + passthrough + legacy-key round-trip).",
"_rebaseline_2026_06_26_3368_cookie_dedup": "Issue #3368 PR6 own growth: src/lib/db/providers.ts 1063->1093 (+30 = the cookie-auth dedup branch in createProviderConnection — name-based upsert + credential-value match, mirroring the existing oauth/apikey dedup so bulk web-session import stops creating duplicate connections on re-import). The pure credential-key + JSON-parse helpers (webSessionCredentialKey, parseProviderSpecificData) were extracted to the new src/lib/db/webSessionDedup.ts (<cap, unit-tested); the remaining SQL queries + match loop are cohesive DB wiring next to the oauth/apikey branches, not extractable. Covered by tests/unit/db-provider-cookie-dedup-3368.test.ts + tests/unit/web-session-dedup-3368.test.ts. Structural shrink of providers.ts tracked in #3501.",
"_rebaseline_2026_06_21_4421_node_lookup": "Issue #4421 own growth: src/lib/db/providers.ts 1050->1063 (+13 = resolveProviderNodeForConnection at the existing provider-node lookup — resolves a connection node by exact id OR the bare derived type when unambiguous, + import). Pure selection logic in the new src/lib/db/providerNodeSelect.ts (<cap, unit-tested); cohesive DB wiring next to getProviderNodeById, not extractable. Covered by tests/unit/provider-node-select-4421.test.ts.",
"_rebaseline_2026_06_21_v3833_release_basered_fixes": "Release v3.8.33: 5 deterministic base-reds inherited from parallel-session merges (fast-gates PR→release não rodam test:unit completo) consertados no PR de release. src/sse/services/auth.ts 2279->2289 (+10): #4530 só fiou maxCooldownMs nos 3 sites de combo.ts; os 4 sites de markAccountUnavailable (per-model quota, grok-web 403, per-model 403, local 404) nunca passavam o cap → resolvo mlSettings uma vez e passo maxCooldownMs em todos. tests/unit/db-core-init.test.ts 864->867 (+3): comentário explicando o cap intencional busy_timeout 5s->2s do v3.8.32. Wiring necessário ao chokepoint de lockout; não extraível. Coberto por model-lockout-max-cooldown.test.ts + db-core-init.test.ts. (Demais base-reds — áudio mp3 #912/#913 dedup de handler em geminiHelper.ts, e closure #3578 src/models/ no package.json files — não cresceram arquivo congelado.)",
"_rebaseline_2026_06_21_v3833_cycle_open_latent_filesize": "Abertura do ciclo v3.8.33: 4 arquivos cresceram no ciclo v3.8.32 sem bump de baseline e o drift escapou do fast-path do release (check:file-size não roda nas fast-gates p/ release/*, só no PR→main full CI, e o crescimento veio de commits entre fca66c644 e o head do merge 912239f46 — ex. #4475 targetFormat). Medido em origin/main (idêntico, cherry-picks deste ciclo NÃO tocam estes 4): open-sse/services/usage.ts 3408->3414, src/lib/db/core.ts 1820->1825, src/lib/usage/providerLimits.ts 949->950, src/shared/constants/providers.ts 3242->3243. Reconcílio ao valor real de main p/ abrir o .33 verde; shrink estrutural rastreado em #3501.",
Expand Down Expand Up @@ -211,7 +212,7 @@
"src/lib/db/core.ts": 1825,
"src/lib/db/migrationRunner.ts": 1125,
"src/lib/db/models.ts": 1259,
"src/lib/db/providers.ts": 1063,
"src/lib/db/providers.ts": 1093,
"src/lib/db/proxies.ts": 1060,
"src/lib/db/settings.ts": 1155,
"src/lib/db/usageAnalytics.ts": 925,
Expand Down
19 changes: 19 additions & 0 deletions open-sse/mcp-server/tools/poolTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { z } from "zod";
import { PoolRegistry } from "../../services/sessionPool/poolRegistry.ts";
import { getWebSessionPoolHealth } from "../../services/webSessionPoolHealth.ts";
import { getBrowserPoolMetrics } from "../../services/browserPool.ts";

// ─── Input Schemas ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -140,6 +141,16 @@ export async function handlePoolHealth(
return report as unknown as Record<string, unknown>;
}

export const browserPoolStatusInput = z.object({});

/**
* Handle browser_pool_status tool (#3368 PR7): return the stealth browser
* pool's live status plus cumulative lifecycle telemetry.
*/
export async function handleBrowserPoolStatus(): Promise<Record<string, unknown>> {
return getBrowserPoolMetrics();
}

// ─── Tool Registry ─────────────────────────────────────────────────────────

export const poolTools = {
Expand Down Expand Up @@ -183,4 +194,12 @@ export const poolTools = {
inputSchema: poolHealthInput,
handler: (args: z.infer<typeof poolHealthInput>) => handlePoolHealth(args),
},
omniroute_browser_pool_status: {
name: "omniroute_browser_pool_status",
description:
"Returns the stealth browser pool's live status (enabled, active contexts, browser running, stealth available, idle age) plus cumulative lifecycle telemetry: browser launches/failures, context create/reuse/evict/release counts, context-create failures, and shutdowns with the last reason.",
scopes: ["read:health"],
inputSchema: browserPoolStatusInput,
handler: () => handleBrowserPoolStatus(),
},
};
26 changes: 19 additions & 7 deletions open-sse/services/browserPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,17 @@ export async function resolvePlaywrightProxy(
const p = await resolver(providerKey);
if (!p?.host) return undefined;
const scheme = p.type === "socks5" ? "socks5" : "http";
return {
// Build explicitly instead of a conditional object spread: the spread form
// widens username/password to `{}` under the LaunchOptions["proxy"] type,
// tripping typecheck once browserPool.ts is pulled into typecheck-core scope.
const proxy: NonNullable<import("playwright").LaunchOptions["proxy"]> = {
server: `${scheme}://${p.host}:${p.port}`,
...(p.username ? { username: p.username, password: p.password ?? "" } : {}),
};
if (p.username) {
proxy.username = String(p.username);
proxy.password = p.password == null ? "" : String(p.password);
}
return proxy;
} catch (err) {
console.warn("[BrowserPool] Failed to resolve proxy from DB:", err);
return undefined;
Expand Down Expand Up @@ -291,6 +298,14 @@ function parseCookieString(
}>;
}

// Clear a key from the pending-creation map once its promise settles, counting
// failures. Kept as a leaf helper so acquireBrowserContext stays under the
// function-length ceiling (#3368 PR7 metrics).
function settlePendingContext(key: string, failed: boolean): void {
if (failed) state.metrics.contextCreateFailures++;
state.pendingContexts.delete(key);
}

export async function acquireBrowserContext(
key: string,
options: BrowserPoolContextOptions
Expand Down Expand Up @@ -382,11 +397,8 @@ export async function acquireBrowserContext(

state.pendingContexts.set(key, createPromise);
createPromise
.then(() => state.pendingContexts.delete(key))
.catch(() => {
state.metrics.contextCreateFailures++;
state.pendingContexts.delete(key);
});
.then(() => settlePendingContext(key, false))
.catch(() => settlePendingContext(key, true));

return createPromise;
}
Expand Down
1 change: 1 addition & 0 deletions scripts/check/check-db-rules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const INTENTIONALLY_INTERNAL = new Set([
"stateReset", // db-internal: 3 callers dentro de src/lib/db/ (core, backup, apiKeys) para coordenação de reset
"stats", // intentionally-internal: src/app/api/settings/database/refresh-stats/route.ts
"tierConfig", // intentionally-internal: open-sse/services/tierResolver.ts (require() dinâmico)
"webSessionDedup", // db-internal: importado só por db/providers.ts (webSessionCredentialKey/parseProviderSpecificData — helpers puros de dedup de credencial web-session split do providers.ts, #3368 PR6)
]);

// Alias para retrocompatibilidade com os testes existentes que importam KNOWN_UNEXPORTED.
Expand Down
44 changes: 44 additions & 0 deletions src/lib/db/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { invalidateDbCache } from "./readCache";
import { normalizeProviderSpecificData } from "@/lib/providers/requestDefaults";
import { bumpProxyConfigGeneration } from "./settings";
import { webSessionCredentialKey, parseProviderSpecificData } from "./webSessionDedup";

type JsonRecord = Record<string, unknown>;

Expand Down Expand Up @@ -191,6 +192,42 @@ export async function getProviderConnectionById(id: string) {
);
}

// #3368 PR6 — dedup web-session cookie/token credentials on connection create.
// Re-importing the same session (e.g. via bulk web-session import) under a
// different or blank name must update the existing connection instead of
// inserting a duplicate, mirroring the apikey dedup (#3023). Extracted from
// createProviderConnection to keep that function below the complexity baseline.
// provider_specific_data is plaintext JSON, so the value is compared directly
// without decryption.
function findExistingCookieConnection(
db: DbLike,
provider: unknown,
name: unknown,
normalizedProviderSpecificData: unknown
): JsonRecord | null {
// 1) Name-based upsert for parity with the apikey path.
if (name) {
const byName =
(db
.prepare(
"SELECT * FROM provider_connections WHERE provider = ? AND auth_type = 'cookie' AND name = ?"
)
.get(provider, name) as JsonRecord | undefined) || null;
if (byName) return byName;
}
// 2) Credential-value dedup against existing cookie rows.
const newCredKey = webSessionCredentialKey(normalizedProviderSpecificData);
if (!newCredKey) return null;
const cookieRows = db
.prepare("SELECT * FROM provider_connections WHERE provider = ? AND auth_type = 'cookie'")
.all(provider) as JsonRecord[];
for (const row of cookieRows) {
const psd = parseProviderSpecificData(row.provider_specific_data);
if (psd && webSessionCredentialKey(psd) === newCredKey) return row;
}
return null;
}

export async function createProviderConnection(data: JsonRecord) {
const db = getDbInstance() as unknown as DbLike;
const now = new Date().toISOString();
Expand Down Expand Up @@ -268,6 +305,13 @@ export async function createProviderConnection(data: JsonRecord) {
}
}
}
} else if (data.authType === "cookie") {
existing = findExistingCookieConnection(
db,
data.provider,
data.name,
normalizedProviderSpecificData
);
}

if (existing) {
Expand Down
57 changes: 57 additions & 0 deletions src/lib/db/webSessionDedup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* db/webSessionDedup.ts — pure helpers for de-duplicating web-session
* (cookie/token) provider credentials. Extracted from providers.ts so the
* cookie-dedup wiring there stays thin (#3368 PR6). No DB access here.
*/

/**
* Reduce a `provider_specific_data` record to a single comparable credential
* value. Cookie/token credentials are mirrored across a provider's storage
* keys (e.g. `cookie`, `sessionToken`, `token`) with the same secret value, so
* any one of them identifies the session. Returns the trimmed value, or null
* when no usable string credential is present.
*/
const PREFERRED_CREDENTIAL_KEYS = [
"cookie",
"token",
"sessionToken",
"session-token",
"sso",
"access_token",
"accessToken",
];

/** First trimmed non-empty string value among `keys` of `rec`, else null. */
function firstNonEmptyString(rec: Record<string, unknown>, keys: readonly string[]): string | null {
for (const key of keys) {
const value = rec[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return null;
}

export function webSessionCredentialKey(psd: unknown): string | null {
if (!psd || typeof psd !== "object") return null;
const rec = psd as Record<string, unknown>;
// Prefer canonical credential keys, then fall back to the first non-empty
// string value (sorted for determinism).
return (
firstNonEmptyString(rec, PREFERRED_CREDENTIAL_KEYS) ??
firstNonEmptyString(rec, Object.keys(rec).sort())
);
}

/** Parse a stored `provider_specific_data` column (JSON string or object). */
export function parseProviderSpecificData(raw: unknown): Record<string, unknown> | null {
if (!raw) return null;
if (typeof raw === "object") return raw as Record<string, unknown>;
if (typeof raw === "string") {
try {
const parsed = JSON.parse(raw);
return parsed && typeof parsed === "object" ? (parsed as Record<string, unknown>) : null;
} catch {
return null;
}
}
return null;
}
2 changes: 2 additions & 0 deletions src/shared/constants/mcpScopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ export const MCP_TOOL_SCOPES: Record<string, readonly McpScope[]> = {
omniroute_pool_health: ["read:health"],
omniroute_pool_reset: ["write:resilience"],
omniroute_pool_warm: ["write:resilience"],
// Stealth browser pool observability (#3368 PR7)
omniroute_browser_pool_status: ["read:health"],
} as const;

// ============ Scope Groups ============
Expand Down
Loading
Loading