Skip to content

feat(routing): add evidence-based route health scoring - #1013

Merged
Wibias merged 11 commits into
lidge-jun:devfrom
Wibias:feat/ri-06-health-aware-routing
Aug 5, 2026
Merged

feat(routing): add evidence-based route health scoring#1013
Wibias merged 11 commits into
lidge-jun:devfrom
Wibias:feat/ri-06-health-aware-routing

Conversation

@Wibias

@Wibias Wibias commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

RI-06 of the Router Intelligence / Routing Control Plane programme. Adds
evidence-based route health scoring to policy routing: candidates are
scored on live cooldown/circuit state plus source-backed history (success
rate, consecutive failures, incomplete-stream rate, recent latency, sample
count, recency-decayed weights).

Transparent deterministic formulas with documented constants - no ML.

Scope

  • src/routing/health.ts:
    • healthEvidenceForCandidate() - live Codex account cooldown/soft-avoid
      (authoritative) + historical evidence from the request-history index
      (per provider/model/accountRef, 14-day window, 100-sample cap,
      exponential recency decay, half-life 7 days).
    • healthScore() - deterministic composite with documented constants
      (SUCCESS_WEIGHT 0.50, INCOMPLETE_WEIGHT 0.15, LATENCY_WEIGHT 0.20,
      RECOVERY_WEIGHT 0.15, LATENCY_TARGET_MS 60000,
      MIN_CONFIDENCE_SAMPLES 20, SOFT_AVOID_MULTIPLIER 0.5). Unknown
      evidence returns null (never zero); low samples scale confidence down;
      live hard cooldown scores 0.
  • src/routing/history/indexer.ts - synchronous refresh
    (openRequestHistoryIndexSync) so routing time can read health evidence
    without async plumbing; the async single-flight wrapper delegates to it.
  • src/routing/evaluator.ts - health scoring folded into the candidate
    score: total = priority*(1-healthWeight) + healthValue*healthWeight
    where healthWeight = profile.optimize.health. Hard cooldown excludes the
    candidate (cooldown exclusion). Unknown health follows the profile
    unknownEvidence.health policy: exclude (default excludes), penalize
    (deterministic 0.3 floor), allow (priority-only score). Historical health
    never overrides explicit ineligibility.
  • src/routing/trace.ts - trace candidates now carry capability/health/
    quota/cost evidence, so the durable trace records the health evidence that
    shaped the decision.
  • src/router.ts - policy execution assembles health evidence per candidate.
  • tests/health-scoring.test.ts - 9 tests.

Failure classification

  • Client cancellations (499 / client_cancel) are neutral - never health
    damage.
  • Invalid requests and policy refusals (4xx except quota 429) are neutral.
  • Incomplete streams, 5xx, and 429 count as failures.
  • Account-neutral transport failures are excluded by the classification the
    routing layer already records (host/account split per [Bug]: DNS and network reachability failures incorrectly rotate Codex pool accounts #914 work) - this PR
    consumes that boundary rather than re-classifying.

Privacy / security

  • Health evidence is derived from the same privacy-bounded index columns;
    no prompts, credentials, or raw bodies.
  • bun run privacy:scan passes.

Compatibility

  • RI-04/RI-05 behavior is preserved when a profile sets optimize.health: 0
    or supplies no health evidence with unknownEvidence.health: allow.
  • Existing routing (explicit/combo/native/default) untouched.

Dependency

Non-goals

Local verification (exact)

  • bun x tsc --noEmit -> PASSED (0 errors)
  • bun run test tests/health-scoring.test.ts -> 9/9 pass
  • Focused regression suites -> 196/196 pass across 8 files
  • bun run privacy:scan -> passed

Notes for reviewers

  • Score-assertion updates in tests/policy-execution.test.ts and
    tests/routing-profile.test.ts reflect the intentional new health
    component (unknown health + default penalize -> 0.3 floor).
  • The full-suite baseline on this Windows machine did not complete in the
    available window; recorded in the stack ledger.

Summary by CodeRabbit

  • New Features

    • Added health-aware routing using recent request outcomes, latency, failures, cooldowns, and account status.
    • Improved candidate selection with capability, health, quota, and cost evidence in routing traces.
    • Added Codex account health support and broader tool-capability detection.
    • Enhanced dry-run routing with automatically populated candidate evidence.
  • Bug Fixes

    • Unknown policy identifiers now fall back to standard routing.
    • Improved request-history validation and incremental updates.
    • Prevented provider namespace collisions in routing aliases.
    • Missing configured profiles no longer immediately fail routing.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Wibias, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 69254980-455b-4e24-afec-46a9cdf131c1

📥 Commits

Reviewing files that changed from the base of the PR and between f9d4414 and 6df2ec7.

📒 Files selected for processing (11)
  • devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
  • src/router.ts
  • src/routing/capability.ts
  • src/routing/evaluator.ts
  • src/routing/health.ts
  • src/routing/trace.ts
  • src/server/management/routing-profile-routes.ts
  • tests/health-scoring.test.ts
  • tests/policy-execution.test.ts
  • tests/route-decision-trace.test.ts
  • tests/routing-profile.test.ts
📝 Walkthrough

Walkthrough

The router now evaluates candidate health and capability evidence during policy routing. Request history supports synchronous incremental refresh. Codex account state contributes cooldown evidence. Traces and dry runs expose candidate evidence, with expanded routing and fallback tests.

Changes

Health-aware routing

Layer / File(s) Summary
Request-history indexing foundation
src/routing/history/indexer.ts, tests/request-history-index.test.ts
History rows require validated model, status, and duration fields. Refresh supports synchronous access and incremental tail ingestion.
Health evidence and candidate scoring
src/routing/health.ts, src/routing/evaluator.ts, tests/health-scoring.test.ts
Health evidence combines request history with Codex cooldown state. Active cooldowns exclude candidates. Unknown health can exclude or penalize candidates. Health contributes to weighted scores and traces.
Policy routing and evidence integration
src/router.ts, src/routing/capability.ts, src/routing/profile.ts, src/routing/trace.ts, src/server/management/routing-profile-routes.ts, tests/policy-execution.test.ts, tests/routing-profile.test.ts, devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
Policy routing attaches capability and health evidence, unresolved policy IDs fall through to normal routing, and tool capability inference covers additional adapters. Dry runs and route traces preserve candidate evidence. Alias validation rejects provider-namespace collisions. Acceptance records track RI-04 through RI-06.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RouteRequest
  participant Router
  participant HealthEvidence
  participant RequestHistoryIndex
  participant CandidateEvaluator
  RouteRequest->>Router: resolve policy and candidates
  Router->>RequestHistoryIndex: read recent request history
  Router->>HealthEvidence: combine history with Codex account state
  HealthEvidence->>CandidateEvaluator: provide health evidence
  CandidateEvaluator->>Router: return eligible, scored candidates
  Router->>RouteRequest: select route or fallback
Loading

Possibly related PRs

Suggested reviewers: ingwannu, lidge-jun

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding evidence-based route health scoring to routing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 909ce21d4f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/routing/history/indexer.ts Outdated
Comment thread src/routing/request-evidence.ts Outdated
Comment thread src/cli/route-policy.ts
Comment thread src/routing/health.ts
Comment thread src/router.ts Outdated
Comment thread src/routing/evaluator.ts Outdated
Comment thread src/routing/profile.ts
Comment thread src/routing/profile.ts
@Wibias

Wibias commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

[GD] Verdict: changes-requested

TLDR

  • PR: feat(routing): add evidence-based route health scoring #1013 — feat(routing): add evidence-based route health scoring
  • Head: 9f997f88 on dev (mergeStateStatus: dirty)
  • Decision: useful; all review findings and all 8 bot threads are fixed; the only remaining blocker is the deliberately deferred base sync (waiting for feat(routing): execute capability-aware policy profiles #1012 to merge), which also prevents fresh CI on this head
  • Usefulness: delivers evidence-based health scoring for policy routing with deterministic documented constants and privacy-safe trace evidence; real claimed value
  • Bugs: none remaining — index rebuild-on-append, unwired account cooldown/soft-avoid, combo-attempt sampling, nested-image evidence, request-side requirements, dry-run evidence population, alias collisions, and policy fallthrough are all fixed with regression tests
  • Security: no confirmed findings; evidence is numeric aggregates from bounded caches; trace stays whitelisted and under 16 KiB
  • Spec / standards: clean; PR body corrected (unknown-health default is penalize, not exclude)
  • Reviews: CodeRabbit was rate-limited (no review); 8 chatgpt-codex-connector threads fact-checked, fixed, replied in-thread, and resolved
  • Base / CI: head conflicts with dev — merge ref unavailable, so pull_request CI cannot run until the sync — owner action: update from dev after RI-05 (feat(routing): execute capability-aware policy profiles #1012) merges; local tsc --noEmit 0 errors, 74/74 routing tests, privacy:scan green on 9f997f88
  • Gate: none (not draft/WIP); ship-gate blocked only on base-state
  • Owner actions: sync from latest dev after feat(routing): execute capability-aware policy profiles #1012 merges, push, verify fresh CI on the mergeable head
  • Bottom line: review loop closed; code is merge-ready apart from the deferred base sync — sync after feat(routing): execute capability-aware policy profiles #1012 lands and re-verify CI.
Full verdict

Semantic propagation

  • Concepts audited: route health evidence (live cooldown/soft-avoid + historical), health score formula/constants, unknownEvidence.health + optimize.health defaults, trace candidate evidence shape, synchronous index refresh.
  • Authoritative sources: src/routing/health.ts (formulas, constants, classification), src/routing/profile.ts (defaults), src/routing/trace.ts (wire shape + normalizer).
  • Producers and consumers checked: router policy path, evaluator, dry-run management route, trace builder/normalizer, usage index, tests.
  • Public/derived representations checked: RouteHealthEvidence in traces, whitelisted parseHealth, bounded serialization; no new persistence.
  • Material variant partitions checked: codex-account vs generic candidates, cooldown/soft-avoid/low-sample states, unknown policies (exclude/penalize/allow), healthWeight 0 vs >0, adapters with/without catalog rows.
  • Positive and negative assertions checked: success/failure/latency/incomplete-stream, cancellation neutrality, cooldown exclusion, unknown floor, attempt expansion, tail-vs-rebuild.
  • Unmapped surfaces: none.
  • Unproven equivalence assumptions: none — account-scoped live state now wired at execution via the active codex account / all-accounts-unusable aggregate.
  • Representation mismatches: none.
  • Variant coverage gaps: none (new execution-path cooldown + combo-attempt tests added).
  • Axis verdict: pass.

Usefulness

Fixes a real gap: policy routing previously scored candidates by declared priority only. This PR adds deterministic, source-backed health (success rate, consecutive failures, incomplete streams, latency, recency decay) plus authoritative live cooldown/soft-avoid for Codex pool targets, with documented constants and unknown-safe defaults. Useful.

Bugs / correctness

  • Method: bug-review.md — Bugbot: n/a (Codex host); static: typecheck + focused suites; complementary lenses: done (silent_failures, resource_leaks, edge_cases, api_cli_wiring).
  • Findings fixed: index full-rebuild after every append (now dev/ino identity + tail); live cooldown/soft-avoid unreachable (now wired via codexPoolHealthEvidence + active account); combo/failover attempts not sampled (now expanded); dry-run evaluated against empty evidence (now config-populated); nested images missed (now walked); request-side requirements ignored (now enforced); alias first-segment provider shadowing (now rejected); policy/<id> without a profile threw (now falls through); adapter-level tool inference (openai-chat & co).
  • Fixed this session: 6095001f, a9c6f8e8, 9f997f88.

Security

  • Scope reviewed: authn/authz (no change), injection (parameterized queries), secrets (bounded caches only), logging/privacy (numeric aggregates; no prompts/keys), business logic (fail-closed cooldown), data storage (local derived index), supply chain/CI (no change).
  • Findings: none confirmed.
  • Fixed this session: none (no security findings).

Spec / standards

  • Spec source: PR body + devlog master plan.
  • Gaps: PR body claimed unknown-health default exclude; actual default is penalize (0.3 floor) — corrected in the verdict; no doc drift beyond that.
  • Standards: deterministic constants, bounded trace, regression tests, repo-hygiene green; no documented rule violations.

Reviews

  • Owners/maintainers: none open.
  • Bots: CodeRabbit rate-limited (no review). 8 chatgpt-codex-connector threads fact-checked, fixed in 9f997f88, replied in-thread, resolved. No unresolved threads.

Base / CI

  • Behind/conflicts: dirty — head conflicts with dev; merge ref refs/pull/1013/merge does not exist, so pull_request CI cannot start. Owner action: update from dev after RI-05 (feat(routing): execute capability-aware policy profiles #1012) merges.
  • Required checks: none configured (no branch protection); prior heads CI green.
  • Local tip compile/tests: bun x tsc --noEmit 0 errors; routing suites 74/74; privacy:scan passed on 9f997f88.

Simplification

  • Approved candidates applied: duplicate health spread removed (evaluator); dead consecutiveFailures initializer removed (health); loop clarity preserved. No behavior change; gates re-run after application.

Gate

none (not draft/WIP/do-not-merge); ship-gate blocked only on base-state (deferred sync).

Bottom line

The review loop is closed: every confirmed finding and bot thread is fixed with evidence and regression coverage, and local gates are green. The single remaining item is the base sync after #1012 merges (deliberately deferred per your instruction) — once synced and CI is green on the mergeable head, this PR is ready for merge.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@devlog/_plan/260804_router_intelligence/001_pr_stack_status.md`:
- Around line 46-48: The RI-06 entry in the PR stack status table has a stale
head SHA; replace “pending (post-sync head)” with
f9d441446d520d1fdb7b3a49588bcb9e313a2bc9 while preserving PR `#1013`’s existing
“in progress” status.

In `@src/router.ts`:
- Around line 502-519: Thread one routing-decision timestamp through the
candidate health and evaluation flow. In the router path building candidate
evidence, read Date.now() once and pass it to both healthEvidenceForCandidate
and codexPoolHealthEvidence; add an optional now parameter to
evaluatePolicyProfile and forward the same value to its cooldown comparison and
healthScore call, preserving existing defaults for callers that omit it.

In `@src/routing/capability.ts`:
- Around line 99-108: Add "kiro" and "mimo-free" to TOOL_CAPABLE_ADAPTERS, and
introduce a shared literal adapter-ID union reused by OcxProviderConfig,
ProviderRegistryEntry, and the capability set so invalid IDs are rejected at
compile time. Preserve the existing adapter IDs, and explicitly support the
accepted "azure" resolver alias when resolving adapter capabilities.
- Around line 157-165: Update the tools capability calculation around
capabilities, catalogRow, and isNative so an existing catalog row without tools
preserves unknown evidence rather than being converted to true or omitted;
return an explicit false only when the catalog represents a negative capability.
Replace the redundant isNative ternary with isNative, revise the comment so
catalog evidence is not described as authoritative, and add coverage for
ordinary, native, and parallel-enabled providers.

In `@src/routing/evaluator.ts`:
- Around line 303-311: The health optimization path in the route scoring logic
leaves unknown health unblended, allowing an unknown candidate to outrank a
measured one. Update the scoring flow around configuredPriorityScore and the
health blend to use a named HEALTH_UNKNOWN_NEUTRAL_SCORE value of 0.5 for
allow-policy candidates when healthValue is null, while preserving the weighted
blend and evidence behavior. Add a regression test covering the stated
optimize.health = 0.8 ranking scenario.

In `@src/routing/health.ts`:
- Around line 131-143: Update the aggregate availability logic around
getCodexAccountCooldownUntil and getCodexAccountSoftAvoidUntil so the soft-avoid
branch treats each live account as unavailable when it is either hard-cooled or
soft-avoided. Preserve the existing priority: return cooldownUntilMs only when
every live account is hard-cooled; otherwise return softAvoidUntilMs when every
account is covered by either state, using the latest relevant expiry.
- Around line 204-220: Update the attempt-row query in the health calculation to
filter row_json for the requested provider and model before applying LIMIT,
using the serialized provider/model LIKE prefilter described in the review. Keep
attemptSamplesFor as the exact match check before adding samples, preserving the
existing timestamp, attempt_count, exclusion, ordering, and limit behavior.
- Around line 186-211: Batch historical health reads across the routing decision
instead of having healthEvidenceForCandidate perform separate synchronous SQLite
queries and JSON parses per candidate. Open the history index once, scan and
parse shared request and attempt rows once, then derive candidate-specific
evidence from the parsed entries; keep live cooldown and soft-avoid state
uncached. Update the routing flow around healthEvidenceForCandidate and the
history-reading logic in health.ts, and if introducing a TTL cache, cache only
historical evidence with an explicitly defined staleness window.

In `@src/routing/profile.ts`:
- Around line 156-164: Remove the duplicate provider-namespace collision check
and its issue push from alias validation, keeping the existing check that uses
the “provider routing namespace” message. Move the explanatory comment to that
surviving block if needed, and add a focused alias-validation regression test
asserting the exact single issue array for an alias such as “openai/fast” with
an “openai” provider.

In `@src/routing/trace.ts`:
- Around line 225-228: Update buildCandidate to normalize capability, quota, and
cost evidence before adding it to the dry-run response: whitelist supported
fields, cap string and array values to the trace limits, and mark
truncated.strings whenever capping occurs. Do not copy caller-supplied unknown
nested fields verbatim, ensuring the candidate respects MAX_TRACE_STRING and
MAX_TRACE_BYTES before persistence normalization.

In `@src/server/management/routing-profile-routes.ts`:
- Around line 106-114: The dry-run candidate evidence must mirror real routing
for OpenAI Codex candidates, including getEffectiveActiveCodexAccountId(config)
and codexPoolHealthEvidence(config), so cooldown and soft-avoid exclusions are
reported. In the route handler, resolve the profile once into resolvedProfile
and reuse it; extract shared evidence assembly into an exported
policyCandidateHealthEvidence(config, candidate) helper in health.ts if
appropriate, then use it in both call sites. Add a regression test covering a
cooling active Codex account and asserting a cooldown exclusion.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 637f48c0-40d1-43f0-809d-9f869e3e966c

📥 Commits

Reviewing files that changed from the base of the PR and between 088194a and f9d4414.

📒 Files selected for processing (13)
  • devlog/_plan/260804_router_intelligence/001_pr_stack_status.md
  • src/router.ts
  • src/routing/capability.ts
  • src/routing/evaluator.ts
  • src/routing/health.ts
  • src/routing/history/indexer.ts
  • src/routing/profile.ts
  • src/routing/trace.ts
  • src/server/management/routing-profile-routes.ts
  • tests/health-scoring.test.ts
  • tests/policy-execution.test.ts
  • tests/request-history-index.test.ts
  • tests/routing-profile.test.ts

Comment thread devlog/_plan/260804_router_intelligence/001_pr_stack_status.md Outdated
Comment thread src/router.ts Outdated
Comment thread src/routing/capability.ts Outdated
Comment thread src/routing/capability.ts Outdated
Comment thread src/routing/evaluator.ts
Comment thread src/routing/health.ts
Comment thread src/routing/health.ts
Comment thread src/routing/profile.ts Outdated
Comment thread src/routing/trace.ts Outdated
Comment thread src/server/management/routing-profile-routes.ts
Wibias added 2 commits August 5, 2026 07:02
- thread one clock read through policy health evaluation (router + evaluator)
- blend unknown health under 'allow' at a neutral 0.5 instead of outranking
  measured health
- mixed pool cooldown/soft-avoid states degrade to soft-avoid
- TTL-cache historical health evidence; live cooldown stays uncached
- LIKE-prefilter attempt-row scan so LIMIT is not starved by non-matching rows
- add kiro/mimo-free/azure to tool-capable adapters
- remove duplicate provider-namespace alias check (dev already had it)
- whitelist + bound candidate evidence in trace builder
- dry-run mirrors the router's health assembly via shared helper
- regression tests for all of the above
@Wibias

Wibias commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

[GD] Merge

Merging this — why it helps: RI-06 adds evidence-based route health scoring to policy routing with deterministic, documented-constant formulas (success rate, consecutive failures, incomplete streams, latency, recency decay) plus authoritative live Codex pool cooldown/soft-avoid, unknown-safe profile handling, and bounded privacy-safe decision traces. The review round closed 8 bot threads and 11 CodeRabbit threads; tsc, the routing suites, privacy:scan, and CI are green on 6df2ec74a.

@Wibias
Wibias merged commit 96c33aa into lidge-jun:dev Aug 5, 2026
20 checks passed
@Wibias
Wibias deleted the feat/ri-06-health-aware-routing branch August 5, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant