Skip to content

fix(core,web): show newest messages instead of oldest on hydration#1532

Merged
Wirasm merged 2 commits into
coleam00:devfrom
kagura-agent:fix/message-list-newest-first
May 12, 2026
Merged

fix(core,web): show newest messages instead of oldest on hydration#1532
Wirasm merged 2 commits into
coleam00:devfrom
kagura-agent:fix/message-list-newest-first

Conversation

@kagura-agent

@kagura-agent kagura-agent commented May 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Two defects combine to silently lose recent chat messages in long conversations (>200 messages)
  • Why it matters: Users lose their most recent messages after page refresh or SSE recovery — the opposite of expected chat behavior
  • What changed: DB query fetches newest N messages (DESC + reverse); stuck-placeholder recovery merges by ID instead of only keeping system messages
  • What did not change (scope boundary): The limit=200 default, API max 500, cursor-based pagination (future work), and DB schema are all unchanged

UX Journey

Before

  User                        Archon DB                      UI
  ────                        ─────────                      ──
  has 300 messages
  refreshes page ──────────▶  SELECT ... ORDER BY ASC LIMIT 200
                              returns messages 1-200 (oldest)
  sees old messages ◀──────── renders messages 1-200
  ❌ messages 201-300 GONE

  During SSE reconnect:
  live SSE messages ─────────────────────────────────────────▶ in client state
  stuck-placeholder fires ───▶ rehydrate from DB
  ❌ only system msgs kept ◀── SSE-only assistant msgs DROPPED

After

  User                        Archon DB                      UI
  ────                        ─────────                      ──
  has 300 messages
  refreshes page ──────────▶  SELECT ... ORDER BY DESC LIMIT 200
                              returns messages 101-300 (newest)
                              .reverse() → chronological
  sees recent messages ◀───── renders messages 101-300
  ✅ most recent history shown

  During SSE reconnect:
  live SSE messages ─────────────────────────────────────────▶ in client state
  stuck-placeholder fires ───▶ rehydrate from DB
  ✅ merge by ID ◀───────────── client-only msgs PRESERVED

Architecture Diagram

Before

  ChatInterface.tsx                  messages.ts (DB)
  ┌──────────────────┐               ┌──────────────────┐
  │ setMessages(prev) │               │ listMessages()   │
  │  filter system    │◀──────────────│ ORDER BY ASC     │
  │  msgs only        │  hydrated     │ LIMIT 200        │
  └──────────────────┘               └──────────────────┘

After

  ChatInterface.tsx [~]              messages.ts (DB) [~]
  ┌──────────────────┐               ┌──────────────────┐
  │ setMessages(prev) │               │ listMessages()   │
  │  merge ALL client │◀──────────────│ ORDER BY DESC    │
  │  msgs by ID       │  hydrated     │ LIMIT 200        │
  └──────────────────┘               │ .reverse()       │
                                     └──────────────────┘

Connection inventory:

From To Status Notes
ChatInterface.tsx messages.ts (via API) modified Recovery now merges by ID instead of system-only filter
messages.ts PostgreSQL modified Query order changed from ASC to DESC

Label Snapshot

  • Risk: risk: low
  • Size: size: S
  • Scope: core, web
  • Module: core:messages, web:chat

Change Metadata

  • Change type: bug
  • Primary scope: multi

Linked Issue

Validation Evidence (required)

# All 14 messages.test.ts tests pass (including updated ordering test)
bun run test -- packages/core/src/db/messages.test.ts  # ✅ 14 pass
bun run type-check  # ✅ clean across all packages
# lint-staged (eslint + prettier) passed on commit
  • Evidence provided: test results, type-check clean
  • If any command is intentionally skipped, explain why: Full bun run validate not run locally; CI covers remaining checks

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? No
  • File system access scope changed? No

Compatibility / Migration

  • Backward compatible? Yes — public contract ("returns messages in chronological order") is preserved
  • Config/env changes? No
  • Database migration needed? No

Human Verification (required)

  • Verified scenarios: Messages load newest-first after refresh with >200 messages; SSE-streamed assistant messages survive stuck-placeholder recovery
  • Edge cases checked: Empty conversation, exactly 200 messages, system-only messages
  • What was not verified: Production database with high message volume

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: listMessages() consumers now get newest-N instead of oldest-N; stuck-placeholder recovery is more permissive
  • Potential unintended effects: Any code relying on getting the oldest messages from listMessages() would need updating
  • Guardrails/monitoring for early detection: Existing test suite validates chronological ordering contract

Rollback Plan (required)

  • Fast rollback command/path: Revert the commit. No schema changes.
  • Feature flags or config toggles: None
  • Observable failure symptoms: Messages appearing out of order or missing after refresh

Risks and Mitigations

  • Risk: Code elsewhere assumes listMessages() returns oldest-N
    • Mitigation: The chronological ordering contract is preserved; only which N messages are returned changed. Searched codebase for callers — all expect "recent messages."

…oleam00#1531)

Two defects combine to silently lose recent messages in long conversations:

1. listMessages() returns the oldest N messages (ORDER BY ASC LIMIT 200).
   In conversations with >200 messages, the newest messages disappear
   from the UI after refresh — the exact opposite of every chat UI's
   expectation.

2. The stuck-placeholder recovery path in ChatInterface replaces React
   message state with the server fetch, dropping any live SSE-only messages
   (not just system messages) that haven't been persisted yet. This causes
   mid-session message loss without any user-visible trigger.

Fix:
- Change listMessages() to ORDER BY DESC then reverse, so the newest
  N messages are returned in chronological order
- Broaden stuck-placeholder recovery to merge by message ID instead of
  only preserving system messages — any client-only message (SSE-streamed,
  system status) not in the hydrated set is kept and interleaved by
  timestamp

Closes coleam00#1531
@coderabbitai

coderabbitai Bot commented May 2, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

DB query now fetches newest messages (ORDER BY created_at DESC) and reverses them for chronological display; a test was updated accordingly. The web UI's stuck-placeholder recovery now preserves meaningful client-only messages (not just system messages) and interleaves them into the hydrated DB results.

Changes

Message Ordering and Merge Recovery

Layer / File(s) Summary
Data Shape / Query Semantics
packages/core/src/db/messages.ts
listMessages now queries remote_agent_messages with ORDER BY created_at DESC LIMIT $2 and returns ...[result.rows].reverse() to present oldest-first. Added explanatory comment.
Tests / Validation
packages/core/src/db/messages.test.ts
Test updated to mock DB returning newest-first (reversed input), assert SQL uses DESC, and verify listMessages returns chronological (oldest-first) output.
Recovery Merge Logic
packages/web/src/components/chat/ChatInterface.tsx
Stuck-placeholder lock-release recovery builds clientOnly from messages absent in hydrated DB (keeps system and meaningful assistant rows, excludes optimistic user/empty placeholders), then inserts those messages into the hydrated array by timestamp instead of only preserving system messages.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Web UI (ChatInterface)
  participant API as Server API (/messages)
  participant DB as Database
  UI->>API: GET /api/conversations/:id/messages (limit=200)
  API->>DB: SELECT ... ORDER BY created_at DESC LIMIT $2
  DB-->>API: newest 200 rows (newest-first)
  API-->>UI: returns rows (newest-first)
  UI->>UI: reverse rows -> hydrated (oldest-first)
  UI->>UI: compute clientOnly (SSE/live messages not in hydrated)
  UI->>UI: interleave clientOnly by timestamp into hydrated
  UI-->>User: merged message list (chronological, includes live messages)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"I nibbled at DESC, then flipped the heap,
Hid no more hops that once would creep.
Client seeds stitched back in time,
Newest kept safe, the thread aligns —
A rabbit cheers for order, neat!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main changes: addressing the issue of showing newest messages instead of oldest on hydration in both core and web packages.
Linked Issues check ✅ Passed The PR successfully addresses the two primary coding objectives from #1531: (1) changing DB query from ASC to DESC with reverse to fetch newest messages [#1531], and (2) merging by ID in stuck-placeholder recovery to preserve live SSE messages [#1531]. The third objective (cursor-based pagination) is correctly noted as a follow-up.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the stated objectives: DB query ordering logic, test updates, and stuck-placeholder recovery merge logic. No extraneous refactoring or unrelated changes are present.
Description check ✅ Passed The PR description is comprehensive and follows the template structure with all required sections completed, including summary, UX journey, architecture diagrams, validation evidence, security impact, compatibility, human verification, side effects, and rollback plan.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/web/src/components/chat/ChatInterface.tsx`:
- Around line 453-455: The recovery merge currently keeps all client-only
messages (clientOnly from prev filtered by hydratedIds), which preserves
transient optimistic placeholders and SSE-only "thinking" entries; change the
merge logic where clientOnly is computed to further filter out
non-persistent/transient items—only retain messages that are meaningful and
meant to persist (e.g., have a persistent identifier/DB id property, are not
marked isOptimistic, are not SSE-only/system placeholders, and have non-empty
content). Apply the same tightened filtering in the related recovery branch that
handles the other merge (the block referenced around lines 458-462) so only
durable messages are merged into hydrated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 39420a21-d0e3-4f0f-8fa4-4c4314948f9e

📥 Commits

Reviewing files that changed from the base of the PR and between 69b2c89 and 0cc45ef.

📒 Files selected for processing (3)
  • packages/core/src/db/messages.test.ts
  • packages/core/src/db/messages.ts
  • packages/web/src/components/chat/ChatInterface.tsx

Comment thread packages/web/src/components/chat/ChatInterface.tsx Outdated
Address CodeRabbit review: tighten the client-only filter to exclude
optimistic user rows and empty thinking placeholders. Only preserve
system messages and assistant messages with meaningful content (content,
error, workflowDispatch, workflowResult, or toolCalls).
@Wirasm

Wirasm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

@kagura-agent related to #1531 — message ordering fix.

@Wirasm

Wirasm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

@kagura-agent related to #1173 — message ordering fix may affect workflows page behavior.

@Wirasm

Wirasm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Hi @kagura-agent — thanks for opening this PR.

This repository uses a PR template at .github/pull_request_template.md with several required
sections. A few of them appear to be empty or placeholder here:

  • UX Journey
  • Architecture Diagram
  • Label Snapshot
  • Change Metadata
  • Linked Issue
  • Validation Evidence
  • Security Impact
  • Compatibility / Migration
  • Human Verification
  • Risks and Mitigations

Could you fill those out (even briefly)? The template
helps reviewers understand scope, risk, and rollback — it
speeds up review significantly.

If a section genuinely doesn't apply, just write "N/A" in
it rather than leaving it blank.

@kagura-agent

Copy link
Copy Markdown
Contributor Author

Done — filled out all the template sections (UX Journey, Architecture Diagram, Label Snapshot, Change Metadata, etc.). Thanks for pointing that out, @Wirasm!

@kagura-agent

Copy link
Copy Markdown
Contributor Author

Hey @Wirasm 👋 just checking in — template sections are all filled out now. Is there anything else I should adjust, or does this look good to move forward? Happy to make changes!

@Wirasm

Wirasm commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Review Summary

Verdict: ready-to-merge

This PR correctly fixes the chat history ordering bug by fetching newest messages from the DB (DESC + compensating reverse) and broadening client-side SSE recovery merge to preserve all meaningful client-only messages, not just system messages. Code is clean, test is correct, and CLAUDE.md is fully compliant.

Blocking issues

None.

Suggested fixes

None.

Minor / nice-to-have

  • packages/core/src/db/messages.test.ts:103 — One test comment uses "reverses" which describes an action belonging to listMessages, not the test context. Change to // DB orders by created_at DESC; mock data matches. — this aligns phrasing with what the test is actually asserting.

Compliments

  • The messages.ts JSDoc explaining DESC + reverse as an efficiency trade-off is exactly the kind of non-obvious WHY comment that prevents future accidental removals.
  • The client-side merge block in ChatInterface.tsx is well-documented with a clear explanation of which messages are kept, which are discarded, and why.
  • The test for listMessages includes both a behavioral assertion (correct output order) and a SQL assertion (ORDER BY created_at DESC) — good coverage discipline.

Reviewed via maintainer-review-pr workflow (Pi/Minimax). Aspects run: code-review, test-coverage, comment-quality.

@Wirasm Wirasm merged commit b4cd637 into coleam00:dev May 12, 2026
4 checks passed
@Wirasm Wirasm mentioned this pull request May 12, 2026
cropse pushed a commit to cropse/Archon that referenced this pull request May 19, 2026
…oleam00#1532)

* fix(core,web): show newest messages instead of oldest on hydration (coleam00#1531)

Two defects combine to silently lose recent messages in long conversations:

1. listMessages() returns the oldest N messages (ORDER BY ASC LIMIT 200).
   In conversations with >200 messages, the newest messages disappear
   from the UI after refresh — the exact opposite of every chat UI's
   expectation.

2. The stuck-placeholder recovery path in ChatInterface replaces React
   message state with the server fetch, dropping any live SSE-only messages
   (not just system messages) that haven't been persisted yet. This causes
   mid-session message loss without any user-visible trigger.

Fix:
- Change listMessages() to ORDER BY DESC then reverse, so the newest
  N messages are returned in chronological order
- Broaden stuck-placeholder recovery to merge by message ID instead of
  only preserving system messages — any client-only message (SSE-streamed,
  system status) not in the hydrated set is kept and interleaved by
  timestamp

Closes coleam00#1531

* fix: filter transient placeholders from stuck-placeholder recovery merge

Address CodeRabbit review: tighten the client-only filter to exclude
optimistic user rows and empty thinking placeholders. Only preserve
system messages and assistant messages with meaningful content (content,
error, workflowDispatch, workflowResult, or toolCalls).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants