LearnNewsExamplesServices
Frontmatter
id12767
titleDetect AiConfig leaf default drift in config migration
stateClosed
labels
bugairegressionarchitecturebuildmodel-experience
assigneesneo-gpt
createdAtJun 8, 2026, 4:15 PM
updatedAtJun 8, 2026, 4:55 PM
githubUrlhttps://github.com/neomjs/neo/issues/12767
authorneo-gpt
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 8, 2026, 4:55 PM

Detect AiConfig leaf default drift in config migration

Closed v13.0.0/archive-v13-0-0-chunk-17 bugairegressionarchitecturebuildmodel-experience
neo-gpt
neo-gpt commented on Jun 8, 2026, 4:15 PM

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:127chatProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')
  • ai/config.template.mjs:136modelProvider: leaf('openAiCompatible', 'NEO_MODEL_PROVIDER', 'string')

But this checkout's gitignored ai/config.mjs still had:

  • ai/config.mjs:136modelProvider: 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

  • detectDrift(projectSourceShape(template), projectSourceShape(config)) returns hasDrift:true when a leaf() has the same env var and type but a different literal default.
  • The drift payload names the changed leaf/default clearly enough for the warning output to tell operators what changed.
  • Existing tests for import/export/env-var drift still pass.
  • A regression fixture covers the #12760 case: NEO_MODEL_PROVIDER default changes from gemini to openAiCompatible.
  • The implementation does not mutate or import the live AiConfig singleton; it remains source-text projection for gitignored config files.

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));".

tobiu referenced in commit 1f53981 - "fix(ai): detect config leaf default drift (#12767) (#12769)" on Jun 8, 2026, 4:55 PM
tobiu closed this issue on Jun 8, 2026, 4:55 PM