Skip to content

fix: deduplicate streaming JSONL entries to prevent ~2x cost overcounting#77

Merged
matt1398 merged 1 commit intomatt1398:mainfrom
Psypeal:fix/cost-overcounting-dedup
Feb 26, 2026
Merged

fix: deduplicate streaming JSONL entries to prevent ~2x cost overcounting#77
matt1398 merged 1 commit intomatt1398:mainfrom
Psypeal:fix/cost-overcounting-dedup

Conversation

@Psypeal
Copy link
Contributor

@Psypeal Psypeal commented Feb 24, 2026

Summary

  • Claude Code writes multiple JSONL entries per API response during streaming, each with the same requestId but incrementally increasing output_tokens
  • Our parser summed every entry independently, inflating costs by ~2x compared to Claude Code's /cost command
  • Fix: keep only the last entry per requestId (the final, complete token counts) — same approach as ryoppippi/ccusage#835

Changes

File Change
src/main/types/messages.ts Add requestId?: string to ParsedMessage
src/main/utils/jsonl.ts Extract requestId from AssistantEntry; add deduplicateByRequestId(); apply dedup in calculateMetrics()
src/renderer/utils/sessionAnalyzer.ts Skip duplicate streaming entries in cost accounting loop
test/main/utils/costCalculation.test.ts 6 new test cases for streaming dedup

How it works

  • deduplicateByRequestId() scans messages to find the last index per requestId, then filters out earlier duplicates
  • Messages without requestId (user, system) pass through unchanged
  • Applied in both calculateMetrics() (main process) and sessionAnalyzer (renderer)

Test plan

  • pnpm typecheck passes
  • pnpm lint:fix — 0 errors
  • pnpm test — 833 tests pass (6 new)
  • Manual: compare session cost with Claude Code /cost output

Closes #74

Summary by CodeRabbit

  • New Features

    • Added streaming deduplication to remove duplicate assistant entries and preserve only the final per-request message.
    • Introduced an optional per-request identifier on messages to enable that deduplication.
  • Improvements

    • Metrics (token and cost) now compute from deduplicated streaming messages for more accurate reporting.

@gemini-code-assist
Copy link

Summary of Changes

Hello @Psypeal, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves an issue where token usage costs were inaccurately inflated due to the way streaming API responses from Claude Code were processed. By implementing a deduplication strategy that considers only the final message for each unique request ID, the system now provides precise cost calculations, aligning with the actual API usage and preventing erroneous overcharges.

Highlights

  • Cost Overcounting Fix: Implemented a mechanism to deduplicate streaming JSONL entries, preventing approximately 2x overcounting of token costs from Claude Code API responses.
  • Accurate Token Counting: Ensured that only the final, complete token counts are used for cost calculation by retaining only the last entry per unique API request ID during streaming.
  • Deduplication Logic: Introduced a dedicated function deduplicateByRequestId to filter out intermediate streaming entries, applying this logic in both main and renderer processes.
Changelog
  • src/main/types/messages.ts
    • Added requestId property to ParsedMessage interface.
  • src/main/utils/jsonl.ts
    • Implemented deduplicateByRequestId function to filter streaming entries.
    • Integrated deduplication into calculateMetrics to ensure accurate token counting.
  • src/renderer/utils/sessionAnalyzer.ts
    • Added logic to identify and skip duplicate streaming entries during session analysis for cost accounting.
  • test/main/utils/costCalculation.test.ts
    • Introduced 6 new test cases to validate the streaming deduplication logic.
Activity
  • All type checks (pnpm typecheck) passed successfully.
  • Linting (pnpm lint:fix) completed with 0 errors.
  • All 833 unit tests (pnpm test) passed, including the 6 new tests for deduplication.
  • Manual testing is pending to compare session cost with Claude Code's /cost output for final verification.
  • This pull request addresses and closes issue Fix cost overcounting — deduplicate streaming JSONL entries #74.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added the bug Something isn't working label Feb 24, 2026
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses a critical bug causing cost over-counting for streaming API responses. The approach of deduplicating entries by requestId is sound, and the changes are implemented correctly in both the main and renderer processes. The addition of comprehensive test cases is excellent and ensures the reliability of the fix. I have one suggestion to improve the efficiency of the deduplication logic in the renderer process, but overall, this is a solid contribution.

Comment on lines +357 to +367
const lastIndexByRequestId = new Map<string, number>();
for (let i = 0; i < messages.length; i++) {
const rid = messages[i].requestId;
if (rid) lastIndexByRequestId.set(rid, i);
}
for (let i = 0; i < messages.length; i++) {
const rid = messages[i].requestId;
if (rid && lastIndexByRequestId.get(rid) !== i) {
duplicateRequestIndices.add(i);
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for finding duplicate request indices can be made more efficient. The current implementation iterates over the messages array twice to first find the last index of each requestId and then to populate the set of duplicates. You can achieve the same result more cleanly and efficiently with a single pass by iterating backwards through the array.

Suggested change
const lastIndexByRequestId = new Map<string, number>();
for (let i = 0; i < messages.length; i++) {
const rid = messages[i].requestId;
if (rid) lastIndexByRequestId.set(rid, i);
}
for (let i = 0; i < messages.length; i++) {
const rid = messages[i].requestId;
if (rid && lastIndexByRequestId.get(rid) !== i) {
duplicateRequestIndices.add(i);
}
}
const seenRequestIds = new Set<string>();
for (let i = messages.length - 1; i >= 0; i--) {
const rid = messages[i].requestId;
if (rid) {
if (seenRequestIds.has(rid)) {
duplicateRequestIndices.add(i);
} else {
seenRequestIds.add(rid);
}
}
}

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds an optional requestId to ParsedMessage and implements per-request streaming deduplication via deduplicateByRequestId, which is applied inside calculateMetrics to avoid double-counting tokens/costs from repeated streaming entries.

Changes

Cohort / File(s) Summary
Type Definitions
src/main/types/messages.ts
Added optional requestId?: string to ParsedMessage.
Streaming Deduplication & Metrics
src/main/utils/jsonl.ts
Propagates requestId from parsed entries; added export function deduplicateByRequestId(messages: ParsedMessage[]): ParsedMessage[] (last-entry-wins per non-empty requestId); integrated deduplication into calculateMetrics so token and cost aggregation uses deduplicated messages.

Possibly related issues

  • matt1398/claude-devtools issue 74 — Implements requestId-based per-request streaming deduplication (matches this PR's dedupe logic).
  • matt1398/claude-devtools issue 80 — Updates calculateMetrics to use requestId deduplication and fixes streaming cost/token accounting (matches the integration change here).

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/utils/sessionAnalyzer.ts (1)

616-634: ⚠️ Potential issue | 🟠 Major

startupTokens and assistantMsgData not guarded by duplicateRequestIndices.

The dedup guard added at line 409 protects cost and token stats, but two other per-message token accumulators are not protected:

  • startupTokens (Lines 618–623): every streaming duplicate entry before the first work tool inflates startupPctOfTotal and the startup overhead assessment.
  • assistantMsgData (Lines 628–633): duplicate entries push additional [timestamp, tokenCount] pairs, skewing the quartile-based token density timeline.
🐛 Proposed fix
-    if (msgType === 'assistant' && !firstWorkToolSeen) {
+    if (msgType === 'assistant' && !firstWorkToolSeen && !duplicateRequestIndices.has(i)) {
       startupMessages++;
       if (m.usage) {
-    if (msgType === 'assistant' && msgTs && m.usage) {
+    if (msgType === 'assistant' && msgTs && m.usage && !duplicateRequestIndices.has(i)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/sessionAnalyzer.ts` around lines 616 - 634, The code adds
per-message token counts to startupTokens and pushes to assistantMsgData without
checking duplicateRequestIndices, causing duplicates to skew startupPctOfTotal
and the token-density timeline; update the two blocks that modify
startupTokens/startupMessages and assistantMsgData to first skip processing if
duplicateRequestIndices indicates this message index is a duplicate (the same
guard used around cost/token stats at the dedup guard near
duplicateRequestIndices), i.e., only accumulate into
startupTokens/startupMessages and push into assistantMsgData when the message is
not a duplicate (check the same index/key used by the existing dedup logic),
while preserving the existing conditions (msgType === 'assistant',
!firstWorkToolSeen, msgTs, m.usage).
🧹 Nitpick comments (3)
src/renderer/utils/sessionAnalyzer.ts (1)

350-368: Dedup logic is duplicated from deduplicateByRequestId in jsonl.ts.

The inline scoped block reimplements the same two-pass algorithm already in src/main/utils/jsonl.ts. If deduplicateByRequestId were promoted to @shared/utils, it could be consumed directly here, eliminating the duplication and ensuring both paths evolve together.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/sessionAnalyzer.ts` around lines 350 - 368, The dedup
logic here duplicates the two-pass algorithm from deduplicateByRequestId;
replace the inline block that builds duplicateRequestIndices by importing and
calling deduplicateByRequestId from a shared module (e.g., move
deduplicateByRequestId in src/main/utils/jsonl.ts to `@shared/utils/jsonl` and
then call it from sessionAnalyzer.ts), passing the messages array and using its
result instead of reimplementing the two-pass loop that computes
lastIndexByRequestId and duplicateRequestIndices.
test/main/utils/costCalculation.test.ts (1)

740-789: deduplicateByRequestId direct tests are missing a mixed-content case.

Both existing direct tests use either all messages without requestId or all messages with one. There's no test exercising the case where some messages have a requestId and some don't — i.e., verifying that non-requestId messages are preserved unchanged when dedup does occur. This case is covered indirectly via calculateMetrics (lines 657–717) but a direct assertion here would close the gap.

🧪 Suggested additional test
+    it('should preserve messages without requestId when dedup occurs', () => {
+      const messages: ParsedMessage[] = [
+        {
+          type: 'assistant',
+          uuid: 'msg-1a',
+          timestamp: new Date(),
+          content: [],
+          requestId: 'req-1',
+          usage: { input_tokens: 100, output_tokens: 50 },
+          toolCalls: [],
+          toolResults: [],
+          isSidechain: false,
+        },
+        {
+          type: 'user',
+          uuid: 'msg-user',
+          timestamp: new Date(),
+          content: 'hello',
+          toolCalls: [],
+          toolResults: [],
+          isSidechain: false,
+          isMeta: false,
+        },
+        {
+          type: 'assistant',
+          uuid: 'msg-1b',
+          timestamp: new Date(),
+          content: [],
+          requestId: 'req-1',
+          usage: { input_tokens: 100, output_tokens: 200 },
+          toolCalls: [],
+          toolResults: [],
+          isSidechain: false,
+        },
+      ];
+
+      const result = deduplicateByRequestId(messages);
+      expect(result).toHaveLength(2);
+      expect(result[0].uuid).toBe('msg-user'); // non-requestId message preserved
+      expect(result[1].uuid).toBe('msg-1b');   // last requestId entry kept
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/utils/costCalculation.test.ts` around lines 740 - 789, Add a direct
unit test for deduplicateByRequestId that mixes messages with and without
requestId: create an array containing (a) one or more messages without
requestId, (b) multiple messages sharing the same requestId (earlier and later),
and (c) another non-requestId message; call deduplicateByRequestId and assert
that duplicates for the shared requestId are reduced to the last entry (check
uuid and usage.output_tokens), that non-requestId messages remain present and in
their original order, and optionally assert reference equality for an untouched
non-requestId message to ensure it was not recreated. Use the same ParsedMessage
shape as other tests and name the test descriptively (e.g., "should preserve
non-requestId messages while deduplicating requestId entries").
src/main/utils/jsonl.ts (1)

316-324: messageCount includes streaming duplicates while all token fields do not.

messageCount: messages.length counts every JSONL entry including the ones discarded by dedup. If callers use this field to infer "number of distinct API calls" it will be inflated for streaming sessions. Consider returning dedupedMessages.length (or exposing both), or at minimum documenting that this reflects raw JSONL entry count.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/jsonl.ts` around lines 316 - 324, The returned object
currently sets messageCount: messages.length which counts raw JSONL entries
(including duplicates removed by dedup), so update the return to report distinct
messages instead: replace messageCount: messages.length with messageCount:
dedupedMessages.length (or add a new field dedupedMessageCount:
dedupedMessages.length and keep messageCount for raw entries) and ensure any
callers that expect "number of API calls" use the deduped value; locate this in
the function that builds the return object (the object containing durationMs,
totalTokens, inputTokens, etc.) and adjust accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/renderer/utils/sessionAnalyzer.ts`:
- Around line 616-634: The code adds per-message token counts to startupTokens
and pushes to assistantMsgData without checking duplicateRequestIndices, causing
duplicates to skew startupPctOfTotal and the token-density timeline; update the
two blocks that modify startupTokens/startupMessages and assistantMsgData to
first skip processing if duplicateRequestIndices indicates this message index is
a duplicate (the same guard used around cost/token stats at the dedup guard near
duplicateRequestIndices), i.e., only accumulate into
startupTokens/startupMessages and push into assistantMsgData when the message is
not a duplicate (check the same index/key used by the existing dedup logic),
while preserving the existing conditions (msgType === 'assistant',
!firstWorkToolSeen, msgTs, m.usage).

---

Nitpick comments:
In `@src/main/utils/jsonl.ts`:
- Around line 316-324: The returned object currently sets messageCount:
messages.length which counts raw JSONL entries (including duplicates removed by
dedup), so update the return to report distinct messages instead: replace
messageCount: messages.length with messageCount: dedupedMessages.length (or add
a new field dedupedMessageCount: dedupedMessages.length and keep messageCount
for raw entries) and ensure any callers that expect "number of API calls" use
the deduped value; locate this in the function that builds the return object
(the object containing durationMs, totalTokens, inputTokens, etc.) and adjust
accordingly.

In `@src/renderer/utils/sessionAnalyzer.ts`:
- Around line 350-368: The dedup logic here duplicates the two-pass algorithm
from deduplicateByRequestId; replace the inline block that builds
duplicateRequestIndices by importing and calling deduplicateByRequestId from a
shared module (e.g., move deduplicateByRequestId in src/main/utils/jsonl.ts to
`@shared/utils/jsonl` and then call it from sessionAnalyzer.ts), passing the
messages array and using its result instead of reimplementing the two-pass loop
that computes lastIndexByRequestId and duplicateRequestIndices.

In `@test/main/utils/costCalculation.test.ts`:
- Around line 740-789: Add a direct unit test for deduplicateByRequestId that
mixes messages with and without requestId: create an array containing (a) one or
more messages without requestId, (b) multiple messages sharing the same
requestId (earlier and later), and (c) another non-requestId message; call
deduplicateByRequestId and assert that duplicates for the shared requestId are
reduced to the last entry (check uuid and usage.output_tokens), that
non-requestId messages remain present and in their original order, and
optionally assert reference equality for an untouched non-requestId message to
ensure it was not recreated. Use the same ParsedMessage shape as other tests and
name the test descriptively (e.g., "should preserve non-requestId messages while
deduplicating requestId entries").

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1233a6f and d24942b.

📒 Files selected for processing (4)
  • src/main/types/messages.ts
  • src/main/utils/jsonl.ts
  • src/renderer/utils/sessionAnalyzer.ts
  • test/main/utils/costCalculation.test.ts

@holstein13
Copy link
Contributor

Nice work on this! The deduplicateByRequestId() approach is clean, and keeping per-model stats accurate by skipping duplicates in the renderer loop is the right call.

We independently landed on the same streaming dedup fix in #79 (now closed in favor of this one). Our PR went a step further by making calculateMetrics() the single source of truth for costs — the renderer reads detail.metrics.costUsd and proc.metrics.costUsd instead of recomputing independently. That eliminates a separate issue where the cost analysis panel and chat header could show slightly different numbers. We've tracked that as #80 for follow-up work once this lands.

Thanks again — the numbers look much more reasonable now.

@matt1398
Copy link
Owner

PR #87 reverted PRs #60, #65, #73 which removes sessionAnalyzer.ts. The jsonl.ts dedup fix is still valid and wanted, but the sessionAnalyzer.ts changes in this PR will conflict.
Could you rebase on main (once #87 merges) and drop the sessionAnalyzer.ts hunks? The core fix in calculateMetrics() is the part we need.

@Psypeal Psypeal force-pushed the fix/cost-overcounting-dedup branch from d24942b to 3fd862a Compare February 26, 2026 08:10
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/utils/jsonl.ts`:
- Around line 287-288: Change the mutable declaration "let costUsd = 0;" to an
immutable "const costUsd = 0;" since costUsd is never reassigned, and update the
surrounding comment to avoid implying a per-message calculation that doesn't
exist—either remove the misleading "Calculate cost per-message, then sum" line
or replace it with a short TODO (e.g., "TODO: implement per-message cost
calculation") if you plan to implement it later; update the symbol referenced
("costUsd") and the nearby comment in jsonl.ts accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d24942b and 3fd862a.

📒 Files selected for processing (2)
  • src/main/types/messages.ts
  • src/main/utils/jsonl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/types/messages.ts

Comment on lines +287 to +288
// Calculate cost per-message, then sum (tiered pricing applies per-API-call, not to aggregated totals)
let costUsd = 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix let to const — pipeline failure.

costUsd is never reassigned, so it must be declared with const. Additionally, the comment mentions "Calculate cost per-message, then sum" but no cost calculation is implemented—the variable stays 0. Consider removing the misleading comment or adding a TODO if cost calculation is planned for a follow-up.

🔧 Proposed fix
-  // Calculate cost per-message, then sum (tiered pricing applies per-API-call, not to aggregated totals)
-  let costUsd = 0;
+  // Cost calculation deferred; costUsd remains undefined for now
+  const costUsd = 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Calculate cost per-message, then sum (tiered pricing applies per-API-call, not to aggregated totals)
let costUsd = 0;
// Cost calculation deferred; costUsd remains undefined for now
const costUsd = 0;
🧰 Tools
🪛 ESLint

[error] 288-288: 'costUsd' is never reassigned. Use 'const' instead.

(prefer-const)

🪛 GitHub Actions: CI

[error] 288-288: 'costUsd' is never reassigned. Use 'const' instead prefer-const

🪛 GitHub Check: validate

[failure] 288-288:
'costUsd' is never reassigned. Use 'const' instead

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/jsonl.ts` around lines 287 - 288, Change the mutable
declaration "let costUsd = 0;" to an immutable "const costUsd = 0;" since
costUsd is never reassigned, and update the surrounding comment to avoid
implying a per-message calculation that doesn't exist—either remove the
misleading "Calculate cost per-message, then sum" line or replace it with a
short TODO (e.g., "TODO: implement per-message cost calculation") if you plan to
implement it later; update the symbol referenced ("costUsd") and the nearby
comment in jsonl.ts accordingly.

…ting

Claude Code writes multiple JSONL entries per API response during streaming, each with the same requestId but incrementally increasing output_tokens. Our parser summed every entry independently, inflating costs by ~2x vs /cost.

Fix: keep only the last entry per requestId (the final, complete token counts).

- Add requestId to ParsedMessage type and populate from AssistantEntry
- Add deduplicateByRequestId() helper (last-entry-wins strategy)
- Apply dedup in calculateMetrics() before summing tokens/cost
- Apply same dedup in sessionAnalyzer cost accounting loop
- Add 6 test cases covering streaming dedup scenarios

Closes matt1398#74
@Psypeal Psypeal force-pushed the fix/cost-overcounting-dedup branch from 3fd862a to ef2e086 Compare February 26, 2026 08:17
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/main/utils/jsonl.ts (1)

235-250: Constrain dedup to assistant messages for future safety.

The function docs say “streaming assistant entries,” but implementation currently dedups any message with requestId. Narrowing scope prevents accidental filtering if other message types later carry that field.

♻️ Proposed refactor
 export function deduplicateByRequestId(messages: ParsedMessage[]): ParsedMessage[] {
   // Map from requestId -> index of last occurrence
   const lastIndexByRequestId = new Map<string, number>();
   for (let i = 0; i < messages.length; i++) {
-    const rid = messages[i].requestId;
+    const msg = messages[i];
+    const rid = msg.type === 'assistant' ? msg.requestId : undefined;
     if (rid) {
       lastIndexByRequestId.set(rid, i);
     }
   }

   // If no requestIds found, no dedup needed
   if (lastIndexByRequestId.size === 0) {
     return messages;
   }

   return messages.filter((msg, i) => {
-    if (!msg.requestId) return true;
+    if (msg.type !== 'assistant' || !msg.requestId) return true;
     return lastIndexByRequestId.get(msg.requestId) === i;
   });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/jsonl.ts` around lines 235 - 250, The dedup currently removes
any message with a requestId; change it so only assistant messages are
considered: when building lastIndexByRequestId only record indices for messages
where msg.requestId is set AND the message is an assistant (e.g., check
messages[i].role === 'assistant' or the project's assistant-type field), and in
the final filter allow non-assistant messages to pass through unmodified (i.e.,
only apply the lastIndexByRequestId check for assistant messages). Use the
existing identifiers (messages, requestId, lastIndexByRequestId) and update both
the initial loop and the filter predicate accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/utils/jsonl.ts`:
- Around line 235-250: The dedup currently removes any message with a requestId;
change it so only assistant messages are considered: when building
lastIndexByRequestId only record indices for messages where msg.requestId is set
AND the message is an assistant (e.g., check messages[i].role === 'assistant' or
the project's assistant-type field), and in the final filter allow non-assistant
messages to pass through unmodified (i.e., only apply the lastIndexByRequestId
check for assistant messages). Use the existing identifiers (messages,
requestId, lastIndexByRequestId) and update both the initial loop and the filter
predicate accordingly.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd862a and ef2e086.

📒 Files selected for processing (2)
  • src/main/types/messages.ts
  • src/main/utils/jsonl.ts

@Psypeal
Copy link
Contributor Author

Psypeal commented Feb 26, 2026

PR #87 reverted PRs #60, #65, #73 which removes sessionAnalyzer.ts. The jsonl.ts dedup fix is still valid and wanted, but the sessionAnalyzer.ts changes in this PR will conflict. Could you rebase on main (once #87 merges) and drop the sessionAnalyzer.ts hunks? The core fix in calculateMetrics() is the part we need.

Hi, matt, I have fixed the conflicts here, please check it.

@matt1398 matt1398 merged commit d86dab7 into matt1398:main Feb 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix cost overcounting — deduplicate streaming JSONL entries

3 participants