Recency on the hot path: the scorer_on_hot_path knob¶
Status: Shipped in v7.26.1.
Closes: a production benchmark finding (2026-06-12) -- the 20% ScoringWeights.recency weight was dead on the MEMORY_CONTEXT hydration path.
Related: FrameworkConfig.scorer_on_hot_path, RetrievalEngine.retrieve, RetrievalScorer._recency_score, src/symfonic/agent/engine.py:_hydrate_impl.
The bug v7.26.1 closes¶
Pre-v7.26.1, the per-turn MEMORY_CONTEXT hydration loop dispatched each enabled layer's retrieve() directly:
for layer_type, layer in self._orchestrator.all_layers().items():
entries = await layer.retrieve(scope, query, top_k=...)
all_entries.extend(entries)
That loop never called RetrievalEngine.retrieve -- the place where RetrievalScorer.score_batch runs the four-signal composition (semantic_similarity * w1 + recency * w2 + frequency * w3 + graph_proximity * w4). The ScoringWeights knob the adopter configured on OrchestratorConfig only mattered for ad-hoc agent._orchestrator.retrieval_engine.retrieve(...) calls; it had zero effect on the per-turn prompt.
A production benchmark on 2026-06-12 surfaced the operational tell: two-week-old episodic events with high cosine similarity ranked above twelve-minute-old events with similar cosine, because no recency term modulated the ranking. The 20% recency=0.2 weight was earning nothing on the hot path.
The fix: an opt-in route to the composed engine¶
v7.26.1 adds FrameworkConfig.scorer_on_hot_path: bool = False. When True, _hydrate_impl routes the long-term layers through RetrievalEngine.retrieve:
With the knob set, every per-turn MEMORY_CONTEXT hydration:
- Embeds the query once (via
embedding_provider). - Pulls vector candidates from the vector backend (2x top_k for re-rank headroom).
- Pulls graph candidates from each enabled long-term layer (semantic, episodic, procedural, prospective).
- Runs
RetrievalScorer.score_batchover the merged candidate set -- the four-signal composition fires (recency / frequency / graph_proximity / semantic). - Returns the top_k results across all enabled layers in one ranked list.
Working memory still rides on top via the recency-prefix block (see Contract B below) -- the FIFO ordering of working_memory_recent_turns is unchanged in both modes.
Default is False on purpose¶
Default-on would change MEMORY_CONTEXT byte-ordering for every adopter at the v7.26.0 to v7.26.1 boundary. Even though the new ordering is "more correct" by spec, prompt-cache parity is the constraint -- every Anthropic + OpenAI cached prefix would rebill on the upgrade. v7.26.1 ships behind a single opt-in flag; the default-flip decision is deferred to v8.0 with a documented migration cycle.
The seven contracts¶
| Contract | Guarantee |
|---|---|
| A | When scorer_on_hot_path=False, _hydrate_impl runs the v7.26.0 per-layer dispatch loop UNCHANGED. The engine is never called. |
| B | The working-memory recency-prefix block (most-recent N working entries injected verbatim at the head of MEMORY_CONTEXT) runs UNCHANGED in both modes. |
| C | When the engine path raises (transient vector outage, embedding-provider failure), the hydrate loop falls back to per-layer dispatch FOR THAT TURN, logs a scorer_hotpath_fallback warning, and increments agent._scorer_hotpath_fallback_count. Next turn re-attempts the engine path. |
| D | Orchestrators built via HMSFactory.build / build_in_memory are automatically wired with a RetrievalEngine. Direct dataclass-constructed orchestrators without an engine fall back to per-layer dispatch with the same telemetry signal. |
| E | ScoringWeights defaults are unchanged (semantic=0.4, recency=0.2, frequency=0.2, graph=0.2). Adopters can re-tune via OrchestratorConfig.scoring_weights. |
| F | The intent_filter_mode=enforce skip (action turns omit long-term layers) is preserved -- the engine call passes layers=effective_layers derived from the intent verdict. |
| G | The engine path returns FEWER total entries than the per-layer path (single composed top_k vs per-layer top_k * len(enabled_layers)). Adopters who relied on the higher per-layer quota should bump default_top_k when opting in. |
Recency math¶
The recency signal is the exponential decay at src/symfonic/memory/graph/scoring.py:34-43:
Default half-life is 7 days (_DEFAULT_HALF_LIFE_DAYS). A twelve-minute-old node scores ~1.0; a 14-day-old node scores ~0.25. With default ScoringWeights(semantic=0.4, recency=0.2), a fresh node at cosine 0.92 beats a stale node at cosine 0.95 because the recency premium covers the cosine deficit.
Surfacing the half-life as a FrameworkConfig.recency_half_life_days knob is deferred to a follow-up release (architect note in plan §10).
Telemetry¶
When the engine path takes the fallback to per-layer dispatch, two signals fire:
- Log:
WARNING symfonic.agent.engine: scorer_hotpath_fallback for tenant=<id> -- ...withexc_infowhen an exception caused the fallback. - Counter:
agent._scorer_hotpath_fallback_countincrements by 1 per fallback.
Adopters can wire the counter into their observability stack via a periodic snapshot from a custom callback or health-check endpoint. Persistent non-zero counts indicate either a backend health issue or a misconfigured orchestrator without a wired engine.
Migration recipe¶
- Build the orchestrator via
HMSFactory.buildorHMSFactory.build_in_memory(these wire the engine automatically). - Set
FrameworkConfig(scorer_on_hot_path=True). - Verify
agent._scorer_hotpath_fallback_count == 0after several turns -- non-zero means the engine path is failing and you should diagnose before relying on the new ordering. - Optional: tune
OrchestratorConfig(scoring_weights=ScoringWeights(recency=0.3, ...))if you want more aggressive recency bias. - Optional: bump
OrchestratorConfig.default_top_kif you noticed fewer entries inMEMORY_CONTEXTpost-flip (Contract G).
Why this is patch (v7.26.1), not minor (v7.27.0)¶
- No new public class.
- New field on
FrameworkConfigis additive + default-false. - No new dependency, no wire schema change, no migration.
- The only observable change is on adopters who explicitly opt in.
The minor designation is reserved for the future default-flip release (v8.0 candidate).
Reference plan¶
.agent/team/plans/2026-06-12-v7.26-B-recency-on-hot-path.md (Shape B1 decision, contracts A-G, sequencing rationale).