fix(routing): make the settings-injected hooks-routing loadable on a bundle that does not ship it - #295
Merged
Brian Krabach (bkrabach) merged 1 commit intoSep 3, 2026
Conversation
…bundle that does not ship it
`routing.matrix: <name>` in settings.yaml injects a `hooks-routing` override
(runtime/config.py). When the active bundle does not already include the
routing-matrix bundle, `_apply_hook_overrides` APPENDS that override verbatim as
the mount-plan entry. Two independent gaps then combined to make the hook fail
at mount, silently:
1. The override carried no `source`. Nothing in the resolver chain knows where
`hooks-routing` lives, so the kernel failed it by name:
Failed to load hook 'hooks-routing': Module 'hooks-routing' not found in
prepared bundle. Available modules: [...]
2. Even WITH a source, the module could not load. It reaches the mount plan
AFTER Bundle.prepare() (settings are applied to the prepared mount plan), so
it is not in the bundle resolver's activated `_paths`. Foundation's
`BundleModuleResolver.async_resolve` exists precisely to lazily activate such
a module from its source hint -- but the kernel loader only takes that path
when the mounted resolver exposes `async_resolve`, and `AppModuleResolver`
(mounted at root as `module-source-resolver`, inherited by every child) did
not. The loader fell to the sync path, and there `except ModuleNotFoundError`
caught the BUILTIN class while the bundle resolver raises
`amplifier_core.module_sources.ModuleNotFoundError`, which is not a subclass
of it -- so the settings fallback (which does honor source hints) never ran
either.
Why it looked like it worked: any session on a bundle that DOES include
routing-matrix (foundation) editable-installs `amplifier_module_hooks_routing`
into the app venv as a side effect, registering an `amplifier.modules` entry
point. From then on the source-less injection resolves via the installed-package
fallback for every bundle -- until `uv tool install` / auto-update recreates the
venv, at which point routing silently vanishes for every bundle that does not
ship the hook. Measured on `anchors-amp-dev` with `routing.matrix: anthropic`
across exactly such a reinstall: the routing banner left the system prompt
(101,106 -> 99,832 chars), the delegate tool dropped its `model_role` parameter
(no `model_role_resolver` capability), and every `model_role` fell through to
the default provider. No error reached the user.
THE FIX (two halves, both needed)
runtime/config.py: when the bundle has no hooks-routing of its own, attach the
canonical source -- `WELL_KNOWN_BUNDLES["routing-matrix"]["remote"]` (the same
entry `amplifier routing` / `amplifier update` use) narrowed to
`#subdirectory=modules/hooks-routing`, matching the bundle's own
behaviors/routing.yaml. Only on the append path: merge_module_items lets an
override's top-level keys win, so attaching unconditionally would clobber a
bundle's deliberately pinned source.
lib/bundle_loader/resolvers.py: give `AppModuleResolver` an `async_resolve`
that delegates to the bundle resolver's lazy activation (falling back to sync
resolve for an older foundation), then the settings fallback, then the same
informative error. Catch BOTH ModuleNotFoundError classes on both paths so the
fallback policy actually applies.
MEASURED, anchors-amp-dev, app venv wiped of the side-effect install (the
post-reinstall condition), same prompt: delegate(agent=self, model_role=fast)
before Failed to load hook 'hooks-routing' ... not found in prepared bundle
routing banner in system prompt: False
after hooks-routing source = git+...routing-matrix@main#subdirectory=modules/hooks-routing
routing banner in system prompt: True
delegate:agent_spawned model_role='fast'
provider_preferences=[{provider: haiku, model: claude-haiku-4-5-20251001}]
child: pri=0 id=haiku effort=high fallback_on_overload=None
child llm:request: anthropic/claude-haiku-4-5-20251001, 0 [PROVIDER] warnings
Tests: 13 new. 1647 pass, up from 1634; no existing test changed. The new
resolver tests fail against the unfixed resolvers.py (6/7; the 7th pins the
exception-class fact). The pre-existing AppModuleResolver tests only ever
raised the builtin class, which is how the mismatch survived.
Brian Krabach (bkrabach)
deleted the
fix/settings-injected-hooks-routing-loadable
branch
September 3, 2026 07:07
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.
The defect
routing.matrix: <name>in settings.yaml injects ahooks-routingoverride (runtime/config.py). When the active bundle does not already include the routing-matrix bundle,_apply_hook_overridesappends that override verbatim as the mount-plan entry — and the hook then fails at mount, silently:No error reaches the user. What they see instead: no routing banner in the system prompt, no
model_roleparameter on thedelegatetool (tool-delegate drops it when nomodel_role_resolvercapability is registered), and everymodel_rolefalling through to the default provider.Why it looked like it worked
Any session on a bundle that does include routing-matrix (
foundation) editable-installsamplifier_module_hooks_routinginto the app venv as a side effect, registering anamplifier.modulesentry point. From then on the source-less injection resolves via the installed-package fallback for every bundle — untiluv tool install/ auto-update recreates the venv, and routing silently vanishes for every bundle that doesn't ship the hook.Measured on
anchors-amp-devwithrouting.matrix: anthropic, across exactly such a reinstall (2026.09.03-b85867c): system prompt shrank 101,106 → 99,832 chars (the banner),delegatelostmodel_role, sub-agents ran on the priority-1 default provider.Two independent gaps, both needed
1. No
sourceon the injected entry (runtime/config.py). Nothing in the resolver chain knows wherehooks-routinglives.2. Even with a source, it couldn't load (
lib/bundle_loader/resolvers.py). The entry reaches the mount plan afterBundle.prepare(), so it isn't in the bundle resolver's activated_paths. Foundation'sBundleModuleResolver.async_resolveexists precisely to lazily activate such a module from its hint — but the kernel loader only takes that path when the mounted resolver exposesasync_resolve, andAppModuleResolver(mounted at root asmodule-source-resolver, inherited by every child) did not. It fell to the sync path, whereexcept ModuleNotFoundError:caught the builtin class while the bundle resolver raisesamplifier_core.module_sources.ModuleNotFoundError— not a subclass — so the settings fallback (which honours source hints) never ran either.The fix
runtime/config.py— when the bundle has nohooks-routingof its own, attach the canonical source:WELL_KNOWN_BUNDLES["routing-matrix"]["remote"](the same entryamplifier routing/amplifier updatealready use) narrowed to#subdirectory=modules/hooks-routing, matching the bundle's ownbehaviors/routing.yaml. Append path only:merge_module_itemslets an override's top-level keys win, so attaching unconditionally would clobber a bundle's deliberately pinned source.lib/bundle_loader/resolvers.py—AppModuleResolver.async_resolve: bundle lazy activation first (syncresolvefor an older foundation without it), then the settings fallback, then the same informative error. Catch bothModuleNotFoundErrorclasses on both paths so the fallback policy actually applies. Policy unchanged: bundle → settings → error.Measured —
anchors-amp-dev, app venv wiped of the side-effect install, same promptdelegate(agent='self', model_role='fast'):Failed to load hook 'hooks-routing'git+…routing-matrix@main#subdirectory=modules/hooks-routingdelegate:agent_spawnedmodel_role=null(param absent)model_role='fast'→[{provider: haiku, model: claude-haiku-4-5-20251001}]pri=0 id=haiku effort=high fallback_on_overload=Nonellm:requestanthropic/claude-haiku-4-5-20251001, thinking budget 32000[PROVIDER]warningsLazy activation installed the module into the venv on that run (entry point now present), so subsequent sessions are fast-path.
Tests
1647 pass, up from 1634. No existing test changed. 13 new:
TestRoutingHookSource/TestRoutingHookSourceIntegration(through the realresolve_bundle_config): source attached when the bundle lacks the hook; attached when the bundle has no hooks at all; a bundle's pinned source is never clobbered; norouting.matrix→ nothing injected; source derives fromWELL_KNOWN_BUNDLES.TestAppModuleResolverLazyActivation: pins that the kernel exception is not the builtin; sync fallback now runs on the kernel class;async_resolvedelegates to bundle lazy activation with the hint; falls back to settings on kernel exception; works with a bundle resolver lackingasync_resolve; raises the informative error when both fail;profile_hintalias.Verified the new resolver tests fail against the unfixed
resolvers.py(6/7 — the 7th pins the exception-class fact). The pre-existingAppModuleResolvertests only ever raised the builtin class, which is how the mismatch survived.ruff check+ruff format --checkclean on all four touched files.Related
microsoft/amplifier-bundle-routing-matrix#57 fixed the other half of the reported symptom — once routing loads, the
fastrole now selects the instance configured for haiku instead of the priority-1 opus instance, so the child no longer inheritsreasoning_effort: xhigh/fallback_on_overload: true. This PR is what makes that fix reachable on a bundle that doesn't ship routing-matrix.