You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two clients sharing a data directory never run the same Mini App at once. The first one records
ownership; the second reports that another client is running it and refuses to start it.
Scope note
Reporting the refusal in the listing and in the picker moved to #256 with the surface that shows it: the
WebUI's Mini App service is not enabled yet, so a presentation built here would be unreachable, and ADR 0020
requires the service cell, the presenter and the view to land as one change. The refusal itself — the part
that prevents a second process over one plugin's data — is delivered here, and it reports the holding
client and its pid so #256 has something to render.
Acceptance criteria
A second client does not spawn a Mini App whose ownership record is live.
A record left behind by a crashed owner is reclaimable, so a dead owner cannot block the app
forever.
A Mini App already running outside this client's record — including one the desktop client is
running — is detected through its derived port answering the way the guard answers, and is
refused with the same message.
Two different Mini Apps may be owned by two different clients at the same time; ownership is per
Mini App, not global.
The refusal happens before spawn, so a race cannot produce two processes over one plugin's data.
All four remaining criteria are proven and implemented; the fifth moved to #256 with your agreement
because the surface that would show it does not exist yet.
1. A second client does not spawn a Mini App whose ownership record is live
prepareRuntime takes ownership before the port loop and before any spawn
(node-runtime.ts, acquireMiniAppOwner). A held endpoint throws MiniAppNodeRuntimeError('OWNED_ELSEWHERE', 'Mini App <id> is already running in <client> (pid <pid>)').
Proven through the scripted-child harness
(packages/local-runtime-v2/test/unit/miniapp/node-runtime-credential.test.ts): refuses a Mini App another client owns, without spawning a process — the second preparation rejects
with OWNED_ELSEWHERE and the second child receives no init message at all.
2. A record left behind by a crashed owner is reclaimable
The proof is an exclusively bound endpoint per Mini App: a socket file on POSIX, a named pipe on
Windows, named from the plugin id. Binding is the ownership; the OS releases it when the owner dies, so
"record present, nobody listening" is stale by definition rather than by timestamp.
The crash path is the interesting one: a dead owner leaves the socket file behind, so a failed bind is
distinguished from a leftover by connecting. reclaims an endpoint left behind by an owner that died
creates a real socket, closes the server without unlinking, and the acquisition succeeds.
ADR 0020's ownership section now records this mechanism, including why this code does not copy the
OAuth lease server's rm(endpoint) before binding: that would steal a live owner's endpoint.
3. A Mini App already running outside the record is detected through its derived port
probeMiniAppPortOwnership answers free | guarded | foreign — the guard's answer, not a bare connect: free for a closed port, guarded when the port answers 401 to an anonymous health request, foreign
when something holds the port and does not answer like the guard, so a stray listener is never mistaken
for a Mini App. Three cases in loopback-guard.test.ts run against a closed port, the guarded fixture,
and a plain node:http server in a child process.
4. Two different Mini Apps may be owned by two different clients at once
Ownership is per plugin: lets two different Mini Apps be owned at the same time, and the endpoint is
named from the plugin data directory rather than a global path.
Moved to #256: reporting the refusal in the listing and the picker
The ticket body now says so. Reason: the WebUI's Mini App service is not enabled yet
(wiring.miniAppService), and ADR 0020 requires the cell, the presenter and the view to land as one
change. A listing presentation built now would be unreachable code. What #256 receives is the readable
refusal — client and pid — which this branch already produces.
Not run: pnpm verify in full; browser and release-package steps are left to CI. No PR was opened for
this branch — it rides behind #262 and the ownership work continues into #255's consumer.
Untested boundary
The desktop client writes no ownership record, so its presence is detected only by its port answering
like the guard. That is the whole of what criterion 3 can promise, and it is stated in ADR 0020.
Activity
feat(webui): take Mini App ownership through an exclusively bound end…
feat(webui): refuse a Mini App another client owns before spawning it
Fectivnfy112357 commentedon Oct 26, 2026
All four remaining criteria are proven and implemented; the fifth moved to #256 with your agreement
because the surface that would show it does not exist yet.
Branch
feat/miniapp-ownership-probe, commits85c94c49→eb617131→e3cc29e9, stacked on #262.1. A second client does not spawn a Mini App whose ownership record is live
prepareRuntimetakes ownership before the port loop and before any spawn(
node-runtime.ts,acquireMiniAppOwner). A held endpoint throwsMiniAppNodeRuntimeError('OWNED_ELSEWHERE', 'Mini App <id> is already running in <client> (pid <pid>)').Proven through the scripted-child harness
(
packages/local-runtime-v2/test/unit/miniapp/node-runtime-credential.test.ts):refuses a Mini App another client owns, without spawning a process— the second preparation rejectswith
OWNED_ELSEWHEREand the second child receives no init message at all.2. A record left behind by a crashed owner is reclaimable
The proof is an exclusively bound endpoint per Mini App: a socket file on POSIX, a named pipe on
Windows, named from the plugin id. Binding is the ownership; the OS releases it when the owner dies, so
"record present, nobody listening" is stale by definition rather than by timestamp.
The crash path is the interesting one: a dead owner leaves the socket file behind, so a failed bind is
distinguished from a leftover by connecting.
reclaims an endpoint left behind by an owner that diedcreates a real socket, closes the server without unlinking, and the acquisition succeeds.
ADR 0020's ownership section now records this mechanism, including why this code does not copy the
OAuth lease server's
rm(endpoint)before binding: that would steal a live owner's endpoint.3. A Mini App already running outside the record is detected through its derived port
probeMiniAppPortOwnershipanswersfree | guarded | foreign— the guard's answer, not a bare connect:freefor a closed port,guardedwhen the port answers 401 to an anonymous health request,foreignwhen something holds the port and does not answer like the guard, so a stray listener is never mistaken
for a Mini App. Three cases in
loopback-guard.test.tsrun against a closed port, the guarded fixture,and a plain
node:httpserver in a child process.4. Two different Mini Apps may be owned by two different clients at once
Ownership is per plugin:
lets two different Mini Apps be owned at the same time, and the endpoint isnamed from the plugin data directory rather than a global path.
Moved to #256: reporting the refusal in the listing and the picker
The ticket body now says so. Reason: the WebUI's Mini App service is not enabled yet
(
wiring.miniAppService), and ADR 0020 requires the cell, the presenter and the view to land as onechange. A listing presentation built now would be unreachable code. What #256 receives is the readable
refusal — client and pid — which this branch already produces.
Gates at
e3cc29e9Not run:
pnpm verifyin full; browser and release-package steps are left to CI. No PR was opened forthis branch — it rides behind #262 and the ownership work continues into #255's consumer.
Untested boundary
The desktop client writes no ownership record, so its presence is detected only by its port answering
like the guard. That is the whole of what criterion 3 can promise, and it is stated in ADR 0020.
feat(webui): probe a derived Mini App port before spawning
feat(webui): take Mini App ownership through an exclusively bound end…
feat(webui): refuse a Mini App another client owns before spawning it