Let a conversation be deleted, and its thread with it - #197
Conversation
Nothing removed a channel, so the roster only ever grew (CopilotKit#196). Adds DELETE /api/channels/:channelId: the channel row, its memberships, agent links, and thread mapping go through the FK cascades that already existed for them, and the deployment asks Intelligence to permanently delete the thread itself. The local delete commits first. A rejected or unreachable upstream thread delete is non-fatal and leaves the channel gone from the roster either way, with an audit row (channel.deleted) naming the thread and whether Intelligence actually forgot it - a channel gone locally with an orphaned thread is a smaller, more honest failure than one still sitting in the roster with its history silently wiped. Removal fans out over the existing channel_activity NOTIFY topic so other open tabs see it live. The sidebar row gets an options menu with a two-button confirm dialog (new alert-dialog.tsx, wrapping @base-ui/react/alert-dialog).
… vanishing The confirm button was AlertDialogAction, which renders the primitive's Close: it shut the dialog the instant it was pressed, before the request it started was answered. A delete that failed then reported nothing at all, leaving the conversation in the roster with no explanation, and "Deleting…" could never appear. A plain button waits for the answer, closes on success, and shows the server's message otherwise. DELETE answers 200 with historyLeftBehind rather than a bare 204. The thread deletion is the half that can fail on its own, and 204 said the whole act succeeded whichever way it went, so a screen had no way to avoid claiming a message history was gone while it was still on the platform. The roster row lost its memo for a stated reason that does not hold: memo compares props and says nothing about a hook, and use-channel-events preserves row identity precisely so rows do not re-render. The audit row dropped the actor in single-user mode, believing audit_events.actor_user_id has a foreign key into users. It has none, and initializeDevActorUser writes that row at start-up anyway. Single-user is the mode .env.example ships switched on, so that was the default row, and it recorded that a conversation was deleted but not by whom. Also move a tab that is looking at the channel another tab just deleted, which was left on a route that no longer resolves, and cover the audit row, which nothing tested.
e84e439 to
ec92a34
Compare
davidmckayv
left a comment
There was a problem hiding this comment.
Merging. The feature is right and the reasoning behind it is sound. Driven in the browser against the real server, and six things fixed in ec92a34.
Driven. Three conversations deleted from the sidebar. Each removed the row, cascaded cleanly (channels, channel_memberships, channel_agents, intelligence_channel_mappings all 0 for the deleted id), and wrote one attributed channel.deleted row naming the thread with threadForgotten: true. The failure path was driven too, with the delete stubbed to fail.
A failed delete vanished silently. The confirm button was AlertDialogAction, which renders the primitive's Close: it shut the dialog the moment it was pressed, before the request it started was answered. I drove this. The dialog closed, the conversation stayed in the roster, and nothing anywhere said why. It also meant the Deleting… state could never appear. A plain button now waits for the answer, closes on success, and otherwise stays open with the server's own message.
A bare 204 could not tell the two outcomes apart. The thread deletion is the half that can fail on its own, and 204 reported the whole act as succeeded whichever way it went, while the dialog told the person their message history was gone. DELETE now answers 200 {"historyLeftBehind": boolean}. Observed on the wire: {"historyLeftBehind":false}. Reported as the question the caller has rather than the two facts behind it, since no thread and a forgotten thread both mean nothing was left behind.
The roster lost its memo for a reason that does not hold. memo compares props and has nothing to say about a hook, so state inside a row is no obstacle to it. Dropping it re-renders every row whenever the sidebar renders, which is the cost the identity-preserving patch in use-channel-events exists to avoid, as the comment right beside it says.
The audit row dropped the actor in single-user mode. The comment says audit_events.actor_user_id has a foreign key into users that this would violate. It has no foreign key, and initializeDevActorUser writes that row at start-up regardless, so neither half is true. Since single-user is the mode .env.example ships switched on, the unattributed row was the default: it recorded that a conversation was deleted, but not by whom. Verified both directly against the table and with a test that fails without the change.
A second tab was left on a dead route. The tab that issues the delete moves itself first; every other tab only dropped the row and stayed pointed at a channel id that no longer resolves.
Nothing tested the audit row, though the channel row is gone by then and it is the only record left. Added, along with the attribution case, the thread-outlived-the-channel case, and the audit-write-throws case. Also restored a docblock the new function had displaced from recordChannelActivityMutationOptions, and rebased onto main, resolving the CHANGELOG.
Full gate green: 1427 pass, 0 fail.
One thing left alone, worth its own change rather than this one: four other files (agents/routes.ts, components/routes.ts, routing/routes.ts, computer/routes.ts) carry the same stale foreign-key belief and drop the actor the same way. That is where this PR copied it from. Correcting them changes what existing rows look like across five surfaces, so it should not ride along here.



Closes #196.
What this changes
Adds
DELETE /api/channels/:channelId. Deletes the channel row, which cascades its memberships, agent links, and Intelligence thread mapping (no migration needed, those FKs already cascade). Also asks Intelligence to permanently delete the thread.Local delete commits first. A failed or unreachable thread delete is non-fatal: the channel is gone from the roster either way, with an audit row naming the thread and whether Intelligence actually forgot it.
Sidebar gets an options menu on each row, with a confirm dialog (new
alert-dialog.tsx, wraps@base-ui/react/alert-dialog, already a dependency).Where it runs
pg_notifyfires inside it on commit, and every replica's own LISTEN connection fans it out to its own sockets.deleted: trueon the existingchannel_activitytopic.Boundary and audit
channel.deletedaudit row on every delete, including a failed upstream thread delete.agentIdsent to Intelligence is derived server-side (channel:<id>), never accepted from the client.Changelog
Added under Unreleased.
Proof
bun run format:check lint typecheck testall pass. Tested manually against a local stack: delete with confirm dialog, deleting the open channel redirects, two tabs stay in sync, audit row shows up.