Surfaced by the 3e (plan checklist) whole-branch review as pre-existing and deliberately left out of scope. Same shape as the eviction bug 3e fixed for plan entries — it has now bitten three times, so it deserves its own pass.
1. Unbounded transcript fields
src/plugins/ai-agent/state/persistence.ts clamps some strings but not all. Measured on the current code:
| Field |
Kind |
Measured size |
tool.tool |
tool |
300,055 bytes |
attachment.connectionName |
user |
300,117 bytes |
plan entries got a structural byte cap in 3e; tool and user did not. A single oversized entry inflates the persisted conversation and can push out real history.
2. clampToLimit throws on a non-string
text and detail have the identical exposure. Blast radius is bounded by agentStore.ts's try/catch — the failure mode is losing the conversation, not corrupting it — but a validator that throws on hostile input is the wrong default.
Scope
- Bound every persisted string by construction, not per-field opt-in — an explicit field set, so a new field is bounded by default rather than by remembering.
- Make
clampToLimit total (non-string → rejected entry, not a throw).
- Pin each bound with a mutation-proven test; assert the measured byte size rather than asserting it by construction (a 3d lesson).
Reference
.superpowers/sdd/3e/whole-branch-review.md §5 row 27, and the explanatory comment 3e left at the site in persistence.ts.
Surfaced by the 3e (plan checklist) whole-branch review as pre-existing and deliberately left out of scope. Same shape as the eviction bug 3e fixed for
planentries — it has now bitten three times, so it deserves its own pass.1. Unbounded transcript fields
src/plugins/ai-agent/state/persistence.tsclamps some strings but not all. Measured on the current code:tool.tooltoolattachment.connectionNameuserplanentries got a structural byte cap in 3e;toolanduserdid not. A single oversized entry inflates the persisted conversation and can push out real history.2.
clampToLimitthrows on a non-stringtextanddetailhave the identical exposure. Blast radius is bounded byagentStore.ts'stry/catch— the failure mode is losing the conversation, not corrupting it — but a validator that throws on hostile input is the wrong default.Scope
clampToLimittotal (non-string → rejected entry, not a throw).Reference
.superpowers/sdd/3e/whole-branch-review.md§5 row 27, and the explanatory comment 3e left at the site inpersistence.ts.