New core:artifacts-store interface + infrastructure/artifacts-cas implementation: segment files + SQLite index, Blake3 hashing, group-commit fsync via flushBefore, recovery tail-scan, manual compactor, and oldest-first disk-cap evictor. Inference events now carry promptArtifactId / responseArtifactId; orchestrators put bytes before emitting. SqliteEventStore wraps its txn in artifactStore.flushBefore so segment data is fsynced before the event commit, making the crash window non-corrupting (TailScanner re-indexes orphan tail records on reopen). Compactor and evictor are mutually exclusive via maintenanceMutex. Step 9 (cloud sync) deferred to a later epic. See docs/reviews/2026-05-18-cas-steps-1-8-review.md for the final review.
4.7 KiB
CAS Artifact Store — Steps 1–8 Final Review
Plan: docs/plans/2026-05-17-cas-artifact-store.md
Date: 2026-05-18
Scope: Steps 1–8 (step 9 cloud sync deferred to a later epic)
Build status: :infrastructure:artifacts-cas:check green, 21/21 tests, detekt + kover clean
Summary
Content-Addressed Store added as a new core interface (core:artifacts-store) plus a SQLite+segment-file implementation (infrastructure/artifacts-cas). Inference events now carry promptArtifactId / responseArtifactId; orchestrators put(...) bytes before emitting events. SqliteEventStore.append wraps its txn in artifactStore.flushBefore { ... } so segment data is fsynced before the event commit. Recovery tail-scan re-indexes orphan tail records on reopen, making the crash window between artifact write and event commit non-corrupting. A manual compactor + oldest-first disk-cap evictor round out the lifecycle; both gated by a maintenanceMutex so they cannot race each other.
CLAUDE.md invariants
All hold:
- Event log remains sole source of truth; segment + index are derivable via
TailScanner.recover(). - No new
EventPayloadsubclasses introduced;InferenceStartedEvent/InferenceCompletedEventalready registered incore/events/.../serialization/Serialization.kt. - No cross-core impl deps.
core:artifacts-storedepends only oncore:events(for theArtifactId = TypeIdtypealias). - Apps → core → infrastructure DAG preserved.
- Polymorphic registration verified for the modified inference event classes.
Findings
MUST FIX
None.
SHOULD FIX
-
CasArtifactStore.open()discards theTailScanReport(infrastructure/artifacts-cas/.../CasArtifactStore.kt:87-91). If recovery truncates a corrupt tail or re-indexes orphan records, the caller has no signal. Log at INFO whentruncatedAt != nullorrecoveredRecords > 0. -
No end-to-end orchestrator→put→append→replay→get test.
SessionOrchestratorIntegrationTestusesNoopArtifactStoreplus aRecordingArtifactStoreto assert put-then-emit ordering, but nothing wires the realCasArtifactStore+SqliteEventStoretogether to verify the crash-window invariant in situ. Add one test: put via real CAS, append an inference event, reopen, replay, assertget(responseArtifactId)returns the original bytes. -
SqliteEventStore.allEvents()(new in this diff) lackswithContext(Dispatchers.IO). Called fromLivenessScannerwhich already wraps the iteration, so currently safe — but for consistency the read methods on the store itself should dispatch internally. -
ReplayOrchestrator.runInferencesuper path still callsartifactStore.putfor the prompt before checking strategy (core/kernel/.../ReplayOrchestrator.kt:178). For non-SkipInferencestrategies during replay this writes a new artifact for an event being re-derived, bloating the store with duplicates. Verify intent; if unintentional, gateputon a replay flag.
NICE TO HAVE
SegmentWriter.appenddoes not fsync per-record; durability comes fromflushBefore. Document this contract inArtifactStore.ktKDoc.Compactor.readLiveEntriesbuildsliveHashHexfromliveIds.value(hex) and compares tohash.toHex()per entry. AByteArrayKeywrapper would avoid per-entry hex allocation.CasArtifactStore.evictdoeslistDirectoryEntriesundermutexto determineactiveId.SegmentWriteralready trackscurrentSegmentId— expose a getter and skip the filesystem call.
Categories with no findings
- Polymorphic serialization registration
- Cross-core deps
- Resource hygiene (
AutoCloseable,.use { }chains) - Detekt antipatterns (
Thread.sleep, bare try/catch, new@Suppress)
Per-step inventory
| Step | Subject | Status |
|---|---|---|
| 1 | ArtifactId value class in core:events |
reused existing TypeId typealias |
| 2 | core:artifacts-store interface module |
done |
| 3 | infrastructure/artifacts-cas MVP (writer/reader/index/store) |
done |
| 4 | DI wiring + flushBefore around SQLite commit |
done |
| 5 | Event schema (promptArtifactId / responseArtifactId) |
done |
| 6 | Recovery tail-scan | done |
| 7 | Compactor + LivenessScanner | done |
| 8 | Disk-cap eviction policy | done |
| 9 | Cloud sync | deferred (later epic) |