feat: notify users when chats are shared - #26914
Conversation
|
/coder-agents-review |
|
Chat: Review posted | View chat Review history
deep-review v0.9.0 | Round 4 | Last posted: Round 4, 11 findings (1 P1, 2 P2, 3 P3, 2 P4, 3 Nit), APPROVE. Review Finding inventoryFindings
Contested and acknowledgedCRF-10 (P3, exp_chats_acl.go:254) - Unbounded notification volume
CRF-11 (P4, exp_chats_acl_test.go:106) - Negative assertion synchronization
Round logRound 1Panel. 1 P1, 2 P2, 2 P3, 1 P4, 3 Nit. Reviewed against e3ac65a..5e80684. Round 2Panel. All 9 R1 findings addressed (6f85d0c). 1 P3, 1 P4 new. Reviewed against 047c474..6f85d0c. Round 3Panel. CRF-11 contested; panel closed (5/5 accept). CRF-10 acknowledged, needs human decision. 0 new findings. Reviewed against 047c474..6f85d0c. Round 4Panel. 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-reviewCRF = Coder Review Finding (P0-P4, Nit, Note)
|
5e80684 to
a8f92d4
Compare
There was a problem hiding this comment.
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.
ae9c96e to
0b6d9fe
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
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.
|
/coder-agents-review |
There was a problem hiding this comment.
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.
8e04ff4 to
7381213
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
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.
Documentation CheckUpdates Needed
Automated review via Coder Agents |
Docs preview📖 View docs preview for |
mafredri
left a comment
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
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.🤖
There was a problem hiding this comment.
🤖 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.
| 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)) |
There was a problem hiding this comment.
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))🤖
There was a problem hiding this comment.
🤖 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.
| 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. |
There was a problem hiding this comment.
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.🤖
There was a problem hiding this comment.
🤖 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.
Thanks for the prose critique. Findings 1-5 (PR description) are applied in the updated description:
Findings 6 and 7 (inline: docs step/paragraph conflict and the partial-vs-total log message) are fixed in 298afd7. |
Adds a
Chat Sharedsystem 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:
directChatReaders(owner plus read-permittedUserACLentries). No group expansion, so the fan-out never reads group memberships and needs noAsSystemRestrictedactor.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.TemplateChatShared, migration000538_chat_shared_notification, groupChat Events, kindsystem, enabled by default, with inbox fallback and golden coverage.docs/ai-coder/agents/chat-sharing.md).Tests:
TestChatACLSharingLifecycle(direct grant notified, group member not notified, zero on unchanged re-apply and on unshare) andTestChatACLSharingNotifiesDirectReadersOnly(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