Astrolabe / ajxpublic /korvus
Repository

Storage Domain Model and Store Interfaces

reference scope internal/store Verification failed
  • internal/store/store.go
  • IncidentStore
  • SessionStore
  • ReportStore
  • ArtifactStore
  • DocsStore
  • IncidentFilter
  • Incident
  • Alert
  • SessionTurn
  • Report
  • Artifact
  • DocChunk
  • SessionContextRef
Relevant source files · 10

internal/store is the seam between Korvus's domain model and PostgreSQL. The package documentation is explicit about its role: PostgreSQL is the source of truth for all incident, session and report state, the concrete implementation lives behind the Store interface, and that implementation is injected at wiring time package documentation. store.go itself contains no SQL at all — only domain types, string enums, sentinels and interfaces — while the pgx implementation lives alongside it in internal/store/postgres.go, split across the incident, session and report files.

Two design rules run through the whole file and are worth understanding before reading any individual type.

The first is that the model is session-centric. An incident is a long-lived context space — comparable to a project rather than to a single agent run — that holds N sessions; a session is one self-contained multi-turn chat that ends by producing a report artifact; and a follow-up session inherits an explicitly enumerated set of artifacts, always from the same incident package documentation. Everything else follows from that shape: reports are per-iteration, artifacts are the uniform enumerable unit of inherited context, and context references are the explicit list of what a session was given.

The second is that nullable columns are represented by pointers, and the mapping to proto3 is lossy in one direction only. A *string or *time.Time field means SQL NULL — never the zero value — and because proto3 has no nullable scalar these collapse to the empty string at the API boundary package documentation. Three fields carry meaning in their nil-ness: Alert.IncidentID is nil while an alert waits out the debounce window, Incident.Fingerprint is nil for a manually opened incident, and Artifact.SessionID is nil for an incident-level upload package documentation. The rule for the boundary is that an empty string maps back to nil, never to a pointer to "" package documentation; the transport layer implements exactly that incidentToProto.

How it works

The domain types

Incident carries the descriptive and lifecycle fields of a context space: title, description, status, severity, origin, free-form label and annotation maps, who opened it, when it started, and when it was closed. Fingerprint is a pointer because only alert-originated incidents have one, and StartedAt is a pointer because incident onset may predate the row's creation.

Alert is one raw Alertmanager alert, stored as delivered: fingerprint, status, labels, annotations, generator URL, start and end times, receipt time and a suppression flag. Its IncidentID is a pointer precisely because alerts can exist before any incident does.

A Session row — the transcript container — is not part of this page's symbol set but is the pivot of the model: it holds the incident it belongs to, a 1-based Seq unique within that incident, the kind (initial or follow_up), the status, the opening prompt, the model and template used, the workspace commit the run executed against, and the audit counters that are mirrored into the report so it stays self-contained session row.

SessionTurn is one step of that transcript. The type comment carries the reason for its shape: UNIQUE (session_id, seq) is what makes after_seq stream resumption exact, because Seq is monotonic and unique per session, so a watcher that reconnects with the highest Seq it has seen resumes with no duplicate and no missed turn, without any server-side cursor state SessionTurn. Per-tool-call detail (args, latency, result) deliberately does not live here — it is emitted as OTel spans — while the Content of a tool turn is the possibly truncated result the model saw.

Report is the persisted outcome of one chat iteration. The first run lands revision 1 and each reopen lands the next; earlier revisions are kept, never overwritten. FromSeq and ToSeq bracket the turns the iteration consumed, MarkdownKey points at the rendered document in blob storage and is empty when authoring failed, and Data is the JSON encoding of the report schema described in Incident Report Schema.

Artifact is any file a session produced or was handed — report, agent output, attachment or workspace archive. SessionID is a pointer so an incident-level attachment can exist outside any session; IncidentID never is, because artifacts are always scoped to an incident; and Revision is 0 exactly when SessionID is nil.

SessionContextRef is one entry in a session's explicitly enumerated inherited context: this session was given that artifact, in that role. Its IncidentID is denormalised for one reason only — so that the two composite foreign keys (session_id, incident_id) → sessions(id, incident_id) and (artifact_id, incident_id) → artifacts(id, incident_id) can both be declared, which makes a cross-incident reference structurally impossible rather than merely forbidden by application code SessionContextRef.

DocChunk is one embedded slice of workspace documentation, keyed by the git commit the workspace was synced from. The embedding is a pgvector column whose dimension is fixed at 1536 by migration 001, and the vector text encoding/decoding lives in internal/store/vector.go — written by hand rather than pulling in a pgvector dependency.

IncidentFilter narrows the incident list: status and origin (empty means all), a limit (zero means the implementation's default) and a cursor (empty requests the first page).

The five interfaces

The persistence contract is decomposed into five interfaces that mirror the five halves of the domain.

IncidentStore covers the incident context space and the alerts attributed to it: creation, single fetch, filtered listing, update, and a separate CloseIncident that takes the terminal status explicitly. That separate method exists because UpdateIncident is not a lifecycle transition — without the status argument, IncidentStatusCancelled would be unreachable through the API IncidentStore. The rest of the interface is the alert-attribution surface used by the ingestion path described in Alert Ingestion, Fingerprinting and Debounce: log a raw alert, list an incident's alerts, attach previously unattributed alerts to an incident, and find open incidents by fingerprint.

SessionStore covers sessions, their transcripts and their inherited context. CreateSession allocates Seq race-free and callers must not compute it themselves SessionStore — the implementation serialises allocation per incident with a transaction-scoped advisory lock, because both cheaper designs (reading MAX(seq) in Go, or computing MAX(seq)+1 in a subquery) lose rows under concurrency seq allocation. ReopenSession moves a finished session back to running and stores the user turn that reopened it atomically, failing with ErrNotFinished otherwise; ListSessionTurns returns turns with Seq greater than the given cursor, ordered by Seq.

ReportStore covers the reports a session lands, one per chat iteration: create, fetch the session's latest revision, fetch one specific revision, list every revision oldest first, and list by incident.

ArtifactStore covers the files sessions produce and inherit, with both a per-session and a per-incident listing.

DocsStore covers the embedded documentation used for retrieval. ReplaceDocChunks swaps the entire chunk set for one workspace ref, SearchDocChunks returns the nearest chunks to a query embedding, PruneDocChunks deletes every chunk not belonging to one of the given refs (which must not be empty), and HasDocChunks reports whether a ref has been indexed at all.

Store is the composition of all five, and its own documentation is a warning rather than an endorsement: consumers should depend on the narrowest embedded interface they actually need — or on their own narrower interface declared in the consuming package — rather than on Store Store.

Narrow interfaces at the consumer

In practice, nobody depends on the full Store. Each consuming package declares its own view, in line with the project's rule that an interface belongs to the package that consumes it.

The session manager declares its own Store with the session, incident, report, artifact and context-ref methods it actually calls, and no more session manager store view. The incident manager declares a five-method view that is exactly the incident half of store.Store incident manager store view, and the alert ingestor declares a six-method view covering incidents and alerts only alert ingestor store view. The docs indexer declares a four-method view of the chunk operations docs indexer store view. Because these are structural interfaces, *store.PostgresStore satisfies all of them without any adapter.

Implementation and wiring

PostgresStore is the pgx/v5 implementation, holding a *pgxpool.Pool that the caller owns; Close releases the store's handle on it PostgresStore PostgresStore::Close. NewPostgresStore deliberately does not dial, migrate or validate connectivity — pool construction and migration belong in wiring, where a failure can be reported against the startup sequence rather than the first query NewPostgresStore. A compile-time assertion pins the full contract to the implementation, so a missing method surfaces where the store is wired rather than far from the code that has to change contract assertion.

The wiring in korvus CLI and Process Wiring follows that order literally: the pool is opened, store.Migrate runs, and only then is the store constructed and handed to the session manager, the alert ingestor and the transport store construction after migration.

Error handling

The package exposes sentinels rather than transport-level errors, and callers switch on them instead of inspecting message text. Reads that match no row return ErrNotFound error sentinels. ErrNotFinished reports a reopen attempt against a session that is still pending, running, finalizing or authoring error sentinels.

The implementation adds two more, translated from PostgreSQL error codes by wrapErr: a unique violation becomes ErrConflict and a foreign-key violation becomes ErrInvalidReference wrapErr. The distinction matters — a foreign-key rejection on this schema most often means the composite (session_id, incident_id) key rejected a row whose session belongs to a different incident, which is the cross-incident isolation guarantee doing its job rather than a transient failure error translation. The domain layer then re-exports these under its own names, e.g. the incident manager's ErrNotFound simply wraps store.ErrNotFound so callers may match either incident manager errors.

Relationships

flowchart TD
  subgraph storepkg["internal/store/store.go"]
    Store["`Store`"]
    IncidentStore["`IncidentStore`"]
    SessionStore["`SessionStore`"]
    ReportStore["`ReportStore`"]
    ArtifactStore["`ArtifactStore`"]
    DocsStore["`DocsStore`"]
  end

  Store --> IncidentStore
  Store --> SessionStore
  Store --> ReportStore
  Store --> ArtifactStore
  Store --> DocsStore

  PostgresStore["`PostgresStore`"] --> Store

  Incident["`Incident`"] --> IncidentStore
  Alert["`Alert`"] --> IncidentStore
  IncidentFilter["`IncidentFilter`"] --> IncidentStore
  SessionTurn["`SessionTurn`"] --> SessionStore
  SessionContextRef["`SessionContextRef`"] --> SessionStore
  Report["`Report`"] --> ReportStore
  Artifact["`Artifact`"] --> ArtifactStore
  DocChunk["`DocChunk`"] --> DocsStore

  subgraph consumers["consumers, each with its own narrow view"]
    sessionMgr["session manager"] --> SessionStore
    incidentMgr["incident manager"] --> IncidentStore
    ingestor["alert ingestor"] --> IncidentStore
    indexer["docs indexer"] --> DocsStore
  end

  transport["`internal/transport/convert.go`"] -.->|"converts rows to proto"| Incident
  transport -.-> SessionTurn
  transport -.-> Report
  transport -.-> Artifact

Reference

Symbol Kind What it does Location
store.go file Domain types, enums, sentinels and the persistence contract; no SQL internal/store/store.go
Store interface Full persistence contract, composed of the five sub-interfaces internal/store/store.go:350
IncidentStore interface Incidents, their alerts, fingerprint lookup and closing internal/store/store.go:279
SessionStore interface Sessions, their turns, reopen and inherited context refs internal/store/store.go:300
ReportStore interface Per-iteration report revisions of a session internal/store/store.go:317
ArtifactStore interface Files produced or inherited, per session and per incident internal/store/store.go:328
DocsStore interface Embedded documentation chunks: replace, search, prune, probe internal/store/store.go:336
Incident struct The long-lived context space an investigation happens in internal/store/store.go:114
Alert struct One raw Alertmanager alert, with nullable incident attribution internal/store/store.go:134
SessionTurn struct One transcript step; monotonic per-session Seq drives stream resumption internal/store/store.go:183
Report struct The persisted outcome of one chat iteration internal/store/store.go:201
Artifact struct Any file produced or handed; incident-scoped, session-optional internal/store/store.go:221
SessionContextRef struct One explicitly inherited artifact, with its role; composite-FK carrier internal/store/store.go:246
DocChunk struct One embedded documentation slice keyed by workspace commit internal/store/store.go:257
IncidentFilter struct Status/origin filter, limit and cursor for ListIncidents internal/store/store.go:270
PostgresStore struct pgx/v5 implementation of the full contract internal/store/postgres.go:20
NewPostgresStore function Wraps an already-configured pool; no dialing or migration internal/store/postgres.go:35

Related pages: PostgreSQL Persistence, Migrations and Vector Search, Session Manager and Lifecycle, Session Persistence, Reopen and Scratch Seeding, Incident Manager: Open, List, Update, Close, Alert Ingestion, Fingerprinting and Debounce, Documentation Chunking, Indexing and Search, Watch Streams, Read APIs, Webhooks and Proto Conversion, Agent Session State, Resume and Event Broadcasting, Incident Report Schema, Generated Korvus v1 Protobuf and Connect API, korvus CLI and Process Wiring, Korvus Overview.


Generated by astrolabe from commit 0c2dca73 on branch main.