Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.023"
VERSION = "0.261.024"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
32 changes: 24 additions & 8 deletions application/single_app/functions_collaboration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1114,10 +1114,23 @@


def ensure_group_collaboration_for_legacy_conversation(source_conversation_id, owner_user, invited_participants=None):
source_conversation_doc = cosmos_group_conversations_container.read_item(
item=source_conversation_id,
partition_key=source_conversation_id,
)
source_container = cosmos_group_conversations_container

Check warning on line 1117 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 1117 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
copy_source_messages = _copy_legacy_group_messages_to_collaboration
source_link_field = 'legacy_source_conversation_id'

Check warning on line 1119 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
try:

Check warning on line 1120 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
source_conversation_doc = source_container.read_item(

Check warning on line 1121 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
item=source_conversation_id,

Check warning on line 1122 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
partition_key=source_conversation_id,

Check warning on line 1123 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
)
except CosmosResourceNotFoundError:

Check warning on line 1125 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
# Group context can classify a conversation without moving its backing stores.

Check warning on line 1126 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
source_container = cosmos_conversations_container

Check warning on line 1127 in application/single_app/functions_collaboration.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
copy_source_messages = _copy_legacy_personal_messages_to_collaboration
source_link_field = 'source_conversation_id'
source_conversation_doc = source_container.read_item(
item=source_conversation_id,
partition_key=source_conversation_id,
)
owner_summary = owner_user or {}
owner_user_id = str(owner_summary.get('user_id') or '').strip()
if not owner_user_id:
Expand Down Expand Up @@ -1196,8 +1209,9 @@
)
collaboration_conversation_doc['strict'] = bool(source_conversation_doc.get('strict', False))
collaboration_conversation_doc['summary'] = source_conversation_doc.get('summary')
collaboration_conversation_doc['legacy_source_conversation_id'] = source_conversation_id
collaboration_conversation_doc['legacy_source_scope'] = 'group'
collaboration_conversation_doc[source_link_field] = source_conversation_id
if source_link_field == 'legacy_source_conversation_id':
collaboration_conversation_doc['legacy_source_scope'] = 'group'

source_context = list(source_conversation_doc.get('context', []) or [])
if source_context:
Expand All @@ -1209,7 +1223,7 @@
if source_locked_contexts:
collaboration_conversation_doc['locked_contexts'] = source_locked_contexts

copied_messages = _copy_legacy_group_messages_to_collaboration(
copied_messages = copy_source_messages(
source_conversation_id,
collaboration_conversation_doc.get('id'),
owner_summary,
Expand All @@ -1230,7 +1244,9 @@
source_conversation_doc['converted_to_collaboration_at'] = conversion_timestamp
source_conversation_doc['is_hidden'] = True
source_conversation_doc['last_updated'] = conversion_timestamp
cosmos_group_conversations_container.upsert_item(source_conversation_doc)
source_container.upsert_item(source_conversation_doc)
invalidate_conversation_cache_for_item(source_conversation_doc, reason="collaboration_source_converted")
invalidate_conversation_cache_for_item(collaboration_conversation_doc, reason="collaboration_converted")

log_event(
'[COLLABORATION] Converted group conversation into collaborative conversation',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@

Planning version: **0.250.062**

Implemented in version: **Not implemented - discovery and planning only**
Historical plan status: **Discovery and planning only; not implemented in full**

Related configuration version: `application/single_app/config.py` currently sets `VERSION = "0.250.062"`.
Related configuration version at planning time: `application/single_app/config.py` set `VERSION = "0.250.062"`.

The backend source-storage mismatch described below is fixed for Development/v1 in
**0.261.024** under [#1472](https://github.com/microsoft/simplechat/issues/1472).
See [Group Collaboration Source Storage Fix](../fixes/GROUP_COLLABORATION_SOURCE_STORAGE_FIX.md)
for the implemented scope and coverage. This does not implement this historical
plan's UI wording, stale-DOM, or endpoint-selection proposals.

## Overview

Expand Down Expand Up @@ -315,4 +321,3 @@ Cover:
1. Should `/from-group/<id>/members` delegate when `<id>` is already a group collaborative conversation ID, or should it return a diagnostic error?
2. Should the UI show a one-line hint with the active group name in the participant picker?
3. Should group participant suggestions include pending group users, or only accepted/current group members? Current behavior should remain accepted/current members unless product requirements change.

167 changes: 167 additions & 0 deletions docs/explanation/fixes/GROUP_COLLABORATION_SOURCE_STORAGE_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Group Collaboration Source Storage Fix (v0.261.024)

Fixed in version: **0.261.024**

Related issue: [#1472](https://github.com/microsoft/simplechat/issues/1472).

The application patch version in `application\single_app\config.py` changes from
**0.261.023** to **0.261.024** for this Development/v1 fix.

## Issue

Adding the first participant to an existing group-scoped single-user conversation
could return `404 Conversation not found`, even while its owner could still open
the conversation and read its history.

V1 sends this invitation to the existing endpoint:

```text
POST /api/collaboration/conversations/from-group/<conversation_id>/members
```

The failure occurred during source lookup, before the selected invitee was
evaluated. It did not necessarily mean the conversation had been deleted.

## Root cause

Normal conversation creation writes to `conversations`, with history in
`messages`. Group knowledge or a group agent can later establish primary group
context and classify the same record as `group-single-user` without moving either
record set.

Group collaboration conversion assumed a different physical layout:
`group_conversations` and `group_messages`. The lookup, history copier, original
source update, and collaboration source links all followed that assumption.
Changing only the lookup would have left the copied history and later source
operations pointed at the wrong containers.

## Implementation

`ensure_group_collaboration_for_legacy_conversation()` now keeps the source
conversation container, existing message copier, and source-link field together
through conversion.

Legacy group storage remains authoritative. Only a
`CosmosResourceNotFoundError` from that lookup permits a regular-storage lookup.
Authorization failures, throttling, service failures, and other exceptions do not
trigger another-store retry. If both containers lack the record, the endpoint
still returns the existing 404.

| Original layout | History copier | Link on the collaborative conversation |
| --- | --- | --- |
| `conversations` / `messages` | `_copy_legacy_personal_messages_to_collaboration()` | `source_conversation_id` |
| `group_conversations` / `group_messages` | `_copy_legacy_group_messages_to_collaboration()` | `legacy_source_conversation_id`, with `legacy_source_scope = 'group'` |

Both paths create a group collaboration. Reusing the regular-storage copier does
not change the conversation's group access rules. It preserves the distinction
between group workspace context and `source_conversation_scope = 'group'` message
provenance, which specifically selects the legacy group message store.

Conversion preserves the existing title, context, tags, classification,
citation-tracking fields, strict mode, summary, and scope locks. The existing
copiers preserve supported message content and metadata, chronological ordering,
uploaded-content attribution, artifact filtering, and generated-image message
associations. Message counts and previews continue to derive from the copied
transcript.

The original is hidden and back-linked in its actual container. Cache
invalidation runs after the hidden source and completed collaboration have been
persisted, preventing cached conversation lists from retaining the pre-conversion
state.

### Source lifecycle

For a regular-stored original, the existing shared AI source helper reuses
`source_conversation_id` and its history instead of creating an empty backing
conversation. Repeat invitations still reuse the collaboration after that helper
updates the backing record's chat type and kind.

Legacy group originals retain their existing separate AI backing-source behavior.
The existing masking, deletion, archival, and retention helpers use the matching
source links and message provenance without new storage schemas or migrations.
Cleanup continues to enforce its source ownership and backward-link guards.

### Preserved restrictions and API contract

- Only the source conversation owner can convert an eligible group conversation.
- The owner must still hold a current allowed group role, and the group's status
must allow chat. Existing chat permissions for active, locked, and
upload-disabled groups remain unchanged; inactive groups remain rejected.
- Invitees must already be current group members. This flow does not add people
to a group workspace or allow arbitrary directory users into the conversation.
- Rejected ownership, group, or invitee requests do not copy history or write
conversion state.
- New conversions return 201 with `created: true`; subsequent invitations reuse
the collaboration and return 200 with `created: false`. Response fields and
creation/invitation events are unchanged.

## Files changed

- `application\single_app\functions_collaboration.py`: paired source selection,
existing copier/link selection, original-container update, and final cache
invalidation.
- `application\single_app\config.py`: application patch version.
- `functional_tests\test_group_collaboration_source_storage_fix.py`: isolated
behavioral regressions using actual production helpers and the v1 route.
- `docs\explanation\features\GROUP_COLLABORATION_MEMBER_INVITE_FIX_PLAN.md`:
distinguish this implemented backend fix from the historical UI proposals.

The route module, personal conversion implementation, and downstream cleanup
helpers are unchanged.

## Validation

The new regression first reproduced the exact 404 through the actual v1
conversion handler against a regular-stored group conversation before the
production fix.

Run the focused behavioral coverage with:

```powershell
python -m pytest -q .\functional_tests\test_group_collaboration_source_storage_fix.py
```

The regression executes the production conversion, participant normalization,
group role/status checks, source bridge, metadata synchronization, masking, and
cleanup helpers against partition-aware in-memory stores. Flask request-context
dispatch exercises the existing route without application startup, Azure clients,
or deployed services.

Coverage includes both layouts, all existing allowed group roles and chat
statuses, empty and populated histories, repeated invitations, current membership
revalidation, personal conversion compatibility, lookup priority, no-mutation
rejections, service-error responses, and manual/retention/archive cleanup that
leaves unrelated records untouched.

The new regression passes **18 tests and 106 subtests** under pytest. The combined
conversion, participant, image-proposal, shared-AI, retention, route-policy, and
documentation run completed with **60 tests and 106 subtests passing**, plus the
one pre-existing uploaded-image regression failure described below.

| Scenario | Before | After |
| --- | --- | --- |
| First group invitation for a regular-stored source | 404 before copying history | 201, with preserved transcript and a linked, hidden regular source |
| Legacy group-store conversion | Supported | Remains supported with its existing source conventions |
| Another invitation after conversion | Reuse required | 200, same collaboration, no transcript copy |
| Missing source in both stores | 404 | Same 404 |
| Non-not-found source storage error | Must surface as an error | No cross-store fallback or false success |

One related pre-existing regression,
`test_collaboration_legacy_message_conversion.py::test_uploaded_image_conversion_preserves_user_sender`,
already fails on the Development baseline: it expects the older uploaded-image
role/content representation rather than the model's current image representation.
That model and test are unchanged. The new conversion coverage checks preservation
of the current uploaded-image sender metadata, provenance, and associations.

## Scope and impact

Owners can share affected group-scoped conversations without moving stored data
or losing the original history. Existing group authorization and participant
restrictions remain in place.

This change is limited to the Development/v1 backend, regression coverage, and
documentation. It makes no React/v2 implementation or parity claim and includes no
UI wording changes, new routes/settings, deployment changes, or data migration.
The broader proposals in the
[historical group invitation plan](../features/GROUP_COLLABORATION_MEMBER_INVITE_FIX_PLAN.md)
are not represented as completed by this fix.
Loading
Loading