diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 05e5743d85..7e7d0315e3 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -1,6 +1,6 @@
{
- "_rebaseline_2026_08_09_9296_adobe_media_capabilities": "PR #9296 (artickc, fix/adobe-firefly-model-capabilities) own growth: src/app/api/v1/models/catalog.ts 1590->1597 (+7). The image and video catalog serializers now expose the already-normalized Adobe Firefly discovery capability data (media_capabilities, plus the existing video modality/size fields) at their only response-emission chokepoints. The discovery parser and capability normalization remain in open-sse/services/adobeFireflyModels.ts; extracting these seven serialization fields would obscure the catalog contract. Covered by tests/unit/adobe-firefly.test.ts and tests/unit/image-upscale.test.ts.",
- "_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent’s conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR’s own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
+ "_rebaseline_2026_08_03_9255_adobe_firefly_durable_sessions": "PR #9255 own cohesive growth: open-sse/services/adobeFireflyClient.ts 2322->2894 adds authenticated-vs-guest IMS classification, browser-risk ARP validation/rebuild, bounded 408 retry/recovery, sticky accepted-session handling, and matching image/video submit recovery at the existing Adobe upstream client chokepoints. This client was already explicitly frozen as a single self-contained upstream integration by #8006/#8510; splitting only the retry/auth helpers now would scatter one request state machine while structural shrink remains tracked in #3501. tests/unit/adobe-firefly.test.ts 871->1136 adds direct regression coverage for guest-token rejection, cookie/ARP rebuilding, 408 retries, sticky accepted ARP reuse, forced auth recovery, and cookie-to-IMS exchange. The obsolete 1179-line managed-Chrome fallback module was deleted rather than rebaselined after the packaged-safe pure-CDP path became authoritative. Focused Adobe suite: 61/61.",
+ "_rebaseline_2026_07_24_8470_hyperagent_sticky_thread": "PR #8470 (artickc, fix/hyperagent-tool-loop-thread-sticky) own growth: open-sse/executors/hyperagent.ts 936->1025 (wc -l; check-file-size.mjs counts via split(\"\\n\").length so the gate sees 937->1026, +89, crosses the 1000 cap). Fixes a real bug where a reverse-conversion proxy (text-Intent/JSON to Claude Code native tool_calls) rewrites assistant messages between agentic tool-loop turns, breaking HyperAgent's conversation-prefix fingerprint and cold-starting the thread mid tool-loop. Adds Anthropic tool_use/tool_result flattening to extractMessageText() plus a new rootUserFingerprint()/root-key lookup tier in resolveHyperAgentThreadBinding()/storeHyperAgentThreadAfterTurn() so the thread stays sticky across the tool loop. Cohesive additions inside the existing single-file executor; not extractable without splitting the executor mid-request-flow. Covered by tests/unit/executor-hyperagent.test.ts (19/19, +5 new cases for tool_result/tool_use flattening + root-key stickiness). Pre-merge review flagged a cross-conversation root-key collision risk (tracked in the PR's own mandatory pre-merge checklist, not yet addressed) — unrelated to this file-size ratchet, tracked separately by /fix-prs.",
"_rebaseline_2026_07_25_8494_capability_filter_fail_closed": "PR #8494 (fix/capability-filters-fail-closed, #8488) own growth: open-sse/services/combo.ts 3640->3693 (+53) adds a fail-closed guard after filterTargetsByRequestCompatibility() — when every eligible target is excluded by request-capability filtering (vision/tools/etc) instead of quota/health, the combo now returns an explicit `capability_mismatch` 400 (describeCapabilityFilterExhaustion, imported from combo/comboStructure.ts) rather than silently falling through to a generic no-targets error, plus a `compatFilterFailOpen` escape hatch (combo config OR settings) mirrored at both the main/auto and round-robin call sites for symmetry. combo/comboStructure.ts (previously under cap, un-frozen) grows 794->918 (+124) — new home for describeCapabilityFilterExhaustion + providerSupportsEmulatedToolCalling (#5240 emulated tool-calling exemption so fail-closed does not regress prompt-emulation-only combos like all-chatgpt-web). Irreducible orchestration wiring at the existing filter chokepoint (same precedent as #7301's universal-cooldown-retry generalization). Companion test tests/unit/combo-routing-engine.test.ts 3409->3449 (+40, fail-closed/fail-open coverage across both call sites) also rebaselined. Covered by tests/unit/8488-capability-filter-fail-closed.test.ts (new) + 95/95 passing across both files. Structural shrink of combo.ts tracked in #3501.",
"_rebaseline_2026_07_25_8499_ts7_result_union_predicates": "PR #8499 (backryun, chore/ts7-types-executor-scattered) own growth: muse-spark-web.ts 1396->1405 (+9, irreducible). Under this workspace's `strictNullChecks: false`, the boolean-literal discriminant on `GraphqlResult` (`{ ok: true } | { ok: false; error: string }`) narrows the positive `.ok===true` branch but leaves `!result.ok` at the full union under TS7, making `.error` unreachable to the checker at the two call sites (warmup, mode-switch). Fixed by adding a single `isGraphqlFailure()` type-predicate helper (doc comment + 3-line body) reused at both call sites instead of duplicating the predicate inline — not extractable to a shared module without splitting a single-file executor's local narrowing helper out of its own file. Covered by the existing muse-spark-web executor test suite (no behavior change, pure narrowing fix).",
"_rebaseline_2026_07_22_8131_windowshide_cloudflared_spawn": "PR #8167 (Dingding-leo, fix/windows-hide-child-process, #8131) own growth: src/lib/cloudflaredTunnel.ts 934->935 (+1, irreducible call-site wiring — the single `windowsHide: true` option added to the existing cloudflared spawn() options object so no transient conhost.exe/cmd console window flashes open on Windows). Covered by the pre-merge-fix regression test tests/unit/windows-hide-child-process-spawns-8131.test.ts (added for the two additional spawn() sites the PR missed: ServiceSupervisor.ts, versionManager/processManager.ts) plus the windowsHide assertion added to tests/unit/services/installers/runNpm-shell-5379.test.ts (installers/utils.ts buildNpmExecOptions).",
@@ -173,14 +173,15 @@
"_rebaseline_2026_07_25_8510_adobe_firefly_reference_images_tests": "#8510 (artickc, feat/adobe-firefly-reference-images) own test growth: tests/unit/adobe-firefly.test.ts 711->871 (+159, entirely this PR's diff — new referenceBlobs upload/dispatch coverage for handleAdobeFireflyImageGeneration, resolveAdobeSourceImageIds, and the storage-upload wire contract). Route-level /v1/images/edits coverage (credentials/rate-limit/4-ref-cap branches added to route.ts) lives in the new tests/unit/8510-adobe-firefly-edits-route.test.ts instead of growing this file further.",
"_rebaseline_basered_codebuddy_cn": "Base-red fix (#4664 CodeBuddy CN): oauth-providers-config.test.ts 867->870 (+3) to align the EXPECTED provider list/config with the codebuddy-cn provider that #4664 added to the registry without updating this test (it asserts 'exactly once').",
"_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.",
- "tests/integration/chat-pipeline.test.ts": 1598,
+ "tests/integration/chat-pipeline.test.ts": 1592,
"tests/integration/chatcore-compression-integration.test.ts": 1114,
"tests/unit/account-fallback-service.test.ts": 1563,
+ "tests/unit/adobe-firefly.test.ts": 1136,
"tests/unit/batch_api.test.ts": 1324,
"tests/unit/cc-compatible-provider.test.ts": 1217,
- "tests/unit/chatcore-translation-paths.test.ts": 2876,
+ "tests/unit/chatcore-translation-paths.test.ts": 2776,
"tests/unit/chatgpt-web.test.ts": 3148,
- "tests/unit/combo-routing-engine.test.ts": 3457,
+ "tests/unit/combo-routing-engine.test.ts": 3449,
"tests/unit/db-migration-runner.test.ts": 1499,
"tests/unit/deepseek-web.test.ts": 1092,
"tests/unit/executor-codex.test.ts": 1339,
@@ -190,18 +191,18 @@
"tests/unit/model-sync-route.test.ts": 1016,
"tests/unit/models-catalog-route.test.ts": 1636,
"tests/unit/perplexity-web.test.ts": 1355,
- "tests/unit/provider-models-route.test.ts": 1787,
- "tests/unit/provider-validation-specialty.test.ts": 2985,
+ "tests/unit/provider-models-route.test.ts": 1784,
+ "tests/unit/provider-validation-specialty.test.ts": 2980,
"tests/unit/providers-page-utils.test.ts": 1106,
"tests/unit/response-sanitizer.test.ts": 1063,
"tests/unit/route-edge-coverage.test.ts": 1241,
"tests/unit/search-handler-extended.test.ts": 1071,
- "tests/unit/sse-auth.test.ts": 1610,
+ "tests/unit/sse-auth.test.ts": 1600,
"tests/unit/stream-utils.test.ts": 2445,
"tests/unit/token-refresh-service.test.ts": 1378,
"tests/unit/translator-openai-responses-req.test.ts": 1194,
- "tests/unit/translator-openai-to-gemini.test.ts": 1622,
- "tests/unit/translator-openai-to-kiro.test.ts": 1275,
+ "tests/unit/translator-openai-to-gemini.test.ts": 1616,
+ "tests/unit/translator-openai-to-kiro.test.ts": 1250,
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1483,
"tests/unit/vscode-token-routes.test.ts": 1256,
@@ -343,55 +344,55 @@
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
"_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
"open-sse/executors/antigravity.ts": 1528,
- "open-sse/executors/base.ts": 1640,
+ "open-sse/executors/base.ts": 1578,
"open-sse/executors/chatgpt-web.ts": 3241,
- "open-sse/executors/codex.ts": 1562,
- "open-sse/executors/cursor.ts": 1563,
+ "open-sse/executors/codex.ts": 1534,
+ "open-sse/executors/cursor.ts": 1560,
"open-sse/executors/deepseek-web.ts": 1148,
"open-sse/executors/grok-web.ts": 1044,
"open-sse/executors/muse-spark-web.ts": 1405,
- "open-sse/handlers/chatCore.ts": 5034,
+ "open-sse/handlers/chatCore.ts": 5020,
"open-sse/handlers/imageGeneration.ts": 3101,
- "open-sse/handlers/responseSanitizer.ts": 1128,
+ "open-sse/handlers/responseSanitizer.ts": 1115,
"open-sse/handlers/search.ts": 1536,
"open-sse/handlers/videoGeneration.ts": 1063,
- "open-sse/mcp-server/schemas/tools.ts": 1553,
- "open-sse/mcp-server/server.ts": 1448,
+ "open-sse/mcp-server/schemas/tools.ts": 1505,
+ "open-sse/mcp-server/server.ts": 1407,
"open-sse/mcp-server/tools/advancedTools.ts": 1120,
- "open-sse/services/accountFallback.ts": 1978,
- "open-sse/services/adobeFireflyClient.ts": 2385,
+ "open-sse/services/accountFallback.ts": 1966,
+ "open-sse/services/adobeFireflyClient.ts": 2894,
"open-sse/services/claudeCodeCompatible.ts": 1202,
"open-sse/services/combo.ts": 3648,
"open-sse/services/compression/strategySelector.ts": 1060,
- "open-sse/services/rateLimitManager.ts": 1167,
- "open-sse/translator/response/openai-responses.ts": 1204,
+ "open-sse/services/rateLimitManager.ts": 1060,
+ "open-sse/translator/response/openai-responses.ts": 1174,
"open-sse/utils/cursorAgentProtobuf.ts": 1505,
"open-sse/utils/stream.ts": 2889,
- "src/app/(dashboard)/dashboard/HomePageClient.tsx": 1388,
+ "src/app/(dashboard)/dashboard/HomePageClient.tsx": 1381,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1031,
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3117,
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1067,
- "src/app/(dashboard)/dashboard/combos/page.tsx": 4703,
+ "src/app/(dashboard)/dashboard/combos/page.tsx": 4647,
"src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": 1283,
"src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": 1022,
"src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": 2615,
"src/app/(dashboard)/dashboard/health/page.tsx": 1165,
- "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1324,
- "src/app/(dashboard)/dashboard/providers/page.tsx": 1944,
+ "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": 1316,
+ "src/app/(dashboard)/dashboard/providers/page.tsx": 1923,
"src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": 1201,
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1019,
- "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1470,
+ "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1464,
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1123,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1573,
"src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1028,
"src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148,
- "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1119,
- "src/app/api/providers/[id]/models/route.ts": 2361,
- "src/app/api/v1/models/catalog.ts": 1597,
+ "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1109,
+ "src/app/api/providers/[id]/models/route.ts": 2250,
+ "src/app/api/v1/models/catalog.ts": 1549,
"src/lib/db/apiKeys.ts": 1529,
- "src/lib/db/core.ts": 1639,
- "src/lib/db/migrationRunner.ts": 1094,
+ "src/lib/db/core.ts": 1637,
+ "src/lib/db/migrationRunner.ts": 1077,
"src/lib/db/models.ts": 1097,
"src/lib/db/providers.ts": 1034,
"src/lib/memory/retrieval.ts": 1073,
@@ -401,16 +402,11 @@
"src/shared/components/RequestLoggerV2.tsx": 1629,
"src/shared/components/analytics/charts.tsx": 1035,
"src/shared/services/cliRuntime.ts": 1122,
- "src/sse/handlers/chat.ts": 1904,
- "src/sse/services/auth.ts": 2520,
+ "src/sse/handlers/chat.ts": 1845,
+ "src/sse/services/auth.ts": 2508,
"tests/unit/account-fallback-service.test.ts": 1572,
- "tests/unit/provider-validation-specialty.test.ts": 2985,
- "open-sse/executors/hyperagent.ts": 1026,
- "src/lib/tokenHealthCheck.ts": 1053,
- "open-sse/executors/default.ts": 1042,
- "open-sse/executors/kiro.ts": 1069,
- "open-sse/translator/request/openai-to-kiro.ts": 1057,
- "open-sse/utils/sseHeartbeat.ts": 149
+ "tests/unit/provider-validation-specialty.test.ts": 2980,
+ "open-sse/executors/hyperagent.ts": 1026
},
"_rebaseline_2026_07_27_v3849_train2": "Merge-train 2 (7 PRs) — owner-approved 2026-07-27. Single entry: chatCore.ts 4955->5006 (#8595, Responses multi-turn image compaction before the context hard-reject). Genuine irreducible growth at the existing compaction chokepoint in handleChatCore — the PR adds a last-resort retry against the concrete budget plus the estimateFinalInputTokens helper, both wired at the pre-existing call site rather than a new branch. Covered by tests/unit/8560-responses-image-compaction.test.ts (4 tests).",
"_rebaseline_2026_07_27_v3849_train3": "Merge-train 3 (13 PRs) — owner-approved 2026-07-27. Both entries are genuine irreducible growth at existing chokepoints, not new branches: src/lib/db/apiKeys.ts 1518->1529 (#8805 cx/* ≡ codex/* API-key model permissions); open-sse/handlers/chatCore.ts 5006->5020 (#8806 real response payload into plugin onResponse hooks). Covered by tests/unit/db-apiKeys-crud.test.ts (4 new cases) and the two plugin-hook test files updated in #8806 respectively.",
@@ -420,145 +416,5 @@
"_rebaseline_2026_07_28_8861_xiaomi_token_plan": "PR #8861 (feat/xiaomi-token-plan-protocol-selector) own growth: EditConnectionModal.tsx 1283->1316 (+33 = the per-connection API-protocol selector field) and open-sse/executors/base.ts 1540->1562 (+22 = alternate-format resolution at the existing buildUrl/headers chokepoint). Both are irreducible wiring at existing call sites.",
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
- "_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.",
- "_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
- "_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
- "_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
- "_rebaseline_2026_07_25_dario_upstream_proxy_selector": "PR #8523 (Dario embedded service): upstream-proxy mode selector replaces the binary CLIProxyAPI toggle with Native/CLIProxyAPI/Dario/Fallback + a fallback-backend picker. ProviderDetailPageClient.tsx 798->804 (+6, new hook fields threaded through to ConnectionsListPanel), ConnectionRow.tsx 942->958 (+16, the mode + conditional fallback-backend replacing a single pill button), useProviderConnections.ts 954->986 (+32, upstreamProxyMode/upstreamProxyFallbackBackend state + handleSetUpstreamProxyMode, handleToggleCliproxyapiMode kept as a thin backward-compat wrapper for the existing hook-shape test). All additive UI/state for the new modes — no unrelated refactor.",
- "_rebaseline_2026_08_02_9242_token_health_transient": "PR #9242 (fix/refresh-circuit-transient): src/lib/tokenHealthCheck.ts 1021 (new file, above cap 1000). The file consolidates token-refresh health checking logic that was previously scattered across auth.ts and tokenRefresh.ts. Cohesive single-responsibility module for refresh circuit state management; not extractable without splitting the refresh state machine. Covered by tests/unit/tokenHealthCheck-transient.test.ts.",
- "_rebaseline_2026_07_28_8870_firefly_ref_cap_timeout": "PR #8870 (fix/adobe-firefly-gpt-ref-cap-timeout) own growth: adobeFireflyClient.ts 2322->2385 (+63 = gpt-image subject-ref hard cap at 2 + adaptive poll timeout budget (base 300s + 60s/ref, max 600s) + defensive .slice on referenceBlobs for gpt/nano/generic families). Fixes live 504s on multi-screenshot listing jobs (Featured Promo / Box Art) where 3–4+ subject refs stall colligo until the old 180s poll budget expires. Helpers adobeFireflyMaxImageRefs/adobeFireflyImageTimeoutMs live next to the existing payload/poll chokepoint (not extractable without splitting the wire recipe mid-PR). Covered by tests/unit/adobe-firefly.test.ts (ref-cap + timeout cases). Structural shrink tracked in #3501.",
- "_rebaseline_2026_08_01_8964_xai_agent_tools": "PR #8964 own growth: chatCore.ts 5020->5034 at the existing native-passthrough chokepoint. Adds xAI Agent Tools passthrough for /v1/responses (xai/xai-oauth/xao): resolve nativeXaiResponsesPassthrough, force openai-responses targetFormat, stamp body marker, and OR into the existing nativeCodexPassthrough sites (web-search bypass + requestEndpointPath). Leaf logic in passthroughHelpers, responsesEndpoint, targetFormat, xai executor, responseSanitizer, usageTracking. Cohesive wiring at the Codex passthrough boundary.",
- "_rebaseline_2026_08_01_8964_response_sanitizer": "PR #8964 own growth: responseSanitizer.ts 1115->1128. Keep cost_in_usd_ticks / server_side_tool_usage(_details) through sanitizeResponsesApiResponse allowlists so native xAI tool responses retain usage.",
- "_rebaseline_2026_08_05_9323_agentrouter_waf_retry": "PR #9323 (fix(agentrouter): retry on 400 content-blocked + burst guard) own growth: open-sse/executors/base.ts 1578->1623 (check-file-size.mjs conta via split(\"\\n\").length; wc -l ve 1622). As +45 linhas sao o WAF_RETRY_CONFIG + o burst guard via gateOutboundRequest() para o WAF do agentrouter.org, com comentarios explicando o porque de cada mitigacao e cobertos por tests/unit/base-executor-waf-retry.test.ts e tests/unit/wafRateLimit.test.ts. Crescimento funcional legitimo, nao inchaco.",
- "_rebaseline_2026_08_05_9529_own_growth": "PR #9529 own growth (base release/v3.8.50 medida EXATAMENTE nos frozen antigos, entao o modo base-relative #8522 nao cobre): open-sse/services/rateLimitManager.ts 1060->1105 (+45: helper applyLimiterSettings() que re-arma o heartbeat do reservoir apos updateSettings — fix do bug Bottleneck 2.19.5 que congelava a fila weighted; TDD em tests/unit/ratelimit-reservoir-refresh.test.ts); tests/integration/chat-pipeline.test.ts 1592->1598 (+6: User-Agent do codex derivado de getCodexClientVersion() em vez de literal pinado — teste-irmao alinhado ao contrato); tests/unit/provider-validation-specialty.test.ts 2980->2985 (+5: cobertura NOVA claude-web 429 -> valid:false, alinhamento #9406); open-sse/translator/response/openai-responses.ts 1174->1204 (+30: buildResponsesReasoningSummaryDelta MOVIDA do leaf pureHelpers.ts para o host — a funcao do #9500 muta stream state e violava o contrato do leaf puro; o LOC total do par host+leaf nao cresceu, o pureHelpers encolheu o mesmo tanto). Crescimento por fix de producao + cobertura adicional + realocacao arquitetural, nao inchaco.",
- "_rebaseline_2026_08_06_v3850_inherited_drift_reconcile": "Reconciliacao 2026-08-06 do drift ACUMULADO da release/v3.8.50 apos o lote de merges de 08-05/06: 13 arquivos acima do frozen no tip puro 8180b49ce1 (medidos pelo proprio gate). O modo PR base-relative (#8522) deixa PRs inocentes passarem, e os rebaselines individuais dos PRs se perderam nas resolucoes sucessivas de conflito deste hot-file — o drift so aparece no modo absoluto (nightly/local). Crescimentos funcionais dos PRs mergeados: #9024 topology click-nav src/app/(dashboard)/dashboard/HomePageClient.tsx; #9324 OpenRouter enrich src/app/(dashboard)/dashboard/providers/page.tsx; #9329 quota card ordering src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx; #9193 context-window suffixes src/sse/handlers/chat.ts; #9332 nested Claude server tool ids open-sse/executors/base.ts; #9228 strip orphaned tool outputs open-sse/executors/codex.ts; #9236 nvidia tool-name normalize open-sse/executors/default.ts; #9314 nested tool_call validation open-sse/executors/kiro.ts; #9260 caller identity REST hops open-sse/mcp-server/server.ts; #8934 cache breakpoints tests tests/unit/chatcore-translation-paths.test.ts; #9193 suffix tests tests/unit/combo-routing-engine.test.ts; #9196 reasoning-on-tool-finish tests tests/unit/sse-auth.test.ts; #9163 GPT-5.6 Max reasoning tests tests/unit/translator-openai-to-kiro.test.ts. default.ts e kiro.ts entram no frozen (estavam sem entrada, acima do cap 1000). Atualizacao pos-medicao (a base avancou durante o ciclo do PR): src/sse/handlers/chat.ts 1857->1877 (#9184 affinity EOF evict) e open-sse/executors/default.ts 1027->1042 (#9005 Kimi K3 tool-name backfill).",
- "_rebaseline_2026_08_06b_v3850_sweepreds_drift": "Segunda reconciliacao de 2026-08-06 (/sweep-reds sobre o tip puro 2ddbbc61a6): 3 arquivos voltaram a passar do frozen apos os merges do mesmo dia, com atribuicao 1:1 por commit. (1) src/app/(dashboard)/dashboard/providers/page.tsx 1928->1944 e (2) open-sse/executors/base.ts 1635->1640, ambos do #9515 (feat(radar): flag-gated signed free-model catalog overlay, commit e7f6b1d130) — o overlay do Radar entra por wiring nos chokepoints ja existentes (a resolucao/verificacao do catalogo assinado mora fora destes dois arquivos); +16 e +5 linhas liquidas nao sao extraiveis sem inventar um leaf por callsite. (3) open-sse/services/accountFallback.ts 1966->1972 do #8704 (commit c4527f97bd), +6 linhas de dados em CREDITS_EXHAUSTED_SIGNALS ('has been exhausted', fixes #8631). src/sse/handlers/chat.ts 1880>1877 tambem estava violando e NAO entra aqui de proposito: e drenado por encolhimento na PR #9598, sem rebaseline. Crescimento proprio DESTA PR: src/lib/db/migrationRunner.ts 1077->1084 (+7) — o guard retroativo em isSchemaAlreadyApplied para os arquivos renumerados 137/138, exigido pela propria mensagem de erro de colisao do runner (ambas as migracoes sao ALTER TABLE ADD COLUMN puro, nao idempotente). Dois `case` + dois `return hasColumn(...)` + 3 linhas de comentario dentro do switch existente; nao extraivel.",
- "_rebaseline_2026_08_06c_v3850_sweepreds_pr2": "Segunda PR do /sweep-reds (fix/release-v3.8.50-basereds-0806b): tests/unit/provider-models-route.test.ts 1784->1787 (medido pelo gate, que conta split(\"\\n\").length) (+2 apos compressao de comentarios) — alinhamento de contrato forcado por dois merges do dia: #9106 tornou gemini-3.1-pro-high user-callable (a entry do alias entra na lista esperada do teste de discovery-retry, +1 linha de dado + 1 de comentario) e ff012ff420 adicionou onboardUser como bootstrap hop (exclusao no mock, ja comprimida a 1 linha). Nao ha o que encolher sem apagar o comentario que explica o porque.",
- "_rebaseline_2026_08_07_v3850_sweepreds_pr2_toolnamemap": "tests/unit/translator-openai-to-gemini.test.ts 1616->1619 (+3). O frozen estava EXATAMENTE no tamanho da base, entao qualquer linha nova viola. #9568 (c9a3361e5a) fez buildChangedToolNameMap emitir entradas IDENTIDADE (o Gemini minusculiza nomes de tool nas respostas, entao o tradutor de resposta precisa da chave para mapear de volta), o que passou a incluir `_toolNameMap` no envelope Antigravity de qualquer request com tools. As 3 linhas sao: a chave nova na lista esperada de Object.keys, 1 comentario explicando POR QUE ela aparece (sem ele o proximo leitor tenta remove-la de novo) e 1 assert do CONTEUDO do map — presenca de chave sozinha nao provaria a entrada identidade, que e justamente o comportamento novo. Nao ha o que extrair: e alinhamento de contrato dentro de um teste existente.",
- "_rebaseline_2026_06_22_4644_deepseek_web_tools": "PR #4644 (BugsBag/robust deepseek-web tool-call parsing): open-sse/executors/deepseek-web.ts 1117->1125 (+8). The new agentic tool-call path emits surrounding text + reasoning before tool_calls and swaps to the dedicated deepseekWebTools.ts parser; the +8 lines are cohesive wiring at the existing transformSSE chokepoint (the parser itself lives in the new deepseekWebTools.ts file, already under cap). The PR's own fast-gate (PR->release) does not run check:file-size, so this surfaced only at release reconcile. Covered by tests/unit/deepseek-web-tools-variants.test.ts + deepseek-web-tools-execute.test.ts.",
- "_rebaseline_2026_06_23_4712_deepseek_web_tool_results": "PR for #4712 (deepseek-web drops role:tool): open-sse/executors/deepseek-web.ts 1125->1148 (+23). messagesToPrompt() now folds role:\\\"tool\\\" results into the single-prompt transcript (recovering the tool name from the preceding assistant tool_calls by tool_call_id) instead of silently dropping them; the lines are cohesive wiring inside the existing function. Covered by tests/unit/deepseek-web-tool-result-prompt-4712.test.ts.",
- "_rebaseline_2026_06_24_headroom_strategy": "Headroom-aware connection selection (dario technique): combo.ts 3168->3180 (+12 = a new `else if (strategy === \\\"headroom\\\")` dispatch branch in handleComboChat that delegates to orderTargetsByHeadroom + its log line, plus the import). The actual logic lives OUT of the god-file: the pure ranker rankByHeadroom/computeHeadroom is the new leaf open-sse/services/combo/headroomRanking.ts (91 LOC, 3190 (+10 = one new `else if (strategy === \\\"quota-share\\\")` dispatch branch in handleComboChat that delegates 100% to selectQuotaShareTarget + its log line, plus the import). All the new logic lives OUT of the god-file in two new leaves under open-sse/services/combo/: quotaShareInflight.ts (in-flight counter with TTL/lease, ~150 LOC 3225 (+35) = one new `else if (strategy === \\\"task-aware\\\")` dispatch branch delegating 100% to selectTaskAwareTarget + its imports/log lines. All scoring/classification logic lives OUT of the god-file in the new leaf open-sse/services/taskAwareRouting.ts (553 LOC 854, -35), but the StackOptions.fidelityGate field, the `const fidelityGate` reads at the two stacked-loop dispatch chokepoints, and the import of FidelityGateConfig are irreducible wiring that cannot leave strategySelector without an architectural refactor of the pre-existing stacked pipeline. Net: 889->854 (+6 vs the pre-Milestone-B frozen 848). Covered by tests/unit/compression/*.test.ts (940 pass).",
- "_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
- "_rebaseline_2026_06_27_5193_antigravity_basered": "Base-red (pre-existing release drift, fast-gate PR->release skips check:file-size): accountFallback.ts 1773->1777 and src/app/api/providers/[id]/test/route.ts 924->940 were already over their frozen caps on release/v3.8.39 independent of any antigravity change. Owner chose to rebaseline (keep the documented issue-reference comments #1846/#1449/#347 etc.) rather than accept the contributor comment-stripping in #5200/#5198. Reverted #5200 to restore the comments; bumped these two frozen caps to the actual base sizes. No logic change.",
- "_rebaseline_2026_06_28_5237_impersonation_ua_refresh": "PR #5237 (refresh impersonation UAs): grok-web.ts 1871->1873 (+2), muse-spark-web.ts 1284->1302 (+18), perplexity-web.ts 1013->1032 (+19). Net semantic change in each file is a single User-Agent constant (Chrome 147->149 for grok/muse; perplexity kept at Firefox 148 to stay matched with the firefox_148 TLS profile — the contributor's 152 bump was reverted to avoid a UA-vs-JA3 mismatch, #2459). The growth is Prettier reflow that lint-staged unavoidably applies to these grandfathered long-line files the moment they are touched; not extractable. src/sse/services/auth.ts 2336->2401 in the same reconcile is #5222's antigravity-LRU-retry growth that merged via --admin without a baseline bump.",
- "_rebaseline_2026_06_28_5243_risk_gate_prepass": "PR #5243 (compression risk-gate pre-pass) own growth: open-sse/services/compression/strategySelector.ts 854->899 (+45). The three exported entry points (applyCompression/applyStackedCompression/applyStackedCompressionAsync) become thin wrappers over pure-extracted private bodies (runCompression/runStackedCompression/runStackedCompressionAsync) so the risk-gate mask->run->restore wrapper sits strictly OUTSIDE the per-step loop — a single universal integration point. The wrapper logic itself (resolveRiskGate/withRiskGate) lives in the new riskGate/strategyWrap.ts (985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 7912181 (+78 = the compare-and-swap guard on the refresh persist — runWithCasGuard/getActiveCasGuard AsyncLocalStorage pair mirroring runWithOnPersist, casGuardShouldSkipPersist that rereads the row right before persisting and skips the write when a concurrent writer already rotated the refresh_token past the one presented, plus getCasGuardStats counters). Fixes the sibling-rotation-revert → token-family-revocation storm. Gated behind an active guard (opt-in; no guard => byte-identical). Wiring lives at the two persist chokepoints inside getAccessToken; the comparison reuses wasRefreshTokenRotated from refreshSerializer. Not extractable without splitting the refresh hot path.",
- "_rebaseline_2026_06_29_5286_memoization": "PR #5286 own growth: strategySelector.ts 899->960 (+61 = the opt-in result-memoization branches in applyCompression/applyCompressionAsync — principal+determinism gate, makeMemoKey lookup/store with model+supportsVision folded into the key, recompute-with-memo-off). Default off (memoizeCompressionResults), so zero behavior change. The memo helpers live in the leaf resultMemo.ts (3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
- "_rebaseline_2026_07_02_5816_qoder": "PR #5816 (@AgentKiller45, qoder PAT via qodercli): qoderCli.ts 666->989, new-above-cap frozen (owner-approved baseline freeze). The growth is the legitimate PAT job-token exchange + quota parsing CLI transport (the pure-JS Cosy path 500'd on every PAT request); extracting the spawn/parse helpers now would just add indirection to a contributor PR mid-merge. Test frozen also raised for this PR's coverage growth: providers-page-utils.test.ts 1052->1092. Additionally clears an inherited base-red from the already-merged #5933 (codex json_schema->text.format): translator-openai-responses-req.test.ts 1097->1172 (+75 regression tests, no offending branch left). All remain frozen (cannot grow further); release captain's rebaseline-at-release supersedes.",
- "_rebaseline_2026_07_09_6126_clinepass_dual_auth": "PR #6126 (@hajilok, dual-auth ClinePass) own growth: tokenRefresh.ts 2181->2182 (+1 = a single `case \\\"clinepass\\\":` fallthrough label added to the existing `case \\\"cline\\\":` in _getAccessTokenInternal's provider switch, so clinepass token refresh dispatches to the already-shared refreshClineToken() instead of silently falling through to the generic OAuth refresh). Irreducible 1-line switch-case wiring at the existing chokepoint; the header-building logic for the same feature was extracted to a new leaf src/shared/utils/clineAuth.ts::buildClinepassHeaders() (well under cap) to avoid growing open-sse/executors/default.ts. Covered by tests/unit/clinepass-provider.test.ts.",
- "_rebaseline_2026_07_09_6363_kiro_external_idp": "PR #6363 (@artickc, Kiro external IdP) own growth: tokenRefresh.ts 2182->2249 (+67 = the external_idp refresh branch inside refreshKiroToken — standard public-client OAuth2 refresh_token grant against the org IdP tokenEndpoint via buildExternalIdpRefreshParams/isExternalIdpAuthMethod from the new leaf open-sse/services/kiroExternalIdp.ts, with invalid_grant/invalid_client -> unrecoverable_refresh_error mapping). Cohesive addition at the existing refreshKiroToken chokepoint. Covered by tests/unit/kiro-external-idp.test.ts.",
- "_rebaseline_2026_07_09_6587_kiro_api_key_auth": "PR #6587 (@strangersp) own growth for Kiro long-lived API-key auth, merged onto v3.8.47 tip: openai-to-kiro.ts 890->912 (+22, auth-header selection for API-key-vs-OAuth-token connections), providerLimits.ts 998->1000 (+2, API-key auth-type branch), translator-openai-to-kiro.test.ts 1234->1257 (+23), providers-page-utils.test.ts 1109->1107 (net -2 after merging with parallel release drift; connectionMatchesProviderCard api_key coverage added), provider-validation-specialty.test.ts 2856->2980 (+124 net after merge with parallel release drift; this PR also removed the file's `@typescript-eslint/no-explicit-any` eslint-suppression entry by fixing all `any` usages, adding typed replacements). Cohesive additive feature growth, well tested; not extractable without splitting the existing chokepoints mid-merge.",
- "_rebaseline_2026_07_09_6678_routing_strategy_9router": "#6678 (SeaXen) — 9router-parity Routing Strategy settings card + per-provider/combo sticky-round-robin override. Own growth: ProviderDetailPageClient.tsx 784->786 (single ProviderAccountRoutingCard mount + import), auth.ts 2448->2458 (providerStrategies override resolution: fallbackStrategy/stickyRoundRobinLimit per-provider cascade in getProviderCredentials). Both additive, zero unrelated refactor; new UI/logic lives in new files (ProviderAccountRoutingCard.tsx, RoutingStrategyCard.tsx, rrState.ts::resolveComboStickyRoundRobinLimit). chat.ts value below reflects the current release tip (grown by other concurrent PRs, e.g. #6640), not this PR own change.",
- "_rebaseline_2026_07_10_6318_omp_letta": "PR #6318 (@hamsa0x7, omp+letta CLI integrations) own growth: cliTools.ts (+53 = 2 registry entries incl. omp docsUrl) and cliRuntime.ts (+18 = runtime-detection wiring for the 2 new tools). Cohesive registry/wiring growth at the existing chokepoints; scope reduced from the original 5 tools (pi/codewhale/jcode shipped separately).",
- "_rebaseline_2026_07_10_gcf_v3_2_decode": "PR #6838 own growth: new vendored file open-sse/services/compression/engines/headroom/gcf/decode_generic.ts frozen at 880 (> 800 cap). It is the vendored GCF generic-profile decoder (spec v3.2 nested flattening plus the prototype-pollution / hasOwnProperty hardening added in this PR's Gemini review). Kept as one file faithful to upstream gcf-typescript so re-vendoring stays a clean copy rather than a re-split each cycle (sibling generic.ts/scalar.ts stay < cap; extraction would also fragment the file's frozen eslint no-explicit-any suppressions). Round-trip + prototype-pollution regression coverage in tests/unit/compression/headroom-smartcrusher.test.ts. Frozen: only shrinks from here.",
- "_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).",
- "_rebaseline_2026_07_15_7070_combos_memo": "PR #7070 (perf/p1-memo) own growth: src/app/(dashboard)/dashboard/combos/page.tsx 4655->4656 (+1 = React.memo wrapping of ComboCard). Covered by tests/unit/ui/combos-page-smoke.test.tsx.",
- "_rebaseline_2026_07_18_7399_xai_oauth_modal": "PR #7399 (xAI OAuth PKCE) own growth: OAuthModal.tsx 993->998 (+5 = provider entry + PKCE flow branch wiring at the existing provider-switch chokepoint; the provider logic itself lives in src/lib/oauth/providers/xai-oauth.ts, new leaf). Third irreducible wiring bump on this modal (969->989->993->998); structural shrink tracked in #3501.",
- "_rebaseline_2026_07_19_6636_codex_session_json": "#6636 own growth: OAuthModal.tsx 998->1030 (gate units, split(\\\"\\\\n\\\").length incl. trailing newline; +32 = session-JSON paste branch for handleManualSubmit plus a shared submitCodexAccessToken() helper extracted from the pre-existing bare-JWT branch, mirroring the #5203 oauthBlobSubmit.ts extraction precedent; the normalizer logic itself lives in the new src/lib/oauth/utils/codexSessionImport.ts leaf module, not here). Fourth irreducible wiring bump on this modal (969->989->993->998->1030); structural shrink tracked in #3501.",
- "_rebaseline_2026_07_19_7546_ghe_copilot_modal": "PR #7546 (GHE Copilot OAuth provider) own growth: OAuthModal.tsx 1030->1056 (gate units). Adds a gheUrl input state, routes ghe-copilot through the existing device-code branch, and threads gheUrl into the device-code request/poll extraData at the existing provider-switch chokepoints (+~24 lines, cohesive with the same pattern as #7399/#6636). The standalone GHE enterprise-URL config step JSX (originally +31 lines inline) was extracted to the new src/shared/components/oauthModal/GheConfigStep.tsx leaf component to minimize the bump; what remains is the irreducible provider-branch wiring. Fifth bump on this modal (969->989->993->998->1030->1056); structural shrink tracked in #3501.",
- "_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.",
- "_rebaseline_2026_07_20_7779_routingcombo_thread": "PR #7779 own growth: chatHelpers.ts 876->877 (+1, thread routingComboId into executeChatWithBreaker for compression-combo assignment). Frozen so it can only shrink.",
- "_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \\\"./db/autoCandidateOverrides\\\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.",
- "_rebaseline_2026_07_21_8027_grok_cli_auth_json_paste": "PR #8027 (RaviTharuma, fix(grok-cli) #7610) own growth: OAuthModal.tsx 1080->1100 (gate units). Requires the full ~/.grok/auth.json (with refresh_token) on the paste-import path instead of a bare JWT, at the existing paste-token chokepoint (renamed tab label, updated instructions/placeholder, textarea for the auth.json blob, inline error surface). The validation logic itself (parseGrokCliPasteToken, previously an inline ~75-line function) was extracted to the new src/lib/oauth/utils/grokCliAuthJson.ts leaf module — mirroring the #6636/#7546 extraction precedent — so only the irreducible UI wiring remains here. Sixth bump on this modal (969->989->993->998->1030->1056->1100); structural shrink tracked in #3501.",
- "_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.",
- "_rebaseline_2026_07_22_7936_namespace_roundtrip": "#7936 (@RCrushMe, Responses-Chat namespace round-trip identity seam) own growth: open-sse/translator/response/openai-responses.ts 1092->1125 (+33) and open-sse/utils/stream.ts 2814->2869 (+55) — threading the namespace-identity seam through the Responses↔Chat translation + stream paths so tool-call namespaces survive the round-trip. Cohesive translation/stream wiring at existing chokepoints, frozen at new size.",
- "_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \\\"codex-responses\\\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \\\"codex-responses\\\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \\\"codex-responses\\\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).",
- "_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.",
- "_rebaseline_2026_07_22_8050_model_lockout_exact_family": "#8050 (@AndrianBalanescu) own growth: accountFallback.ts 1864->1892 (+28) — exact-vs-family model-lockout scoping (getModelLockKey/isModelLocked/clearModelLock/getModelLockoutInfo) so an Antigravity 404 for one bare model no longer hijacks the whole family cooldown. Cohesive lockout logic; frozen at new size.",
- "_rebaseline_2026_07_22_8081_reasoning_placeholder_guard": "#8081 (@Dingding-leo) own growth: openai-responses.ts 1125->1137 (+12) restructuring the reasoning-placeholder guard so it skips only the empty content block and still emits finish_reason/tool_calls in the same chunk. Cohesive translator wiring; frozen at new size.",
- "_rebaseline_2026_07_22_8210_openrouter_midstream_error": "PR #8210 (hartmark, fix/openrouter-midstream-error-surfacing) own growth: open-sse/translator/response/openai-responses.ts 1137->1163 (+26) measured on the merged tip (release 1137 + this PR own growth). Adds a single new branch inside openaiToOpenAIResponsesResponse() that detects an OpenRouter-style mid-stream aggregator error (HTTP 200 SSE chunk with empty choices + a top-level error object) and surfaces it as state.upstreamError instead of silently falling through to the no-op/awaitingTrailingUsage path, which previously masked the failure as a false empty-success completion and skipped combo fallback. Irreducible call-site addition at the existing chunk-dispatch chokepoint (mirrors the Gemini-to-OpenAI translator's #4177 precedent for the same class of upstream error surfacing). Note: this baseline entry does NOT cover the separate pre-existing +11 drift already on the release tip from #8081/#8162 (1125->1136, unrelated reasoning-placeholder-stripping fix merged after this PR branched) — that drift belongs to the maintainer's rebaseline, not this PR.",
- "_rebaseline_2026_07_22_8211_gemini_malformed_tool_choice": "PR #8211 (hartmark, fix/gemini-malformed-function-call-tool-choice) own growth: open-sse/translator/response/gemini-to-openai.ts 771->821 (+50, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds MALFORMED_FUNCTION_CALL/UNEXPECTED_TOOL_CALL handling inside geminiToOpenAIResponse(): synthesizes a `malformed_tool_call` tool_calls entry so finish_reason normalizes to the standard \\\"tool_calls\\\" instead of an unrecognized raw enum value that OpenAI-compatible clients (e.g. OpenClaw) silently ignore, and always synthesizes (rather than skipping when a real tool call already exists) so a malformed attempt alongside a real one in the same turn is not silently discarded. Irreducible cohesive addition at the existing candidate/finishReason translation chokepoint (mirrors the 9router#2462 raw-finish-reason precedent immediately below it in the same function). Covered by the PR's own tests/unit test additions for both the malformed-only and malformed-plus-real-call cases.",
- "_rebaseline_2026_07_22_8213_chat_abandoned_target_abort": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/sse/handlers/chat.ts 1794->1860 (+66, measured against the PR's own merge-base — the release tip separately carries an unrelated -5 net shrink from #8013's antigravity callable-catalog alignment, which this PR's branch does not include and this entry does not cover). Adds resolveDispatchClientRawRequest(): merges a per-target modelAbortSignal into clientRawRequest.signal (via mergeAbortSignals) so a combo target abandoned by comboTargetTimeoutMs actually observes its own abort and reaches its cleanup path, instead of hanging forever inside withRateLimit/acquireAccountSemaphore and leaking a permanent 'pending' dashboard entry (live incident, log id 1784418258231-14961a). Also wires combo-exhausted rejection logging to capture request body + attempted models via the new rejectedRequestUsage helper. Irreducible additions at the existing chat dispatch chokepoint. Covered by the PR's own combo-config + integration test additions.",
- "_rebaseline_2026_07_22_8213_combo_cooldown_wait_recording": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/combo.ts 3548->3604 (+56, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Fixes combo cooldown-wait state recording so a bogus 503 is no longer crystallized when the cooldown-wait vars reset every setTry, adds an OpenAI-format SSE error frame path for combo-exhausted rejections (capturing request body + attempted models), and gives an abandoned per-target dispatch its own timeout instead of leaking a permanent 'pending' dashboard entry. Irreducible additions at the existing handleComboChat dispatch/retry chokepoint (mirrors the prior quota-share/headroom/task-aware strategy-branch precedents already frozen in this file). Covered by the PR's own combo-config + Gemini TPM-ceiling benchmark test additions.",
- "_rebaseline_2026_07_22_8213_gemini_tpm_quota_cooldown_wait": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: open-sse/services/accountFallback.ts 1857->1932 on the merged tip (release 1892 incl #8050 +35, plus this PR own growth +40); measured against the PR own merge-base was 1857->1898 (+41 — the release tip separately carries an unrelated +34 from #8050's antigravity 404 model-not-found lockout scoping, which this PR's branch does not include and this entry does not cover). Own growth is the Gemini TPM-ceiling classification + cooldown-wait wiring feeding into the combo cooldown-wait state machine (rate-limit wedge recovery) introduced by this PR's commit series. Irreducible additions at the existing account-fallback/model-lockout chokepoint. Covered by the PR's own gemini-rate-limit-tracker and TPM-ceiling benchmark test additions.",
- "_rebaseline_2026_07_22_8213_health_unblock_model_cooldowns": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/app/(dashboard)/dashboard/health/page.tsx 1094->1165 (+71, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip). Adds handleUnblockAll/handleUnblockOne dashboard actions (DELETE /api/resilience/model-cooldowns) so an operator can manually clear a Gemini TPM-wedge model lockout surfaced by this PR's cooldown-wait fixes, instead of waiting out the ceiling. Irreducible UI wiring at the existing health-page action chokepoint. Covered by the PR's own dashboard/resilience test additions.",
- "_rebaseline_2026_07_22_8213_requestloggerdetail_unblock_ui": "PR #8213 (hartmark, fix/gemini-tpm-quota-cooldown-wait) own growth: src/shared/components/RequestLoggerDetail.tsx 799->941 (+142, entirely this PR's diff — no other commit touched this file between the PR's merge-base and the release tip; crosses the general 800-line new-file cap so is frozen here for the first time). Adds a collapsible section header (open/expand-less toggle) plus per-log-entry unblock (`unblocking`/`cleared` state, isCombo503 detection) so the request-logger detail panel surfaces the same Gemini TPM cooldown-wait / model-lockout unblock action introduced by this PR at the individual-request level (mirrors the health-page bulk unblock action added in the same PR). Covered by the PR's own dashboard/resilience test additions.",
- "_rebaseline_2026_07_22_fusion_8013_8098_antigravity": "Fusion of #8013 (backryun, catalog/IDE-CLI-split rewrite) + #8098 (nguyenha935, protocol-fidelity/fail-closed/credits/tool-cloaking): open-sse/services/usage/antigravity.ts NEW 802 (>cap 800, +2 — #8098 credits/tier usage service on #8013's profile-aware headers). Test growth (models-catalog-route 1605->1608, provider-models-route 1752->1757 from #8013 Gemini 3.6 catalog) tracked in testFrozen.",
- "_rebaseline_2026_07_23_8127_grok_weekly_quota": "#8127 (@apoapostolov) own growth: src/sse/handlers/chat.ts 1861->1865 (+4) — weekly quota tracking for grok-web wires a quota-fetch hook at the existing dispatch chokepoint. Thin wiring mirroring adjacent provider-quota branches; not extractable. Covered by tests/unit/grok-quota-fetcher.test.ts.",
- "_rebaseline_2026_07_23_8143_empty_catch_logging": "#8143 (@chirag127) own growth: open-sse/utils/stream.ts 2869->2887 (+18) — replacing empty catch blocks in the SSE stream subsystem with console.debug logging (Rule #6 silent-swallow fix, issues #8138-#8142). Cohesive logging additions at the existing catch chokepoints, not extractable; frozen at new size. Covered by tests/unit/stream-handler-catch-logging-8143.test.ts.",
- "_rebaseline_2026_07_23_8219_cache_ttl_settings_sidebar": "#8219 (@oyi77) own growth: sections.ts 806->813 (+7) — configurable model-catalog cache-TTL settings adds a new sidebar nav entry + its visibility wiring. Sidebar item array is data, not extractable logic; frozen at new size.",
- "_rebaseline_2026_07_23_8247_8248_model_unhealthy": "#8247+#8248 own growth: accountFallback.ts 1940->1941 (+1, irreducible import statement only — the substantive #8248 DEGRADED-pattern classifier was extracted into open-sse/config/errorConfig.ts, which has ample headroom, instead of growing this frozen file; #8247's fix is a single existing-line condition change, net zero lines). Scoping the credits-exhausted 403/429 branch to isCompatibleProvider() (per-model-quota openai/anthropic-compatible-* nicknames) so it stays model-scoped instead of terminalling the whole connection, and classifying NVIDIA NIM 'Function ... DEGRADED' 400 bodies as model-access-denied instead of a raw passthrough 400. Covered by tests/unit/8247-accountfallback-model-unhealthy.test.ts and tests/unit/8248-accountfallback-nvidia-degraded.test.ts.",
- "_rebaseline_2026_07_23_8252_combo_400_advance": "#8252 (@RaviTharuma) own growth: accountFallback.ts 1932->1940 (+8) + combo.ts 3604->3630 (+26) — advance combo on model-scoped 400s wrapped as invalid/Bad-Request. Irreducible wiring at existing account-fallback + combo dispatch chokepoints. Covered by combo-model-scoped-400-advance.test.ts.",
- "_rebaseline_2026_07_23_8266_alibaba_media": "#8266 (@backryun) own growth: imageRegistry.ts 821->979 (+158) — Alibaba-family media models (Qwen image/video, Bailian, Wan) added to the image/video registry. Registry model data, not extractable logic; frozen at new size.",
- "_rebaseline_2026_07_24_8388_compression_detail_persist": "#8388 (compression engine DETAIL settings — Headroom/session-dedup/CCR — dropped on save) own growth: src/lib/db/compression.ts 866->872 (+6 = irreducible call-site wiring at the existing getCompressionSettings/updateCompressionSettings chokepoint: one import line, one `...buildDetailConfigDefaults()` spread in the seed config, and one `case \\\"sessionDedup\\\": case \\\"ccr\\\": applyDetailConfigUpdate(config, key, parsed); break;` load-switch case, mirroring the existing headroom/#8056 case immediately above it). The actual normalizer logic (normalizeSessionDedupConfig/normalizeCcrConfig, matching SESSION_DEDUP_SCHEMA/CCR_SCHEMA bounds) was EXTRACTED into a new leaf src/lib/db/compressionDetailNormalizers.ts (well under cap) so this frozen file only carries the minimal dispatch wiring. Covered by tests/unit/8388-compression-detail-persist.test.ts (schema-accept + full DB save->reload round-trip for both new sub-objects, plus a no-regression assertion on the existing headroom round-trip).",
- "_rebaseline_2026_07_24_responses_toolcalls_log_summary": "hartmark, fix/responses-tool-calls-log-summary own growth: open-sse/translator/response/openai-responses.ts 1163->1174 (+11). closeToolCall() now also writes the completed tool call into the shared state.toolCalls Map (already populated by the openai-to-claude / claude-to-openai / gemini-to-openai response translators) so stream.ts's completion-log summary builder (which reads state.toolCalls, not this translator's own funcCallIds/funcNames/funcArgsBuf bookkeeping) reports finish_reason \\\"tool_calls\\\" and message.tool_calls for openai->openai-responses translated streams instead of always logging \\\"stop\\\" with no tool_calls — the actual client-facing SSE events were already correct; only the persisted call-log summary was wrong. Irreducible call-site addition at the existing tool-call-close chokepoint. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts.",
- "_rebaseline_2026_07_25_8476_combo_input_bound_homogeneous_scope": "PR #8476 (herjarsa, fix/8375-8459-combo-image-fixes, #8375) own growth: open-sse/services/combo.ts 3642->3679 (+37 net: +29 the PR's own isInputBoundFailure short-circuit for deterministic context_length_exceeded/context_window_exceeded failures, +8 a /green-prs pre-merge fix scoping that short-circuit to homogeneous remainders only — the shipped code fired unconditionally on ANY target, regressing the intentional heterogeneous-combo fallback #6637/isContextOverflow400 protects, exactly as flagged by this PR's own review evidence but never actually implemented in the branch). The fix compares orderedTargets[i+1..] modelStr against the failing target's modelStr at the existing executeTarget dispatch chokepoint (mirrors the sameProviderNext precedent a few lines below) — irreducible call-site wiring, not extractable without hiding the dispatch boundary. Covered by tests/unit/combo-input-bound-failure-8375.test.ts (homogeneous pool still short-circuits) and the new tests/unit/combo-input-bound-heterogeneous-8375.test.ts (heterogeneous combo now correctly falls through to the larger-context target).",
- "_rebaseline_2026_07_25_adobe_firefly_reference_images": "Follow-up to #8006: storage upload + referenceBlobs for image/video and /v1/images/edits dispatch. adobeFireflyClient.ts 1958->2317 (+upload helpers, extract sources, resolve blob ids). Note: 2317 not 2316 — check-file-size.mjs counts LOC via split(\\\"\\\\n\\\").length (counts the trailing-newline empty element), which is 1 higher than `wc -l` on a file ending in \\\\n; the PR's original entry (2316) was measured with wc -l and undercounted by 1 against the actual gate.",
- "_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
- "_rebaseline_pr4592_exclude_exhausted_auto": "Reconcile #4592 already-merged growth: combo.ts 2991->3036 (+45, terminal-status quota-cutoff exclusion in buildAutoCandidates + opt-in gate). Fast-gate PR->release does not run check:file-size.",
- "open-sse/executors/antigravity.ts": "1528",
- "open-sse/executors/base.ts": "1640",
- "open-sse/executors/chatgpt-web.ts": "3241",
- "open-sse/executors/codex.ts": "1562",
- "open-sse/executors/cursor.ts": "1563",
- "open-sse/executors/deepseek-web.ts": "1148",
- "open-sse/executors/grok-web.ts": "1044",
- "open-sse/executors/muse-spark-web.ts": "1405",
- "open-sse/handlers/chatCore.ts": "5034",
- "open-sse/handlers/imageGeneration.ts": "3101",
- "open-sse/handlers/responseSanitizer.ts": "1128",
- "open-sse/handlers/search.ts": "1536",
- "open-sse/handlers/videoGeneration.ts": "1063",
- "open-sse/mcp-server/schemas/tools.ts": "1553",
- "open-sse/mcp-server/server.ts": "1448",
- "open-sse/mcp-server/tools/advancedTools.ts": "1120",
- "open-sse/services/accountFallback.ts": "1978",
- "open-sse/services/adobeFireflyClient.ts": "2385",
- "open-sse/services/claudeCodeCompatible.ts": "1202",
- "open-sse/services/combo.ts": "3648",
- "open-sse/services/compression/strategySelector.ts": "1060",
- "open-sse/services/rateLimitManager.ts": "1167",
- "open-sse/translator/response/openai-responses.ts": "1204",
- "open-sse/utils/cursorAgentProtobuf.ts": "1505",
- "open-sse/utils/stream.ts": "2889",
- "src/app/(dashboard)/dashboard/HomePageClient.tsx": "1388",
- "src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": "1031",
- "src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": "3117",
- "src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": "1067",
- "src/app/(dashboard)/dashboard/combos/page.tsx": "4703",
- "src/app/(dashboard)/dashboard/costs/CostOverviewTab.tsx": "1283",
- "src/app/(dashboard)/dashboard/costs/quota-share/components/PoolWizard.tsx": "1022",
- "src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx": "2615",
- "src/app/(dashboard)/dashboard/health/page.tsx": "1165",
- "src/app/(dashboard)/dashboard/providers/[id]/components/modals/EditConnectionModal.tsx": "1324",
- "src/app/(dashboard)/dashboard/providers/page.tsx": "1944",
- "src/app/(dashboard)/dashboard/runtime/RuntimePageClient.tsx": "1201",
- "src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": "1019",
- "src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": "1470",
- "src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": "1123",
- "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": "1629",
- "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": "1573",
- "src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": "1028",
- "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": "2148",
- "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": "1119",
- "src/app/api/providers/[id]/models/route.ts": "2361",
- "src/app/api/v1/models/catalog.ts": "1597",
- "src/lib/tokenHealthCheck.ts": "1053",
- "src/lib/db/apiKeys.ts": "1529",
- "src/lib/db/core.ts": "1639",
- "src/lib/db/migrationRunner.ts": "1094",
- "src/lib/db/models.ts": "1097",
- "src/lib/db/providers.ts": "1034",
- "src/lib/memory/retrieval.ts": "1073",
- "src/lib/tailscaleTunnel.ts": "1202",
- "src/lib/usage/providerLimits.ts": "1013",
- "src/shared/components/OAuthModal.tsx": "1134",
- "src/shared/components/RequestLoggerV2.tsx": "1629",
- "src/shared/components/analytics/charts.tsx": "1035",
- "src/shared/services/cliRuntime.ts": "1122",
- "src/sse/handlers/chat.ts": "1904",
- "src/sse/services/auth.ts": "2508",
- "tests/unit/account-fallback-service.test.ts": "1572",
- "tests/unit/provider-validation-specialty.test.ts": "2985",
- "open-sse/executors/hyperagent.ts": "1026",
- "open-sse/executors/default.ts": "1042",
- "open-sse/executors/kiro.ts": "1069",
- "open-sse/translator/request/openai-to-kiro.ts": "1057",
- "open-sse/utils/sseHeartbeat.ts": "142",
- "_rebaseline_2026_08_04_9305_sse_comments": "#9305 fix: broadened sseCommentsEnabled()"
+ "_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests."
}
diff --git a/open-sse/services/adobeFireflyChromeRuntime.ts b/open-sse/services/adobeFireflyChromeRuntime.ts
deleted file mode 100644
index 2b727852ed..0000000000
--- a/open-sse/services/adobeFireflyChromeRuntime.ts
+++ /dev/null
@@ -1,1179 +0,0 @@
-/**
- * Adobe Firefly optional Chrome (CDP) session runtime.
- *
- * Default product path is the same as other OmniRoute web-cookie providers
- * (notion-web, perplexity-web, …): pure HTTP with the pasted Cookie/JWT — NO browser.
- *
- * Browser warm is OPT-IN for proactive use (`ADOBE_FIREFLY_BROWSER_REFRESH=1`) and may
- * also run mid-batch 408 recovery via `allowWithoutEnvOptIn`.
- *
- * **Mode (critical for colligo):** Forter risk scores reject Chrome `--headless=new`.
- * Live verification: headless warm → still 408; off-screen **headed** Chrome → 200.
- * Default is therefore **off-screen headed** (window parked at -32000,-32000 — not visible
- * on a normal desktop). Opt into true headless only with `ADOBE_FIREFLY_CHROME_HEADLESS=1`
- * (known-broken for generate). Debug on-screen: `ADOBE_FIREFLY_CHROME_VISIBLE=1`.
- */
-
-import { spawn, type ChildProcess } from "node:child_process";
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
-import { join } from "node:path";
-import {
- buildAdobeArpSessionIdFromCookies,
- extractAdobeForterTimestampMs,
- mergeAdobeCookieHeaders,
- type AdobeFireflySession,
-} from "./adobeFireflySession.ts";
-import {
- extractAdobeCookieHeader,
- isAdobeUserAccessToken,
- looksLikeAdobeJwt,
- decodeAdobeJwtPayload,
-} from "./adobeFireflyClient.ts";
-
-const DEFAULT_CDP_PORT = Number(process.env.ADOBE_FIREFLY_CHROME_CDP_PORT || 9334);
-const PROFILE_DIR_NAME = "adobe-chrome-profile";
-
-type Log = { info?: (...a: unknown[]) => void; warn?: (...a: unknown[]) => void };
-
-type RuntimeState = {
- port: number;
- profileDir: string;
- chromeProc: ChildProcess | null;
- browser: import("playwright").Browser | null;
- context: import("playwright").BrowserContext | null;
- page: import("playwright").Page | null;
- lastWarmAt: number;
- lastCookieSeed: string;
- /** "offscreen" | "visible" | "headless" */
- mode: string;
-};
-
-let runtime: RuntimeState | null = null;
-let warmChain: Promise = Promise.resolve();
-let startingChrome: Promise | null = null;
-/** Temporary mode override (e.g. force a visible window for interactive sign-in). */
-let modeOverride: "offscreen" | "visible" | "headless" | null = null;
-
-/** Prefer off-screen headed (Forter works). Headless is opt-in and usually rejected. */
-function resolveChromeMode(): "offscreen" | "visible" | "headless" {
- if (modeOverride) return modeOverride;
- if (process.env.ADOBE_FIREFLY_CHROME_HEADLESS === "1") return "headless";
- // Legacy alias: ADOBE_FIREFLY_CHROME_HEADED=1 meant "show window"
- if (
- process.env.ADOBE_FIREFLY_CHROME_VISIBLE === "1" ||
- process.env.ADOBE_FIREFLY_CHROME_HEADED === "1"
- ) {
- return "visible";
- }
- return "offscreen";
-}
-
-async function safePageWait(page: import("playwright").Page, ms: number): Promise {
- try {
- if (page.isClosed()) return;
- await page.waitForTimeout(ms);
- } catch {
- /* page closed / target destroyed — caller will re-acquire */
- }
-}
-
-async function ensureLivePage(
- context: import("playwright").BrowserContext,
- preferred: import("playwright").Page | null
-): Promise {
- if (preferred && !preferred.isClosed()) {
- try {
- // Touch the page; if target is dead this throws
- void preferred.url();
- return preferred;
- } catch {
- /* fall through */
- }
- }
- const existing =
- context.pages().find((p) => !p.isClosed() && /firefly\.adobe\.com/i.test(p.url())) ||
- context.pages().find((p) => !p.isClosed());
- if (existing) return existing;
- return context.newPage();
-}
-
-function dataDir(): string {
- return (
- String(process.env.DATA_DIR || process.env.OMNIROUTE_DATA_DIR || "").trim() ||
- join(process.cwd(), ".data")
- );
-}
-
-function profileDir(): string {
- // Prefer LOCALAPPDATA when present so the managed Chrome profile survives restarts.
- const local =
- process.env.LOCALAPPDATA ||
- process.env.HOME ||
- process.env.USERPROFILE ||
- "";
- if (local) {
- const p = join(local, "OmniRoute", PROFILE_DIR_NAME);
- try {
- mkdirSync(p, { recursive: true });
- } catch {
- /* ignore */
- }
- return p;
- }
- const p = join(dataDir(), PROFILE_DIR_NAME);
- try {
- mkdirSync(p, { recursive: true });
- } catch {
- /* ignore */
- }
- return p;
-}
-
-function findChromeExecutable(): string | null {
- if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
- return process.env.CHROME_PATH;
- }
- const candidates = [
- "C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
- "C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
- join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
- "/usr/bin/google-chrome",
- "/usr/bin/chromium-browser",
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
- ];
- for (const c of candidates) {
- if (c && existsSync(c)) return c;
- }
- return null;
-}
-
-async function waitForCdp(port: number, timeoutMs: number): Promise {
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const r = await fetch(`http://127.0.0.1:${port}/json/version`);
- if (r.ok) return;
- } catch {
- /* retry */
- }
- await new Promise((r) => setTimeout(r, 350));
- }
- throw new Error(`Chrome CDP not ready on port ${port}`);
-}
-
-async function killPortOwner(port: number): Promise {
- if (process.platform !== "win32") return;
- try {
- const { execSync } = await import("node:child_process");
- execSync(
- `powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"`,
- { stdio: "ignore", timeout: 8000 }
- );
- } catch {
- /* ignore */
- }
-}
-
-function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> {
- const out: Array<{ name: string; value: string }> = [];
- for (const part of String(cookieHeader || "").split(";")) {
- const idx = part.indexOf("=");
- if (idx <= 0) continue;
- let name = part.slice(0, idx).trim();
- let value = part.slice(idx + 1).trim();
- try {
- name = decodeURIComponent(name);
- } catch {
- /* keep */
- }
- if (
- (value.startsWith('"') && value.endsWith('"')) ||
- (value.startsWith("'") && value.endsWith("'"))
- ) {
- value = value.slice(1, -1);
- }
- if (!name || /[\r\n\0]/.test(value)) continue;
- out.push({ name, value });
- }
- return out;
-}
-
-/** Detect whether the process listening on `port` was started with --headless. */
-async function isPortChromeHeadless(port: number): Promise {
- if (process.platform !== "win32") return null;
- try {
- const { execSync } = await import("node:child_process");
- const out = execSync(
- `powershell -NoProfile -Command "$c=Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if(-not $c){exit 2}; $p=Get-CimInstance Win32_Process -Filter (\\"ProcessId=$($c.OwningProcess)\\"); if($p.CommandLine -match 'headless'){Write-Output 'headless'}else{Write-Output 'headed'}"`,
- { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"] }
- ).trim();
- if (out === "headless") return true;
- if (out === "headed") return false;
- return null;
- } catch {
- return null;
- }
-}
-
-async function tryConnectExistingCdp(
- chromium: typeof import("playwright").chromium,
- port: number,
- dir: string,
- desiredMode: string,
- log?: Log
-): Promise {
- try {
- const r = await fetch(`http://127.0.0.1:${port}/json/version`);
- if (!r.ok) return null;
-
- // Never reuse a headless Chrome for recovery — colligo rejects its Forter tokens.
- // Desired "headless" may reuse headless; offscreen/visible must get a headed process.
- if (desiredMode !== "headless") {
- const headless = await isPortChromeHeadless(port);
- if (headless === true) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `existing CDP on ${port} is headless — killing and restarting as ${desiredMode}`
- );
- await killPortOwner(port);
- return null;
- }
- }
-
- const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
- const context = browser.contexts()[0] || (await browser.newContext());
- const page = await ensureLivePage(context, null);
- log?.info?.(
- "ADOBE-FIREFLY",
- `reused existing Chrome CDP port=${port} desiredMode=${desiredMode} pages=${context.pages().length}`
- );
- return {
- port,
- profileDir: dir,
- chromeProc: null,
- browser,
- context,
- page,
- lastWarmAt: 0,
- lastCookieSeed: "",
- mode: desiredMode,
- };
- } catch {
- return null;
- }
-}
-
-/**
- * Chrome remembers last window bounds in the profile. Off-screen warms park the window at
- * ~(-32000,-32000) / secondary-monitor coords — a later "visible" sign-in then opens Firefly
- * off-screen and the user sees nothing. Reset placement on disk before a visible spawn.
- */
-function resetChromeWindowPlacementOnDisk(dir: string, log?: Log): void {
- const candidates = [
- join(dir, "Default", "Preferences"),
- join(dir, "Preferences"),
- ];
- const onScreen = {
- bottom: 960,
- left: 80,
- maximized: false,
- right: 1360,
- top: 60,
- work_area_bottom: 1080,
- work_area_left: 0,
- work_area_right: 1920,
- work_area_top: 0,
- };
- for (const path of candidates) {
- if (!existsSync(path)) continue;
- try {
- const raw = readFileSync(path, "utf8");
- const obj = JSON.parse(raw) as Record;
- const browser = (obj.browser && typeof obj.browser === "object"
- ? (obj.browser as Record)
- : {}) as Record;
- browser.window_placement = onScreen;
- browser.window_placement_popup = onScreen;
- obj.browser = browser;
- // Avoid session restore putting us back off-screen.
- if (obj.profile && typeof obj.profile === "object") {
- (obj.profile as Record).exit_type = "Normal";
- (obj.profile as Record).exited_cleanly = true;
- }
- writeFileSync(path, JSON.stringify(obj), "utf8");
- log?.info?.("ADOBE-FIREFLY", `reset Chrome window_placement on disk (${path})`);
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `could not reset window_placement: ${err instanceof Error ? err.message : String(err)}`
- );
- }
- }
-}
-
-/** After CDP connect, force the browser window onto the primary work area (visible sign-in). */
-async function forceChromeWindowOnScreen(
- browser: import("playwright").Browser,
- page: import("playwright").Page,
- log?: Log
-): Promise {
- try {
- const cdp = await page.context().newCDPSession(page);
- const { windowId } = (await cdp.send("Browser.getWindowForTarget" as "Browser.getWindowForTarget")) as {
- windowId: number;
- };
- await cdp.send("Browser.setWindowBounds" as "Browser.setWindowBounds", {
- windowId,
- bounds: {
- left: 80,
- top: 60,
- width: 1280,
- height: 900,
- windowState: "normal",
- },
- });
- await page.bringToFront().catch(() => {});
- // Best-effort Windows focus (Chrome can open behind the host app).
- if (process.platform === "win32") {
- try {
- const { execSync } = await import("node:child_process");
- execSync(
- `powershell -NoProfile -Command "$p=Get-Process chrome -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -match 'Firefly|Adobe|Chrome' } | Select-Object -First 1; if($p){ Add-Type -Name W -Namespace N -MemberDefinition '[DllImport(\\\"user32.dll\\\")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport(\\\"user32.dll\\\")] public static extern bool ShowWindow(IntPtr h,int n);'; [N.W]::ShowWindow($p.MainWindowHandle,9) | Out-Null; [N.W]::SetForegroundWindow($p.MainWindowHandle) | Out-Null }"`,
- { stdio: "ignore", timeout: 5000 }
- );
- } catch {
- /* ignore */
- }
- }
- log?.info?.("ADOBE-FIREFLY", "forced Chrome window on-screen (80,60 1280x900)");
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `forceChromeWindowOnScreen failed: ${err instanceof Error ? err.message : String(err)}`
- );
- }
-}
-
-async function ensureChromeStarted(
- log?: Log,
- opts?: { forceRestart?: boolean }
-): Promise {
- const mode = resolveChromeMode();
-
- // Always kill the CDP port on forceRestart (even if in-memory runtime is null — leftover
- // off-screen Chrome from a prior warm is the usual "browser didn't appear" case).
- if (opts?.forceRestart) {
- try {
- await runtime?.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- }
-
- if (runtime?.browser && runtime.context) {
- // If cached runtime mode is headless but we need headed Forter, restart.
- if (runtime.mode === "headless" && mode !== "headless") {
- log?.warn?.("ADOBE-FIREFLY", "cached Chrome is headless — restarting headed for Forter");
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- } else {
- try {
- await fetch(`http://127.0.0.1:${runtime.port}/json/version`);
- // Double-check process still headed when we need it
- if (mode !== "headless") {
- const hl = await isPortChromeHeadless(runtime.port);
- if (hl === true) {
- log?.warn?.("ADOBE-FIREFLY", "live CDP became headless — restarting");
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- } else {
- runtime.page = await ensureLivePage(runtime.context, runtime.page);
- return runtime;
- }
- } else {
- runtime.page = await ensureLivePage(runtime.context, runtime.page);
- return runtime;
- }
- } catch {
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- }
- }
- }
-
- if (startingChrome) return startingChrome;
-
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") {
- throw new Error("ADOBE_FIREFLY_BROWSER_REFRESH=0");
- }
-
- startingChrome = (async () => {
- const chromePath = findChromeExecutable();
- if (!chromePath) throw new Error("Google Chrome not found (set CHROME_PATH)");
-
- let chromium: typeof import("playwright").chromium;
- try {
- chromium = (await import("playwright")).chromium;
- } catch {
- throw new Error("playwright package not available for CDP connect");
- }
-
- const port = DEFAULT_CDP_PORT;
- const dir = profileDir();
-
- // Prefer reusing a healthy headed CDP (do NOT kill mid-warm — was causing "page closed").
- // Never reuse headless when desired mode is offscreen/visible.
- if (!opts?.forceRestart) {
- const existing = await tryConnectExistingCdp(chromium, port, dir, mode, log);
- if (existing) {
- runtime = existing;
- return existing;
- }
- }
-
- // Kill stale listener before spawn (headless leftover / force restart).
- await killPortOwner(port);
-
- // Visible sign-in: wipe off-screen bounds left by prior off-screen warms.
- if (mode === "visible") {
- resetChromeWindowPlacementOnDisk(dir, log);
- }
-
- // Off-screen headed is the default: Forter accepts real Chrome; headless=new is rejected.
- const args = [
- `--remote-debugging-port=${port}`,
- `--user-data-dir=${dir}`,
- "--no-first-run",
- "--no-default-browser-check",
- "--disable-blink-features=AutomationControlled",
- "--disable-features=TranslateUI",
- "--disable-session-crashed-bubble",
- "--hide-crash-restore-bubble",
- ...(mode === "headless"
- ? ["--headless=new", "--disable-gpu", "--window-size=1280,900"]
- : mode === "offscreen"
- ? [
- "--window-position=-32000,-32000",
- "--window-size=1280,900",
- // Start minimized as extra belt-and-suspenders (Windows may still create a taskbar entry).
- "--start-minimized",
- ]
- : [
- // Explicit on-screen position — profile restore alone is not enough.
- "--window-position=80,60",
- "--window-size=1280,900",
- "--start-maximized",
- ]),
- mode === "visible" ? "https://firefly.adobe.com/" : "https://firefly.adobe.com/generate/image",
- ];
-
- log?.info?.(
- "ADOBE-FIREFLY",
- `starting Chrome CDP profile=${dir} port=${port} mode=${mode} (offscreen=headed parked off-display; visible=on-screen sign-in)`
- );
- const chromeProc = spawn(chromePath, args, {
- stdio: "ignore",
- detached: true,
- // Hide the spawn console only; Chrome UI must remain visible for sign-in.
- windowsHide: mode !== "visible",
- });
- chromeProc.unref();
-
- await waitForCdp(port, 45_000);
- const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
- const context = browser.contexts()[0] || (await browser.newContext());
- const page = await ensureLivePage(context, null);
-
- if (mode === "visible") {
- await forceChromeWindowOnScreen(browser, page, log);
- }
-
- runtime = {
- port,
- profileDir: dir,
- chromeProc,
- browser,
- context,
- page,
- lastWarmAt: 0,
- lastCookieSeed: "",
- mode,
- };
- return runtime;
- })();
-
- try {
- return await startingChrome;
- } finally {
- startingChrome = null;
- }
-}
-
-async function seedCookies(
- context: import("playwright").BrowserContext,
- cookieHeader: string
-): Promise {
- const pairs = parseCookieHeader(cookieHeader);
- let n = 0;
- for (const { name, value } of pairs) {
- for (const domain of [".adobe.com", "firefly.adobe.com", ".firefly.adobe.com"]) {
- try {
- await context.addCookies([
- { name, value, domain, path: "/", secure: true, sameSite: "Lax" },
- ]);
- n++;
- break;
- } catch {
- /* try next domain */
- }
- }
- }
- return n;
-}
-
-function extractUserJwtFromStorageRaw(raw: string): string {
- const matches = String(raw || "").match(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g) || [];
- for (const tok of matches) {
- if (looksLikeAdobeJwt(tok) && isAdobeUserAccessToken(tok)) return tok;
- }
- return "";
-}
-
-async function readSpaUserJwt(page: import("playwright").Page): Promise {
- const tokens = await page.evaluate(() => {
- const out: string[] = [];
- for (const key of Object.keys(sessionStorage)) {
- if (!/adobeid_ims_access_token|clio-playground/i.test(key)) continue;
- out.push(sessionStorage.getItem(key) || "");
- }
- return out;
- });
- for (const raw of tokens) {
- const tok = extractUserJwtFromStorageRaw(raw);
- if (tok) return tok;
- }
- // broader scan
- const all = await page.evaluate(() => {
- const out: string[] = [];
- for (const key of Object.keys(sessionStorage)) out.push(sessionStorage.getItem(key) || "");
- return out;
- });
- for (const raw of all) {
- const tok = extractUserJwtFromStorageRaw(raw);
- if (tok) return tok;
- }
- return "";
-}
-
-async function injectUserJwt(page: import("playwright").Page, token: string): Promise {
- if (!token) return;
- await page
- .evaluate((t) => {
- for (const key of Object.keys(sessionStorage)) {
- if (!key.includes("adobeid_ims_access_token")) continue;
- try {
- const obj = JSON.parse(sessionStorage.getItem(key) || "{}") as Record;
- obj.tokenValue = t;
- obj.access_token = t;
- obj.valid = true;
- obj.expire = Date.now() + 20 * 3600 * 1000;
- obj.expires_in = 86400000;
- obj.client_id = "clio-playground-web";
- sessionStorage.setItem(key, JSON.stringify(obj));
- } catch {
- /* skip */
- }
- }
- }, token)
- .catch(() => {});
-}
-
-async function humanize(page: import("playwright").Page): Promise {
- try {
- if (page.isClosed()) return;
- for (let i = 0; i < 16; i++) {
- if (page.isClosed()) return;
- await page.mouse.move(100 + i * 45, 160 + (i % 5) * 35, { steps: 4 });
- await safePageWait(page, 80);
- }
- // Light scroll nudges Forter / passive listeners on real headed Chrome.
- await page.mouse.wheel(0, 240).catch(() => {});
- await safePageWait(page, 200);
- await page.mouse.wheel(0, -120).catch(() => {});
- } catch {
- /* ignore */
- }
-}
-
-/** Poll jar until forterToken timestamp advances past `minTs`, or timeout. */
-async function waitForFresherForter(
- context: import("playwright").BrowserContext,
- minTs: number,
- timeoutMs: number,
- log?: Log
-): Promise {
- const start = Date.now();
- let best = 0;
- while (Date.now() - start < timeoutMs) {
- const cookie = await jarCookieHeader(context);
- const ts = extractAdobeForterTimestampMs(cookie);
- if (ts > best) best = ts;
- if (ts > minTs) {
- log?.info?.(
- "ADOBE-FIREFLY",
- `Chrome forter refreshed (ts=${ts}, deltaMs=${ts - minTs})`
- );
- return ts;
- }
- await new Promise((r) => setTimeout(r, 1500));
- }
- log?.warn?.(
- "ADOBE-FIREFLY",
- `Chrome forter did not advance past ${minTs} within ${timeoutMs}ms (best=${best})`
- );
- return best;
-}
-
-async function jarCookieHeader(context: import("playwright").BrowserContext): Promise {
- const jar = await context.cookies();
- // Prefer firefly-relevant cookies; keep full jar for rebuild pieces
- return jar.map((c) => `${c.name}=${c.value}`).join("; ");
-}
-
-async function buildArpFromContext(
- context: import("playwright").BrowserContext,
- page: import("playwright").Page
-): Promise<{ arp: string; cookie: string }> {
- const cookie = await jarCookieHeader(context);
- const ls = await page
- .evaluate(() => ({
- bfp: localStorage.getItem("bfp") || "",
- fpjs: localStorage.getItem("fpjs") || "",
- }))
- .catch(() => ({ bfp: "", fpjs: "" }));
- let blob = cookie;
- if (ls.bfp && !/(?:^|;\s*)bfp=/.test(blob)) blob = mergeAdobeCookieHeaders(blob, `bfp=${ls.bfp}`);
- if (ls.fpjs && !/(?:^|;\s*)fpjs=/.test(blob)) {
- blob = mergeAdobeCookieHeaders(blob, `fpjs=${encodeURIComponent(ls.fpjs)}`);
- }
- const arp =
- buildAdobeArpSessionIdFromCookies(blob, {
- bfp: ls.bfp || undefined,
- fpjs: ls.fpjs || undefined,
- }) || "";
- return { arp, cookie: extractAdobeCookieHeader(blob) || blob };
-}
-
-/**
- * Warm (or create) the durable Chrome Firefly session.
- * Returns accessToken + cookie + arpSessionId ready for generate-async.
- */
-export async function warmAdobeFireflyViaChrome(opts: {
- cookie: string;
- accessToken?: string;
- log?: Log;
- /** Wait for interactive login if only guest JWT is present (ms, 0 = don't wait). */
- waitForLoginMs?: number;
- /**
- * Mid-batch 408 recovery: allow warm without ADOBE_FIREFLY_BROWSER_REFRESH=1.
- * Uses off-screen headed Chrome by default (Forter-safe; no normal-desktop window).
- */
- allowWithoutEnvOptIn?: boolean;
- /** When true (or ADOBE_FIREFLY_CHROME_PING=1), prove ARP with in-page generate-async. */
- proveWithPing?: boolean;
-}): Promise {
- // Kill switch
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") return null;
- // Default OFF for proactive use; recovery may pass allowWithoutEnvOptIn.
- if (!opts.allowWithoutEnvOptIn && process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "1") return null;
- if (process.env.NODE_ENV === "test" || process.env.VITEST || process.env.NODE_TEST_CONTEXT) {
- return null;
- }
-
- const run = warmChain.then(async () => {
- const log = opts.log;
- const cookieIn = extractAdobeCookieHeader(opts.cookie) || opts.cookie;
- if (!cookieIn?.trim() && !opts.accessToken) return null;
-
- const forterBefore = extractAdobeForterTimestampMs(cookieIn);
- // Recovery path always prefers a fresh headed Chrome (stale headless CDP is poison).
- const rt = await ensureChromeStarted(log, {
- forceRestart: Boolean(opts.allowWithoutEnvOptIn) || process.env.ADOBE_FIREFLY_CHROME_FORCE_RESTART === "1",
- });
- const context = rt.context!;
- let page = await ensureLivePage(context, rt.page);
-
- if (cookieIn && cookieIn !== rt.lastCookieSeed) {
- const n = await seedCookies(context, cookieIn);
- rt.lastCookieSeed = cookieIn;
- log?.info?.("ADOBE-FIREFLY", `Chrome seeded ${n} cookie entries`);
- }
-
- // Navigate / reload with page-closed recovery (prior flaky "Target page closed").
- const gotoFirefly = async () => {
- page = await ensureLivePage(context, page);
- if (!/firefly\.adobe\.com/i.test(page.url())) {
- await page.goto("https://firefly.adobe.com/generate/image", {
- waitUntil: "domcontentloaded",
- timeout: 90_000,
- });
- } else {
- await page.reload({ waitUntil: "domcontentloaded", timeout: 90_000 }).catch(async () => {
- page = await ensureLivePage(context, null);
- await page.goto("https://firefly.adobe.com/generate/image", {
- waitUntil: "domcontentloaded",
- timeout: 90_000,
- });
- });
- }
- };
-
- await gotoFirefly();
- await safePageWait(page, 8_000);
- await humanize(page);
-
- let jwt = await readSpaUserJwt(page).catch(() => "");
- if (!jwt && opts.accessToken && isAdobeUserAccessToken(opts.accessToken)) {
- page = await ensureLivePage(context, page);
- await injectUserJwt(page, opts.accessToken);
- await page.reload({ waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => {});
- await safePageWait(page, 6_000);
- await humanize(page);
- jwt = (await readSpaUserJwt(page).catch(() => "")) || opts.accessToken;
- log?.info?.("ADOBE-FIREFLY", "Chrome injected cached user JWT into SPA sessionStorage");
- }
-
- // Wait for interactive login if still no user JWT (one-time profile SSO)
- const waitMs = opts.waitForLoginMs ?? Number(process.env.ADOBE_FIREFLY_LOGIN_WAIT_MS || 0);
- if (!jwt && waitMs > 0) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `No user JWT yet — sign in to Firefly in the Chrome window (wait ${Math.round(waitMs / 1000)}s)`
- );
- const start = Date.now();
- while (Date.now() - start < waitMs) {
- await safePageWait(page, 2000);
- page = await ensureLivePage(context, page);
- jwt = await readSpaUserJwt(page).catch(() => "");
- if (jwt) break;
- }
- }
-
- if (!jwt && opts.accessToken && isAdobeUserAccessToken(opts.accessToken)) {
- jwt = opts.accessToken;
- }
- if (!jwt || !isAdobeUserAccessToken(jwt)) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: still no AdobeID user JWT (cookie-only guest)");
- // Still return ARP if possible — caller may already have JWT
- if (!opts.accessToken) return null;
- jwt = opts.accessToken;
- }
-
- // Give Forter SDK time to mint a NEW forterToken (stale paste is the usual 408 root cause).
- const forterWaitMs = Number(process.env.ADOBE_FIREFLY_FORTER_WAIT_MS || 45_000);
- await waitForFresherForter(context, forterBefore, forterWaitMs, log);
-
- // Second humanize + short settle after token land
- page = await ensureLivePage(context, page);
- await humanize(page);
- await safePageWait(page, 2_000);
-
- let { arp, cookie } = await buildArpFromContext(context, page);
- if (!arp) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: could not rebuild ARP from jar — one more reload");
- await gotoFirefly();
- await safePageWait(page, 8_000);
- await humanize(page);
- await waitForFresherForter(context, forterBefore, 20_000, log);
- ({ arp, cookie } = await buildArpFromContext(context, page));
- }
- if (!arp) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: could not rebuild ARP from jar");
- return null;
- }
-
- // Prove colligo accepts this ARP. Default ON for recovery path; env can force either way.
- const shouldPing =
- opts.proveWithPing === true ||
- process.env.ADOBE_FIREFLY_CHROME_PING === "1" ||
- (opts.allowWithoutEnvOptIn && process.env.ADOBE_FIREFLY_CHROME_PING !== "0");
- if (shouldPing) {
- page = await ensureLivePage(context, page);
- const ok = await pingGenerateInPage(page, jwt, arp, log);
- if (!ok) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- "Chrome ping generate failed — waiting for forter once more and rebuilding ARP"
- );
- await waitForFresherForter(context, extractAdobeForterTimestampMs(cookie), 20_000, log);
- ({ arp, cookie } = await buildArpFromContext(context, page));
- if (arp) {
- page = await ensureLivePage(context, page);
- const ok2 = await pingGenerateInPage(page, jwt, arp, log);
- if (!ok2) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome ping still failed — returning ARP for node retry");
- }
- }
- }
- }
-
- rt.page = page;
- rt.lastWarmAt = Date.now();
- const ftrTs = extractAdobeForterTimestampMs(cookie);
- log?.info?.(
- "ADOBE-FIREFLY",
- `Chrome warm OK (mode=${rt.mode}, arpLen=${arp.length}, forterTs=${ftrTs || 0}, forterDeltaMs=${ftrTs && forterBefore ? ftrTs - forterBefore : "n/a"}, user=${String(decodeAdobeJwtPayload(jwt)?.user_id || "").slice(0, 20)})`
- );
-
- return {
- accessToken: jwt,
- cookie,
- arpSessionId: arp,
- tokenExpiresAt: (() => {
- const p = decodeAdobeJwtPayload(jwt);
- const created = Number(p?.created_at || 0);
- const exp = Number(p?.expires_in || 0);
- return created && exp ? created + exp : Date.now() + 20 * 3600_000;
- })(),
- updatedAt: Date.now(),
- fingerprint: "chrome",
- source: "browser" as const,
- };
- });
-
- // Serialize warms
- warmChain = run.then(
- () => undefined,
- () => undefined
- );
- try {
- return await run;
- } catch (err) {
- opts.log?.warn?.(
- "ADOBE-FIREFLY",
- `Chrome warm failed: ${err instanceof Error ? err.message : String(err)}`
- );
- // Soft-reset page/browser handle but do not kill Chrome process — reuse next warm.
- if (runtime) {
- runtime.page = null;
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime.browser = null;
- runtime.context = null;
- }
- runtime = null;
- return null;
- }
-}
-
-/**
- * Wipe Adobe SSO from the managed profile so "Add Account" can log into a *new* identity
- * instead of silently reusing the previous Adobe session.
- */
-async function clearAdobeBrowserSession(
- context: import("playwright").BrowserContext,
- page: import("playwright").Page,
- log?: Log
-): Promise {
- try {
- await context.clearCookies();
- } catch {
- /* ignore */
- }
- try {
- await page.goto("https://firefly.adobe.com/", { waitUntil: "domcontentloaded", timeout: 60_000 });
- await page
- .evaluate(() => {
- try {
- sessionStorage.clear();
- } catch {
- /* ignore */
- }
- try {
- localStorage.clear();
- } catch {
- /* ignore */
- }
- })
- .catch(() => {});
- } catch {
- /* ignore */
- }
- // Best-effort IMS logout so the next load shows the sign-in UI.
- try {
- await page.goto(
- "https://auth.services.adobe.com/en_US/index.html?callback=https%3A%2F%2Ffirefly.adobe.com%2F",
- {
- waitUntil: "domcontentloaded",
- timeout: 45_000,
- }
- );
- await safePageWait(page, 1500);
- } catch {
- /* ignore */
- }
- log?.info?.("ADOBE-FIREFLY", "sign-in: cleared prior Adobe session for a fresh login");
-}
-
-/**
- * Interactive one-time sign-in for the "browser session" credential model.
- * Opens a VISIBLE managed Chrome (persistent profile), navigates to Firefly, and waits for the
- * user to log in. Returns the IMS JWT + cookie jar so generate works immediately without
- * depending on sessionStorage surviving a browser close.
- * Never throws — returns { success:false } on timeout / unavailable.
- */
-export async function loginAdobeFireflyViaChrome(opts: {
- cookie?: string;
- /** Max time to wait for the user to complete login (ms). Default 5 min. */
- waitForLoginMs?: number;
- /**
- * When true (default for "Add Account"), wipe the prior Adobe SSO so a *new* account can be
- * signed in instead of reopening the previous logged-in profile.
- */
- freshSession?: boolean;
- log?: Log;
-}): Promise<{
- success: boolean;
- account?: string;
- accessToken?: string;
- cookie?: string;
- arpSessionId?: string;
-}> {
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") {
- return { success: false };
- }
- const log = opts.log;
- const prev = modeOverride;
- modeOverride = "visible";
- const fresh = opts.freshSession !== false; // default true for multi-account Add Account
- try {
- // Fresh visible window (a cached off-screen CDP would be parked off-display for login).
- // forceRestart ALWAYS kills port 9334 + restarts with on-screen bounds.
- const rt = await ensureChromeStarted(log, { forceRestart: true });
- const context = rt.context!;
- let page = await ensureLivePage(context, rt.page);
-
- // Re-assert on-screen + foreground (profile may re-apply bad bounds after first paint).
- await forceChromeWindowOnScreen(rt.browser!, page, log);
-
- if (fresh) {
- await clearAdobeBrowserSession(context, page, log);
- page = await ensureLivePage(context, null);
- rt.lastCookieSeed = "";
- } else {
- const cookieIn = opts.cookie ? extractAdobeCookieHeader(opts.cookie) || opts.cookie : "";
- if (cookieIn) {
- const n = await seedCookies(context, cookieIn);
- rt.lastCookieSeed = cookieIn;
- log?.info?.("ADOBE-FIREFLY", `sign-in: seeded ${n} cookie entries as a hint`);
- }
- }
-
- await page
- .goto("https://firefly.adobe.com/", { waitUntil: "domcontentloaded", timeout: 90_000 })
- .catch(() => {});
- page = await ensureLivePage(context, page);
- await forceChromeWindowOnScreen(rt.browser!, page, log);
- log?.info?.(
- "ADOBE-FIREFLY",
- `sign-in: Chrome window open ON-SCREEN (fresh=${fresh}) — waiting for Adobe login…`
- );
-
- const waitMs = opts.waitForLoginMs ?? Number(process.env.ADOBE_FIREFLY_LOGIN_WAIT_MS || 300_000);
- const start = Date.now();
- let jwt = "";
- while (Date.now() - start < waitMs) {
- await safePageWait(page, 2500);
- page = await ensureLivePage(context, page);
- jwt = await readSpaUserJwt(page).catch(() => "");
- if (jwt && isAdobeUserAccessToken(jwt)) break;
- }
- const ok = Boolean(jwt && isAdobeUserAccessToken(jwt));
- const account = ok ? String(decodeAdobeJwtPayload(jwt)?.user_id || "") : undefined;
-
- // Capture durable credentials BEFORE closing the window (sessionStorage JWT dies with the tab).
- let cookie = "";
- let arpSessionId = "";
- if (ok) {
- try {
- const built = await buildArpFromContext(context, page);
- cookie = extractAdobeCookieHeader(built.cookie) || built.cookie || "";
- arpSessionId = built.arp || "";
- } catch {
- cookie = (await jarCookieHeader(context).catch(() => "")) || "";
- }
- }
-
- log?.info?.(
- "ADOBE-FIREFLY",
- ok
- ? `sign-in OK (account=${account?.slice(0, 24)}, cookieLen=${cookie.length}, arpLen=${arpSessionId.length})`
- : "sign-in timed out — no AdobeID session"
- );
-
- // Close the visible window; the persistent profile keeps the SSO for off-screen warms.
- try {
- await rt.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- return {
- success: ok,
- account,
- accessToken: ok ? jwt : undefined,
- cookie: ok ? cookie : undefined,
- arpSessionId: ok ? arpSessionId : undefined,
- };
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `sign-in failed: ${err instanceof Error ? err.message : String(err)}`
- );
- try {
- await runtime?.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- return { success: false };
- } finally {
- modeOverride = prev;
- }
-}
-
-async function pingGenerateInPage(
- page: import("playwright").Page,
- token: string,
- arp: string,
- log?: Log
-): Promise {
- try {
- const res = await page.evaluate(
- async ({ token, arp }) => {
- const claims = JSON.parse(
- atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
- ) as { user_id?: string };
- const prompt = "ping";
- const data = new TextEncoder().encode(String(claims.user_id || "") + "-" + prompt);
- const hash = await crypto.subtle.digest("SHA-256", data);
- const nonce = [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
- const r = await fetch("https://firefly-3p.ff.adobe.io/v2/3p-images/generate-async", {
- method: "POST",
- headers: {
- Authorization: "Bearer " + token,
- "x-api-key": "clio-playground-web",
- "content-type": "application/json",
- accept: "*/*",
- "x-nonce": nonce,
- "x-arp-session-id": arp,
- },
- credentials: "include",
- body: JSON.stringify({
- n: 1,
- seeds: [1],
- output: { storeInputs: true },
- prompt,
- referenceBlobs: [],
- modelSpecificPayload: { size: "auto" },
- modelId: "gpt-image",
- modelVersion: "2",
- generationMetadata: { module: "text2image", submodule: "ff-image-generate" },
- generationSettings: { detailLevel: 1 },
- }),
- });
- return { status: r.status, body: (await r.text()).slice(0, 120) };
- },
- { token, arp }
- );
- log?.info?.("ADOBE-FIREFLY", `Chrome ping generate status=${res.status}`);
- return res.status === 200 || res.status === 202;
- } catch (e) {
- log?.warn?.("ADOBE-FIREFLY", `Chrome ping error: ${e instanceof Error ? e.message : String(e)}`);
- return false;
- }
-}
-
-/**
- * Submit generate-async inside the warmed Chrome page (same TLS/cookie jar as SPA).
- * Falls back to null so caller can use node fetch with the warmed ARP.
- */
-export async function adobeFireflyGenerateInChrome(opts: {
- accessToken: string;
- arpSessionId: string;
- payload: Record;
- prompt: string;
- log?: Log;
-}): Promise<{ status: number; body: string; headers: Record } | null> {
- if (!runtime?.page) return null;
- try {
- const res = await runtime.page.evaluate(
- async ({ token, arp, payload, prompt }) => {
- const claims = JSON.parse(
- atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
- ) as { user_id?: string };
- const data = new TextEncoder().encode(
- String(claims.user_id || "") + "-" + String(prompt || "").slice(0, 256)
- );
- const hash = await crypto.subtle.digest("SHA-256", data);
- const nonce = [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
- const r = await fetch("https://firefly-3p.ff.adobe.io/v2/3p-images/generate-async", {
- method: "POST",
- headers: {
- Authorization: "Bearer " + token,
- "x-api-key": "clio-playground-web",
- "content-type": "application/json",
- accept: "*/*",
- "x-nonce": nonce,
- "x-arp-session-id": arp,
- },
- credentials: "include",
- body: JSON.stringify(payload),
- });
- const headers: Record = {};
- r.headers.forEach((v, k) => {
- headers[k] = v;
- });
- return { status: r.status, body: await r.text(), headers };
- },
- {
- token: opts.accessToken,
- arp: opts.arpSessionId,
- payload: opts.payload,
- prompt: opts.prompt,
- }
- );
- return res;
- } catch (e) {
- opts.log?.warn?.(
- "ADOBE-FIREFLY",
- `in-Chrome generate failed: ${e instanceof Error ? e.message : String(e)}`
- );
- return null;
- }
-}
-
-/** Test helper */
-export function __resetAdobeFireflyChromeRuntimeForTests(): void {
- runtime = null;
- warmChain = Promise.resolve();
-}