feat(import): orchestrate idempotent per-turn import to imports ref · Entire
feat(import): orchestrate idempotent per-turn import to imports ref
33c1d4b→main·
computermode·3w ago·2 files·+250 added/-0 removed
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
8db9350e70f6View transcript
Changes
2
cmd/entire/cli/importclaude
Aimporter.go+119
Aimporter_test.go+131
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
package importclaude
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/go-git/go-git/v6"
"github.com/entireio/cli/cmd/entire/cli/agent"
cp "github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
"github.com/entireio/cli/cmd/entire/cli/session"
"github.com/entireio/cli/redact"
)
const importVersion = 1
// Options configures an import run.
type Options struct {
RepoRoot string
OverridePath string
SessionFilter []string
Now time.Time
DryRun bool
}
// Result summarizes an import run.
type Result struct {
SessionsScanned int
TurnsImported int
TurnsSkipped int
}
// Run imports the repo's Claude Code transcripts (within the lookback window)
// as read-only, local-only checkpoints on entire/imports/v1. It is idempotent:
// turns whose deterministic ID already exists are skipped.
func Run(ctx context.Context, repo *git.Repository, opts Options) (Result, error) {
var res Result
files, err := DiscoverSessions(opts.RepoRoot, opts.OverridePath, opts.Now, opts.SessionFilter)
if err != nil {
return res, err
}
stores, err := cp.Open(ctx, repo, cp.OpenOptions{Refs: ptrRefs(cp.ImportsRefs())})
if err != nil {
return res, fmt.Errorf("open imports store: %w", err)
}
existing := make(map[string]bool)
if infos, listErr := stores.Persistent.List(ctx); listErr == nil {
for _, in := range infos {
existing[in.CheckpointID.String()] = true
}
}
for _, path := range files {
res.SessionsScanned++
sessionID := strings.TrimSuffix(filepath.Base(path), ".jsonl")
full, readErr := os.ReadFile(path)
if readErr != nil {
return res, fmt.Errorf("read %s: %w", path, readErr)
}
subagentsDir := filepath.Join(filepath.Dir(path), sessionID, "subagents")
urns, splitErr := SplitTurns(full, subagentsDir)
if splitErr != nil {
return res, splitErr
}
for _, turn := range turns {
cid := DeriveCheckpointID(sessionID, turn.UUID)
if existing[cid.String()] {
res.TurnsSkipped++
continue
}
if opts.DryRun {
res.TurnsImported++ // counts what would import
continue
}
if err := writeTurn(ctx, stores, cid, sessionID, path, full, turn); err != nil {
return res, err
}
existing[cid.String()] = true
res.TurnsImported++
}
}
return res, nil
}
func writeTurn(ctx context.Context, stores *cp.Stores, cid id.CheckpointID, sessionID, path string, full []byte, turn Turn) error {
red, err := redact.JSONLBytes(full)
if err != nil {
return fmt.Errorf("redact transcript: %w", err)
}
prov := &cp.Provenance{
Source: "claude-code", TranscriptPath: path, SessionID: sessionID,
TurnUUID: turn.UUID, ParentUUID: turn.ParentUUID,
LineStart: turn.LineStart, LineEnd: turn.LineEnd,
ContentHash: turn.ContentHash, ImportVersion: importVersion,
}
return stores.Persistent.Write(ctx, cp.Session(cp.WriteOptions{
CheckpointID: cid,
SessionID: sessionID,
CreatedAt: turn.CreatedAt,
Strategy: "import",
Kind: string(session.KindImported),
Agent: agent.AgentTypeClaudeCode,
Model: turn.Model,
Transcript: red,
Prompts: []string{turn.Prompt},
CheckpointsCount: 1,
CheckpointTranscriptStart: turn.LineStart,
TokenUsage: turn.Tokens,
Provenance: prov,
}))
}
func ptrRefs(r cp.PersistentRefs) *cp.PersistentRefs { return &r }
Acmd/entire/cli/importclaude/importer.go+119
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package importclaude
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/go-git/go-git/v6"
"github.com/go-git/go-git/v6/plumbing/object"
cp "github.com/entireio/cli/cmd/entire/cli/checkpoint"
"github.com/entireio/cli/cmd/entire/cli/testutil"
)
func initRepoWithCommit(t *testing.T) (*git.Repository, string) {
t.Helper()
repoDir := t.TempDir()
testutil.InitRepo(t, repoDir)
repo, err := git.PlainOpen(repoDir)
if err != nil {
t.Fatal(err)
}
wt, err := repo.Worktree()
if err != nil {
t.Fatal(err)
}
testutil.WriteFile(t, repoDir, "f.txt", "x")
if _, err := wt.Add("f.txt"); err != nil {
t.Fatal(err)
}
if _, err := wt.Commit("init", &git.CommitOptions{
Author: &object.Signature{Name: "Test", Email: "test@test.com"},
}); err != nil {
t.Fatal(err)
}
return repo, repoDir
}
func writeFixtureSession(t *testing.T, dir, name string) {
t.Helper()
content := strings.Join([]string{
`{"type":"user","uuid":"u1","timestamp":"2026-06-20T00:00:00Z","message":{"role":"user","content":"first"}}`,
`{"type":"assistant","uuid":"a1","message":{"id":"m1","model":"claude-x","content":[{"type":"text","text":"ok"}],"usage":{"output_tokens":5}}}`,
`{"type":"user","uuid":"u2","timestamp":"2026-06-20T00:01:00Z","message":{"role":"user","content":"second"}}`,
}, "\n") + "\n"
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestRun_ImportsAndIsIdempotent(t *testing.T) {
t.Parallel()
repo, repoDir := initRepoWithCommit(t)
claudeDir := t.TempDir()
writeFixtureSession(t, claudeDir, "sess1.jsonl")
opts := Options{RepoRoot: repoDir, OverridePath: claudeDir, Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC)}
res, err := Run(context.Background(), repo, opts)
if err != nil {
t.Fatal(err)
}
if res.TurnsImported != 2 {
t.Fatalf("want 2 imported, got %+v", res)
}
res2, err := Run(context.Background(), repo, opts)
if err != nil {
t.Fatal(err)
}
if res2.TurnsImported != 0 || res2.TurnsSkipped != 2 {
t.Fatalf("re-run not idempotent: %+v", res2)
}
stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{Refs: ptrRefs(cp.ImportsRefs())})
if err != nil {
t.Fatal(err)
}
infos, err := stores.Persistent.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(infos) != 2 || !infos[0].Imported {
t.Fatalf("expected 2 imported on imports ref, got %+v", infos)
}
v1, err := cp.Open(context.Background(), repo, cp.OpenOptions{})
if err != nil {
t.Fatal(err)
}
v1infos, err := v1.Persistent.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(v1infos) != 0 {
t.Fatalf("imports leaked onto v1: %+v", v1infos)
}
}
func TestRun_DryRunWritesNothing(t *testing.T) {
t.Parallel()
repo, repoDir := initRepoWithCommit(t)
claudeDir := t.TempDir()
writeFixtureSession(t, claudeDir, "sess1.jsonl")
res, err := Run(context.Background(), repo, Options{
RepoRoot: repoDir, OverridePath: claudeDir, DryRun: true,
Now: time.Date(2026, 6, 25, 0, 0, 0, 0, time.UTC),
})
if err != nil {
t.Fatal(err)
}
if res.TurnsImported != 2 {
t.Fatalf("dry-run should count 2 turns, got %+v", res)
}
stores, err := cp.Open(context.Background(), repo, cp.OpenOptions{Refs: ptrRefs(cp.ImportsRefs())})
if err != nil {
t.Fatal(err)
}
infos, err := stores.Persistent.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(infos) != 0 {
t.Fatalf("dry-run must not write, got %+v", infos)
}
}