symfonic.memory.backends.postgres_conversation_generation¶
postgres_conversation_generation ¶
Durable erase generations: what a turn read, proved still current at write time.
An erase intent fences reads while an admin forget runs. It cannot fence a turn that read before the intent committed: that turn holds erased transcript or memory in its context and may stage, publish or append it after the forget returned a completed receipt (CORE-199 P3-4).
Every admin forget advances a generation row for its scope in its transcript erase, the transaction that also retires the intent; until then the standing intent refuses every check (shared pools have no intent and erase in one transaction). A turn captures the generations covering its conversation before it reads anything, and every memory mutation and the final transcript append re-read them under the cursor-table lock the advance conflicts with. A changed or new covering row means the turn's reads predate an erase, so the write refuses. Rows are never deleted, so a stale turn stays stale after the intent clears or the process restarts.
Rows hold public scope paths compared by complete level, as intents do. A
read names the path's own complete-level ancestors -- a finite list derived
from the path, at most :data:MAX_DEPTH long -- and looks each up by primary
key, so it does not grow with the table, which only ever grows.
advance
async
¶
Invalidate every turn under scope; the caller holds the cursor lock.
A shared-pool forget takes the tenant lock first, as a mutation does, so the two orders agree and cannot deadlock.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
capture
async
¶
The generations covering path now; take before the turn reads.
A stale snapshot can only return an older token, which refuses later.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
covering_paths ¶
path and each complete-level ancestor, root first, all canonical.
Exactly the stored paths whose scope covers path: an advance stores
scope.path, and a scope covers path only as one of its ancestors
of the same identity version. A noncanonical or overdeep path is refused.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
observe
async
¶
:func:capture, except a tolerant authority without the table reads empty.
Memory's own pool is tolerant: a database no conversation log was installed in has had no joint forget, and a table created later starts empty, so an empty token stays exact.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
require
async
¶
require(pool: Any, path: str, token: tuple[tuple[str, int], ...], *, tolerant: bool = False) -> None
Prove token current in a transaction that conflicts with an advance.
A memory mutation calls this while holding the tenant mutation lock, which the memory erase also takes. Either this check precedes the advance, and the erase then waits for the mutation and removes it, or it sees the advance and refuses.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
require_accounted
async
¶
require_accounted(pool: Any, path: str, accounted: frozenset[tuple[str, int]], *, tolerant: bool, admitted: bool) -> None
Refuse a mutation of path unless accounted names every covering row.
The mutation's own scope, not the turn's: a mutation that presents no
admission accounts for nothing, so it may not write where any completed
joint forget reached. Ordered against an advance exactly as :func:require.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
require_installed
async
¶
Refuse before any erase step when the generation table is absent or shadowed.
Source code in src/symfonic/memory/backends/postgres_conversation_generation.py
require_on
async
¶
Refuse unless token is current; the caller holds the cursor lock.