import: address review — pi branch-safe model, copilot streaming scan · Entire
import: address review — pi branch-safe model, copilot streaming scan
be12306→main·
computermode·3w ago·3 files·+44 added/-6 removed
- pi: extract the model over the [0,end) prefix (the same buffer token usage uses) instead of the [start,end) slice. Pi's branch-aware ExtractModel walks parentId back to the root, so the slice must keep the file's beginning; truncating only the end preserves those chains while still bounding the turn. Fixes wrong/empty per-turn model on branched sessions and when a turn's own assistant message omits the model. Adds a regression test.
- copilot: scan events.jsonl line-by-line and stop at the first session.start instead of reading the whole file and allocating via splitRawLines.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Sessions
0b1a3d9f7f74View transcript
Changes
3
cmd/entire/cli/agentimport
Mcopilot.go+10/-3
Mpi.go+8/-3
Mpi_test.go+26
1
2
3
4
5
6
7
66 unmodified lines
74
75
76
76
77
78
79
80
80
81
82
83
84
85
86
87
88
89
90
3 unmodified lines
94
95
96
90
97
98
99
100
package agentimport
import (
"bufio"
"context"
"encoding/json"
"fmt"
66 unmodified lines
// it in this repo (gitRoot or cwd is the repo root or a descendant). Sessions
// whose location can't be determined are treated as not belonging to the repo.
func copilotSessionInRepo(path, repoRoot string) bool {
data, err := os.ReadFile(path) //nolint:gosec // path discovered under the configured session dir
f, err := os.Open(path) //nolint:gosec // path discovered under the configured session dir
if err != nil {
return false
}
for _, raw := range splitRawLines(data) {
defer func() { _ = f.Close() }()
// Scan line-by-line and stop at the first session.start (normally line 0)
// rather than slurping the whole transcript, which can be large.
scanner := bufio.NewScanner(f)
scanner.Buffer(make([]byte, 0, bufio.MaxScanTokenSize), 10*1024*1024) // 10 MB max line
for scanner.Scan() {
var evt struct {
Type string `json:"type"`
Data struct {
3 unmodified lines
} `json:"context"`
} `json:"data"`
}
if err := json.Unmarshal(raw, &evt); err != nil || evt.Type != "session.start" {
if err := json.Unmarshal(scanner.Bytes(), &evt); err != nil || evt.Type != "session.start" {
continue
}
return repoMatches(evt.Data.Context.GitRoot, repoRoot) || repoMatches(evt.Data.Context.Cwd, repoRoot)
Mcmd/entire/cli/agentimport/copilot.go+10/-3
93 unmodified lines
94
95
96
97
98
97
98
99
100
101
102
103
104
105
106
107
108
104
109
110
111
112
93 unmodified lines
end = starts[k+1]
}
// Bound token usage to [start, end): truncate to the first `end` lines,
// then let the agent helper slice from `start`.
// Bound this turn to [start, end) by truncating to the first `end` lines
// (keeping the file from line 0). Pi's branch-aware helpers resolve the
// active branch by walking parentId back to the root, so the prefix MUST
// retain the beginning — truncating the end is safe (parents are always
// earlier lines) but slicing off the start would break those chains.
// CalculateTokenUsage then slices forward from `start`; ExtractModel
// reports the active-branch model as of this turn's end.
truncated := joinLines(rawLines[:end])
tokens, err := ag.CalculateTokenUsage(truncated, start)
if err != nil {
return nil, fmt.Errorf("token usage for turn %d: %w", k, err)
}
model, mErr := ag.ExtractModel(joinLines(rawLines[start:end]))
model, mErr := ag.ExtractModel(truncated)
if mErr != nil {
model = ""
}
Mcmd/entire/cli/agentimport/pi.go+8/-3
79 unmodified lines
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
79 unmodified lines
}
}
// TestPiSplitTurns_ModelInheritedOverPrefix guards the branch-resolution fix:
// a later turn whose own assistant message omits the model must still resolve
// the model from an earlier active-branch message. This only works when the
// model is extracted over the [0,end) prefix (chains intact); extracting over
// the [start,end) slice would strip the earlier model and yield "".
func TestPiSplitTurns_ModelInheritedOverPrefix(t *testing.T) {
t.Parallel()
full := []byte(strings.Join([]string{
`{"type":"message","id":"pu1","message":{"role":"user","content":"first"}}`,
`{"type":"message","id":"pa1","message":{"role":"assistant","content":[{"type":"text","text":"ok"}],"model":"model-A","usage":{"input":10,"output":5}}}`,
`{"type":"message","id":"pu2","message":{"role":"user","content":"second"}}`,
`{"type":"message","id":"pa2","message":{"role":"assistant","content":[{"type":"text","text":"done"}],"usage":{"input":20,"output":7}}}`,
}, "\n") + "\n")
turns, err := piImporter{}.SplitTurns(SessionFile{Path: filepath.Join(t.TempDir(), "s.jsonl"), SessionID: "s"}, full)
if err != nil {
t.Fatal(err)
}
if len(turns) != 2 {
t.Fatalf("want 2 turns, got %d", len(turns))
}
if turns[1].Model != "model-A" {
t.Errorf("turn1 model = %q, want model-A inherited from the active branch", turns[1].Model)
}
}
func TestPiSplitTurns_ToolResultIsNotATurn(t *testing.T) {
t.Parallel()
// A toolResult-role message is not a user prompt and must not start a turn.
Mcmd/entire/cli/agentimport/pi_test.go+26