Skip to content

feat: workspace owners can permanently delete a channel - #257

Open
sercada wants to merge 1 commit into
openclaw:mainfrom
sercada:feat/delete-channels
Open

sercada wants to merge 1 commit into
openclaw:mainfrom
sercada:feat/delete-channels

Conversation

@sercada

@sercada sercada commented Sep 13, 2026

Copy link
Copy Markdown
Contributor
Maintainer edits

MUST: Keep Allow edits from maintainers enabled for this PR so maintainers
can help update the branch when needed.

What Problem This Solves

Workspace owners can archive a channel but cannot remove a channel they no longer
want, together with its messages and files.

User Impact

User impact: owners can delete a channel from Channel settings → Delete
channel...
, from the API, or with clickclack channels delete. The dialog
shows what will be removed (messages, thread replies, pins, topics, files and
their size), offers to archive instead, and enables deletion only after the
channel name is typed. Deletion is permanent. People viewing the channel are
moved to their fallback conversation with a notice. The server refuses to
delete a workspace's last channel and the Guests workspace's provisioned
channels. On SQLite, two new migrations make deleting many messages fast: a
10k-message channel in a 120k-message workspace now deletes in 0.3 s instead of
6.6 minutes. Existing workspace deletion benefits the same way.

Why This Change Was Made

  • Deletion reuses the existing foreign-key cascade in one transaction, so the
    channel, messages, replies, reactions, pins, channel topics, read pointers,
    and notification settings go together. Uploads attached only to that channel
    are removed and their objects go through the durable cleanup queue already
    used for workspace deletion; uploads still used elsewhere and the workspace
    icon are kept. The audit log records the channel and the removed counts.
  • On SQLite, every row removed by the cascade did two full scans: one of
    messages to find replies and quotes (parent_message_id and
    quoted_message_id had no index), and one of the FTS5 search index, because
    the delete trigger matched the unindexed messages_fts.message_id. Deleting
    a channel therefore grew with channel size × workspace size. New partial
    indexes cover the child keys on both stores, and message_search_rows records
    each message's FTS rowid so the search triggers update rows by rowid. Both
    are needed; either alone leaves most of the cost.
  • A workspace-scoped channel.deleted event tells clients to leave the channel.
    Earlier events for the channel stay in the log, so replay cursors remain valid;
    realtime delivery already skips events whose channel no longer exists.
  • On PostgreSQL, a deletion locks the workspace row (FOR NO KEY UPDATE) before
    it counts or selects anything, in the same workspace-first order as workspace
    deletion and updates. Competing deletions cannot both pass the last-channel
    check, and the workspace icon cannot move onto an upload being removed; event
    appends and inserts, which take KEY SHARE, are not blocked. Candidate uploads
    are then locked FOR UPDATE and re-checked, so an attachment elsewhere either
    commits first (and the upload is kept) or waits and fails its foreign key.
  • Only human owners can delete. Moderators, members, and bot tokens get 403;
    blocked deletions return 409 with a blocker code the web app explains.
  • clickclack channels delete deletes only a channel named with --channel on
    its own command line (global or subcommand form). CLICKCLACK_CHANNEL and the
    saved default channel pick the channel for everyday commands, so they never
    choose what gets deleted. Without --yes the command prints the counts and
    exits.

The new migrations (sqlite/0043, sqlite/0044, postgres/0036) share numbers
with the agent question migrations in #258. Migrations apply by file name, so both
work together; whichever PR lands second can renumber to keep the sequence tidy.

Evidence

Settings danger zone Confirmation with preview Another member when it is deleted
settings confirm watcher

The CLI with a default channel in the environment, real server and CLI from
the same build:

Previous head `aa459dd5`: channels delete --yes deleted #launch from CLICKCLACK_CHANNEL
# previous-head-aa459dd5: workspace with #launch and #old-launch (2 messages); the shell exports CLICKCLACK_CHANNEL=launch
channels: launch old-launch
$ clickclack channels delete --yes
  deleted #launch: 0 messages, 0 thread replies, 0 files
  exit=0
channels: old-launch
$ clickclack channels delete --channel old-launch
  cannot delete #old-launch: 2 messages, 0 thread replies, 0 files (last_channel)
  exit=1
$ clickclack channels delete --channel old-launch --yes
  cannot delete #old-launch: 2 messages, 0 thread replies, 0 files (last_channel)
  exit=1
channels: old-launch
This head: the default channel is ignored; the named channel is deleted after the preview
# branch: workspace with #launch and #old-launch (2 messages); the shell exports CLICKCLACK_CHANNEL=launch
channels: launch old-launch
$ clickclack channels delete --yes
  usage: clickclack channels delete --channel CHANNEL --yes
  exit=1
channels: launch old-launch
$ clickclack channels delete --channel old-launch
  refusing to permanently delete #old-launch: 2 messages, 0 thread replies, 0 files without --yes
  exit=1
$ clickclack channels delete --channel old-launch --yes
  deleted #old-launch: 2 messages, 0 thread replies, 0 files
  exit=0
channels: launch

Tests:

  • Shared store suite on SQLite and Postgres (channeldeletiontest): the
    channel's messages, topics, search results, and exclusive upload are removed
    while other channels, shared uploads, and the workspace icon survive; the
    deletion counts match the preview; a replay cursor pointing at one of the
    channel's old events still replays forward to channel.deleted; guard rails
    for the last channel, provisioned Guests channels, archived channels, and
    non-owners.
  • TestCascadeChildKeysUseIndexes (SQLite query plan) and
    TestCascadeChildKeyIndexesExist (Postgres).
  • PostgreSQL interleavings, held open with real locks instead of timing:
    TestDeleteChannelSerializesLastChannelDecisions runs two deletions of a
    workspace's last two channels (one succeeds, one gets last_channel), and
    TestDeleteChannelKeepsUploadsAttachedDuringDeletion attaches the channel's
    only upload elsewhere while the deletion is paused after choosing it (the
    attachment either keeps the upload or fails). Both pass three times in a row;
    against the previous revision they fail with deleted=2 blocked=0 remaining=0
    and attachment succeeded but kept 0 rows.
  • TestSearchTriggersFindRowsThroughTheRecordedRowid: upgrades a database with
    an existing message, detaches the search rows from their message IDs, then
    edits one message and deletes the other's channel; both rows are still
    replaced or removed and search returns the edited message. With the old
    message_id triggers it fails with detached rows = 2.
  • TestChannelDeletionHTTP: 403 for members and bot tokens, 404 for
    unknown or already deleted channels, 204 for the owner, the live and
    replayed channel.deleted event on a member's socket, the audit entry, and
    the last_channel blocker with 409.
  • TestChannelsDeleteRequiresExplicitConfirmation: the CLI prints the preview
    and refuses without --yes.
  • TestChannelsDeleteIgnoresDefaultChannels runs the CLI entry point with
    CLICKCLACK_CHANNEL, then with a saved default channel: channels delete --yes
    sends no DELETE, while --channel in either flag position deletes. Without the
    fix it fails with CLICKCLACK_CHANNEL chose the channel to delete.
  • tests/e2e/channel-deletion.spec.ts: an owner deletes after reviewing the
    preview and typing the name; a member watching the channel is moved out with
    a notice; the last channel cannot be deleted.
  • apps/web/src/lib/channel-deletion.test.ts: name confirmation, blocker
    messages, and the notice text.
  • Local CI steps: pnpm fmt:check, pnpm lint, pnpm typecheck,
    pnpm -r typecheck, web unit tests, pnpm docs:site, and go test ./...
    with CLICKCLACK_POSTGRES_TEST_DSN, deadcode, and the embedded build is
    current and repeatable (coverage gate 86.8%). On this machine these tests also
    fail on unchanged main: TestHTTPBodyDeadlineStillBoundsStalledRequestBodies,
    uploadstore TestR2HeaderNetworkLifecycle/progressing_PUT, and intermittently
    TestHTTPErrorPathsAndSPA (1 of 4 runs on main), and
    TestHTTPSlashCommandRequiresChannelWriteAuthorityBeforeCallback (1 of 5 runs
    on main, 5 of 5 pass on this branch). The first CI run's Playwright job
    failed once in chat.spec.ts › clicking the active conversation does not refetch its messages; locally that test passes 3 of 3 alone and the whole
    chat.spec.ts plus channel-deletion.spec.ts pass 42 of 42 with two workers.

Deleting a channel on SQLite (Python sqlite3 3.45 applying the real
migrations; 120,000 messages in 12 channels; the deleted channel has 7,000
roots, 2,500 thread replies, and 500 quote replies):

Schema Delete time
main 395.4 s
child-key indexes only 220.3 s
this PR (indexes and search row map) 0.29 s

Applying this PR's migrations to that populated database took 0.14 s. A
smaller run (30,000 messages, 3,000 deleted) shows the two fixes are
independent: 27.7 s on main, 11.5 s with only the search row map, 17.7 s with
only the indexes, and 0.10 s with both.

AI-assisted: prepared with Claude Code; I reviewed the change and the evidence.

🤖 Generated with Claude Code

@sercada
sercada requested a review from a team as a code owner September 13, 2026 12:33
@clawsweeper

clawsweeper Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

ClawSweeper review complete

ClawSweeper finished reviewing this revision. The review result is being finalized.

View the workflow run.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. proof: sufficient Contributor real behavior proof is sufficient. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Sep 13, 2026
@clawsweeper

clawsweeper Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed September 13, 2026, 9:32 AM ET / 13:32 UTC (Revision 3).

ClawSweeper review

What this changes

Adds permanent owner-controlled channel deletion through the web app, API, SDK, and CLI, with confirmation, upload cleanup, viewer notifications, and database deletion optimizations.

Merge readiness

Ready for maintainer review

The feature remains absent from main and v0.5.0. The prior CLI and PostgreSQL findings are addressed, and the supplied behavior evidence supports this revision; no blocking correctness finding remains.

Priority: P2
Reviewed head: c6d1ef841098ed1874b460bde5e5ba62fe0568d1

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Useful, coherent implementation with real behavior evidence and the prior blocking findings resolved.
Proof confidence 🦞 diamond lobster (5/6) Sufficient (terminal): The real-server CLI transcript demonstrates rejection of ambient targets and successful explicit deletion; inspected screenshots show confirmation and viewer fallback, while populated SQLite migration measurements and backend regressions support database behavior.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (terminal): The real-server CLI transcript demonstrates rejection of ambient targets and successful explicit deletion; inspected screenshots show confirmation and viewer fallback, while populated SQLite migration measurements and backend regressions support database behavior.
Evidence reviewed 8 items Repository policy: Read the complete root AGENTS.md and checked the affected subtrees for nested policies; none were found. SQL query changes have corresponding sqlc-generated changes. No maintainer-notes directory was present.
Still necessary on main and release: The pinned main-to-head diff introduces the deletion routes and implementations. Main retains channel creation and updates without this deletion endpoint; the v0.5.0 server routes likewise lack it. The related questions PR addresses a distinct capability.
Prior CLI finding resolved: Deletion now requires ChannelFromFlag or a subcommand flag before resolving the target. The regression covers environment-only and saved-default rejection without DELETE, plus both supported explicit flag positions. The captured body includes real-server before/after output showing the ambient channel survives and the explicitly selected channel is deleted.
Findings None None.
Security None None.

How this fits together

ClickClack’s channel administration connects authenticated workspace users to SQL-backed conversations and uploaded files. Deletion removes channel-owned records transactionally, queues file cleanup, and informs connected clients through realtime events.

flowchart TD
  A[Web app or CLI] --> B[Authenticated deletion API]
  B --> C[Owner and channel safeguards]
  C --> D[Transactional database deletion]
  D --> E[Durable file cleanup]
  D --> F[Realtime deletion event]
  F --> G[Viewers leave deleted channel]
Loading

Before merge

None.

Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production and test delta Production +1344/-6; tests +1165/-0; generated +519/-27 Production growth supports the cross-client deletion flow and database safeguards; counts exclude documentation and separate generated assets.
Database migrations 3 migrations, including 1 SQLite backfill The persisted search-row mapping has populated-upgrade evidence and focused regression coverage.

Technical review

Best possible solution:

Keep permanent deletion explicitly targeted and owner-authorized, preserve shared files, and retain archive as the reversible alternative.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a feature request: main lacks permanent channel deletion. The previous CLI defect is addressed by source, regression coverage, and supplied before/after terminal output.

Is this the best way to solve the issue?

Yes. Reusing transactional cascades, existing authorization, and durable upload cleanup provides a coherent implementation without replacing archive or changing saved channel defaults.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning medium; reviewed against eeefa0412808.

Labels

Label changes:

  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The real-server CLI transcript demonstrates rejection of ambient targets and successful explicit deletion; inspected screenshots show confirmation and viewer fallback, while populated SQLite migration measurements and backend regressions support database behavior.
  • remove status: ⏳ waiting on author: Current PR status label is status: 👀 ready for maintainer look.
  • remove rating: 🦐 gold shrimp: Current PR rating is rating: 🐚 platinum hermit, so this older rating label is no longer current.
  • remove proof: 📸 screenshot: Current real behavior proof evidence kind is terminal.
  • remove merge-risk: 🚨 compatibility: Current PR review selected no merge-risk labels.

Label justifications:

  • P2: This is a bounded workspace-administration feature with explicit destructive-action safeguards, rather than an urgent existing-runtime regression.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The real-server CLI transcript demonstrates rejection of ambient targets and successful explicit deletion; inspected screenshots show confirmation and viewer fallback, while populated SQLite migration measurements and backend regressions support database behavior.
  • proof: sufficient: Contributor real behavior proof is sufficient. The real-server CLI transcript demonstrates rejection of ambient targets and successful explicit deletion; inspected screenshots show confirmation and viewer fallback, while populated SQLite migration measurements and backend regressions support database behavior.

Evidence

What I checked:

  • Repository policy: Read the complete root AGENTS.md and checked the affected subtrees for nested policies; none were found. SQL query changes have corresponding sqlc-generated changes. No maintainer-notes directory was present. (AGENTS.md:1, c6d1ef841098)
  • Still necessary on main and release: The pinned main-to-head diff introduces the deletion routes and implementations. Main retains channel creation and updates without this deletion endpoint; the v0.5.0 server routes likewise lack it. The related questions PR addresses a distinct capability. (apps/api/internal/httpapi/server.go:235, 648202b79b2f)
  • Prior CLI finding resolved: Deletion now requires ChannelFromFlag or a subcommand flag before resolving the target. The regression covers environment-only and saved-default rejection without DELETE, plus both supported explicit flag positions. The captured body includes real-server before/after output showing the ambient channel survives and the explicitly selected channel is deleted. (apps/api/cmd/clickclack/client_commands.go:173, c6d1ef841098)
  • Prior PostgreSQL findings resolved: Deletion locks the workspace before counting channels, then locks candidate uploads and re-reads their retention conditions. Focused database tests exercise competing last-channel deletions and attachment creation during deletion; the captured body reports repeated passing runs and failing results against the prior implementation. (apps/api/internal/store/postgres/channel_deletion.go:46, c6d1ef841098)
  • Authorization and final effects: The HTTP owner rejects bot tokens, and both stores check workspace ownership inside the deletion transaction before deleting records or creating cleanup jobs. PostgreSQL ownership transfer acquires membership FOR UPDATE locks, conflicting with the deletion check’s KEY SHARE lock; SQLite uses immediate transactions. HTTP coverage rejects members and bot tokens before a subsequent owner deletion removes the upload object. (apps/api/internal/httpapi/channels.go:160, c6d1ef841098)
  • Migration compatibility evidence: The SQLite migration backfills message-to-search-row mappings before replacing triggers. The upgrade regression creates a message before migration, then verifies editing, deletion, and search afterward. The captured body reports applying the real migrations to a populated 120,000-message SQLite database in 0.14 seconds and deleting the selected channel in 0.29 seconds. Both migrators track complete filenames, so the numeric overlap with feat: bots ask structured questions that people answer from a card #258 is not a migration-key collision. (apps/api/internal/store/sqlite/migrations/0044_message_search_rows.sql:5, c6d1ef841098)

Likely related people:

  • steipete: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)
  • sercada: Suggested for follow-up; no historical authorship or introduction is verified. (role: unverified routing candidate; confidence: low)

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (2 earlier review cycles)
  • reviewed 2026-09-13T12:40:18.152Z sha b6d6079 :: blocked before merge. :: [P1] [P1] Revalidate upload retention before deleting and queueing cleanup | [P2] [P2] Serialize the last-channel check across the workspace
  • reviewed 2026-09-13T13:02:10.522Z sha aa459dd :: blocked before merge. :: [P1] [P1] Require explicit CLI channel selection before permanent deletion

@sercada
sercada force-pushed the feat/delete-channels branch from b6d6079 to aa459dd Compare September 13, 2026 12:57
Owners delete a channel from Channel settings, the API, or the CLI after
reviewing what will be removed and typing the channel name. The channel's
messages, replies, reactions, pins, topics, read state, and exclusive uploads
go in one transaction; a workspace-scoped channel.deleted event moves viewers
out. The last channel and the Guests workspace's provisioned channels cannot
be deleted. The CLI deletes only a channel named with --channel on its command
line, never CLICKCLACK_CHANNEL or the saved default.

On SQLite, cascading many message deletions scanned both the messages table
and the FTS index once per row. Child-key indexes and a message-to-search-row
map make a 10k-message channel in a 120k-message workspace delete in 0.3 s
instead of 6.6 minutes, which also speeds up workspace deletion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sercada
sercada force-pushed the feat/delete-channels branch from aa459dd to c6d1ef8 Compare September 13, 2026 13:27
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. labels Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant