fix(agentimport): keep subagent baseline when a turn's snapshot is nil · Entire

fix(agentimport): keep subagent baseline when a turn's snapshot is nil

1e53147→main·

suhaanthayyil·3d ago·2 files·+53 added/-1 removed

rescopeSubagentTokensToDeltas advanced prevCumulative unconditionally, even when a turn's cumulative SubagentTokens was nil (the subagent transcript transiently failed to read, so CalculateTotalTokenUsage continue-d past it). A later turn whose snapshot reappeared then subtracted a nil baseline and re-reported the full cumulative, reintroducing the double-counting the rescoping removes. Only advance prevCumulative when the snapshot is non-nil, matching the live path where accumulateTokenUsage replaces SubagentTokens only for non-nil snapshots.

Changes

2

102 unmodified lines

103
104
105
106
106
107
108
109
110
111
112
113
114
115
116
117
118

102 unmodified lines

}
        cumulative := turns[i].Tokens.SubagentTokens
        turns[i].Tokens.SubagentTokens = types.SubtractTokenUsage(cumulative, prevCumulative)
        prevCumulative = cumulative
        // 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+11/-1

82 unmodified lines

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

82 unmodified lines

}
}

// 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)",
            urns[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