# 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 `EventPayload` subclasses introduced; `InferenceStartedEvent` / `InferenceCompletedEvent` already registered in `core/events/.../serialization/Serialization.kt`.
- No cross-core impl deps. `core:artifacts-store` depends only on `core:events` (for the `ArtifactId = TypeId` typealias).
- Apps → core → infrastructure DAG preserved.
- Polymorphic registration verified for the modified inference event classes.

## Findings

### MUST FIX
None.

### SHOULD FIX

1. **`CasArtifactStore.open()` discards the `TailScanReport`** (`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 when `truncatedAt != null` or `recoveredRecords > 0`.

2. **No end-to-end orchestrator→put→append→replay→get test.** `SessionOrchestratorIntegrationTest` uses `NoopArtifactStore` plus a `RecordingArtifactStore` to assert put-then-emit ordering, but nothing wires the real `CasArtifactStore` + `SqliteEventStore` together to verify the crash-window invariant in situ. Add one test: put via real CAS, append an inference event, reopen, replay, assert `get(responseArtifactId)` returns the original bytes.

3. **`SqliteEventStore.allEvents()` (new in this diff) lacks `withContext(Dispatchers.IO)`.** Called from `LivenessScanner` which already wraps the iteration, so currently safe — but for consistency the read methods on the store itself should dispatch internally.

4. **`ReplayOrchestrator.runInference` super path still calls `artifactStore.put`** for the prompt before checking strategy (`core/kernel/.../ReplayOrchestrator.kt:178`). For non-`SkipInference` strategies during replay this writes a new artifact for an event being re-derived, bloating the store with duplicates. Verify intent; if unintentional, gate `put` on a replay flag.

### NICE TO HAVE

- `SegmentWriter.append` does not fsync per-record; durability comes from `flushBefore`. Document this contract in `ArtifactStore.kt` KDoc.
- `Compactor.readLiveEntries` builds `liveHashHex` from `liveIds.value` (hex) and compares to `hash.toHex()` per entry. A `ByteArrayKey` wrapper would avoid per-entry hex allocation.
- `CasArtifactStore.evict` does `listDirectoryEntries` under `mutex` to determine `activeId`. `SegmentWriter` already tracks `currentSegmentId` — 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) |
