fix(combo): universal-handoff fixes — bare-fallback note, same-request scoping, silent-failure logging - #12338
Conversation
Both generateHandoffAsync (context-relay) and generateUniversalHandoffAsync (universal-handoff) had three return paths -- empty history, non-ok summary model response, and unparseable model output -- that returned/discarded the outcome with zero logging anywhere. context_handoffs staying empty gave no signal on why: every live handoff falls back to the bare no-summary note (the model gets a generic "conversation continues below" system message with no real content), which is what the user actually sees, and there was no way to tell whether the cause was upstream instability, a malformed model reply, or something else. Found live: OmniRoute's context_handoffs table had zero rows despite repeated real handoffs over several hours, with no diagnostic trail explaining why generation kept failing.
…ummary-generation failures) into dev/omniroute-dev-combined
…t doesn't provide
Observed live: a weak free-tier fallback model (mimo-v2.5-free), handed
this exact bare note ("the conversation continues below without losing
context") plus only two trimmed tool-output strings as input -- no real
conversation history -- fabricated a plausible-sounding but entirely
invented response about an unrelated ML research topic. The note claimed
continuity the request never actually provided, and the model filled the
gap by inventing content instead of behaving conservatively.
The note now says the opposite: no summary is available, the input below
is the entire context available, and the model should not invent details
about history it cannot see.
…ummary-generation failures) into dev/omniroute-dev-combined # Conflicts: # open-sse/services/contextHandoff.ts # tests/unit/universal-handoff.test.ts
… targets A same-request fallback target (i > 0 within the same client request) serves the SAME request the failed primary target would have served, with the original messages already intact -- the client never saw the earlier target fail, so there is no genuine handoff to explain. The universal- handoff mechanism previously compared every attempt (including same- request fallbacks) against the session's PREVIOUS TURN model, injecting a context-free note whenever they differed. That replaces real conversation content with a note claiming continuity the request doesn't provide. Observed live: a same-request fallback landing on a weak free-tier model, handed this note plus only trimmed tool-output input (no real history), fabricated an entirely invented response about an unrelated topic instead of just answering the actual request. Gates both handoff call sites (injection + background-summary trigger) on i === 0. recordSessionModelUsage bookkeeping stays unconditional -- it must reflect whichever model actually served the response, since the next request's i === 0 comparison depends on that being accurate. This restores the intent of an earlier attempt at this same fix (ffbd069, part of diegosouzapw#12227) that was reverted because it broke a test asserting the OLD behavior was correct; that test's premise conflated same-request fallback with genuine cross-turn provider switches. Updated it to assert the corrected contract: a same-request fallback target receives the original request unmodified, with zero handoff messages injected.
…-lines ratchet Compacted the two multi-line log calls added earlier in this branch and the nested response-parsing try/catch -- no behavior change.
|
Extended this PR while investigating live: the empty- Same-request fallback targets ( This restores the intent of an earlier attempt at this same fix in #12227 (commit ffbd069) that was reverted because it broke a test asserting the old behavior was correct -- that test conflated same-request fallback with genuine cross-turn provider switches. Fixed now with the corrected, narrower contract: a same-request fallback target gets the original request untouched. |
…ummary-generation failures) into dev/omniroute-dev-combined # Conflicts: # open-sse/services/contextHandoff.ts # tests/unit/universal-handoff.test.ts
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
…re-fallback note, same-request scoping, silent-failure logging) into dev/omniroute-dev-combined
… faster (#12293) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…from crashing /api/providers (#12355) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…o the client (#12447) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…e output_index (#12445) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…2446) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…pty output array (#12460) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…erf fixes (#12448) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
… faster (diegosouzapw#12293) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
… faster (diegosouzapw#12293) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…from crashing /api/providers (diegosouzapw#12355) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…o the client (diegosouzapw#12447) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…e output_index (diegosouzapw#12445) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…egosouzapw#12446) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…pty output array (diegosouzapw#12460) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…zapw#12461) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…t scoping, silent-failure logging (diegosouzapw#12338) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
…erf fixes (diegosouzapw#12448) Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem. O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (diegosouzapw#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (diegosouzapw#12338, os fixes do universal-handoff). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva. Obrigado, @hartmark.
Summary
Three related fixes to the universal-handoff mechanism, found and root-caused live while investigating a real production report: a handoff reply that carried no actual context and, separately, one that fabricated an entirely invented response.
Originally scoped narrower; consolidated here (replacing #12227, which is now closed as superseded — see that PR for the initial, incomplete Spanish-note fix this supersedes).
What was actually happening
Traced live:
context_handoffshad zero rows despite handoffs firing repeatedly over hours, and separately a same-request fallback landed on a weak free-tier model that fabricated a plausible-sounding but entirely invented response about an unrelated topic.Fix 1 — same-request fallback was never a genuine handoff
The universal-handoff mechanism compared every combo target attempt — including same-request fallback steps (
i > 0, invisible to the client since the primary target's failure is never exposed) — against the session's previous-turn model. A same-request fallback landing on a different model than last turn wrongly counted as a "genuine handoff," injecting a system-role note and kicking off a background summary call for a switch the client never actually experienced.An earlier attempt at exactly this fix (commit
ffbd0699, part of the now-closed #12227) was reverted because it broketests/unit/combo-context-relay.test.ts, which asserted the old behavior was correct. That test's premise conflated same-request fallback with genuine cross-turn provider switches — fixed here with the corrected, narrower contract: a same-request fallback target now gets the original request completely untouched, gated oni === 0at both call sites (injection + background-summary trigger).recordSessionModelUsagebookkeeping stays unconditional, since it must reflect whichever model actually served the response regardless ofi.Fix 2 — the bare-fallback note claimed continuity it didn't provide
When no prior summary exists (e.g. the very first handoff for a session, or generation failed), the fallback note said "The conversation continues below without losing context" — while the actual request carried only trimmed input (e.g. a bare tool result), no real history. A weak fallback model handed that note, plus almost no real context, filled the gap by inventing content instead of behaving conservatively. The note now says the opposite: no summary is available, the input below is the entire context available, do not invent details about history you cannot see.
Fix 3 — silent failures gave no diagnostic trail
Both handoff-summary generators (
generateHandoffAsync/generateUniversalHandoffAsync) had three failure returns — empty history, non-oksummary-model response, unparseable model output — with zero logging anywhere. This is howcontext_handoffsstayed empty for hours with no way to tell why. Added a shared logger on all three paths in both functions.User Impact
context_handoffsbecomes traceable in logs.Validation
tests/unit/universal-handoff.test.ts,tests/unit/context-handoff.test.ts,tests/unit/combo-context-relay.test.ts,tests/unit/service-context-handoff.test.ts,tests/unit/db-context-handoffs.test.ts,tests/unit/chat-context-relay.test.ts,tests/integration/combo-matrix/context-relay-handoff.test.ts— all green, including the rewrittencombo-context-relay.test.tscase now asserting the corrected same-request-fallback contract.upstream/release/v3.8.51tip at branch time.call_logsartifact inspection, deployed the fix, confirmed clean boot and continued healthy serving.