🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: notify users when chats are shared - #26914

Merged
DanielleMaywood merged 13 commits into
mainfrom
danielle/codagt-563-add-notifications-when-a-chat-is-shared
Jul 6, 2026
Merged

feat: notify users when chats are shared#26914
DanielleMaywood merged 13 commits into
mainfrom
danielle/codagt-563-add-notifications-when-a-chat-is-shared

Conversation

@DanielleMaywood

@DanielleMaywood DanielleMaywood commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Adds a Chat Shared system notification when a chat ACL update grants a user direct read access.

When the ACL changes, coderd notifies users who are newly granted read via the user ACL. Recipients are the new direct readers minus the previous direct readers minus the initiator, so re-applying an unchanged ACL notifies nobody and the sharer is never self-notified. Users who gain access only through a group ACL are not notified, which keeps notification volume bound to the ACL payload rather than group size. The notification links to /agents/{chatID}.

Details:

  • Recipients resolved in Go by directChatReaders (owner plus read-permitted UserACL entries). No group expansion, so the fan-out never reads group memberships and needs no AsSystemRestricted actor.
  • Fan-out runs in a background goroutine on the server context (api.ctx), so it does not block the PATCH response and a disconnect no longer cancels it once started. A disconnect between commit and the initiator lookup, or an enqueue failure, still drops the notification with only a warning log.
  • Template TemplateChatShared, migration 000538_chat_shared_notification, group Chat Events, kind system, enabled by default, with inbox fallback and golden coverage.
  • Docs updated (docs/ai-coder/agents/chat-sharing.md).

Tests: TestChatACLSharingLifecycle (direct grant notified, group member not notified, zero on unchanged re-apply and on unshare) and TestChatACLSharingNotifiesDirectReadersOnly (an org admin who is not the owner and belongs to the shared group shares with that group plus one direct user; only the direct user is notified).

Supersedes #26329, which was auto-closed as stale and cannot be reopened after the branch was rebased onto current main.

Note

🤖 This PR was written by Coder Agent on behalf of Danielle Maywood

@linear-code

linear-code Bot commented Jul 1, 2026

Copy link
Copy Markdown

CODAGT-563

@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-07-06 09:17 UTC by @DanielleMaywood
Spend: $97.41 / $100.00

Review history
  • R1 (2026-07-01): 18 reviewers, 3 Nit, 1 P1, 2 P2, 2 P3, 1 P4, REQUEST_CHANGES. Review
  • R2 (2026-07-01): 10 reviewers, 3 Nit, 1 P1, 2 P2, 3 P3, 2 P4, COMMENT. Review
  • R3 (2026-07-02): 5 reviewers, 3 Nit, 1 P1, 2 P2, 3 P3, 2 P4, COMMENT. Review
  • R4 (2026-07-06): 6 reviewers, 3 Nit, 1 P1, 2 P2, 3 P3, 2 P4, APPROVE. Review

deep-review v0.9.0 | Round 4 | dd216b9..7381213

Last posted: Round 4, 11 findings (1 P1, 2 P2, 3 P3, 2 P4, 3 Nit), APPROVE. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 P2 Author fixed (6f85d0c) exp_chats_acl.go:247 Initiator self-notifies when member of a shared group; reachable for admin users R1 Netero P3, Mafu-san P2 Yes
CRF-2 P1 Author fixed (6f85d0c) dbauthz/dbauthz.go:3439 Missing dbauthz test entry, CI blocks merge R1 Meruem P1, Ryosuke P2, Zoro P2 Yes
CRF-3 P2 Author fixed (6f85d0c) exp_chats_acl.go:210 Synchronous notification fan-out blocks HTTP response for unbounded duration R1 Hisoka P2, Knov P2, Mafuuu P3 Yes
CRF-4 P3 Author fixed (6f85d0c) dbauthz/dbauthz.go:3439 Missing rationale comment on actor-only auth wrapper R1 Knuckle P3, Kite Nit, Kurapika Nit, Meruem Nit, Zoro Nit Yes
CRF-5 P3 Author fixed (6f85d0c) exp_chats_acl.go:211 Warning log missing recipient count for observability R1 Chopper Yes
CRF-6 Nit Author fixed (6f85d0c) exp_chats_acl.go:230 slice.Unique redundant after SQL EXCEPT R1 Knuckle Nit, Ryosuke Nit, Pariston Nit, Zoro P3 Yes
CRF-7 P4 Author fixed (6f85d0c) exp_chats_acl_test.go:81 No test for overlapping direct+group membership R1 Bisky Yes
CRF-8 Nit Author fixed (6f85d0c) exp_chats_acl.go:260 chatACLReadIDs naming ambiguity (verb vs qualifier) R1 Gon Yes
CRF-9 Nit Author fixed (6f85d0c) exp_chats_acl.go:267 chatACLReadIDs silently swallows UUID parse errors unlike sibling chatACLUsers R1 Leorio Nit, Chopper Note Yes
CRF-10 P3 Author fixed (7381213) exp_chats_acl.go:254 Unbounded notification volume when sharing with large groups R2 Hisoka P3, Meruem Note Yes
CRF-11 P4 Author contested; panel closed R3 (5/5 accept) exp_chats_acl_test.go:106 Negative notification assertions unsynchronized with async goroutine R2 Meruem P3, Bisky Note Yes

Contested and acknowledged

CRF-10 (P3, exp_chats_acl.go:254) - Unbounded notification volume

  • Finding: Sharing a chat with a large group (10k members) triggers ~20k+ DB operations from one PATCH request via background fan-out. No recipient cap or bulk-insert path.
  • Author accepted R3: Accepts linear cost. Fan-out is background, non-blocking, concurrency-capped at 10. Feature is experimental. Capping would silently drop notifications. Bulk-insert is a cross-cutting notification subsystem change, out of scope.

CRF-11 (P4, exp_chats_acl_test.go:106) - Negative assertion synchronization

  • Finding: Three require.Empty checks run immediately after UpdateChatACL returns, before the async notification goroutine completes. They assert "nothing enqueued at this instant," not "goroutine completed with zero enqueues."
  • Author defense: The recipient diff for an unchanged or shrinking ACL is deterministically empty. No enqueue can occur regardless of timing. Assertions cannot flake. require.Never would slow tests without adding real protection.
  • Panel closure (R3, 5/5): All five reviewers verified the invariant independently. The recipient diff computation is a pure function of the data snapshot. For unchanged or shrinking ACLs, the diff is empty by construction, making Enqueue unreachable. FakeEnqueuer is mutex-protected, so concurrent access is safe. Stress runs (-race -count=10 at multiple GOMAXPROCS) confirmed. The assertion tests data correctness, not timing.

Round log

Round 1

Panel. 1 P1, 2 P2, 2 P3, 1 P4, 3 Nit. Reviewed against e3ac65a..5e80684.

Round 2

Panel. All 9 R1 findings addressed (6f85d0c). 1 P3, 1 P4 new. Reviewed against 047c474..6f85d0c.

Round 3

Panel. CRF-11 contested; panel closed (5/5 accept). CRF-10 acknowledged, needs human decision. 0 new findings. Reviewed against 047c474..6f85d0c.

Round 4

Panel. CRF-10 addressed by design change (group expansion removed). All 11 findings closed. 0 new findings. 6 reviewers, unanimous. Reviewed against dd216b9..7381213.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@DanielleMaywood
DanielleMaywood force-pushed the danielle/codagt-563-add-notifications-when-a-chat-is-shared branch from 5e80684 to a8f92d4 Compare July 1, 2026 12:03

@coder-agents-review coder-agents-review Bot 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.

Clean, well-scoped notification feature. The SQL recipient-diff query (new_readers EXCEPT old_readers with group expansion) is an elegant single-pass computation that keeps the complexity in the database. Test coverage is solid: positive assertions verify the exact recipient set with labels, targets, and createdBy, while negative assertions cover idempotent re-apply and unshare. The maps.Clone snapshot inside the transaction correctly captures pre-mutation state.

1 P1, 2 P2, 2 P3, 1 P4, 3 Nit.

The P1 blocks CI: TestMethodTestSuite requires a test entry for every dbauthz querier method, and GetChatShareNotificationRecipientIDs is missing. Three reviewers verified the failure independently.

The P2s are (1) initiator self-notification when sharing with a group the initiator belongs to, and (2) synchronous notification fan-out blocking the HTTP response for an unbounded duration when sharing with large groups. Both need a human decision: fix in this PR or file a ticket.

The PR description claims "the initiator is never self-notified," but the SQL query excludes owner_id, not initiator_id. These diverge when an admin (not the chat owner) shares a chat with a group they belong to. Admin users have ActionShare on ResourceChat at Site level, so this path is reachable today.

"The one fix that unblocks everything: add the dbauthz test entry. That's the pit stop the car missed." (Ryosuke)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/database/dbauthz/dbauthz.go Outdated
Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/database/dbauthz/dbauthz.go Outdated
Comment thread coderd/exp_chats_acl.go
Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/exp_chats_acl_test.go Outdated
Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/exp_chats_acl.go
@DanielleMaywood
DanielleMaywood force-pushed the danielle/codagt-563-add-notifications-when-a-chat-is-shared branch 3 times, most recently from ae9c96e to 0b6d9fe Compare July 1, 2026 13:38
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot 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.

All 9 R1 findings addressed. The redesign from SQL-based recipient query to Go-based group expansion via effectiveChatReaders is structurally cleaner: deduplication falls out of the map, the initiator filter is explicit, and the owner is always in both reader sets so the diff naturally excludes them. The background goroutine on api.ctx fixes the HTTP latency and context cancellation issues. The new TestChatACLSharingExcludesGroupMemberInitiator test covers the non-owner initiator case and direct+group deduplication. All R1 fixes verified by 10 reviewers.

1 P3, 1 P4 new.

The P3 is about unbounded notification volume: sharing a chat with a large group (the "everyone" group in a 10k-member org) triggers ~10k enqueue calls from one PATCH request. The fan-out is bounded in concurrency (10 at a time) and runs in the background, so there's no correctness or latency issue. But the sustained DB load from a single user action is worth a human decision: accept the linear cost, cap recipients with a warning, or defer to a bulk notification API.

Minor note: the PR description's migration number (000534) doesn't match the actual migration file (000535). The CRF attributions in the description reference the superseded PR #26329's review and don't map to the current review's finding numbers.

"I tried to build a case against this change and couldn't. The problem is correctly understood, the solution is proportional, and the fix is at the right level." (Pariston)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats_acl.go
Comment thread coderd/exp_chats_acl_test.go
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot 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.

No new findings. CRF-11 (negative assertion synchronization) closed by panel vote: 5/5 reviewers verified the author's defense. The recipient diff for an unchanged or shrinking ACL is deterministically empty, making Enqueue unreachable regardless of goroutine timing. The assertion tests data correctness, not a timing window.

CRF-10 (unbounded notification volume) remains open for a human decision. The author's reasoning is sound: the fan-out is background, non-blocking, concurrency-capped, and the feature is experimental. Capping would silently drop legitimate notifications. A bulk-insert notification API would bound the cost but is a cross-cutting subsystem change. A human should decide: accept the linear cost, file a ticket for a bulk path, or close the finding.

11 findings across 3 rounds: 9 fixed, 1 closed by panel vote, 1 awaiting human disposition. The code is clean.

"The invariant is in the data, not the timing." (Meruem)

🤖 This review was automatically generated with Coder Agents.

Derive the notifier context once at the notification boundary instead of inside effectiveChatReaders, document the ResourceGroupMember grant, drop a self-explanatory comment, and revert a stray trailing newline in chats.sql.
Run the chat-shared notification fan-out on the server context instead of the request context so a large group does not block the PATCH response and a client disconnect does not drop notifications. Log the recipient count on failure and log ACL entries with invalid UUIDs. Add a regression test for a non-owner group-member initiator and direct/group reader deduplication.
@DanielleMaywood
DanielleMaywood force-pushed the danielle/codagt-563-add-notifications-when-a-chat-is-shared branch from 8e04ff4 to 7381213 Compare July 6, 2026 09:04
@DanielleMaywood

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot 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.

All 11 findings across 4 rounds resolved. No new findings. 6 reviewers, unanimous.

The R4 design simplification is the cleanest iteration yet. directChatReaders replaces group expansion with a simple UserACL-only reader set, eliminating the unbounded fan-out (CRF-10) at the design level rather than capping it. No ResourceGroupMember:read escalation needed. Notification volume is now bounded by the ACL payload, which the caller controls.

The product tradeoff (group grants give access but don't trigger notifications) is well-documented in the PR description and tested explicitly in TestChatACLSharingNotifiesDirectReadersOnly.

"I tried to build a case against this change and could not. The problem is correctly understood, the solution is proportional, and the fix is at the right level." (Pariston)

🤖 This review was automatically generated with Coder Agents.

@DanielleMaywood
DanielleMaywood marked this pull request as ready for review July 6, 2026 11:12
@coder-tasks

coder-tasks Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Documentation Check

Updates Needed

  • docs/ai-coder/agents/chat-sharing.md - Line 14 stated "Coder does not create a separate share link or notify recipients." Updated to reflect that directly shared users receive a Chat Shared notification with a link, and that group-ACL members are not notified.

Automated review via Coder Agents

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/ai-coder/agents/chat-sharing.md

@DanielleMaywood DanielleMaywood changed the title feat(coderd): notify users when chats are shared feat: notify users when chats are shared Jul 6, 2026

@mafredri mafredri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Writing critique of the PR text (title, description, comments, log messages, templates, docs). Every prose unit was enumerated and judged; findings below give the unit, the failure, and a full replacement. Inline comments carry the code-side and docs-side findings.

Finding 1: opening sentence is wider than the mechanism.
Unit: "Adds a Chat Shared system notification when a chat's ACL is updated."
Failure: the notification does not fire on every ACL update; the next paragraph itself says re-applying an unchanged ACL notifies nobody. The two claims conflict.
Replacement: "Adds a Chat Shared system notification when a chat ACL update grants a user direct read access."

Finding 2: "a client disconnect does not drop notifications" is oversized, in two places.
Unit: description bullet "Fan-out runs in a background goroutine on the server context (api.ctx), so it never blocks the PATCH response and a client disconnect does not drop notifications." and the code comment at exp_chats_acl.go:213 (inline comment there).
Failure: GetUserByID runs on the request context after the transaction commits and before the goroutine starts; a disconnect in that window cancels it and the whole fan-out is skipped with only a warning. Enqueue failures and server shutdown (api.ctx cancel) also drop notifications. The mechanism narrows the drop window; it does not eliminate drops.
Replacement bullet: "Fan-out runs in a background goroutine on the server context (api.ctx), so it does not block the PATCH response and a disconnect no longer cancels it once started. A disconnect between commit and the initiator lookup, or an enqueue failure, still drops the notification with only a warning log."

Finding 3: "ResourceGroupMember escalation" is a coinage that does not deliver its mechanism.
Unit: "No group expansion, so no ResourceGroupMember escalation and no AsSystemRestricted."
Failure: "escalation" stands in for a causal chain the reader must already know: expanding groups means reading group memberships, which the request actor may not be authorized for, which is what would force a system actor. The bullet names the consequence without the mechanism.
Replacement: "No group expansion, so the fan-out never reads group memberships and needs no AsSystemRestricted actor."

Finding 4: docs bullet duplicates a fact already stated.
Unit: "Docs updated (docs/ai-coder/agents/chat-sharing.md) to note direct shares are notified and group members are not."
Failure: the second paragraph already states who is and is not notified; restating it here puts the same fact in two places. The bullet's only new information is which file changed.
Replacement: "Docs updated (docs/ai-coder/agents/chat-sharing.md)."

Finding 5: second test description is a noun pile.
Unit: "TestChatACLSharingNotifiesDirectReadersOnly (non-owner org-admin group-member initiator sharing with a group plus a direct user notifies only the direct user)."
Failure: five stacked modifiers before "initiator" force the reader to re-parse; the actor and the action are buried.
Replacement: "TestChatACLSharingNotifiesDirectReadersOnly (an org admin who is not the owner and belongs to the shared group shares with that group plus one direct user; only the direct user is notified)."

Findings 6 and 7 are inline: the docs paragraph now conflicts with the unchanged step above it, and the fan-out warning log describes partial failure as total failure.

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/exp_chats_acl.go Outdated
if err != nil {
api.Logger.Warn(ctx, "failed to load chat share initiator", slog.Error(err), slog.F("chat_id", chat.ID))
} else {
// api.ctx, not the request ctx, so disconnects don't drop notifications.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding: oversized claim. "disconnects don't drop notifications" is wider than the mechanism: GetUserByID above runs on the request context after commit, so a disconnect in that window skips the fan-out entirely (warn log only), and enqueue failures or api.ctx cancellation on shutdown also drop notifications. The goroutine narrows the drop window; it does not eliminate drops. Same claim appears in the PR description (see review body).

Replacement:

// api.ctx, not the request ctx, so a disconnect no longer cancels the fan-out once it starts.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Fixed in 298afd7. Reworded to // api.ctx, not the request ctx, so a disconnect no longer cancels the fan-out once it starts. and corrected the same overclaim in the PR description.

Comment thread coderd/exp_chats_acl.go Outdated
newChat := aReq.New
go func() {
if count, err := api.notifyChatShared(api.ctx, oldChat, newChat, initiator); err != nil {
api.Logger.Warn(api.ctx, "failed to enqueue chat shared notification", slog.Error(err), slog.F("chat_id", newChat.ID), slog.F("recipient_count", count))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding: log message describes partial failure as total failure. notifyChatShared returns len(recipientIDs) and an errors.Join that may hold a subset, so when 1 of 5 enqueues fails this logs the singular "failed to enqueue chat shared notification" with recipient_count=5. The reader cannot tell how many failed or that the count is attempts, not failures.

Replacement:

api.Logger.Warn(api.ctx, "failed to enqueue one or more chat shared notifications", slog.Error(err), slog.F("chat_id", newChat.ID), slog.F("attempted_recipients", count))

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Fixed in 298afd7. The log is now failed to enqueue one or more chat shared notifications with attempted_recipients (renamed from recipient_count), since the count is attempts and the joined error may hold only a subset of failures.

Comment thread docs/ai-coder/agents/chat-sharing.md Outdated
Comment on lines +12 to +14
1. Copy the chat URL from your browser and send it to the recipients.

Coder does not create a separate share link or notify recipients. Recipients need the chat URL for initial access.
Coder does not create a separate share link. Users you share with directly receive a **Chat Shared** notification with a link to open the chat. Members who gain access only through a group are not notified, so send them the chat URL for initial access.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding: the new paragraph conflicts with the unchanged step above it. Step 6 still says "Copy the chat URL from your browser and send it to the recipients", unconditionally, while this paragraph says directly shared users get a link automatically and only group members need the URL. A reader following the steps does redundant work; a reader reading both units gets contradictory instructions. The instruction belongs in the step, the behavior in the paragraph.

Replacement for step 6:

1. If you shared with a group, copy the chat URL from your browser and send it to the group members.

Replacement for this paragraph:

Coder does not create a separate share link. Users you share with directly receive a **Chat Shared** notification with a link to open the chat. Members who gain access only through a group are not notified.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Fixed in 298afd7. Step 6 now applies only to group shares (If you shared with a group, copy the chat URL...), and the paragraph drops the redundant trailing URL clause.

Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/exp_chats_acl.go

Copy link
Copy Markdown
Contributor Author

🤖 This comment was written by Coder Agent on behalf of Danielle Maywood 🤖

Thanks for the prose critique. Findings 1-5 (PR description) are applied in the updated description:

  • Opening sentence narrowed to "when a chat ACL update grants a user direct read access."
  • Fan-out bullet now states the commit-window disconnect and enqueue failures still drop the notification with a warning log.
  • "ResourceGroupMember escalation" replaced with "the fan-out never reads group memberships and needs no AsSystemRestricted actor."
  • Docs bullet trimmed to just the file name.
  • Second test description de-noun-piled to "an org admin who is not the owner and belongs to the shared group shares with that group plus one direct user; only the direct user is notified."

Findings 6 and 7 (inline: docs step/paragraph conflict and the partial-vs-total log message) are fixed in 298afd7.

Comment thread coderd/exp_chats_acl.go Outdated
Comment thread coderd/exp_chats_acl.go Outdated
@DanielleMaywood
DanielleMaywood merged commit 7c78698 into main Jul 6, 2026
32 of 33 checks passed
@DanielleMaywood
DanielleMaywood deleted the danielle/codagt-563-add-notifications-when-a-chat-is-shared branch July 6, 2026 12:16
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 6, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants