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.
[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
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
- 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.
- From an agent thread with full access, call the MCP tool
t3_project_deletewith the scratch project's id andforce: 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 witht3_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_eventsholds oneproject.deletedevent for the scratch project (commandmcp:<uuid>), and 1,872thread.deletedevents whose command ids aremcp:<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 needsforce: true. Folder on disk stays." It does not say thatforcedeletes every thread.apps/server/src/mcp/toolkits/project/handlers.ts:367: the handler passes the call straight tomutateProjectWithClone.McpToolAccess.writesEnvironmentchecks 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): withforce: true, deletes[...snapshot.threads, ...snapshot.archivedThreads]one by one. There is no check for pinned or running threads, and no check againstisScratchProject(apps/server/src/project/ManagedProjectFolders.ts:307).ProjectNotEmptyErrorsays only "Project is not empty." with no count, so an agent that hits it retries withforce: truewithout learning what is at stake.- There is no
thread.restore/project.restorecommand (onlythread.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
| 1 | -- read-only, statev2.sqlite |
| 2 | SELECT count(*) FROM orchestration_events |
| 3 | WHERE event_type = 'thread.deleted' |
| 4 | AND command_id LIKE 'mcp:<uuid>:delete-thread:%'; -- 1872 |
| 5 | SELECT 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, ... |
| 8 | SELECT 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.
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.
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".