fix(mental-models): last_refreshed_at reflects refresh time, not the source watermark - #3532
fix(mental-models): last_refreshed_at reflects refresh time, not the source watermark#3532benfrank241 wants to merge 1 commit into
Conversation
…source watermark mental_models.last_refreshed_at served two purposes: the wall-clock time of the last refresh AND the source-data watermark (newest in-scope memory) that staleness keys off. When a model's source memories are static, the column never advanced even though refreshes kept rewriting content, so any time-based scheduler reading last_refreshed_at to decide "already refreshed?" re-refreshed the model indefinitely. Split the two meanings: add last_refreshed_source_watermark as the dedicated watermark that staleness/"due for refresh" keys off, and revert last_refreshed_at to a true wall-clock timestamp that advances on every content-writing refresh. The new column is backfilled from the current last_refreshed_at (which today holds the watermark), so staleness is unchanged across the migration; consumers COALESCE to last_refreshed_at for rows not yet stamped. - update_mental_model: content path always stamps last_refreshed_at = NOW() and writes the watermark to the new column; the no-op path advances only the watermark. - compute_mental_model_is_stale, _may_need_refresh, _mental_model_processed_watermark and delta created_after key off COALESCE(source_watermark, last_refreshed_at), preserving the "don't re-refresh when there's no new data" behaviour. - Both fields exposed in MentalModelResponse; is_stale guidance points list consumers at the watermark. Tests: last_refreshed_at advances on a content refresh with an unchanged watermark; staleness still keys off the watermark (no never-refresh regression). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Diagnosis here is right, and the column split is the right shape. I've opened #3538 with the same approach plus the consumer/CI work this needs, so closing this one out — the details, in case they're useful: The Oracle name trap. The no-op path. Control plane freshness. Generated files. Adding a field to Thanks for chasing this down — the root cause read was the hard part. |
Problem
mental_models.last_refreshed_atserved two purposes at once: the wall-clock time of the last refresh and the source-data watermark (the newest in-scope memory) that staleness keys off. When a model's source memories are static, the column never advances even though refreshes keep rewriting content — so any time-based scheduler that readslast_refreshed_atto decide "have I already refreshed this?" concludes it never happened and re-refreshes the model indefinitely.This is a general correctness/fairness issue: under a workload where some models have static sources, those models get refreshed on every scheduler tick.
Fix
Split the two meanings:
last_refreshed_source_watermark(new nullable column) as the dedicated source-data watermark that staleness / "due for refresh" keys off.last_refreshed_atto a true wall-clock timestamp that advances on every content-writing refresh.last_refreshed_at(which today holds the watermark), so staleness behaviour is unchanged across the migration. ConsumersCOALESCE(last_refreshed_source_watermark, last_refreshed_at)for rows not yet stamped.Changes
update_mental_model: content path always stampslast_refreshed_at = NOW()and writes the watermark to the new column; the no-op (no new facts) path advances only the watermark.compute_mental_model_is_stale,_may_need_refresh,_mental_model_processed_watermark, and deltacreated_afterkey offCOALESCE(source_watermark, last_refreshed_at)— preserving "don't re-refresh when there's no new data".MentalModelResponse;is_staledocs point list consumers at the watermark.Tests
last_refreshed_atadvances on a content-writing refresh whose source watermark is unchanged (the reported bug).last_refreshed_atdoes not mask a genuinely new memory (guards the inverse never-refresh regression).🤖 Generated with Claude Code