Skip to content

fix(anthropic): alias the session_search and memory schemas on the OAuth wire - #76807

Closed
kshitijk4poor wants to merge 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage-76669-oauth-tool-schema-aliases
Closed

fix(anthropic): alias the session_search and memory schemas on the OAuth wire#76807
kshitijk4poor wants to merge 3 commits into
NousResearch:mainfrom
kshitijk4poor:salvage-76669-oauth-tool-schema-aliases

Conversation

@kshitijk4poor

@kshitijk4poor kshitijk4poor commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

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 usage on 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: alias session_searchchat_history_lookup on the OAuth path only — tool name, tool description, system-prompt text, and replayed history tool_use names.
  • agent/transports/anthropic.py: reverse the alias in normalize_response so the dispatcher receives the canonical session_search.
  • Tests for the OAuth request shape, the response round-trip, and the API-key passthrough.

Follow-ups (review findings):

  • The memory schema is aliased too (→ context_notes). Issue OAuth (Claude Pro/Max): exposing memory or session_search tool schema deterministically triggers HTTP 400 "You're out of extra usage" #65365's A/B repro isolates two independent triggers — base + session_search → 400 and base + memory → 400. The salvaged change covered only the first, so default-config users still hit the rejection.
  • Alias dict split by purpose. Renaming a tool and rewriting the prose that describes it have different safety envelopes: memory is 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_search remains a prose-safe token. One dict driving both would have handed the model mangled instructions for a tool it still has to use.
  • Prose aliases match 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 doesn't exist and has no reverse mapping.
  • [Bug]: Anthropic OAuth strips 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 named mcp_chat_history_lookup would have been misrouted to session_search. The alias is now the last resort.
  • An alias can never duplicate a real tool's wire name. If a user owns an MCP tool named 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.)
  • Root-cause comment corrected — it claimed the classifier fingerprints "the combination of session_search and skill_manage guidance terms"; skill_manage has no supporting evidence and the repro shows each schema triggering on its own, not in combination.

Validation

Check Result
tests/agent/test_anthropic_mcp_prefix_strip.py 14 passed (7 salvaged + 7 new)
tests/agent/ -k anthropic 288 passed, 3 skipped
tests/agent/transports/ 216 passed
E2E, real imports + real tool registry 26/26 — both aliases round-trip, terminal/MCP tools unaffected, longer identifiers not mangled, memory prose intact, caller inputs unmutated, old cached session_search names still dispatch, non-OAuth clean
Mutation checks reverting word-boundary matching, the registry-precedence order, the memory alias, or the wire-name collision guard each fails a test
ruff clean

Prompt caching: the rename is deterministic, so the cached prefix stays byte-stable within a conversation — verified across PYTHONHASHSEED=0/1/42/random (the sorted() 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.

konsisumer and others added 2 commits August 2, 2026 15:11
…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.
@alt-glitch alt-glitch added type/bug Something isn't working P1 High — major feature broken, no workaround comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/memory Memory tool and memory providers tool/skills Skills system (list, view, manage) provider/anthropic Anthropic native Messages API area/billing Account usage, credit usage, billing (cross-cutting) needs-decision Awaiting maintainer decision before any implementation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 2, 2026
… 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.
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown

This was generated by AI during triage.

Summary

Twenty-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 consolidation

Keep #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 graph

flowchart 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"
Loading

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/billing Account usage, credit usage, billing (cross-cutting) area/memory Memory subsystem: store, providers, sync, background reviews area/sessions Session lifecycle, resume, persistence, history comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint needs-decision Awaiting maintainer decision before any implementation P1 High — major feature broken, no workaround provider/anthropic Anthropic native Messages API sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/memory Memory tool and memory providers tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OAuth (Claude Pro/Max): exposing memory or session_search tool schema deterministically triggers HTTP 400 "You're out of extra usage"

5 participants