mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-23 22:09:56 +00:00
* fix(cli): filter raw SSE event JSON from leaking into chat messages
Two types of internal JSON were appearing as visible text in Telegram
Mini App and web chat:
1. `rate_limit_event` — the rate limit parser returned `null` for
unknown statuses, causing raw JSON to pass through as assistant text.
Changed to `{ suppress: true }` so all rate_limit_event variants are
handled; new statuses that need display can be added explicitly.
2. `{ type: "output", data: { ... } }` — internal session metadata
envelopes leaked through the ACP text chunk pipeline. Added an
`isInternalEventJson` filter that catches JSON objects with known
internal envelope types (output, event, queue-operation) before they
enter the text buffer.
Closes #386
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): narrow internal event filter to match only leaked metadata shape
Address review feedback: the broad type-based filter could suppress
legitimate assistant JSON with type "event" or "queue-operation".
Narrow the check to only match the specific leaked metadata envelope:
{ type: "output", data: { parentUuid, sessionId, userType } }
Add negative tests confirming other JSON types pass through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): suppress malformed rate_limit_event without resetsAt
Address review: rate_limit_event payloads missing resetsAt still leaked
as raw JSON because parseRateLimitText returned null before reaching the
unknown-status suppress. Move the allowed check before the resetsAt
guard and suppress malformed payloads instead of passing them through.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): handle parentUuid: null in internal event filter
Root/first-message metadata envelopes have parentUuid: null rather than
a string, so the filter missed them. Accept both string and null.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* test(cli): add e2e regression tests for metadata envelope filtering
Add AcpMessageHandler tests that verify leaked { type: "output", data }
metadata envelopes (both parentUuid string and null) are dropped before
reaching the text buffer.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix when cumulative metadata chunk arrives
When a leaked metadata envelope arrives as cumulative streaming chunks
(first an incomplete JSON prefix, then the full blob), the filter
dropped the full chunk but left the prefix in bufferedText. Clear the
buffer when the detected internal JSON starts with the buffered prefix.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): round resetsAt to integer for pipe-delimited format
The web-side regex uses \d+ to parse the timestamp, so a float value
would silently fail to match. Apply Math.round to ensure integer output.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(cli): clear buffered prefix for cumulative rate_limit_event chunks
The prefix-clearing logic only applied to the isInternalEventJson
branch but not to the parseRateLimitText branch, so cumulative
rate_limit_event chunks could leave a raw JSON prefix in the buffer.
Hoist the prefix check before both filters and apply uniformly.
Add regression tests for suppressed and displayable cumulative
rate_limit_event scenarios.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: HAPI <noreply@hapi.run>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
import type { AgentMessage } from './types';
|
|
|
|
/**
|
|
* Detect rate_limit_event JSON in agent text output and convert to
|
|
* a standardized AgentMessage, so the web layer doesn't need to
|
|
* parse undocumented Claude-internal JSON formats.
|
|
*
|
|
* Converted text format (pipe-delimited, parsed by web's reducerEvents.ts):
|
|
* - "Claude AI usage limit warning|{unixSeconds}|{percentInt}|{rateLimitType}"
|
|
* - "Claude AI usage limit reached|{unixSeconds}|{rateLimitType}"
|
|
*
|
|
* Returns null if the text is not a rate_limit_event (pass through as-is).
|
|
* Returns { suppress: true } for known-noisy statuses (e.g. 'allowed').
|
|
* Returns { suppress: false, message } for statuses worth displaying.
|
|
*/
|
|
export type RateLimitResult =
|
|
| null
|
|
| { suppress: true }
|
|
| { suppress: false; message: AgentMessage };
|
|
|
|
export function parseRateLimitText(text: string): RateLimitResult {
|
|
if (text[0] !== '{') return null;
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (typeof parsed !== 'object' || parsed === null) return null;
|
|
|
|
// Unwrap { type: "output", data: { ... } } wrapper
|
|
const record = parsed as Record<string, unknown>;
|
|
let inner = record;
|
|
if (record.type === 'output' && typeof record.data === 'object' && record.data !== null) {
|
|
inner = record.data as Record<string, unknown>;
|
|
}
|
|
|
|
if (inner.type !== 'rate_limit_event') return null;
|
|
|
|
const info = inner.rate_limit_info;
|
|
if (typeof info !== 'object' || info === null) return null;
|
|
|
|
const { status, resetsAt, utilization, rateLimitType } = info as Record<string, unknown>;
|
|
|
|
// Suppress early for statuses that never need display,
|
|
// before checking resetsAt — malformed payloads should not leak.
|
|
if (status === 'allowed') {
|
|
return { suppress: true };
|
|
}
|
|
|
|
if (typeof resetsAt !== 'number') {
|
|
// Malformed rate_limit_event (missing resetsAt) — suppress to prevent
|
|
// raw JSON from leaking into chat.
|
|
return { suppress: true };
|
|
}
|
|
|
|
// Ensure integer for the pipe-delimited format (web regex uses \d+)
|
|
const resetsAtInt = Math.round(resetsAt);
|
|
|
|
if (status === 'allowed_warning') {
|
|
const pct = typeof utilization === 'number' ? Math.round(utilization * 100) : 0;
|
|
const limitType = typeof rateLimitType === 'string' ? rateLimitType : '';
|
|
return {
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: `Claude AI usage limit warning|${resetsAtInt}|${pct}|${limitType}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
if (status === 'rejected') {
|
|
const limitType = typeof rateLimitType === 'string' ? rateLimitType : '';
|
|
return {
|
|
suppress: false,
|
|
message: {
|
|
type: 'text',
|
|
text: `Claude AI usage limit reached|${resetsAtInt}|${limitType}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
// Unknown status — suppress to prevent raw JSON from leaking into chat.
|
|
// If a new status needs to be displayed, add an explicit branch above.
|
|
return { suppress: true };
|
|
}
|