feat(channels): sidebar settings menu, channel rate limits, limits for everyone - #892
Merged
Merged
Conversation
…r everyone Channel settings existed but the feature was half-built: reachable only from a gear inside the channel view, `rules` read by nothing, `char_limit` enforced for mind senders only, and no rate limit anywhere. - Sidebar channel rows get a dots menu (Channel settings / Make private / Leave channel), matching the mind rows. The settings modal is now reachable without opening the channel first. - New channel-wide rate limit (`rate_limit` + `rate_window`): at most N messages per W seconds pooled across all senders. Over-limit sends are refused with 429 naming the limit and when it frees up; nothing is queued or dropped. - Both limits now apply to every sender, not just minds, and `char_limit` is a per-message rather than per-text-block rule. Enforcement moves into `lib/chat/channel-limits.ts`, called from the `/api/v1/chat` handler. - Channels introduce themselves to minds — description, rules, and limits — once per channel per session, alongside the participant profiles. Previously a mind met a limit only by being refused by it and never saw `rules` at all. - Settings editing tightens from any member to creator-or-admin. The creator is the participant already stamped `role: "owner"` at creation, so this needs no new column; ownerless channels (the commons) are admin-only. - The web composer shows a live counter against the limit and blocks an over-limit send; a refused send now surfaces the server's reason instead of a generic failure, and withdraws its optimistic bubble. Two bugs fixed along the way: - The channel row's `X` called `deleteConversation`, hard-deleting the channel for every participant. It is now an explicit "Leave channel". - `handleSave` in the settings modal called `.trim()` on `bind:value` from an `<input type="number">`, which yields a number — so saving a character limit always threw and silently did nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Code review of the channel-limits branch surfaced ten verified defects. Fixes: - **Sidebar settings could wipe a channel.** The modal seeds its form once at mount, and both callers could open it before the settings fetch resolved — so a channel with settings showed a blank form, and Save erased its description, rules and both limits and flipped a private channel public. App.svelte now loads before mounting; Chat.svelte disables the gear until settings resolve. - **Owner-only settings were bypassable.** `DELETE /api/v1/conversations/:id` let any participant destroy a channel and re-create it as owner with no limits. Deleting a channel now takes the same owner-or-admin authority as editing it. - **Automated traffic spent the rate budget.** The window excluded role `"system"` but not `"event"`, so commons announcements could rate-limit a channel into silence with nobody having spoken. Only `role = "user"` counts. - **Files staged before the limit check**, leaving minds holding pending offers for messages that were never sent, one more per retry. The check now runs before staging (and before fixModelEscapes-normalized text is measured). - **echo-text bypassed the limits** while its writes still consumed the window — a mind with echoText on was never held to a limit at all. It now checks, and drops + logs an over-limit echo. Bridge inbound stays exempt on purpose: refusing it would delete words already said on another platform. Documented. - **A refused send destroyed the typed message.** The composer clears before dispatch, so withdrawing the optimistic bubble lost the only copy. onSend now reports refusal and the composer restores the text and attachments. - **"Leave channel" silently forfeited ownership** for good, since ownership is only the participant row. Owners now get an in-place confirm saying so. - **A half-cleared rate pair** was accepted and silently stopped enforcing while the API still reported it — and blocked the patch that would repair it. Coherence is now judged against the row as the patch will leave it. - **The channel card was never re-announced.** A limit added mid-session never reached the mind, and a failed read marked the channel introduced for good. It is now keyed on the settings row's updated_at, tracked separately from the once-per-session participant profiles. - **No UI path to delete a channel** remained after the destructive X was removed. Added a two-click "Delete channel" for owners and admins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The #865 restart test respawned the daemon as soon as the `npx` wrapper's exit event fired. That wrapper is not the daemon: the real process can still hold the listening socket, so the replacement lost the bind with EADDRINUSE and exited, and `waitForHealth()` was satisfied by the outgoing daemon — whose HTTP server closes last in `shutdown()`, long after `scheduler.stop()`. The test then spent its full 90s fire window watching a scheduler nobody was running. The cure already existed in this file, at the auto-upgrade restart: give the shutdown a real budget, then wait for the port itself with `waitForPortFree()`, which escalates to the pid actually holding it. Mirror it here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Channel settings existed but the feature was half-built: reachable only from a gear inside the channel view,
rulesread by nothing at all,char_limitenforced for mind senders only, and no rate limit anywhere in the repo.What this adds
rate_limit+rate_window): at most N messages per W seconds, pooled across all senders. Over-limit sends are refused with 429 naming the limit and when it frees up. Nothing is queued or silently dropped.char_limitbecame a per-message rather than per-text-block rule. Enforcement lives in one place,lib/chat/channel-limits.ts.rulesat all.Behavior changes to know about
createConversationalready stamps the creatorrole: "owner". A mind therefore can't lift a limit set to restrain it — but it also loses edit rights on channels it didn't create, and ownerless channels (the commons) become admin-only.Xis gone. It calleddeleteConversation— a hard delete that destroyed the channel and its whole history for every participant, available to any participant. Replaced by an explicit two-click Delete channel, gated to owners and admins on both client and server.char_limitcounts the whole message.Bugs found and fixed along the way
.trim()onbind:valuefrom an<input type="number">, which yields a number — so saving a character limit always threw and silently did nothing, ever since the modal shipped.Xhard-delete above.The second commit closes ten review findings in total (rate window counting automated
"event"rows; files staged before the limit check leaving orphaned pending offers; echo-text bypassing limits; a refused send destroying the typed message; leaving silently forfeiting ownership; a half-cleared rate pair silently disabling enforcement; the channel card never re-announced after a mid-session change).Verification
npm test3360/3360,npm run test:e2e70/70, tsc + svelte-check + template typecheck clean.~/.volute): menu, settings round-trip on a fully-populated channel, save-unchanged preserving all five settings, the63/50counter in red, the real rate-limit text in chat, the refused message returning to the composer, owner-leave and delete confirms, and the authz gating A/B'd by flipping ownership (as a plain member the settings items and the header gear both disappear).Follow-ups filed rather than folded in: #890 (
GET /api/v1/channels/:namehas no authorization at all) and #891 (thechannels.private/conversations.privatedrift).🤖 Generated with Claude Code