mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-03 21:35:13 +00:00
Some checks are pending
CI / semgrep (push) Waiting to run
Some agents (Pi, and others for injected turns) write a message's `content` as a string instead of an array of blocks. Parsers did `(content ?? []).filter`, which throws on a string — and because the daily-cache backfill swallowed parse errors, one bad session silently wiped the entire trend/history. - Add normalizeContentBlocks(): string -> one text block, array -> as-is (by reference; drops null/undefined elements so the same crash can't happen one level down), else -> []. Applied across pi/codex/droid/cursor-agent and the Claude path in parser.ts. - Isolate per-file parse failures in parseProviderSources: skip the offending file (warn once per provider) instead of re-throwing and aborting the whole backfill. The stale cache entry is already cleared, so the file is excluded. - Surface backfill failures in hydrateCache via stderr instead of silently returning an empty cache. Likely fixes #425 (previous-day always 0) for the throwing-file cause. Tests: content-utils unit tests + a Pi string-content regression test.
26 lines
1.5 KiB
TypeScript
26 lines
1.5 KiB
TypeScript
/// Normalize a message's `content` into an array of content blocks.
|
|
///
|
|
/// Most agent session formats write `content` as an array of typed blocks
|
|
/// (`{ type: 'text' | 'tool_use' | ... }`), and the parsers filter over that
|
|
/// array. But some agents (Pi, and others for programmatically injected turns)
|
|
/// legitimately write `content` as a plain **string**. A raw string reaching
|
|
/// `.filter`/`.some` throws a TypeError mid-parse — and because the 365-day
|
|
/// daily-cache backfill swallows parse errors, that single bad record silently
|
|
/// wipes the entire trend/history (issue #441).
|
|
///
|
|
/// This coerces defensively: arrays pass through, a string becomes one text
|
|
/// block, and anything else (null/undefined/number/object) becomes empty.
|
|
export function normalizeContentBlocks<T extends { type?: string; text?: string }>(
|
|
content: T[] | string | null | undefined,
|
|
): T[] {
|
|
if (Array.isArray(content)) {
|
|
// A clean array (the overwhelming common case) is returned by reference — no
|
|
// copy. Only when an element is a non-object (null/undefined/primitive) do we
|
|
// filter, since the call sites read `.type` on each element and a null would
|
|
// throw — the same crash class this helper exists to prevent, one level down.
|
|
const isBlock = (b: T): boolean => b != null && typeof b === 'object'
|
|
return content.every(isBlock) ? content : content.filter(isBlock)
|
|
}
|
|
if (typeof content === 'string') return [{ type: 'text', text: content } as T]
|
|
return []
|
|
}
|