Use case
KittyLitter (the mobile app) currently only works while the Local Studio desktop app is
running on the same machine. The Litter bridge gateway — which the phone connects to — is hosted
inside the agent-runtime (a Node/Hono HTTP service that also runs the pi coding agent and
serves /api/agent/*), and that agent-runtime is spawned as a child process of the Electron
desktop app. When the desktop app is closed, the gateway dies, and the phone loses its
connection.
Users want to keep using the mobile app from anywhere — including when the Mac (or whichever
machine runs the desktop app) is off. That means the gateway must be able to run independently of
the desktop application, hosted alongside the controller on a machine that stays online.
This is a parent (tracking) issue for the refactor that decouples the runtime layers so the
mobile app no longer depends on the desktop app process — while keeping the desktop app itself
able to reach the gateway and the controller as before.
Current coupling (why the mobile app depends on the desktop app)
The decoupling target is the agent-runtime ↔ Local Studio desktop app relationship. Today the
two are tightly coupled in several ways:
1. Process lifetime
frontend/desktop/logic/agent-runtime-server.ts forks the agent-runtime as a child of the
Electron main process (detached: false) and kills it on app exit (and on stale-launch
timeouts). frontend/desktop/project.mjs and frontend/desktop/logic/app-server.ts do the same.
The runtime only exists while the desktop app runs — which is exactly why the phone cannot work
when the Mac is off.
2. Filesystem/state
The desktop injects LOCAL_STUDIO_DATA_DIR (Electron's userDataDir) when it spawns the runtime.
The runtime reads and writes its durable state there: api-settings.json (controller
backendUrl/apiKey), litter-controller-id, litter-bridge.json (gateway secret + metadata),
projects.json, pi-agent/ models/sessions, connectors, goals, automations and the OAuth vault.
The controller connection and the gateway identity therefore live inside the Mac's user-data
directory.
3. Controller-URL coupling
The runtime resolves its model backend from settings.backendUrl
(services/agent-runtime/src/pi-runtime-models.ts#mergeControllers → providers
baseUrl: ${controller.url}/v1), and the gateway fetches controller /health, /status, /gpus
and /v1/metrics/vllm from the same setting. The runtime already reaches the configured
controller over HTTP — this part is already compatible with a remote controller.
4. Dual frontend consumption (the biggest gap)
The Next.js frontend consumes the runtime in two different ways:
- (a) Proxied over HTTP —
/api/agent/* routes pass through proxyToAgentRuntime, honoring
LOCAL_STUDIO_AGENT_RUNTIME_URL. These are already remote-capable.
- (b) Direct in-process imports — settings, connectors, plugins, projects, skills,
local-agents, and the /api/proxy/[...path] target import @local-studio/agent-runtime/*
modules inside the Next.js server process, sharing the same data dir. These are NOT proxied,
so a remote runtime leaves the desktop unable to manage settings/connectors/plugins/projects
until they are converted to HTTP calls.
5. Loopback chattiness
The desktop sets LOCAL_STUDIO_FRONTEND_BASE (the runtime calls the frontend for browser/pi
context and OAuth redirects) and LOCAL_STUDIO_AGENT_RUNTIME_URL (the frontend calls the runtime).
google-oauth-loopback.ts binds 127.0.0.1 for the OAuth callback. The two processes share a
data dir, a secret, and loopback URLs.
6. Pairing
frontend/desktop/logic/kittylitter-pairing.ts execs a local kittylitter binary
(kittylitter pair) which reads the runtime's published litter-bridge.json
(loopback http://127.0.0.1:<port>/api/litter-bridge/v1 + secret + controllerId) to produce the
QR payload the phone scans. Both the desktop IPC handler and the
/api/kittylitter/pairing route shell out to this local binary. This is the root of the
KittyLitter is unavailable (ENOENT) error and is inherently same-host.
7. Gateway reachability
server.ts binds hostname: "127.0.0.1" and the gateway metadata URL is hardcoded loopback. The
pairing schema already permits a relay field, but the gateway never sets it — nothing today makes
the gateway reachable from a phone off the local network.
Goal of the refactor: decouple the layers
The refactor should remove the desktop-app dependency from the mobile (KittyLitter) path, while
keeping the desktop app able to reach the gateway and controller as it does today. Concretely:
-
Promote the agent-runtime to a standalone, deployable service. It already is a discrete
Node/Hono process (services/agent-runtime/src/server.ts, dist/standalone.mjs) with a health
endpoint. Treat it as a first-class service that can run on a remote/headless host (systemd /
container / long-lived process), configured via environment (LOCAL_STUDIO_DATA_DIR,
BACKEND_URL, API_KEY, port/host bind) instead of Electron-main-injected values.
-
Decouple the mobile connection from the desktop process. The gateway must be able to run
remotely and stay alive independently of the desktop app. The phone connects to the gateway
wherever it lives; nothing in the phone path may require the desktop app to be running.
-
Move durable identity + controller settings to the runtime/controller side so a remote
runtime has its own stable controllerId, gateway secret and controller URL — out of the Mac's
user-data directory.
-
Close the in-process frontend gap. Convert the settings/connectors/plugins/projects/skills/
local-agents routes that currently import runtime modules in-process to proxy to the runtime
over HTTP, completing the "everything through LOCAL_STUDIO_AGENT_RUNTIME_URL" model. This keeps
the desktop app fully functional while the runtime is remote.
-
Make pairing remote-capable. Serve the pairing payload from an authenticated gateway/controller
endpoint (secret→token, controllerId→node_id, optional relay) so a phone can pair without a local
kittylitter binary reading a loopback file.
-
Make the gateway reachable off-Mac — configurable bind, TLS/public URL, or tunnel/relay — so
the phone can reach it over the network, not just 127.0.0.1.
Desired end state
- Mobile app (KittyLitter) depends on the gateway (wherever it is hosted), not on the
desktop app process. Works from the phone when the desktop app / Mac is off, as long as the
gateway host (e.g., the remote controller) is online.
- Desktop app can still reach the gateway and controller exactly as today (it depends on the
gateway for the mobile-bridge features), but the two layers are decoupled: the desktop no longer
owns the runtime's lifecycle, state or pairing.
Acceptance criteria
- Start the gateway/agent-runtime standalone on a remote host (no desktop app involved) and pair +
use KittyLitter from a phone against it.
- Close the desktop app on the machine the runtime is not on and confirm the phone connection
persists (gateway unaffected).
- Desktop app still fully functional against a remote runtime: settings, connectors, plugins,
projects, sessions, model picker, agent turns.
- Existing local-only flow (desktop + local runtime + local controller) keeps working unchanged.
Use case
KittyLitter (the mobile app) currently only works while the Local Studio desktop app is
running on the same machine. The Litter bridge gateway — which the phone connects to — is hosted
inside the agent-runtime (a Node/Hono HTTP service that also runs the pi coding agent and
serves
/api/agent/*), and that agent-runtime is spawned as a child process of the Electrondesktop app. When the desktop app is closed, the gateway dies, and the phone loses its
connection.
Users want to keep using the mobile app from anywhere — including when the Mac (or whichever
machine runs the desktop app) is off. That means the gateway must be able to run independently of
the desktop application, hosted alongside the controller on a machine that stays online.
This is a parent (tracking) issue for the refactor that decouples the runtime layers so the
mobile app no longer depends on the desktop app process — while keeping the desktop app itself
able to reach the gateway and the controller as before.
Current coupling (why the mobile app depends on the desktop app)
The decoupling target is the agent-runtime ↔ Local Studio desktop app relationship. Today the
two are tightly coupled in several ways:
1. Process lifetime
frontend/desktop/logic/agent-runtime-server.tsforks the agent-runtime as a child of theElectron main process (
detached: false) and kills it on app exit (and on stale-launchtimeouts).
frontend/desktop/project.mjsandfrontend/desktop/logic/app-server.tsdo the same.The runtime only exists while the desktop app runs — which is exactly why the phone cannot work
when the Mac is off.
2. Filesystem/state
The desktop injects
LOCAL_STUDIO_DATA_DIR(Electron'suserDataDir) when it spawns the runtime.The runtime reads and writes its durable state there:
api-settings.json(controllerbackendUrl/apiKey),litter-controller-id,litter-bridge.json(gateway secret + metadata),projects.json,pi-agent/models/sessions, connectors, goals, automations and the OAuth vault.The controller connection and the gateway identity therefore live inside the Mac's user-data
directory.
3. Controller-URL coupling
The runtime resolves its model backend from
settings.backendUrl(
services/agent-runtime/src/pi-runtime-models.ts#mergeControllers→ providersbaseUrl: ${controller.url}/v1), and the gateway fetches controller/health,/status,/gpusand
/v1/metrics/vllmfrom the same setting. The runtime already reaches the configuredcontroller over HTTP — this part is already compatible with a remote controller.
4. Dual frontend consumption (the biggest gap)
The Next.js frontend consumes the runtime in two different ways:
/api/agent/*routes pass throughproxyToAgentRuntime, honoringLOCAL_STUDIO_AGENT_RUNTIME_URL. These are already remote-capable.local-agents, and the
/api/proxy/[...path]target import@local-studio/agent-runtime/*modules inside the Next.js server process, sharing the same data dir. These are NOT proxied,
so a remote runtime leaves the desktop unable to manage settings/connectors/plugins/projects
until they are converted to HTTP calls.
5. Loopback chattiness
The desktop sets
LOCAL_STUDIO_FRONTEND_BASE(the runtime calls the frontend for browser/picontext and OAuth redirects) and
LOCAL_STUDIO_AGENT_RUNTIME_URL(the frontend calls the runtime).google-oauth-loopback.tsbinds127.0.0.1for the OAuth callback. The two processes share adata dir, a secret, and loopback URLs.
6. Pairing
frontend/desktop/logic/kittylitter-pairing.tsexecs a localkittylitterbinary(
kittylitter pair) which reads the runtime's publishedlitter-bridge.json(loopback
http://127.0.0.1:<port>/api/litter-bridge/v1+ secret + controllerId) to produce theQR payload the phone scans. Both the desktop IPC handler and the
/api/kittylitter/pairingroute shell out to this local binary. This is the root of theKittyLitter is unavailable (ENOENT)error and is inherently same-host.7. Gateway reachability
server.tsbindshostname: "127.0.0.1"and the gateway metadata URL is hardcoded loopback. Thepairing schema already permits a
relayfield, but the gateway never sets it — nothing today makesthe gateway reachable from a phone off the local network.
Goal of the refactor: decouple the layers
The refactor should remove the desktop-app dependency from the mobile (KittyLitter) path, while
keeping the desktop app able to reach the gateway and controller as it does today. Concretely:
Promote the agent-runtime to a standalone, deployable service. It already is a discrete
Node/Hono process (
services/agent-runtime/src/server.ts,dist/standalone.mjs) with a healthendpoint. Treat it as a first-class service that can run on a remote/headless host (systemd /
container / long-lived process), configured via environment (
LOCAL_STUDIO_DATA_DIR,BACKEND_URL,API_KEY, port/host bind) instead of Electron-main-injected values.Decouple the mobile connection from the desktop process. The gateway must be able to run
remotely and stay alive independently of the desktop app. The phone connects to the gateway
wherever it lives; nothing in the phone path may require the desktop app to be running.
Move durable identity + controller settings to the runtime/controller side so a remote
runtime has its own stable controllerId, gateway secret and controller URL — out of the Mac's
user-data directory.
Close the in-process frontend gap. Convert the settings/connectors/plugins/projects/skills/
local-agents routes that currently import runtime modules in-process to proxy to the runtime
over HTTP, completing the "everything through
LOCAL_STUDIO_AGENT_RUNTIME_URL" model. This keepsthe desktop app fully functional while the runtime is remote.
Make pairing remote-capable. Serve the pairing payload from an authenticated gateway/controller
endpoint (secret→token, controllerId→node_id, optional relay) so a phone can pair without a local
kittylitterbinary reading a loopback file.Make the gateway reachable off-Mac — configurable bind, TLS/public URL, or tunnel/relay — so
the phone can reach it over the network, not just
127.0.0.1.Desired end state
desktop app process. Works from the phone when the desktop app / Mac is off, as long as the
gateway host (e.g., the remote controller) is online.
gateway for the mobile-bridge features), but the two layers are decoupled: the desktop no longer
owns the runtime's lifecycle, state or pairing.
Acceptance criteria
use KittyLitter from a phone against it.
persists (gateway unaffected).
projects, sessions, model picker, agent turns.