Files
correx/docs/reviews/2026-05-18-cas-steps-1-8-review.md
T
kami 219e2c762e feat(cas): content-addressed artifact store (steps 1–8)
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.
2026-05-18 12:22:38 +04:00

63 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CAS Artifact Store — Steps 18 Final Review
**Plan:** `docs/plans/2026-05-17-cas-artifact-store.md`
**Date:** 2026-05-18
**Scope:** Steps 18 (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) |