Skip to content

fix: the chat's thread lists were empty for everyone - #1661

Merged
rbuergi merged 2 commits into
mainfrom
fix/threads-menu-empty
Aug 16, 2026
Merged

fix: the chat's thread lists were empty for everyone#1661
rbuergi merged 2 commits into
mainfrom
fix/threads-menu-empty

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Symptom

The Threads picker in the chat side panel lists nothing. Same for the in-thread nav menu's "other open threads".

Two independent defects, one symptom

1 — the picker was scoped to the page you're standing on. SwitchToResumeModeAsync queried
namespace:{NavigationService.CurrentNamespace}/_Thread. CurrentNamespace is the page's node path (LayoutArea stamps the top area's Address), and a thread is created under whatever node it was started from — so on any page that never hosted a thread of its own, the picker asked for a namespace that holds nothing. Verified against memex: nodeType:Thread namespace:Doc/Architecture/AsynchronousCalls/_Thread0 results, while nodeType:Thread content.createdBy:rbuergi → the user's threads across every partition.

2 — the nav menu filtered on a field that is structurally null. SetupMyThreadsSubscription asked the server for every thread and then kept the ones where n.CreatedBy == me. Threads live in the _Thread satellite table, and the authorship columns exist only on mesh_nodes — a satellite read projects NULL::text AS created_by (PostgreSqlStorageAdapter.AuthorCols). So MeshNode.CreatedBy is null on every thread that arrives, and that predicate was false for every row, for every signed-in user. (Confirmed on live data: a thread node returns no createdBy; a mesh_nodes node does.)

The subtlety that makes #2 easy to get wrong: the server-side createdBy: term is fine — the query layer maps it onto the stored content. Only the projected value is null.

The fix

One place, MeshWeaver.AI/ThreadQueries.cs, holding the two rules that are invisible at the call site:

  • ownership is decided by the query (content.createdBy) — the same predicate the dashboard's thread query already uses and that CrossPartitionThreadQueryTests already pins — never re-filtered in the client;
  • a thread list is not scoped to the current page.

MyThreads / MyOpenThreads / ThreadsUnder now back the picker, the nav menu and the sub-thread list.

"Don't we have tests for it?" — we did not, and that's why it broke twice

Nothing covered these two lists. What existed: CrossPartitionThreadQueryTests (the dashboard's thread query — the same content.createdBy lesson, learned once and pinned for one call site only), SyncedQueryProjectionContractTest (the select: contract, added after b78cfe9 — the previous time this same picker silently returned nothing), and ThreadResumeTest (loading one thread's messages). The chat's lists were hand-written query strings that no test touched.

New ThreadListQueryTests (real PG-backed mesh, workspace.GetQuery — the production path) pins:

  • a thread started on an ordinary content page is found by "my threads" (kills the page scoping);
  • another user's thread is not;
  • Done drops out of MyOpenThreads but stays in MyThreads (also pins that the status enum matches by name);
  • the row arrives with no envelope authorship at all, so re-filtering it client-side drops everything — the trap that only reproduces on PG.

Each test owns a unique creator id, so the deliberately unscoped cross-partition query matches only its own rows (no #834-style paging flake).

Verification

  • dotnet build -c Release -warnaserror clean: MeshWeaver.Blazor.Portal (builds MeshWeaver.AI), MeshWeaver.Hosting.PostgreSql.Test.
  • ThreadListQueryTests — 4 passed against a real Postgres container. (First run caught a wrong claim in my own test — that the server-side envelope term matches nothing — which is how the "server-side is fine, the value is null" distinction above got established.)
  • Not visually confirmed in a running portal; the queries are pinned by the tests above.

Left alone deliberately

ChatHistorySelector.razor carries the same createdBy:{userId} shape but is referenced nowhere — dead code. Its server-side term is in fact correct; it wants deleting rather than editing, which is a separate call.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KmBFaHTqhy7h742wkAaJKj

Two lists, two independent defects, one symptom ("No threads yet"):

* The side panel's thread picker scoped itself to
  `namespace:{NavigationService.CurrentNamespace}/_Thread`. CurrentNamespace is
  the PAGE's node path (LayoutArea stamps the top area's Address), and a thread
  is created under whatever node it was started from — so on every page that had
  never hosted a thread of its own, the picker queried a namespace holding
  nothing.
* The in-thread nav menu asked the server for every thread and then kept the ones
  where `n.CreatedBy == me`. Threads live in the _Thread satellite table, whose
  reads project NULL::text for the authorship columns
  (PostgreSqlStorageAdapter.AuthorCols), so MeshNode.CreatedBy is null on every
  thread that arrives and the predicate was false for every row, for every user.

Both now go through ThreadQueries — one place holding the two rules that are
invisible at the call site: ownership is decided by the query (content.createdBy,
matching the dashboard query CrossPartitionThreadQueryTests already pins), and a
thread list is never scoped to the page you happen to be on.

ThreadListQueryTests pins all of it against a real PG-backed mesh, including the
trap that only reproduces there: the row arrives with no envelope authorship at
all, so re-filtering it in the client drops everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmBFaHTqhy7h742wkAaJKj
Copilot AI lite review requested due to automatic review settings August 16, 2026 09:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes empty thread lists in the Blazor chat UI by centralizing and correcting the thread query strings used by the side-panel picker and in-thread navigation, and adds PostgreSQL-backed tests to prevent regressions specific to satellite-table projection behavior.

Changes:

  • Introduce ThreadQueries as the single source of truth for “my threads”, “my open threads”, and “threads under” query strings.
  • Update ThreadChatView to use the new centralized queries (removing incorrect namespace scoping and client-side filtering on MeshNode.CreatedBy).
  • Add new PG integration tests that pin the corrected behavior, and add a What’s New entry documenting the fix.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
test/MeshWeaver.Hosting.PostgreSql.Test/ThreadListQueryTests.cs Adds real Postgres-backed regression tests for thread list queries, including the satellite-table CreatedBy projection pitfall.
src/MeshWeaver.AI/ThreadQueries.cs Centralizes thread list query construction to prevent future call-site drift and repeats of the same failure modes.
src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs Switches the chat UI’s thread picker/nav menu/sub-thread list to the centralized queries; removes faulty page scoping and envelope-based client filtering.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-16-your-threads-list-again.md Documents the user-visible fix in What’s New.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/MeshWeaver.Hosting.PostgreSql.Test/ThreadListQueryTests.cs Outdated
Comment thread src/MeshWeaver.Blazor.Portal/Chat/ThreadChatView.razor.cs Outdated
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 0)

918 tests   917 ✅  11m 3s ⏱️
 10 suites    1 💤
 10 files      0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 5)

1 371 tests   1 370 ✅  5m 2s ⏱️
   11 suites      1 💤
   11 files        0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 4)

1 542 tests   1 536 ✅  7m 25s ⏱️
   11 suites      6 💤
   11 files        0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 3)

   11 files     11 suites   6m 1s ⏱️
2 196 tests 2 005 ✅ 191 💤 0 ❌
2 557 runs  2 366 ✅ 191 💤 0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 1)

2 178 tests   2 075 ✅  7m 23s ⏱️
   11 suites    103 💤
   11 files        0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results (shard 2)

2 384 tests   2 380 ✅  7m 48s ⏱️
   11 suites      4 💤
   11 files        0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown

Test Results

    65 files      65 suites   44m 43s ⏱️
10 589 tests 10 283 ✅ 306 💤 0 ❌
10 950 runs  10 644 ✅ 306 💤 0 ❌

Results for commit d5db5d3.

♻️ This comment has been updated with latest results.

Copilot was right: every read sat inside ImpersonateAsSystem, so the tests never
exercised the production caller — a circuit user on the secured, per-result RLS
path. An RLS regression where a user cannot see their own threads would have
passed silently, which is the very class of failure this suite exists for.

System impersonation is now scoped to SEEDING only; each assertion runs as the
owning user via AccessContext. Creating the User partition root is enough —
onboarding writes the {owner}/_Access grant itself (adding one explicitly fails
with "Node already exists").

MyThreads_ExcludesAnotherUsersThread also had to change shape: with the other
user's thread in its own partition, RLS hid it regardless and the assertion
passed without the ownership term ever being evaluated. Both threads now live in
the SAME partition with different creators, so only the query can exclude it.

Also fixes the grammar nit in the ThreadChatView comment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmBFaHTqhy7h742wkAaJKj
@rbuergi
rbuergi merged commit 087f049 into main Aug 16, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants