bundle

package
v2.3.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 2, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package bundle provides the Stores struct that bundles all store dependencies. This avoids circular imports between app and mcp packages.

SQLite handoff executor: exactly-once materialization of canonical handoffs on the local backend (REM-HANDOFF-001, REM-HANDOFF-002, design RD3/RD4).

Package bundle: p95 save-latency envelope registry (W2.2, REQ-TX-002).

This file declares the REGISTERED p95 save-latency envelope for the bounded SQLiteUnitOfWork save path. It is the quantitative contract referenced by REQ-TX-002's defect-pin scenario:

"A saturation test floods the writer → No save blocks beyond the bounded
 duration, p95 save latency stays within the registered envelope, and the
 suite pins that the prior unbounded behavior is gone."

The envelope is a MEASURED, DEFENSIBLE bound — not a guess. It is asserted by TestP95SaveLatencyEnvelope_Registered (architecture test). The saturation test TestSaturationFlood_ResolvesBounded REPORTS the measured p95 against this envelope as an informational diagnostic, while its bounded-timeout and all-resolved gates enforce correctness (bounded resolution, no failures). See latency_test.go.

Index

Constants

View Source
const DefaultP95SaveLatencyEnvelope = 500 * time.Millisecond

DefaultP95SaveLatencyEnvelope is the registered upper bound on p95 per-save latency for a SQLiteUnitOfWork save under the standard saturation-flood scenario (50 concurrent writers, production-like DSN with busy_timeout=5s, WAL journal mode, single-writer connection pool, DefaultBusyRetryConfig).

MEASUREMENT RATIONALE (recorded 2026-07-26 on the dev machine):

Measured p95 under 50-writer saturation flood (3 runs):
  118.086ms, 121.152ms, 122.961ms  →  median ≈ 121ms

The envelope is set to 500ms — a ~4.1× multiple of the measured p95.
This provides comfortable headroom for slower CI runners (which can be
2–3× slower than a dev workstation) while still bounding unbounded
blocking: a regression that reintroduces unbounded blocking would inflate
p95 to the driver busy_timeout (5s) or beyond, far exceeding this envelope.

The envelope is CALIBRATED against saturationFloodN=50. It is intentionally
generous (not a tight latency SLO) because its purpose is to catch
UNBOUNDED blocking regressions, not to enforce production-grade latency.
A value too tight would flake on CI; a value too loose (e.g. >5s) would
not meaningfully bound the defect. 500ms balances both concerns.

REGISTRATION CONTRACT:

  • Positive and finite (asserted by TestP95SaveLatencyEnvelope_Registered).
  • p95 reported as a diagnostic by TestSaturationFlood_ResolvesBounded (timeout/all-resolved gates enforce correctness).
  • Profiled by BenchmarkSaturationFlood_SaveLatency (supplementary).

Variables

This section is empty.

Functions

func IsSQLiteBusy

func IsSQLiteBusy(err error) bool

IsSQLiteBusy reports whether err represents a SQLITE_BUSY / SQLITE_LOCKED condition. This is the stable, retryable signal callers check after UnitOfWork.Do returns an error (REQ-TX-002 edge scenario).

Detection uses two paths:

  1. PRIMARY: typed detection via errors.As against *sqlite.Error. If the error carries a typed code, the primary code (lower 8 bits, handling extended result codes) is compared against SQLITE_BUSY (5) and SQLITE_LOCKED (6). This is robust against driver message-format changes.
  2. FALLBACK: case-insensitive substring matching against busyErrorSubstrings, for errors that are not typed *sqlite.Error.

func SaveWithEmbedIntent

func SaveWithEmbedIntent(ctx context.Context, stores *Stores, obs *domain.Observation) error

SaveWithEmbedIntent saves an observation. When the outbox and UnitOfWork are wired (non-nil), it also enqueues an embed+upsert intent in the SAME transaction as the observation write — the intent commits atomically with the observation, so the embedding worker can process it asynchronously with full durability (REQ-EMB-002 transactional outbox).

When the outbox or UnitOfWork is nil (zero-embedding mode, or vector search unavailable), it performs a standalone Save with NO outbox activity — the zero-embedding local path is byte-for-byte unchanged (REQ-EMB-001 non-goal).

Saturation: the worker is the single authoritative source of the saturation threshold (WorkerConfig.MaxBacklog, consulted via Worker.IsSaturated). When the worker reports saturation, the save fails-closed — the transaction never begins and the caller sees the error. No embedding work is silently accepted under overload (REQ-EMB-001). When the worker is absent (Worker == nil), no saturation gate is applied: in production the worker and outbox are always paired, so this only affects test wiring and preserves zero-worker behavior.

func TxHandle

func TxHandle(ctx context.Context) any

TxHandle retrieves the shared transaction handle stashed by SQLiteUnitOfWork.Do in the context. Returns nil if no UnitOfWork transaction is active. The caller passes this handle to each participant's WithinTx.

func WireSearchFeedback

func WireSearchFeedback(stores *Stores)

WireSearchFeedback connects the search store's request-scoped feedback attribution to the observation store's persistence layer. After wiring, search.Store.RecordFeedback persists feedback via Observations.RecordSearchFeedback, attributed to the originating SearchID's query — never a shared global.

When stores, Search, or Observations is nil, this is a safe no-op (feedback stays disabled rather than falling back to any shared state). When the sink is not wired, RecordFeedback validates the SearchID but performs no persistence (REQ-RET-001: record-against-known-SearchID only).

This replaces the removed shared mutable search-query field, which raced under concurrent searches and could misattribute feedback to whichever search ran last.

Types

type SQLiteHandoffExecutor

type SQLiteHandoffExecutor struct {
	// contains filtered or unexported fields
}

SQLiteHandoffExecutor materializes a canonical handoff exactly once per (scope, key) on the SQLite backend. One UnitOfWork transaction covers the receipt claim/read, SaveWithEffect, the optional relation edge, and the committed receipt row. Every participant enlists on the SAME *sql.Tx stashed by SQLiteUnitOfWork.Do, so any failure — at any failpoint — rolls back all effects atomically: no observation, edge, or receipt row survives, and a pending receipt is never left behind.

Replay and conflict (RD3): a committed receipt whose canonical bytes (and hash) equal the incoming payload replays without re-materialization, returning the original observation ref with WriteStatusReplayed. The same (scope, key) with different canonical bytes — even under an equal SHA-256 — is a conflict that mutates nothing.

Concurrency/restart (RD4): SQLite serializes writers, so racing callers lose their snapshot at the first write; the UnitOfWork's busy retry reruns the whole handoff, the loser then observes the committed receipt and replays. An unknown commit is resolved by retrying with the same key.

func NewSQLiteHandoffExecutor

func NewSQLiteHandoffExecutor(stores *Stores) *SQLiteHandoffExecutor

NewSQLiteHandoffExecutor builds an executor over a fully wired Stores bundle: Observations, Graph, and UnitOfWork must all share the same *sql.DB.

func (*SQLiteHandoffExecutor) ExecuteHandoff

func (e *SQLiteHandoffExecutor) ExecuteHandoff(ctx context.Context, scope domain.HandoffScope, key string, canonical domain.CanonicalHandoff, hash [32]byte) (domain.ObservationWriteResult, error)

ExecuteHandoff runs the single-transaction handoff for (scope, key).

type SQLiteUnitOfWork

type SQLiteUnitOfWork struct {
	// contains filtered or unexported fields
}

SQLiteUnitOfWork implements domain.UnitOfWork for the SQLite backend. It coordinates multiple TxParticipants within a single shared *sql.Tx.

func NewSQLiteUnitOfWork

func NewSQLiteUnitOfWork(db *sql.DB, cfg domain.BusyRetryConfig) *SQLiteUnitOfWork

NewSQLiteUnitOfWork creates a UnitOfWork for the given shared *sql.DB. If cfg is the zero value, DefaultBusyRetryConfig is used.

func (*SQLiteUnitOfWork) Do

Do runs fn with all participants sharing ONE *sql.Tx. On any error (from fn or a participant), the transaction is rolled back atomically — no partial state is committed. On SQLITE_BUSY, Do retries up to MaxRetries with capped backoff before returning a stable retryable error.

The participants slice is validated before fn runs: every entry must be non-nil. A nil entry is a programming error (the caller declared a participant but passed nil). This gives the parameter meaning without changing the enlistment model: fn is still responsible for enlisting each participant via participant.WithinTx(ctx, TxHandle(ctx), work). The shared *sql.Tx ensures all participant writes commit or roll back atomically.

type Stores

type Stores struct {
	Observations      *sqlitestore.Store
	Sessions          *session.Store
	Search            *search.Store
	Prompts           *prompt.Store
	Graph             *graphstore.Store
	Scoring           *scoringstore.Store
	Vectors           domain.VectorIndex
	TemporalSnapshots *sqlitestore.TemporalSnapshotRepository
	Entities          *entitystore.Store
	Metrics           *sqlitestore.MetricsRepository
	QualityMetrics    *sqlitestore.QualityMetricsRepository
	Code              *sqlitestore.CodeStore

	// Embeddings is the optional embedding service for vector search.
	Embeddings embedding.Service

	// Outbox is the transactional embed+upsert outbox (ADR-04, W4). It is nil
	// in zero-embedding mode (Embeddings == nil) or when vector search is not
	// available. When non-nil alongside UnitOfWork, the save path enqueues embed
	// intents atomically with the observation write (REQ-EMB-002).
	Outbox *sqlitestore.OutboxStore

	// Worker is the durable embedding worker handle (ADR-04, W4.2). It is nil in
	// zero-embedding mode or when vector search is unavailable. Exposing it on
	// the bundle lets status, health checks, and future waves access the worker
	// through the composition root rather than reaching into App internals. The
	// worker's lifecycle (Start/Drain) is still owned by App; callers MUST NOT
	// call Start on a worker returned here (it is already running).
	//
	// The worker is ALSO the single source of truth for outbox saturation
	// (Worker.IsSaturated, backed by WorkerConfig.MaxBacklog). SaveWithEmbedIntent
	// consults it directly — there is no duplicated bundle-side threshold.
	Worker *embedding.Worker

	// UnitOfWork coordinates atomic cross-store saves (W2.1, REQ-TX-001).
	// It is nil until wired by the composition root (app.go); tests construct
	// it directly via NewSQLiteUnitOfWork. When non-nil, callers that need
	// multi-participant atomicity SHOULD use Do() instead of per-store Save().
	UnitOfWork domain.UnitOfWork
}

Stores bundles all store dependencies needed by MCP, HTTP, and CLI.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL