Skip to content

fix(inject): eliminate race condition in SSE token counting - #3

Merged
DevanshuNEU merged 1 commit into
OpenCodeIntel:mainfrom
DevanshuNEU:fix/inject-token-race-condition
Mar 23, 2026
Merged

fix(inject): eliminate race condition in SSE token counting#3
DevanshuNEU merged 1 commit into
OpenCodeIntel:mainfrom
DevanshuNEU:fix/inject-token-race-condition

Conversation

@DevanshuNEU

Copy link
Copy Markdown
Contributor

Summary

handleClaudeEvent was calling countTokens() as fire-and-forget per delta chunk, updating summary.outputTokens asynchronously. The finally block read summary.outputTokens immediately after the stream ended — before any of those .then() callbacks had resolved — resulting in consistently under-counted or zero output tokens in STREAM_COMPLETE.

Fix: accumulate all delta text synchronously into a string buffer during the stream, then make a single parallel BPE round-trip for both input and output tokens in the finally block before posting STREAM_COMPLETE. Intermediate batch flushes keep a chars/4 estimate for real-time UI updates.

Type of Change

  • fix — Bug fix

What Was Changed

  • entrypoints/inject.ts
    • handleClaudeEvent: removed per-chunk countTokens() call from content_block_delta; message_start now sets a synchronous chars/4 input estimate instead of firing an async count
    • decodeSSEStream: added outputTextBuffer string accumulator; each content_block_delta appends text and updates summary.outputTokens via chars/4; finally block awaits Promise.all([countTokens(promptText), countTokens(outputTextBuffer)]) and overwrites estimates with accurate BPE counts before STREAM_COMPLETE is posted

How to Test

  1. bun run compile — should exit clean
  2. bun run test — all 26 tests should pass
  3. Load the extension on claude.ai, send a message, and confirm the [LCO] [Complete] log line shows non-zero input and output token counts

Checklist

  • Tests pass locally (bun run test)
  • TypeScript compiles clean (bun run compile)
  • Extension builds without errors (bun run build)
  • No secrets, hardcoded URLs, or sensitive tokens exposed
  • Comments are professional and clear — no emojis, no AI-generated filler
  • Commit messages follow conventional commits (feat:, fix:, refactor:, test:, chore:)

Notes for Reviewer

The chars/4 approximation used during streaming is intentionally rough — it keeps the UI responsive without any async overhead. The accurate BPE count replaces it atomically in finally before STREAM_COMPLETE fires, so the final stored value is always the precise count. If countTokens times out (5s fallback), the chars/4 estimate is preserved rather than overwriting with zero.

@DevanshuNEU
DevanshuNEU merged commit 9ae250d into OpenCodeIntel:main Mar 23, 2026
1 check passed
DevanshuNEU added a commit that referenced this pull request May 4, 2026
…-36]

Rule 0 returns Healthy when turnCount <= 2 AND contextPct < 30,
before the per-model classifier runs. Blocks stale growthRate,
isDetailHeavy, and any future projection wrapper from escalating
a session that has no real history yet.

Acceptance criteria:
- Healthy on any conversation with turnCount <= 2 AND contextPct < 30
  regardless of prior tab/conversation state
- Wrappers like escalateForProjection still run on the returned
  HealthScore so a real draft can escalate after the guard
- AC #3 (overlay resets on new chat) already satisfied by existing
  SPA-nav reset path in claude-ai.content.ts (PR #29)

12 new tests: positive path (5), boundary (3), does-not-mask (4).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant