Skip to content

fix(engine): stamp memory_units.updated_at on the writes that change a memory (#3490) - #3502

Open
nicoloboschi wants to merge 1 commit into
mainfrom
fix/memory-units-updated-at
Open

fix(engine): stamp memory_units.updated_at on the writes that change a memory (#3490)#3502
nicoloboschi wants to merge 1 commit into
mainfrom
fix/memory-units-updated-at

Conversation

@nicoloboschi

Copy link
Copy Markdown
Collaborator

Fixes #3490.

memory_units.updated_at reads as "when this memory last changed", and consumers chase it (WHERE updated_at > watermark) for incremental export, cache invalidation and the mental-model staleness check. Several write paths never touched it, so the chase silently skipped their changes and reported itself finished.

What now stamps the column

where what it changes
memory_engine.update_document SET tags — a document's tags propagated to its memories
memories/pg/writes.set_memory_embedding the stored vector (curation edit / revert re-embed)
memories/pg/writes.set_invalidation_reason the archived row's reason
transfer/importer._restore_fact_lifecycle created_at
transfer/importer._import_observations event_date
transfer/importer._link_observation_sources proof_count, source_memory_ids

What deliberately does not

The un-consolidate paths and mark_consolidatedconsolidated_at / consolidation_failed_at. That exemption already existed and was documented in mark_consolidated; this PR keeps it and makes it explicit at the requeue sites that clear the markers inline, because it is load-bearing rather than an oversight:

  • those columns are scheduler state, not the memory — nothing about the fact changed;
  • any_memory_updated_since (the mental-model staleness check) and the delta-refresh watermark both read updated_at. Stamping bookkeeping would make every consolidation pass look like an edit to every fact it folded, re-flagging mental models stale and re-feeding unchanged facts to a delta refresh — an LLM-billed refresh for no content change.

That is also why this went with option 2 from the issue (fix the call sites) rather than a BEFORE UPDATE trigger: a trigger cannot express the exemption, and the two readings of the column — "row last written" vs "memory last changed" — genuinely conflict here. To stop "the next one added is a new gap" being the whole cost of that choice, the contract is now written down on META_UPDATED_AT in the memories interface (so a store extension that owns memories has the same rule to keep), with mark_consolidated pointing at it as the single exception.

Not addressed, and unaddressable with a timestamp: hard deletes. A consumer that must catch those still needs a content fingerprint.

Tests

tests/test_memory_units_updated_at.py pins both halves — the paths that must stamp, and the bookkeeping ones that must not. The three "must stamp" cases fail on main and pass here.

Also ran: test_observation_invalidation, test_document_tracking, test_document_transfer, test_memory_curation, test_admin_bank_transfer, test_admin_backup_restore, test_consolidation*, test_mental_models, test_mental_model_delta, test_memories_extension — all green.

…a memory (#3490)

`updated_at` reads as "when this memory last changed" and consumers chase it
(`WHERE updated_at > watermark`) for incremental export, cache invalidation and
the mental-model staleness check. Several write paths never touched it, so the
chase silently skipped their changes and reported itself finished: the document
tag propagation, `set_memory_embedding`, `set_invalidation_reason` and the
transfer importer's event_date / proof_count / source_memory_ids / created_at
fixups. Those statements now stamp the column.

Consolidation bookkeeping stays exempt, deliberately. `consolidated_at` and
`consolidation_failed_at` are scheduler state, not the memory: stamping them
would make every consolidation pass look like an edit to every fact it folded,
re-flagging mental models stale and re-feeding unchanged facts to a delta
refresh for no content change. `mark_consolidated` already documented that
choice; the requeue sites that clear the markers inline now say so too.

The contract is written down on META_UPDATED_AT in the memories interface, so a
store that owns memories itself has the same rule to keep — and so the next
write path added has something to check itself against.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory_units.updated_at is not maintained by ~15 UPDATE paths

1 participant