Repository navigation
feat(durable): Postgres storage at one round trip per commit, reads from memory - #4
Merged
rgarcia merged 5 commits intoOct 18, 2026
Conversation
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.
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
The Postgres storage adapter mapped each
Storagecall to one statement and re-read state between every phase. Against a database ~9 ms away that put one plain model turn at 61 round trips and admitting a single input at 22. This PR makes the adapter cost what the harness's design costs: one round trip per checkpoint, and nothing for reads it already knows the answer to. Semantics are unchanged; the conformance suite, including both ownership-loss cases and lossless lone-surrogate identities, passes against a real database.Five commits, each reviewable alone:
scanEntries,entry, andfindLatestHeadMarkerwith fork ancestry; current document scans and addresses; and materialized current document content.commit_batch, aplpgsqlfunction created by schema version 2. It locks the session row, checks ownership at the sequence the caller expects, validates the batch against stored state, writes every table, and advances the session. Each rule raises the adapter's existing message with its own SQLSTATE (DU001–DU009), which the adapter maps back to its errors. Records and content travel inside the batch as JSON text and are cast on insert: Postgres accepts a lone-surrogate escape when storingjsonbut rejects it in any function that decodes string values, so nothing in the function decodes a record. Historicaldocument()reads are one statement; a request key with no submission is cached as absent.Also:
PostgresStorage.opentakesmigrate: falseto skip the per-open schema check for deployments that migrate in an explicit step.Statement counts,
log_statement = allon a faux-model run:open(withmigrate: false)submitone inputA plain turn is four checkpoints and one document scan. The floor for this design is four; going lower means the harness committing fewer checkpoints per turn.
plpgsqlis built into every Postgres and needs only the privilegesCREATE TABLEalready needs, so this stays a generic adapter: no extensions, no driver assumptions beyondPostgresQueryable, and the function is created, versioned, and tested with the schema.For comparison, the official SQLite adapter and every third-party Postgres adapter I could find (
j-koester/pi-durable-postgres,jonpulsifer/infra,schani/pi-orb,dispatchlabs-ai/dispatch,Jason-vh/kyle) share the original one-statement-per-record shape with no caching.Testing
test/postgres-storage.test.tswithPI_DURABLE_POSTGRES_URL: 54 passed.packages/durablesuite: 43 files, 908 passed.tsc --noEmit -p tsconfig.build.jsonandbiome check --error-on-warningsclean.