fix(agents): normalize non-array tool-result content at transcript ingest#98891
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 2, 2026, 12:14 AM ET / 04:14 UTC. Summary PR surface: Source +6, Tests +197. Total +203 across 2 files. Reproducibility: yes. Source inspection gives a high-confidence current-main reproduction path: JSONL session load preserves malformed Review metrics: 1 noteworthy metric.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land this ingest-boundary normalization if exact-head CI stays green, then close the canonical issue and reconcile the broader overlapping PRs as superseded or follow-up only if maintainers want a wider provider-helper contract. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection gives a high-confidence current-main reproduction path: JSONL session load preserves malformed Is this the best way to solve the issue? Yes. Normalizing once at the JSONL ingest boundary is the best narrow fix for persisted-session replay because it restores the canonical AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6e79ca3cbc7d. Label changesLabel justifications:
Evidence reviewedPR surface: Source +6, Tests +197. Total +203 across 2 files. View PR surface stats
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
8b2d871 to
dc5e04e
Compare
|
Landed via squash onto
|
What Problem This Solves
Fixes #98825 — after #97742, a tool result whose persisted
contentis a plain string (JSONL transcript passthrough) replays as empty tool output.extractToolResultText()iterates its input; a string iterates character-by-character, every 1-char primitive fails the object-block check, and the extracted replay text is"". The tool run succeeds, but the model's next turn sees nothing (src/llm/providers/tool-result-text.ts, consumed by all provider converters and transport streams).The same type lie also crashes the paths that call
msg.content.some(...)/.filter(...)(openai-completions,openai-responses-shared,mistral,openai-transport-stream, vision-model Google paths).Why This Change Was Made
ToolResultMessage.contentis typed(TextContent | ImageContent)[](packages/llm-core/src/types.ts), but session JSONL load passed whatever the file contained straight through. The repo already normalizes the identical case for assistant content (src/llm/providers/transform-messages.ts— "JSONL passthrough") — tool results never got the same treatment, andtransformMessageswouldn't be enough anyway since transport streams don't call it.This PR fixes the lie once at the ingest boundary instead of teaching every consumer to tolerate it: when a session entry is parsed from JSONL, toolResult string
contentbecomes[{ type: "text", text }]and a single non-array object becomes[object]. Every downstream consumer — provider converters, transport streams, the.some()/.filter()sites, and the shared helpers — keeps receiving the canonical typed array with no signature changes.Alternative approach considered: #98826 / #98838 / #98863 widen the shared helpers to accept
unknownand normalize inside them. That keepsToolResultMessage.contentdishonest, spreads tolerance across consumers (plus a newextractToolResultImageBlockshelper for the.some()sites), and needsas nevercasts in tests to exercise states the type system says can't exist. Normalizing at ingest deletes the problem instead; helper signatures stayreadonly unknown[].Also dedups
parseSessionEntriesintoparseJsonlEntries(their bodies were identical), so all session-file readers (SessionManager.open, transcript file state, btw transcripts, CLI session history) share the one normalizing parser. Net prod diff is ±0 LOC.User Impact
(no output)— the "tool ran fine but the model went blank" bug from bug(llm): tool-result replay can drop non-array output content #98825.Thanks @snowzlmbot for the report and analysis in #98825.
Evidence
{ output: ... }object content loads as a one-element array (src/agents/sessions/session-manager.tool-result-replay.test.ts).streamAnthropicproduces the string astool_resultcontent — red-checked on the pre-fix tree, where the Anthropic payload carried an empty string.node scripts/run-vitest.mjs src/agents/sessions/session-manager.tool-result-replay.test.ts src/agents/sessions/session-manager.test.ts src/agents/embedded-agent-runner/transcript-file-state.test.ts— 76 tests passing.parseSessionEntriesdedup:node scripts/run-vitest.mjs src/agents/compaction.test.ts src/agents/cli-runner/session-history.test.ts— 38 tests passing.