# fix(attribution): scan full transcript for subagents spawned before checkpoint

`40ddd81`→[main](/content/gh/entireio/cli/commits/main/index.html)·

suhaanthayyil·1w ago·4 files·+180 added/-6 removed

ExtractAllModifiedFiles and CalculateTotalTokenUsage extracted spawned
subagent IDs from the startLine-sliced transcript, so a subagent spawned
before the checkpoint's startLine was missed once startLine advanced — its
later file modifications and token usage went uncounted. Extract agent IDs
from the full transcript (startLine=0) while keeping main-agent extraction
scoped to the slice. Covers both the Claude Code and Factory Droid paths.

Closes #329

Co-authored-by: Cursor <cursoragent@cursor.com>

## Changes

4

- cmd/entire/cli/agent

- claudecode

- Mtranscript.go+18/-4

- Mtranscript\_test.go+75

- factoryaidroid

- Mtranscript.go+17/-2

- Mtranscript\_test.go+70

```
394 unmodified lines

395
396
397
398
399
398
399
400
401
402
403
404
405
406
407
408
409
46 unmodified lines

456
457
458
452
453
459
460
461
462
463
464
465
466
467
468
469
470

394 unmodified lines

// Calculate token usage from parsed transcript
	mainUsage := CalculateTokenUsage(parsed)

// Extract spawned agent IDs from the same parsed transcript
	agentIDs := ExtractSpawnedAgentIDs(parsed)
	// 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).
	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 (skip when subagentsDir is empty to avoid reading from cwd)
	if len(agentIDs) > 0 && subagentsDir != "" {
46 unmodified lines

}
	}

// Find spawned subagents and collect their modified files (skip when subagentsDir is empty to avoid reading from cwd)
	agentIDs := ExtractSpawnedAgentIDs(parsed)
	// 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.
	fullParsed, err := transcript.ParseFromBytes(transcriptData)
	if err != nil {
		return nil, fmt.Errorf("failed to parse full transcript: %w", err)
	}
	agentIDs := ExtractSpawnedAgentIDs(fullParsed)
	if subagentsDir == "" {
		return files, nil
	}
```

Mcmd/entire/cli/agent/claudecode/transcript.go+18/-4

```
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.
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
369
370
371
372
373
374
375
376
377
378
379
36 unmodified lines

416
417
418
412
419
420
421
422
423
424
425
426
427
428
429
430

365 unmodified lines

mainUsage := CalculateTokenUsage(parsed)

agentIDs := ExtractSpawnedAgentIDs(parsed)
	// 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).
	fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)
	if err != nil {
		return nil, fmt.Errorf("failed to parse full transcript: %w", err)
	}
	agentIDs := ExtractSpawnedAgentIDs(fullParsed)
	if len(agentIDs) > 0 && subagentsDir != "" {
		subagentUsage := &agent.TokenUsage{}
		for agentID := range agentIDs {
36 unmodified lines

fileSet[f] = true

agentIDs := ExtractSpawnedAgentIDs(parsed)
	// 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.
	fullParsed, _, err := ParseDroidTranscriptFromBytes(data, 0)
	if err != nil {
		return nil, fmt.Errorf("failed to parse full transcript: %w", err)
	}
	agentIDs := ExtractSpawnedAgentIDs(fullParsed)
	if subagentsDir == "" {
		return files, nil
	}
```

Mcmd/entire/cli/agent/factoryaidroid/transcript.go+17/-2

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

// 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()

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
