Task 103 — PostgreSQL session store
Phase: 8
Status: in_progress (shipped as staged + restart-gated; live switch needs an owner decision)
Owner: —
Slug: postgres-session-store
Studio Stage: 6
Status note (2026-10-07, 2.0.0 release prep). Shipping as partial, by design —
in_progressis accurate and should not be tidied todone. The Postgres store and the restart-gated selection UI are implemented; what is deliberately not implemented is the live, no-restart switch, becauseStorage:SessionStoreis read fromIConfiguration(notAppConfig) and switching backends at runtime risks data loss. Closing the gap requires changing where the agent reads that setting, which is an owner decision rather than a ticket to grind down. Shipped as a documented limitation inCHANGELOG-EN.md/CHANGELOG-RU.md. Evidence: VERIFICATION.md § “Stage 6.12”.
Goal
Абстракция ISessionStore + Postgres implementation для централизованного хранения сессий всех агентов. Опция “collective mind mode” — shared memory между агентами.
Acceptance criteria
ISessionStore— interface (extract from SqliteSessionStore)- Methods: SaveInteraction, GetSession, ListSessions, SaveAudit, etc. (all existing methods)
SqliteSessionStore— implementsISessionStore(refactor, no behavior change)- [~]
PostgresSessionStore— implementsISessionStore(foundation: sessions + interactions + budget + audit + approvals working end-to-end; durable tasks + checkpoints + escalations + sandbox + skill-evaluations throwNotImplementedExceptionfor follow-up)- Schema: sessions, interactions, audit, budget, approvals (bootstrapped on first use)
- Schema: task_states/durable tasks, task_checkpoints, escalations, sandbox_executions, skill_evaluations (follow-up)
-
[x] Connection: StorageConfig.SessionStore= { provider: sqlitepostgres, connectionString } - Uses
Npgsql(already in deps for PostgresMeshConfig) SKIP LOCKEDfor concurrent agent writes (follow-up; current impl uses single-connection + SemaphoreSlim like the SQLite store)
StorageConfig— addSessionStoresection:"Storage": { "DataRoot": "data", "SessionStore": { "Provider": "sqlite", "ConnectionString": null } }- [~] Collective mind mode:
-
[x] StorageConfig.CollectiveMind= { enabled: false, sharedMemory: false, sessionIsolation: “per-agent”“shared” } - Shared memory: agents read/write common memory namespace (follow-up — config present, behavior TBD)
- Session isolation: per-agent (default) or shared (any agent reads any session) (follow-up)
-
Program.cs— DI: choose SqliteSessionStore or PostgresSessionStore based on config- Migration path: SQLite → Postgres (basic: export/import script, full migration tool later) — not started; PostgreSQL is greenfield for now (no existing SQLite → PG migration required)
- Unit tests: SqliteSessionStore contract smoke (6 tests, all green)
dotnet build+dotnet testpass
Sub-tasks
- Inspect
SqliteSessionStoreand enumerate all public surface - Create
ISessionStoreinterface covering all public async methods (sessions, interactions, request_stats, sandbox_executions, budget, audit, task_states, approval_requests, skill_evaluations, escalations, checkpoints) - Make
SqliteSessionStoreimplementISessionStore(mark interface, no behavior change) - Add
StorageConfig.SessionStore(Provider + ConnectionString + Schema) andStorageConfig.CollectiveMind(Enabled + SharedMemory + SessionIsolation) with backward-compatible defaults - Add
PostgresSessionStoreskeleton implementingISessionStore(Npgsql-based, schema bootstrap, sessions + interactions + budget + audit + approvals as MVP subset; remaining surface throwsNotImplementedExceptionwith explicit follow-up pointer) - Wire DI: conditional registration in
Program.csbased onStorageConfig.SessionStore.Provider(kept inline rather than a separateSessionStoreFactoryclass to keep the diff small) - Update
Program.cs: registerISessionStore; keep concreteSqliteSessionStorefor direct wiring (consumers that needSqliteConnection.Connectionkeep compiling unchanged) - Unit tests: contract surface, config defaults, Postgres constructor arg-validation (6 tests in
SessionStoreInterfaceTests) dotnet build+dotnet testpass (full suite: 2081/2081 relevant tests green; 8 pre-existing failures are unrelated — Redis/Otel/Transport/Numeric/ResilientLLM tests require external services)
Implementation notes
ISessionStorelives insrc/agent/Storage/ISessionStore.cs(same namespace asSqliteSessionStoreso existingusing Hercules.Storage;keeps working).SqliteSessionStorekeeps its full public surface; sync overloads remain (call async via.GetAwaiter().GetResult()).- The shared
Connectionproperty onSqliteSessionStore(used bySkillQualityStoreetc.) stays on the concrete class —ISessionStoredoes NOT exposeSqliteConnectionto avoid leaking the storage backend. PostgresSessionStoreis a new file insrc/agent/Storage/PostgresSessionStore.cs(~34 KB) with full schema bootstrap (idempotent) and the load-bearing methods implemented.- DI keeps the legacy
SqliteSessionStoreregistered as a concrete type so consumers (SqliteOutboxStore,SqliteTaskRepository,SqliteDistillationStore,SkillQualityStore) compile unchanged.ISessionStoreresolves to the selected backend. - The Postgres implementation deliberately starts with a subset of methods (sessions, interactions, budget, audit, approvals) implemented end-to-end; the remaining surface is marked with
throw new NotImplementedException("PostgresSessionStore: ... (task_103 follow-up).")so callers can detect the gap early. This is a foundation PR — full surface parity is the next roadmap tick (depends on the SQLite durability work in task_106/108). - Concurrency: same single-connection +
SemaphoreSlim(1,1) model as the SQLite store.SKIP LOCKEDqueues andNpgsqlDataSourcepooling are follow-up work and are not required to satisfy the contract.
Validation
dotnet build src/agent/Hercules.csproj→ 0 errorsdotnet build src/agent/Hercules.WebApi/Hercules.WebApi.csproj→ 0 errorsdotnet test --filter "FullyQualifiedName~SessionStoreInterfaceTests"→ 6/6 passeddotnet test(full suite, excluding pre-existing flaky / external-service tests) → 2081/2081 passed- Pre-existing failures (8 tests) confirmed unrelated to this change by re-running against the clean baseline (
git stash+ remove new files + re-test). They require Redis / network / OpenTelemetry exporters and are independent ofISessionStore.
Follow-ups (next roadmap tick)
- Implement the remaining
PostgresSessionStoremethods: durable tasks (SaveDurableTaskAsync/LoadDurableTaskAsync/ etc.), checkpoints, escalations, sandbox, skill-evaluations, audit filter queries (GetAuditLogByTargetAsync,GetAuditLogQueryAsync), reflection queries (GetLowConfidenceAsync,GetSessionInteractionsAsync,GetModeStatsAsync, etc.). Tracked in this task file asNotImplementedExceptionstubs. - Add
NpgsqlDataSourcepooling andSELECT … FOR UPDATE SKIP LOCKEDfor multi-agent shared-DB scenarios. - Implement collective-mind semantics: when
StorageConfig.CollectiveMind.Enabled = true+SessionIsolation = "shared", route session reads across all agents; for now the config is plumbed but the policy layer is a follow-up. - SQLite → Postgres migration tooling (export/import script). Not required for greenfield Postgres deployments.
Dependencies
- task_069 (PostgreSQL shared state backend) — done (Npgsql already available)
Scope / Likely files
src/agent/Storage/ISessionStore.cs (new), src/agent/Storage/SqliteSessionStore.cs (refactor), src/agent/Storage/PostgresSessionStore.cs (new), src/agent/Config/AppConfig.cs, src/agent/Program.cs
Links
- Studio Stage 6: ../EPIC_Hercules_Studio/tasks/stage_06_config_restart.md
- Backlog: ../backlog.md