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

This commit is contained in:
Junmo Kim
2026-04-09 20:16:34 +08:00
committed by GitHub
parent f1daed80d6
commit 2eae161139
4 changed files with 161 additions and 0 deletions
@@ -20,4 +20,8 @@ describe('isClaudeChatVisibleMessage', () => {
expect(isClaudeChatVisibleMessage({ type: 'assistant' })).toBe(true)
expect(isClaudeChatVisibleMessage({ type: 'summary' })).toBe(true)
})
it('hides rate_limit_event messages from chat delivery', () => {
expect(isClaudeChatVisibleMessage({ type: 'rate_limit_event' } as any)).toBe(false)
})
})
@@ -269,6 +269,104 @@ describe('SDKToLogConverter', () => {
})
})
describe('Internal event filtering', () => {
it('should suppress rate_limit_event with allowed status', () => {
const sdkMessage = {
type: 'rate_limit_event',
rate_limit_info: {
status: 'allowed',
resetsAt: 1775559600,
rateLimitType: 'five_hour'
}
} as unknown as SDKMessage
expect(converter.convert(sdkMessage)).toBeNull()
})
it('should convert allowed_warning to pipe-delimited text', () => {
const sdkMessage = {
type: 'rate_limit_event',
rate_limit_info: {
status: 'allowed_warning',
resetsAt: 1775559600,
utilization: 0.85,
rateLimitType: 'five_hour'
}
} as unknown as SDKMessage
const logMessage = converter.convert(sdkMessage)
expect(logMessage).not.toBeNull()
expect(logMessage!.type).toBe('assistant')
expect((logMessage as any).message.content[0].text).toBe(
'Claude AI usage limit warning|1775559600|85|five_hour'
)
})
it('should convert rejected to pipe-delimited text', () => {
const sdkMessage = {
type: 'rate_limit_event',
rate_limit_info: {
status: 'rejected',
resetsAt: 1775559600,
rateLimitType: 'five_hour'
}
} as unknown as SDKMessage
const logMessage = converter.convert(sdkMessage)
expect(logMessage).not.toBeNull()
expect(logMessage!.type).toBe('assistant')
expect((logMessage as any).message.content[0].text).toBe(
'Claude AI usage limit reached|1775559600|five_hour'
)
})
it('should not break parent chain when rate_limit_event is suppressed', () => {
const user = converter.convert({
type: 'user',
message: { role: 'user', content: 'hi' }
} as SDKUserMessage)
converter.convert({
type: 'rate_limit_event',
rate_limit_info: { status: 'allowed' }
} as unknown as SDKMessage)
const assistant = converter.convert({
type: 'assistant',
message: { role: 'assistant', content: [{ type: 'text', text: 'hello' }] }
} as SDKAssistantMessage)
expect(assistant!.parentUuid).toBe(user!.uuid)
})
it('should chain parent correctly when rate_limit_event is converted', () => {
const user = converter.convert({
type: 'user',
message: { role: 'user', content: 'hi' }
} as SDKUserMessage)
const warning = converter.convert({
type: 'rate_limit_event',
rate_limit_info: {
status: 'allowed_warning',
resetsAt: 1775559600,
utilization: 0.8,
rateLimitType: 'five_hour'
}
} as unknown as SDKMessage)
const assistant = converter.convert({
type: 'assistant',
message: { role: 'assistant', content: [{ type: 'text', text: 'hello' }] }
} as SDKAssistantMessage)
expect(warning!.parentUuid).toBe(user!.uuid)
expect(assistant!.parentUuid).toBe(warning!.uuid)
})
})
describe('Convenience function', () => {
it('should convert single message without state', () => {
const sdkMessage: SDKUserMessage = {
+55
View File
@@ -86,10 +86,65 @@ export class SDKToLogConverter {
this.context.parentUuid = null
}
/**
* Convert rate_limit_event to pipe-delimited text matching the ACP path format,
* or suppress if the status does not need display (e.g. 'allowed').
* Must not mutate converter state (UUID chain) so dropped events are invisible.
*/
private convertRateLimitEvent(sdkMessage: SDKMessage): RawJSONLines | null {
const info = (sdkMessage as any).rate_limit_info
if (typeof info !== 'object' || info === null) return null
const { status, resetsAt, utilization, rateLimitType } = info
if (status === 'allowed') return null
if (typeof resetsAt !== 'number') return null
const resetsAtInt = Math.round(resetsAt)
let text: string
if (status === 'allowed_warning') {
const pct = typeof utilization === 'number' ? Math.round(utilization * 100) : 0
const limitType = typeof rateLimitType === 'string' ? rateLimitType : ''
text = `Claude AI usage limit warning|${resetsAtInt}|${pct}|${limitType}`
} else if (status === 'rejected') {
const limitType = typeof rateLimitType === 'string' ? rateLimitType : ''
text = `Claude AI usage limit reached|${resetsAtInt}|${limitType}`
} else {
return null
}
const parentUuid = this.lastUuid
const uuid = randomUUID()
const timestamp = new Date().toISOString()
this.lastUuid = uuid
return {
parentUuid,
isSidechain: false,
userType: 'external' as const,
cwd: this.context.cwd,
sessionId: this.context.sessionId,
version: this.context.version,
gitBranch: this.context.gitBranch,
uuid,
timestamp,
type: 'assistant',
message: {
role: 'assistant',
content: [{ type: 'text', text }]
}
} as RawJSONLines
}
/**
* Convert SDK message to log format
*/
convert(sdkMessage: SDKMessage): RawJSONLines | null {
if (sdkMessage.type === 'rate_limit_event') {
return this.convertRateLimitEvent(sdkMessage)
}
const uuid = randomUUID()
const timestamp = new Date().toISOString()
let parentUuid = this.lastUuid;