Build checkpoint subtree paths via path.Join helper (no tree change) · Entire
Build checkpoint subtree paths via path.Join helper (no tree change)
5cc6d2b·
Soph·2w ago·2 files·+117 added/-66 removed
A reviewer flagged fmt.Sprintf("%s%d/%s", basePath, idx, paths.MetadataFileName)
as looking like a missing slash — it's correct only because basePath carries a
trailing "/". Replace the string-concat path construction with a
checkpointSubtreePath(base, segs...) helper over stdlib path.Join, so paths join
correctly without relying on that invariant.
- Helper uses the
pathpackage (NEVER path/filepath — git tree paths are always
"/"; filepath.Join would emit "\" on Windows and corrupt tree keys). path.Join
cleans separators, so base may be "" (ref root), "/ /" (v1, trailing
slash), or a clean dir — all join identically. - Converted every full-path site and routed the directory-prefix intermediates
through the helper as CLEAN dirs (sessionDir, taskDir), updating their
consumers (writeSessionToSubdirectory, writeTranscript, writeCompactTranscript,
replaceTranscript, replaceSkillEvents, copyMetadataDir, writeTaskCheckpoint*). - The two prefix-scoping deletes that relied on the trailing slash now append it
explicitly: HasPrefix(key, sessionDir+"/") (so "1" doesn't match sibling "10")
and the transcript chunk cleanup. Leading-slash SessionFilePaths values become
"/" + checkpointSubtreePath(...).
No behavior change: committed-tree assertions (checkpoint write/read, tripwire,
update), integration (378), and both canary modes (git-branch 59/59, git-refs
58/59 +1 skip) are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
973f8248e820View transcript
Changes
2
cmd/entire/cli/checkpoint
Mpersistent.go+84/-66
- Asubtree_path_test.go+33
9 unmodified lines
10
11
12
13
14
15
16
122 unmodified lines
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
82 unmodified lines
239
240
241
229
242
243
244
245
32 unmodified lines
278
279
280
268
281
282
283
284
6 unmodified lines
291
292
293
281
294
295
296
297
36 unmodified lines
334
335
336
324
337
338
339
340
11 unmodified lines
352
353
354
342
355
356
357
358
11 unmodified lines
370
371
372
360
373
374
375
376
377
378
366
379
380
381
382
6 unmodified lines
389
390
391
379
392
393
394
395
3 unmodified lines
399
400
401
389
390
402
403
404
405
406
20 unmodified lines
427
428
429
417
418
430
431
432
433
434
435
436
437
438
425
439
440
441
442
3 unmodified lines
446
447
448
435
449
450
451
438
452
453
440
454
455
456
457
444
458
459
460
461
14 unmodified lines
476
477
478
465
479
480
481
482
3 unmodified lines
486
487
488
475
489
490
491
492
9 unmodified lines
502
503
504
491
505
506
507
508
19 unmodified lines
528
529
530
517
531
532
533
534
3 unmodified lines
538
539
540
527
528
541
542
543
544
545
14 unmodified lines
560
561
562
549
563
564
565
566
18 unmodified lines
585
586
587
574
588
589
590
591
9 unmodified lines
601
602
603
590
591
604
605
606
607
608
609
610
611
598
612
613
614
615
17 unmodified lines
633
634
635
622
636
637
638
639
11 unmodified lines
651
652
653
640
654
655
656
643
644
657
658
659
660
646
661
662
663
664
1 unmodified line
666
667
668
654
669
670
671
672
673
659
660
674
675
676
677
663
664
678
679
680
681
682
5 unmodified lines
688
689
690
676
677
691
692
693
694
695
696
697
698
699
700
37 unmodified lines
738
739
740
725
726
741
742
743
744
745
746
730
747
748
749
750
13 unmodified lines
764
765
766
750
767
768
769
770
37 unmodified lines
808
809
810
794
795
811
812
813
814
815
47 unmodified lines
863
864
865
849
866
867
868
869
22 unmodified lines
892
893
894
878
895
896
897
898
83 unmodified lines
982
983
984
968
985
986
987
988
38 unmodified lines
1027
1028
1029
1013
1030
1031
1032
1033
19 unmodified lines
1053
1054
1055
1039
1040
1056
1057
1058
1059
1060
1061
3 unmodified lines
1065
1066
1067
1050
1068
1069
1070
1071
20 unmodified lines
1092
1093
1094
1077
1095
1096
1097
1098
1099
1082
1100
1101
1102
1103
32 unmodified lines
1136
1137
1138
1121
1139
1140
1141
1142
617 unmodified lines
1760
1761
1762
1745
1746
1763
1764
1765
1766
1767
34 unmodified lines
1802
1803
1804
1787
1805
1806
1807
1808
10 unmodified lines
1819
1820
1821
1804
1822
1823
1824
1825
7 unmodified lines
1833
1834
1835
1818
1836
1837
1838
1839
21 unmodified lines
1861
1862
1863
1846
1864
1865
1866
1867
28 unmodified lines
1896
1897
1898
1881
1899
1900
1901
1902
142 unmodified lines
2045
2046
2047
2030
2048
2049
2050
2051
44 unmodified lines
2096
2097
2098
2081
2099
2100
2101
2102
9 unmodified lines
"io"
"log/slog"
"os"
"path"
"path/filepath"
"sort"
"strconv"
122 unmodified lines
return subtree, nil
}
// checkpointSubtreePath joins a checkpoint-relative git tree path from a base and
// trailing segments using path.Join. Git tree paths are always "/"-separated, so
// this uses the stdlib path package (never path/filepath, which would emit "\\" on
// Windows and corrupt tree keys). path.Join cleans separators, so base may be ""
// (per-checkpoint-ref root), a clean dir ("a3/b2.../0"), or a trailing-slash dir
// ("a3/b2.../"): checkpointSubtreePath("", "0", "metadata.json") == "0/metadata.json"
// and checkpointSubtreePath("a3/b2.../", "0", "metadata.json") == "a3/b2.../0/metadata.json".
// Callers therefore need not maintain the trailing-slash invariant by hand.
func checkpointSubtreePath(base string, segs ...string) string {
return path.Join(append([]string{base}, segs...)...)
}
// flattenExisting flattens a checkpoint's current subtree into a path->entry map
// keyed under basePath, so the per-checkpoint write helpers (which build paths as
// basePath+"<n>/<file>") see the existing files. basePath is ""
return plumbing.ZeroHash, err
}
rootMetadataPath := basePath + paths.MetadataFileName
rootMetadataPath := checkpointSubtreePath(basePath, paths.MetadataFileName)
entry, exists := entries[rootMetadataPath]
if !exists {
return plumbing.ZeroHash, ErrCheckpointNotFound
}
// Find the latest session's metadata path (0-based indexing)
latestIndex := len(checkpointSummary.Sessions) - 1
sessionMetadataPath := fmt.Sprintf("%s%d/%s", basePath, latestIndex, paths.MetadataFileName)
sessionMetadataPath := checkpointSubtreePath(basePath, strconv.Itoa(latestIndex), paths.MetadataFileName)
sessionEntry, exists := entries[sessionMetadataPath]
if !exists {
return plumbing.ZeroHash, "", fmt.Errorf("session metadata not found at %s", sessionMetadataPath)
}
return plumbing.ZeroHash, err
}
sessionIndex := -1
var sessionMeta *Metadata
for i := range len(checkpointSummary.Sessions) {
metaPath := fmt.Sprintf("%s%d/%s", basePath, i, paths.MetadataFileName)
metaPath := checkpointSubtreePath(basePath, strconv.Itoa(i), paths.MetadataFileName)
if metaEntry, metaExists := entries[metaPath]; metaExists {
meta, metaErr := s.readMetadataFromBlob(metaEntry.Hash)
if metaErr == nil && meta.SessionID == opts.SessionID {
}
}
}
sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex)
sessionDir := checkpointSubtreePath(basePath, strconv.Itoa(sessionIndex))
// Replace transcript (full replace, not append).
// Transcript is pre-redacted by the caller (enforced by RedactedBytes type).
if err := s.replaceTranscript(ctx, opts.Transcript, agentType, startLine, opts.PrecomputedBlobs, sessionPath, entries); err != nil {
if err := s.replaceTranscript(ctx, opts.Transcript, agentType, startLine, opts.PrecomputedBlobs, sessionDir, entries); err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to replace transcript: %w", err)
}
}
// succeeds now), so re-derive the pointer from the tree entry and rewrite
// the root summary when it changed.
compactPath := ""
if _, ok := entries[sessionPath+paths.CompactTranscriptFileName]; ok {
compactPath = "/" + sessionPath + paths.CompactTranscriptFileName
if _, ok := entries[checkpointSubtreePath(sessionDir, paths.CompactTranscriptFileName)]; ok {
compactPath = "/" + checkpointSubtreePath(sessionDir, paths.CompactTranscriptFileName)
}
if checkpointSummary.Sessions[sessionIndex].CompactTranscript != compactPath {
checkpointSummary.Sessions[sessionIndex].CompactTranscript = compactPath
}
if err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to create prompt blob: %w", err)
}
entries[sessionPath+paths.PromptFileName] = object.TreeEntry{
Name: sessionPath + paths.PromptFileName,
promptPath := checkpointSubtreePath(sessionDir, paths.PromptFileName)
entries[promptPath] = object.TreeEntry{
Name: promptPath,
Mode: filemode.Regular,
Hash: blobHash,
}
if len(opts.SkillEvents) > 0 {
if err := s.replaceSkillEvents(opts.SkillEvents, sessionPath, entries); err != nil {
if err := s.replaceSkillEvents(opts.SkillEvents, sessionDir, entries); err != nil {
return plumbing.ZeroHash, fmt.Errorf("failed to replace skill events: %w", err)
}
}
}
// writeTaskCheckpointEntries writes task-specific checkpoint entries and returns the task metadata path.
func (s *treeWriter) writeTaskCheckpointEntries(ctx context.Context, opts WriteOptions, basePath string, entries map[string]object.TreeEntry) (string, error) {
taskPath := basePath + "tasks/" + opts.ToolUseID + "/"
taskDir := checkpointSubtreePath(basePath, "tasks", opts.ToolUseID)
if opts.IsIncremental {
return s.writeIncrementalTaskCheckpoint(opts, taskPath, entries)
}
return s.writeFinalTaskCheckpoint(ctx, opts, taskPath, entries)
}
// writeIncrementalTaskCheckpoint writes an incremental checkpoint file during task execution.
func (s *treeWriter) writeIncrementalTaskCheckpoint(opts WriteOptions, taskPath string, entries map[string]object.TreeEntry) (string, error) {
// writeFinalTaskCheckpoint writes the final checkpoint.json and subagent transcript.
func (s *treeWriter) writeFinalTaskCheckpoint(ctx context.Context, opts WriteOptions, taskPath string, entries map[string]object.TreeEntry) (string, error) {
}
// writeStandardCheckpointEntries writes session files to numbered subdirectories and
// returns the absolute file paths from the git tree root for the sessions map.
func (s *treeWriter) writeStandardCheckpointEntries(ctx context.Context, opts WriteOptions, basePath string, entries map[string]object.TreeEntry, checkpointVersion string) error {
var existingSummary *CheckpointSummary
metadataPath := basePath + paths.MetadataFileName
metadataPath := checkpointSubtreePath(basePath, paths.MetadataFileName)
if entry, exists := entries[metadataPath]; exists {
existing, err := s.readSummaryFromBlob(entry.Hash)
if err == nil {
}
}
// We read and capture BEFORE writeSessionToSubdirectory clears the subtree,
// otherwise we'd only ever see our own write.
if sessionIndex == 0 {
if entry, exists := entries[fmt.Sprintf("%s0/%s", basePath, paths.MetadataFileName)]; exists {
if existingMeta, readErr := s.readMetadataFromBlob(entry.Hash); readErr == nil && existingMeta.SessionID != opts.SessionID {
logging.Error(ctx, "refusing checkpoint write: session 0 holds a different sessionID",
slog.String("checkpoint_id", opts.CheckpointID.String()),
)
}
}
}
// Write session files to numbered subdirectory
sessionPath := fmt.Sprintf("%s%d/", basePath, sessionIndex)
sessionFilePaths, err := s.writeSessionToSubdirectory(ctx, opts, sessionPath, entries)
sessionDir := checkpointSubtreePath(basePath, strconv.Itoa(sessionIndex))
sessionFilePaths, err := s.writeSessionToSubdirectory(ctx, opts, sessionDir, entries)
if err != nil {
return err
}
// Copy additional metadata files from directory if specified (to session subdirectory)
if opts.MetadataDir != "" {
if err := s.copyMetadataDir(ctx, opts.MetadataDir, sessionPath, entries); err != nil {
if err := s.copyMetadataDir(ctx, opts.MetadataDir, sessionDir, entries); err != nil {
return fmt.Errorf("failed to copy metadata directory: %w", err)
}
}
}
// metadata with a DIFFERENT sessionID, that's the exact bug shape.
// Loud WARN so we get a log trace instead of only the symptom.
if sessionIndex == 0 {
path := fmt.Sprintf("%s0/%s", basePath, paths.MetadataFileName)
path := checkpointSubtreePath(basePath, "0", paths.MetadataFileName)
if entry, exists := entries[path]; exists {
if existingMeta, readErr := s.readMetadataFromBlob(entry.Hash); readErr == nil && existingMeta.SessionID != opts.SessionID {
logging.Warn(ctx, "checkpoint write overwrites session 0 with a different sessionID — potential overwrite regression",
slog.String("checkpoint_id", opts.CheckpointID.String()),
)
}
}
}
// writeSessionToSubdirectory writes a single session's files to a numbered subdirectory.
// Returns the absolute file paths from the git tree root for the sessions map.
func (s *treeWriter) writeSessionToSubdirectory(ctx context.Context, opts WriteOptions, sessionPath string, entries map[string]object.TreeEntry) (SessionFilePaths, error) {
func (s *treeWriter) writeSessionToSubdirectory(ctx context.Context, opts WriteOptions, sessionDir string, entries map[string]object.TreeEntry) (SessionFilePaths, error) {
filePaths := SessionFilePaths{}
// Clear any existing entries at this path so stale files from a previous
// write (e.g. prompt.txt) don't persist on overwrite.
// Clear any existing entries under this session dir so stale files from a
// previous write (e.g. prompt.txt) don't persist on overwrite. Match on the
// dir plus "/" so a sibling session (e.g. "10") isn't caught by "1".
for key := range entries {
if strings.HasPrefix(key, sessionPath) {
delete(entries, key)
}
}
// Write transcript. Transcript points at full.jsonl (CLI
// rewind/resume/explain read it by filename); the compact transcript.jsonl,
// when written, is also pushed and pointed at by CompactTranscript.
wroteTranscript, err := s.writeTranscript(ctx, opts, sessionPath, entries)
wroteTranscript, err := s.writeTranscript(ctx, opts, sessionDir, entries)
if err != nil {
return filePaths, err
}
if wroteTranscript {
filePaths.Transcript = "/" + sessionPath + paths.TranscriptFileName
filePaths.ContentHash = "/" + sessionPath + paths.ContentHashFileName
filePaths.Transcript = "/" + checkpointSubtreePath(sessionDir, paths.TranscriptFileName)
filePaths.ContentHash = "/" + checkpointSubtreePath(sessionDir, paths.ContentHashFileName)
// Point at the compact transcript only when it was actually written
// (best-effort), deriving from the tree entry so the path can't dangle.
if _, ok := entries[sessionPath+paths.CompactTranscriptFileName]; ok {
filePaths.CompactTranscript = "/" + sessionPath + paths.CompactTranscriptFileName
if _, ok := entries[checkpointSubtreePath(sessionDir, paths.CompactTranscriptFileName)]; ok {
filePaths.CompactTranscript = "/" + checkpointSubtreePath(sessionDir, paths.CompactTranscriptFileName)
}
}
}
if err != nil {
return filePaths, err
}
entries[sessionPath+paths.PromptFileName] = object.TreeEntry{
Name: sessionPath + paths.PromptFileName,
promptPath := checkpointSubtreePath(sessionDir, paths.PromptFileName)
entries[promptPath] = object.TreeEntry{
Name: promptPath,
Mode: filemode.Regular,
Hash: blobHash,
}
filePaths.Prompt = "/" + sessionPath + paths.PromptFileName
filePaths.Prompt = "/" + promptPath
// Write session-level metadata.json (Metadata with all fields including initial_attribution)
}
if err != nil {
return filePaths, err
}
entries[sessionPath+paths.MetadataFileName] = object.TreeEntry{
Name: sessionPath + paths.MetadataFileName,
sessionMetadataPath := checkpointSubtreePath(sessionDir, paths.MetadataFileName)
entries[sessionMetadataPath] = object.TreeEntry{
Name: sessionMetadataPath,
Mode: filemode.Regular,
Hash: metadataHash,
}
filePaths.Metadata = "/" + sessionPath + paths.MetadataFileName
filePaths.Metadata = "/" + sessionMetadataPath
}
return filePaths, nil
}
}
// copyMetadataDir copies all files from a directory to the checkpoint path.
// Used to include additional metadata files like task checkpoints, subagent transcripts, etc.
func (s *treeWriter) copyMetadataDir(ctx context.Context, metadataDir, sessionDir string, entries map[string]object.TreeEntry) error {
func (s *treeWriter) copyMetadataDir(ctx context.Context, metadataDir, basePath string, entries map[string]object.TreeEntry) error {
error := filepath.Walk(metadataDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
}
})
...