Skip to content

fix(channels): keep the backend task channel in sync on rename and delete - #3228

Closed
adamleithp wants to merge 2 commits into
mainfrom
posthog-code/channel-rename-delete-task-sync
Closed

fix(channels): keep the backend task channel in sync on rename and delete#3228
adamleithp wants to merge 2 commits into
mainfrom
posthog-code/channel-rename-delete-task-sync

Conversation

@adamleithp

Copy link
Copy Markdown
Contributor

Problem

Channels exist in two places: the desktop file-system folder (the sidebar entry) and a backend task channel that owns the task feed and thread messages — and the UI maps one onto the other by name. That made two channel actions lossy:

  • Rename only moved the folder. The name no longer matched any backend channel, so the feed silently auto-provisioned a brand-new empty channel, orphaning every existing task and message under the old name.
  • Delete only removed the folder, leaving the backend channel live but unreachable with all its tasks stranded on it.

Changes

  • Rename now moves the data-bearing backend channel by id in lockstep with the folder: backend first (if it fails, nothing changes), and the backend name is reverted if the folder rename then fails. Server-side name errors (invalid/taken) surface in the error toast.
  • Delete now soft-deletes the backend channel's tasks and the channel itself before removing the folder — both are soft deletes server-side, so nothing is lost and everything is recoverable. Individual task deletes are best-effort so one flaky request can't block the delete; a backend failure aborts before the folder is touched.
  • Added renameTaskChannel / deleteTaskChannel to the api-client (the backend endpoints already existed; the client never called them).
  • Updated the delete confirmation copy, which previously claimed tasks were not deleted.

How did you test this?

  • 8 new unit tests in useChannels.test.tsx covering the rename lockstep/rollback paths and the delete ordering/failure paths; full @posthog/ui suite passes.
  • turbo typecheck for @posthog/api-client + @posthog/ui, Biome clean.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

@trunk-io

trunk-io Bot commented Jul 7, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit a7483f1.

@adamleithp
adamleithp requested a review from k11kirky July 7, 2026 20:04
@adamleithp
adamleithp marked this pull request as ready for review July 7, 2026 20:04
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(channels): keep backend task channel..." | Re-trigger Greptile

Comment thread packages/ui/src/features/canvas/hooks/useChannels.test.tsx Outdated
Comment thread packages/ui/src/features/canvas/hooks/useChannels.ts
adamleithp and others added 2 commits July 8, 2026 13:47
Renaming a channel folder left the backend task channel behind (it is resolved by name), so the feed re-provisioned an empty channel and existing tasks/messages were orphaned. Rename now moves the backend channel by id in lockstep with the folder, with rollback if the folder rename fails.

Deleting a channel now soft-deletes the backend channel's tasks and the channel itself (both soft deletes server-side, so data is recoverable) before removing the folder, instead of leaving them orphaned on an unreachable channel.

Generated-By: PostHog Code
Task-Id: 3ac7dc89-890b-4013-bf1f-5dc40476f093
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@adamleithp
adamleithp force-pushed the posthog-code/channel-rename-delete-task-sync branch from 9d724ca to a7483f1 Compare July 8, 2026 12:48
// backend name back so the two sides stay in sync.
if (backendChannel) {
await client
.renameTaskChannel(backendChannel.id, from)

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.

Once the folder rename fails and this compensating revert is attempted, its own failure is discarded here (.catch(() => undefined)). If the revert also fails (e.g. a concurrent rename already took from, or a transient error), the backend channel is left permanently stuck on to while the folder still shows from — with no log or user-visible signal beyond the generic "Couldn't rename channel" toast. Worth at least logging via ROOT_LOGGER so the desync is diagnosable.

await client.deleteTaskChannel(backendChannel.id);
}
}
return client.deleteDesktopFileSystem(id);

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.

This delete path soft-deletes the backend channel and its tasks unconditionally before an unguarded deleteDesktopFileSystem(id) call — unlike renameMutation below, there's no rollback if this final call fails. If it fails after the backend soft-delete already succeeded, the folder stays in place but its backend channel is now soft-deleted and absent from getTaskChannels(). Reopening that folder later will make useBackendChannel silently provision a brand-new empty channel, permanently orphaning the original tasks with no error surfaced about that specific consequence.

const deleteMutation = useMutation({
mutationFn: async (id: string) => {
mutationFn: async ({ id, name }: { id: string; name: string }) => {
if (!client) throw new Error("Not authenticated");

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.

Both deleteMutation and renameMutation implement multi-step, cross-backend orchestration inline in a useMutation — list backend channels, find a match by normalized name, conditionally mutate, and (for rename) manually compensate on failure. Per this repo's CLAUDE.md, that's business orchestration ("orchestration, retries, dedupe, sagas... Inversify services only" is packages/core's job), and Rule 5 says hooks should wrap exactly one mutation, with multi-source orchestration belonging in a service method. The repo already has a Saga abstraction (packages/shared/src/saga.ts) used by other packages/core sagas, and a sibling ChannelTasksService in packages/core/src/canvas/ — this logic seems like a natural fit there instead, and would be reusable/testable outside a React hook.

// than 500 tasks soft-deletes only the first page here. The channel
// itself is still soft-deleted below, so any stragglers stay attached
// to it and remain recoverable — nothing is lost, just left behind.
const tasks = await client.getTasks({ channel: backendChannel.id });

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.

This fires one concurrent HTTP DELETE per task with no batching — for a channel near the 500-task getTasks cap, that's up to 500 simultaneous requests. Promise.allSettled handles individual failures fine, but there's no cap on concurrency, which could trigger backend rate-limiting during the burst on a large channel. (Not a new pattern in this codebase, but worth a concurrency limit here given the potential scale.)

// local has changed.
const normalized = normalizeChannelName(name);
if (normalized !== PERSONAL_CHANNEL_NAME) {
const backendChannels = await client.getTaskChannels();

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.

This "find the public backend channel by normalized name" lookup is duplicated here and in renameMutation below, and a very similar lookup already exists in useBackendChannel (useTaskChannels.ts). Worth extracting to one shared helper (e.g. findPublicBackendChannel(channels, normalizedName)) so the matching rule only needs to change in one place.

@adamleithp adamleithp closed this Jul 27, 2026
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.

2 participants