Merge pull request #1710 from entireio/fix/329-subagent-extract · Entire
Log in
Merge pull request #1710 from entireio/fix/329-subagent-extract
0e05d1d→main·
peyton-alt·2d ago·18 files·+1,324 added/-31 removed
fix(attribution): scan full transcript for subagents spawned before checkpoint
Changes
18
cmd/entire/cli
agent
Magent.go+19/-1
claudecode
Mtranscript.go+44/-6
Mtranscript_test.go+75
factoryaidroid
Mtranscript.go+40/-3
Mtranscript_test.go+70
types
Mtoken_usage.go+32
agentimport
Magentimport.go+17/-1
Mclaude.go+6/-4
Mfactory.go+4/-2
Mlinesplit.go+63/-1
Asubagent_tokens_test.go+232
session
Mstate.go+42/-1
Msession_adopt.go+8
Msession_adopt_test.go+61
strategy
Mmanual_commit_condensation.go+37/-8
Mmanual_commit_git.go+58/-2
Mmanual_commit_hooks.go+1/-2
Asubagent_token_dedup_test.go+515
360 unmodified lines
361
362
363
364
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
360 unmodified lines
ExtractAllModifiedFiles(transcriptData []byte, fromOffset int, subagentsDir string) ([]string, error)
// CalculateTotalTokenUsage computes token usage including all spawned subagents.
// The subagentsDir parameter specifies where subagent transcripts are stored.
// The subagentsDir parameter specifies where subagent transcripts are stored
// (an empty subagentsDir skips subagent accounting and leaves SubagentTokens nil).
//
// CONTRACT — the returned SubagentTokens is a CUMULATIVE-SINCE-SESSION-START
// snapshot, NOT a delta scoped to fromOffset like the main-agent fields
// (InputTokens/OutputTokens/...). Implementations MUST discover spawned agent
// IDs from the FULL transcript prefix [0,end) — so a subagent spawned before\
// fromOffset is still found (#329) — and re-read each subagent transcript from\
// line 0 on every call. Consequently a subagent's full total repeats on every\
// call after it is first discovered.\
//\
// Callers that accumulate across checkpoints/turns therefore MUST NOT sum\
// SubagentTokens across calls: replace the running total with the latest\
// snapshot, and rescope any window delta by subtracting a previously captured\
// baseline (see accumulateTokenUsage / resetCheckpointWindow and\
// session.State.SubagentTokensBaseline in cmd/entire/cli/strategy, and\
// rescopeSubagentTokensToDeltas in cmd/entire/cli/agentimport for the import\
// path). An implementation that instead returned per-window deltas would\
// silently break that accounting with no compile-time or test signal.\
CalculateTotalTokenUsage(transcriptData []byte, fromOffset int, subagentsDir string) (*TokenUsage, error)\
}\
```\
\
Mcmd/entire/cli/agent/agent.go+19/-1\
\
```\
394 unmodified lines\
\
395\
396\
397\
398\
399\
398\
399\
400\
401\
401\
402\
402\
403\
404\
405\
406\
407\
408\
409\
410\
411\
412\
413\
414\
415\
416\
417\
418\
419\
420\
421\
422\
423\
424\
425\
426\
427\
428\
429\
430\
431\
43 unmodified lines\
\
475\
476\
477\
452\
453\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
494\
495\
496\
497\
\
394 unmodified lines\
\
// Calculate token usage from parsed transcript\
mainUsage := CalculateTokenUsage(parsed)\
\
// Extract spawned agent IDs from the same parsed transcript\
agentIDs := ExtractSpawnedAgentIDs(parsed)\
if subagentsDir == "" {\
return mainUsage, nil\
}\
\
// Calculate subagent token usage (skip when subagentsDir is empty to avoid reading from cwd)\
if len(agentIDs) > 0 && subagentsDir != "" {\
// Extract spawned agent IDs from the FULL transcript (startLine=0), not the\
// sliced portion. A subagent spawned before this checkpoint's startLine can\
// keep writing to its transcript in later turns; scanning only the slice\
// would miss it and undercount subagent token usage (#329).\
//\
// PERF (considered, retained deliberately): this re-parses the full\
// transcript in addition to the sliced parse above — two JSONL parses per\
// call, growing with session length. A single-pass version was rejected as\
// not worth the risk: ParseFromBytes silently drops malformed lines, so a\
// parsed-entry index does not correspond to a raw line number and naively\
// slicing the full parse at startLine would misattribute main-agent usage;\
// doing it safely would mean threading raw-line numbers through the shared\
// transcript parser used by every agent. A cheap line scan for the Task\
// marker instead of a full parse would duplicate ExtractSpawnedAgentIDs'\
// nested tool_result decoding. The common no-subagent case already avoids\
// this cost entirely via the subagentsDir == "" short-circuit above.\
fullParsed, err := transcript.ParseFromBytes(transcriptData)\
if err != nil {\
return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
}\
agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
\
// Calculate subagent token usage. This re-reads each subagent transcript from\
// line 0 on every call, so mainUsage.SubagentTokens is a cumulative-since-\
// session-start snapshot — see the CalculateTotalTokenUsage interface contract\
// in cmd/entire/cli/agent for how callers must accumulate it.\
if len(agentIDs) > 0 {\
subagentUsage := &agent.TokenUsage{}\
for agentID := range agentIDs {\
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
43 unmodified lines\
\
}\
}\
\
// Find spawned subagents and collect their modified files (skip when subagentsDir is empty to avoid reading from cwd)\
agentIDs := ExtractSpawnedAgentIDs(parsed)\
if subagentsDir == "" {\
return files, nil\
}\
\
// Find spawned subagents from the FULL transcript (startLine=0): a subagent\
// spawned before this checkpoint's startLine may keep modifying files in\
// later turns, and scanning only the slice would miss it (#329). Main-agent\
// file extraction above stays scoped to the slice.\
//\
// PERF: the second full-transcript parse is retained deliberately for the\
// same reasons documented on CalculateTotalTokenUsage above; the common\
// no-subagent case is short-circuited by the subagentsDir == "" guard.\
fullParsed, err := transcript.ParseFromBytes(transcriptData)\
if err != nil {\
return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
}\
agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
for agentID := range agentIDs {\
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
agentLines, agentErr := transcript.ParseFromFileAtLine(agentPath, 0)\
```\
\
Mcmd/entire/cli/agent/claudecode/transcript.go+44/-6\
\
```\
886 unmodified lines\
\
887\
888\
889\
890\
891\
892\
893\
894\
895\
896\
897\
898\
899\
900\
901\
902\
903\
904\
905\
906\
907\
908\
909\
910\
911\
912\
913\
914\
915\
916\
917\
918\
919\
920\
921\
922\
923\
924\
925\
926\
927\
928\
929\
930\
931\
932\
933\
934\
935\
936\
937\
938\
939\
940\
941\
942\
943\
944\
945\
946\
947\
948\
949\
950\
951\
952\
953\
954\
955\
956\
957\
958\
959\
960\
961\
962\
963\
964\
\
886 unmodified lines\
\
t.Errorf("missing expected file %q", f)\
}\
}\
\
// Regression for #329: a subagent spawned BEFORE the checkpoint's startLine\
// must still be discovered, because it can keep modifying files in later turns.\
// The Task spawn/result live in lines before startLine; only the full transcript\
// scan finds them.\
func TestExtractAllModifiedFiles_FindsSubagentSpawnedBeforeStartLine(t *testing.T) {\
t.Parallel()\
\
tmpDir := t.TempDir()\
subagentsDir := tmpDir + "/tasks/toolu_task1"\
c := &ClaudeCodeAgent{}\
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
t.Fatalf("failed to create subagents dir: %v", err)\
}\
\
transcriptData := buildJSONL(\
makeTaskToolUseLine(t, "a1", "toolu_taskA"), // line 0 (before startLine)\
makeTaskResultLine(t, "uA", "toolu_taskA", "subA"), // line 1 (before startLine)\
makeWriteToolLine(t, "a2", "/repo/main.go"), // line 2 (>= startLine)\
)\
writeJSONLFile(t, subagentsDir+"/agent-subA.jsonl",\
makeWriteToolLine(t, "sa1", "/repo/helper.go"),\
)\
\
files, err := c.ExtractAllModifiedFiles(transcriptData, 2, subagentsDir)\
if err != nil {\
t.Fatalf("ExtractAllModifiedFiles() error: %v", err)\
}\
\
got := make(map[string]bool, len(files))\
for _, f := range files {\
got[f] = true\
}\
if !got["/repo/main.go"] {\
t.Errorf("missing main-agent file /repo/main.go: %v", files)\
}\
if !got["/repo/helper.go"] {\
t.Errorf("subagent spawned before startLine was not discovered; missing /repo/helper.go: %v", files)\
}\
}\
\
// Regression for #329: subagent token usage must be counted even when the\
// subagent was spawned before the checkpoint's startLine.\
func TestCalculateTotalTokenUsage_CountsSubagentSpawnedBeforeStartLine(t *testing.T) {\
t.Parallel()\
\
tmpDir := t.TempDir()\
subagentsDir := tmpDir + "/tasks/toolu_task1"\
c := &ClaudeCodeAgent{}\
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
t.Fatalf("failed to create subagents dir: %v", err)\
}\
\
// Subagent spawned in lines 0-1 (before startLine=2); main usage on line 2.\
transcriptData := buildJSONL(\
makeTaskToolUseLine(t, "a1", "toolu_taskB"),\
makeTaskResultLine(t, "uB", "toolu_taskB", "subB"),\
`{"type":"assistant","uuid":"a2","message":{"id":"m2","usage":{"input_tokens":300,"output_tokens":150}}}`,\
)\
writeJSONLFile(t, subagentsDir+"/agent-subB.jsonl",\
`{"type":"assistant","uuid":"sa1","message":{"id":"sm1","usage":{"input_tokens":50,"output_tokens":25}}}`,\
)\
\
usage, err := c.CalculateTotalTokenUsage(transcriptData, 2, subagentsDir)\
if err != nil {\
t.Fatalf("CalculateTotalTokenUsage() error: %v", err)\
}\
if usage.SubagentTokens == nil {\
t.Fatal("subagent spawned before startLine was not counted (SubagentTokens is nil)")\
}\
if usage.SubagentTokens.InputTokens != 50 || usage.SubagentTokens.OutputTokens != 25 {\
t.Errorf("subagent tokens = input %d output %d, want input 50 output 25",\
usage.SubagentTokens.InputTokens, usage.SubagentTokens.OutputTokens)\
}\
}\
```\
\
Mcmd/entire/cli/agent/claudecode/transcript\_test.go+75\
\
```\
365 unmodified lines\
\
366\
367\
368\
369\
370\
369\
370\
371\
372\
373\
374\
375\
376\
377\
378\
379\
380\
381\
382\
383\
384\
385\
386\
387\
388\
389\
390\
391\
392\
393\
394\
395\
396\
397\
35 unmodified lines\
\
433\
434\
435\
412\
436\
437\
438\
439\
440\
441\
442\
443\
444\
445\
446\
447\
448\
449\
450\
451\
452\
453\
454\
455\
\
365 unmodified lines\
\
mainUsage := CalculateTokenUsage(parsed)\
\
agentIDs := ExtractSpawnedAgentIDs(parsed)\
if len(agentIDs) > 0 && subagentsDir != "" {\
if subagentsDir == "" {\
return mainUsage, nil\
}\
\
// Extract spawned agent IDs from the FULL transcript (startLine=0): a\
// subagent spawned before this checkpoint's startLine can keep writing to\
// its transcript, so scanning only the slice would undercount it (#329).\
//\
// PERF (considered, retained deliberately): this re-parses the full\
// transcript in addition to the sliced parse above — two JSONL parses per\
// call, growing with session length. A single-pass version was rejected:\
// the Droid parser drops non-message / malformed lines, so a parsed-entry\
// index does not map to a raw line number and naively slicing the full parse\
// at startLine would misattribute main-agent usage; doing it safely would\
// mean threading raw-line numbers through the shared parser. The common\
// no-subagent case already avoids this via the subagentsDir == "" guard.\
fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)\
if err != nil {\
return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
}\
agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
// This re-reads each subagent transcript from line 0 on every call below, so\
// mainUsage.SubagentTokens ends up cumulative-since-session-start — see the\
// CalculateTotalTokenUsage interface contract in cmd/entire/cli/agent for how\
// callers must accumulate it (shared with Claude Code).\
if len(agentIDs) > 0 {\
subagentUsage := &agent.TokenUsage{}\
for agentID := range agentIDs {\
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
35 unmodified lines\
\
fileSet[f] = true\
}\
\
agentIDs := ExtractSpawnedAgentIDs(parsed)\
if subagentsDir == "" {\
return files, nil\
}\
\
// Find spawned subagents from the FULL transcript (startLine=0): a subagent\
// spawned before this checkpoint's startLine may keep modifying files in\
// later turns, and scanning only the slice would miss it (#329). Main-agent\
// file extraction above stays scoped to the slice.\
//\
// PERF: the second full-transcript parse is retained deliberately for the\
// same reasons documented on CalculateTotalTokenUsageFromBytes above; the\
// common no-subagent case is short-circuited by the subagentsDir == "" guard.\
fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)\
if err != nil {\
return nil, fmt.Errorf("failed to parse full transcript: %w", err)\
}\
agentIDs := ExtractSpawnedAgentIDs(fullParsed)\
for agentID := range agentIDs {\
agentPath := filepath.Join(subagentsDir, fmt.Sprintf("agent-%s.jsonl", agentID))\
agentLines, _, agentErr := ParseDroidTranscript(agentPath, 0)\
```\
\
Mcmd/entire/cli/agent/factoryaidroid/transcript.go+40/-3\
\
```\
1223 unmodified lines\
\
1224\
1225\
1226\
1227\
1228\
1229\
1230\
1231\
1232\
1233\
1234\
1235\
1236\
1237\
1238\
1239\
1240\
1241\
1242\
1243\
1244\
1245\
1246\
1247\
1248\
1249\
1250\
1251\
1252\
1253\
1254\
1255\
1256\
1257\
1258\
1259\
1260\
1261\
1262\
1263\
1264\
1265\
1266\
1267\
1268\
1269\
1270\
1271\
1272\
1273\
1274\
1275\
1276\
1277\
1278\
1279\
1280\
1281\
1282\
1283\
1284\
1285\
1286\
1287\
1288\
1289\
1290\
1291\
1292\
1293\
1294\
1295\
1296\
\
1223 unmodified lines\
\
t.Errorf("missing expected file %q", f)\
}\
}\
\
// Regression for #329: a subagent spawned BEFORE the checkpoint's startLine must\
// still be discovered for file extraction (it can keep modifying files later).\
func TestExtractAllModifiedFilesFromBytes_FindsSubagentSpawnedBeforeStartLine(t *testing.T) {\
t.Parallel()\
\
tmpDir := t.TempDir()\
subagentsDir := tmpDir + "/tasks/toolu_task1"\
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
t.Fatalf("failed to create subagents dir: %v", err)\
}\
\
data := joinJSONL(\
makeTaskToolUseLine(t, "a1", "toolu_taskC"), // line 0 (before startLine)\
makeTaskResultLine(t, "uC", "toolu_taskC", "sub1"), // line 1 (before startLine)\
makeWriteToolLine(t, "a2", "/repo/main.go"), // line 2 (>= startLine)\
)\
writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",\
makeWriteToolLine(t, "sa1", "/repo/helper.go"),\
)\
\
files, err := ExtractAllModifiedFilesFromBytes(data, 2, subagentsDir)\
if err != nil {\
t.Fatalf("ExtractAllModifiedFilesFromBytes() error: %v", err)\
}\
\
got := make(map[string]bool, len(files))\
for _, f := range files {\
got[f] = true\
}\
if !got["/repo/main.go"] {\
t.Errorf("missing main-agent file /repo/main.go: %v", files)\
}\
if !got["/repo/helper.go"] {\
t.Errorf("subagent spawned before startLine was not discovered; missing /repo/helper.go: %v", files)\
}\
}\
\
// Regression for #329: subagent token usage must be counted even when the\
// subagent was spawned before the checkpoint's startLine.\
func TestCalculateTotalTokenUsageFromBytes_CountsSubagentSpawnedBeforeStartLine(t *testing.T) {\
t.Parallel()\
\
tmpDir := t.TempDir()\
subagentsDir := tmpDir + "/tasks/toolu_task1"\
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
t.Fatalf("failed to create subagents dir: %v", err)\
}\
\
data := joinJSONL(\
makeTaskToolUseLine(t, "a1", "toolu_taskD"), // line 0 (before startLine)\
makeTaskResultLine(t, "uD", "toolu_taskD", "sub1"), // line 1 (before startLine)\
makeAssistantTokenLine(t, "a2", "msg_main", 300, 150), // line 2\
)\
writeJSONLFile(t, subagentsDir+"/agent-sub1.jsonl",\
makeAssistantTokenLine(t, "sa1", "msg_sub", 50, 25),\
)\
\
usage, err := CalculateTotalTokenUsageFromBytes(data, 2, subagentsDir)\
if err != nil {\
t.Fatalf("CalculateTotalTokenUsageFromBytes() error: %v", err)\
}\
if usage.SubagentTokens == nil {\
t.Fatal("subagent spawned before startLine was not counted (SubagentTokens is nil)")\
}\
if usage.SubagentTokens.InputTokens != 50 || usage.SubagentTokens.OutputTokens != 25 {\
t.Errorf("subagent tokens = input %d output %d, want input 50 output 25",\
usage.SubagentTokens.InputTokens, usage.SubagentTokens.OutputTokens)\
}\
}\
```\
\
Mcmd/entire/cli/agent/factoryaidroid/transcript\_test.go+70\
\
```\
44 unmodified lines\
\
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\
\
44 unmodified lines\
\
sum.SubagentTokens = AddTokenUsage(aSub, bSub)\
return sum\
}\
\
// SubtractTokenUsage returns a-b, recursing into subagent usage and clamping\
// every field at zero (a nil operand is treated as zero). Neither input is\
// mutated. Used to rescope a cumulative-since-session-start snapshot (e.g.\
// subagent token usage, which is always re-read from the start of each\
// subagent transcript) down to a delta since a previously captured baseline.\
func SubtractTokenUsage(a, b *TokenUsage) *TokenUsage {\
if a == nil {\
return nil\
}\
if b == nil {\
b = &TokenUsage{}\
}\
diff := &TokenUsage{\
InputTokens: clampSubtract(a.InputTokens, b.InputTokens),\
CacheCreationTokens: clampSubtract(a.CacheCreationTokens, b.CacheCreationTokens),\
CacheReadTokens: clampSubtract(a.CacheReadTokens, b.CacheReadTokens),\
OutputTokens: clampSubtract(a.OutputTokens, b.OutputTokens),\
APICallCount: clampSubtract(a.APICallCount, b.APICallCount),\
}\
diff.SubagentTokens = SubtractTokenUsage(a.SubagentTokens, b.SubagentTokens)\
return diff\
}\
\
// clampSubtract returns a-b, floored at zero so a stale or racy baseline\
// never produces a negative delta.\
func clampSubtract(a, b int) int {\
if a < b {\
return 0\
}\
return a - b\
}\
```\
\
Mcmd/entire/cli/agent/types/token\_usage.go+32\
\
```\
43 unmodified lines\
\
44\
45\
46\
47\
47\
48\
49\
50\
51\
52\
53\
54\
55\
56\
57\
58\
59\
164 unmodified lines\
\
224\
225\
226\
227\
228\
229\
230\
231\
232\
233\
234\
235\
236\
\
43 unmodified lines\
\
UUID string\
Prompt, Model string\
CreatedAt time.Time\
Tokens *types.TokenUsage\
// Tokens is this turn's token usage. Every field is a per-turn delta:\
// main-agent fields are scoped to the turn's [LineStart, LineEnd) slice by\
// the token helpers, and SubagentTokens is rescoped from the cumulative\
// snapshot those helpers return to a per-turn increment by\
// rescopeSubagentTokensToDeltas (see linesplit.go). That invariant lets\
// callers sum turns freely: writeSessionState sums them for the session\
// total and each imported checkpoint stores its own turn's delta, so a\
// subagent's tokens are counted exactly once rather than re-added on every\
// turn after it is discovered.\
Tokens *types.TokenUsage\
}\
\
// Importer is the per-agent seam: it locates an agent's transcripts for a repo\
164 unmodified lines\
\
if turn.Model != "" {\
model = turn.Model\
}\
// turn.Tokens holds per-turn deltas for every field, including\
// SubagentTokens (rescoped from a cumulative snapshot in\
// rescopeSubagentTokensToDeltas — see the Turn.Tokens doc). Summing\
// them therefore yields the correct session total: main-agent fields\
// add up, and the subagent deltas sum back to the final cumulative\
// subagent snapshot exactly once instead of being multiplied by the\
// number of turns after each subagent was first discovered.\
tokens = types.AddTokenUsage(tokens, turn.Tokens)\
}\
if started.IsZero() {\
```\
\
Mcmd/entire/cli/agentimport/agentimport.go+17/-1\
\
```\
31 unmodified lines\
\
32\
33\
34\
35\
36\
37\
38\
35\
36\
37\
38\
39\
40\
41\
42\
43\
\
31 unmodified lines\
\
return discoverSessionFiles(dir, now, sessionFilter, jsonlSessionResolver(".jsonl", identitySessionID))\
}\
\
// SplitTurns produces one Turn per user-prompt line. Token usage for each turn\
// is computed on the slice [LineStart, LineEnd) so turns don't double-count\
// later turns. tool_result lines (Type == "user" but no text content) do not\
// start a turn.\
// SplitTurns produces one Turn per user-prompt line. Main-agent token usage for\
// each turn is computed on the slice [LineStart, LineEnd) so turns don't\
// double-count later turns; subagent token usage is discovered from the full\
// prefix and rescoped to a per-turn delta by splitLineTurns (see\
// rescopeSubagentTokensToDeltas). tool_result lines (Type == "user" but no text\
// content) do not start a turn.\
func (claudeImporter) SplitTurns(sf SessionFile, full []byte) ([]Turn, error) {\
subagentsDir := filepath.Join(filepath.Dir(sf.Path), sf.SessionID, "subagents")\
ag := &claudecode.ClaudeCodeAgent{}\
```\
\
Mcmd/entire/cli/agentimport/claude.go+6/-4\
\
```\
33 unmodified lines\
\
34\
35\
36\
37\
38\
37\
38\
39\
40\
41\
42\
43\
\
33 unmodified lines\
\
}\
\
// SplitTurns produces one Turn per user-prompt envelope, bounded by the next.\
// Token usage (including spawned subagents) is delegated to the Factory agent;\
// the model is read once from the session's adjacent settings file. Droid\
// Token usage is delegated to the Factory agent; spawned-subagent usage comes\
// back as a cumulative snapshot and is rescoped to a per-turn delta by\
// splitLineTurns (see rescopeSubagentTokensToDeltas). The model is read once\
// from the session's adjacent settings file. Droid\
// envelopes carry no per-message timestamp (the agent stamps events with\
// time.Now() at hook time), so every turn falls back to the transcript file's\
// modtime — the same fallback the Gemini importer uses.\
```\
\
Mcmd/entire/cli/agentimport/factory.go+4/-2\
\
```\
1\
2\
3\
3\
4\
5\
6\
7\
8\
9\
10\
45 unmodified lines\
\
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\
\
package agentimport\
\
import "time"\
import (\
"time"\
\
"github.com/entireio/cli/cmd/entire/cli/agent/types"\
)\
\
// parseTimestamp parses an RFC3339 timestamp, returning the zero time when the\
// string is empty or unparseable. Shared by the importers that read a per-turn\
45 unmodified lines\
\
turn.LineStart, turn.LineEnd = start, end\
turns = append(turns, *turn)\
}\
rescopeSubagentTokensToDeltas(turns)\
return turns, nil\
}\
\
// rescopeSubagentTokensToDeltas converts each turn's SubagentTokens from the\
// cumulative-since-session-start snapshot the token helpers return into the\
// per-turn increment (this turn's cumulative minus the previous turn's).\
//\
// The subagent-aware token helpers (claudecode/factoryaidroid\
// CalculateTotalTokenUsage) discover spawned agent IDs from the full transcript\
// prefix [0,end) — so a subagent spawned before the current turn is still found\
// (#329) — and re-read each agent-<id>.jsonl from line 0. That makes a turn's\
// SubagentTokens a cumulative snapshot that repeats every already-discovered\
// subagent's full total on every later turn, unlike the main-agent fields\
// (InputTokens/OutputTokens/...), which are scoped to the turn's own\
// [start,end) slice and are genuine per-turn deltas.\
//\
// Both import consumers sum per-turn token usage: writeSessionState folds the\
// turns together with AddTokenUsage for the session total, and every imported\
// checkpoint stores its turn's TokenUsage (downstream consumers sum those).\
// Summing the cumulative snapshot multiplies a subagent's tokens by the number\
// of turns after it is first discovered (trail finding 019f5ea3). Rescoping to\
// per-turn deltas fixes both without special-casing either: each checkpoint\
// carries only the subagent usage attributable to its turn, and summing the\
// deltas reconstructs the final cumulative total exactly once.\
//\
// This mirrors the live path, which keeps the latest cumulative snapshot in\
// state.TokenUsage (accumulateTokenUsage replaces rather than adds\
// SubagentTokens) and rescopes each checkpoint window to "cumulative minus a\
// captured baseline" via types.SubtractTokenUsage and\
// SessionState.SubagentTokensBaseline (see cmd/entire/cli/strategy). Here the\
// baseline for turn k is turn k-1's cumulative snapshot. The cumulative is\
// monotonic non-decreasing across turns (discovered-agent set only grows and\
// each subagent file total is fixed), so the clamped subtraction is exact and\
// the deltas sum back to the final snapshot.\
//\
// Turns without a discovered subagent have a nil SubagentTokens and are left\
// untouched, so this is a no-op for the non-subagent-aware importers that route\
// through splitLineTurns (cursor/pi/codex/copilot).\
func rescopeSubagentTokensToDeltas(turns []Turn) {\
var prevCumulative *types.TokenUsage\
for i := range turns {\
if turns[i].Tokens == nil {\
continue\
}\
cumulative := turns[i].Tokens.SubagentTokens\
turns[i].Tokens.SubagentTokens = types.SubtractTokenUsage(cumulative, prevCumulative)\
// Only advance the baseline when this turn carried a snapshot. A turn\
// whose agent-<id>.jsonl transiently failed to read has a nil cumulative\
// (CalculateTotalTokenUsage continue-s past the error); resetting\
// prevCumulative to nil here would make the next non-nil snapshot subtract\
// nothing and re-report the full cumulative, reintroducing the\
// double-counting this rescoping removes. Mirrors the live path, where\
// accumulateTokenUsage only replaces SubagentTokens when the incoming\
// snapshot is non-nil.\
if cumulative != nil {\
prevCumulative = cumulative\
}\
}\
}\
```\
\
Mcmd/entire/cli/agentimport/linesplit.go+63/-1\
\
```\
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\
132\
133\
134\
135\
136\
137\
138\
139\
140\
141\
142\
143\
144\
145\
146\
147\
148\
149\
150\
151\
152\
153\
154\
155\
156\
157\
158\
159\
160\
161\
162\
163\
164\
165\
166\
167\
168\
169\
170\
171\
172\
173\
174\
175\
176\
177\
178\
179\
180\
181\
182\
183\
184\
185\
186\
187\
188\
189\
190\
191\
192\
193\
194\
195\
196\
197\
198\
199\
200\
201\
202\
203\
204\
205\
206\
207\
208\
209\
210\
211\
212\
213\
214\
215\
216\
217\
218\
219\
220\
221\
222\
223\
224\
225\
226\
227\
228\
229\
230\
231\
232\
\
package agentimport\
\
import (\
"context"\
"os"\
"path/filepath"\
"strings"\
"testing"\
\
"github.com/entireio/cli/cmd/entire/cli/agent/types"\
)\
\
// These regression tests pin the trail-817 fix: the subagent-aware importers\
// (Claude Code, Factory AI Droid) get their per-turn SubagentTokens as a\
// cumulative-since-session-start snapshot (agent IDs are discovered from the\
// full transcript prefix and each subagent transcript is re-read from line 0),\
// so a subagent spawned in an early turn repeats its full total on every later\
// turn. Both import consumers sum per-turn usage — writeSessionState for the\
// session total and the per-imported-checkpoint TokenUsage that downstream sums\
// — so before the fix a subagent's tokens were multiplied by the number of\
// turns after it was first discovered. rescopeSubagentTokensToDeltas\
// (linesplit.go) rescopes those snapshots to per-turn deltas so the total is\
// counted exactly once. Reverting that call makes both assertions below fail.\
\
const (\
// The single spawned subagent's on-disk transcript totals, asserted to be\
// counted exactly once across a 3-turn session.\
wantSubagentInput = 50\
wantSubagentOutput = 25\
wantSubagentCalls = 1\
\
// Main-agent totals summed across the three turns (per-slice deltas), kept\
// intact by the fix.\
wantMainInput = 600 // 100 + 200 + 300\
wantMainOutput = 300 // 50 + 100 + 150\
wantMainCalls = 3\
)\
\
// sumTurnSubagentTokens sums each turn's SubagentTokens the way both the session\
// total and any downstream sum of per-checkpoint TokenUsage would. With the fix\
// the turns hold per-turn deltas, so this reconstructs the subagent total once;\
// without it each turn holds the cumulative snapshot and this multiplies.\
func sumTurnSubagentTokens(turns []Turn) types.TokenUsage {\
var sum types.TokenUsage\
for _, tr := range turns {\
if tr.Tokens == nil || tr.Tokens.SubagentTokens == nil {\
continue\
}\
s := tr.Tokens.SubagentTokens\
sum.InputTokens += s.InputTokens\
sum.CacheCreationTokens += s.CacheCreationTokens\
sum.CacheReadTokens += s.CacheReadTokens\
sum.OutputTokens += s.OutputTokens\
sum.APICallCount += s.APICallCount\
}\
return sum\
}\
\
func assertSubagentCountedOnce(t *testing.T, label string, got *types.TokenUsage) {\
t.Helper()\
if got == nil {\
t.Fatalf("%s: SubagentTokens is nil, want input=%d output=%d calls=%d",\
label, wantSubagentInput, wantSubagentOutput, wantSubagentCalls)\
}\
if got.InputTokens != wantSubagentInput || got.OutputTokens != wantSubagentOutput ||\
got.APICallCount != wantSubagentCalls {\
t.Errorf("%s: subagent tokens counted more than once: got input=%d output=%d calls=%d, "+\
"want input=%d output=%d calls=%d (cumulative snapshot summed across turns)",\
label, got.InputTokens, got.OutputTokens, got.APICallCount,\
wantSubagentInput, wantSubagentOutput, wantSubagentCalls)\
}\
}\
\
func writeSubagentTranscript(t *testing.T, sf SessionFile, agentID, line string) {\
t.Helper()\
subagentsDir := filepath.Join(filepath.Dir(sf.Path), sf.SessionID, "subagents")\
if err := os.MkdirAll(subagentsDir, 0o755); err != nil {\
t.Fatalf("mkdir subagents dir: %v", err)\
}\
agentPath := filepath.Join(subagentsDir, "agent-"+agentID+".jsonl")\
if err := os.WriteFile(agentPath, []byte(line+"\n"), 0o600); err != nil {\
t.Fatalf("write subagent transcript: %v", err)\
}\
}\
\
// TestRescopeSubagentTokensToDeltas_NilCumulativeThenReappears pins finding\
// 019f5ebc-cf27: when a turn's cumulative SubagentTokens snapshot is transiently\
// nil (the subagent's agent-<id>.jsonl failed to read, so CalculateTotalTokenUsage\
// continue-d past it and left SubagentTokens nil) and a later turn's snapshot\
// reappears non-nil, prevCumulative must NOT be reset to nil for the nil turn —\
// otherwise SubtractTokenUsage(cumulative, nil) on the reappearing turn returns\
// the full cumulative again and reintroduces the double-counting the PR fixes.\
// The deltas must still sum to the final cumulative exactly once.\
func TestRescopeSubagentTokensToDeltas_NilCumulativeThenReappears(t *testing.T) {\
turns := []Turn{\
{Tokens: &types.TokenUsage{InputTokens: 10, SubagentTokens: &types.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1}}},\
// Transient read failure: main-agent tokens present, subagent snapshot nil.\
{Tokens: &types.TokenUsage{InputTokens: 20}},\
// Snapshot reappears, having grown to 300/150.\
{Tokens: &types.TokenUsage{InputTokens: 30, SubagentTokens: &types.TokenUsage{InputTokens: 300, OutputTokens: 150, APICallCount: 3}}},\
}\
\
rescopeSubagentTokensToDeltas(turns)\
\
// Turn 0 delta = 100-0 = 100.\
if turns[0].Tokens.SubagentTokens == nil || turns[0].Tokens.SubagentTokens.InputTokens != 100 {\
t.Fatalf("turn0 subagent delta = %#v, want input=100", turns[0].Tokens.SubagentTokens)\
}\
// Turn 1 had a nil snapshot: its delta stays nil.\
if turns[1].Tokens.SubagentTokens != nil {\
t.Fatalf("turn1 subagent delta = %#v, want nil", turns[1].Tokens.SubagentTokens)\
}\
// Turn 2 delta must be rescoped against turn 0's cumulative (100), NOT nil:\
// 300-100 = 200, not the full 300.\
if turns[2].Tokens.SubagentTokens == nil || turns[2].Tokens.SubagentTokens.InputTokens != 200 {\
t.Fatalf("turn2 subagent delta = %#v, want input=200 (300 cumulative minus turn0 baseline 100)",\
turns[2].Tokens.SubagentTokens)\
}\
\
// The per-turn deltas must sum to the final cumulative (300) exactly once.\
sum := sumTurnSubagentTokens(turns)\
if sum.InputTokens != 300 || sum.OutputTokens != 150 || sum.APICallCount != 3 {\
t.Fatalf("summed subagent deltas = input=%d output=%d calls=%d, want 300/150/3 (counted once)",\
sum.InputTokens, sum.OutputTokens, sum.APICallCount)\
}\
}\
\
// TestImport_ClaudeSubagentTokensCountedOnceAcrossTurns builds a Claude session\
// where a subagent is spawned in the first turn and two more user-prompt turns\
// follow, then asserts the subagent's tokens are counted exactly once both in\
// the summed per-turn/per-checkpoint usage and in the imported session total.\
func TestImport_ClaudeSubagentTokensCountedOnceAcrossTurns(t *testing.T) {\
importRepo(t) // chdir into a repo for session-state storage; no t.Parallel (t.Chdir)\
\
dir := t.TempDir()\
sf := SessionFile{Path: filepath.Join(dir, "s.jsonl"), SessionID: "s"}\
\
// Turn 1 spawns subagent "subX" (Task tool_use + tool_result carrying the\
// agentId), then three user-prompt turns each with their own assistant\
// usage. The tool_result line is type "user" but has no text, so it does\
// not start a turn.\
full := []byte(strings.Join([]string{\
`{"type":"user","uuid":"u1","message":{"role":"user","content":"first"}}`,\
`{"type":"assistant","uuid":"a0","message":{"content":[{"type":"tool_use","id":"toolu_task1","name":"Task","input":{"prompt":"go"}}]}}`,\
`{"type":"user","uuid":"r1","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_task1","content":"agentId: subX"}]}}`,\
`{"type":"assistant","uuid":"a1","message":{"id":"m1","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":100,"output_tokens":50}}}`,\
`{"type":"user","uuid":"u2","message":{"role":"user","content":"second"}}`,\
`{"type":"assistant","uuid":"a2","message":{"id":"m2","content":[{"type":"text","text":"ok2"}],"usage":{"input_tokens":200,"output_tokens":100}}}`,\
`{"type":"user","uuid":"u3","message":{"role":"user","content":"third"}}`,\
`{"type":"assistant","uuid":"a3","message":{"id":"m3","content":[{"type":"text","text":"ok3"}],"usage":{"input_tokens":300,"output_tokens":150}}}`,\
}, "\n") + "\n")\
if err := os.WriteFile(sf.Path, full, 0o600); err != nil {\
t.Fatalf("write transcript: %v", err)\
}\
writeSubagentTranscript(t, sf, "subX",\
`{"type":"assistant","uuid":"sa1","message":{"id":"sm1","content":[{"type":"text","text":"sub"}],"usage":{"input_tokens":50,"output_tokens":25}}}`)\
\
turns, err := claudeImporter{}.SplitTurns(sf, full)\
if err != nil {\
t.Fatalf("SplitTurns: %v", err)\
}\
assertSubagentTurns(t, claudeImporter{}, sf, turns)\
}\
\
// TestImport_FactorySubagentTokensCountedOnceAcrossTurns is the Factory AI Droid\
// analogue: Droid envelopes, subagent spawned in the first turn, three prompt\
// turns, subagent tokens counted exactly once.\
func TestImport_FactorySubagentTokensCountedOnceAcrossTurns(t *testing.T) {\
importRepo(t)\
\
dir := t.TempDir()\
sf := SessionFile{Path: filepath.Join(dir, "s.jsonl"), SessionID: "s"}\
\
full := []byte(strings.Join([]string{\
`{"type":"message","id":"u1","message":{"role":"user","content":"first"}}`,\
`{"type":"message","id":"a0","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_task1","name":"Task","input":{"prompt":"go"}}]}}`,\
`{"type":"message","id":"r1","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_task1","content":"agentId: subX"}]}}`,\
`{"type":"message","id":"a1","message":{"role":"assistant","id":"m1","content":[{"type":"text","text":"ok"}],"usage":{"input_tokens":100,"output_tokens":50}}}`,\
`{"type":"message","id":"u2","message":{"role":"user","content":"second"}}`,\
`{"type":"message","id":"a2","message":{"role":"assistant","id":"m2","content":[{"type":"text","text":"ok2"}],"usage":{"input_tokens":200,"output_tokens":100}}}`,\
`{"type":"message","id":"u3","message":{"role":"user","content":"third"}}`,\
`{"type":"message","id":"a3","message":{"role":"assistant","id":"m3","content":[{"type":"text","text":"ok3"}],"usage":{"input_tokens":300,"output_tokens":150}}}`,\
}, "\n") + "\n")\
if err := os.WriteFile(sf.Path, full, 0o600); err != nil {\
t.Fatalf("write transcript: %v", err)\
}\
writeSubagentTranscript(t, sf, "subX",\
`{"type":"message","id":"se1","message":{"role":"assistant","id":"sm1","content":[{"type":"text","text":"sub"}],"usage":{"input_tokens":50,"output_tokens":25}}}`)\
\
turns, err := factoryImporter{}.SplitTurns(sf, full)\
if err != nil {\
t.Fatalf("SplitTurns: %v", err)\
}\
assertSubagentTurns(t, factoryImporter{}, sf, turns)\
}\
\
// assertSubagentTurns runs the shared assertions for a 3-turn session with one\
// spawned subagent: the summed per-turn (== per-checkpoint) subagent tokens and\
// the imported session total each count the subagent exactly once, while the\
// main-agent totals still sum across turns.\
func assertSubagentTurns(t *testing.T, imp Importer, sf SessionFile, turns []Turn) {\
t.Helper()\
ctx := context.Background()\
if len(turns) != 3 {\
t.Fatalf("want 3 turns, got %d", len(turns))\
}\
\
// Per-checkpoint proof: summing each turn's stored TokenUsage.SubagentTokens\
// (which is exactly what writeTurn persists per imported checkpoint) must\
// reconstruct the subagent total once, not 3x.\
perCheckpoint := sumTurnSubagentTokens(turns)\
assertSubagentCountedOnce(t, "sum of per-turn SubagentTokens", &perCheckpoint)\
\
// Session-total proof: the imported session.State.TokenUsage folds the\
// turns via writeSessionState the same way production Run does.\
if err := writeSessionState(ctx, imp, sf, turns); err != nil {\
t.Fatalf("writeSessionState: %v", err)\
}\
st := loadState(t, sf.SessionID)\
if st == nil || st.TokenUsage == nil {\
t.Fatalf("no imported session token usage written: %+v", st)\
}\
assertSubagentCountedOnce(t, "session total SubagentTokens", st.TokenUsage.SubagentTokens)\
\
// The main-agent fields are genuine per-slice deltas and must still sum.\
if st.TokenUsage.InputTokens != wantMainInput || st.TokenUsage.OutputTokens != wantMainOutput ||\
st.TokenUsage.APICallCount != wantMainCalls {\
t.Errorf("main-agent totals = input=%d output=%d calls=%d, want input=%d output=%d calls=%d",\
st.TokenUsage.InputTokens, st.TokenUsage.OutputTokens, st.TokenUsage.APICallCount,\
wantMainInput, wantMainOutput, wantMainCalls)\
}\
}\
```\
\
Acmd/entire/cli/agentimport/subagent\_tokens\_test.go+232\
\
```\
275 unmodified lines\
\
276\
277\
278\
279\
279\
280\
281\
282\
283\
284\
285\
286\
287\
288\
289\
290\
291\
292\
293\
294\
295\
296\
297\
298\
299\
300\
301\
302\
303\
304\
305\
306\
307\
308\
309\
310\
311\
312\
313\
314\
140 unmodified lines\
\
455\
456\
457\
458\
459\
460\
461\
462\
463\
464\
465\
466\
467\
468\
469\
470\
471\
472\
473\
474\
475\
\
275 unmodified lines\
\
// Set from hook data when the agent provides it.\
ModelName string `json:"model_name,omitempty"`\
\
// Token usage tracking (accumulated across all checkpoints in this session)\
// Token usage tracking (accumulated across all checkpoints in this session).\
//\
// DECISION: SubagentTokens is "latest snapshot wins", not summed. Subagent\
// usage arrives as a cumulative-since-session-start total (each subagent\
// transcript is re-read from line 0 every call), so accumulateTokenUsage\
// replaces rather than adds it (see cmd/entire/cli/strategy). Tradeoff: if\
// the main transcript resets or rotates mid-session (compaction writing a\
// fresh file, or a resume that truncates), a subsequent snapshot can be\
// SMALLER than a previous one, so this session-wide total regresses\
// (undercounts) for the rest of the session. This is accepted: undercounting\
// after a transcript reset is preferable to the multiplicative overcount the\
// summing approach produced, and the alternative (a session-wide high-water\
// mark) would mask genuine subagent-transcript cleanup. Checkpoint deltas do\
// not share this exposure — CheckpointTokenUsage.SubagentTokens is derived as\
// (this total - SubagentTokensBaseline) and floored at 0 by clampSubtract, so\
// a shrunk snapshot yields 0, never a negative or stale delta.\
TokenUsage *agent.TokenUsage `json:"token_usage,omitempty"`\
\
// CheckpointTokenUsage tracks hook-provided token usage since the last condensation.\
// This is checkpoint-scoped; TokenUsage remains the session-wide total.\
CheckpointTokenUsage *agent.TokenUsage `json:"checkpoint_token_usage,omitempty"`\
\
// SubagentTokensBaseline is a snapshot of TokenUsage.SubagentTokens captured\
// at the last condensation reset. Subagent token usage is always re-read\
// from the start of each subagent transcript (agent IDs are discovered from\
// the full main transcript so subagents spawned before the checkpoint\
// window are still found), so it arrives as a cumulative-since-session-start\
// total rather than a per-checkpoint delta. This baseline lets\
// CheckpointTokenUsage.SubagentTokens be rescoped to "since last\
// condensation" via SubtractTokenUsage instead of re-adding the same\
// cumulative total on every checkpoint.\
SubagentTokensBaseline *agent.TokenUsage `json:"subagent_tokens_baseline,omitempty"`\
\
// SkillEvents records explicit native skill signals observed during this session.\
// Stored as sidecar metadata so consumers can collapse skill-related transcript events\
// without mutating the raw agent transcript.\
140 unmodified lines\
\
s.TranscriptLinesAtStart = 0\
}\
\
// RebaselineSubagentTokens snapshots the current cumulative subagent total\
// (TokenUsage.SubagentTokens) into SubagentTokensBaseline so the next checkpoint\
// window's CheckpointTokenUsage.SubagentTokens is rescoped to "since this\
// re-baseline" rather than re-reporting the full cumulative subagent total.\
//\
// The invariant is: every site that starts a fresh checkpoint window by clearing\
// CheckpointTokenUsage MUST also re-baseline. Callers: the condensation reset\
// helper (resetCheckpointWindow) and cross-repo session adoption, which likewise\
// opens a fresh target-local window. Sharing this here keeps the two in step.\
func (s *State) RebaselineSubagentTokens() {\
if s.TokenUsage != nil {\
s.SubagentTokensBaseline = s.TokenUsage.SubagentTokens\
}\
}\
\
// RealignAttributionBase sets AttributionBaseCommit to newBase and clears any\
// bookkeeping whose meaning depends on attribution being diverged from the\
// shadow-branch base. Call this every time a code path intentionally brings\
```\
\
Mcmd/entire/cli/session/state.go+42/-1\
\
```\
479 unmodified lines\
\
480\
481\
482\
483\
484\
485\
486\
487\
488\
489\
490\
491\
492\
493\
\
479 unmodified lines\
\
adopted.LastCheckpointID = id.EmptyCheckpointID\
adopted.LastCheckpointCommitHash = ""\
adopted.CheckpointTokenUsage = nil\
// Re-baseline the subagent cumulative for the fresh target-local window. The\
// cloned TokenUsage carries the SOURCE session's full cumulative subagent\
// total; without re-baselining here, the first post-adopt checkpoint would\
// subtract the source's (stale or nil) baseline and over-report — potentially\
// the source session's entire subagent usage. Mirrors resetCheckpointWindow's\
// baseline capture so the first adopted checkpoint only counts target-side\
// subagent growth, consistent with the PromptWindowBase reset below.\
adopted.RebaselineSubagentTokens()\
\
adopted.FullyCondensed = false\
adopted.UntrackedFilesAtStart = untrackedFiles\
```\
\
Mcmd/entire/cli/session\_adopt.go+8\
\
```\
12 unmodified lines\
\
13\
14\
15\
16\
17\
18\
19\
1082 unmodified lines\
\
1102\
1103\
1104\
1105\
1106\
1107\
1108\
1109\
1110\
1111\
1112\
1113\
1114\
1115\
1116\
1117\
1118\
1119\
1120\
1121\
1122\
1123\
1124\
1125\
1126\
1127\
1128\
1129\
1130\
1131\
1132\
1133\
1134\
1135\
1136\
1137\
1138\
1139\
1140\
1141\
1142\
1143\
1144\
1145\
1146\
1147\
1148\
1149\
1150\
1151\
1152\
1153\
1154\
1155\
1156\
1157\
1158\
1159\
1160\
1161\
1162\
1163\
1164\
1165\
1166\
1167\
\
12 unmodified lines\
\
"time"\
\
"github.com/entireio/cli/cmd/entire/cli/agent"\
"github.com/entireio/cli/cmd/entire/cli/agent/types"\
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"\
"github.com/entireio/cli/cmd/entire/cli/internal/flock"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
1082 unmodified lines\
\
}\
}\
\
// TestSessionAdopt_RebaselinesSubagentTokens pins finding 019f5ebf-dc42: cross-repo\
// adoption opens a fresh target-local checkpoint window (StepCount=0,\
// CheckpointTokenUsage=nil), but the cloned TokenUsage carries the SOURCE\
// session's full cumulative subagent total. If SubagentTokensBaseline is not\
// re-baselined to that cumulative, the first post-adopt checkpoint subtracts a\
// stale/nil baseline and over-reports the source session's subagent usage.\
func TestSessionAdopt_RebaselinesSubagentTokens(t *testing.T) {\
for _, tc := range []struct {\
name string\
sourceBaseline *agent.TokenUsage\
}{\
// Source never condensed: baseline is nil, so the first adopted\
// checkpoint would report the entire cumulative subagent total.\
{name: "never-condensed-source", sourceBaseline: nil},\
// Source condensed at an earlier window: its baseline is stale relative\
// to the current cumulative and must not carry into the target window.\
{name: "previously-condensed-source", sourceBaseline: &agent.TokenUsage{InputTokens: 200, OutputTokens: 100, APICallCount: 2}},\
} {\
t.Run(tc.name, func(t *testing.T) {\
targetRepo := setupAdoptRepo(t)\
testutil.WriteFile(t, targetRepo, "feature.txt", "agent change\n")\
t.Chdir(targetRepo)\
\
adopted, _, err := buildAdoptedSessionState(context.Background(), &session.State{\
SessionID: "test-adopt-subagent-baseline-" + tc.name,\
AgentType: agent.AgentTypeClaudeCode,\
StartedAt: time.Now().Add(-5 * time.Minute),\
Phase: session.PhaseActive,\
BaseCommit: "source-head",\
WorktreePath: "/source/repo",\
TokenUsage: &agent.TokenUsage{\
InputTokens: 1000,\
OutputTokens: 500,\
APICallCount: 10,\
SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
},\
SubagentTokensBaseline: tc.sourceBaseline,\
})\
if err != nil {\
t.Fatalf("buildAdoptedSessionState failed: %v", err)\
}\
\
if adopted.SubagentTokensBaseline == nil {\
t.Fatal("adopted SubagentTokensBaseline = nil, want re-baselined to the cumulative subagent total")\
}\
if adopted.SubagentTokensBaseline.InputTokens != 500 || adopted.SubagentTokensBaseline.OutputTokens != 250 {\
t.Fatalf("adopted SubagentTokensBaseline = %#v, want cumulative subagent total 500/250",\
adopted.SubagentTokensBaseline)\
}\
\
// The first post-adopt checkpoint delta (cumulative - baseline) must be\
// zero: adoption should count only target-side subagent growth.\
delta := types.SubtractTokenUsage(adopted.TokenUsage.SubagentTokens, adopted.SubagentTokensBaseline)\
if delta.InputTokens != 0 || delta.OutputTokens != 0 || delta.APICallCount != 0 {\
t.Fatalf("first post-adopt subagent delta = %#v, want zero", delta)\
}\
})\
}\
}\
\
func TestSessionAdopt_PreservesReviewAndInvestigateMetadata(t *testing.T) {\
for _, tc := range []struct {\
name string\
```\
\
Mcmd/entire/cli/session\_adopt\_test.go+61\
\
```\
264 unmodified lines\
\
265\
266\
267\
268\
269\
270\
271\
268\
269\
270\
271\
272\
273\
274\
275\
460 unmodified lines\
\
736\
737\
738\
739\
740\
741\
742\
743\
744\
745\
746\
747\
748\
749\
750\
751\
752\
753\
754\
755\
756\
757\
758\
759\
760\
761\
762\
763\
764\
765\
766\
767\
768\
769\
770\
771\
468 unmodified lines\
\
1240\
1241\
1242\
1212\
1213\
1243\
1244\
1245\
1246\
111 unmodified lines\
\
1358\
1359\
1360\
1331\
1332\
1361\
1362\
1363\
1364\
\
264 unmodified lines\
\
// Backfill session state token usage from the freshly-extracted transcript.\
// Copilot CLI writes session.shutdown after the hooks return, so by condensation\
// time we can recover the authoritative full-session total from the transcript\
// while keeping checkpoint metadata scoped to CheckpointTranscriptStart.\
if backfillUsage := sessionStateBackfillTokenUsage(ctx, ag, state.AgentType, sessionData.Transcript, sessionData.TokenUsage); backfillUsage != nil {\
state.TokenUsage = backfillUsage\
}\
// while keeping checkpoint metadata scoped to CheckpointTranscriptStart. The\
// recompute drops SubagentTokens (subagentsDir=""); the helper preserves the\
// cumulative subagent total across the backfill so resetCheckpointWindow's\
// baseline does not regress to nil (finding 019f5ebf-a57e).\
applyBackfilledSessionTokenUsage(ctx, ag, state, sessionData.Transcript, sessionData.TokenUsage)\
\
if !hasTokenUsageData(sessionData.TokenUsage) && hasTokenUsageData(state.CheckpointTokenUsage) {\
sessionData.TokenUsage = accumulateTokenUsage(nil, state.CheckpointTokenUsage)\
460 unmodified lines\
\
return hasTokenUsageData(usage.SubagentTokens)\
}\
\
// applyBackfilledSessionTokenUsage overwrites state.TokenUsage with the\
// transcript-recomputed session total (see sessionStateBackfillTokenUsage) when\
// one is available, preserving the cumulative subagent total across the backfill.\
//\
// The recompute runs with subagentsDir="" (see extractSessionData), so the\
// backfilled usage never carries SubagentTokens, whereas state.TokenUsage holds\
// the authoritative cumulative subagent total accumulated by SaveStep.\
// resetCheckpointWindow captures the next window's baseline from\
// state.TokenUsage.SubagentTokens after CondenseSession returns, so letting the\
// backfill drop it would make the baseline nil and the next checkpoint re-report\
// the full cumulative subagent total. The cumulative is folded onto a copy so it\
// is never mixed into checkpointUsage, which is the checkpoint-scoped value\
// written to metadata.\
func applyBackfilledSessionTokenUsage(ctx context.Context, ag agent.Agent, state *SessionState, transcript []byte, checkpointUsage *agent.TokenUsage) {\
backfillUsage := sessionStateBackfillTokenUsage(ctx, ag, state.AgentType, transcript, checkpointUsage)\
if backfillUsage == nil {\
return\
}\
var priorSubagentTokens *agent.TokenUsage\
if state.TokenUsage != nil {\
priorSubagentTokens = state.TokenUsage.SubagentTokens\
}\
if backfillUsage.SubagentTokens == nil && priorSubagentTokens != nil {\
preserved := *backfillUsage\
preserved.SubagentTokens = priorSubagentTokens\
backfillUsage = &preserved\
}\
state.TokenUsage = backfillUsage\
}\
\
// sessionStateBackfillTokenUsage returns the best session-level token usage to\
// persist in session state after condensation.\
func sessionStateBackfillTokenUsage(ctx context.Context, ag agent.Agent, agentType types.AgentType, transcript []byte, checkpointUsage *agent.TokenUsage) *agent.TokenUsage {\
468 unmodified lines\
\
slog.Int("checkpoints_condensed", result.CheckpointsCount),\
)\
\
state.StepCount = 0\
state.CheckpointTokenUsage = nil\
resetCheckpointWindow(state)\
state.CheckpointTranscriptStart = result.TotalTranscriptLines\
state.CheckpointTranscriptSize = int64(len(result.Transcript))\
state.Phase = session.PhaseIdle\
111 unmodified lines\
\
return nil\
}\
\
state.StepCount = 0\
state.CheckpointTokenUsage = nil\
resetCheckpointWindow(state)\
state.CheckpointTranscriptStart = result.TotalTranscriptLines\
state.LastCheckpointID = checkpointID\
state.LastCheckpointCommitHash = state.BaseCommit\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_condensation.go+37/-8\
\
```\
121 unmodified lines\
\
122\
123\
124\
125\
126\
127\
128\
129\
130\
131\
132\
133\
134\
135\
136\
137\
138\
139\
140\
141\
142\
143\
144\
145\
146\
147\
148\
149\
150\
151\
152\
153\
154\
155\
182 unmodified lines\
\
338\
339\
340\
341\
342\
343\
344\
345\
346\
347\
348\
349\
350\
351\
352\
353\
354\
17 unmodified lines\
\
372\
373\
374\
336\
375\
376\
377\
378\
338\
379\
380\
381\
382\
383\
384\
385\
386\
387\
388\
389\
390\
391\
392\
393\
394\
395\
396\
397\
398\
399\
400\
401\
402\
\
121 unmodified lines\
\
if step.TokenUsage != nil {\
state.TokenUsage = accumulateTokenUsage(state.TokenUsage, step.TokenUsage)\
state.CheckpointTokenUsage = accumulateTokenUsage(state.CheckpointTokenUsage, step.TokenUsage)\
// step.TokenUsage.SubagentTokens is a cumulative-since-session-start\
// snapshot (agent IDs are discovered from the full transcript and each\
// subagent's own transcript is re-read from its start on every call —\
// see CalculateTotalTokenUsage in the claudecode/factoryaidroid\
// packages), not a per-step delta like the rest of TokenUsage.\
// accumulateTokenUsage already replaces (rather than adds) the\
// SubagentTokens field for that reason, so state.TokenUsage ends up\
// correctly holding the latest cumulative total. CheckpointTokenUsage\
// additionally needs rescoping to "since last condensation" by\
// subtracting the baseline captured at the last reset, otherwise the\
// full cumulative subagent total would be reported again at every\
// checkpoint instead of just this checkpoint's share.\
//\
// Derive the checkpoint delta FRESH each call from the session-wide\
// cumulative (state.TokenUsage.SubagentTokens) minus the baseline —\
// do NOT mutate CheckpointTokenUsage.SubagentTokens in place. A later\
// step in the same window can carry step.TokenUsage != nil but\
// SubagentTokens == nil (the subagent transcript was cleaned up, so\
// CalculateTotalTokenUsage returned APICallCount==0 and left it nil);\
// accumulateTokenUsage then leaves CheckpointTokenUsage.SubagentTokens\
// at its already-rescoped value, and re-subtracting the baseline from\
// that would double-subtract and (via clampSubtract) shrink or zero a\
// real subagent total. Recomputing from the session-wide cumulative\
// is idempotent regardless of whether this step carried a snapshot.\
if state.CheckpointTokenUsage != nil {\
state.CheckpointTokenUsage.SubagentTokens = types.SubtractTokenUsage(\
state.TokenUsage.SubagentTokens, state.SubagentTokensBaseline)\
}\
}\
\
if !branchExisted {\
182 unmodified lines\
\
// accumulateTokenUsage adds new token usage to existing accumulated usage.\
// If existing is nil, returns a copy of incoming. If incoming is nil, returns existing unchanged.\
//\
// SubagentTokens is handled differently from the other fields: main-agent\
// usage (InputTokens, OutputTokens, ...) arrives per step as a delta scoped to\
// that step's transcript slice, so it is correct to sum deltas across steps.\
// Subagent usage arrives as a cumulative-since-session-start snapshot instead\
// — CalculateTotalTokenUsage discovers agent IDs from the full transcript\
// (so a subagent spawned before the current checkpoint window is still\
// found) and re-reads each subagent transcript from its start on every call.\
// Summing that snapshot across steps would re-add a subagent's full usage on\
// every subsequent step after it was first discovered, so SubagentTokens is\
// replaced with the latest snapshot rather than added.\
func accumulateTokenUsage(existing, incoming *agent.TokenUsage) *agent.TokenUsage {\
if incoming == nil {\
return existing\
17 unmodified lines\
\
existing.OutputTokens += incoming.OutputTokens\
existing.APICallCount += incoming.APICallCount\
\
// Accumulate subagent tokens if present\
// Replace (not add) subagent tokens: incoming.SubagentTokens is already\
// the cumulative total as of this step, so the latest snapshot supersedes\
// whatever was recorded before rather than stacking on top of it.\
if incoming.SubagentTokens != nil {\
existing.SubagentTokens = accumulateTokenUsage(existing.SubagentTokens, incoming.SubagentTokens)\
existing.SubagentTokens = incoming.SubagentTokens\
}\
\
return existing\
}\
\
// resetCheckpointWindow resets the per-checkpoint accumulation window after a\
// condensation reset. It zeroes the step count, clears the checkpoint-scoped\
// token usage, and snapshots the cumulative subagent total into\
// SubagentTokensBaseline so the next window's CheckpointTokenUsage.SubagentTokens\
// can be rescoped to "since this condensation" rather than re-reporting the full\
// cumulative subagent total (see accumulateTokenUsage and the SaveStep rescoping\
// in this file, plus SessionState.SubagentTokensBaseline). Shared by all three\
// condensation reset sites (CondenseSessionByID, CondenseAndMarkFullyCondensed,\
// condenseAndUpdateState) so the baseline capture cannot drift between them.\
func resetCheckpointWindow(state *SessionState) {\
state.StepCount = 0\
state.CheckpointTokenUsage = nil\
state.RebaselineSubagentTokens()\
}\
\
// deleteShadowBranch deletes a shadow branch by name.\
// Returns nil if the branch doesn't exist (idempotent).\
// Uses git CLI instead of go-git's RemoveReference because go-git v5\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_git.go+58/-2\
\
```\
1406 unmodified lines\
\
1407\
1408\
1409\
1410\
1411\
1410\
1411\
1412\
1413\
\
1406 unmodified lines\
\
newHead := head.Hash().String()\
state.BaseCommit = newHead\
state.RealignAttributionBase(newHead)\
state.StepCount = 0\
state.CheckpointTokenUsage = nil\
resetCheckpointWindow(state)\
state.CheckpointTranscriptStart = result.TotalTranscriptLines\
state.CheckpointTranscriptSize = int64(len(result.Transcript))\
```\
\
Mcmd/entire/cli/strategy/manual\_commit\_hooks.go+1/-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\
120\
121\
122\
123\
124\
125\
126\
127\
128\
129\
130\
131\
132\
133\
134\
135\
136\
137\
138\
139\
140\
141\
142\
143\
144\
145\
146\
147\
148\
149\
150\
151\
152\
153\
154\
155\
156\
157\
158\
159\
160\
161\
162\
163\
164\
165\
166\
167\
168\
169\
170\
171\
172\
173\
174\
175\
176\
177\
178\
179\
180\
181\
182\
183\
184\
185\
186\
187\
188\
189\
190\
191\
192\
193\
194\
195\
196\
197\
198\
199\
200\
201\
202\
203\
204\
205\
206\
207\
208\
209\
210\
211\
212\
213\
214\
215\
216\
217\
218\
219\
220\
221\
222\
223\
224\
225\
226\
227\
228\
229\
230\
231\
232\
233\
234\
235\
236\
237\
238\
239\
240\
241\
242\
243\
244\
245\
246\
247\
248\
249\
250\
251\
252\
253\
254\
255\
256\
257\
258\
259\
260\
261\
262\
263\
264\
265\
266\
267\
268\
269\
270\
271\
272\
273\
274\
275\
276\
277\
278\
279\
280\
281\
282\
283\
284\
285\
286\
287\
288\
289\
290\
291\
292\
293\
294\
295\
296\
297\
298\
299\
300\
301\
302\
303\
304\
305\
306\
307\
308\
309\
310\
311\
312\
313\
314\
315\
316\
317\
318\
319\
320\
321\
322\
323\
324\
325\
326\
327\
328\
329\
330\
331\
332\
333\
334\
335\
336\
337\
338\
339\
340\
341\
342\
343\
344\
345\
346\
347\
348\
349\
350\
351\
352\
353\
354\
355\
356\
357\
358\
359\
360\
361\
362\
363\
364\
365\
366\
367\
368\
369\
370\
371\
372\
373\
374\
375\
376\
377\
378\
379\
380\
381\
382\
383\
384\
385\
386\
387\
388\
389\
390\
391\
392\
393\
394\
395\
396\
397\
398\
399\
400\
401\
402\
403\
404\
405\
406\
407\
408\
409\
410\
411\
412\
413\
414\
415\
416\
417\
418\
419\
420\
421\
422\
423\
424\
425\
426\
427\
428\
429\
430\
431\
432\
433\
434\
435\
436\
437\
438\
439\
440\
441\
442\
443\
444\
445\
446\
447\
448\
449\
450\
451\
452\
453\
454\
455\
456\
457\
458\
459\
460\
461\
462\
463\
464\
465\
466\
467\
468\
469\
470\
471\
472\
473\
474\
475\
476\
477\
478\
479\
480\
481\
482\
483\
484\
485\
486\
487\
488\
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\
515\
\
package strategy\
\
import (\
"context"\
"fmt"\
"os"\
"path/filepath"\
"testing"\
"time"\
\
"github.com/entireio/cli/cmd/entire/cli/agent"\
"github.com/entireio/cli/cmd/entire/cli/agent/types"\
"github.com/entireio/cli/cmd/entire/cli/paths"\
"github.com/entireio/cli/cmd/entire/cli/testutil"\
"github.com/go-git/go-git/v6"\
"github.com/go-git/go-git/v6/plumbing/object"\
"github.com/stretchr/testify/require"\
)\
\
// TestAccumulateTokenUsage_SubagentTokensReplacedNotSummed is a focused unit\
// test on accumulateTokenUsage: CalculateTotalTokenUsage (claudecode and\
// factoryaidroid) discovers subagent IDs from the full transcript and re-reads\
// each subagent transcript from line 0 on every call, so incoming.SubagentTokens\
// is always a cumulative-since-session-start snapshot, not a per-step delta.\
// Summing that snapshot across steps (as accumulateTokenUsage does for the\
// main-agent fields) would re-add a subagent's full usage on every subsequent\
// step after it was first discovered. accumulateTokenUsage must replace\
// SubagentTokens with the latest snapshot instead.\
func TestAccumulateTokenUsage_SubagentTokensReplacedNotSummed(t *testing.T) {\
subagentSnapshot := &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5}\
\
step1 := &agent.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1, SubagentTokens: subagentSnapshot}\
existing := accumulateTokenUsage(nil, step1)\
require.NotNil(t, existing.SubagentTokens)\
require.Equal(t, 500, existing.SubagentTokens.InputTokens)\
require.Equal(t, 250, existing.SubagentTokens.OutputTokens)\
\
// Second step within the same checkpoint window: the subagent transcript\
// hasn't changed, so CalculateTotalTokenUsage returns the SAME cumulative\
// snapshot again. Main-agent fields are per-step deltas and should sum;\
// SubagentTokens must NOT double.\
step2 := &agent.TokenUsage{InputTokens: 100, OutputTokens: 50, APICallCount: 1, SubagentTokens: subagentSnapshot}\
existing = accumulateTokenUsage(existing, step2)\
\
require.Equal(t, 200, existing.InputTokens, "main-agent InputTokens should sum across steps")\
require.Equal(t, 100, existing.OutputTokens, "main-agent OutputTokens should sum across steps")\
require.NotNil(t, existing.SubagentTokens)\
require.Equal(t, 500, existing.SubagentTokens.InputTokens, "SubagentTokens must be replaced, not summed")\
require.Equal(t, 250, existing.SubagentTokens.OutputTokens, "SubagentTokens must be replaced, not summed")\
}\
\
// TestSaveStep_SubagentTokensNotDoubleCountedAcrossCheckpoints exercises the\
// real SaveStep path for both Claude Code and Factory AI Droid (the two\
// agents whose CalculateTotalTokenUsage implementations discover subagent IDs\
// from the full transcript per #329) and proves that a subagent discovered\
// before a checkpoint window is folded into that checkpoint's token usage\
// exactly once, not re-added on every subsequent checkpoint it remains\
// discoverable in.\
func TestSaveStep_SubagentTokensNotDoubleCountedAcrossCheckpoints(t *testing.T) {\
agentTypes := []types.AgentType{agent.AgentTypeClaudeCode, agent.AgentTypeFactoryAIDroid}\
\
for _, agentType := range agentTypes {\
t.Run(string(agentType), func(t *testing.T) {\
dir := t.TempDir()\
testutil.InitRepo(t, dir)\
repo, err := git.PlainOpen(dir)\
require.NoError(t, err)\
\
worktree, err := repo.Worktree()\
require.NoError(t, err)\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
_, err = worktree.Add("test.txt")\
require.NoError(t, err)\
_, err = worktree.Commit("Initial commit", &git.CommitOptions{\
Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
})\
require.NoError(t, err)\
\
t.Chdir(dir)\
ctx := context.Background()\
s := &ManualCommitStrategy{}\
sessionID := "2026-07-10-subagent-dedup-" + string(agentType)\
\
metadataDir := ".entire/metadata/" + sessionID\
metadataDirAbs := filepath.Join(dir, metadataDir)\
require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
transcript := `{"type":"human","message":{"content":"test"}}` + "\n"\
require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
// Checkpoint 1, step 1: a subagent spawned before this checkpoint's\
// window is discovered via the full-transcript scan (#329) and its\
// cumulative usage as of now is 500/250 across 5 calls.\
subagentAtCheckpoint1 := &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5}\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 1 step 1",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: subagentAtCheckpoint1,\
},\
}))\
\
// Checkpoint 1, step 2: same turn window, subagent transcript\
// unchanged (CalculateTotalTokenUsage would return the identical\
// cumulative snapshot again since it always re-reads from line 0).\
// Change the working tree so SaveStep sees a real diff to save.\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 1 step 2",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: subagentAtCheckpoint1,\
},\
}))\
\
state, err := s.loadSessionState(ctx, sessionID)\
require.NoError(t, err)\
require.NotNil(t, state.CheckpointTokenUsage)\
require.NotNil(t, state.CheckpointTokenUsage.SubagentTokens)\
require.Equal(t, 500, state.CheckpointTokenUsage.SubagentTokens.InputTokens,\
"subagent usage must be folded once per checkpoint window, not once per step")\
require.Equal(t, 250, state.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
require.Equal(t, 200, state.CheckpointTokenUsage.InputTokens, "main-agent deltas still sum across steps")\
\
// Simulate the condensation reset that happens between checkpoints:\
// CheckpointTokenUsage is cleared and SubagentTokensBaseline snapshots\
// the cumulative subagent total counted so far, so the next\
// checkpoint's CheckpointTokenUsage.SubagentTokens is scoped to\
// "since this reset" instead of the whole session again.\
require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
st.StepCount = 0\
st.CheckpointTokenUsage = nil\
if st.TokenUsage != nil {\
st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens\
}\
st.CheckpointTranscriptStart = 10\
return nil\
}))\
\
// Checkpoint 2, step 1: the same subagent is still discoverable (its\
// marker line is still in the full transcript) and has grown a bit\
// more since checkpoint 1.\
subagentAtCheckpoint2 := &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6}\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 2 step 1",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: subagentAtCheckpoint2,\
},\
}))\
\
state2, err := s.loadSessionState(ctx, sessionID)\
require.NoError(t, err)\
\
// The session-wide total tracks the latest cumulative subagent\
// snapshot directly (it is already cumulative) — not the sum of the\
// checkpoint-1 and checkpoint-2 snapshots.\
require.NotNil(t, state2.TokenUsage.SubagentTokens)\
require.Equal(t, 620, state2.TokenUsage.SubagentTokens.InputTokens,\
"session-wide subagent total must be the latest cumulative snapshot, not summed across checkpoints")\
require.Equal(t, 310, state2.TokenUsage.SubagentTokens.OutputTokens)\
\
// Checkpoint 2's own CheckpointTokenUsage.SubagentTokens must be\
// rescoped to just what grew since the checkpoint-1 baseline\
// (620-500, 310-250), not the full cumulative total again.\
require.NotNil(t, state2.CheckpointTokenUsage)\
require.NotNil(t, state2.CheckpointTokenUsage.SubagentTokens)\
require.Equal(t, 120, state2.CheckpointTokenUsage.SubagentTokens.InputTokens,\
"checkpoint 2's subagent delta must exclude what was already counted in checkpoint 1")\
require.Equal(t, 60, state2.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
})\
}\
}\
\
// TestSaveStep_SubagentBaselineNotDoubleSubtractedWhenLaterStepDropsSubagent\
// pins the double-subtraction bug: within a single checkpoint window, once a\
// step has set CheckpointTokenUsage.SubagentTokens (rescoped by subtracting the\
// baseline), a LATER step whose TokenUsage is non-nil but carries no\
// SubagentTokens (subagent transcript cleaned up, so CalculateTotalTokenUsage\
// returns APICallCount==0 and leaves SubagentTokens nil) must not cause the\
// baseline to be subtracted a second time. accumulateTokenUsage only REPLACES\
// SubagentTokens when the incoming snapshot is non-nil, so a nil-subagent step\
// leaves CheckpointTokenUsage.SubagentTokens at its already-rescoped value; a\
// per-step re-subtraction would shrink (and via clampSubtract zero) a real\
// subagent total. The checkpoint delta must be derived FRESH each call from the\
// session-wide cumulative snapshot minus the baseline instead.\
func TestSaveStep_SubagentBaselineNotDoubleSubtractedWhenLaterStepDropsSubagent(t *testing.T) {\
agentTypes := []types.AgentType{agent.AgentTypeClaudeCode, agent.AgentTypeFactoryAIDroid}\
\
for _, agentType := range agentTypes {\
t.Run(string(agentType), func(t *testing.T) {\
dir := t.TempDir()\
testutil.InitRepo(t, dir)\
repo, err := git.PlainOpen(dir)\
require.NoError(t, err)\
\
worktree, err := repo.Worktree()\
require.NoError(t, err)\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
_, err = worktree.Add("test.txt")\
require.NoError(t, err)\
_, err = worktree.Commit("Initial commit", &git.CommitOptions{\
Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
})\
require.NoError(t, err)\
\
t.Chdir(dir)\
ctx := context.Background()\
s := &ManualCommitStrategy{}\
sessionID := "2026-07-13-subagent-nodouble-" + string(agentType)\
\
metadataDir := ".entire/metadata/" + sessionID\
metadataDirAbs := filepath.Join(dir, metadataDir)\
require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
transcript := `{"type":"human","message":{"content":"test"}}` + "\n"\
require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
// Checkpoint 1: a subagent is discovered with cumulative usage 500/250.\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 1",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
},\
}))\
\
// Condensation reset: baseline snapshots the cumulative subagent total.\
require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
st.StepCount = 0\
st.CheckpointTokenUsage = nil\
if st.TokenUsage != nil {\
st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens\
}\
st.CheckpointTranscriptStart = 10\
return nil\
}))\
\
// Checkpoint 2, step 1: the subagent has grown to 620/310. The\
// checkpoint delta must be 620-500 / 310-250 = 120 / 60.\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 2 step 1",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6},\
},\
}))\
\
// Checkpoint 2, step 2: same window, but this step's TokenUsage carries\
// NO SubagentTokens (the subagent transcript was cleaned up, so\
// CalculateTotalTokenUsage found APICallCount==0 and left SubagentTokens\
// nil). accumulateTokenUsage will not replace SubagentTokens, so it stays\
// at the checkpoint-1-baseline-subtracted 120/60 — and the baseline must\
// NOT be subtracted again.\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 2 step 2",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agentType,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
// SubagentTokens intentionally nil.\
},\
}))\
\
state, err := s.loadSessionState(ctx, sessionID)\
require.NoError(t, err)\
\
// Session-wide total keeps the latest cumulative snapshot (620/310):\
// the nil-subagent step must not clobber or shrink it.\
require.NotNil(t, state.TokenUsage.SubagentTokens)\
require.Equal(t, 620, state.TokenUsage.SubagentTokens.InputTokens,\
"session-wide subagent total must retain the latest cumulative snapshot")\
require.Equal(t, 310, state.TokenUsage.SubagentTokens.OutputTokens)\
\
// Checkpoint delta must remain the checkpoint-1-baseline-subtracted\
// 120/60, NOT 620-500-500 clamped to 0. This is the regression.\
require.NotNil(t, state.CheckpointTokenUsage)\
require.NotNil(t, state.CheckpointTokenUsage.SubagentTokens)\
require.Equal(t, 120, state.CheckpointTokenUsage.SubagentTokens.InputTokens,\
"baseline must be subtracted once, not re-subtracted on a later nil-subagent step")\
require.Equal(t, 60, state.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
// Main-agent deltas still sum across all three steps in the window.\
require.Equal(t, 200, state.CheckpointTokenUsage.InputTokens,\
"main-agent deltas sum across checkpoint-2 steps")\
})\
}\
}\
\
// TestSaveStep_CheckpointSubagentAlwaysDerivedFromSessionCumulative walks the\
// finding-1 edge matrix in one window after a baseline reset: a nil-subagent\
// first step, a step that grows the subagent, then repeated nil-subagent steps.\
// After every step the checkpoint subagent total must equal the session-wide\
// cumulative minus the baseline (idempotent), never drifting from repeated\
// subtraction. The strategy-layer accounting is agent-agnostic, so one agent\
// exercises it.\
func TestSaveStep_CheckpointSubagentAlwaysDerivedFromSessionCumulative(t *testing.T) {\
dir := t.TempDir()\
testutil.InitRepo(t, dir)\
repo, err := git.PlainOpen(dir)\
require.NoError(t, err)\
\
worktree, err := repo.Worktree()\
require.NoError(t, err)\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v0"), 0o644))\
_, err = worktree.Add("test.txt")\
require.NoError(t, err)\
_, err = worktree.Commit("Initial commit", &git.CommitOptions{\
Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
})\
require.NoError(t, err)\
\
t.Chdir(dir)\
ctx := context.Background()\
s := &ManualCommitStrategy{}\
sessionID := "2026-07-13-subagent-edgematrix"\
\
metadataDir := ".entire/metadata/" + sessionID\
metadataDirAbs := filepath.Join(dir, metadataDir)\
require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName),\
[]byte(`{"type":"human","message":{"content":"test"}}`+"\n"), 0o644))\
\
rev := 0\
save := func(sub *agent.TokenUsage) {\
rev++\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte(fmt.Sprintf("rev%d", rev)), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: fmt.Sprintf("step %d", rev),\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agent.AgentTypeClaudeCode,\
TokenUsage: &agent.TokenUsage{InputTokens: 10, APICallCount: 1, SubagentTokens: sub},\
}))\
}\
// checkpointSubIn returns the current checkpoint subagent InputTokens (0 when nil).\
checkpointSubIn := func() int {\
st, loadErr := s.loadSessionState(ctx, sessionID)\
require.NoError(t, loadErr)\
if st.CheckpointTokenUsage == nil || st.CheckpointTokenUsage.SubagentTokens == nil {\
return 0\
}\
return st.CheckpointTokenUsage.SubagentTokens.InputTokens\
}\
\
// Establish a baseline of 400 via a first window + reset.\
save(&agent.TokenUsage{InputTokens: 400, APICallCount: 4})\
require.NoError(t, MutateSessionState(ctx, sessionID, func(st *SessionState) error {\
st.StepCount = 0\
st.CheckpointTokenUsage = nil\
st.SubagentTokensBaseline = st.TokenUsage.SubagentTokens // 400\
st.CheckpointTranscriptStart = 5\
return nil\
}))\
\
// Edge: nil first step of the window — cumulative stays 400, delta 0.\
save(nil)\
require.Equal(t, 0, checkpointSubIn(), "nil first step: delta is cumulative(400)-baseline(400)=0")\
\
// Growth step — cumulative 550, delta 150.\
save(&agent.TokenUsage{InputTokens: 550, APICallCount: 5})\
require.Equal(t, 150, checkpointSubIn(), "growth step: delta is 550-400")\
\
// Repeated nil steps must NOT shrink the delta (idempotent derive-fresh).\
save(nil)\
require.Equal(t, 150, checkpointSubIn(), "nil step must not re-subtract baseline")\
save(nil)\
require.Equal(t, 150, checkpointSubIn(), "second nil step must not re-subtract baseline")\
}\
\
// TestCondenseSessionByID_CapturesSubagentBaselineViaRealResetPath drives a REAL\
// condensation (CondenseSessionByID) rather than hand-simulating the reset, so\
// the production baseline-snapshot code in resetCheckpointWindow — shared by the\
// three condensation reset sites — is exercised where it actually lives. It then\
// runs a follow-up checkpoint to prove the baseline captured by the real path is\
// used to rescope the next checkpoint's subagent delta.\
func TestCondenseSessionByID_CapturesSubagentBaselineViaRealResetPath(t *testing.T) {\
dir := t.TempDir()\
testutil.InitRepo(t, dir)\
repo, err := git.PlainOpen(dir)\
require.NoError(t, err)\
\
worktree, err := repo.Worktree()\
require.NoError(t, err)\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v1"), 0o644))\
_, err = worktree.Add("test.txt")\
require.NoError(t, err)\
_, err = worktree.Commit("Initial commit", &git.CommitOptions{\
Author: &object.Signature{Name: "Test", Email: "test@test.com", When: time.Now()},\
})\
require.NoError(t, err)\
\
t.Chdir(dir)\
ctx := context.Background()\
s := &ManualCommitStrategy{}\
sessionID := "2026-07-13-subagent-realreset"\
\
metadataDir := ".entire/metadata/" + sessionID\
metadataDirAbs := filepath.Join(dir, metadataDir)\
require.NoError(t, os.MkdirAll(metadataDirAbs, 0o755))\
// The assistant line carries real usage data (message.id + usage). Real\
// Claude Code transcripts always do, which makes sessionStateBackfillTokenUsage\
// fire during condensation (its InputTokens > 0 branch) and overwrite\
// state.TokenUsage with the transcript-recomputed value — which is computed\
// with subagentsDir="" and therefore drops SubagentTokens. This is what makes\
// this test guard the REAL condensation path: without preserving the\
// cumulative subagent total across the backfill, resetCheckpointWindow would\
// snapshot a nil baseline and the next checkpoint would re-report the full\
// cumulative subagent total (finding 019f5ebf-a57e).\
transcript := `{"type":"human","message":{"content":"do the thing"}}\
{"type":"assistant","uuid":"a1","message":{"id":"m1","usage":{"input_tokens":300,"output_tokens":150}}}\
`\
require.NoError(t, os.WriteFile(filepath.Join(metadataDirAbs, paths.TranscriptFileName), []byte(transcript), 0o644))\
\
// Checkpoint 1: subagent discovered with cumulative usage 500/250.\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v2"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 1",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agent.AgentTypeClaudeCode,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: &agent.TokenUsage{InputTokens: 500, OutputTokens: 250, APICallCount: 5},\
},\
}))\
\
// Drive the REAL condensation reset path (not a hand-simulated one). This\
// executes resetCheckpointWindow inside CondenseSessionByID.\
require.NoError(t, s.CondenseSessionByID(ctx, sessionID))\
\
state, err := s.loadSessionState(ctx, sessionID)\
require.NoError(t, err)\
require.Equal(t, 0, state.StepCount, "real condensation must reset StepCount")\
require.Nil(t, state.CheckpointTokenUsage, "real condensation must clear CheckpointTokenUsage")\
require.NotNil(t, state.SubagentTokensBaseline,\
"real condensation must snapshot the subagent baseline")\
require.Equal(t, 500, state.SubagentTokensBaseline.InputTokens,\
"baseline must capture the cumulative subagent total at condensation")\
require.Equal(t, 250, state.SubagentTokensBaseline.OutputTokens)\
\
// Checkpoint 2 after the real reset: the subagent grew to 620/310. Its\
// checkpoint delta must be rescoped against the real-path baseline (120/60).\
require.NoError(t, os.WriteFile(filepath.Join(dir, "test.txt"), []byte("v3"), 0o644))\
require.NoError(t, s.SaveStep(ctx, StepContext{\
SessionID: sessionID,\
MetadataDir: metadataDir,\
MetadataDirAbs: metadataDirAbs,\
ModifiedFiles: []string{"test.txt"},\
CommitMessage: "checkpoint 2",\
AuthorName: "Test",\
AuthorEmail: "test@test.com",\
AgentType: agent.AgentTypeClaudeCode,\
TokenUsage: &agent.TokenUsage{\
InputTokens: 100, OutputTokens: 50, APICallCount: 1,\
SubagentTokens: &agent.TokenUsage{InputTokens: 620, OutputTokens: 310, APICallCount: 6},\
},\
}))\
\
state2, err := s.loadSessionState(ctx, sessionID)\
require.NoError(t, err)\
require.NotNil(t, state2.CheckpointTokenUsage)\
require.NotNil(t, state2.CheckpointTokenUsage.SubagentTokens)\
require.Equal(t, 120, state2.CheckpointTokenUsage.SubagentTokens.InputTokens,\
"checkpoint delta must be rescoped against the real-path baseline")\
require.Equal(t, 60, state2.CheckpointTokenUsage.SubagentTokens.OutputTokens)\
}\
```\
\
Acmd/entire/cli/strategy/subagent\_token\_dedup\_test.go+515