- Budget prompts for one chat run one at a time and re-read the total, so a
parallel call sees what the previous approval booked and identical prompts
can't coalesce into one answer that releases several charges.
- Totals are never evicted (a forgotten total would renew a spent budget) and
move with their policy when a process starts multiplexing.
- A native entry rejected for a malformed budget is not revived by a same-name
package's looser one.
Servers (config.yaml tool_costs/spend_limit/cost_unit, portable packages
extensions...servers.<name>.costs/spend_limit/cost_unit) can declare what each
tool spends. The approval prompt names the cost and the chat's running total,
and a costed call that would take a chat past spend_limit asks even on a
trust: full server; unattended contexts refuse it without dispatching.
Tally is per chat (approval session key) per profile-scoped server key, counts
only dispatched calls, and refunds a call that never reached the server.
Without a declaration nothing changes. Idea: Adolanium/hermes-tripo.
rekey_unscoped_connections moved the bare key but left the owner in
_server_scope_keys as None and the tools in the process-global registry map.
After the switch, status read the server as configured, config pruning and
/reload-mcp (which select by owner == scope) skipped it, and teardown
deregistered from the wrong slot. The owner, the adoption set and the tool
registrations now move to the launch profile's scope with the key.
A single-profile process keys MCP connections by bare name. After the switch,
serves_routed_profile() is true and lookups resolve (launch scope, name), so the
default profile's servers read as disconnected until a restart.
tools.mcp_tool_scope.rekey_unscoped_connections moves every bare-name ledger
entry under the launch scope at activation.
Under gateway.multiplex_profiles every connection ledger in tools/mcp_tool.py
(_servers, _server_scope_keys/_server_tool_scopes, connecting/error/cooldown
maps, the circuit breaker, lazy schema-cache configs, trust metadata) was keyed
by the bare server NAME. The common per-tenant layout — each profile names its
server `github`/`notion` with its own token — gave only the first profile a
connection: the second profile's register_mcp_servers saw the name as "already
connected", refused to adopt it (different credentials, 4ddbcbd35e), and left
the profile silently tool-less with a healthy-looking `configured` status
(#106005 Bug 1/2, #91654). Siblings of the same bug: profile A's failing `x`
put profile B's healthy `x` into A's 10-minute connect cooldown and A's open
circuit breaker short-circuited B's calls; toolsets._resolve_toolset_memo was
not scope-keyed, so B resolved A's `mcp-<server>` tool names.
Keys are now the connection key from the new tools/mcp_tool_scope.py: the bare
name outside a multiplexer (single-profile processes are unchanged) and
(owner_scope, name) under one. Call-time lookups (_resolve_server_key) prefer
the calling scope's own connection, then a shared connection it adopted, so
identical-route profiles still share one subprocess. _select_new_servers,
the cooldown/breaker/trust maps, lazy registration and get_mcp_status all read
and write through the composite key; teardown resolves a task's key by
identity (the MCP loop has no profile context). The toolset memo key includes
registry.current_scope_key().
An owner's scoped /reload-mcp tore down its connection and, with it, every
adopting profile's tool overlay; nothing re-ran the adopters' discovery until
they reloaded. shutdown_mcp_servers(scope=) now records the orphaned adopters
and register_mcp_servers re-registers them under their own home + secret scope
once the owner's rediscovery pass completes.
Docs: multi-profile-gateways.md states the per-profile connection rule.
Fixes#106005Fixes#91654
Co-authored-by: Bergmann89 <info@bergmann89.de>
Co-authored-by: Izzy-Gottz <srulynj@gmail.com>