symfonic.memory.backends.postgres_conversation_erase¶
postgres_conversation_erase ¶
Durable transcript-erase intents: the saga record of a cross-pool admin forget.
A memory store and a conversation log on two pools cannot commit together.
The admin forget therefore runs memory first and erases the transcript only
after memory completes (CORE-199 P3-4). Between those two commits -- across
every ForgetInProgress step, and across a crash after memory finished --
the transcript still exists while its memory is gone. An agent turn reading it
then would carry erased content back into new memory.
An intent closes that window. It commits before memory is touched, and while it stands no turn reads, appends to, or forks a covered conversation. The transcript erase deletes the intent in the same transaction as the transcript, so a standing intent is durable evidence that an erase is unfinished, and the only way to retire it is the forget that finishes.
Intent rows hold public scope paths, compared by complete level. Lexical SQL
narrows candidates; :meth:MemoryScope.covers decides.
Every statement here names its tables unqualified, so it means whichever
schema the connection resolved them in. Two schemas on the active
search_path holding a same-named native_conversation_* relation make
that identity ambiguous, and a statement prepared before the earlier one
existed keeps reading the later one. :func:refuse_if_shadowed refuses then,
before any fence read, rather than choosing either copy.
clear
async
¶
Delete intents scope covers, inside the transcript-erase transaction.
An ancestor's intent is not this forget's to retire: its erase is wider.
Source code in src/symfonic/memory/backends/postgres_conversation_erase.py
covers ¶
Whether stored path outer covers inner by complete level.
Source code in src/symfonic/memory/backends/postgres_conversation_erase.py
lock_fresh
async
¶
Take the cursor-table lock so every later read in the block sees past it.
The block's own transaction issues the lock as its first statement, so its
snapshot follows the lock at any isolation. A joined caller transaction may
have fixed its snapshot before an intent committed; only READ COMMITTED
re-reads per statement, so any other isolation is refused before a read.
The refusal precedes the lock: a joined transaction keeps its locks to its
end, and a caller that caught the refusal would otherwise hold every erase
intent out. SHOW takes no snapshot.
The shadow check reads, so it follows the lock in the block's own transaction, which a refusal rolls back, and precedes it in a joined one, which is READ COMMITTED by then.
Source code in src/symfonic/memory/backends/postgres_conversation_erase.py
refuse_if_pending
async
¶
Refuse when a standing intent covers any of paths.
Readers and writers call this after :func:lock_fresh, whose lock
conflicts with :func:request's, so an intent is either visible here or
commits after the caller's transaction and its erase removes what it added.
Source code in src/symfonic/memory/backends/postgres_conversation_erase.py
refuse_if_shadowed
async
¶
Refuse when a conversation relation's schema is ambiguous on conn.
Checked per call, not once per connection: a shadow created after the connection prepared its statements leaves those plans on the old copy.
Source code in src/symfonic/memory/backends/postgres_conversation_erase.py
request
async
¶
Commit an intent for scope; idempotent for a resumed forget.
Refuses first if the erase cannot finish: its transcript erase advances
the erase generation (postgres_conversation_generation).