diff --git a/.env.example b/.env.example index 2010903..5dee470 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,11 @@ EMBEDDING_DIMENSIONS=1024 # LLM_PROVIDER=openai # LLM_MODEL=gpt-4o-mini +# --- Runtime config mutation (dev/test only) --- +# Opt-in gate for PUT /memories/config. Leave unset in production — the +# route returns 410 Gone unless this is true. See docs/consuming-core.md. +# CORE_RUNTIME_CONFIG_MUTATION_ENABLED=false + # --- Railway --- # On Railway, DATABASE_URL is injected by the Postgres plugin. # Set OPENAI_API_KEY in Railway service variables. diff --git a/.env.test.example b/.env.test.example index dc275fd..165b99d 100644 --- a/.env.test.example +++ b/.env.test.example @@ -6,3 +6,7 @@ DATABASE_URL=postgresql://supermem:supermem@localhost:5433/supermem OPENAI_API_KEY=test-placeholder EMBEDDING_DIMENSIONS=1024 PORT=3051 + +# Enable PUT /memories/config for tests/local dev. Production leaves this +# unset so the route returns 410 Gone. See docs/consuming-core.md. +CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true diff --git a/docs/api-reference.md b/docs/api-reference.md index b9fbc1f..530fd57 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -657,15 +657,14 @@ Deactivate a lesson. ### PUT /memories/config -Update runtime configuration. Changes are in-memory only (for experimentation). +Update runtime configuration at runtime. **Dev/test only** — production +deploys return `410 Gone`. Gated by the startup-validated env var +`CORE_RUNTIME_CONFIG_MUTATION_ENABLED`; see `docs/consuming-core.md`. + +**Mutable fields (Phase 7 Step 3c contract — 4 fields total):** -**Request:** ```json { - "embedding_provider": "ollama", - "embedding_model": "mxbai-embed-large", - "llm_provider": "ollama", - "llm_model": "qwen3:8b", "similarity_threshold": 0.3, "audn_candidate_threshold": 0.7, "clarification_conflict_threshold": 0.8, @@ -675,14 +674,39 @@ Update runtime configuration. Changes are in-memory only (for experimentation). All fields are optional. Only provided fields are updated. -**Response:** -```json -{ - "applied": { "llm_model": "qwen3:8b" }, - "config": { "...current config snapshot..." }, - "note": "Provider/model changes are applied in-memory for local experimentation." -} -``` +**Startup-only fields (rejected with 400):** `embedding_provider`, +`embedding_model`, `llm_provider`, `llm_model`. Set these via env vars +(`EMBEDDING_PROVIDER`, `EMBEDDING_MODEL`, `LLM_PROVIDER`, `LLM_MODEL`) +and restart the process. The embedding/LLM provider caches are fixed +at first use, so mid-flight mutation never took effect in v1. + +**Responses:** + +- `200 OK` (success, dev/test with mutable fields): + ```json + { + "applied": ["similarityThreshold", "maxSearchResults"], + "config": { "...current config snapshot..." }, + "note": "Threshold updates applied in-memory for local experimentation. Restart the process to change provider/model." + } + ``` + +- `400 Bad Request` (any startup-only field present): + ```json + { + "error": "Provider/model selection is startup-only", + "detail": "Fields embedding_provider cannot be mutated at runtime — the embedding/LLM provider caches are fixed at first use. Set the equivalent env vars (EMBEDDING_PROVIDER, EMBEDDING_MODEL, LLM_PROVIDER, LLM_MODEL) and restart the process.", + "rejected": ["embedding_provider"] + } + ``` + +- `410 Gone` (production — mutation disabled): + ```json + { + "error": "PUT /memories/config is deprecated for production", + "detail": "Set CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true to enable runtime mutation in dev/test environments. Production deploys should use startup env vars." + } + ``` --- diff --git a/docs/consuming-core.md b/docs/consuming-core.md index 28b5fe2..13ae75a 100644 --- a/docs/consuming-core.md +++ b/docs/consuming-core.md @@ -102,6 +102,56 @@ to treat core exactly as it ships. migration convenience and will be narrowed. Research should prefer the root export and raise an issue if something it needs is missing. +## Config surface: supported vs experimental + +Runtime config is split into two contracts. The split is documented in +`src/config.ts` via `SUPPORTED_RUNTIME_CONFIG_FIELDS` (39 fields) and +`INTERNAL_POLICY_CONFIG_FIELDS` (66 fields). A partition test +(`src/__tests__/config-partition.test.ts`) enforces disjointness and full +coverage — any new `RuntimeConfig` field must be tagged into one bucket. + +- **`SupportedRuntimeConfig`** — fields with a stable contract. Consumers may + rely on their semantics, defaults, and presence. Breaking changes go through + a documented deprecation cycle. This is where infrastructure (database, + port), provider/model selection (embedding, LLM, cross-encoder), and major + feature toggles (entity graph, lessons, repair loop, agentic retrieval, etc.) + live. +- **`InternalPolicyConfig`** — experimental / tuning flags. Thresholds, scoring + weights, MMR/PPR lambdas, staging internals, affinity-clustering knobs, + entropy-gate parameters, composite-grouping parameters, etc. **No stability + guarantee.** These may be renamed, re-defaulted, or removed between minor + versions. Consumers must not persist values in deployment configs expecting + them to remain meaningful. Promoted to the supported set when a field's + behavior stabilizes. + +Both types are re-exported from the root package. Docs, code review, and +release notes should reference `SUPPORTED_RUNTIME_CONFIG_FIELDS` as the +authoritative list of what's stable. + +### `PUT /memories/config` — dev/test only + +As of Phase 7 Step 3b, `PUT /memories/config` is gated by the startup-validated +flag `runtimeConfigMutationEnabled` (env: `CORE_RUNTIME_CONFIG_MUTATION_ENABLED`). + +- **Production** deploys leave the flag unset → the route returns `410 Gone`. + Production config must come from env vars at process start, not runtime HTTP + mutation. +- **Dev / test** deploys set `CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true` → + the route mutates the runtime singleton. `.env.test` has this set by + default so local test runs and CI continue to work. + +Even in dev/test, provider/model fields (`embedding_provider`, `embedding_model`, +`llm_provider`, `llm_model`) are rejected with 400 — these are startup-only +because the embedding/LLM provider caches are fixed at first use. Set them +via env vars and restart the process. Only `similarity_threshold`, +`audn_candidate_threshold`, `clarification_conflict_threshold`, and +`max_search_results` are mutable. + +Routes read the flag from a memoized startup snapshot through +`configRouteAdapter.current().runtimeConfigMutationEnabled` — they never +re-check `process.env` at request time, matching the workspace rule that +config is validated once at startup. + ## What belongs in research, not core Research harnesses, benchmarks, eval runners, experimental retrieval diff --git a/src/__tests__/config-partition.test.ts b/src/__tests__/config-partition.test.ts new file mode 100644 index 0000000..57e8cc5 --- /dev/null +++ b/src/__tests__/config-partition.test.ts @@ -0,0 +1,47 @@ +/** + * Phase 7 config-split partition test. + * + * Pins the partition of `RuntimeConfig` fields between the supported public + * contract (`SUPPORTED_RUNTIME_CONFIG_FIELDS`) and the internal/experimental + * policy surface (`INTERNAL_POLICY_CONFIG_FIELDS`). The two must be disjoint + * and their union must cover every runtime field — otherwise the "supported + * vs experimental" documentation drifts silently as new fields land. + * + * This is the Step 3a fence from the post-Phase-6 follow-on plan. + */ + +import { describe, it, expect } from 'vitest'; +import { + config, + SUPPORTED_RUNTIME_CONFIG_FIELDS, + INTERNAL_POLICY_CONFIG_FIELDS, +} from '../config.js'; + +describe('runtime config partition', () => { + const supported = new Set(SUPPORTED_RUNTIME_CONFIG_FIELDS); + const internal = new Set(INTERNAL_POLICY_CONFIG_FIELDS); + const runtimeFields = new Set(Object.keys(config)); + + it('supported and internal partitions are disjoint', () => { + const overlap = [...supported].filter((field) => internal.has(field)); + expect(overlap).toEqual([]); + }); + + it('union covers every RuntimeConfig field present on the singleton', () => { + const missing = [...runtimeFields].filter( + (field) => !supported.has(field) && !internal.has(field), + ); + expect(missing).toEqual([]); + }); + + it('no partition field references a non-existent RuntimeConfig key', () => { + const strays = [...supported, ...internal].filter( + (field) => !runtimeFields.has(field), + ); + expect(strays).toEqual([]); + }); + + it('exposes a stable count for review-time sanity', () => { + expect(supported.size + internal.size).toBe(runtimeFields.size); + }); +}); diff --git a/src/__tests__/memory-route-config-seam.test.ts b/src/__tests__/memory-route-config-seam.test.ts index 40db6c1..59ed393 100644 --- a/src/__tests__/memory-route-config-seam.test.ts +++ b/src/__tests__/memory-route-config-seam.test.ts @@ -26,6 +26,7 @@ interface MutableRouteConfig { crossEncoderEnabled: boolean; agenticRetrievalEnabled: boolean; repairLoopEnabled: boolean; + runtimeConfigMutationEnabled: boolean; } describe('memory route config seam', () => { @@ -48,6 +49,7 @@ describe('memory route config seam', () => { crossEncoderEnabled: true, agenticRetrievalEnabled: false, repairLoopEnabled: true, + runtimeConfigMutationEnabled: true, }; search.mockResolvedValue({ @@ -137,6 +139,36 @@ describe('memory route config seam', () => { expect(updatedHealthBody.config.max_search_results).toBe(7); }); + it('PUT /memories/config returns 400 when provider/model fields are included (startup-only)', async () => { + const res = await fetch(`${booted.baseUrl}/memories/config`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ embedding_provider: 'openai', max_search_results: 5 }), + }); + expect(res.status).toBe(400); + const body = await res.json(); + expect(body.error).toMatch(/startup-only/i); + expect(body.rejected).toContain('embedding_provider'); + }); + + it('PUT /memories/config returns 410 when runtimeConfigMutationEnabled is false', async () => { + const originalFlag = routeConfig.runtimeConfigMutationEnabled; + routeConfig.runtimeConfigMutationEnabled = false; + try { + const res = await fetch(`${booted.baseUrl}/memories/config`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ max_search_results: 99 }), + }); + expect(res.status).toBe(410); + const body = await res.json(); + expect(body.error).toMatch(/deprecated/i); + expect(body.detail).toMatch(/CORE_RUNTIME_CONFIG_MUTATION_ENABLED/); + } finally { + routeConfig.runtimeConfigMutationEnabled = originalFlag; + } + }); + it('clamps search limits using the injected adapter snapshot', async () => { await fetch(`${booted.baseUrl}/memories/search`, { method: 'POST', diff --git a/src/app/runtime-container.ts b/src/app/runtime-container.ts index c314d39..1b2f0f1 100644 --- a/src/app/runtime-container.ts +++ b/src/app/runtime-container.ts @@ -128,12 +128,15 @@ export interface CoreRuntimeConfigRouteAdapter { crossEncoderEnabled: boolean; agenticRetrievalEnabled: boolean; repairLoopEnabled: boolean; + /** + * Startup-validated flag for whether PUT /memories/config should mutate + * runtime config. Production deploys leave this false; dev/test toggles + * it on via the CORE_RUNTIME_CONFIG_MUTATION_ENABLED env var. Routes + * read this snapshot — never re-check env at request time. + */ + runtimeConfigMutationEnabled: boolean; }; update: (updates: { - embeddingProvider?: import('../config.js').EmbeddingProviderName; - embeddingModel?: string; - llmProvider?: import('../config.js').LLMProviderName; - llmModel?: string; similarityThreshold?: number; audnCandidateThreshold?: number; clarificationConflictThreshold?: number; @@ -225,6 +228,7 @@ export function createCoreRuntime(deps: CoreRuntimeDeps): CoreRuntime { crossEncoderEnabled: config.crossEncoderEnabled, agenticRetrievalEnabled: config.agenticRetrievalEnabled, repairLoopEnabled: config.repairLoopEnabled, + runtimeConfigMutationEnabled: config.runtimeConfigMutationEnabled, }; }, update(updates) { diff --git a/src/config.ts b/src/config.ts index c32755b..dfef578 100644 --- a/src/config.ts +++ b/src/config.ts @@ -122,13 +122,23 @@ interface RuntimeConfig { costLogDir: string; costRunId: string; conflictAutoResolveMs: number; + /** + * Dev/test-only: when true, PUT /memories/config mutates the runtime + * singleton. Production deploys leave this unset (false) — the route + * returns 410 Gone. Startup-validated; routes read the memoized value + * through configRouteAdapter, never re-check at request time. + */ + runtimeConfigMutationEnabled: boolean; } +/** + * Fields accepted by `updateRuntimeConfig()`. Provider/model selection + * (embeddingProvider, embeddingModel, llmProvider, llmModel) is intentionally + * absent: embedding.ts and llm.ts cache stateful provider instances at first + * call, so mid-flight mutation never took effect in v1. Freezing these as + * startup-only is a bug fix. Set them via env before process start. + */ interface RuntimeConfigUpdates { - embeddingProvider?: string; - embeddingModel?: string; - llmProvider?: string; - llmModel?: string; similarityThreshold?: number; audnCandidateThreshold?: number; clarificationConflictThreshold?: number; @@ -168,27 +178,6 @@ function parseLlmProvider(value: string | undefined, fallback: LLMProviderName): return value as LLMProviderName; } -function requireSupportedProvider( - provider: EmbeddingProviderName | LLMProviderName, - kind: 'embedding' | 'llm', -): void { - if (provider === 'openai') return; - if (provider === 'ollama') return; - if (provider === 'groq' && kind === 'llm' && config.groqApiKey) return; - if (provider === 'anthropic' && kind === 'llm' && config.anthropicApiKey) return; - if (provider === 'google-genai' && kind === 'llm' && config.googleApiKey) return; - if (provider === 'openai-compatible' && kind === 'embedding' && config.embeddingApiUrl) return; - if (provider === 'openai-compatible' && kind === 'llm' && config.llmApiUrl) return; - throw new Error(`Provider "${provider}" is not configured for ${kind}`); -} - -function requireNonEmpty(value: string, field: string): string { - const trimmed = value.trim(); - if (!trimmed) { - throw new Error(`${field} must be a non-empty string`); - } - return trimmed; -} function requireFiniteNumber(value: number, field: string): number { if (!Number.isFinite(value)) { @@ -347,31 +336,13 @@ export const config: RuntimeConfig = { costLogDir: optionalEnv('COST_LOG_DIR') ?? 'data/cost-logs', costRunId: optionalEnv('COST_RUN_ID') ?? '', conflictAutoResolveMs: parseInt(optionalEnv('CONFLICT_AUTO_RESOLVE_MS') ?? '86400000', 10), + runtimeConfigMutationEnabled: + (process.env.CORE_RUNTIME_CONFIG_MUTATION_ENABLED ?? 'false') === 'true', }; export function updateRuntimeConfig(updates: RuntimeConfigUpdates): string[] { const applied: string[] = []; - if (updates.embeddingProvider !== undefined) { - const provider = parseEmbeddingProvider(updates.embeddingProvider, config.embeddingProvider); - requireSupportedProvider(provider, 'embedding'); - config.embeddingProvider = provider; - applied.push('embeddingProvider'); - } - if (updates.embeddingModel !== undefined) { - config.embeddingModel = requireNonEmpty(updates.embeddingModel, 'embeddingModel'); - applied.push('embeddingModel'); - } - if (updates.llmProvider !== undefined) { - const provider = parseLlmProvider(updates.llmProvider, config.llmProvider); - requireSupportedProvider(provider, 'llm'); - config.llmProvider = provider; - applied.push('llmProvider'); - } - if (updates.llmModel !== undefined) { - config.llmModel = requireNonEmpty(updates.llmModel, 'llmModel'); - applied.push('llmModel'); - } if (updates.similarityThreshold !== undefined) { config.similarityThreshold = requireFiniteNumber(updates.similarityThreshold, 'similarityThreshold'); applied.push('similarityThreshold'); @@ -394,3 +365,101 @@ export function updateRuntimeConfig(updates: RuntimeConfigUpdates): string[] { return applied; } + +/** + * Public/supported operator config surface. Fields listed here are part of + * v2's stable contract: consumers can rely on their semantics and presence, + * and changes go through a documented deprecation cycle. + * + * This is a documentation type — it does not constrain threading. The runtime + * still carries a single `RuntimeConfig` object; this array tags the public + * subset so docs, tests, and future config-split work have a single source of + * truth. See also: `docs/consuming-core.md`. + */ +export const SUPPORTED_RUNTIME_CONFIG_FIELDS = [ + // Infrastructure + 'databaseUrl', 'openaiApiKey', 'port', + // Provider / model selection (startup config) + 'embeddingProvider', 'embeddingModel', 'embeddingDimensions', + 'embeddingApiUrl', 'embeddingApiKey', + 'llmProvider', 'llmModel', 'llmApiUrl', 'llmApiKey', + 'groqApiKey', 'anthropicApiKey', 'googleApiKey', + 'ollamaBaseUrl', 'vectorBackend', 'skipVectorIndexes', 'llmSeed', + 'crossEncoderModel', 'crossEncoderDtype', + // Operator-visible runtime + 'maxSearchResults', 'retrievalProfile', 'retrievalProfileSettings', + // Major feature toggles (surfaced in GET /memories/health) + 'entityGraphEnabled', 'lessonsEnabled', 'agenticRetrievalEnabled', + 'iterativeRetrievalEnabled', 'hybridSearchEnabled', 'repairLoopEnabled', + 'crossEncoderEnabled', 'auditLoggingEnabled', 'adaptiveRetrievalEnabled', + 'consensusValidationEnabled', 'namespaceClassificationEnabled', + // Cost / cache ops + 'extractionCacheDir', 'costLogDir', 'costRunId', 'costLoggingEnabled', + // Dev/test-only mutation gate for PUT /memories/config (see docs/consuming-core.md) + 'runtimeConfigMutationEnabled', +] as const; + +/** + * Internal policy config — experimental / tuning flags. Fields here may + * change semantics, defaults, or be removed without notice. Consumers should + * NOT rely on these in production. Promoted into the supported contract when + * a field's behavior stabilizes. + */ +export const INTERNAL_POLICY_CONFIG_FIELDS = [ + // Retrieval thresholds + 'similarityThreshold', 'audnCandidateThreshold', 'audnSafeReuseMinSimilarity', + 'crossAgentCandidateThreshold', 'clarificationConflictThreshold', + // Repair loop tuning + 'repairLoopMinSimilarity', 'repairSkipSimilarity', + 'repairDeltaThreshold', 'repairConfidenceFloor', + // MMR + 'mmrEnabled', 'mmrLambda', + // Link expansion + 'linkExpansionEnabled', 'linkExpansionMax', + 'linkSimilarityThreshold', 'linkExpansionBeforeMMR', + // Scoring weights + 'scoringWeightSimilarity', 'scoringWeightImportance', 'scoringWeightRecency', + // PPR + 'pprEnabled', 'pprDamping', + // Staging / tracing + 'stagedLoadingEnabled', 'retrievalTraceEnabled', + // Extraction internals + 'extractionCacheEnabled', 'embeddingCacheEnabled', + 'chunkedExtractionEnabled', 'chunkSizeTurns', 'chunkOverlapTurns', + 'consensusExtractionEnabled', 'consensusExtractionRuns', + 'entropyGateEnabled', 'entropyGateThreshold', 'entropyGateAlpha', + // Affinity clustering + 'affinityClusteringThreshold', 'affinityClusteringMinSize', + 'affinityClusteringBeta', 'affinityClusteringTemporalLambda', + // Trust + 'trustScoringEnabled', 'trustScoreMinThreshold', 'trustPenaltyEnabled', + // Decay / caps + 'decayCycleEnabled', 'decayRetentionThreshold', 'decayMinAgeDays', + 'memoryCapEnabled', 'memoryCapMax', 'memoryCapWarnRatio', + // Entity tuning + 'entityResolutionThreshold', 'entitySearchMinSimilarity', + // Lesson tuning + 'lessonSimilarityThreshold', + // Consensus tuning + 'consensusMinMemories', + // Query expansion / augmentation + 'queryExpansionEnabled', 'queryExpansionMinSimilarity', + 'queryAugmentationEnabled', 'queryAugmentationMaxEntities', + 'queryAugmentationMinSimilarity', + // Rerank tuning + 'rerankSkipTopSimilarity', 'rerankSkipMinGap', + // Fast AUDN + 'fastAudnEnabled', 'fastAudnDuplicateThreshold', + // Observation / deferred + 'observationNetworkEnabled', 'deferredAudnEnabled', 'deferredAudnBatchSize', + // Composite grouping + 'compositeGroupingEnabled', 'compositeMinClusterSize', + 'compositeMaxClusterSize', 'compositeSimilarityThreshold', + // Conflict handling + 'conflictAutoResolveMs', +] as const; + +export type SupportedRuntimeConfigField = typeof SUPPORTED_RUNTIME_CONFIG_FIELDS[number]; +export type InternalPolicyConfigField = typeof INTERNAL_POLICY_CONFIG_FIELDS[number]; +export type SupportedRuntimeConfig = Pick; +export type InternalPolicyConfig = Pick; diff --git a/src/index.ts b/src/index.ts index e1df81c..a7d80fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,19 @@ export { MemoryService, type IngestResult, type RetrievalResult } from './servic export { MemoryRepository, type MemoryRow, type SearchResult, type EpisodeRow, type MemoryMetadata } from './db/memory-repository.js'; export { ClaimRepository } from './db/claim-repository.js'; export { pool } from './db/pool.js'; -export { config, updateRuntimeConfig, type EmbeddingProviderName, type LLMProviderName, type VectorBackendName } from './config.js'; +export { + config, + updateRuntimeConfig, + SUPPORTED_RUNTIME_CONFIG_FIELDS, + INTERNAL_POLICY_CONFIG_FIELDS, + type EmbeddingProviderName, + type LLMProviderName, + type VectorBackendName, + type SupportedRuntimeConfigField, + type InternalPolicyConfigField, + type SupportedRuntimeConfig, + type InternalPolicyConfig, +} from './config.js'; export { createMemoryRouter } from './routes/memories.js'; export { type RetrievalCitation } from './services/retrieval-format.js'; export { diff --git a/src/routes/memories.ts b/src/routes/memories.ts index 3d3965f..b76ad8e 100644 --- a/src/routes/memories.ts +++ b/src/routes/memories.ts @@ -39,19 +39,18 @@ interface RuntimeConfigRouteSnapshot { crossEncoderEnabled: boolean; agenticRetrievalEnabled: boolean; repairLoopEnabled: boolean; + runtimeConfigMutationEnabled: boolean; } interface RuntimeConfigRouteUpdates { - embeddingProvider?: EmbeddingProviderName; - embeddingModel?: string; - llmProvider?: LLMProviderName; - llmModel?: string; similarityThreshold?: number; audnCandidateThreshold?: number; clarificationConflictThreshold?: number; maxSearchResults?: number; } +const STARTUP_ONLY_CONFIG_FIELDS = ['embedding_provider', 'embedding_model', 'llm_provider', 'llm_model'] as const; + const defaultRuntimeConfigRouteAdapter: RuntimeConfigRouteAdapter = { current() { return readRuntimeConfigRouteSnapshot(); @@ -266,11 +265,23 @@ function registerHealthRoute(router: Router, configRouteAdapter: RuntimeConfigRo function registerConfigRoute(router: Router, configRouteAdapter: RuntimeConfigRouteAdapter): void { router.put('/config', async (req: Request, res: Response) => { try { + if (!configRouteAdapter.current().runtimeConfigMutationEnabled) { + res.status(410).json({ + error: 'PUT /memories/config is deprecated for production', + detail: 'Set CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true to enable runtime mutation in dev/test environments. Production deploys should use startup env vars.', + }); + return; + } + const rejected = STARTUP_ONLY_CONFIG_FIELDS.filter((field) => req.body[field] !== undefined); + if (rejected.length > 0) { + res.status(400).json({ + error: 'Provider/model selection is startup-only', + detail: `Fields ${rejected.join(', ')} cannot be mutated at runtime — the embedding/LLM provider caches are fixed at first use. Set the equivalent env vars (EMBEDDING_PROVIDER, EMBEDDING_MODEL, LLM_PROVIDER, LLM_MODEL) and restart the process.`, + rejected, + }); + return; + } const applied = configRouteAdapter.update({ - embeddingProvider: req.body.embedding_provider, - embeddingModel: req.body.embedding_model, - llmProvider: req.body.llm_provider, - llmModel: req.body.llm_model, similarityThreshold: req.body.similarity_threshold, audnCandidateThreshold: req.body.audn_candidate_threshold, clarificationConflictThreshold: req.body.clarification_conflict_threshold, @@ -279,7 +290,7 @@ function registerConfigRoute(router: Router, configRouteAdapter: RuntimeConfigRo res.json({ applied, config: formatHealthConfig(configRouteAdapter.current()), - note: 'Provider/model changes are applied in-memory for local experimentation.', + note: 'Threshold updates applied in-memory for local experimentation. Provider/model selection is startup-only — restart the process to change it.', }); } catch (err) { handleRouteError(res, 'PUT /memories/config', err); @@ -686,6 +697,7 @@ function readRuntimeConfigRouteSnapshot(): RuntimeConfigRouteSnapshot { crossEncoderEnabled: config.crossEncoderEnabled, agenticRetrievalEnabled: config.agenticRetrievalEnabled, repairLoopEnabled: config.repairLoopEnabled, + runtimeConfigMutationEnabled: config.runtimeConfigMutationEnabled, }; }