Studio Code: run the remote-session bridge as a background daemon - #3300
Conversation
Adds `studio code remote-session start|stop|status` subcommands so the Telegram bridge can keep running after the user closes their terminal. The existing `studio code --remote-session` flag still works as a foreground entry point. Whole surface stays gated by STUDIO_ENABLE_REMOTE_SESSION.
There was a problem hiding this comment.
Pull request overview
Adds daemon-style lifecycle management for the experimental Telegram remote-session bridge in the Studio CLI so the bridge can keep running after the launching terminal is closed.
Changes:
- Introduces
studio code remote-session {start [--detach], stop, status}(feature-flag gated) and wires it into the CLI command tree. - Implements daemon helpers (PID file, start/stop/status) and integrates daemon-child hooks into
runRemoteSession(). - Adds unit/integration-ish tests for the daemon helpers and updates the spec to document the new surface.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/common/lib/well-known-paths.ts | Adds a well-known path helper for the remote-session PID file. |
| specs/studio-code-remote-session-spec.md | Documents the new daemon control subcommands and PID tracking. |
| apps/cli/remote-session/tests/daemon.test.ts | Adds test coverage for daemon status/PID handling and start/stop behavior. |
| apps/cli/remote-session/index.ts | Installs PID hooks when running as the detached daemon child; suppresses interactive stdout in daemon mode. |
| apps/cli/remote-session/daemon.ts | New daemonization implementation: spawn detached child, PID file lifecycle, stop/status helpers. |
| apps/cli/index.ts | Registers the new remote-session subcommand tree when the feature flag is enabled. |
| apps/cli/commands/ai/remote-session.ts | New yargs command implementation for start/stop/status and detach behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- writePidFile() now mkdirs the config directory so the daemon doesn't rely on migration/logger init order having created it. - Fix the doc comment on DAEMON_CHILD_ENV_VAR to match the actual isDaemonChild() check (must be exactly "1"). - stopDaemon() leaves the PID file in place and returns stopped:false when the process is still alive after the SIGKILL grace, so status keeps reporting accurately. The CLI surfaces this as a stderr message and a non-zero exit. - Replace hardcoded PID 999999 in the test fixtures with a spawn-then-await-exit helper so the dead-PID assertions don't depend on the PID never being reused on Linux CI.
# Conflicts: # apps/cli/remote-session/index.ts
📊 Performance Test ResultsComparing aec0bdc vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
epeicher
left a comment
There was a problem hiding this comment.
Thanks @gcsecsey, this is working great! I have tested all scenarios and they ar working as expected:
| Scenario | Output |
|---|---|
| No daemon yet | ![]() |
| Start as daemon | ![]() |
| Confirm from a fresh terminal | ![]() |
| Bridge end-to-end | ![]() |
| Graceful stop | ![]() |
I have also tested the feature flag, and I can confirm that the feature is not displayed when the flag is disabled.
As a follow-up, I think we can remove the --remote-session as a flag as we are using it as a command. What do you think?
As a conclusion, I think this greatly improves the UX of the remote session. Great work! 🙌
|
Thanks for the review @epeicher! 🙌
Absolutely, I'll create a follow-up for this. |
## Related issues - Fixes STU-1655 - Builds on STU-1649 / #3300 ## Proposed Changes Earlier iterations of `/remote-session` blocked the REPL while the poll loop ran. With the daemonization landed in #3300, the slash command becomes a thin REPL-side controller for the background daemon and the REPL never blocks. Subcommands, gated behind `STUDIO_ENABLE_REMOTE_SESSION`: - `/remote-session start` — validates config in-process so a missing token surfaces as `ui.showError`, then calls `startDaemon()` to spawn the detached child. Idempotent if a daemon is already running. Reports the PID and tells the user to ping `Dolly (@wordpress_com_bot)` on Telegram. - `/remote-session stop` — calls `stopDaemon()`, surfaces friendly messages for already-stopped / SIGKILL fallback / refused-to-die. There is intentionally no `/remote-session status` slash command — the bottom-bar indicator already shows whether the daemon is running, and `studio code remote-session status` covers the out-of-REPL case. Mechanics: - `SlashCommandDef` gains an optional `getArgumentCompletions(prefix)` hook so typing `/remote-session ` lights up `start`/`stop` in the autocomplete dropdown via pi-tui's existing argument-completion path. - The REPL dispatcher now matches on the first whitespace token (`/${name} <args>`) so `/remote-session <sub>` resolves to the remote-session command and the handler parses the subcommand. This is the dispatcher change that was previously dropped in `96b7ab6cb` — now justified by an actual subcommand surface. - New `daemon-status-poll` module mirrors the on-disk PID file into a new bottom-bar indicator (`PromptEditor.daemonStatusMessage`) every 5s, so external start/stop and unexpected daemon death keep the REPL state honest. Indicator reads `Remote session active` in green and coexists with the existing login `statusMessage` (`Remote session active · Logged in as Gergely`). - `AiOutputAdapter` gains `setDaemonStatus({ running, pid? })`; `JsonAdapter` no-ops it. ## Testing Instructions 1. Build: `npm run cli:build`. 2. With `STUDIO_ENABLE_REMOTE_SESSION=true`, launch `studio code`. Type `/` — `remote-session` should appear in the autocomplete dropdown. Type `/remote-session ` (trailing space) — `start`, `stop` should appear. 3. Without a WP.com login, run `/remote-session start`. The REPL should show a red error about needing a bearer token. 4. After `/login` (or an existing token), run `/remote-session start` — should print a success message and the bottom-right of the prompt should show a green `Remote session active` message. 5. From a fresh terminal, run `node apps/cli/dist/cli/main.mjs code remote-session status` — should report running, same PID. 6. Send a Telegram message to the bot — the daemon should reply. 7. Back in the REPL, run `/remote-session stop` — should print `Remote-session stopped (PID …)` and the indicator should disappear. 8. External-stop path: start the daemon via `/remote-session start`, then run `studio code remote-session stop` from another terminal. Within 5s, the bottom-bar indicator should clear on its own (5s poll). 9. With the flag off (`unset STUDIO_ENABLE_REMOTE_SESSION`), confirm `/remote-session` is hidden from autocomplete and falls through to the AI agent. 10. Tests: `npm test -- --project cli apps/cli/ai/tests/slash-commands.test.ts apps/cli/ai/tests/daemon-status-poll.test.ts`. | Scenario | Trigger | Screenshot | | --- | --- | --- | | Autocomplete lists `/remote-session` (flag on) | Type `/` in the REPL | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 14 54@2x" src="https://github.com/user-attachments/assets/002b2394-be2d-4104-ac8d-09adf5857c30" /> | | Subcommand autocomplete | Type `/remote-session ` | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 14 35@2x" src="https://github.com/user-attachments/assets/ba9dd70b-fcec-4ea0-8958-4ef8e1a0dcb6" /> | | Missing token → red error, no daemon spawned | `/remote-session start` (no WP.com login) | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 15 47@2x" src="https://github.com/user-attachments/assets/689b4c60-0c3e-4702-af7b-d0cea20691d1" /> | | Daemon started + bottom-bar indicator | `/remote-session start` (logged in) | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 16 17@2x" src="https://github.com/user-attachments/assets/7aad7983-86b5-49ce-bec0-8f375f8a9c32" /> | | External status check matches | `studio code remote-session status` (fresh terminal) | <img width="984" height="210" alt="CleanShot 2026-05-01 at 14 16 43@2x" src="https://github.com/user-attachments/assets/91d7c513-c4f3-4f40-9fa9-a9f2c5d6300b" /> | | Telegram round-trip | Send a message to @wordpress_com_bot | <img width="1066" height="1024" alt="CleanShot 2026-05-01 at 14 18 18@2x" src="https://github.com/user-attachments/assets/4b2ad570-0cb5-423b-b56d-bbd56cda7e91" /> | | Daemon stopped + indicator cleared | `/remote-session stop` | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 18 37@2x" src="https://github.com/user-attachments/assets/9fff9315-1518-46d3-9265-d4fad9727d12" /> | | External stop reflected within 5s | `studio code remote-session stop` (fresh terminal, then watch the REPL) | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 19 06@2x" src="https://github.com/user-attachments/assets/36c1fabb-3839-4474-989c-9b4337e89421" /><img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 19 08@2x" src="https://github.com/user-attachments/assets/b0d52603-c827-4d5d-87f9-a28deff5648c" /> | | Slash command hidden when flag is off | Type `/` with `STUDIO_ENABLE_REMOTE_SESSION` unset | <img width="1798" height="1040" alt="CleanShot 2026-05-01 at 14 19 55@2x" src="https://github.com/user-attachments/assets/5c684468-0e78-4a66-96a2-028072a201b7" /> | ## Pre-merge Checklist - [x] TypeScript clean, lint clean, full suite passes (1537 tests).
## Related issues - Fixes STU-1682 - Follow-up to #3300 and #3315 ## How AI was used in this PR Implementation drafted with Claude Code; I reviewed the diff, ran the test suite, and smoke-tested the help output and the surviving entry points locally. ## Proposed Changes The experimental Telegram bridge originally shipped with a single autostart flag (`studio code --remote-session`). Since then, the daemonizable subcommand tree (`studio code remote-session start|stop|status`, #3300) and the in-REPL `/remote-session` slash command (#3315) have made the flag redundant. This PR removes it, so the surface has a single obvious way in. - Drops the `--remote-session`, `--remote-chat-id`, and `--remote-bot` options from the `studio code` parser, along with the short-circuit dispatcher that handed off to `runRemoteSession()`. The chat/bot pinning flags still live on `studio code remote-session start`. - Keeps the hidden `--message-from-stdin` option (still gated on `STUDIO_ENABLE_REMOTE_SESSION`) because the daemon's turn runner spawns `studio code --json --message-from-stdin` for each polled Telegram message. - Updates `specs/studio-code-remote-session-spec.md` and `RELEASE-NOTES.txt` so the documented entry points are the subcommand tree and slash command, with a STU-1682 status note explaining the cleanup. ## Testing Instructions - `npm run cli:build` - `node apps/cli/dist/cli/main.mjs code --help` should NOT list `--remote-session` (with or without `STUDIO_ENABLE_REMOTE_SESSION=true`). - `STUDIO_ENABLE_REMOTE_SESSION=true node apps/cli/dist/cli/main.mjs code remote-session --help` should still show `start`, `stop`, and `status`. - `STUDIO_ENABLE_REMOTE_SESSION=true node apps/cli/dist/cli/main.mjs code remote-session start --help` should still list `--detach`, `--remote-chat-id`, `--remote-bot`. - With `STUDIO_ENABLE_REMOTE_SESSION=true` and a configured token, run `studio code remote-session start --detach`, send a Telegram message, confirm the local agent replies, then `studio code remote-session stop`. - Inside an interactive `studio code` REPL, run `/remote-session start` and `/remote-session stop` and confirm the daemon indicator updates as before. - `npm test -- apps/cli/ai apps/cli/remote-session apps/cli/commands/ai` and `npm run typecheck` should pass. | Scenario | Command | Output | | --- | --- | --- | | Flag removed (feature flag on) | `STUDIO_ENABLE_REMOTE_SESSION=true studio code --help` | <img width="1314" height="688" alt="CleanShot 2026-05-06 at 10 37 30@2x" src="https://github.com/user-attachments/assets/0dd40f34-1fcd-4fce-839b-5444a14af3f8" /> | | Subcommand tree intact | `STUDIO_ENABLE_REMOTE_SESSION=true studio code remote-session --help` | <img width="1302" height="530" alt="CleanShot 2026-05-06 at 10 40 07@2x" src="https://github.com/user-attachments/assets/686fcf40-c7ec-4c6a-a441-0ba8dc143b63" /> | ## Pre-merge Checklist - [ ] Have you checked for TypeScript, React or other console errors?





Related issues
How AI was used in this PR
Implementation drafted with Claude Code, I reviewed the diff, ran the test suite, and smoke-tested the new CLI surface locally.
Proposed Changes
The experimental Telegram remote-session bridge added in #3196 (STU-1612) blocks the terminal it was launched from. If the user closes that terminal, the bridge dies and Studio loses connectivity to the remote agent. This PR makes the bridge daemonizable so users can close the terminal without dropping the session.
New subcommand tree under
studio code, gated behindSTUDIO_ENABLE_REMOTE_SESSION=true:studio code remote-session start [--detach] [--remote-chat-id N] [--remote-bot foo]. Without--detachit behaves likestudio code --remote-session. With--detachit forks a detached child viaspawn(..., { detached: true, stdio: 'ignore', windowsHide: true }), setsSTUDIO_REMOTE_SESSION_DAEMON_CHILD=1on the child env, and waits up to 5s for the child to write~/.studio/remote-session.pid(mode 0600) before returning. Config (token) is validated in the parent, so a missing token fails fast in the foreground.studio code remote-session stop. Reads the PID file, sends SIGTERM, polls for exit up to 5s, escalates to SIGKILL if needed, and always removes the PID file. The daemon's existing SIGTERM handler keeps the graceful detach path, so chats still receive🔴 Local agent detached.studio code remote-session status. Probes liveness viaprocess.kill(pid, 0), removes stale PID files automatically, and prints a human-readable status.The pre-existing
studio code --remote-sessionflag continues to work unchanged.Testing Instructions
npm run cli:buildexport STUDIO_ENABLE_REMOTE_SESSION=trueand make sure you have a remote-session token (logging in with/logininsidestudio codeis enough)node apps/cli/dist/cli/main.mjs code remote-session statusshould printnot runningnode apps/cli/dist/cli/main.mjs code remote-session start --detachshould printStarted (PID …, log: …)and returnstatusfrom a fresh terminal: should still report runningnode apps/cli/dist/cli/main.mjs code remote-session stopshould print stopped, and Telegram should receive the detach message.Negative path
node apps/cli/dist/cli/main.mjs code remote-session start --detachshould print the missing-token error in the foreground and exit non-zero with no daemon spawned and no PID file writtenstudio code --helpdoes NOT showremote-sessionwhen the feature flag is offstudio code remote-session statusstudio code remote-session start --detachstudio code remote-session statusstudio code remote-session stopstudio code remote-session statusstudio code remote-session start --detach(second time)studio code remote-session start --detach(clean DEV_CONFIG_DIR)studio code remote-session stopstudio code remote-session status(after editing PID file to a dead PID)studio code --helpstudio code --helpPre-merge Checklist