Skip to content

fix(sse): restore task-aware routing config on restart (#8601) - #8604

Closed
MumuTW wants to merge 3 commits into
diegosouzapw:release/v3.8.49from
MumuTW:fix/task-routing-config-not-restored
Closed

MumuTW wants to merge 3 commits into
diegosouzapw:release/v3.8.49from
MumuTW:fix/task-routing-config-not-restored

Conversation

@MumuTW

@MumuTW MumuTW commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Fixes #8601.

Problem

PUT /api/settings/task-routing persists the T05 Task-Aware Smart Routing config to settings.taskRouting, but nothing ever reads it back. After any restart the feature reverts to enabled: false and the hardcoded DEFAULT_TASK_MODEL_MAP, discarding the operator's configuration.

Two independent root causes — fixing only the first would not have worked.

A. No boot hydration

rg taskRouting -t ts src open-sse returned only the write path and the Zod schema. applyRuntimeSettings does not cover this key, so it needs an explicit hydration step — same as the Global System Prompt (#2470) and the Thinking-Budget config (#5312).

B. Module-level let is duplicated per module graph

let _config: TaskRoutingConfig = { enabled: false, ... };   // taskAwareRouter.ts:171

thinkingBudget.ts:63-68 documents why this breaks: a module-level binding is duplicated per graph, so boot hydration on the instrumentation graph never reaches the copy src/sse/handlers/chat.ts:573 reads. This is the exact failure #5312 fix-A hit on the VPS.

Fix

  • taskAwareRouter.ts — move the config store to the globalThis pattern used by thinkingBudget.ts and systemPrompt.ts; add hydrateTaskRoutingConfig(settings), accepting either the JSON string the route writes or an already-parsed object, failing open on malformed input. Runtime stats are never restored from the persisted blob.
  • src/instrumentation-node.ts — call it next to the Thinking-Budget restore. Wired into the real boot path, not the unused src/server-init.ts (called out as unused at instrumentation-node.ts:366 / :407).

No behavior change when nothing is persisted: the in-memory defaults stay in place, and the feature remains off by default.

Validation (Hard Rule #18 — TDD)

tests/unit/task-routing-config-restore-8601.test.ts, 5 cases. Written first, confirmed red:

SyntaxError: The requested module '.../taskAwareRouter.ts'
does not provide an export named 'hydrateTaskRoutingConfig'

Green after the fix — 20/20 including the pre-existing chat-route-coverage suite:

node --import tsx/esm --test tests/unit/task-routing-config-restore-8601.test.ts \
                              tests/unit/chat-route-coverage.test.ts
# tests 20 / # pass 20 / # fail 0

The cross-module-graph case (root cause B) was separately proven to have teeth — importing a module under two distinct specifiers does yield two real instances, so the test would fail on the old let-based store:

module-let  -> A: true   B: false     <- not shared (the bug)
globalstore -> A: true   B: true      <- shared (the fix)

Gates

  • npm run typecheck:core — clean
  • npx eslint on changed files — 4 no-explicit-any, all pre-existing (4 on the base ref, 4 after; eslint-suppressions.json already records count: 4 for this file). No new violations, no baseline update needed.

@MumuTW
MumuTW requested a review from diegosouzapw as a code owner July 25, 2026 18:49
@MumuTW

MumuTW commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

The two red checks are pre-existing base breakage, not this PR

Fast Quality Gates and Unit Tests fast-path (2/4) fail identically on the base commit itself. Verified by running both on a pristine worktree at 4053e2314 with no changes from this PR:

Check Pristine base 4053e2314 This PR's CI
check:complexity-ratchets complexity=2169 / cognitiveComplexity=956 complexity=2169 / cognitiveComplexity=956
tests/unit/serial/combo-quota-share-cooldown-wait-timing.test.ts 2 pass / 2 fail — 429 !== 200, 403 !== 429 same two assertions

Byte-identical. Unrelated PR #8599 fails on the same two jobs in the same CI window.

Already tracked, no new issue filed:

Per #8542, a green board can't be the evidence here — the fail-fast masks downstream gates. Local validation for this PR:

node --import tsx/esm --test tests/unit/task-routing-config-restore-8601.test.ts \
                              tests/unit/chat-route-coverage.test.ts
# tests 20 / # pass 20 / # fail 0

npm run typecheck:core     # clean
npx eslint <changed files>  # 4 no-explicit-any, all pre-existing (suppressions record count: 4)

Everything else on this PR is green: Vitest, Unit 1/3/4, ESLint, Docs Gates, Merge integrity, semgrep, dast-smoke.

…8601)

The T05 Task-Aware Smart Routing config was persisted to settings.taskRouting
by PUT /api/settings/task-routing but never read back, so it silently reverted
to enabled:false + the hardcoded default model map on every restart.

Two root causes, both fixed:

- No boot hydration existed. Adds hydrateTaskRoutingConfig(settings), wired into
  src/instrumentation-node.ts next to the Thinking-Budget restore (diegosouzapw#5312). It
  accepts either the JSON string the route persists or an already-parsed object,
  and fails open on malformed values. applyRuntimeSettings does not cover this
  key, same as the Global System Prompt (diegosouzapw#2470).

- The config lived in a plain module-level `let`, which is duplicated per module
  graph — a boot hydration would have landed on the instrumentation graph's copy
  and never reached the one src/sse/handlers/chat.ts reads. This is the exact
  break diegosouzapw#5312 fix-A hit on the VPS. Moves the store to the globalThis pattern
  already used by thinkingBudget.ts and systemPrompt.ts.

Runtime stats are never restored from the persisted blob.

Note the hydration is wired into instrumentation-node.ts, not the unused
src/server-init.ts.
@MumuTW
MumuTW force-pushed the fix/task-routing-config-not-restored branch from 7d43ec9 to 593de28 Compare July 26, 2026 09:33
Same tip fix as diegosouzapw#8657 so Merge integrity is green without waiting for
that PR to land. Regenerated via generate-agent-skills --apply.
MumuTW added a commit to MumuTW/OmniRoute that referenced this pull request Jul 27, 2026
MumuTW added a commit to MumuTW/OmniRoute that referenced this pull request Jul 27, 2026
diegosouzapw pushed a commit that referenced this pull request Jul 27, 2026
…adowing (#8602, #8603) (#8605)

* fix(sse): restore task-aware routing config on restart (#8601)

The T05 Task-Aware Smart Routing config was persisted to settings.taskRouting
by PUT /api/settings/task-routing but never read back, so it silently reverted
to enabled:false + the hardcoded default model map on every restart.

Two root causes, both fixed:

- No boot hydration existed. Adds hydrateTaskRoutingConfig(settings), wired into
  src/instrumentation-node.ts next to the Thinking-Budget restore (#5312). It
  accepts either the JSON string the route persists or an already-parsed object,
  and fails open on malformed values. applyRuntimeSettings does not cover this
  key, same as the Global System Prompt (#2470).

- The config lived in a plain module-level `let`, which is duplicated per module
  graph — a boot hydration would have landed on the instrumentation graph's copy
  and never reached the one src/sse/handlers/chat.ts reads. This is the exact
  break #5312 fix-A hit on the VPS. Moves the store to the globalThis pattern
  already used by thinkingBudget.ts and systemPrompt.ts.

Runtime stats are never restored from the persisted blob.

Note the hydration is wired into instrumentation-node.ts, not the unused
src/server-init.ts.

* docs(changelog): add fragment for #8604 task-routing boot restore

* fix(sse): route task-aware defaults by intent, guard fitness pattern order (#8602, #8603)

Two related defects in the hand-maintained model-quality tables.

#8602 — DEFAULT_TASK_MODEL_MAP hardcoded literal provider/model ids
(openai/gpt-4o, gemini/gemini-2.5-flash-lite, deepseek/deepseek-chat, ...).
Wrong twice over: the ids rotted by a generation or two, and applyTaskAwareRouting
overwrites body.model directly, so a literal target skipped auto-combo's 13-factor
scoring (quota, circuit-breaker health, cost, latency, stability), connection
cooldown and model lockout — hard-failing for any operator with no connection for
that provider. Refreshing the strings would only reset the rot clock, so the
defaults now name auto/* INTENTS that resolve against the operator's actually
connected backends:

  coding        -> auto/coding
  analysis      -> auto/reasoning
  vision        -> auto/vision
  summarization -> auto/chat:fast
  background    -> auto/chat:cheap

creative and chat stay pass-through. Operators can still pin a specific model via
PUT /api/settings/task-routing; only the shipped defaults change. No provider/model
literal remains in the module.

#8603 — the pattern-shadowing fix LANDED UPSTREAM while this PR was open
(9f5be22, Train 1D). lookupStaticFitnessTable now ranks patterns longest-first,
so gpt-4o-mini no longer inherits gpt-4o's 0.9 and deepseek-v3.2 no longer inherits
deepseek-v3's 0.85. This PR therefore no longer changes that behaviour — the
upstream scan is kept verbatim.

What remains for #8603 is the regression guard. The resolution chain hits the DB
(user_override / arena_elo / models.dev tier) before reaching layer 4, so asserting
the ordering through getTaskFitness would depend on DB fixture state. The layer is
exposed as getStaticFitnessTableScore and pinned directly by
taskFitness-pattern-order-8603.test.ts (7 cases), so the guarantee survives future
edits to FITNESS_TABLE. Those 7 cases were written against this PR's original
implementation and pass unchanged against the upstream one — independent
confirmation that the two are behaviourally equivalent.

* docs(changelog): add fragment for #8605 task-routing intent + fitness order
@diegosouzapw

Copy link
Copy Markdown
Owner

Closing — this change shipped inside #8605

Thanks for this — it was a real fix, and it did land. Verified against origin/release/v3.8.49:

open-sse/services/taskAwareRouter.ts:187   // #8601: the config MUST live on globalThis, NOT a module-level `let`
open-sse/services/taskAwareRouter.ts:248   export function hydrateTaskRoutingConfig(settings: unknown): boolean
src/instrumentation-node.ts:410-412        const { hydrateTaskRoutingConfig } = ... if (hydrateTaskRoutingConfig(settings))

The reason this PR stayed open is worth recording: the squash commit of #8605 (merged today) carries as its first sub-commit exactly fix(sse): restore task-aware routing config on restart (#8601) — the content of this PR. #8605 incorporated this branch before submitting, so the work merged under that PR's number rather than this one.

Closing as merged-via-#8605. The boot-time hydration is live on the release branch.

@diegosouzapw diegosouzapw mentioned this pull request Jul 28, 2026
HouMinXi pushed a commit to HouMinXi/OmniRoute that referenced this pull request Aug 2, 2026
…adowing (diegosouzapw#8602, diegosouzapw#8603) (diegosouzapw#8605)

* fix(sse): restore task-aware routing config on restart (diegosouzapw#8601)

The T05 Task-Aware Smart Routing config was persisted to settings.taskRouting
by PUT /api/settings/task-routing but never read back, so it silently reverted
to enabled:false + the hardcoded default model map on every restart.

Two root causes, both fixed:

- No boot hydration existed. Adds hydrateTaskRoutingConfig(settings), wired into
  src/instrumentation-node.ts next to the Thinking-Budget restore (diegosouzapw#5312). It
  accepts either the JSON string the route persists or an already-parsed object,
  and fails open on malformed values. applyRuntimeSettings does not cover this
  key, same as the Global System Prompt (diegosouzapw#2470).

- The config lived in a plain module-level `let`, which is duplicated per module
  graph — a boot hydration would have landed on the instrumentation graph's copy
  and never reached the one src/sse/handlers/chat.ts reads. This is the exact
  break diegosouzapw#5312 fix-A hit on the VPS. Moves the store to the globalThis pattern
  already used by thinkingBudget.ts and systemPrompt.ts.

Runtime stats are never restored from the persisted blob.

Note the hydration is wired into instrumentation-node.ts, not the unused
src/server-init.ts.

* docs(changelog): add fragment for diegosouzapw#8604 task-routing boot restore

* fix(sse): route task-aware defaults by intent, guard fitness pattern order (diegosouzapw#8602, diegosouzapw#8603)

Two related defects in the hand-maintained model-quality tables.

diegosouzapw#8602 — DEFAULT_TASK_MODEL_MAP hardcoded literal provider/model ids
(openai/gpt-4o, gemini/gemini-2.5-flash-lite, deepseek/deepseek-chat, ...).
Wrong twice over: the ids rotted by a generation or two, and applyTaskAwareRouting
overwrites body.model directly, so a literal target skipped auto-combo's 13-factor
scoring (quota, circuit-breaker health, cost, latency, stability), connection
cooldown and model lockout — hard-failing for any operator with no connection for
that provider. Refreshing the strings would only reset the rot clock, so the
defaults now name auto/* INTENTS that resolve against the operator's actually
connected backends:

  coding        -> auto/coding
  analysis      -> auto/reasoning
  vision        -> auto/vision
  summarization -> auto/chat:fast
  background    -> auto/chat:cheap

creative and chat stay pass-through. Operators can still pin a specific model via
PUT /api/settings/task-routing; only the shipped defaults change. No provider/model
literal remains in the module.

diegosouzapw#8603 — the pattern-shadowing fix LANDED UPSTREAM while this PR was open
(f5df2cf, Train 1D). lookupStaticFitnessTable now ranks patterns longest-first,
so gpt-4o-mini no longer inherits gpt-4o's 0.9 and deepseek-v3.2 no longer inherits
deepseek-v3's 0.85. This PR therefore no longer changes that behaviour — the
upstream scan is kept verbatim.

What remains for diegosouzapw#8603 is the regression guard. The resolution chain hits the DB
(user_override / arena_elo / models.dev tier) before reaching layer 4, so asserting
the ordering through getTaskFitness would depend on DB fixture state. The layer is
exposed as getStaticFitnessTableScore and pinned directly by
taskFitness-pattern-order-8603.test.ts (7 cases), so the guarantee survives future
edits to FITNESS_TABLE. Those 7 cases were written against this PR's original
implementation and pass unchanged against the upstream one — independent
confirmation that the two are behaviourally equivalent.

* docs(changelog): add fragment for diegosouzapw#8605 task-routing intent + fitness order
@MumuTW
MumuTW deleted the fix/task-routing-config-not-restored branch September 5, 2026 10:19
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…adowing (diegosouzapw#8602, diegosouzapw#8603) (diegosouzapw#8605)

* fix(sse): restore task-aware routing config on restart (diegosouzapw#8601)

The T05 Task-Aware Smart Routing config was persisted to settings.taskRouting
by PUT /api/settings/task-routing but never read back, so it silently reverted
to enabled:false + the hardcoded default model map on every restart.

Two root causes, both fixed:

- No boot hydration existed. Adds hydrateTaskRoutingConfig(settings), wired into
  src/instrumentation-node.ts next to the Thinking-Budget restore (diegosouzapw#5312). It
  accepts either the JSON string the route persists or an already-parsed object,
  and fails open on malformed values. applyRuntimeSettings does not cover this
  key, same as the Global System Prompt (diegosouzapw#2470).

- The config lived in a plain module-level `let`, which is duplicated per module
  graph — a boot hydration would have landed on the instrumentation graph's copy
  and never reached the one src/sse/handlers/chat.ts reads. This is the exact
  break diegosouzapw#5312 fix-A hit on the VPS. Moves the store to the globalThis pattern
  already used by thinkingBudget.ts and systemPrompt.ts.

Runtime stats are never restored from the persisted blob.

Note the hydration is wired into instrumentation-node.ts, not the unused
src/server-init.ts.

* docs(changelog): add fragment for diegosouzapw#8604 task-routing boot restore

* fix(sse): route task-aware defaults by intent, guard fitness pattern order (diegosouzapw#8602, diegosouzapw#8603)

Two related defects in the hand-maintained model-quality tables.

diegosouzapw#8602 — DEFAULT_TASK_MODEL_MAP hardcoded literal provider/model ids
(openai/gpt-4o, gemini/gemini-2.5-flash-lite, deepseek/deepseek-chat, ...).
Wrong twice over: the ids rotted by a generation or two, and applyTaskAwareRouting
overwrites body.model directly, so a literal target skipped auto-combo's 13-factor
scoring (quota, circuit-breaker health, cost, latency, stability), connection
cooldown and model lockout — hard-failing for any operator with no connection for
that provider. Refreshing the strings would only reset the rot clock, so the
defaults now name auto/* INTENTS that resolve against the operator's actually
connected backends:

  coding        -> auto/coding
  analysis      -> auto/reasoning
  vision        -> auto/vision
  summarization -> auto/chat:fast
  background    -> auto/chat:cheap

creative and chat stay pass-through. Operators can still pin a specific model via
PUT /api/settings/task-routing; only the shipped defaults change. No provider/model
literal remains in the module.

diegosouzapw#8603 — the pattern-shadowing fix LANDED UPSTREAM while this PR was open
(74e2e45, Train 1D). lookupStaticFitnessTable now ranks patterns longest-first,
so gpt-4o-mini no longer inherits gpt-4o's 0.9 and deepseek-v3.2 no longer inherits
deepseek-v3's 0.85. This PR therefore no longer changes that behaviour — the
upstream scan is kept verbatim.

What remains for diegosouzapw#8603 is the regression guard. The resolution chain hits the DB
(user_override / arena_elo / models.dev tier) before reaching layer 4, so asserting
the ordering through getTaskFitness would depend on DB fixture state. The layer is
exposed as getStaticFitnessTableScore and pinned directly by
taskFitness-pattern-order-8603.test.ts (7 cases), so the guarantee survives future
edits to FITNESS_TABLE. Those 7 cases were written against this PR's original
implementation and pass unchanged against the upstream one — independent
confirmation that the two are behaviourally equivalent.

* docs(changelog): add fragment for diegosouzapw#8605 task-routing intent + fitness order
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(backend): task-aware routing config silently reverts to defaults on every restart — settings.taskRouting never rehydrated

2 participants