@@ -3,9 +3,12 @@ package checkpoint
33import (
44 "context"
55 "errors"
6+ "fmt"
7+ "log/slog"
68 "sort"
79
810 "github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
11+ "github.com/entireio/cli/cmd/entire/cli/logging"
912)
1013
1114// kindRoutingStore resolves id-keyed reads across the two git backends so a repo
@@ -20,10 +23,12 @@ import (
2023// a git-branch primary the branch is authoritative for hex, so refs is not
2124// consulted.
2225//
23- // List unions both backends (disjoint ID spaces). Writes are NOT kind-routed:
24- // they go to the configured primary (+ mirrors) via writer, since a new
25- // checkpoint's ID is already minted to match the primary's format
26- // (see checkpoint.GenerateCheckpointID).
26+ // List unions both backends (disjoint ID spaces). Creates (Session) are NOT
27+ // kind-routed: they go to the configured primary (+ mirrors) via writer, since
28+ // a new checkpoint's ID is already minted to match the primary's format (see
29+ // checkpoint.GenerateCheckpointID). Backfills update an existing checkpoint,
30+ // so they follow the same store order as reads, though only
31+ // ErrCheckpointNotFound falls through (stricter than reads) — see Write.
2732type kindRoutingStore struct {
2833 writer PersistentStore // configured primary + mirrors (fanout); handles Write
2934 branch PersistentStore // git-branch store; serves hex reads
@@ -183,9 +188,103 @@ func (s *kindRoutingStore) ReadSessionMetadataAndPrompts(ctx context.Context, ch
183188 return mp .meta , mp .prompts , err
184189}
185190
186- // Write is not kind-routed: it targets the configured primary (+ mirrors).
191+ // Write routes a create (Session) to the configured primary (+ mirrors): a new
192+ // checkpoint's ID is already minted to match the primary's format (see
193+ // checkpoint.GenerateCheckpointID). Backfills target an EXISTING checkpoint,
194+ // which — like reads — may live in either git backend (e.g. a pre-migration hex
195+ // checkpoint still on the v1 branch under a git-refs primary), so they follow
196+ // the read order, falling through to the next store on ErrCheckpointNotFound.
197+ //
198+ // The fallthrough is deliberately stricter than read routing's firstResolved
199+ // (which falls through on absent OR any error): only the not-found sentinel
200+ // falls through here. Redirecting a write to another backend after a transient
201+ // primary failure could fork the data, so a hard error aborts and surfaces.
202+ // Note the stores' backfill absence probes are local-only (the refs store's
203+ // refBase does not on-demand fetch like its read path does); a checkpoint
204+ // whose ref exists only remotely backfills to the fallback store.
187205func (s * kindRoutingStore ) Write (ctx context.Context , req WriteRequest ) error {
188- return s .writer .Write (ctx , req ) //nolint:wrapcheck // primary error is the operation's error, surfaced verbatim
206+ checkpointID , isBackfill := backfillTarget (req )
207+ if ! isBackfill {
208+ return s .writer .Write (ctx , req ) //nolint:wrapcheck // primary error is the operation's error, surfaced verbatim
209+ }
210+ stores := s .backfillOrder (checkpointID )
211+ var err error
212+ for i , st := range stores {
213+ err = st .Write (ctx , req )
214+ if ! errors .Is (err , ErrCheckpointNotFound ) {
215+ if err == nil && i > 0 {
216+ // The most consequential routing decision here: the data landed
217+ // somewhere other than the configured primary, and mirrors
218+ // (which follow the primary) were skipped. Record it so "why is
219+ // this backfill on the v1 branch and not in refs / the mirror"
220+ // stays diagnosable.
221+ logging .Info (ctx , "checkpoint: backfill served by fallback store; absent from primary, mirrors skipped" ,
222+ slog .String ("checkpoint_id" , checkpointID .String ()),
223+ slog .String ("request_type" , fmt .Sprintf ("%T" , req )))
224+ }
225+ return err //nolint:wrapcheck // in-package store error surfaced verbatim
226+ }
227+ if i < len (stores )- 1 {
228+ logging .Debug (ctx , "checkpoint: backfill target absent in store, trying next" ,
229+ slog .String ("checkpoint_id" , checkpointID .String ()),
230+ slog .String ("request_type" , fmt .Sprintf ("%T" , req )),
231+ slog .Int ("store_index" , i ))
232+ }
233+ }
234+ return err //nolint:wrapcheck // ErrCheckpointNotFound from the final store, surfaced verbatim
235+ }
236+
237+ // backfillTarget returns the checkpoint ID a backfill request updates.
238+ // ok is false for Session (a create) and unknown request types, which are not
239+ // kind-routed.
240+ //
241+ // WriteRequest is a closed union: any new backfill-shaped request type MUST be
242+ // added to this switch, or it silently gets create routing — primary-only, no
243+ // fallback — which for a pre-migration checkpoint reintroduces the discarded-
244+ // write bug this routing exists to prevent.
245+ func backfillTarget (req WriteRequest ) (id.CheckpointID , bool ) {
246+ switch r := req .(type ) {
247+ case SessionTranscript :
248+ return r .CheckpointID , true
249+ case SessionSummary :
250+ return r .CheckpointID , true
251+ case CheckpointAttribution :
252+ return r .CheckpointID , true
253+ default :
254+ return id .EmptyCheckpointID , false
255+ }
256+ }
257+
258+ // backfillOrder returns the write targets for a backfill of checkpointID, in
259+ // the same priority order reads use. The store that is the configured primary
260+ // is replaced by writer, so a backfill landing on the primary still fans out to
261+ // mirrors; a backfill landing on a fallback store deliberately skips mirrors
262+ // (mirrors follow the primary).
263+ func (s * kindRoutingStore ) backfillOrder (checkpointID id.CheckpointID ) []PersistentStore {
264+ order := s .readOrder (checkpointID )
265+ targets := make ([]PersistentStore , len (order ))
266+ for i , st := range order {
267+ if s .isPrimary (st ) {
268+ targets [i ] = s .writer
269+ } else {
270+ targets [i ] = st
271+ }
272+ }
273+ return targets
274+ }
275+
276+ // isPrimary reports whether st is the configured primary's read store.
277+ func (s * kindRoutingStore ) isPrimary (st PersistentStore ) bool {
278+ switch s .primaryType {
279+ case BackendTypeGitBranch :
280+ return st == s .branch
281+ case BackendTypeGitRefs :
282+ return st == s .refs
283+ default :
284+ // Not a real configuration today (buildPrimary only accepts the git
285+ // backends); backfills would bypass writer and therefore mirrors.
286+ return false
287+ }
189288}
190289
191290// kindRoutingStoreWithAuthor adds the optional AuthorReader capability, routing
0 commit comments