Skip to content

fix: filter rate_limit_event from Claude Remote/Local chat paths#423

Merged
tiann merged 4 commits intotiann:mainfrom
junmo-kim:fix/rate-limit-claude-path-leak
Apr 9, 2026
Merged

fix: filter rate_limit_event from Claude Remote/Local chat paths#423
tiann merged 4 commits intotiann:mainfrom
junmo-kim:fix/rate-limit-claude-path-leak

Conversation

@junmo-kim
Copy link
Copy Markdown
Contributor

Summary

PR #388 handled rate_limit_event display for the ACP path, but Claude Remote (SDK) and Local (JSONL scanner) paths still leak the raw JSON into the web UI chat.

This adds filtering at two layers:

  • sdkToLogConverter.convert() — early return before UUID generation and sidechain bookkeeping, preventing the event from reaching the web via the remote SDK path
  • isClaudeChatVisibleMessage() — marks rate_limit_event as non-visible, covering the local session scanner path and acting as a second line of defense

Test plan

  • Unit: rate_limit_event returns null from converter
  • Unit: dropped event does not break parent UUID chain
  • Unit: isClaudeChatVisibleMessage({ type: 'rate_limit_event' }) returns false
  • E2E: start a new Claude session, send a message, verify no raw JSON in chat

Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • No findings on the added/modified lines.

Summary

  • Review mode: initial
  • No issues found in the current diff.
  • Residual risk: the new shared/src/messages.ts branch is covered indirectly through CLI tests, but there is still no direct shared/web regression coverage for that consumer path.

Testing

  • Not run (automation environment: bun was not available, so the targeted Vitest files could not be executed)

HAPI Bot

Claude SDK emits rate_limit_event messages that are not recognized by
the web normalizer, causing them to fall through to the safeStringify
fallback and render as raw JSON in the chat UI.

Add an early return in isClaudeChatVisibleMessage to mark
rate_limit_event as non-visible, preventing it from reaching the
web layer via the local session scanner path.
…erter

Guard against rate_limit_event at the top of convert(), before UUID
generation and sidechain bookkeeping. This prevents phantom UUIDs from
corrupting the parent chain when a rate_limit_event carries a
parent_tool_use_id, and stops the event from reaching the web UI via
the remote SDK message path.

Includes parent-chain-integrity test to verify dropped events do not
break UUID continuity.
@junmo-kim junmo-kim force-pushed the fix/rate-limit-claude-path-leak branch from 1792f82 to 6430850 Compare April 9, 2026 03:38
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Displayable Claude rate-limit statuses are now dropped instead of converted into the event format the web already understands, so allowed_warning / rejected disappear entirely on the SDK path. Evidence cli/src/claude/utils/sdkToLogConverter.ts:93, shared/src/messages.ts:42, cli/src/agent/rateLimitParser.ts:61, web/src/chat/reducerEvents.ts:3
    Suggested fix:
    if (sdkMessage.type === 'rate_limit_event') {
        const text = formatClaudeRateLimitEvent((sdkMessage as any).rate_limit_info)
        if (!text) return null
    
        return {
            ...baseFields,
            type: 'assistant',
            message: {
                role: 'assistant',
                content: [{ type: 'text', text }]
            }
        }
    }

Summary
Review mode: follow-up after new commits

  • 1 major issue found.
  • The raw-JSON leak is blocked, but this implementation also removes Claude usage-limit warnings/rejections on the structured SDK path instead of translating them into the existing UI event format.

Testing

  • Not run (automation environment: bun was not available, so the targeted Vitest files could not be executed)

HAPI Bot

* Convert SDK message to log format
*/
convert(sdkMessage: SDKMessage): RawJSONLines | null {
if (sdkMessage.type === 'rate_limit_event') return null
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] This drops every rate_limit_event, including the displayable allowed_warning / rejected cases. ACP already converts those statuses into the Claude AI usage limit ... text that the web reducer turns into limit-warning / limit-reached events (cli/src/agent/rateLimitParser.ts:61, web/src/chat/reducerEvents.ts:3). Returning null here fixes the raw-JSON leak by removing the warning entirely on the SDK path.

Suggested fix:

if (sdkMessage.type === 'rate_limit_event') {
    const text = formatClaudeRateLimitEvent((sdkMessage as any).rate_limit_info)
    if (!text) return null

    return {
        ...baseFields,
        type: 'assistant',
        message: {
            role: 'assistant',
            content: [{ type: 'text', text }]
        }
    }
}

The previous commit suppressed all rate_limit_event messages, but
allowed_warning and rejected statuses should be converted to the
pipe-delimited text format the web layer already understands.

- allowed: suppress (no display needed)
- allowed_warning → "Claude AI usage limit warning|{resetsAt}|{pct}|{type}"
- rejected → "Claude AI usage limit reached|{resetsAt}|{type}"

Addresses review feedback on PR tiann#423.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • [Major] Displayable SDK rate_limit_event messages now self-parent. convertRateLimitEvent() assigns this.lastUuid = uuid before filling parentUuid, so every allowed_warning / rejected message points to its own UUID instead of the previous message. That breaks the converter's existing parent-chain invariant from cli/src/claude/utils/sdkToLogConverter.test.ts:222, and the new tests only cover parent preservation for suppressed events at cli/src/claude/utils/sdkToLogConverter.test.ts:325.
    Suggested fix:
    const parentUuid = this.lastUuid
    const uuid = randomUUID()
    const timestamp = new Date().toISOString()
    this.lastUuid = uuid
    
    return {
        parentUuid,
        ...
    }

Summary
Review mode: follow-up after new commits

  • 1 major issue found.
  • The latest diff fixes the earlier warning/rejected drop, but it now emits a self-referential parent link for each displayable SDK rate-limit event.

Testing

  • Not run (automation environment: bun was not installed, so the targeted Vitest files could not be executed)

HAPI Bot

this.lastUuid = uuid

return {
parentUuid: this.lastUuid,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[MAJOR] this.lastUuid has already been overwritten with uuid on the line above, so this stores the new message's own UUID in parentUuid. Every displayable allowed_warning / rejected event becomes self-parented instead of pointing at the previous message, which breaks the converter's existing parent-chain invariant (cli/src/claude/utils/sdkToLogConverter.test.ts:222). The added tests only cover the suppressed-event chain (cli/src/claude/utils/sdkToLogConverter.test.ts:325).

Suggested fix:

const parentUuid = this.lastUuid
const uuid = randomUUID()
const timestamp = new Date().toISOString()
this.lastUuid = uuid

return {
    parentUuid,
    ...
}

The previous commit set this.lastUuid before reading it for parentUuid,
causing every converted rate_limit_event to be a self-referencing node
(parentUuid === uuid). Capture parentUuid before assigning the new uuid,
matching the ordering used by the main convert() method.

Add test for parent chain continuity through converted (non-suppressed)
rate_limit_event messages.
Copy link
Copy Markdown

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

Findings

  • No actionable issues found in the latest head.

Summary
Review mode: follow-up after new commits

  • No issues found in the current diff after checking the touched Claude SDK conversion, shared visibility filter, and existing web event parsing path.
  • Residual risk: static review only; the targeted Vitest files were not executed because bun is not installed in this automation environment.

Testing

  • Not run (automation environment: bun not installed)

HAPI Bot

@tiann tiann merged commit 2eae161 into tiann:main Apr 9, 2026
2 checks passed
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.

2 participants