Files
OmniRoute/CLAUDE.md
Diego Rodrigues de Sa e Souza 555b21d296 Release v3.8.37 (#5053)
* chore(release): open v3.8.37 development cycle

* chore(ci): harden release flow — ratchet decoupling, fast-path drift gates, build-scope guard, heap default (#5054)

Implements improvements 1-4 from the v3.8.36 release benchmark (_tasks/release-bench/v3.8.36/PLANO-MELHORIA.md):

1. Quality Ratchet decoupled from flaky coverage (ci.yml): the shard→coverage→ratchet
   chain meant a single flaky Coverage Shard SKIPPED the whole Quality Ratchet on the
   release PR (v3.8.36 #4854), so cycle drift only surfaced post-merge in #5029. The job
   now runs on !cancelled(); coverage download is continue-on-error and the ratchet runs
   --allow-missing, so the DETERMINISTIC gates (eslint/complexity/cognitive/duplication/
   codeql) stay blocking even when coverage is unavailable.

2. Fast-path drift gates (quality.yml PR→release): added check:complexity,
   check:cognitive-complexity, and a new lightweight check:pack-policy (pack-artifact
   unexpected-files check WITHOUT a build, via --policy-only) so drift + stray-tarball-file
   regressions are caught/rebaselined PER-PR instead of cascading onto the release PR.

3. Build heap default 4096→8192 MB (build-next-isolated.mjs): the clean graph peaks
   ~3.9 GB and brushed the old 4 GB ceiling; 8 GB gives headroom. Comment notes heap is
   NOT the fix for a poisoned scope (run check:build-scope instead).

4. check:build-scope gate (new): fails if .ts/.tsx/.js/.jsx files in the tsconfig scope
   exceed a threshold — catches worktrees/cruft leaking into the build scope (the v3.8.36
   OOM root cause: 355,215 vs 4,547 files) BEFORE it detonates next build. Wired into the
   fast-path.

* fix(auth): only trust forwarding headers from loopback TCP peers (#4689)

Integrated into release/v3.8.37 — loopback-gated forwarding headers (IP spoofing fix). Cherry-picked onto current release tip; ipUtils.test.ts 9/9 green.

* fix(codex): treat OAuth 401 as unrecoverable refresh failure (#4686)

Integrated into release/v3.8.37 — codex OAuth 401 treated as unrecoverable refresh. Cherry-picked onto release tip; token-refresh-service.test.ts 38/38 green.

* fix(translator): preserve reasoning_effort for non-Copilot Responses clients (#4688)

Integrated into release/v3.8.37 — preserve reasoning_effort for non-Copilot Responses clients. Cherry-picked onto release tip; tests 47/47 green.

* fix(translator): coerce tool descriptions to strings in OpenAI normalization (#4675)

Integrated into release/v3.8.37 — coerce tool descriptions to strings in OpenAI normalization. Cherry-picked onto release tip; tests 3/3 green.

* feat(sse): x-omniroute-strip-reasoning header to drop reasoning_content (#4678)

Integrated into release/v3.8.37 — x-omniroute-strip-reasoning header. Cherry-picked onto release tip (resolved chatCore.ts/headers.ts adjacency conflict, kept resolveCompressionHeader + isStripReasoningRequested); tests 8/8 green.

* fix(combo): flatten Anthropic tool messages + tool history to prevent upstream 503 (#4648)

Integrated into release/v3.8.37 — flattenToolHistory helper (combo anti-503). Cherry-picked onto release tip; tests 9/9 green.

* feat(headroom): proxy lifecycle management + dashboard UI (Docker sidecar supported) (#4649)

Integrated into release/v3.8.37 — headroom proxy lifecycle (status/start/stop, local-only + spawn-capable per Rules #15/#17). Cherry-picked onto release tip; lifecycle 7/7 + route-guard 43/43 + check:cycles green.

* feat(cli): multi-model support for Factory Droid CLI (#4682)

Integrated into release/v3.8.37 — Factory Droid multi-model support. Cherry-picked onto release tip (kept readJsoncConfig + droidCustomModels imports); droid-custom-models 11/11 green.

* fix(providers): require Default Model in compatible-provider API-key setup (#4641)

Integrated into release/v3.8.37 — require Default Model in compatible-provider API-key setup. Cherry-picked fix + test-move onto release tip (kept release providerSpecificData + QuotaScrapingFields; fixed moved-test import path; baseline rebaseline unneeded, 865<866); UI test 2/2 green.

* fix(dashboard): stop double-masking already-masked API key in list (E2E 3/9 regression) (#4671)

Integrated into release/v3.8.37 — render server-masked key verbatim (drop redundant maskKey call). Note: release's maskKey already guards '****' (since v3.8.34), so this is a safe simplification; added a contract test pinning the **** passthrough invariant (2/2 green, would fail against the pre-guard maskKey = the historical double-mask bug).

* chore(quality): rebaseline file-size for rc17 PR batch drift

Own growth from the merged rc17 PRs (#4678/#4686/#4688) at existing chokepoints —
cohesive, not extractable:
- open-sse/handlers/responseSanitizer.ts 1103->1122 (SanitizeOpenAIResponseOptions + stripReasoning, #4678)
- open-sse/services/tokenRefresh.ts 2070->2090 (codex 401 unrecoverable-refresh guard, #4686)
- tests/unit/token-refresh-service.test.ts 1322->1353 (401 regression case, #4686)
- tests/unit/translator-openai-responses-req.test.ts 1047->1050 (reasoning_effort assertion, #4688)

* docs(env): document HEADROOM_URL in .env.example + ENVIRONMENT.md

The headroom proxy lifecycle (#4649) reads HEADROOM_URL (src/lib/headroom/detect.ts,
default http://localhost:8787) but it was missing from the env contract, tripping
check:env-doc-sync. Adds the var to both .env.example (commented, has a default) and
the Proxy Health table in ENVIRONMENT.md.

* fix(sse): stream writer mock abort() returns a Promise (#4788)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): fall back to default data dir when DATA_DIR is not writable (#4767)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(oauth): verify Cursor installation on Linux before auto-import (#4770)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): track Ollama streaming usage from raw NDJSON chunks (#4754)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): strip enumDescriptions from antigravity tool schema (#4740)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): include low-level cause details in formatProviderError (#4741)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(translator): strip x-anthropic-billing-header in claude-to-openai (#4728)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): gate Kiro image attachments behind a Claude-capability check (#4763)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): read Antigravity usage from the response.usageMetadata envelope (#4785)

Integrated into release/v3.8.37 — Antigravity response.usageMetadata envelope. Cherry-picked onto release tip (resolved test-tail adjacency with #4754 Ollama block); usage-extractor 23/23 green.

* fix(api): fall back to existing access token for any OAuth provider on refresh failure (#4786)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(cli): verify launchd registration + skip self-SIGTERM in macOS autostart (#4765)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(executors): anthropic-compatible-* gateways get Bearer alongside x-api-key (#4729)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): json_schema fallback for OpenAI-compatible providers (#4766)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): use workos auth token shape for cline (#4787)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* feat(sse): parse Gemini CLI 429 retryDelay from structured RetryInfo (#4738)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; tests green.

* fix(sse): finalize tool_calls finish_reason on early stream end in OpenAI Responses translator (#4764)

Integrated into release/v3.8.37 — computeFinishReason finalizes tool_calls on early stream end (Responses translator). Cherry-picked onto release tip; responses-translation-fixes 29/29 green.

* test(sse): golden-lock provider.ts translate-path across all providers (#4734)

Integrated into release/v3.8.37 — golden-lock for provider.ts translate-path. Cherry-picked onto release tip; snapshot regenerated against the current provider set (UPDATE_GOLDEN=1, 167 entries); golden test 3/3 deterministic.

* chore(quality): rebaseline file-size for rc17 leva2 PR batch drift

Own growth from the merged leva2 PRs (cohesive, not extractable):
- src/lib/usage/providerLimits.ts 950->955 (#4786)
- open-sse/executors/default.ts NEW frozen @828 (#4729 + #4766 + #4787 header branches)
- open-sse/translator/request/openai-to-kiro.ts 807->814 (#4763)
- open-sse/translator/response/openai-responses.ts 923->937 (#4764)
- tests/unit/executor-default-base.test.ts 1339->1440 (#4766)
- tests/unit/translator-openai-to-kiro.test.ts 918->980 (#4763)

* fix(dashboard): align Engine Combos editor engines with API schema (#4955) (#5062)

The named-combos pipeline dropdown offered four engines (headroom,
session-dedup, ccr, llmlingua) that stackedPipelineStepSchema rejects, so
selecting one made PUT /api/context/combos/[id] return HTTP 400 while
saveCombo swallowed the non-OK response (if (!res.ok) return). Editing the
default 'Standard Savings' combo and changing an engine reproduced the 400.

- Add canonical STACKED_PIPELINE_ENGINE_INTENSITIES next to the schema as the
  single source of truth; the client dropdown imports it so it can never drift
  from the discriminated union the API validates against.
- Surface save errors and empty-name/empty-pipeline validation in the editor
  instead of failing silently.
- Add a parity unit test asserting the UI engine map equals the schema union
  and that every (engine, intensity) the UI emits is accepted.

* fix(sse): filter nameless hosted tools when converting Responses API to Chat format (#4789)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): keep desktop sidebar visible via explicit CSS class (#4812)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip enumDescriptions from Antigravity tool schemas (#4813)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(dashboard): resolve passthrough model aliases by providerId in ModelSelectModal (#4815)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(oauth): allow per-connection refresh lead-time override via providerSpecificData.refreshLeadMs (#4818)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip X-Stainless-* headers and normalize SDK User-Agent for OpenAI-compatible endpoints (#4820)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Gemini built-in tools when functionDeclarations present in Antigravity envelope (#4821)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(api): surface a Docker-localhost hint on provider-node validation connection errors (#4822)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve bare model names to connection defaultModel before upstream calls (#4825)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(build): trace-include sql.js sql-wasm.wasm in standalone bundle (#4839)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip Composer <|final|> sentinel markers leaking after Composer reasoning (#4842)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(config): sync full SiliconFlow model list into registry (#4844)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): close reasoning before message content in Responses stream (#4848)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): reject unsupported Kiro [1m] context suffix (#4816)

Integrated into release/v3.8.37 — cherry-picked onto release tip; test-tail conflict with #4763 resolved (kept both image + [1m] test blocks); CHANGELOG re-merged; 29/29 green.

* fix(db): validate HuggingFace tokens via whoami-v2 auth probe (#4819)

Integrated into release/v3.8.37 — defining commit re-homed onto the god-file-split validation module (validateHuggingFaceProvider in validation/openaiFormat.ts + map wiring); 115/115 green.

* fix(sse): make anthropic-version default-guard case-insensitive (#4823)

Integrated into release/v3.8.37 — conflict with #4729 Bearer-fallback resolved (kept both Bearer fallback + case-insensitive anthropic-version guard); 48/48 green.

* fix(sse): sanitize Kiro tool schemas to avoid 400 "Improperly formed request" (#4847)

Integrated into release/v3.8.37 — conflict in kiro-to-openai.ts resolved (kept release fallbackToolCallId + adopted #1375 toolNameMap remap); 7/7 green.

* feat(sse): add GPT-4 to the GitHub Copilot provider (#4798)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add GPT-4o mini to GitHub Copilot provider (#4797)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(api): add MiniMax-M3 pricing row (#4814)

Integrated into release/v3.8.37 — pricing row re-homed onto god-file-split pricing/regional.ts (pricing.ts is now a barrel); 4/4 green.

* fix(cli): save runtime deps with --save-exact so a sibling install can't prune them (#4841)

Integrated into release/v3.8.37 — trayRuntime conflict resolved (kept release SYSTRAY_SPEC + added --save-exact); 2/2 green.

* fix(sse): preserve required fields in antigravity tool schemas (#4843)

Integrated into release/v3.8.37 — conflict resolved (kept #4740/#4813 enumDescriptions strip + typed normalizeSchemaTypes, added required-preservation helpers; test-tail merged keeping both enumDescriptions + required tests); 7/7 green.

* chore(quality): rebaseline file-size for rc17b leva3 PR batch drift

* fix(sse): strip reasoning blobs from agentic context to prevent O(n^2) token growth (#4849)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): unwrap Qoder HTTP 200 SSE error envelope so fallback can trigger (#4850)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): strip temperature for Claude models with extended thinking (#4853)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): emit valid concatenable kiro tool_calls.arguments deltas (#4855)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(sse): add toggleable tool-source diagnostics (#4856)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): redact api key from the AUTH debug log in the chat handler (#4858)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): forward AI SDK image parts in Responses translator (#4859)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): resolve custom combos by id and case-insensitive name (#4446) (#4869)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): exclude WS bridge controller-closed error from provider breaker (#4602) (#4870)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(providers): add xAI Grok inbound translators and thinking patcher (#4910)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(embeddings): add dimensions override field to embedding combos (#4913)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* feat(oauth): Codex bulk-import endpoint — POST /api/oauth/codex/import (#4914)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(antigravity): retry transient upstream failures (#4941)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): surface malformed HTTP-200 upstream responses (#4942)

Integrated into release/v3.8.37 — cherry-picked defining commit onto release tip; CHANGELOG re-merged; tests green.

* fix(sse): normalize Codex custom tools (apply_patch) to { input: string } schema (#4862)

Integrated into release/v3.8.37 — conflict in request/openai-responses.ts resolved (kept #4789 nameless-tool skip + added #1007 custom-tool {input:string} normalization); 48/48 green incl. #4789/#4859 regression.

* fix(sse): dense, deterministic output ordering in Responses API response.completed (#4906)

Integrated into release/v3.8.37 — manual integration with #4862 in response/openai-responses.ts (custom-tool funcItem + dense recordCompletedItem). Fixed a latent #4848 interaction: the close-reasoning-before-message guard force-closed <think>-tag reasoning prematurely, which dense output (#4906) then snapshotted as a partial buffer ("plan" vs "planning") — scoped the guard to native reasoning_content (!inThinking) in BOTH transformer + translator paths. Full Responses suite 203/203 green incl. #4848/#4862 regression.

* feat(sse): auto-promote successful combo model to position #1 (#4852)

Integrated into release/v3.8.37 — dropped the stale file-size-baseline.json hunk (re-derived against the rc17b rebaseline); code+test applied clean; 13/13 green.

* feat(providers): add Pioneer AI (Fastino Labs) provider (#4909)

Integrated into release/v3.8.37 — providers.ts apikey block re-homed onto god-file-split src/shared/constants/providers/apikey/frontier-labs.ts (inline APIKEY_PROVIDERS no longer exists); registry/pioneer + providers/index.ts applied clean; 6/6 green.

* add DGrid AI gateway provider (#4931)

Integrated into release/v3.8.37 — rebased the contributor's commit onto the release tip; providers.ts god-file-split conflict resolved by relocating the dgrid APIKEY_PROVIDERS entry into apikey/gateways.ts; CHANGELOG added. 7/7 green. Thanks @dgridOP!

* chore(quality): rebaseline file-size for rc17b leva4 PR batch drift

* docs(routing): sync combo strategy docs for Fusion (17 strategies) (#5067)

Fusion (16th strategy, panel fan-out + judge synthesis) and headroom
shipped but the strategy-count docs were stale (14/15) and omitted both.
Update every combo-strategy reference to the canonical 17, add fusion +
headroom to all strategy tables, and add a dedicated Fusion section to
AUTO-COMBO.md documenting judgeModel / fusionTuning config + an example.

- CLAUDE.md, README.md, FEATURES.md, RESILIENCE_GUIDE.md,
  ARCHITECTURE.md, OPEN_SSE_ARCHITECTURE.md, OMNIROUTE_VS_ALTERNATIVES.md,
  docs/README.md, request-pipeline.mmd: 14/15 -> 17, list fusion + headroom
- docs/routing/AUTO-COMBO.md: strategy table + new Fusion strategy section
- docs/openapi.yaml: add reset-window, headroom, fusion to the strategy enum

* fix(oauth): classify /api/oauth/cursor/auto-import as local-only (route-guard) (#5070)

The Cursor auto-import route runs execFile("which", ["cursor"]) to verify a
local Cursor install before importing credentials — a child-process spawn. The
check:route-guard-membership gate (Hard Rules #15/#17) flagged it as an
unclassified spawn-capable route: reachable past the loopback gate, an
RCE-via-tunnel surface (a leaked JWT over a tunnel could trigger the spawn).

Classify the specific path in LOCAL_ONLY_API_PREFIXES so loopback enforcement
runs unconditionally before any auth check. Scoped to the exact path — the rest
of /api/oauth/ (browser redirect/callback flows) stays remote-reachable.

TDD: added a failing-then-passing assertion in route-guard-local-prefix.test.ts
(classification + an over-broadening guard proving sibling OAuth paths stay
remote). check:route-guard-membership now reports 0 new gaps.

* chore(release): v3.8.37 — 2026-06-26

---------

Co-authored-by: dgridOP <dgrid_op@outlook.com>
2026-06-26 02:51:06 -03:00

35 KiB
Raw Permalink Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Quick Start

npm install                    # Install deps (auto-generates .env from .env.example)
npm run dev                    # Dev server at http://localhost:20128
npm run build                  # Production build (Next.js 16 standalone)
npm run lint                   # ESLint (0 errors expected; warnings are pre-existing)
npm run typecheck:core         # TypeScript check (should be clean)
npm run typecheck:noimplicit:core  # Strict check (no implicit any)
npm run test:coverage          # Unit tests + coverage gate (60/60/60/60 — statements/lines/functions/branches)
npm run check                  # lint + test combined
npm run check:cycles           # Detect circular dependencies

Running Tests

# Single test file (Node.js native test runner — most tests)
node --import tsx/esm --test tests/unit/your-file.test.ts

# Vitest (MCP server, autoCombo, cache)
npm run test:vitest

# All suites
npm run test:all

For full test matrix, see CONTRIBUTING.md → "Running Tests". For deep architecture, see AGENTS.md.


Project at a Glance

OmniRoute — unified AI proxy/router. One endpoint, 231 LLM providers, auto-fallback.

Layer Location Purpose
API Routes src/app/api/v1/ Next.js App Router — entry points
Handlers open-sse/handlers/ Request processing (chat, embeddings, etc)
Executors open-sse/executors/ Provider-specific HTTP dispatch
Translators open-sse/translator/ Format conversion (OpenAI↔Claude↔Gemini)
Transformer open-sse/transformer/ Responses API ↔ Chat Completions
Services open-sse/services/ Combo routing, rate limits, caching, etc
Database src/lib/db/ SQLite domain modules (83 files, 97 migrations)
Domain/Policy src/domain/ Policy engine, cost rules, fallback logic
MCP Server open-sse/mcp-server/ 87 tools (33 base + memory/skill/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 30 scopes
A2A Server src/lib/a2a/ JSON-RPC 2.0 agent protocol
Skills src/lib/skills/ Extensible skill framework
Memory src/lib/memory/ Persistent conversational memory

Monorepo: src/ (Next.js 16 app), open-sse/ (streaming engine workspace), electron/ (desktop app), tests/, bin/ (CLI entry point).


Request Pipeline

Client → /v1/chat/completions (Next.js route)
  → CORS → Zod validation → auth? → policy check → prompt injection guard
  → handleChatCore() [open-sse/handlers/chatCore.ts]
    → cache check → rate limit → combo routing?
      → resolveComboTargets() → handleSingleModel() per target
    → translateRequest() → getExecutor() → executor.execute()
      → fetch() upstream → retry w/ backoff
    → response translation → SSE stream or JSON
    → If Responses API: responsesTransformer.ts TransformStream

API routes follow a consistent pattern: Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse). No global Next.js middleware — interception is route-specific.

Combo routing (open-sse/services/combo.ts): 17 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion). Each target calls handleSingleModel() which wraps handleChatCore() with per-target error handling and circuit breaker checks. The fusion strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (open-sse/services/fusion.ts). See docs/routing/AUTO-COMBO.md for the 9-factor Auto-Combo scoring + the full strategy table and docs/architecture/RESILIENCE_GUIDE.md for the 3 resilience layers.


Resilience Runtime State

OmniRoute has three related but distinct temporary-failure mechanisms. Keep their scope separate when debugging routing behavior. See the 3-layer resilience diagram (source: docs/diagrams/resilience-3layers.mmd) for an at-a-glance map.

Provider Circuit Breaker

Scope: whole provider, e.g. glm, openai, anthropic.

Purpose: stop sending traffic to a provider that is repeatedly failing at the upstream/service level, so one unhealthy provider does not slow down every request.

Implementation:

  • Core class: src/shared/utils/circuitBreaker.ts
  • Chat gate/execution wiring: src/sse/handlers/chatHelpers.ts, src/sse/handlers/chat.ts
  • Runtime status API: src/app/api/monitoring/health/route.ts
  • Shared wrappers: open-sse/services/accountFallback.ts
  • Persisted state table: domain_circuit_breakers

States:

  • CLOSED: normal traffic is allowed.
  • OPEN: provider is temporarily blocked; callers get a provider-circuit-open response or combo routing skips to another target.
  • HALF_OPEN: reset timeout has elapsed; allow a probe request. Success closes the breaker, failure opens it again.

Defaults (open-sse/config/constants.ts):

  • OAuth providers: threshold 3, reset timeout 60s.
  • API-key providers: threshold 5, reset timeout 30s.
  • Local providers: threshold 2, reset timeout 15s.

Only provider-level failure statuses should trip the provider breaker:

(408, 500, 502, 503, 504);

Do not trip the whole-provider breaker for normal account/key/model errors like most 401, 403, or 429 cases. Those usually belong to connection cooldown or model lockout. A generic API-key provider 403 should be recoverable unless it is classified as a terminal provider/account error.

The breaker uses lazy recovery, not a background timer. When OPEN expires, reads such as getStatus(), canExecute(), and getRetryAfterMs() refresh the state to HALF_OPEN, so dashboards and combo candidate builders do not keep excluding an expired provider forever.

Connection Cooldown

Scope: one provider connection/account/key.

Purpose: temporarily skip one bad key/account while allowing other connections for the same provider to continue serving requests.

Implementation:

  • Write/update path: src/sse/services/auth.ts::markAccountUnavailable()
  • Account selection/filtering: src/sse/services/auth.ts::getProviderCredentials...
  • Cooldown calculation: open-sse/services/accountFallback.ts::checkFallbackError()
  • Settings: src/lib/resilience/settings.ts

Important fields on provider connections:

rateLimitedUntil;
testStatus: "unavailable";
lastError;
lastErrorType;
errorCode;
backoffLevel;

During account selection, a connection is skipped while:

new Date(rateLimitedUntil).getTime() > Date.now();

Cooldowns are also lazy: when rateLimitedUntil is in the past, the connection becomes eligible again. On successful use, clearAccountError() clears testStatus, rateLimitedUntil, error fields, and backoffLevel.

Default connection cooldown behavior:

  • OAuth base cooldown: 5s.
  • API-key base cooldown: 3s.
  • API-key 429 should prefer upstream retry hints (Retry-After, reset headers, or parseable reset text) when available.
  • Repeated recoverable failures use exponential backoff:
baseCooldownMs * 2 ** failureIndex;

The anti-thundering-herd guard prevents concurrent failures on the same connection from repeatedly extending the cooldown or double-incrementing backoffLevel.

Terminal states are not cooldowns. banned, expired, and credits_exhausted are intended to stay unavailable until credentials/settings change or an operator resets them. Do not overwrite terminal states with transient cooldown state.

Model Lockout

Scope: provider + connection + model.

Purpose: avoid disabling a whole connection when only one model is unavailable or quota-limited for that connection.

Examples:

  • Per-model quota providers returning 429.
  • Local providers returning 404 for one missing model.
  • Provider-specific mode/model permission failures such as selected Grok modes.

Model lockout lives in open-sse/services/accountFallback.ts and lets the same connection continue serving other models.

Debugging Guidance

  • If all keys for a provider are skipped, inspect both provider breaker state and each connection's rateLimitedUntil/testStatus.
  • If a provider appears permanently excluded after the reset window, check whether code is reading raw state instead of using getStatus()/canExecute().
  • If one provider key fails but others should work, prefer connection cooldown over provider breaker.
  • If only one model fails, prefer model lockout over connection cooldown.
  • If a state should self-recover, it should have a future timestamp/reset timeout and a read path that refreshes expired state. Permanent statuses require manual credential or config changes.

Key Conventions

Code Style

  • 2 spaces, semicolons, double quotes, 100 char width, es5 trailing commas (enforced by lint-staged via Prettier)
  • Imports: external → internal (@/, @omniroute/open-sse) → relative
  • Naming: files=camelCase/kebab, components=PascalCase, constants=UPPER_SNAKE
  • ESLint: no-eval, no-implied-eval, no-new-func = error everywhere; no-explicit-any = warn in open-sse/ and tests/
  • TypeScript: strict: false, target ES2022, module esnext, resolution bundler. Prefer explicit types.

Database

  • Always go through src/lib/db/ domain modules — never write raw SQL in routes or handlers
  • Never add logic to src/lib/localDb.ts (re-export layer only)
  • Never barrel-import from localDb.ts — import specific db/ modules instead
  • DB singleton: getDbInstance() from src/lib/db/core.ts (WAL journaling)
  • Migrations: src/lib/db/migrations/ — versioned SQL files, idempotent, run in transactions

Error Handling

  • try/catch with specific error types, log with pino context
  • Never swallow errors in SSE streams — use abort signals for cleanup
  • Return proper HTTP status codes (4xx/5xx)

Security

  • Never use eval(), new Function(), or implied eval
  • Validate all inputs with Zod schemas
  • Encrypt credentials at rest (AES-256-GCM)
  • Upstream header denylist: src/shared/constants/upstreamHeaders.ts — keep sanitize, Zod schemas, and unit tests aligned when editing
  • Public upstream credentials (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): MUST be embedded via resolvePublicCred() from open-sse/utils/publicCreds.tsnever as string literals. See docs/security/PUBLIC_CREDS.md for the mandatory pattern.
  • Error responses (HTTP / SSE / executor / MCP handler): MUST route through buildErrorBody() or sanitizeErrorMessage() from open-sse/utils/error.tsnever put raw err.stack or err.message in a response body. See docs/security/ERROR_SANITIZATION.md.
  • Shell commands built from variables: when calling exec()/spawn() with a script that needs runtime values, pass them via the env option (shell-escaped automatically) — never string-interpolate untrusted/external paths into the script body. Reference: src/mitm/cert/install.ts::updateNssDatabases.
  • Secure-by-default libraries (tldrsec/awesome-secure-defaults): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.

Common Modification Scenarios

Adding a New Provider

  1. Register in src/shared/constants/providers.ts (Zod-validated at load)
  2. Add executor in open-sse/executors/ if custom logic needed (extend BaseExecutor)
  3. Add translator in open-sse/translator/ if non-OpenAI format
  4. Add OAuth config in src/lib/oauth/constants/oauth.ts if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via resolvePublicCred() (see docs/security/PUBLIC_CREDS.md), never as a literal
  5. Register models in open-sse/config/providerRegistry.ts
  6. Write tests in tests/unit/ (include the publicCreds shape assertion if you added a new embedded default)

Adding a New API Route

  1. Create directory under src/app/api/v1/your-route/
  2. Create route.ts with GET/POST handlers
  3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
  4. Handler goes in open-sse/handlers/ (import from there, not inline)
  5. Error responses use buildErrorBody() / errorResponse() from open-sse/utils/error.ts (auto-sanitized — never put err.stack or err.message raw in the body). See docs/security/ERROR_SANITIZATION.md.
  6. Add tests — including at least one assertion that error responses do not leak stack traces (!body.error.message.includes("at /"))

Adding a New DB Module

  1. Create src/lib/db/yourModule.ts — import getDbInstance from ./core.ts
  2. Export CRUD functions for your domain table(s)
  3. Add migration in src/lib/db/migrations/ if new tables needed
  4. Re-export from src/lib/localDb.ts (add to the re-export list only)
  5. Write tests

Adding a New MCP Tool

  1. Add tool definition in open-sse/mcp-server/tools/ with Zod input schema + async handler
  2. Register in tool set (wired by createMcpServer())
  3. Assign to appropriate scope(s)
  4. Write tests (tool invocation logged to mcp_audit table)

Adding a New A2A Skill

  1. Create skill in src/lib/a2a/skills/ (5 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report)
  2. Skill receives task context (messages, metadata) → returns structured result
  3. Register in A2A_SKILL_HANDLERS in src/lib/a2a/taskExecution.ts
  4. Expose in src/app/.well-known/agent.json/route.ts (Agent Card)
  5. Write tests in tests/unit/
  6. Document in docs/frameworks/A2A-SERVER.md skill table

Adding a New Cloud Agent

  1. Create agent class in src/lib/cloudAgent/agents/ extending CloudAgentBase (3 already exist: codex-cloud, devin, jules)
  2. Implement createTask, getStatus, approvePlan, sendMessage, listSources
  3. Register in src/lib/cloudAgent/registry.ts
  4. Add OAuth/credentials handling if needed (src/lib/oauth/providers/)
  5. Tests + document in docs/frameworks/CLOUD_AGENT.md

Adding a New Embedded Service

  1. Create installer in src/lib/services/installers/{name}.ts modeled on ninerouter.ts (use runNpm from installers/utils.ts — no shell interpolation, hard rule #13).
  2. Register the service in src/lib/services/bootstrap.ts (add to SERVICES[] array and extend buildSpawnArgsFactory()).
  3. Add a DB seed row for the new service in src/lib/db/migrations/ (version_manager table, status='not_installed', auto_start=0).
  4. Create 7 API endpoints under src/app/api/services/{name}/ (_lib.ts, install, start, stop, restart, update, status, auto-start). All delegate errors through createErrorResponse(). The shared logs endpoint is already wired via [name]/logs/route.ts.
  5. Verify /api/services/ is in LOCAL_ONLY_API_PREFIXES in src/server/authz/routeGuard.ts; add a test asserting isLocalOnlyPath() returns true for the new prefix if you add one (hard rule #17).
  6. Add a UI tab in src/app/(dashboard)/dashboard/providers/services/tabs/ reusing ServiceStatusCard, ServiceLifecycleButtons, ServiceLogsPanel.
  7. Document in docs/frameworks/EMBEDDED-SERVICES.md (update §1 service table + §4 API reference) and docs/openapi.yaml.
  8. Write tests: unit (tests/unit/services/), integration (tests/integration/services/, gated by RUN_SERVICES_INT=1), and update docs/ops/RELEASE_CHECKLIST.md smoke section.

Adding a New Guardrail / Eval / Skill / Webhook event

  • Guardrail: src/lib/guardrails/ → docs: docs/security/GUARDRAILS.md
  • Eval suite: src/lib/evals/ → docs: docs/frameworks/EVALS.md
  • Skill (sandbox): src/lib/skills/ → docs: docs/frameworks/SKILLS.md
  • Webhook event: src/lib/webhookDispatcher.ts → docs: docs/frameworks/WEBHOOKS.md

Reference Documentation

For any non-trivial change, read the matching deep-dive first:

Area Doc
Repo navigation docs/architecture/REPOSITORY_MAP.md
Architecture docs/architecture/ARCHITECTURE.md
Engineering reference docs/architecture/CODEBASE_DOCUMENTATION.md
Auto-Combo (9-factor scoring, 17 strategies) docs/routing/AUTO-COMBO.md
Resilience (3 mechanisms) docs/architecture/RESILIENCE_GUIDE.md
Reasoning replay docs/routing/REASONING_REPLAY.md
Skills framework docs/frameworks/SKILLS.md
Memory system (FTS5 + Qdrant) docs/frameworks/MEMORY.md
Cloud agents docs/frameworks/CLOUD_AGENT.md
Guardrails (PII / injection / vision) docs/security/GUARDRAILS.md
Public upstream credentials (Gemini/etc.) docs/security/PUBLIC_CREDS.md
Error message sanitization docs/security/ERROR_SANITIZATION.md
Evals docs/frameworks/EVALS.md
Compliance / audit docs/security/COMPLIANCE.md
Webhooks docs/frameworks/WEBHOOKS.md
Authorization pipeline docs/architecture/AUTHZ_GUIDE.md
Stealth (TLS / fingerprint) docs/security/STEALTH_GUIDE.md
Agent protocols (A2A / ACP / Cloud) docs/frameworks/AGENT_PROTOCOLS_GUIDE.md
MCP server docs/frameworks/MCP-SERVER.md
A2A server docs/frameworks/A2A-SERVER.md
API reference + OpenAPI docs/reference/API_REFERENCE.md + docs/openapi.yaml
Provider catalog (auto-generated) docs/reference/PROVIDER_REFERENCE.md
Release flow docs/ops/RELEASE_CHECKLIST.md
Embedded services docs/frameworks/EMBEDDED-SERVICES.md
Quality gates (~48 scripts, allowlist policy) docs/architecture/QUALITY_GATES.md

Testing

What Command
Unit tests npm run test:unit
Single file node --import tsx/esm --test tests/unit/file.test.ts
Vitest (MCP, autoCombo) npm run test:vitest
E2E (Playwright) npm run test:e2e
Protocol E2E (MCP+A2A) npm run test:protocols:e2e
Ecosystem npm run test:ecosystem
Coverage gate npm run test:coverage (60/60/60/60 — statements/lines/functions/branches)
Coverage report npm run coverage:report

PR rule: If you change production code in src/, open-sse/, electron/, or bin/, you must include or update tests in the same PR.

Test layer preference: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.

Both test runners must pass: npm run test:unit (Node native — most tests) AND npm run test:vitest (MCP server, autoCombo, cache) cover non-overlapping files. Both are wired in CI (jobs test-unit and test-vitest) and must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.

Bug fix / issue triage protocol (Hard Rule #18): Every fix for a reported issue must be validated by one of the following — no exceptions:

  1. TDD (preferred) — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
  2. Real-environment test (when TDD is not possible) — deploy to the production VPS (root@192.168.0.15) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
  3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.

Why this matters: fixing bug A while opening bug B is worse than not fixing at all. The TDD/VPS gate enforces surgical scope — you touch only what the failing test proves is broken. Examples where this paid off: #3090 (claude-web 403), #3113 (WS HTTP fallback), #3052 (heap-guard auto-calibration).

Copilot coverage policy: When a PR changes production code and coverage is below 60% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.


Git Workflow

# Never commit directly to main
git checkout -b feat/your-feature
git commit -m "feat: describe your change"
git push -u origin feat/your-feature

Branch prefixes: feat/, fix/, refactor/, docs/, test/, chore/

Commit format (Conventional Commits): feat(db): add circuit breaker — scopes: db, sse, oauth, dashboard, api, cli, docker, ci, mcp, a2a, memory, skills

Husky hooks:

  • pre-commit: lint-staged + check-docs-sync + check:any-budget:t11
  • pre-push: fast deterministic gates (check:any-budget:t11 + check:tracked-artifacts); intentionally excludes test:unit (slow — covered by the CI test-unit job). Activated 2026-06-13 (Quality Gates Fase 6A.12).

Worktree isolation (MANDATORY for every development task)

Multiple sessions/agents work this repo in parallel. The main checkout is shared, so a git checkout/branch switch in it silently discards another session's uncommitted work and yanks the branch out from under whatever else is running (incidents: 2026-06-05, 2026-06-13).

Rule: never develop on the shared main checkout. Every task gets its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator before creating it.

  1. Ask first — which base branch? Before creating anything, ask the operator (via AskUserQuestion, unless they already told you) from which branch the new worktree/branch should be cut. Do NOT assume main or "whatever I'm on" — the answer is usually the active release/vX.Y.Z, but it can be another feature/release branch. Get the base explicitly.

  2. Create an isolated worktree + branch off that base (never reuse the main checkout). 🔴 MANDATORY PATH: every worktree lives under .claude/worktrees/ — and nowhere else. This is the single canonical location (the same dir the native EnterWorktree tool uses). It is gitignored AND in the tsconfig.json / .dockerignore excludes, so worktrees never leak into the build scope. Never use .worktrees/, repo-root, or any other path — a worktree outside .claude/worktrees/ (a) escapes the build-scope excludes and poisons next build (the tsconfig include: **/* globs ~70× the codebase → OOM; incident 2026-06-25) and (b) scatters worktrees across two dirs.

    BASE_BRANCH="release/vX.Y.Z"          # ← the branch the operator confirmed in step 1
    TASK="feat/your-feature"               # feat/ fix/ refactor/ docs/ test/ chore/
    git fetch origin "$BASE_BRANCH"
    git worktree add ".claude/worktrees/${TASK##*/}" -b "$TASK" "origin/$BASE_BRANCH"
    cd ".claude/worktrees/${TASK##*/}"
    # symlink node_modules from the main checkout to skip a per-worktree npm install:
    ln -s "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
    

    In Claude Code prefer the native EnterWorktree tool (it already creates worktrees under .claude/worktrees/): create the worktree with the command above, then call EnterWorktree with its path.

  3. Work, commit, push, open the PR — all from inside the worktree. Never git checkout a different branch inside a worktree another session might share.

  4. Tear down only your own worktree + branch when done, from the main checkout: git worktree remove .claude/worktrees/<dir> then git branch -D <task>. Never blanket-delete fix/*/feat/* — other sessions keep their own; delete only the branches you created, by name.

  5. Never touch another session's worktree, branch, or uncommitted changes. If git worktree list shows worktrees you didn't create, leave them alone. End every session with the main checkout back on the branch it started on (the active release/vX.Y.Z, never main).


Environment

  • Runtime: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules
  • TypeScript: 6.0+, target ES2022, module esnext, resolution bundler
  • Path aliases: @/*src/, @omniroute/open-sseopen-sse/, @omniroute/open-sse/*open-sse/*
  • Default port: 20128 (API + dashboard on same port)
  • Data directory: DATA_DIR env var, defaults to ~/.omniroute/
  • Key env vars: PORT, JWT_SECRET, API_KEY_SECRET, INITIAL_PASSWORD, REQUIRE_API_KEY, APP_LOG_LEVEL
  • Setup: cp .env.example .env then generate JWT_SECRET (openssl rand -base64 48) and API_KEY_SECRET (openssl rand -hex 32)

Quality Gates & Ratchets

OmniRoute has ~48 quality-gate scripts (scripts/check/ + scripts/quality/) wired across 9 gate-running jobs in .github/workflows/ci.yml (lint, quality-gate, quality-extended, docs-sync-strict, i18n-ui-coverage, i18n, pr-test-policy, test-vitest, sonarqube), plus the quality.yml fast-gates job (PR→release/**) and 3 nightly workflows (nightly-property, nightly-resilience, nightly-llm-security; nightly-mutation once merged). Full inventory, per-job breakdown, and operational procedures are in docs/architecture/QUALITY_GATES.md.

Quick reference:

  • Gates in jobs lint + docs-sync-strict: pass/fail policy gates — fix the violation or add an allowlist entry with a justification comment + tracking issue.
  • Gates in job quality-gate: ratchet — metrics (ESLint warnings, code coverage, duplication, complexity) must not regress vs quality-baseline.json. Update via npm run quality:ratchet -- --update when a metric genuinely improves.
  • Job test-vitest runs npm run test:vitest (MCP tools, autoCombo, cache) — blocking. test:vitest:ui is advisory until UI component tests are triaged.

Allowlist policy (short form): Fix the cause; use the allowlist only for pre-existing violations you cannot fix in the same PR. Add a comment with justification + issue number. Stale allowlist entries (suppressing a violation that no longer exists) will be caught by the stale-enforcement added in Fase 6A.3.


Hard Rules

  1. Never commit secrets or credentials
  2. Never add logic to localDb.ts
  3. Never use eval() / new Function() / implied eval
  4. Never commit directly to main
  5. Never write raw SQL in routes — use src/lib/db/ modules
  6. Never silently swallow errors in SSE streams
  7. Always validate inputs with Zod schemas
  8. Always include tests when changing production code
  9. Coverage must not regress below the baseline frozen in quality-baseline.json (ratchet); absolute floor is 60% (statements/lines/functions/branches). Update the baseline via npm run quality:ratchet -- --update only when coverage genuinely improves. See docs/architecture/QUALITY_GATES.md.
  10. Never bypass Husky hooks (--no-verify, --no-gpg-sign) without explicit operator approval.
  11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals — always go through resolvePublicCred() (open-sse/utils/publicCreds.ts). See docs/security/PUBLIC_CREDS.md.
  12. Never return raw err.stack / err.message in HTTP / SSE / executor responses — always route through buildErrorBody() or sanitizeErrorMessage() (open-sse/utils/error.ts). See docs/security/ERROR_SANITIZATION.md.
  13. Never string-interpolate external paths or runtime values into shell scripts passed to exec()/spawn() — pass via the env option instead. Reference: src/mitm/cert/install.ts::updateNssDatabases.
  14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: js/stack-trace-exposure raised on callsites that already route through sanitizeErrorMessage() is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as false positive referencing docs/security/ERROR_SANITIZATION.md.
  15. Never expose routes that spawn child processes (/api/mcp/, /api/cli-tools/runtime/) without isLocalOnlyPath() classification in src/server/authz/routeGuard.ts. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See docs/security/ROUTE_GUARD_TIERS.md.
  16. Never credit or advertise an AI assistant, LLM, or automation account in any commit/PR metadata. Two forbidden forms, both equivalent — they route attribution to a bot account (or advertise AI authorship) and hide the real author (diegosouzapw): (a) Co-Authored-By trailers naming an AI/bot (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at anthropic.com / openai.com / bot-owned noreply.github.com addresses); (b) AI-generation footers or descriptions anywhere in a commit message, PR title/body, or CHANGELOG — e.g. 🤖 Generated with [Claude Code], "Generated with Claude Code", "Made with ", or any Co-authored-by: Claude/GPT/Copilot line. This overrides any harness, template, or tool default that auto-appends such a footer (e.g. the Claude Code PR-body/commit default) — strip it before pushing; do not let it reach a commit, PR, or CHANGELOG. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard Co-authored-by: Name <email> trailers; the upstream-port workflows (/port-upstream-features, /port-upstream-issues) depend on this.
  17. Never expose routes under /api/services/ or /dashboard/providers/services/*/embed/ without isLocalOnlyPath() classification in src/server/authz/routeGuard.ts. These routes can spawn child processes (npm install, node). Loopback enforcement happens unconditionally before any auth check — a leaked JWT via tunnel cannot trigger process spawning. See docs/security/ROUTE_GUARD_TIERS.md.
  18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
  19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via AskUserQuestion) before creating the worktree/branch — never assume main or the currently checked-out branch. A git checkout in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never fix/*/feat/* wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active release/vX.Y.Z, never main). See Git Workflow → "Worktree isolation".

PII & Stream Sanitization Learnings

1. Regex Security (ReDoS)

All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges {1,7}) to prevent catastrophic backtracking when processing untrusted inputs.

2. SSE Snapshot Handling

When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (done or completed events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.

3. Database Handles in Tests

Ensure that any unit tests that trigger database migrations or establish SQLite connections call resetDbInstance() and properly clean up/close all DB handles in a test.after(...) hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.