[Bug]: MCP t3_project_delete with force deletes the built-in "No project" scratch project and every thread in it, running and pinned ones included, with no confirmation or count#33766

opened9h agobyRriskneutral00

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Area

apps/server

Steps to reproduce

  1. Have the built-in "No project" scratch project hold many threads (here: 436 threads, 16 of them pinned and 285 not archived; 1,872 thread rows counting subagent threads). Some of them are running.
  2. From an agent thread with full access, call the MCP tool t3_project_delete with the scratch project's id and force: true. (In our case an agent was cleaning up after a test. It deleted the two projects the test had created, then also deleted the scratch project, because the test's threads had been launched with t3_thread_launch { scratch: true } and so lived there.)

Expected behavior

At least one of these:

  • The scratch project cannot be deleted through MCP (or at all). The server knows which project it is (ManagedProjectFolders.isScratchProject) and recreates it on demand, so deleting it has no use beyond deleting every thread in it.
  • A forced delete through MCP tells the caller how many threads (pinned, running, archived) it will delete and needs a second call that confirms that count, the way the web UI shows a destructive confirmation before it sends force: true.
  • A call does not delete the calling thread, or the thread that orchestrates it, without saying so.

Actual behavior

One call deleted the project and every thread in it, with no confirmation and no count:

  • orchestration_events holds one project.deleted event for the scratch project (command mcp:<uuid>), and 1,872 thread.deleted events whose command ids are mcp:<uuid>:delete-thread:<threadId>, all from that single call, over about 63 seconds. That is every thread the project held: 436 threads (16 pinned) and their subagent threads.
  • Three running delegated-task runs were cancelled (run.updated → cancelled). Pinned threads were deleted the same as any other.
  • The tool returned success. The caller had no step at which it saw what would go.
  • A new "No project" project was created about 5 minutes later the next time a scratch thread was launched, so the old threads are no longer in any live project.

Where it comes from (main @ e4d86abca9c0, same code in the 0.1.7 build):

  • apps/server/src/mcp/toolkits/project/tools.ts:98-100: description "Delete one project. Nonempty needs force: true. Folder on disk stays." It does not say that force deletes every thread.
  • apps/server/src/mcp/toolkits/project/handlers.ts:367: the handler passes the call straight to mutateProjectWithClone. McpToolAccess.writesEnvironment checks only full access and linked callers, not whether the target is the scratch project or holds the caller.
  • apps/server/src/project/ProjectService.ts:472-501 (deleteChildThreads): with force: true, deletes [...snapshot.threads, ...snapshot.archivedThreads] one by one. There is no check for pinned or running threads, and no check against isScratchProject (apps/server/src/project/ManagedProjectFolders.ts:307).
  • ProjectNotEmptyError says only "Project is not empty." with no count, so an agent that hits it retries with force: true without learning what is at stake.
  • There is no thread.restore / project.restore command (only thread.unarchive), so there is no supported way back. The rows survive only because of the soft delete in #17845. #32378 would purge deleted threads' projection rows after the 1-day retention window, which would make an accident like this permanent after one day.

Related but not duplicates: #17845 (deleted threads keep their rows), #32378 (purge them), #2866 / #4632 (deleting a project fails), #3855 (project remove --force in the CLI).

Impact

Major degradation or frequent failure

All conversation history of the default project disappeared from the app in one tool call, including pinned threads and work that was running. Any agent with full access can do this, and agents launched with scratch: true are told that is their own space.

Version or commit

0.1.7 (commit 322f6544dfad); code unchanged on main @ e4d86abca9c0

Environment

macOS 27, T3 Code desktop 0.1.7, Orchestrator V2, Claude Code provider calling the built-in T3 MCP server

Logs or stack traces

shell
1-- read-only, statev2.sqlite
2SELECT count(*) FROM orchestration_events
3 WHERE event_type = 'thread.deleted'
4 AND command_id LIKE 'mcp:<uuid>:delete-thread:%'; -- 1872
5SELECT json_extract(payload_json,'$.status'), count(*) FROM orchestration_events
6 WHERE event_type = 'run.updated' AND command_id LIKE 'mcp:<uuid>:%'
7 GROUP BY 1; -- cancelled 3, ...
8SELECT title, deleted_at FROM projection_projects
9 WHERE workspace_root = '<base>/scratch'; -- two "No project" rows, the old one deleted

Workaround

None in the product. The thread rows still exist with deleted_at set, so they can only be recovered by editing the database by hand, and only before a purge like #32378 runs. Until there is a guard, agents should be told never to call t3_project_delete on the scratch project and to delete test threads one by one.

Synced from GitHub

One more effect that makes this worse: deleting a thread also queues attachment.cleanup for every attachment it has (apps/server/src/orchestration-v2/ThreadDeletion.ts:229-235). The thread rows are only soft-deleted, but the attachment files are removed. After the forced project delete above, none of the 108 attachment files on the deleted threads were left on disk. Attachments on live threads from the same hours were still there. Even a hand-restored thread comes back without its images and files. Related: #30017.

Synced from GitHub

Correction to the attachment count above: the 108 files were removed from T3's attachments folder when the threads were deleted, as described. In the manual restore that followed, 86 of the 108 were recovered and 22 were not; those 22 are lost. T3 has no way to restore them.

Synced from GitHub

Proposed fix: #33790. Agents can no longer use t3_project_delete to delete a project that has threads; the refusal names the thread count and points to the app's own Remove project confirmation. "No project" is refused outright. The PR has before-and-after results from a sandboxed dev server, and I'm happy to reshape it, for example to keep force and only guard "No project".