fix logs-only prompt reads · Entire
fix logs-only prompt reads
3283c77→main·
pfleidi·1mo ago·6 files·+55 added/-56 removed
Read logs-only rewind prompts from committed checkpoint session directories instead of assuming a root prompt file.
Update the v1.1 topology test to use real committed metadata and remove stale session-listing API docs after deleting those functions.
Sessions
ec15c6881b9dView transcript
?\ Implement Checkpoints v1.1 Topology CoverageCodex·GPT-5.5·1 step
Changes
6
MCLAUDE.md-1
cmd/entire/cli/strategy
Mcommon.go+14/-10
Mcommon_test.go+12
Mmanual_commit_rewind.go+8/-5
docs/architecture
- Msessions-and-checkpoints.md+7/-12
416 unmodified lines
417
418
419
420
420
421
422
416 unmodified lines
- `SaveTaskStep()` - Save subagent task step checkpoint
- `GetRewindPoints()` / `Rewind()` - List and restore to checkpoints
- `GetSessionLog()` / `GetSessionInfo()` - Retrieve session data
- `ListSessions()` / `GetSession()` - Session discovery
#### How It Works
MCLAUDE.md-1
938 unmodified lines
939
940
941
942
942
943
944
945
946
945
946
947
948
949
950
951
952
953
954
953
955
956
956
957
958
959
957
958
959
960
961
962
962
963
963
964
965
966
967
968
969
970
938 unmodified lines
// ReadAllSessionPromptsFromTree reads the first prompt for all sessions in a multi-session checkpoint.
// Returns a slice of prompts parallel to sessionIDs (oldest to newest).
// For single-session checkpoints, returns a slice with just the root prompt.
// For single-session checkpoints, returns a slice with just the session prompt.
func ReadAllSessionPromptsFromTree(tree *object.Tree, checkpointPath string, sessionCount int, sessionIDs []string) []string {
if sessionCount <= 1 || len(sessionIDs) <= 1 {
// Single session - just return the root prompt
prompt := ReadSessionPromptFromTree(tree, checkpointPath)
prompt := ReadSessionPromptFromTree(tree, checkpointPath+"/0")
if prompt == "" {
prompt = ReadSessionPromptFromTree(tree, checkpointPath)
}
if prompt != "" {
return []string{prompt}
}
return nil
}
// Multi-session: read prompts from archived folders (0/, 1/, etc.) and root
prompts := make([]string, len(sessionIDs))
// Read archived session prompts (folders 0, 1, ... N-2)
for i := range sessionCount - 1 {
archivedPath := fmt.Sprintf("%s/%d", checkpointPath, i)
prompts[i] = ReadSessionPromptFromTree(tree, archivedPath)
sessionLimit := min(sessionCount, len(prompts))
for i := range sessionLimit {
sessionPath := fmt.Sprintf("%s/%d", checkpointPath, i)
prompts[i] = ReadSessionPromptFromTree(tree, sessionPath)
}
// Read the most recent session prompt (at root level)
prompts[len(prompts)-1] = ReadSessionPromptFromTree(tree, checkpointPath)
// Older committed metadata stored the latest prompt at the checkpoint root.
latestIndex := sessionLimit - 1
if latestIndex >= 0 && prompts[latestIndex] == "" {
prompts[latestIndex] = ReadSessionPromptFromTree(tree, checkpointPath)
}
return prompts
}
Mcmd/entire/cli/strategy/common.go+14/-10
1598 unmodified lines
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1598 unmodified lines
})
}
func TestReadAllSessionPromptsFromTree(t *testing.T) {
t.Parallel()
tree := buildCommittedTree(t, map[string]string{
"a3/b2c4d5e6f7/0/prompt.txt": "First session prompt",
"a3/b2c4d5e6f7/1/prompt.txt": "Second session prompt",
})
got := ReadAllSessionPromptsFromTree(tree, "a3/b2c4d5e6f7", 2, []string{"session-1", "session-2"})
assert.Equal(t, []string{"First session prompt", "Second session prompt"}, got)
}
func TestIsEmptyRepository(t *testing.T) {
t.Parallel()
t.Run("empty repo returns true", func(t *testing.T) {
Mcmd/entire/cli/strategy/common_test.go+12
124 unmodified lines
125
126
127
128
128
129
130
131
132
132
133
134
135
4 unmodified lines
140
141
142
143
143
144
145
146
13 unmodified lines
160
161
162
163
163
164
165
166
52 unmodified lines
219
220
221
222
222
223
224
225
226
227
228
124 unmodified lines
// GetLogsOnlyRewindPoints finds commits in the current branch's history that have
// condensed session logs on the entire/checkpoints/v1 branch. These are commits that
// condensed session logs in committed checkpoint storage. These are commits that
// were created with session data but the shadow branch has been condensed.
// The function works by:
// 1. Getting all checkpoints from the entire/checkpoints/v1 branch
// 1. Getting all checkpoints from committed checkpoint storage
// 2. Building a map of checkpoint ID -> checkpoint info
// 3. Scanning the current branch history for commits with Entire-Checkpoint trailers
// 4. Matching by checkpoint ID (stable across amend/rebase)
4 unmodified lines
}
defer repo.Close()
// Get all checkpoints from entire/checkpoints/v1 branch
// Get all checkpoints from committed checkpoint storage
checkpoints, err := s.listCheckpoints(ctx)
if err != nil {
// No checkpoints yet is fine
13 unmodified lines
}
}
// Get metadata branch tree for reading session prompts (best-effort, ignore errors)
// Get committed metadata read tree for session prompts (best-effort, ignore errors)
readRef := cpkg.ResolveCommittedRefs(ctx).Read
metadataTree, _ := GetMetadataRefTree(repo, readRef) //nolint:errcheck // Best-effort for session prompts
52 unmodified lines
sessionPrompt = sessionPrompts[len(sessionPrompts)-1]
}
} else {
sessionPrompt = ReadSessionPromptFromTree(metadataTree, checkpointPath)
sessionPrompt = ReadLatestSessionPromptFromCommittedTree(metadataTree, cpInfo.CheckpointID, cpInfo.SessionCount)
if sessionPrompt == "" {
sessionPrompt = ReadSessionPromptFromTree(metadataTree, checkpointPath)
}
if sessionPrompt != "" {
sessionPrompts = []string{sessionPrompt}
}
}
}
Mcmd/entire/cli/strategy/manual_commit_rewind.go+8/-5
19 unmodified lines
20
21
22
23
23
24
25
459 unmodified lines
485
486
487
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
517
503
504
505
506
19 unmodified lines
"github.com/entireio/cli/redact"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing"
"github.com/go-git/go-git/v6/plumbing/filemode"
"github.com/go-git/go-git/v6/plumbing/object"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
459 unmodified lines
cpID := id.MustCheckpointID("a1b2c3d4e5f6")
const wantPrompt = "only-on-mirror"
// Build the checkpoint commit by hand: WriteCommitted lands prompt.txt
// under <sharded>/0/, but ReadSessionPromptFromTree reads <sharded>/prompt.txt.
promptBlob, promptErr := checkpoint.CreateBlobFromContent(repo, []byte(wantPrompt))
require.NoError(t, promptErr)
summaryJSON := `{"checkpoint_id":"` + cpID.String() + `","sessions":[]}`
summaryBlob, summaryErr := checkpoint.CreateBlobFromContent(repo, []byte(summaryJSON))
require.NoError(t, summaryErr)
shardedPath := cpID.Path() // "a1/b2c3d4e5f6"
treeEntries := map[string]object.TreeEntry{
shardedPath + "/prompt.txt": {
Name: shardedPath + "/prompt.txt",
Mode: filemode.Regular,
Hash: promptBlob,
},
shardedPath + "/metadata.json": {
Name: shardedPath + "/metadata.json",
Mode: filemode.Regular,
Hash: summaryBlob,
},
}
treeHash, treeErr := checkpoint.BuildTreeFromEntries(t.Context(), repo, treeEntries)
require.NoError(t, treeErr)
commitHash, commitErr := checkpoint.CreateCommit(t.Context(), repo, treeHash, plumbing.ZeroHash, "checkpoint commit", "Test", "test@test.com")
require.NoError(t, commitErr)
// Mirror carries the checkpoint; v1 points at the initial commit (no metadata).
require.NoError(t, checkpoint.NewGitStore(repo).WriteCommitted(t.Context(), checkpoint.WriteCommittedOptions{
CheckpointID: cpID,
SessionID: "test-session-v11-rewind",
Strategy: "manual-commit",
Transcript: redact.AlreadyRedacted([]byte("transcript\n")),
Prompts: []string{wantPrompt},
AuthorName: "Test",
AuthorEmail: "test@test.com",
}))
v1Ref := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
committedRef, err := repo.Reference(v1Ref, true)
require.NoError(t, err)
// Mirror carries the checkpoint; v1 points at the initial commit (no metadata).
mirrorRef := plumbing.ReferenceName(paths.MetadataRefName)
require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(mirrorRef, commitHash)))
require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(mirrorRef, committedRef.Hash())))
require.NoError(t, repo.Storer.SetReference(plumbing.NewHashReference(v1Ref, baseHash)))
// HEAD trailer drives the picker's log walk.
```
Mcmd/entire/cli/strategy/manual\_commit\_test.go+14/-28
54 unmodified lines
55 56 57 58 58 59 60 61 62 63 64 65 66 67 68 69 60 61 62 63 64 65 66 67 267 unmodified lines
335 336 337 343 338 339 340 341
54 unmodified lines
Interface
Session Operations
Session Access
Sessions are accessed via standalone functions in strategy/session.go:
// ListSessions returns all sessions from entire/checkpoints/v1,
// plus additional sessions from strategies implementing SessionSource.
func ListSessions() ([]Session, error)
// GetSession finds a session by ID (supports prefix matching).
func GetSession(sessionID string) (*Session, error)
strategy/session.go keeps the Session and Checkpoint data types used by
status/explain formatting. Active session state is read from .git/entire-sessions/
through session.StateStore; committed checkpoint/session content is read through
the committed checkpoint store (checkpoint.NewCommittedReadStore(...)) and
command-specific strategy methods such as GetSessionInfo.
Checkpoint Storage (Low-Level)
267 unmodified lines
strategy/
├── session.go # Session and Checkpoint types, ListSessions(), GetSession()
├── session.go # Session and Checkpoint types
session/
├── state.go # Active session state (StateStore, .git/entire-sessions/)
Mdocs/architecture/sessions-and-checkpoints.md+7/-12