Skip to main content
PostgreSQL stores Formation’s canonical conversation graph, memory revisions, and execution history. Each graph belongs to one agent’s perspective. Other agents observing the same external message create their own observations.

Writes and deduplication

A perspective-bound graph store resolves the connector, conversation, external identity, and actor; inserts the message event; and records participation. The transactional store commits those related writes together. Foreign-perspective identifiers cannot be used to insert a message into another agent’s graph. Message bodies are immutable observations. Identity and conversation rows can be upserted as their metadata changes. A duplicate (conversation_id, external_message_id) returns the existing message without replacing its body or moving its participation timestamp forward. This is a logical append-only conversation contract, not a claim that every database row is immutable. Configuration, leases, queue state, retention, and current-state projections have their own update and cleanup rules.

Intake, execution, and delivery

Slack, Telegram, and phone connectors persist accepted intake before acknowledging it. Graph capture then precedes model execution. Durable intake describes the queue and the connectors that still use an in-process handoff. Runs record model calls, tool outcomes, budget usage, and ownership. Their checkpoints support restart recovery without repeating completed rounds. Unknown external effects are not automatically retried. The actual outbound message ID is learned from the connector send, so the canonical outbound event is recorded afterward. A durable reply boundary precedes the send. A crash in this interval can leave delivery uncertain; persistence does not make a remote send exactly once.

Context reads

Every graph and memory read is scoped to the owning agent. Audience and lineage checks further restrict what can enter a turn. Search indexes are derived data; they cannot grant access to a row or replace canonical text. See the data model, context resolution, and memory privacy.

Authoring a migration

Create a forward migration with a unique UTC timestamp prefix:
The migration runner applies every version not yet recorded in its ledger, in numeric order within that pending batch. A pending version below the largest applied version is still applied. Applied versions are checked against their recorded checksums; do not edit or renumber a migration already deployed. Timestamp prefixes reduce conflicts between independent branches. They are not a dependency solver. Put dependent changes in the same file/PR and ensure their required order explicitly. Legacy numbered migrations remain valid ledger keys; new migrations must use timestamps. Run the embedded migration runner, which is also used at service boot:
Do not replace it with a raw loop over SQL files: that bypasses the migration ledger, checksums, and validation.

Query generation and validation

internal/store is generated from db/queries and migrations. Edit the SQL, then regenerate rather than modifying generated Go:
For a disposable local PostgreSQL database:
Database integration tests are skipped when their database variable is absent. CI supplies PostgreSQL and runs the migration runner before integration tests. Formation uses pgx’s simple-query protocol for compatibility with transaction-pooling proxies. For JSONB parameters, use an explicit SQL cast and a string value; passing Go []byte can encode a bytea literal instead of JSON.