Low severity and latent today. Filing because the seam is misleading, not because anything is broken right now.
All refs are against master 1712536e7.
What the fallback reads
When a request arrives without a model chain, the module uses binding.config.model_chain (lib.rs:11344, requested_model_chain.unwrap_or(&binding.config.model_chain)). That chain is built in config.rs:486-530 from exactly four JSON pointers:
/historian/module_model + /historian/module_fallback_models, or failing those
/historian/model + /historian/fallback_models
Why it's always empty on a schema-valid config
assets/magic-context.schema.json declares none of those four keys. historian's top-level properties are color, description, disable, disallowed_tools, maxSteps, maxTokens, mode, omp, opencode, permission, pi, prompt, temperature, tools, top_p, two_pass. The model lives only in the per-harness blocks: historian.opencode.model, .pi.model, .omp.model.
So a config written to the schema gives the module an empty on-disk chain. On a live per-harness config here, all four pointers evaluate to null. module_model isn't in the user-facing docs or CONFIGURATION.md either; I only found it in internal design docs and in the config.rs tests. Because additionalProperties is unset, the schema tolerates the key without declaring it.
Why nothing breaks today
TS hosts always send a non-empty chain, so the fallback is never reached:
- historian:
rust-mode-transform.ts:2642 sends historian_model_chain: resolvedHistorianModelChain(deps)
- classify:
classify.ts:500 sends model_chain whenever it's non-empty
It becomes live if a host ever sends an empty chain, or if a module consumer depends on the on-disk config. Then the request fails with "classify producer has no usable model" (lib.rs:11429) instead of using the configured models. I read that consequence from source. I have not exercised a no-chain request end to end.
Second half: dreamer tasks read the historian's keys
dreamer.run_task falls back to the same binding.config.model_chain, which is built only from /historian/*. A dreamer config (dreamer.opencode.model etc.) is never consulted on this path. If someone gives the dreamer a different chain from the historian and relies on the module fallback, the dreamer runs the historian's models, with nothing in the logs to show it.
Options, not prescribing
- Read the per-harness layout the schema actually defines (
historian.<harness>.model, keyed by the binding's harness), and read dreamer.* for dreamer tasks.
- If
module_model / module_fallback_models is the intended interface for non-TS consumers (the comment at config.rs:487-495 suggests it separates plugin-namespace ids from runner catalog ids), declare both in the schema and document them so users can find them.
- At minimum, log once when the fallback resolves to an empty chain, so it doesn't fail as "no usable model" without saying why.
Verification status
- Pointer set and schema contents: read on master
1712536e7.
- Empty result on a real per-harness config: measured.
- Host-always-sends: read at both call sites.
- Empty-chain failure path: read, not exercised.
Low severity and latent today. Filing because the seam is misleading, not because anything is broken right now.
All refs are against master
1712536e7.What the fallback reads
When a request arrives without a model chain, the module uses
binding.config.model_chain(lib.rs:11344,requested_model_chain.unwrap_or(&binding.config.model_chain)). That chain is built inconfig.rs:486-530from exactly four JSON pointers:/historian/module_model+/historian/module_fallback_models, or failing those/historian/model+/historian/fallback_modelsWhy it's always empty on a schema-valid config
assets/magic-context.schema.jsondeclares none of those four keys.historian's top-level properties arecolor, description, disable, disallowed_tools, maxSteps, maxTokens, mode, omp, opencode, permission, pi, prompt, temperature, tools, top_p, two_pass. The model lives only in the per-harness blocks:historian.opencode.model,.pi.model,.omp.model.So a config written to the schema gives the module an empty on-disk chain. On a live per-harness config here, all four pointers evaluate to
null.module_modelisn't in the user-facing docs orCONFIGURATION.mdeither; I only found it in internal design docs and in theconfig.rstests. BecauseadditionalPropertiesis unset, the schema tolerates the key without declaring it.Why nothing breaks today
TS hosts always send a non-empty chain, so the fallback is never reached:
rust-mode-transform.ts:2642sendshistorian_model_chain: resolvedHistorianModelChain(deps)classify.ts:500sendsmodel_chainwhenever it's non-emptyIt becomes live if a host ever sends an empty chain, or if a module consumer depends on the on-disk config. Then the request fails with
"classify producer has no usable model"(lib.rs:11429) instead of using the configured models. I read that consequence from source. I have not exercised a no-chain request end to end.Second half: dreamer tasks read the historian's keys
dreamer.run_taskfalls back to the samebinding.config.model_chain, which is built only from/historian/*. A dreamer config (dreamer.opencode.modeletc.) is never consulted on this path. If someone gives the dreamer a different chain from the historian and relies on the module fallback, the dreamer runs the historian's models, with nothing in the logs to show it.Options, not prescribing
historian.<harness>.model, keyed by the binding's harness), and readdreamer.*for dreamer tasks.module_model/module_fallback_modelsis the intended interface for non-TS consumers (the comment atconfig.rs:487-495suggests it separates plugin-namespace ids from runner catalog ids), declare both in the schema and document them so users can find them.Verification status
1712536e7.