Diagnose & fix the auto-flow bug #15

Open
opened 2026-05-16 22:22:35 +02:00 by arne · 1 comment
Owner

What to build

The Live updates promise in CONTEXT.md is currently broken: inbound messages from a peer do not appear in the open thread without manual page refresh. Diagnose the failure point and fix it.

Diagnostic recipe (the audit narrowed the candidates):

  1. Open the app to a thread. Open DevTools → Network → /events → "EventStream" tab.
  2. Have a peer send a message (or trigger one via posta-server tooling).
  3. Three observable outcomes drive different fixes:
    • No event: thread-updated frame arrives → server-side peerFromInboundData in internal/web/events.go likely returns "" because the upstream inbound DTO doesn't carry peer at the top level. Fix the extraction.
    • Frame arrives but the DOM doesn't update → bridge-side bug. Likely htmx 1.9's sse:NAME trigger normalization vs. the live:thread-updated CustomEvent chain (see live.js:138-145 for the known workaround). Tighten the bridge.
    • Frame arrives and DOM updates but is wrong (e.g. missing the new message) → fragment-render path issue on the /c/{peer} refetch.

Surgical fix expected. If the diagnostic reveals a larger underlying issue, open follow-up issues rather than expanding scope here.

Acceptance criteria

  • Root cause identified and recorded in the PR description
  • Inbound messages from a peer appear in the open thread within ~1s of arrival, without manual refresh
  • Sidebar unread badge and last-message preview update for the affected contact at the same time
  • Manual verification: send a message between two test accounts and observe both halves of the Live updates promise

Blocked by

None - can start immediately

## What to build The Live updates promise in `CONTEXT.md` is currently broken: inbound messages from a peer do not appear in the open thread without manual page refresh. Diagnose the failure point and fix it. Diagnostic recipe (the audit narrowed the candidates): 1. Open the app to a thread. Open DevTools → Network → `/events` → "EventStream" tab. 2. Have a peer send a message (or trigger one via posta-server tooling). 3. Three observable outcomes drive different fixes: - **No `event: thread-updated` frame arrives** → server-side `peerFromInboundData` in `internal/web/events.go` likely returns `""` because the upstream `inbound` DTO doesn't carry `peer` at the top level. Fix the extraction. - **Frame arrives but the DOM doesn't update** → bridge-side bug. Likely htmx 1.9's `sse:NAME` trigger normalization vs. the `live:thread-updated` CustomEvent chain (see `live.js:138-145` for the known workaround). Tighten the bridge. - **Frame arrives and DOM updates but is wrong** (e.g. missing the new message) → fragment-render path issue on the `/c/{peer}` refetch. Surgical fix expected. If the diagnostic reveals a larger underlying issue, open follow-up issues rather than expanding scope here. ## Acceptance criteria - [ ] Root cause identified and recorded in the PR description - [ ] Inbound messages from a peer appear in the open thread within ~1s of arrival, without manual refresh - [ ] Sidebar unread badge and last-message preview update for the affected contact at the same time - [ ] Manual verification: send a message between two test accounts and observe both halves of the Live updates promise ## Blocked by None - can start immediately
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: bug
Summary: Inbound messages from peers do not appear in the open thread without manual refresh; the Live updates promise (CONTEXT.md) is broken end-to-end.

Current behavior:
When a peer sends a message and the user has the corresponding thread open, the new bubble does not appear in the DOM until the user manually refreshes. The sidebar's unread badge and last-message preview are similarly stale.

Desired behavior:
The Live updates promise from CONTEXT.md § Live updates: within ~1s of the inbound event reaching chat.posta.no, the open thread shows the new bubble and the sidebar reflects the unread count + last-message preview, on every browser tab the user has open.

Reproduction during triage already narrowed the failure surface:

Go-side tests for the translation layer pass with the canonical posta-server DTO {"peer":"...","rowId":N,"direction":"in"}. Specifically the inbound → (contact-changed + thread-updated) translation and the peer extraction helper both behave correctly when fed contract-shaped input. Failure mode 1 — server-side peer extraction returning empty — is ruled out for the documented DTO shape.

Two failure surfaces remain:

  • Mode 2 — browser bridge. event: thread-updated arrives on the wire but the DOM does not update. Suspect: htmx-1.9's sse:NAME normalization interacts poorly with the bridge's live:thread-updated CustomEvent dispatch path. The bridge already has a workaround for this (raw EventSource.addEventListener), but the wiring may have a timing or rebind gap (e.g. the <main> swap replaces the element and the new element's hx-trigger="live:thread-updated from:this" does not get processed in time).
  • Mode 3 — fragment re-render. The frame arrives, htmx fetches the thread fragment, the swap completes, but the fragment server-side does not yet contain the new row (a propagation/cache race against posta-server).

Diagnostic order for the agent:

  1. Sanity-check the live wire by capturing one inbound frame from a real posta-server stream — confirm the actual DTO shape matches the test contract. If it doesn't, the bug is failure mode 1 after all and the fix is in the upstream-event-shape extraction.
  2. If the wire DTO matches: open DevTools EventStream against a running chat.posta.no, send a peer message, observe whether event: thread-updated lands. If yes → mode 2. If no → mode 1 disguised by a non-contract upstream shape; treat as case 1.
  3. For mode 2: instrument the bridge to log each step (thread-updated received → peer parsed → peer-match? → CustomEvent dispatched → htmx triggered → fetch issued → swap completed). Find the first step that doesn't fire.
  4. For mode 3: check the timing — chat.posta.no's /c/{peer} fragment refetch must see the new row. If it doesn't, that's a posta-server read-after-write race; the right fix is a small bounded retry on the fragment fetch.

Key interfaces:

  • The Live updates protocol table in CONTEXT.md is the authoritative wire mapping; do not duplicate it in code comments (see related issue on doc-comment cleanup).
  • The bridge's pure decision logic should remain in live.js. If the fix involves splitting the bridge into pure functions, that's the scope of a separate issue — not this one.

Acceptance criteria:

  • Root cause identified, stated in the PR description with reference to which of the three failure modes applied
  • Inbound peer messages appear in the open thread within ~1s, no manual refresh
  • Sidebar unread badge and last-message preview update at the same time
  • Manual verification with two test accounts: one sends, the other observes both thread body and sidebar update live
  • No new logs added to hot paths (use temporary instrumentation during debugging and remove before merging; the project is already cleaning up debug residue in a separate issue)

Out of scope:

  • The bridge refactor into pure functions + goja tests (separate issue)
  • The scroll preservation contract (separate issue)
  • The active-peer migration to query param (separate issue)
  • The per-bubble append refactor (deliberately deferred until the refetch path is proven to work)
  • Push subsystem behavior
> *This was generated by AI during triage.* ## Agent Brief **Category:** bug **Summary:** Inbound messages from peers do not appear in the open thread without manual refresh; the Live updates promise (CONTEXT.md) is broken end-to-end. **Current behavior:** When a peer sends a message and the user has the corresponding thread open, the new bubble does not appear in the DOM until the user manually refreshes. The sidebar's unread badge and last-message preview are similarly stale. **Desired behavior:** The Live updates promise from `CONTEXT.md § Live updates`: within ~1s of the inbound event reaching chat.posta.no, the open thread shows the new bubble *and* the sidebar reflects the unread count + last-message preview, on every browser tab the user has open. **Reproduction during triage already narrowed the failure surface:** Go-side tests for the translation layer pass with the canonical posta-server DTO `{"peer":"...","rowId":N,"direction":"in"}`. Specifically the inbound → (`contact-changed` + `thread-updated`) translation and the peer extraction helper both behave correctly when fed contract-shaped input. **Failure mode 1 — server-side peer extraction returning empty — is ruled out for the documented DTO shape.** Two failure surfaces remain: - **Mode 2 — browser bridge.** `event: thread-updated` arrives on the wire but the DOM does not update. Suspect: htmx-1.9's `sse:NAME` normalization interacts poorly with the bridge's `live:thread-updated` CustomEvent dispatch path. The bridge already has a workaround for this (raw `EventSource.addEventListener`), but the wiring may have a timing or rebind gap (e.g. the `<main>` swap replaces the element and the new element's `hx-trigger="live:thread-updated from:this"` does not get processed in time). - **Mode 3 — fragment re-render.** The frame arrives, htmx fetches the thread fragment, the swap completes, but the fragment server-side does not yet contain the new row (a propagation/cache race against posta-server). **Diagnostic order for the agent:** 1. Sanity-check the live wire by capturing one `inbound` frame from a real posta-server stream — confirm the actual DTO shape matches the test contract. If it doesn't, the bug is failure mode 1 after all and the fix is in the upstream-event-shape extraction. 2. If the wire DTO matches: open DevTools EventStream against a running chat.posta.no, send a peer message, observe whether `event: thread-updated` lands. If yes → mode 2. If no → mode 1 disguised by a non-contract upstream shape; treat as case 1. 3. For mode 2: instrument the bridge to log each step (`thread-updated` received → peer parsed → peer-match? → CustomEvent dispatched → htmx triggered → fetch issued → swap completed). Find the first step that doesn't fire. 4. For mode 3: check the timing — chat.posta.no's `/c/{peer}` fragment refetch must see the new row. If it doesn't, that's a posta-server read-after-write race; the right fix is a small bounded retry on the fragment fetch. **Key interfaces:** - The `Live updates protocol` table in `CONTEXT.md` is the authoritative wire mapping; do not duplicate it in code comments (see related issue on doc-comment cleanup). - The bridge's pure decision logic should remain in `live.js`. If the fix involves splitting the bridge into pure functions, that's the scope of a separate issue — not this one. **Acceptance criteria:** - [ ] Root cause identified, stated in the PR description with reference to which of the three failure modes applied - [ ] Inbound peer messages appear in the open thread within ~1s, no manual refresh - [ ] Sidebar unread badge and last-message preview update at the same time - [ ] Manual verification with two test accounts: one sends, the other observes both thread body and sidebar update live - [ ] No new logs added to hot paths (use temporary instrumentation during debugging and remove before merging; the project is already cleaning up debug residue in a separate issue) **Out of scope:** - The bridge refactor into pure functions + goja tests (separate issue) - The scroll preservation contract (separate issue) - The active-peer migration to query param (separate issue) - The per-bubble append refactor (deliberately deferred until the refetch path is proven to work) - Push subsystem behavior
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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
posta/chat#15
No description provided.