Skip to content

feat: persist experience records in PostgreSQL - #1

Merged
fabbrik merged 1 commit into
mainfrom
feat/2-1-persist-experience-records
Sep 17, 2026
Merged

fabbrik merged 1 commit into
mainfrom
feat/2-1-persist-experience-records

Conversation

@fabbrik

@fabbrik fabbrik commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Summary

Story 2.1: the first durable Experience Record store.

  • Abstractions
    • ExperienceRecord, a canonical, immutable record.
    • IExperienceRecordStore, with create-only save, get by ID and scoped query. Results are typed: Created, Found, NotFound, Denied, Invalid and Conflict. Infrastructure failures throw ExperienceStoreException.
    • AuthorizationContext gains optional ApplicationId, ProjectId, TeamId, AgentId and UserId bounds, plus Permits(Scope). The tenant must always match, and a non-null bound must match exactly.
  • New package AgentExperience.Storage.Postgres (plain Npgsql 10.0.3, pinned exactly)
    • Checks input, then authorization, before any connection opens.
    • Scope matching runs in SQL and is exact and case-sensitive. A null optional field matches only null.
    • The record is stored as a JSONB payload with a payload_version column, so domain types carry no version field. Reading an unknown version throws.
    • The schema script Migrations/0001_create_experience_records.sql is embedded in the package and exposed through PostgresExperienceRecordSchema.
    • The package README covers the trusted host boundary, results and data semantics.

Not in this PR

  • A migration runner (DbUp, journal, advisory lock). It is deferred to a follow-up that must land before Story 2.4. Until then, hosts apply the embedded script themselves.
  • A test that cancels an operation mid-flight. It is deferred because a reliable version needs a deliberate blocking setup.

Notes

  • CreatedAt and UpdatedAt are truncated to microseconds, because timestamptz stores nothing finer. Timestamps nested inside the payload keep full precision.
  • An existing contract test asserted that AuthorizationContext has no ApplicationId or ProjectId. It now asserts the new nullable bounds.

Test plan

  • dotnet build --configuration Release: 0 warnings, 0 errors
  • dotnet test --configuration Release: 303 of 303 pass, including 40 store tests against a pgvector/pgvector:pg16 container. Locally I used TESTCONTAINERS_RYUK_DISABLED=true.
  • CI green

🤖 Generated with Claude Code

Add the ExperienceRecord contract and IExperienceRecordStore port, optional
authority bounds on AuthorizationContext, and the AgentExperience.Storage.Postgres
package: plain Npgsql store with authorization checked before database access,
exact scope predicates in SQL, adapter-owned payload versioning, and an embedded
schema script. The migration runner is deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@fabbrik
fabbrik merged commit 4477692 into main Sep 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant