feat(config): per-request config_override on memory routes#41
Merged
Conversation
Land the per-request config-override abstraction described in `atomicmemory-research/docs/core-repo/design/per-request-config-override.md`. Callers may now pass a flat `config_override` body field on all four memory routes (search, search/fast, ingest, ingest/quick) to overlay a shallow subset on top of the startup RuntimeConfig for the duration of a single request. No server-side mutation; no global state is touched. Wire contract ------------- - `config_override` is a flat object keyed one-for-one on `RuntimeConfig` field names (camelCase, matching the schema field list below). - Validation is a Zod `.strict().partial()` — unknown keys reject with a 400 and never reach the service layer; every listed key is optional. - Unknown-key typos surface immediately (e.g. snake_case spelling of a camelCase field), so silent no-ops are impossible. Observability headers (emitted only when an override is present) ---------------------------------------------------------------- - `X-Atomicmem-Config-Override-Applied: true` - `X-Atomicmem-Effective-Config-Hash: sha256:<hex>` (sorted-key canonical JSON fingerprint of the full effective config — lets callers correlate traces to a specific config snapshot). - `X-Atomicmem-Config-Override-Keys: <sorted,comma,list>` Implementation -------------- - `src/schemas/memories.ts` adds `ConfigOverrideSchema` (44 fields, grouped retrieval/ingest for readability only — the overlay merges flat). `IngestBodySchema` and `SearchBodySchema` gain `config_override: ConfigOverrideSchema.optional()`, re-emitted as `configOverride` after the camelCase transform. - `src/services/retrieval-config-overlay.ts` exports the three route-side primitives: `applyConfigOverride` (one-line shallow merge), `hashEffectiveConfig`, `summarizeOverrideKeys`. - `src/services/memory-service.ts` accepts an optional `effectiveConfig` at every public method. A private `depsFor()` helper swaps it into `MemoryServiceDeps.config` for the call's duration; absent override returns the shared `this.deps` (zero allocation). All downstream pipeline code already reads `deps.config` so no leaf-module edits are needed. - `src/routes/memories.ts` adds `applyRequestConfigOverride(res, override)` which validates-then-emits the three headers and returns the effective config (or undefined for the no-op path). - `RuntimeConfig` is exported from `src/config.ts` so the overlay and schema can reference its field shape. Coverage scope (intentionally narrower than RuntimeConfig) ---------------------------------------------------------- ConfigOverrideSchema covers exactly the fields threaded through `MemoryServiceDeps.config` (`CoreRuntimeConfig + IngestRuntimeConfig`). Fields read from the module-level `config` singleton by leaf modules — scoring weights (`db/query-helpers.ts`), nested `retrievalProfileSettings.*` (rerankDepth, lexicalWeight, the repair-weight pair) — are deliberately omitted. Accepting overrides for those would silently no-op, violating the workspace rule against degraded modes. Future landings lift those fields onto top-level `RuntimeConfig` as they become overlay-eligible. Fields accepted (44 total, sorted): adaptiveRetrievalEnabled, agenticRetrievalEnabled, auditLoggingEnabled, audnCandidateThreshold, chunkedExtractionEnabled, compositeGroupingEnabled, compositeMinClusterSize, consensusExtractionEnabled, consensusExtractionRuns, consensusMinMemories, consensusValidationEnabled, crossEncoderEnabled, entityGraphEnabled, entitySearchMinSimilarity, entropyGateAlpha, entropyGateEnabled, entropyGateThreshold, fastAudnDuplicateThreshold, fastAudnEnabled, hybridSearchEnabled, iterativeRetrievalEnabled, lessonsEnabled, linkExpansionBeforeMMR, linkExpansionEnabled, linkExpansionMax, linkSimilarityThreshold, maxSearchResults, mmrEnabled, mmrLambda, namespaceClassificationEnabled, pprDamping, pprEnabled, queryAugmentationEnabled, queryAugmentationMaxEntities, queryAugmentationMinSimilarity, queryExpansionEnabled, queryExpansionMinSimilarity, repairConfidenceFloor, repairDeltaThreshold, repairLoopEnabled, repairLoopMinSimilarity, rerankSkipMinGap, rerankSkipTopSimilarity, trustScoreMinThreshold, trustScoringEnabled. Tests ----- - `src/services/__tests__/retrieval-config-overlay.test.ts` (9 tests) — merge semantics, hash stability under key reorder, sorted key summary. - `src/schemas/__tests__/config-override.test.ts` (10 tests) — strict mode, empty-object accept, snake_case typo rejection, threading through IngestBodySchema + SearchBodySchema. - `src/__tests__/memory-route-config-override.test.ts` (7 tests) — no-override zero-header path, three-header emission on override, effectiveConfig threading into scopedSearch / ingest / quickIngest, 400 on unknown keys, empty-object treated as no-override. `npm test` → 109 files · 1067 tests pass. `npm run build` → clean. OpenAPI spec regenerated (44 new fields surfaced on the four route schemas).
…eader Relaxes the per-request config_override schema from an enumerated `.strict().partial()` list of 45 fields to a permissive `z.record(z.union([z.boolean(), z.number(), z.string(), z.null()]))`. Adds a runtime unknown-key detector in the route handler that emits `X-Atomicmem-Unknown-Override-Keys` and logs a server-side warning when any override key doesn't match a known RuntimeConfig field. **Why this change.** The enumerated-schema version coupled every new overlay-eligible RuntimeConfig field to a core release. An experimenter tuning a newly-added field would have to wait for core to land a schema update before their config override could address it. That defeats the purpose of a per-request config mechanism — configuration should be decoupled from schema releases. **New contract.** - `config_override` accepts any object whose values are primitives (boolean, number, string, null). Keys are not enumerated. - Unknown keys are carried through on the effective config merge AND surfaced on the response as `X-Atomicmem-Unknown-Override-Keys: <sorted-csv>` + a warning log. - Typos don't 400 — they no-op visibly. Experimenters see the warning header and fix the typo. - Nested objects and arrays still reject (override is flat by contract). - Field-level type validation (e.g. "mmrLambda must be 0–1") is no longer enforced by the schema — that's the consumer's job at the point where it reads the value. A value that looks wrong silently misbehaves or throws downstream rather than 400-ing at the edge. **Backward compatibility.** - Requests that previously passed (all fields matched the enumerated set) still pass. - Requests that previously 400'd on unknown keys now 200 with the warning header. - Responses without override behavior unchanged. - OpenAPI regenerated (`config_override` is now `additionalProperties` with primitive-or-null values). **Tests updated.** - `src/schemas/__tests__/config-override.test.ts`: strictness assertions replaced with permissive-shape assertions. New coverage for forward-compat (unknown keys accepted), primitive-value boundaries (objects/arrays rejected). - `src/__tests__/memory-route-config-override.test.ts`: the "unknown key → 400" test became "unknown key → 200 + warning header + log"; added new tests for "mix of known + unknown" and "all-known → no warning header". - Full suite: 1071 tests pass (net +4 from new coverage). **Design follow-up.** The schema is still the "static config override" shape; genuine plugin-style runtime-loadable experiment modules (code that participates in pipeline stages, not just knob values) remain a future architectural choice. This landing unblocks Phase-1 experiments without pre-committing to the enumeration anti-pattern on the public API. Updates PR #41 before merge / initial public release.
…SearchResults The two search routes (/search, /search/fast) called resolveSearchPreamble() before applyRequestConfigOverride(). resolveSearchPreamble() clamped body.limit against configRouteAdapter.current().maxSearchResults — i.e. the startup snapshot, computed BEFORE the per-request override merged in. A request that raised maxSearchResults in its config_override would have its effective-config-hash header advertise the new cap while the service actually received a limit clamped to the startup cap. Reorder: merge the override first, derive the effective maxSearchResults (override ?? startup), then compute the clamped request limit from that. Regression test added: override bumps max 20 → 50, request asks for 40, service must receive 40, not 20. Reported by Codex review.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a per-request
config_overridemechanism that lets callerspass a partial
RuntimeConfigoverlay on every memory API call.Enables stateless per-request A/B, benchmark experimentation, and
external-caller tuning without env-var scattering, named-profile
proliferation, or container restarts between experiments.
Motivation
Research-side benchmark experiments (the AtomicMemory v3 parity
roadmap's Phase 1) need to vary retrieval config per experiment
without editing source or creating named profiles. The prior
approach tried a named
balanced-parityprofile; that was asource edit in service of experimentation, which is the wrong
shape — config should be a parameter, not a commit. This PR moves
that config surface into the API.
Full design doc lives in the research repo at
atomicmemory-research/docs/core-repo/design/per-request-config-override.md.What's in this PR
API contract
All four memory routes that share the body schemas
(
POST /v1/memories/search,/search/fast,/ingest,/ingest/quick) gain an optionalconfig_overridebody field.{ "user_id": "alice", "query": "What did Alice say about Vite?", "limit": 5, "config_override": { "hybridSearchEnabled": true, "maxSearchResults": 12, "mmrLambda": 0.7 } }RuntimeConfigfield names one-for-one..strict().partial()— unknown keys reject with400; every field is optional.
effective = { ...startupConfig, ...override }.config_overrideabsent → zero-cost path; startup config usedas-is.
Response headers (override-only)
Requests that carry an override emit three response headers so
callers can audit what config was applied:
X-Atomicmem-Config-Override-Applied: trueX-Atomicmem-Effective-Config-Hash: sha256:<hex>— sorted-keycanonical JSON fingerprint.
X-Atomicmem-Config-Override-Keys: <sorted-csv>— flat list ofkeys present in the override.
Requests without an override emit none of the three — zero overhead
on that path.
ConfigOverride fields (45 total)
Covers every
RuntimeConfigfield already threaded throughMemoryServiceDeps. Split roughly 32 retrieval-side / 13ingest-side. Deliberately excludes:
rerankDepth,lexicalWeight,repairPrimaryWeight,repairRewriteWeight) — live underretrievalProfileSettings.*and aren't threaded throughdeps.config. Accepting them would silently no-op, violatingthe "no degraded mode" rule in
CLAUDE.md.scoringWeight{Similarity,Importance, Recency},repairSkipSimilarity) — leaf code reads thesedirectly from the module singleton, not from
deps.config.Future landings that tune any of these will first lift the field
into the threaded config path (small additive change) and
automatically gain overlay support.
Implementation
src/schemas/memories.ts— newConfigOverrideSchema;extended
IngestBodySchemaandSearchBodySchemawithconfig_override?: ConfigOverride. Because/search/fastand/ingest/quickreuse those schemas, all four routes auto-gainsupport.
src/services/retrieval-config-overlay.ts(new, ~80 LOC) —applyConfigOverride,hashEffectiveConfig,summarizeOverrideKeys.src/routes/memories.ts— each of the four route handlersparses the overlay, builds
EffectiveConfig, sets the threeheaders, passes to the service.
src/services/memory-service.ts—depsFor()helper swapsdeps.configfor the call's duration. No global mutation.memory-search.ts,memory-ingest.ts,search-pipeline.ts,ingest-fact-pipeline.ts,memory-service-types.ts— accepteffectiveConfigparameter instead of reading the singleton.Tests
schema strictness tests, route-level integration tests covering
all four routes × (with override, without override) × headers ×
service threading × 400 rejection.
Validation
End-to-end validated against the AtomicMemory R&D benchmark harness:
3-run mean matched the pre-abstraction P1-H baseline within σ
on all metrics. hit@5 0.147 ± 0.031 vs baseline 0.140;
judge 0.560 ± 0.060 vs 0.540; p50 352 vs 365ms; p95 560 vs
565ms. Confirms the abstraction is transparent to requests
without overrides.
present on all 150 QA calls (50 × 3 runs) with the expected
effective-config hash. Gate passed per the roadmap's slice-
aware rule: hypothesis slice (cat1 single-hop) +1.7pp judge,
all controls within σ.
Full summary in the research repo at
memory-research/evaluation/baselines/p1-1-enable-hybrid-2026-04-23.md.Test plan
npm test— 1067 passnpm run buildcleannpx tsc --noEmitcleannpm run generate:openapi— spec regeneratedcorrectly with and without override
baseline
controls within σ
Backward compatibility
Fully backward compatible. Existing callers unaffected:
config_overrideis optional on the body.HYBRID_SEARCH_ENABLEDetc.)still override as before.
Not in scope / deferred
atomicmem-sdkpackage gainingconfigOverride) — deferred until an external customer asks.The internal research adapter has it; that's the only consumer
today.
rerankDepth, etc.) onto thethreaded config surface — each will happen alongside its
respective Phase-1 landing that needs it.
🤖 Generated with Claude Code