Skip to content

Let a conversation be deleted, and its thread with it - #197

Merged
davidmckayv merged 2 commits into
CopilotKit:mainfrom
CristianPeralta:remove-a-conversation-and-forget-its-thread
Aug 24, 2026
Merged

Let a conversation be deleted, and its thread with it#197
davidmckayv merged 2 commits into
CopilotKit:mainfrom
CristianPeralta:remove-a-conversation-and-forget-its-thread

Conversation

@CristianPeralta

Copy link
Copy Markdown
Contributor

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

  • New state that outlives a request? None, this only deletes rows.
  • Second replica: the delete is one transaction; pg_notify fires inside it on commit, and every replica's own LISTEN connection fans it out to its own sockets.
  • Serialized: membership check and delete share a transaction. A second delete finds no membership, gets 404.
  • Fanned out to a browser: yes, deleted: true on the existing channel_activity topic.
  • New listener, port, schedule: none.

Boundary and audit

  • channel.deleted audit row on every delete, including a failed upstream thread delete.
  • agentId sent to Intelligence is derived server-side (channel:<id>), never accepted from the client.

Changelog

Added under Unreleased.

Proof

bun run format:check lint typecheck test all 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.

@CristianPeralta

Copy link
Copy Markdown
Contributor Author

Menu, confirm dialog, and the resulting audit row:

Delete menu

Confirm dialog

Audit row

CristianPeralta and others added 2 commits August 24, 2026 09:50
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.
@davidmckayv
davidmckayv force-pushed the remove-a-conversation-and-forget-its-thread branch from e84e439 to ec92a34 Compare August 24, 2026 17:02
davidmckayv
davidmckayv previously approved these changes Aug 24, 2026

@davidmckayv davidmckayv 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.

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.

@davidmckayv
davidmckayv merged commit 983c909 into CopilotKit:main Aug 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nothing removes a channel, so the roster only ever grows

2 participants