Skip to content

Fix shared multi-user conversation reload and AI streaming - #1283

Merged
Paul Lizer (paullizer) merged 2 commits into
Developmentfrom
paullizer-multi-user-conversation-bug
Aug 18, 2026
Merged

Fix shared multi-user conversation reload and AI streaming#1283
Paul Lizer (paullizer) merged 2 commits into
Developmentfrom
paullizer-multi-user-conversation-bug

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #1281

Sharing a personal conversation left it unusable end to end. Two independent defects were involved.

Root cause 1 — the personal-only messages endpoint was used for shared conversations

selectConversation() called the personal loadMessages() inside its collaborative branch:

if (isCollaborativeConversation && window.chatCollaboration?.activateConversation) {
    await loadMessages(conversationId);          // personal-only endpoint
    scrollConversationViewToBottom();
    await window.chatCollaboration.activateConversation(conversationId, metadata);
}

loadMessages() fetches /conversation/<id>/messages, whose handler reads only cosmos_conversations_container. A shared conversation lives in cosmos_collaboration_conversations_container under a different id than its hidden source_conversation_id, so the read always raised CosmosResourceNotFoundError → guaranteed 404 plus a "Conversation not found" danger toast on every reload and sidebar click.

The call was also redundant — activateConversation()loadConversationMessages() already clears #chatbox and renders the shared messages. Two side effects regressed because of it:

  • updateComparisonChatUploadCatalog([]) ran from the catch, so chat uploads in shared conversations never reached the Compare/Analyze picker.
  • updateConversationTaskDocumentsFromMessages() never ran, so task documents from the previously viewed conversation stayed cached against the wrong conversation.

Root cause 2 — the Blueprint migration broke the internal stream endpoint lookup

internal_stream_view = current_app.view_functions.get('chat_stream_api')

Commit 094424bc ("Harden route blueprint security policies") moved every route module onto Blueprints. app.py registers chat routes via register_route_blueprint('backend_chats', ...), so /api/chat/stream is declared with @bp.route(...) and Flask stores it under backend_chats.chat_stream_api. The unqualified lookup returned None, so every collaborative AI request failed with Stream interrupted: Chat streaming endpoint is unavailable before the model was ever called. Group collaborative conversations share the same bridge and were broken too.

current_app.view_functions was the only remaining Blueprint-prefix assumption in the app — every url_for() call was already dotted.

Changes (v0.250.224)

File Change
route_backend_collaboration.py Added _resolve_internal_view_function() (exact-match fast path first, then last-segment match) and a [COLLABORATION] log event when resolution fails.
chat-conversations.js Removed the personal loadMessages() call from the collaborative branch.
chat-collaboration.js loadConversationMessages() now clears stale search highlights, rehydrates task documents, refreshes the comparison chat upload catalog, and reapplies a pending highlight.
chat-messages.js Exported updateComparisonChatUploadCatalog for reuse.

Follow-up hardening (v0.250.225)

Three further defects surfaced while tracing this bug. None caused the reported symptoms, so they were split out rather than mixed into the fix above.

Shared stream errors now always carry conversation_kind

chat-streaming.js picks its post-error recovery endpoint from conversation_kind, falling back to the personal endpoint when absent. None of the seven _serialize_stream_error() call sites in the bridge set it — so a shared conversation would have hit the exact 404 this PR removes.

It could not fire in practice: the surrounding guard also requires message_id, and those error payloads never carried one. A latent trap rather than a live defect — but one that returns the moment anyone adds message_id to an error payload.

Rather than repeating the field seven times, all failures now funnel through one nested helper that cannot omit it. test_collaboration_stream_errors_always_carry_conversation_kind walks the AST of stream_collaboration_message_api and asserts exactly one raw _serialize_stream_error call remains, that it sets conversation_kind=COLLABORATION_KIND, and that every failure path routes through the helper — so a new error path without the tag now fails CI.

Repaired 59 stale @app.route assertions across 32 test files

Production has exactly one @app.route left — an example inside a swagger_wrapper.py docstring. The test suite still asserted the old form in 82 places across 40 files.

This is how root cause 2 shipped. test_collaboration_shared_ai_workflow.py existed specifically to guard this bridge, but broke on line 35 (an @app.route assertion) and died before reaching line 37, which checked the endpoint lookup. The test that should have caught the bug was standing right there, red for an unrelated reason.

Each rewrite was verified against a real @bp.route path in application/single_app before being changed. 14 occurrences were deliberately left alone because no matching production route exists — those point at routes that appear to have been removed or renamed, which is a separate problem that must not be papered over with a passing assertion.

Measured on the affected files: 47 failures → 34, zero newly broken.

Dead post-stream reload guard — filed as #1286, not fixed here

chat-streaming.js:1449 and :1515 guard on typeof window.chatMessages?.loadMessages === 'function', but loadMessages is not among the six functions assigned to window.chatMessages, and git log -S confirms it never was. Dead since commit 54e37c87.

The backend sets reload_messages: true when an agent plugin persists extra message documents into Cosmos, so those stay invisible until a manual reload. Impact is probably narrow — the final payload renders image_url separately — but sizing it needs a repro, and switching on a path that has never executed in production is not a safe blind change.


Tests

New functional_tests/test_collaboration_multi_user_reload_and_stream_fix.py (7 checks) — builds a Flask app registering chat_stream_api on a Blueprint named backend_chats exactly like production and asserts the resolver finds it even though chat_stream_api is not a view_functions key; asserts the unprefixed case still resolves and unknown names return None; asserts the frontend wiring and the error-tagging contract. The resolver is loaded by compiling just its AST node, so real behavior is exercised without standing up Cosmos/Search/OpenAI.

New ui_tests/test_chat_collaboration_conversation_load.py — extracts the shipped loadConversationMessages() source, runs it in Chromium, and asserts /conversation/<id>/messages is never requested, the collaboration endpoint is called exactly once, all four side effects fire, and no console errors occur.

Both new tests were verified to fail against pre-fix source and pass after.

Other test repairs (all already failing before this PR)

  • test_collaboration_shared_ai_workflow.py and test_message_metadata_loading_fix.py asserted pre-Blueprint @app.route / view_functions.get(...) forms.
  • Five tests asserted config.py VERSION with exact equality, which .github/instructions explicitly forbids and which any version bump breaks. Converted to assert_app_version_at_least().

Validation

Check Result
New functional test 7/7 passed
New UI test passed
test_collaboration_shared_ai_workflow.py passed (was failing)
test_chat_layered_message_masking.py passed (was failing)
Route policy tests (3) 12/12 passed
Full ui_tests/ suite 28 → 27 failures
Functional tests referencing changed JS 13 → 10 failures
Functional tests with repaired route assertions 47 → 34 failures
Chat ES module graph loaded in Chromium resolved, no console or page errors

Zero newly broken tests in any suite. Every remaining failure was confirmed identical against stashed pre-change source (Azure Cosmos credentials required at import, or unrelated stale assertions). All checks re-run after the Development merge at 194e3f75.

Sharing a personal conversation left it unusable end to end. Two independent
defects were involved.

selectConversation() called the personal loadMessages() for collaborative
conversations, which fetches /conversation/<id>/messages. That handler only
reads cosmos_conversations_container, but a shared conversation lives in the
collaboration container under a different id than its hidden
source_conversation_id, so the read always 404'd and raised a "Conversation not
found" toast. The call was redundant too: activateConversation() already loads
and renders the shared messages. Two side effects regressed as a result --
chat uploads never reached the Compare/Analyze picker, and task documents from
the previously viewed conversation stayed cached.

Separately, the collaboration stream bridge resolved the internal chat stream
view with current_app.view_functions.get('chat_stream_api'). The Blueprint
security hardening moved chat routes onto a 'backend_chats' Blueprint, so the
endpoint key became 'backend_chats.chat_stream_api'. The lookup returned None
and every collaborative AI request failed with "Chat streaming endpoint is
unavailable" before the model was called. Group collaborative conversations
share the same bridge and were affected as well.

Add a Blueprint-tolerant _resolve_internal_view_function() that keeps the
exact-match fast path for views registered directly on an app, and log a
[COLLABORATION] event when resolution fails so this cannot regress silently.
Drop the personal loader from the collaborative branch and move the search
highlight, task document, and comparison catalog side effects into
loadConversationMessages() so shared conversations keep parity.

Also repair test_collaboration_shared_ai_workflow.py and
test_message_metadata_loading_fix.py, which asserted the pre-Blueprint route
and view-lookup forms, and convert five exact-equality config.py VERSION
assertions to the mandated assert_app_version_at_least() helper -- all were
already failing before this change and are directly in the blast radius of a
version bump.

Fixes #1281

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer

Copy link
Copy Markdown
Contributor Author

Copilot resolve the merge conflicts in this pull request

# Conflicts:
#	docs/explanation/release_notes.md

Co-authored-by: paullizer <34814295+paullizer@users.noreply.github.com>

Copilot AI commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Copilot resolve the merge conflicts in this pull request

Resolved the Development merge conflict and pushed merge commit 194e3f75.

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