diff --git a/src/web/schemas.ts b/src/web/schemas.ts index f40a2a16..6a23a304 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -707,7 +707,16 @@ export const StatusTelemetrySchema = z.object({ miss_recache_tokens: z.number().nullish(), last_miss_at: z.number().nullish(), recache_tokens_if_cold: z.number().nullish(), - last_miss_cause: z.object({ causes: z.array(z.unknown()).max(20).nullish() }).nullish(), + // Keep the first 20 causes instead of 400-ing the whole post (which also drops the + // footer, the plan chip and the model report it carries) when Claude Code reports more. The + // preprocess slices BEFORE the array validates, so an over-long array costs one bounded + // slice, never per-element validation across every entry (parsePromptCache then keeps + // the string ones among those 20). + last_miss_cause: z + .object({ + causes: z.preprocess((v) => (Array.isArray(v) ? v.slice(0, 20) : v), z.array(z.unknown()).nullish()), + }) + .nullish(), }) .nullish(), }) diff --git a/test/routes/status-telemetry-routes.test.ts b/test/routes/status-telemetry-routes.test.ts index 4fa3f049..faa96ebd 100644 --- a/test/routes/status-telemetry-routes.test.ts +++ b/test/routes/status-telemetry-routes.test.ts @@ -154,6 +154,20 @@ describe('POST /api/status-telemetry', () => { expect(events.filter((e) => e === SessionPromptCache)).toHaveLength(1); }); + it('keeps the first 20 miss causes instead of refusing a post that carries more', async () => { + const causes = Array.from({ length: 25 }, (_, i) => `cause_${i}`); + const res = await post({ + sessionId: SID, + data: { ...REAL, prompt_cache: { warm: false, ttl: '1h', last_miss_cause: { causes } } }, + }); + // Accepted, not 400: an over-long causes array must not blank the whole statusline. + expect(res.statusCode).toBe(200); + const session = h.ctx.sessions.get(SID) as unknown as { promptCache: { lastMissCauses?: string[] } | null }; + expect(session.promptCache?.lastMissCauses).toHaveLength(20); + expect(session.promptCache?.lastMissCauses?.[0]).toBe('cause_0'); + expect(session.promptCache?.lastMissCauses?.[19]).toBe('cause_19'); + }); + it('rejects a malformed body (missing sessionId) with 400', async () => { const res = await post({ data: REAL }); expect(res.statusCode).toBe(400);