If you delete a browser profile while it is running in Mosaic, its browser closes first and then the profile goes to the trash. If the browser refuses to close, the profile is not trashed at all: it stays on the list, where it still has a Stop button. Until we fixed it, deleting a project closed nothing, and the other five ways in treated a browser that would not close as a success, so a window could stay open behind a row that had already disappeared.

The bug, as it was reported

The report was short and angry: moving a profile to the trash left its browser open. It was correct, and worse than it sounds. The row vanished, so the window still on screen belonged to a profile the interface said did not exist. That window kept its proxy session. It kept writing into a cookie jar nothing would read again. And there was no row left anywhere with a Stop button on it.

A profile is a fingerprint, a proxy and a cookie jar that agree with each other. A browser that outlives its row is still all three, still online, and now unmanaged. For anyone keeping accounts apart, that is the last thing you want running unnoticed.

Six doors, three behaviours

Mosaic can be driven from its window, from the local REST API and over MCP, and all three can trash a profile. When we listed every path, there were six, and they did three different things:

DoorWhat it did before
IPC profiles.deletestopped the browser, swallowed a failure
IPC profiles.bulkDeletestopped the browser, swallowed a failure
REST DELETE /profiles/:idstopped the browser, swallowed a failure
REST POST /profiles/bulk/deletestopped the browser, swallowed a failure
MCP delete_profilestopped the browser, swallowed a failure
Delete project (IPC, REST, MCP)did not stop anything

The project doors were plainly broken. store.deleteProject trashes every profile in the project, one level below where anyone had looked for a running browser.

The other five were broken more quietly. Each one called the stop like this:

await launcher.stop(id).catch(() => undefined)

That line turns "this browser will not close" into a clean success. The caller got a 200, the row went away, and the window stayed.

Your profiles, cookies and proxies stay on your computerIn Mosaic: Local first. Profiles, cookies and proxies stay on your computer. Free holds 3 profiles. Pro is $49 a month for any number.

How it works: Local first

One implementation, and a failure that costs the trash

The fix has two halves.

First, there is now exactly one piece of code allowed to trash a profile: src/main/engine/trash.ts. Every door calls it, the project doors included. It is the only caller of store.trashProfile, deleteProfile and deleteProject.

Second, the failure mode is inverted. If a stop fails, the trash is what gets refused, not the browser. The profile stays on the list, still described, still stoppable. Refusing is something you can recover from; a hidden browser is not. Every caller gets back { trashed, stopped, failures }, with profile names in it, never a bare { ok: true }.

We measured it twice on our own machine. A profile launched through the app ran as pid 77661. profiles.bulkDelete answered "Moved 1 profile to the trash, closing 1 browser." and ps -p 77661 found nothing. Then a project holding a running profile, pid 78268: projects.delete returned { ok: true, stopped: 1 }, and that process was gone too. Before the change, the second one left the browser running.

The Mosaic trash with two deleted profiles, each with Restore and Delete
The trash keeps profiles for 30 days by default

The check found a door the fix missed

We wrote a build check for this rule, check:trash-stops. It fails the build if anything outside trash.ts calls the store's trash methods directly. Its first run named two places the fix had not reached.

One was POST /profiles/bulk/delete, a real sixth door we had not counted when we started. The other was launcher.retireIfOneTime, which retires a one-time profile from inside the cleanup that runs after its browser has already exited. That one is legitimate: sending it through the shared code would call stop from inside stop. So it carries a trash-ok: marker with its reason, on that line only. A whole-file exemption would have let a second direct call slip in beside it unnoticed.

Deleting a running profile from the window now

The confirmation counts the open browsers in your selection and says so before you agree: "3 of them have a browser open, and those are closed first". The row menu also stopped greying out Move to trash on a running profile. Two doors were giving two answers to one question, and the honest answer is yes, and the browser closes.

This matters more once an assistant is driving profiles for you, as in an AI assistant that runs your profiles: a model that deletes a profile cannot see a window left open on the screen. Everything here stays on your computer (what is kept locally has the list), and the reasons a stray session links accounts are in how to manage multiple accounts without getting linked.