Skip to content

Commit 39234cb

Browse files
committed
Preserve subagent token totals during condensation
Condensation was recalculating Claude Code token usage without the session subagent transcript directory, so metadata written on user commits dropped Task-spawned token totals even though live session state could see them. The fix reuses the transcript-dir/session-id convention already used by lifecycle hooks and locks the behavior with a focused condensation regression. Constraint: Checkpoint metadata pushes for this branch must route through stale2000/cli, so the committed Entire settings point checkpoint_remote at the fork. Rejected: Rework token parsing or checkpoint storage | the loss was caused by a missing subagent transcript directory argument, not the parser or metadata schema. Confidence: high Scope-risk: narrow Directive: Keep condensation subagent path derivation aligned with lifecycle hook path derivation when changing transcript layouts. Tested: go test ./cmd/entire/cli/strategy -run 'TestCondenseSession' -count=1 Tested: go test ./cmd/entire/cli/agent/claudecode -run 'TestCalculateTotalTokenUsage|TestExtractAllModifiedFiles' -count=1 Not-tested: Full mise run check because unrelated local uiform work currently trips lint before this branch is isolated. Entire-Checkpoint: 36286545221f
1 parent 9403248 commit 39234cb

3 files changed

Lines changed: 73 additions & 3 deletions

File tree

.entire/settings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"strategy_options": {
55
"checkpoint_remote": {
66
"provider": "github",
7-
"repo": "entireio/cli-checkpoints"
7+
"repo": "stale2000/cli"
88
},
99
"filtered_fetches": true
1010
},

cmd/entire/cli/strategy/manual_commit_condensation.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,13 @@ func (s *ManualCommitStrategy) extractOrCreateSessionData(ctx context.Context, r
462462
}
463463
}
464464

465+
func subagentsDirForTranscript(transcriptPath, sessionID string) string {
466+
if transcriptPath == "" || sessionID == "" {
467+
return ""
468+
}
469+
return filepath.Join(filepath.Dir(transcriptPath), sessionID, "subagents")
470+
}
471+
465472
// generateSummary produces an LLM-generated summary of the session transcript.
466473
// The transcript must be pre-redacted to avoid sending secrets to the LLM.
467474
// Returns nil if the scoped transcript is empty or generation fails.
@@ -898,7 +905,8 @@ func (s *ManualCommitStrategy) extractSessionData(ctx context.Context, repo *git
898905
// extract them from offset 0; consumers can filter by checkpoint_transcript_start
899906
// if they only render the checkpoint-scoped slice.
900907
if len(data.Transcript) > 0 {
901-
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, checkpointTranscriptStart, "") //TODO: why do we not use here subagents dir?
908+
subagentsDir := subagentsDirForTranscript(liveTranscriptPath, sessionID)
909+
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, checkpointTranscriptStart, subagentsDir)
902910
data.SkillEvents = agent.ExtractSkillEvents(ctx, ag, data.Transcript, 0)
903911
}
904912

@@ -940,7 +948,8 @@ func (s *ManualCommitStrategy) extractSessionDataFromLiveTranscript(ctx context.
940948
// extract them from offset 0; consumers can filter by checkpoint_transcript_start
941949
// if they only render the checkpoint-scoped slice.
942950
if len(data.Transcript) > 0 {
943-
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, state.CheckpointTranscriptStart, "") //TODO: why do we not use here subagents dir?
951+
subagentsDir := subagentsDirForTranscript(transcriptPath, state.SessionID)
952+
data.TokenUsage = agent.CalculateTokenUsage(ctx, ag, data.Transcript, state.CheckpointTranscriptStart, subagentsDir)
944953
data.SkillEvents = agent.ExtractSkillEvents(ctx, ag, data.Transcript, 0)
945954
}
946955

cmd/entire/cli/strategy/manual_commit_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"github.com/go-git/go-git/v6/plumbing/object"
2424
"github.com/stretchr/testify/assert"
2525
"github.com/stretchr/testify/require"
26+
27+
_ "github.com/entireio/cli/cmd/entire/cli/agent/claudecode" // Register Claude Code agent for condensation token tests
2628
)
2729

2830
const testTrailerCheckpointID id.CheckpointID = "a1b2c3d4e5f6"
@@ -3323,6 +3325,65 @@ func TestCondenseSession_TranscriptRelocatedMidSession(t *testing.T) {
33233325
}
33243326
}
33253327

3328+
func TestCondenseSession_ClaudeSubagentTokenUsage(t *testing.T) {
3329+
dir := t.TempDir()
3330+
testutil.InitRepo(t, dir)
3331+
testutil.WriteFile(t, dir, "README.md", "initial\n")
3332+
testutil.GitAdd(t, dir, "README.md")
3333+
testutil.GitCommit(t, dir, "initial")
3334+
t.Chdir(dir)
3335+
3336+
sessionID := "2026-06-09-claude-subagent-token"
3337+
transcriptDir := filepath.Join(dir, ".claude", "transcripts")
3338+
transcriptPath := filepath.Join(transcriptDir, "main.jsonl")
3339+
subagentsDir := filepath.Join(transcriptDir, sessionID, "subagents")
3340+
require.NoError(t, os.MkdirAll(subagentsDir, 0o755))
3341+
3342+
mainTranscript := strings.Join([]string{
3343+
`{"type":"assistant","uuid":"a-main","message":{"id":"msg_main","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_task1","name":"Task","input":{"description":"write helper","prompt":"write helper"}}],"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":10}}}`,
3344+
`{"type":"user","uuid":"u-main","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_task1","content":"agentId: sub1"}]}}`,
3345+
}, "\n") + "\n"
3346+
require.NoError(t, os.WriteFile(transcriptPath, []byte(mainTranscript), 0o644))
3347+
3348+
subagentTranscript := `{"type":"assistant","uuid":"a-sub","message":{"id":"msg_sub","type":"message","role":"assistant","content":[{"type":"text","text":"wrote helper"}],"usage":{"input_tokens":200,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":20}}}` + "\n"
3349+
require.NoError(t, os.WriteFile(filepath.Join(subagentsDir, "agent-sub1.jsonl"), []byte(subagentTranscript), 0o644))
3350+
3351+
repo, err := git.PlainOpen(dir)
3352+
require.NoError(t, err)
3353+
head, err := repo.Head()
3354+
require.NoError(t, err)
3355+
initialHash := head.Hash().String()
3356+
3357+
state := &SessionState{
3358+
SessionID: sessionID,
3359+
BaseCommit: initialHash,
3360+
AttributionBaseCommit: initialHash,
3361+
WorktreePath: dir,
3362+
TranscriptPath: transcriptPath,
3363+
FilesTouched: []string{"helper.go"},
3364+
AgentType: agent.AgentTypeClaudeCode,
3365+
ModelName: "claude-sonnet-repro",
3366+
CheckpointTranscriptStart: 0,
3367+
}
3368+
3369+
s := &ManualCommitStrategy{}
3370+
checkpointID := id.MustCheckpointID("dd11cc22bb33")
3371+
_, err = s.CondenseSession(context.Background(), repo, checkpointID, state, nil)
3372+
require.NoError(t, err)
3373+
3374+
store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs())
3375+
content, err := store.ReadLatestSessionContent(t.Context(), checkpointID)
3376+
require.NoError(t, err)
3377+
3378+
require.NotNil(t, content.Metadata.TokenUsage, "TokenUsage should be persisted")
3379+
assert.Equal(t, 100, content.Metadata.TokenUsage.InputTokens)
3380+
assert.Equal(t, 10, content.Metadata.TokenUsage.OutputTokens)
3381+
require.NotNil(t, content.Metadata.TokenUsage.SubagentTokens, "SubagentTokens should survive condensation")
3382+
assert.Equal(t, 200, content.Metadata.TokenUsage.SubagentTokens.InputTokens)
3383+
assert.Equal(t, 20, content.Metadata.TokenUsage.SubagentTokens.OutputTokens)
3384+
assert.Equal(t, 1, content.Metadata.TokenUsage.SubagentTokens.APICallCount)
3385+
}
3386+
33263387
// TestCondenseSession_GeminiTranscript verifies that CondenseSession works correctly
33273388
// with Gemini JSON format transcripts, including prompt extraction and format detection.
33283389
func TestCondenseSession_GeminiTranscript(t *testing.T) {

0 commit comments

Comments
 (0)