Repository navigation
Conversation
PostgresStorage stores Sessions in a Postgres schema over a caller-owned pool such as pg.Pool, without adding a runtime dependency. Each Session holds an advisory lock for its lifetime, and every commit checks the owner token written by the latest open.
Each record used to cost three round trips (id check, id claim, insert) and each document action read its record twice. A commit now checks and claims ids as sets, inserts each table's rows in one multi-row statement, reads the documents it touches once, and clears and adds revisions together, all as data-modifying CTEs of the statement that advances the session. Conversation records are cached per storage, which is the Session's only writer. Admitting one input goes from 22 queries to 6, creating a conversation from 37 to 13, and a plain model turn from 61 to 28. PostgresStorage.open takes migrate: false for deployments that migrate in an explicit step.
One statement reads the session row, the batch's claimed ids, and its documents as JSON; one data-modifying CTE statement writes the batch. Every CTE is gated on the session still carrying this owner at the sequence the read observed, and the final UPDATE advances it, so a takeover by another process writes nothing and is reported as before. A single statement is atomic, so no transaction brackets the commit.
The storage is the Session's only writer, so a record it has read stays valid until it writes that record. Tasks and submissions by id and request, each conversation's newest entries and head markers, current document scans and addresses, and the materialized current content of documents are cached and kept current by every commit. Cached values are JSON-encoded so callers get detached objects, as from the database. The delta version check joins the commit's pre-read. A plain turn is now 9 round trips, from 61.
…hema function A plpgsql function created by schema version 2 locks the session row, checks ownership at the expected sequence, validates the batch against the stored state, writes every table, and advances the session. Each rule raises the adapter's message with its own SQLSTATE, which the adapter maps back to its errors. Records and content travel inside the batch as JSON text and are cast on insert, because Postgres rejects lone-surrogate escapes in any function that decodes string values. Historical document reads are one statement, and a request key with no submission is cached.
…rom memory (#4) feat(durable): Postgres storage at one round trip per commit, reads from memory
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
PostgresStorageto@earendil-works/pi-durable/storage/postgres, a fourth storage backend beside memory, SQLite, and JSONL.Design
query,connect, andrelease/on/offon a client). Apg.Poolsatisfies it as is;pgand@types/pgare dev dependencies for tests only.SqliteStorage. Same tables, same commit logic, same error messages, Postgres SQL. Records arejson(notjsonb) so the stored text round-trips exactly. Indexed strings stay JSON-encoded, as in SQLite, because drivers send UTF-8 and would replace lone surrogates.session_idfirst. The schema defaults topi_durable, can be overridden withschema, and is created and migrated on first open under a transaction-scoped advisory lock.open()takes a session-level advisory lock on a client it keeps checked out untilclose(), and rejects if another process holds the Session.open()also writes a fresh owner token. Every commit reads the Session rowFOR UPDATEand rejects if the token changed, so a process whose lock connection silently died cannot write after another process takes over.erroron the lock client marks the storage lost; later operations reject.document()reads in aREPEATABLE READ READ ONLYtransaction so the record and its revisions come from one snapshot.close()waits for admitted operations before releasing the lock. The pool needs at least two connections.Testing
test/postgres-storage.test.tsruns the shared storage conformance suite twice (fresh storage, and reopen after every commit) plus ownership and migration tests: Session isolation, second-open rejection, lock-connection loss viapg_terminate_backend, commit fencing after takeover, ID and sequence persistence, concurrent first opens of a fresh schema, and newer-schema rejection. These are skipped unlessPI_DURABLE_POSTGRES_URLis set, so CI without Postgres is unaffected.PI_DURABLE_POSTGRES_URLset (908 passed, 1 pre-existing skip).13-recoveryand31-reload-and-restartwith their SQLite storage swapped forPostgresStorage; output matched SQLite exactly (task resumes from its checkpoint after reopen; reload and restart behave the same).npm run build, roottsc --noEmit, biome, and the pre-commit checks pass. The roottscrun also confirmspg.Poolis assignable to the pool interface.