Merged
Size
L
Change Breakdown
Bug Fix65%
Refactor25%
Docs10%
#27624fix: Ensure monotonic message timestamps for agent (no-changelog)

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 lastCreatedAt while building the list.
  • For input and response messages: set createdAt to max(hint, lastCreatedAt + 1), where hint is an existing message time or Date.now().
  • For history (DB-loaded): keep timestamps exact; advance lastCreatedAt to the max so new live messages always sort strictly later (handles clock skew and prior monotonic runs).
  • deserialize() recomputes lastCreatedAt from 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
© 2026 · via Gitpulse