Skip to content

VC-822: Detach a removed project instead of deleting its tickets and Sessions - #937

Merged
hussainph merged 5 commits into
mainfrom
volli/VC-822-removing-a-project-hard-deletes-all-its-tickets
Oct 19, 2026
Merged

hussainph merged 5 commits into
mainfrom
volli/VC-822-removing-a-project-hard-deletes-all-its-tickets

Conversation

@hussainph

@hussainph hussainph commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Stacked on #920 (VC-796, head 7b90883c9): that PR's host-owned project.remove is where this change lives, so its commits are in this branch (fast-forward, no rewrite). Review the last three commits (a850ed9e9, 3540ae7fc, 2caa84b34) for VC-822. Once #920 merges, the diff against main is those commits alone.

Problem

Removing a project ran DELETE FROM projects WHERE id = ?. The foreign-key cascades then deleted:

  • every live and archived ticket, with its labels, events, comments, signals and blob links;
  • every Session, with its whole ledger;
  • the project's Automations, mcp_servers and workspace_epochs.

The confirm dialog only said "The folder on disk is untouched." Nothing could undo the removal. Re-adding the same folder created a new project with next_ticket_number = 1, while the old volli/<PREFIX>-N-* branches were still in the repository. The cascaded MCP servers' credentials stayed in the credential store, and no screen showed them.

Fix

The owner chose detach. Removal now stamps a new nullable projects.removed_at column (migration 064, expand-only, probe-gated like 053 and 063) instead of deleting the row. The cascades themselves are unchanged; removal no longer triggers them.

removeProject (packages/host-core/src/project-remove.ts) keeps #920's order: stop the live work, then detach (where #920 deleted), then forget the MCP credentials, end the feed and announce.

Hidden everywhere a project is listed. listProjects and countProjects skip detached rows, and every listing reads through them: the rail and bootstrap, workspace lists (desktop and hostd), CLI doors, the board feed, boot recovery, and worktree allow-lists and sweeps. buildBootstrapPayload also drops a detached project's tickets and labels. getProjectById and findProjectByPath still return a detached row, because looking a project up by id or path is not listing it.

Re-adding the same folder re-attaches it (project-create.ts). The same row comes back with its id, name, base branch, ticket counter, tickets, Sessions and Automations, at the end of the rail. No base-branch detection runs. The re-attach is announced through onCreated, so every window re-reads the board; desktop's volli:project-create now broadcasts data-changed { projectId }. volli project add --dry-run previews the re-attach.

Nothing reads, writes or starts work in a detached project:

  • SessionLocationResolver.prepare and reaffirm refuse with "This Session's project was removed from Volli. Add its folder again to use it." That covers new bindings, scheduled resumes, delegates, and turns on a binding that outlived the removal. resolve still answers, since reading where a Session ran starts nothing.
  • Automations: findProject now means "on the rail", so edits are refused. A Run (project or ticket scope) is refused as the new PROJECT_REMOVED. A due schedule is recorded as "Skipped — The project was removed from Volli." in the project's own history.
  • Board: board.snapshot and board.roster answer "Unknown project". Every write through recorded is refused with "This project was removed from Volli. Add its folder again to change it." A write the board already took still replays.
  • Terminals: the PTY scope refuses a ticket or Board terminal with "This project was removed from Volli. Add its folder again to use it." An unknown project id still spawns, as before.
  • hostd: servedWorkspace answers null, which closes every Workspace door for a removed project.
  • An armed column's countdown is dropped: readTicket reads the ticket as gone.

MCP credentials: forgetRemovedServers drops the credential of any server whose row is gone or whose project is detached. The server rows stay for a re-add. The servers' in-flight sign-ins were already cancelled by #920's disconnectProject.

Dialog text is now: "The folder and its tickets and Sessions are kept. Add the folder again to bring them back."

Export carries removedAt.

Decisions

  • Detach, not delete (the owner's decision). Transcript artifacts and the ticket-number counter no longer need reclaiming: nothing is deleted, and a re-add keeps the counter.
  • Credentials are forgotten on detach, as the ticket's acceptance asks. No screen shows a removed project's servers, so nobody would revoke those credentials. The cost: a re-attached project's MCP servers ask to sign in again, or for their secrets again.
  • A removed project keeps its ticket prefix for its return. This supersedes the first revision's "prefix uniqueness among projects on the rail".
    • Prefixes cannot be changed. So a new folder that took a removed project's prefix would leave the old folder impossible to add again.
    • A new project is therefore checked against removed projects too, and refused with: "Ticket prefix "OL" is used by Old, which was removed. Add its folder again first."
    • The re-attach refusal ("Ticket prefix "X" is already used by Y.") stays as a guard. Only an older build, which does not see the removal, can now cause that collision.
    • The owner can reverse this.
  • Automation schedules keep being evaluated while the project is detached. Each due run is refused as PROJECT_REMOVED and recorded as a skip in the project's own Run history. I did not filter the scheduler: a filtered schedule stays unevaluated after a re-add until something refreshes the scheduler, and its missed occurrences would later be reported as "not observed". Consecutive refusals are not coalesced.
  • The worktrees of a removed project are untouched. Its container leaves ownedContainers, the allow-list every destructive worktree path checks, so no sweep, trim or orphan delete reaches its checkouts.
  • N−1 degradation: an older build does not read removed_at, so it shows a detached project on its rail as an ordinary one. Nothing is lost. If a new project took a removed project's prefix, that older build shows two projects with that prefix. The min-reader floor is unchanged (58).
  • removeProject on an id that is not on the rail (unknown or already removed) is a no-op.
  • deleteProject stays as it was. No product door calls it any more; its cascade test still documents the schema.

Not in this PR

  • A way to remove a project's history permanently. There is none; removal only detaches.
  • ticket.move (executeTicketMove) does not pass through the board's recorded gate. No door can name a removed project's ticket: CLI resolution sees only projects on the rail, and the renderer has forgotten the project.

Open question

  • With cloud on, the renderer follows the board feed (boardProtocol() in lib/boot.ts). Removal disposes the project's feed. On a re-add, seedProject opens the protocol sync again, and board.snapshot answers again because the project is back on the rail. I have not verified in a live app that the re-opened feed carries later changes for that Workspace. Flag-off, which every stable build runs, reads the board through bootstrap and roster, and that path is covered by the data-ipc test.

Review (READY WITH NITS at 3540ae7fc)

  • SHOULD 1, prefix dead end: done as the default. See Decisions; this supersedes the earlier prefix decision.
  • SHOULD 2, doors that act by id:
    • done: hostd servedWorkspace, the PTY scope (ticket and Board terminals) and the board commands (snapshot, roster, and every write through recorded);
    • not done: ticket.move (see above).
  • SHOULD 3, run refusal: done. New PROJECT_REMOVED code (shared AutomationRunRefusalCode, not in the protocol schema), and the sentence "The project was removed from Volli." It is refused before the ledger accepts the Run, so no durable command stores the code. Coalescing consecutive refusals: not done.
  • Nits:
    • findProjectByPath now documents its exact-string match: a different spelling of the same folder registers a second project, as it always has.
    • The dry-run says the project comes back under its old name.
    • New tests for ownedContainers leaving out a removed project's container, and for a removed project's due schedule end to end over the real Run door.
    • The dialog copy is shortened.
    • The cloud-on question is recorded above.
  • Relink onto a removed project's folder: fixed in this PR. It is refused (claimed) with "This folder belongs to Old, which was removed. Add it again instead." instead of failing on the UNIQUE constraint.

Tests

Red first: with #920's project-remove.ts (delete) restored over this branch, project-remove.test.ts failed 5 of 12, including "keeps the project's tickets and Sessions, and only hides it from the rail" (row undefined, cascade). It passes with the change.

Run locally, one file at a time with --maxWorkers=1, all passing:

  • host-core:
    • project-remove: detach order step by step with what the database held, history kept, wall-clock default, no-op for a removed id, and end to end over hostProjectLiveWork (no binding left, Sessions kept).
    • project-create: re-attach keeps id, counter, tickets and Sessions with no detection and one announcement; preview; a concurrent double add; refused on a taken prefix or a missing folder.
    • db/projects-repo: detach and reattach semantics, listings, count scopes, id and path lookups.
    • mcp/settings: credentials forgotten for a detached project's servers and for a missing row, another project's kept, server row kept.
    • session-runtime/location: prepare and reaffirm refused while detached, resolve answers, both allowed again after a re-attach.
    • automation-services: findProject and the armed readTicket respect detach.
    • operator-door: volli project add previews and re-attaches.
    • handlers/host-handlers: removed_at stamped at the host clock.
    • db/export, db/migrations-lock, db/migrations.
    • db/migrations-authority-collision: CI caught this one. The 053 collision repair is now judged at 054, where it ends; the head check expects only 064's added NULL column.
    • Review round:
      • board/commands, pty/scope, automations/run and project-relink;
      • worktree/containers and automation-services.headless (the real scheduler records PROJECT_REMOVED);
      • project-create: a new project is refused a removed project's prefix;
      • db/projects-repo, automation-services and operator-door.
  • hostd host-protocol: servedWorkspace answers only while the project is on the rail.
  • desktop main/data-ipc (217): removal hides the project from bootstrap; re-adding the folder brings back the project and its ticket and broadcasts { projectId }.

Other checks:

  • vp check on every touched file: pass.
  • pnpm check:protocol-schema --base origin/main: pass.
  • node scripts/lock-migrations.mjs: current. Migration 064 is not claimed by any open PR.

Not run locally (the machine is shared): package typecheck, coverage, smokes. CI is the proof. CI gate was green on 3540ae7fc with no reruns. The review round's head is 2caa84b34.

CI rerun (disclosed)

Run 39253523302 on 2caa84b34 failed two jobs outside this diff. With approval, the failed jobs were rerun once:

  • Smoke (shard 1/3): automations-picker-smoke timed out waiting for "Move only" to light up during the drag, at check 6 and then check 8 on its retry. This is the known flake VC-844.
  • Test (packages): packages/agent-runtime/src/pi/models.test.ts > PiFileCredentialStore caching > does not join a shared pass that a write has already overtaken hit "Test timed out in 5000ms" (job 118062947625). This is a new flake, to be filed. The PR does not touch agent-runtime, and the same job passed on 3540ae7fc.

On the one rerun, both jobs passed, including that test. CI gate is green on 2caa84b34.

Overlap

git merge-tree is clean against origin/main and the heads of #909, #919, #920, #921 and #922, plus #633, #928 and #933. #651 and #665 already conflict with main on the same files without this change.

Removing a project ran DELETE FROM projects, and the foreign-key cascades
took every live and archived ticket, comment, Automation and Session with
its whole ledger, behind a dialog that said only that the folder on disk
was untouched.

Removal now detaches the project (migration 064, expand-only, nullable
projects.removed_at). The host still stops the project's live work first
(VC-796), then stamps removed_at instead of deleting the row:

- every listing (rail, bootstrap, workspace lists, CLI doors, worktree
  allow-lists) hides a removed project and its tickets;
- adding the same folder again re-attaches the same row, with its id,
  base branch, ticket counter, tickets and Sessions, at the end of the
  rail, and announces it so every window re-reads the board; a prefix a
  project on the rail took meanwhile refuses the re-attach;
- nothing starts work in a removed project: the Session location
  resolver refuses prepare and reaffirm, Automations refuse to run or be
  edited there, and an armed column's countdown is dropped;
- the credentials of its MCP servers are forgotten, since no Settings
  screen shows them any more; the server rows stay for a re-add;
- the confirm says the tickets and Sessions are kept and how to get them
  back; the export carries removedAt.
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: f255608e-56ab-4593-a680-3d62c77cb947
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…s column

The 053-collision suite compared every projects row and the table's DDL
after migrating to the head, so migration 064's removed_at column broke
its 'nothing changed' claim. The repair is now judged at 054, where it
ends, and the head check expects only the added NULL column.
… doors

- A new project may not take a removed project's ticket prefix: prefixes
  cannot change, so taking it would leave the old folder impossible to add
  again. The refusal names the removed project and the one fix.
- hostd serves no Workspace for a removed project (servedWorkspace), a
  terminal does not open in one (PTY scope), and its board takes no write
  and answers no snapshot or roster; a write it already took still replays.
- A Run in a removed project is refused as PROJECT_REMOVED, so a schedule's
  skip reads "Skipped — The project was removed from Volli." instead of
  "not found".
- Relinking a project onto a removed project's folder is refused with a
  sentence instead of failing on the path's UNIQUE constraint.
- Shorter confirm copy; the dry-run says the old name is kept; the exact
  path match of a re-add is documented; tests for a removed project's
  worktree container and for a removed project's due schedule end to end.
@hussainph
hussainph merged commit 2c926f1 into main Oct 19, 2026
23 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.

1 participant