Severity: LOW. Found in the 2026-07-16 storage/AI sweep (deferred).
agent-do.ts fires maybeSummarize() fire-and-forget with no lock. The DO input gate lets two turns (e.g. concurrent WS + HTTP) interleave at await points, so both can cross the 20-message threshold and read the same lastSummary before either writes → two sum: rows for overlapping ranges + duplicate mem:fact:* writes.
Also a boundary off-by-one: maybeSummarize uses startAfter: msg:${lastTimestamp} where lastTimestamp is the prior summary's to. Since msg keys are msg:{ts}:{id}, the boundary message (msg:{ts}:{id} > msg:{ts}) is re-included → one message double-counted each window.
Files: workers/api/src/agent-do.ts (fire point), workers/api/src/agent-storage.ts (maybeSummarize, boundary).
Fix sketch: a lightweight in-DO lock/guard around summarization; store the boundary as the full msg key (or advance past {ts}:{id}) to exclude the already-summarized message.
Impact: duplicate summary rows + facts, one double-counted message. Not corruption. Deferred because the fix risks wedging summarization if the lock isn't released cleanly.
Severity: LOW. Found in the 2026-07-16 storage/AI sweep (deferred).
agent-do.tsfiresmaybeSummarize()fire-and-forget with no lock. The DO input gate lets two turns (e.g. concurrent WS + HTTP) interleave atawaitpoints, so both can cross the 20-message threshold and read the samelastSummarybefore either writes → twosum:rows for overlapping ranges + duplicatemem:fact:*writes.Also a boundary off-by-one:
maybeSummarizeusesstartAfter: msg:${lastTimestamp}wherelastTimestampis the prior summary'sto. Since msg keys aremsg:{ts}:{id}, the boundary message (msg:{ts}:{id}>msg:{ts}) is re-included → one message double-counted each window.Files:
workers/api/src/agent-do.ts(fire point),workers/api/src/agent-storage.ts(maybeSummarize, boundary).Fix sketch: a lightweight in-DO lock/guard around summarization; store the boundary as the full msg key (or advance past
{ts}:{id}) to exclude the already-summarized message.Impact: duplicate summary rows + facts, one double-counted message. Not corruption. Deferred because the fix risks wedging summarization if the lock isn't released cleanly.