fix(anthropic): alias the session_search and memory schemas on the OAuth wire - #76807
fix(anthropic): alias the session_search and memory schemas on the OAuth wire#76807kshitijk4poor wants to merge 3 commits into
Conversation
…s rewrite Follow-ups from review of the salvaged fix. Issue NousResearch#65365's A/B repro isolates TWO triggers: the session_search schema alone reproduces the 400, and the memory schema alone reproduces it too. The salvaged change covered only session_search, so default-config subscription users still hit the rejection. - Alias the memory tool to context_notes on the OAuth wire, with the matching reverse mapping so it dispatches back as memory. - Split the single alias dict by purpose. Renaming a tool and rewriting the prose that describes it are different operations with different safety envelopes: memory is ordinary English throughout the system prompt and its own description, so only its NAME is aliased, while session_search stays a prose-safe token. Conflating the two would have handed the model mangled instructions for a tool it still has to use. - Match prose aliases on word boundaries. System blocks carry user-supplied text (project AGENTS.md, memory snapshots); a bare substring replace rewrote references like tools/session_search_tool.py into a path that does not exist and has no reverse mapping. - Restore the NousResearchGH-25255 registry-precedence contract in normalize_response. The alias reverse-lookup ran before the registry check, so a real MCP server tool named mcp_chat_history_lookup would have been misrouted to session_search. The alias is now the last resort, after the registry. - Correct the root-cause comment: it claimed the classifier fingerprints the combination of session_search and skill_manage guidance terms. skill_manage has no supporting evidence, and the issue's repro shows each schema triggering the 400 on its own, not in combination. Verified: 11 tests in the touched file, 285 anthropic + 216 transport tests green; E2E round-trip with real imports and the real tool registry (26/26 assertions); three mutation checks confirm each new guard fails when reverted.
… name
Found by running the review gate against the follow-up commit itself.
If a user has an MCP server tool named mcp_chat_history_lookup or
mcp_context_notes, it lands on the same wire name as the aliased
session_search / memory tool. Two identical tool names in one request is a
hard 400 from Anthropic — every call fails, which is strictly worse than
the classifier bug being fixed. Verified live before and after:
before: ['mcp__chat_history_lookup', 'mcp__chat_history_lookup',
'mcp__context_notes', 'mcp__context_notes'] -> duplicates
after: ['mcp__session_search', 'mcp__chat_history_lookup',
'mcp__memory', 'mcp__context_notes'] -> none
The outbound side now collects the wire names owned by non-alias tools and
skips the alias for any contested name, so a genuinely registered tool
keeps it. This mirrors the inbound registry-precedence rule, keeping both
directions in agreement about who owns a name.
Also from the gate:
- Test the invariant that makes leaving memory's prose unaliased safe: a
model following the system prompt can emit the canonical name, and the
bare-name registry fallback resolves mcp__memory -> memory.
- Assert the prose-alias set stays a subset of the alias map (a violation
is a bare KeyError at import) and that no alias's wire name is another
alias's canonical name (which would chain-rewrite prose).
- Document why sorted() over the frozenset is load-bearing: raw frozenset
order is hash-seed dependent, so a second prose alias would make system
bytes differ per process and break prompt caching unreproducibly.
Verified across PYTHONHASHSEED=0/1/42/random: identical output today,
and 6 distinct orderings without sorted() at 5 entries.
- Trim the call-site comment that restated the constant's own docs.
SummaryTwenty-two PRs address or reference this issue complex, covering endpoint selection, billing-error classification, OAuth credential identity, tool-name and system-prompt request-shape triggers, billing attribution, and auxiliary credential refresh. The diffs therefore represent distinct causes or recovery layers rather than one interchangeable fix. Related pull requests
Duplicates#6498, #21019, #40020, and #40073 overlap on extra-usage classification and guidance, with the focused behavior on main via #56128. #17681, #46687, and #28872 form the mcp-prefix chain, with #47723 superseding the viable mcp__ changes; #48177, #48202, and #69844 share the billing-marker mechanism; #76669 was salvaged into #76807; and #72263 overlaps the provider-neutral stale-input work in #62008. Suggested consolidationKeep #75697 and #76807 open with their focused salvage paths: they are the lane-eligible best fixes for the API-key-to-OAuth identity swap and the session_search/memory schema triggers. Keep #72173, #69844, and #72263 only with the explicit salvage conditions stated above; for #69844, the current A/B evidence does not override the blocking contributor policy decision on #48177. Close #6498, #6854, #10576, #17681, #19260, #21019, #24250 unless narrowed as specified, #26960, #28872, #32318, #40020, #40073, #46687, #48177, #48202, #70054, and #76669 as implemented, superseded, duplicate, or policy-blocked references. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I65365(["issue #65365 (open)"])
subgraph Dup76669 ["PRs duplicating each other"]
P76669["PR #76669 (closed)"]
P76807["PR #76807 (open)"]
end
P76807 -->|best fix| I65365
class I65365 open
class P76669 closed
class P76807 open
class P76669 best
class P76807 best
class P76807 target
click I65365 "https://github.com/NousResearch/hermes-agent/issues/65365"
click P76669 "https://github.com/NousResearch/hermes-agent/pull/76669"
click P76807 "https://github.com/NousResearch/hermes-agent/pull/76807"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 22 pull requests and 22 issues in this complex. Each diff was read against this issue; Assessment working set: 244 kB of PR diffs, 253 kB of issue/PR text, 82 kB of discussion (169 comments), 110 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
Summary
Claude Pro/Max subscription users can use the session-search and memory toolsets again. Both tool schemas are renamed to neutral names on the Anthropic OAuth wire and restored to their registry names before dispatch, so Anthropic's billing classifier stops scoring the request as a third-party app and rerouting it to the (empty) extra-usage lane — the misleading HTTP 400
You're out of extra usageon a perfectly valid token.Salvages #76669 by @konsisumer — commit cherry-picked with authorship preserved — plus one follow-up commit closing review findings.
Changes
Salvaged (@konsisumer):
agent/anthropic_adapter.py: aliassession_search→chat_history_lookupon the OAuth path only — tool name, tool description, system-prompt text, and replayed historytool_usenames.agent/transports/anthropic.py: reverse the alias innormalize_responseso the dispatcher receives the canonicalsession_search.Follow-ups (review findings):
memoryschema is aliased too (→ context_notes). Issue OAuth (Claude Pro/Max): exposingmemoryorsession_searchtool schema deterministically triggers HTTP 400 "You're out of extra usage" #65365's A/B repro isolates two independent triggers —base + session_search → 400andbase + memory → 400. The salvaged change covered only the first, so default-config users still hit the rejection.memoryis ordinary English throughout the system prompt ("persistent memory across sessions", "OS, CPU, memory, disk") and its own description, so only its name is aliased;session_searchremains a prose-safe token. One dict driving both would have handed the model mangled instructions for a tool it still has to use.AGENTS.md, memory snapshots); a bare substring replace rewrote references liketools/session_search_tool.pyinto a path that doesn't exist and has no reverse mapping.mcp_prefix from Hermes-native MCP tool names, breaking registry lookup #25255 registry precedence restored. The alias reverse-lookup ran before the registry check, so a real MCP server tool namedmcp_chat_history_lookupwould have been misrouted tosession_search. The alias is now the last resort.mcp_chat_history_lookup/mcp_context_notes, the aliased tool collided with it — two identical tool names in one request is a hard 400, i.e. every call fails, strictly worse than the bug being fixed. The outbound side now yields the contested name to the genuine tool, mirroring the inbound precedence rule. (Found by running the review gate against my own follow-up commit, and verified live.)session_searchandskill_manageguidance terms";skill_managehas no supporting evidence and the repro shows each schema triggering on its own, not in combination.Validation
tests/agent/test_anthropic_mcp_prefix_strip.pytests/agent/ -k anthropictests/agent/transports/terminal/MCP tools unaffected, longer identifiers not mangled,memoryprose intact, caller inputs unmutated, old cachedsession_searchnames still dispatch, non-OAuth cleanmemoryalias, or the wire-name collision guard each fails a testPrompt caching: the rename is deterministic, so the cached prefix stays byte-stable within a conversation — verified across
PYTHONHASHSEED=0/1/42/random(thesorted()over the prose-alias set is load-bearing; without it, 5 aliases produced 6 different orderings). Existing OAuth conversations take a one-time cache invalidation on upgrade (the system text changes) — unavoidable for any fix at this layer.Not verified live: whether Anthropic now bills these requests to plan limits. That needs a subscription account (none available in this environment — HTTP 200 wouldn't prove the billing lane anyway, only the dashboard does). The premise rests on #65365's deterministic A/B repro; @tyoon10 and @Krypt0nBull3t both reproduced it independently and the reporter offered to test candidate branches.
Based on #76669 by @konsisumer. Fixes #65365.