fix(init): seed resolveAIOptions from loadConfig() (closes #203)#1060
fix(init): seed resolveAIOptions from loadConfig() (closes #203)#1060vincedk-alt wants to merge 2 commits into
Conversation
Re-init no longer requires users to re-pass embedding flags they already wrote down in ~/.gbrain/config.json. resolveAIOptions now reads loadConfig() first, then applies CLI flag overrides on a per-field basis. Resolution chain (each step overrides the previous per-field): 1. ~/.gbrain/config.json via loadConfig() -- NEW 2. --model SHORTHAND (recipe lookup; sets both model + dims) 3. --embedding-model VERBOSE (overrides shorthand) 4. --embedding-dimensions N (overrides recipe-derived dims) 5. --expansion-model / --chat-model (additive) Gateway defaults still apply downstream for fields nothing in the chain populates -- cold-start behavior unchanged. claw-test fresh-install scenario is unaffected. Known limitation (deferred to follow-up issue): if a user sets GBRAIN_EMBEDDING_MODEL + GBRAIN_EMBEDDING_DIMENSIONS env vars but has NO existing config.json AND no DATABASE_URL, loadConfig() returns null at src/core/config.ts:135. Env-only callers still hit OpenAI/1536 gateway defaults on cold-start. Pinned by case 5 in the new test. Tests: test/init-config-first.test.ts -- 5 cases pinning the resolution chain (config-only, flag-only, both-override, empty cold-start, env-only known-limitation). Uses withEnv() + tempdir GBRAIN_HOME, no mock.module, no env mutation outside helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…garrytan#203 fully) PR garrytan#1060 closed the flat-shape case (config has `embedding_model` / `embedding_dimensions` but init ignored them). This commit closes the ORIGINAL reproducer in jamebobob's issue body: the v0.10.x NESTED shape `{embedding: {provider, model, dimensions, base_url}}`. v0.27 (PR garrytan#257) flattened the embedding config from nested to top-level fields. No migration was written. Users on v0.10.x configs silently fall through to gateway defaults (OpenAI 1536) on every command because no code path reads the nested shape. PR garrytan#1060's seed step only matched flat fields, so it didn't help users still carrying the original shape. This commit adds a back-compat read in `loadConfig()` that maps the nested shape to flat fields IN MEMORY before the env-merge step: - `embedding.{provider, model}` → `embedding_model: "provider:model"` - `embedding.dimensions` → `embedding_dimensions` - `embedding.{base_url, provider}` → `provider_base_urls[provider] = base_url` Flat fields win when both shapes are present (case 7). The next `saveConfig` serializes only declared flat fields, so the nested object drops out on save. Tests: 2 new cases in test/init-config-first.test.ts (case 6: jamebobob reproducer, case 7: flat-wins-over-nested). Suite now 9 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up commit: v0.10.x nested-shape coverage (close #203 fully)The original commit closed the case for users who already have flat-field configs ( PR #257 flattened the config in v0.27 but no migration was written. Anyone carrying a config.json from before that release silently falls through to gateway defaults (OpenAI 1536) on every command because no code path reads the nested shape. New commit
The next Two new test cases in
Full suite now 9 pass / 0 fail. Typecheck clean. This closes #203 for both shapes — users on the v0.10.x nested config AND users who've already manually re-saved to the flat shape. |
Summary
Re-init no longer ignores the embedding settings users already wrote down in
~/.gbrain/config.json.gbrain init --pglite(no flags) on a brain whose persisted config declares a non-defaultembedding_model/embedding_dimensions/provider_base_urlsnow respects those values instead of silently falling back to OpenAI 1536. Closes #203.Why
resolveAIOptionsinsrc/commands/init.tsonly consumed CLI flags. It never readloadConfig(), soaiOptscame out empty when no flags were passed. That tripped theif (embedding_model || chat_model)gate further down ininitPGLite(line 386), soconfigureGatewaywas never called,_configstayed null, andinitSchema()'s catch-block fell back to literal1536inpglite-engine.ts:212.Net result on every re-init without flags: the user's persisted choice was silently overridden by the OpenAI default. v0.28.5's
embedding-dim-checkcatches the resulting column mismatch on the next embed, but only AFTER the wrong-shape brain is already on disk and the user has spent another minute figuring out why their--embedding-model lmstudio:...setup keeps producingvector(1536)columns.What changed
One function (
resolveAIOptions) insrc/commands/init.ts. SeedsoutfromloadConfig()at the top, before the existing flag-handling code. Flags still win on a per-field basis. New resolution chain:~/.gbrain/config.jsonvialoadConfig()— NEW--model SHORTHANDrecipe lookup (sets model + dims)--embedding-model VERBOSE(overrides shorthand)--embedding-dimensions N(overrides recipe-derived dim)--expansion-model/--chat-model(additive)Gateway defaults still apply downstream when nothing in the chain populates a field — cold-start behavior unchanged.
claw-test's fresh-install scenario keeps working.Two edge cases caught during review and fixed in this PR:
lmstudio:nomic 768and the user reruns init with--embedding-model openai:text-embedding-3-large(no--embedding-dimensions), the seeded768is cleared so the recipe-default derivation re-fires for the new provider. Without this, the user would silently get OpenAI configured at 768-dim.provider_base_urlsrides along. Custom endpoint overrides (e.g.provider_base_urls.lmstudiopointing athttp://localhost:1234/v1) are preserved through the seed +saveConfigcycle. Without this, re-init dropped the user's endpoint override and the gateway fell back to recipe defaults on next call.Tests
test/init-config-first.test.ts— 7 cases, hermetic against the runner env via anisolateEnv()helper that unsetsDATABASE_URL,GBRAIN_DATABASE_URL, and everyGBRAIN_EMBEDDING_*/GBRAIN_*_MODEL/OPENAI_API_KEY/ANTHROPIC_API_KEY. UseswithEnv()from the canonical test helpers; nomock.module, no*.serial.test.tsquarantine needed. The 7 cases pin:--embedding-modelswitches provider away from config → seeded dim cleared, recipe-default re-derivesprovider_base_urlspreserved through the seed cycleCase 7 documents a separate bug class not closed by this PR: when there's no config.json AND no
DATABASE_URL,loadConfig()early-returns null atconfig.ts:135, so the env-var merge below that early-return never fires. SettingGBRAIN_EMBEDDING_DIMENSIONS=768env-only on a truly cold install still produces a 1536-dim schema. Filed at #1058 for separate fix.Deliberate-failure run:
DATABASE_URL=... GBRAIN_EMBEDDING_MODEL=... GBRAIN_EMBEDDING_DIMENSIONS=999 bun test test/init-config-first.test.ts→ 7 pass, 0 fail. TheisolateEnv()is adversarially proven, not just theoretical.Verification
bun run typecheck— cleanbun run verify— all 13 pre-checks pass (check:privacy, check:test-isolation with 410 non-serial files scanned, etc.)bun test test/init-config-first.test.ts— 7 pass, 0 failbun run test— full fast loop ran with 0 fails (two shards exceeded the 600s per-shard timeout cap; per gbrain CLAUDE.md that's the documented "infrastructure problem" classification, not test failures; all 4 shards' completed work reports 0 fail; new test is in shard 2 which completed in 1857 pass / 0 fail / 0 skip / rc=0)vector(768)(verified via directpg_attribute format_typeprobe) on a config-first init with no flagsReviewer notes
Two adversarial reviews ran against this branch before opening the PR:
bun -esmoke-test syntax error caught and re-run with ground truth, the rebase pre-check added before any force-push, and a test-isolation hole closed byisolateEnv()).codex review --base upstream/master3 rounds: round 1 surfaced the model-swap dim-stale bug (fixed), round 2 surfacedprovider_base_urlsfield-loss (fixed), round 3 clean.Both reviewers were against the same branch tip after each fix; their findings did not overlap, which is the cross-model-review value-add CLAUDE.md Rule 5b describes.
🤖 Generated with Claude Code