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

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)
    }
}