From 3c20b3bf4bb0b1da10454d7b7ac61e65aba6b786 Mon Sep 17 00:00:00 2001 From: artickc Date: Mon, 3 Aug 2026 02:17:35 +0300 Subject: [PATCH] chore(adobe-firefly): remove obsolete browser fallback --- config/quality/file-size-baseline.json | 218 +-- .../services/adobeFireflyChromeRuntime.ts | 1179 ----------------- 2 files changed, 37 insertions(+), 1360 deletions(-) delete mode 100644 open-sse/services/adobeFireflyChromeRuntime.ts 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 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(); -}