Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cli/src/claude/utils/chatVisibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})
98 changes: 98 additions & 0 deletions cli/src/claude/utils/sdkToLogConverter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
55 changes: 55 additions & 0 deletions cli/src/claude/utils/sdkToLogConverter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions shared/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ export function isClaudeChatVisibleSystemSubtype(subtype: unknown): subtype is s
}

export function isClaudeChatVisibleMessage(message: { type: unknown; subtype?: unknown }): boolean {
if (message.type === 'rate_limit_event') {
return false
}

if (message.type !== 'system') {
return true
}
Expand Down
Loading