Context
During the v13 cost-safety follow-up on 2026-06-08, PR #12760 had already merged and changed the tracked Tier-1 config default from remote Gemini to local OpenAI-compatible routing:
ai/config.template.mjs:127 — chatProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')
ai/config.template.mjs:136 — modelProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')
But this checkout's gitignored ai/config.mjs still had:
ai/config.mjs:136 — modelProvider: leaf('gemini', 'NEO_MODEL_PROVIDER', 'string')
The live Memory Core healthcheck therefore still reported providers.summary.active = gemini / model = gemini-3.5-flash even though the tracked template was cost-safe. Running the script's own shape detector confirms the problem:
{
"missingImports": [],
"missingExports": [],
"missingEnvVars": [],
"hasDrift": false
}Live latest-open sweep: checked latest 80 open issues at 2026-06-08T14:18Z. No open ticket scopes default-value drift in initServerConfigs.mjs. Adjacent open items were #12740, #12743, #12746, #12747, #12748, and #12765, but none covers gitignored config overlays silently preserving old defaults after a template default flip.
Duplicate sweep: targeted GitHub searches for config.mjs default value drift migrate-config and NEO_MODEL_PROVIDER openAiCompatible config.mjs drift found closed predecessors #10815 and #12102. These are related but not equivalent: #10815 explicitly named semantic default drift as hard, but the current implementation still only detects structural/env-name shape; #12102 scoped overlay-preserving template advancement for missing leaves/import materialization, not same-env default-value changes. Semantic KB sweep query_documents("initServerConfigs config.mjs template drift default value migration") surfaced test/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjs and config docs, but no live open duplicate.
The Problem
initServerConfigs.mjs claims to detect template/config drift, and docs tell operators to run npm run prepare -- --migrate-config after template changes. That guidance is insufficient for the exact v13 cost-safety incident class: default-value changes inside existing leaf(default, env, type) calls are invisible when imports, exports, and env-var names are unchanged.
For NEO_MODEL_PROVIDER, that means a checkout can pull the merged local-first default, run the sanctioned config refresh path, and still keep a gitignored overlay defaulting Memory Core summaries to remote Gemini. The operator sees current source, but the live MCP/daemon process reads stale local config.
This is distinct from #12742. #12742 changed the tracked default. This ticket ensures existing overlays can detect and refresh that semantic change.
The Architectural Reality
ai/scripts/setup/initServerConfigs.mjs:81 projects imports, named exports, and env-var literals only.
ai/scripts/setup/initServerConfigs.mjs:119 says env-var projection catches new config leaves.
ai/scripts/setup/initServerConfigs.mjs:186 returns drift only from missing imports, missing exports, and missing env vars.
- Same-env default changes are not represented, so
leaf('gemini', 'NEO_MODEL_PROVIDER', 'string') and leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string') compare equal.
- ADR 0019 says
leaf(default, env, type) owns defaults and env override. The default is semantic config substrate, not an incidental string.
The Fix
Extend the config drift projection for initServerConfigs.mjs to capture env-bound leaf signatures, at minimum {path, default, env, type} or an equivalent stable representation, so --migrate-config can detect when the template changes the default for an existing env-bound leaf.
Suggested shape:
- Add a leaf/default projection for static
leaf(<literal>, '<ENV>', '<type>') calls and raw {env, default, parse} object leaves where practical.
- Include default-value drift in
detectDrift() output with a clear field such as changedLeafDefaults.
- Preserve existing import/export/env-var drift behavior.
- Add tests proving
modelProvider: leaf('gemini', 'NEO_MODEL_PROVIDER', 'string') vs leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string') reports drift.
- Decide whether
--migrate-config should overwrite the stale overlay as today, or whether this belongs behind the future overlay-preserving mode. For the current v13 cost-safety lane, fail-visible is the minimum; silent hasDrift:false is not acceptable.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
projectSourceShape() |
ai/scripts/setup/initServerConfigs.mjs |
Project env-bound leaf defaults in addition to imports/exports/env names |
Existing structural projection remains |
JSDoc update |
Unit fixture with same env var, changed default |
detectDrift() |
same script |
Report default-value drift as hasDrift:true |
Warn-only unless migration flag is used |
Drift output docs |
Unit test for NEO_MODEL_PROVIDER default flip |
| Operator config refresh |
npm run prepare -- --migrate-config docs |
No longer reports silent for cost-safety default flips |
Human can still manually edit gitignored config |
Deployment docs follow-up only if behavior changes |
Reproducer above |
Decision Record impact
aligned-with ADR 0019. This treats leaf(default, env, type) default values as part of the reactive Provider SSOT contract and avoids parallel env/default resolution.
Acceptance Criteria
Out of Scope
- Replacing the current overwrite behavior with a full overlay-preserving merge mode. That broader work is the #12102 lineage.
- Changing provider defaults again; #12742 already did that.
- Restarting agent harnesses or MCP servers.
- Committing gitignored
config.mjs overlays.
Avoided Traps
- Treating this as operator error. The operator followed the documented mental model; the detector returned
hasDrift:false for a real semantic drift.
- Only telling agents to restart MCP servers. Restarting a process with stale gitignored config preserves the wrong default.
- Adding provider-specific special cases for
NEO_MODEL_PROVIDER. The bug is generic to same-env leaf() default changes.
Related
Parent: #12740
Related: #12742
Related: #12752
Related: #10815
Related: #12102
Origin Session ID: e8f07ef9-ef7e-4815-8ff4-7abe13720621
Handoff Retrieval Hints: query_raw_memories("NEO_MODEL_PROVIDER config.mjs default drift initServerConfigs hasDrift false"); exact repro command node --input-type=module -e "import {projectShape, detectDrift} from './ai/scripts/setup/initServerConfigs.mjs'; const t=await projectShape('./ai/config.template.mjs'); const c=await projectShape('./ai/config.mjs'); console.log(JSON.stringify(detectDrift(t,c), null, 2));".
Context
During the v13 cost-safety follow-up on 2026-06-08, PR #12760 had already merged and changed the tracked Tier-1 config default from remote Gemini to local OpenAI-compatible routing:
ai/config.template.mjs:127—chatProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')ai/config.template.mjs:136—modelProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')But this checkout's gitignored
ai/config.mjsstill had:ai/config.mjs:136—modelProvider: leaf('gemini', 'NEO_MODEL_PROVIDER', 'string')The live Memory Core healthcheck therefore still reported
providers.summary.active = gemini/model = gemini-3.5-flasheven though the tracked template was cost-safe. Running the script's own shape detector confirms the problem:{ "missingImports": [], "missingExports": [], "missingEnvVars": [], "hasDrift": false }Live latest-open sweep: checked latest 80 open issues at 2026-06-08T14:18Z. No open ticket scopes default-value drift in
initServerConfigs.mjs. Adjacent open items were #12740, #12743, #12746, #12747, #12748, and #12765, but none covers gitignored config overlays silently preserving old defaults after a template default flip.Duplicate sweep: targeted GitHub searches for
config.mjs default value drift migrate-configandNEO_MODEL_PROVIDER openAiCompatible config.mjs driftfound closed predecessors #10815 and #12102. These are related but not equivalent: #10815 explicitly named semantic default drift as hard, but the current implementation still only detects structural/env-name shape; #12102 scoped overlay-preserving template advancement for missing leaves/import materialization, not same-env default-value changes. Semantic KB sweepquery_documents("initServerConfigs config.mjs template drift default value migration")surfacedtest/playwright/unit/ai/scripts/setup/initServerConfigs.spec.mjsand config docs, but no live open duplicate.The Problem
initServerConfigs.mjsclaims to detect template/config drift, and docs tell operators to runnpm run prepare -- --migrate-configafter template changes. That guidance is insufficient for the exact v13 cost-safety incident class: default-value changes inside existingleaf(default, env, type)calls are invisible when imports, exports, and env-var names are unchanged.For
NEO_MODEL_PROVIDER, that means a checkout can pull the merged local-first default, run the sanctioned config refresh path, and still keep a gitignored overlay defaulting Memory Core summaries to remote Gemini. The operator sees current source, but the live MCP/daemon process reads stale local config.This is distinct from #12742. #12742 changed the tracked default. This ticket ensures existing overlays can detect and refresh that semantic change.
The Architectural Reality
ai/scripts/setup/initServerConfigs.mjs:81projects imports, named exports, and env-var literals only.ai/scripts/setup/initServerConfigs.mjs:119says env-var projection catches new config leaves.ai/scripts/setup/initServerConfigs.mjs:186returns drift only from missing imports, missing exports, and missing env vars.leaf('gemini', 'NEO_MODEL_PROVIDER', 'string')andleaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')compare equal.leaf(default, env, type)owns defaults and env override. The default is semantic config substrate, not an incidental string.The Fix
Extend the config drift projection for
initServerConfigs.mjsto capture env-bound leaf signatures, at minimum{path, default, env, type}or an equivalent stable representation, so--migrate-configcan detect when the template changes the default for an existing env-bound leaf.Suggested shape:
leaf(<literal>, '<ENV>', '<type>')calls and raw{env, default, parse}object leaves where practical.detectDrift()output with a clear field such aschangedLeafDefaults.modelProvider: leaf('gemini', 'NEO_MODEL_PROVIDER', 'string')vsleaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')reports drift.--migrate-configshould overwrite the stale overlay as today, or whether this belongs behind the future overlay-preserving mode. For the current v13 cost-safety lane, fail-visible is the minimum; silenthasDrift:falseis not acceptable.Contract Ledger Matrix
projectSourceShape()ai/scripts/setup/initServerConfigs.mjsdetectDrift()hasDrift:trueNEO_MODEL_PROVIDERdefault flipnpm run prepare -- --migrate-configdocssilentfor cost-safety default flipsDecision Record impact
aligned-with ADR 0019. This treats
leaf(default, env, type)default values as part of the reactive Provider SSOT contract and avoids parallel env/default resolution.Acceptance Criteria
detectDrift(projectSourceShape(template), projectSourceShape(config))returnshasDrift:truewhen aleaf()has the same env var and type but a different literal default.NEO_MODEL_PROVIDERdefault changes fromgeminitoopenAiCompatible.AiConfigsingleton; it remains source-text projection for gitignored config files.Out of Scope
config.mjsoverlays.Avoided Traps
hasDrift:falsefor a real semantic drift.NEO_MODEL_PROVIDER. The bug is generic to same-envleaf()default changes.Related
Parent: #12740 Related: #12742 Related: #12752 Related: #10815 Related: #12102
Origin Session ID: e8f07ef9-ef7e-4815-8ff4-7abe13720621
Handoff Retrieval Hints:
query_raw_memories("NEO_MODEL_PROVIDER config.mjs default drift initServerConfigs hasDrift false"); exact repro commandnode --input-type=module -e "import {projectShape, detectDrift} from './ai/scripts/setup/initServerConfigs.mjs'; const t=await projectShape('./ai/config.template.mjs'); const c=await projectShape('./ai/config.mjs'); console.log(JSON.stringify(detectDrift(t,c), null, 2));".