fix: the chat's thread lists were empty for everyone - #1661
Conversation
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
There was a problem hiding this comment.
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
ThreadQueriesas the single source of truth for “my threads”, “my open threads”, and “threads under” query strings. - Update
ThreadChatViewto use the new centralized queries (removing incorrect namespace scoping and client-side filtering onMeshNode.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.
Test Results (shard 0)918 tests 917 ✅ 11m 3s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results (shard 5)1 371 tests 1 370 ✅ 5m 2s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results (shard 4)1 542 tests 1 536 ✅ 7m 25s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results (shard 3) 11 files 11 suites 6m 1s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)2 178 tests 2 075 ✅ 7m 23s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)2 384 tests 2 380 ✅ 7m 48s ⏱️ Results for commit d5db5d3. ♻️ This comment has been updated with latest results. |
Test Results 65 files 65 suites 44m 43s ⏱️ 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
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.
SwitchToResumeModeAsyncqueriednamespace:{NavigationService.CurrentNamespace}/_Thread.CurrentNamespaceis the page's node path (LayoutAreastamps the top area'sAddress), 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/_Thread→ 0 results, whilenodeType:Thread content.createdBy:rbuergi→ the user's threads across every partition.2 — the nav menu filtered on a field that is structurally null.
SetupMyThreadsSubscriptionasked the server for every thread and then kept the ones wheren.CreatedBy == me. Threads live in the_Threadsatellite table, and the authorship columns exist only onmesh_nodes— a satellite read projectsNULL::text AS created_by(PostgreSqlStorageAdapter.AuthorCols). SoMeshNode.CreatedByis 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 nocreatedBy; amesh_nodesnode 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:content.createdBy) — the same predicate the dashboard's thread query already uses and thatCrossPartitionThreadQueryTestsalready pins — never re-filtered in the client;MyThreads/MyOpenThreads/ThreadsUndernow 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 samecontent.createdBylesson, learned once and pinned for one call site only),SyncedQueryProjectionContractTest(theselect:contract, added after b78cfe9 — the previous time this same picker silently returned nothing), andThreadResumeTest(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:Donedrops out ofMyOpenThreadsbut stays inMyThreads(also pins that the status enum matches by name);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 -warnaserrorclean:MeshWeaver.Blazor.Portal(buildsMeshWeaver.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.)Left alone deliberately
ChatHistorySelector.razorcarries the samecreatedBy:{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