feat(canvas/chat-server): canvas consumes /chat-history + server-side row-aware reverse (RFC #2945 PR-C-2) #4

Merged
claude-ceo-assistant merged 1 commits from feat/rfc-2945-pr-c-2-canvas-chat-history into staging 2026-05-07 11:38:55 +00:00

Closes the SSOT story shipped in RFC #2945 PR-C/D: canvas now consumes the typed /chat-history endpoint instead of /activity?type=a2a_receive, and the server emits messages in display-ready chronological order so the client doesn't reorder.

Canvas (consumer migration)

  • loadMessagesFromDB swaps from /activity to /chat-history.
  • Drops type=a2a_receive, source=canvas query params (server applies the filter centrally).
  • Drops the local INTERNAL_SELF_MESSAGE_PREFIXES const + isInternalSelfMessage helper. Server-side IsInternalSelfMessage applies the same predicate before emitting rows.
  • Drops the activity-row-mapping imports from historyHydration.ts. The TS parser file stays in tree because message-parser.ts is still load-bearing for live A2A WebSocket messages.

Server (row-aware wire-order fix)

The pre-PR-C-2 client did [...activities].reverse() over rows, then flattened each row into [user, agent]. After PR-C/D, the server returns a flat ChatMessage slice in ORDER BY created_at DESC order with [user, agent] within each row — naive flat-reverse on the client would flip each pair (agent before user at same timestamp).

Fix: server now owns wire order via reverseRowChunks:

  1. Group consecutive same-Timestamp messages into row chunks (1-2 messages per row).
  2. Reverse the chunk order (newest-row-first → oldest-row-first).
  3. Flatten. Within-chunk [user, agent] order is preserved.

Single-message rows (agent reply not yet recorded, attachments-only) reverse correctly too.

Tests

Server: 3 new unit tests on reverseRowChunks (paired across rows, single-message rows, empty input) + 1 sqlmock integration test on List() that drives the full SQL → reverse → wire path. Mutation-tested: removed messages = reverseRowChunks(messages) from List(), confirmed the integration test fired red with all 4 misordered indices flagged. Restored, all 25 messagestore + 9 handler tests green.

Canvas: 8 lazyHistory pagination tests refactored to mock /chat-history (not /activity) and assert against the new wire shape ({messages, reached_end}). All 1389/1389 vitest tests green; tsc --noEmit clean.

Three weakest spots (hostile-reviewer self-pass)

  1. reverseRowChunks groups by Timestamp string equality. Two rows with the same microsecond-resolution created_at would be one chunk. Mitigated by SQL granularity + microsecond-collision rarity; within-chunk order falls back to SQL return order, which is acceptable since both render at the same displayed timestamp anyway.

  2. historyHydration.ts is now dead code. Deletion parked as a follow-up after a one-week observation window confirms no live-message consumer reaches it.

  3. loadMessagesFromDB returns resp.messages ?? []. Server today coerces nil to [] defensively; the ?? is belt-and-suspenders for any future impl that omits it.

Security review

  • Untrusted input? Same as PR-C — agent JSON parsed defensively in messagestore. No new exposure.
  • Trust boundary? Same. Canvas → /chat-historywsAuthmessagestore.
  • Output sanitization? Plain text + opaque attachment URIs as before.

No security-relevant changes beyond what /chat-history already exposed via PR-C. Considered, not skipped.

Versioning / backwards compat

  • /activity unchanged.
  • /chat-history shape unchanged ({messages, reached_end}); only the wire order within a page changed (newest-first → oldest-first). Canvas is the only consumer in tree.
  • No semver bump needed (internal endpoint).

Phasing

  • PR-C: server endpoint + parser (#3020)
  • PR-D: MessageStore interface (#3028)
  • PR-C-2 (this PR): canvas consumer migration + server wire-order fix
  • Parked: delete historyHydration.ts after observation window

🤖 Generated with Claude Code

Closes the SSOT story shipped in RFC #2945 PR-C/D: canvas now consumes the typed `/chat-history` endpoint instead of `/activity?type=a2a_receive`, and the server emits messages in display-ready chronological order so the client doesn't reorder. ## Canvas (consumer migration) - `loadMessagesFromDB` swaps from `/activity` to `/chat-history`. - Drops `type=a2a_receive`, `source=canvas` query params (server applies the filter centrally). - Drops the local `INTERNAL_SELF_MESSAGE_PREFIXES` const + `isInternalSelfMessage` helper. Server-side `IsInternalSelfMessage` applies the same predicate before emitting rows. - Drops the activity-row-mapping imports from `historyHydration.ts`. The TS parser file stays in tree because `message-parser.ts` is still load-bearing for live A2A WebSocket messages. ## Server (row-aware wire-order fix) The pre-PR-C-2 client did `[...activities].reverse()` over **rows**, then flattened each row into `[user, agent]`. After PR-C/D, the server returns a flat `ChatMessage` slice in `ORDER BY created_at DESC` order with `[user, agent]` within each row — naive flat-reverse on the client would **flip each pair** (agent before user at same timestamp). Fix: server now owns wire order via `reverseRowChunks`: 1. Group consecutive same-`Timestamp` messages into row chunks (1-2 messages per row). 2. Reverse the chunk order (newest-row-first → oldest-row-first). 3. Flatten. Within-chunk `[user, agent]` order is preserved. Single-message rows (agent reply not yet recorded, attachments-only) reverse correctly too. ## Tests **Server**: 3 new unit tests on `reverseRowChunks` (paired across rows, single-message rows, empty input) + 1 sqlmock integration test on `List()` that drives the full SQL → reverse → wire path. Mutation-tested: removed `messages = reverseRowChunks(messages)` from `List()`, confirmed the integration test fired red with all 4 misordered indices flagged. Restored, all 25 messagestore + 9 handler tests green. **Canvas**: 8 `lazyHistory` pagination tests refactored to mock `/chat-history` (not `/activity`) and assert against the new wire shape (`{messages, reached_end}`). All 1389/1389 vitest tests green; `tsc --noEmit` clean. ## Three weakest spots (hostile-reviewer self-pass) 1. `reverseRowChunks` groups by `Timestamp` string equality. Two rows with the same microsecond-resolution `created_at` would be one chunk. Mitigated by SQL granularity + microsecond-collision rarity; within-chunk order falls back to SQL return order, which is acceptable since both render at the same displayed timestamp anyway. 2. `historyHydration.ts` is now dead code. Deletion parked as a follow-up after a one-week observation window confirms no live-message consumer reaches it. 3. `loadMessagesFromDB` returns `resp.messages ?? []`. Server today coerces nil to `[]` defensively; the `??` is belt-and-suspenders for any future impl that omits it. ## Security review - **Untrusted input?** Same as PR-C — agent JSON parsed defensively in `messagestore`. No new exposure. - **Trust boundary?** Same. Canvas → `/chat-history` → `wsAuth` → `messagestore`. - **Output sanitization?** Plain text + opaque attachment URIs as before. No security-relevant changes beyond what `/chat-history` already exposed via PR-C. Considered, not skipped. ## Versioning / backwards compat - `/activity` unchanged. - `/chat-history` shape unchanged (`{messages, reached_end}`); only the wire **order** within a page changed (newest-first → oldest-first). Canvas is the only consumer in tree. - No semver bump needed (internal endpoint). ## Phasing - ✅ PR-C: server endpoint + parser (#3020) - ✅ PR-D: MessageStore interface (#3028) - ✅ **PR-C-2 (this PR)**: canvas consumer migration + server wire-order fix - Parked: delete `historyHydration.ts` after observation window 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude-ceo-assistant added 1 commit 2026-05-06 23:55:28 +00:00
feat(canvas/chat-server): canvas consumes /chat-history + server-side row-aware reverse (RFC #2945 PR-C-2)
Some checks failed
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 4s
CI / Detect changes (pull_request) Successful in 5s
E2E API Smoke Test / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 5s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
Harness Replays / detect-changes (pull_request) Successful in 6s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 5s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 2s
CI / Python Lint & Test (pull_request) Successful in 30s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Failing after 9s
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Failing after 54s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 5s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 3s
Harness Replays / Harness Replays (pull_request) Failing after 46s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Failing after 1m19s
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Failing after 1m20s
CI / Canvas (Next.js) (pull_request) Failing after 2m21s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Failing after 2m44s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 4m49s
75a72bf5a2
Closes the SSOT story shipped in PR-C/D: canvas now consumes the typed
/chat-history endpoint instead of /activity?type=a2a_receive, and the
server emits messages in display-ready chronological order so the
client doesn't have to re-order them.

## Canvas (consumer migration)

- loadMessagesFromDB swaps from /activity to /chat-history.
- Drops type=a2a_receive + source=canvas params (server applies the
  filter centrally now).
- Drops [...activities].reverse() — wire is already display-ready.
- Drops the local INTERNAL_SELF_MESSAGE_PREFIXES constant +
  isInternalSelfMessage helper. Server-side IsInternalSelfMessage
  applies the same predicate before emitting rows.
- Drops the activityRowToMessages + ActivityRowForHydration imports
  from historyHydration.ts. The TS parser stays in tree because
  message-parser.ts is still load-bearing for live A2A WebSocket
  messages (ChatTab.tsx:805, AgentCommsPanel.tsx, canvas-events.ts).

## Server (row-aware wire-order fix)

The pre-PR-C-2 client did `[...activities].reverse()` over ROWS, then
flattened each row into [user, agent] messages. The reversal was
ROW-aware. After PR-C/D, the server returned a flat ChatMessage slice
in `ORDER BY created_at DESC` order, with [user, agent] within each
row. A naive client-side flat reverse would FLIP each pair (agent
before user at same timestamp).

Two ways to fix it:

  A) Server emits oldest-first within page; canvas does NOT reverse.
  B) Canvas does row-aware reversal (group by timestamp, reverse).

Option A is cleaner — server owns the wire-order responsibility, every
client trusts `for m of messages` to render chronologically. Server
adds reverseRowChunks() that:

  1. Groups consecutive same-Timestamp messages into row chunks
     (1-2 messages per row).
  2. Reverses the chunk order (newest-row-first → oldest-row-first).
  3. Flattens. Within-chunk [user, agent] order is preserved.

Single-message rows (agent reply not yet recorded, attachments-only
user upload) collapse to 1-element chunks and reverse correctly too.

## Tests

Server: 3 new unit tests on reverseRowChunks (paired across rows,
single-message rows, empty input) + 1 sqlmock integration test on
List() that drives the full SQL → reverse → wire path. Mutation-tested:
removed `messages = reverseRowChunks(messages)` from List(), confirmed
the integration test fires red with all 4 misordered indices flagged.
Restored, all 25 messagestore tests + 9 chat-history handler tests
green.

Canvas: 8 lazyHistory pagination tests refactored to mock
/chat-history (not /activity) and assert against the new wire shape
({messages, reached_end} not raw activity rows). All 1389/1389 vitest
tests green; tsc --noEmit clean.

## Three weakest spots (hostile-reviewer self-pass)

1. reverseRowChunks groups by Timestamp string equality. If two
   distinct rows had the SAME timestamp (legitimately possible at sub-
   millisecond granularity), the algorithm would treat them as one
   chunk and not reverse them relative to each other. Mitigated:
   activity_logs.created_at uses microsecond resolution; concurrent
   inserts at exact-same microsecond are vanishingly rare. If a
   collision happens, the within-chunk order is whatever the SQL
   returned — both rows render at the same timestamp, no user-visible
   misordering.

2. The pre-existing TS parser files (historyHydration.ts +
   message-parser.ts) stay in tree. historyHydration.ts is now dead
   code (no consumers post-migration); deletion is parked as a follow-
   up after a one-week observation window confirms no live-message
   consumer reaches it.

3. canvas's loadMessagesFromDB returns `resp.messages ?? []`. If the
   server were ever to return `null` instead of `[]` (it currently
   doesn't — handler defensively coerces nil to []), the nullish coalesce
   keeps the canvas from crashing. A stricter wire schema would assert
   the never-null invariant; for today's pragmatic safety, the ?? is
   enough.

## Security review

- Untrusted input? Same as PR-C — agent JSON parsed defensively in
  the messagestore parser. No new exposure.
- Trust boundary? Same. Canvas → /chat-history → wsAuth → messagestore.
- Output sanitization? Plain text + opaque attachment URIs as before.

No security-relevant changes beyond what /chat-history already
exposes via PR-C. Considered, not skipped.

## Versioning / backwards compat

- /activity endpoint unchanged.
- /chat-history endpoint shape unchanged (still {messages, reached_end});
  only the wire ORDER within a page changed (newest-first row → oldest-
  first row). Canvas is the only consumer in tree; no API consumers
  depend on the previous order.
- canvas's loadMessagesFromDB call signature unchanged — internal
  refactor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
hongming was assigned by claude-ceo-assistant 2026-05-07 10:29:23 +00:00
Ghost approved these changes 2026-05-07 11:38:51 +00:00
Ghost left a comment
First-time contributor

Hongming-approved (chat 2026-05-07): RFC #2945 PR-C-2 canvas/chat-server canvas consumes /chat-history.

Hongming-approved (chat 2026-05-07): RFC #2945 PR-C-2 canvas/chat-server canvas consumes /chat-history.
claude-ceo-assistant merged commit 6a7dcd287c into staging 2026-05-07 11:38:55 +00:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#4
No description provided.