Repository navigation
fix(memory): avoid rejected mid-conversation system injection on Claude when preceding turn isn't a tool result - #11303
Merged
diegosouzapw merged 1 commit intoAug 24, 2026
Conversation
…de when preceding turn isn't a tool result (#11290) Claude Opus 5 rejects the #3890 cache-safe mid-array system splice with HTTP 400 when the assistant turn immediately before the splice point is plain text rather than a server-side tool result. Adding claude/anthropic outright to systemMessageMustBeFirst() would revert the #3890 cache-hit optimization for every Claude request, so instead injectMemory() now only falls back to leading-system-message placement for the specific Claude- family requests where that condition holds, preserving the mid-array splice everywhere it remains safe (non-Claude providers unconditionally, and Claude providers whose preceding turn does end in a server tool result).
6 tasks done
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
…de when preceding turn isn't a tool result (diegosouzapw#11290) (diegosouzapw#11303) Validated on a 4-PR combined board: memory-cache-safe-injection + memory-glm-injection + chatcore-memory-skills-injection 39/39 (node:test), injection.test.ts 23/23 (vitest), typecheck:core clean, check:cycles clean, gates within baseline. Narrows the diegosouzapw#3890 mid-array system-splice skip to the exact shape that 400s on Claude Opus 5 (splice point not preceded by a server tool result) instead of disabling the cache-safe optimization for every Claude request — preserves the fast path everywhere it's actually safe. Closes diegosouzapw#11290.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #11290.
Claude Opus 5 rejects OmniRoute's
#3890cache-safe mid-array system-messagesplice with HTTP 400 in one specific shape: when the assistant turn
immediately before the splice point is plain text rather than a server-side
tool result (
*_tool_resultcontent block).injectMemory()(
src/lib/memory/injection.ts) always spliced the memory-context systemmessage right before the last user turn whenever
cacheSafewas set and theprovider was not in the
systemMessageMustBeFirst()strict set — andanthropic/claudewere never in that set, so every Claude cache-saferequest hit the same mid-array shape regardless of what preceded it.
Design decision
Two options were on the table:
claude/anthropictoBUILTIN_PROVIDERS_SYSTEM_MUST_BE_FIRST(
systemMessageMustBeFirst()), forcing every Claude request onto theleading-system-message path.
specific requests that trigger the 400.
Option 1 is the simplest change but reverts the
#3890prompt-cache-hitoptimization for every Claude request that uses prompt caching, including
the (presumably majority) requests where the preceding turn already ends in a
tool result and the splice works fine — a real cost regression for no gain
in those cases.
This PR takes option 2:
injectMemory()gained a narrower, additive checkthat only applies when
cacheSafeis on, the provider is in the Claudefamily (
claude,anthropic,isAnthropicCompatibleProvider()— which alsocovers
isClaudeCodeCompatibleProvider()'s narrower prefix), and the messageimmediately before the splice point is not an assistant turn ending in a
*_tool_resultcontent block. In that case it falls back to the existinginjectSystemFirst()leading-message placement (merging into the request'ssystem message, exactly like the strict-provider path already does) instead
of splicing. The mid-array splice is preserved everywhere it is safe: for all
non-Claude providers unconditionally, and for Claude providers whose
preceding turn does end in a server tool result.
Changes
src/lib/memory/injection.ts: addedisClaudeFamilyProvider()andendsWithServerToolResult()helpers, and a new gate ininjectMemory()applied after the existing
systemMessageMustBeFirst()check.src/lib/memory/__tests__/injection.test.ts: TDD regression test provingthe fallback (RED before the fix, GREEN after — verified locally by
reverting
injection.tsand re-running the new test in isolation).tests/unit/memory-cache-safe-injection.test.ts: the 3 pre-existinganthropic-provider cache-safe tests encoded exactly the now-fixed buggyshape (mid-array splice after a plain-text assistant turn), so they were
switched to a non-Claude-family provider (
openai) to keep testing theoriginal
#3890generic mechanism without regressing it. Added a newdescribe block with 4 tests covering: the plain-text fallback, the
preserved splice when the preceding turn is a server tool result, the
Claude-Code-compatible passthrough provider id, and confirmation that
non-Claude providers are unaffected.
Test plan
node --import tsx/esm --test tests/unit/memory-cache-safe-injection.test.ts tests/unit/memory-glm-injection.test.ts tests/unit/chatcore-memory-skills-injection.test.ts— all green (39 tests)npx vitest run src/lib/memory/__tests__/injection.test.ts— all green (23 tests)injection.tsto the pre-fix version, re-ran the new#11290vitest test — it failed as expected (mid-array splice still occurred), then restored the fix — it passed.npx eslinton the 3 changed files — clean (no newno-explicit-anybeyond the pre-existing, suppressed count formemory-cache-safe-injection.test.ts).tsc --pretty false -p tsconfig.typecheck-core.json— clean, no errors.npm run check:cycles— no cycles introduced by the newsrc/shared/constants/providersimport.