Message timestamps now enforce strict ordering
Agent message history will no longer shuffle or reorder between turns or after suspend/resume, thanks to monotonic timestamp enforcement in the message list.
When multiple agent messages were created in the same millisecond, they shared a createdAt timestamp. Reloading message history from storage—Postgres, SQLite, or in-memory—could return these messages in any order, making conversations appear scrambled or loop through states unpredictably.
The fix lives in AgentMessageList, which now tracks the last assigned timestamp and ensures every new message gets a strictly later value. Live messages use max(existing_hint, lastCreatedAt + 1); history loaded from the database keeps its original timestamp but advances lastCreatedAt so subsequent messages still sort strictly after. The deserialize() method recomputes the tracking value from restored messages, so conversations resume in the correct sequence.
Storage backends now persist each message's own createdAt rather than overwriting it with the current time, keeping the sort key authoritative across save/load cycles.
View Original GitHub Description
Summary
Why message history looked shuffled
Several messages in one agent turn could end up with the same createdAt (the same Date.now() millisecond) or with timestamps that did not follow append order. Thread history loaded from storage is ordered by createdAt (Postgres/SQLite also use a seq tiebreaker). Duplicate or inconsistent times still produce unstable ordering for anything that sorts by time only. InMemoryMemory used the stored timestamp for filtering and replay; pagination with before / limit could slice the wrong boundary—messages appeared to reorder between turns or after suspend/resume.
Fix: monotonic timestamps in AgentMessageList
- Track
lastCreatedAtwhile building the list. - For
inputandresponsemessages: setcreatedAttomax(hint, lastCreatedAt + 1), wherehintis an existing message time orDate.now(). - For
history(DB-loaded): keep timestamps exact; advancelastCreatedAtto the max so new live messages always sort strictly later (handles clock skew and prior monotonic runs). deserialize()recomputeslastCreatedAtfrom restored messages so continuation after checkpoint stays ordered.
Persistence (InMemoryMemory, Postgres, SQLite) stores the message-owned createdAt so reloads match the list. Design notes: packages/@n8n/agents/docs/agent-runtime-architecture.md (Design decisions: Monotonic createdAt for persisted order).
Related Linear tickets, Github issues, and Community forum posts
Review / Merge checklist
- PR title and summary are descriptive. (conventions)
- Docs updated in-repo (
agent-runtime-architecture.md) for this behavior - Tests included
- PR labeled for backport if urgent