Skip to content

Studio Code: run the remote-session bridge as a background daemon - #3300

Merged
gcsecsey merged 4 commits into
trunkfrom
gcsecsey/stu-1649
May 5, 2026
Merged

Studio Code: run the remote-session bridge as a background daemon#3300
gcsecsey merged 4 commits into
trunkfrom
gcsecsey/stu-1649

Conversation

@gcsecsey

@gcsecsey gcsecsey commented Apr 30, 2026

Copy link
Copy Markdown
Member

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 behind STUDIO_ENABLE_REMOTE_SESSION=true:

  • studio code remote-session start [--detach] [--remote-chat-id N] [--remote-bot foo]. Without --detach it behaves like studio code --remote-session. With --detach it forks a detached child via spawn(..., { detached: true, stdio: 'ignore', windowsHide: true }), sets STUDIO_REMOTE_SESSION_DAEMON_CHILD=1 on 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 via process.kill(pid, 0), removes stale PID files automatically, and prints a human-readable status.

The pre-existing studio code --remote-session flag continues to work unchanged.

Testing Instructions

  • npm run cli:build
  • export STUDIO_ENABLE_REMOTE_SESSION=true and make sure you have a remote-session token (logging in with /login inside studio code is enough)
  • node apps/cli/dist/cli/main.mjs code remote-session status should print not running
  • node apps/cli/dist/cli/main.mjs code remote-session start --detach should print Started (PID …, log: …) and return
  • Re-run status from a fresh terminal: should still report running
  • Send a Telegram message to your test bot: the local agent should respond
  • node apps/cli/dist/cli/main.mjs code remote-session stop should print stopped, and Telegram should receive the detach message.

Negative path

  • In a clean DEV_CONFIG_DIR with no token, node apps/cli/dist/cli/main.mjs code remote-session start --detach should print the missing-token error in the foreground and exit non-zero with no daemon spawned and no PID file written
  • Confirm studio code --help does NOT show remote-session when the feature flag is off
Scenario Command Output
No daemon yet studio code remote-session status CleanShot 2026-05-01 at 10 48 35@2x
Start as daemon studio code remote-session start --detach CleanShot 2026-05-01 at 10 48 56@2x
Confirm from a fresh terminal studio code remote-session status CleanShot 2026-05-01 at 10 49 30@2x
Bridge end-to-end Telegram chat CleanShot 2026-05-01 at 10 53 52@2x
Graceful stop studio code remote-session stop CleanShot 2026-05-01 at 10 54 09@2x
Back to clean state studio code remote-session status CleanShot 2026-05-01 at 10 54 20@2x
Already running studio code remote-session start --detach (second time) CleanShot 2026-05-01 at 10 54 39@2x
No token configured studio code remote-session start --detach (clean DEV_CONFIG_DIR) CleanShot 2026-05-01 at 10 55 38@2x
Stop when not running studio code remote-session stop CleanShot 2026-05-01 at 10 56 02@2x
Stale PID file studio code remote-session status (after editing PID file to a dead PID) CleanShot 2026-05-01 at 10 57 52@2x
Feature flag off studio code --help CleanShot 2026-05-01 at 10 59 31@2x
Feature flag on studio code --help CleanShot 2026-05-01 at 10 58 09@2x

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread apps/cli/remote-session/daemon.ts
Comment thread apps/cli/remote-session/daemon.ts
Comment thread apps/cli/remote-session/tests/daemon.test.ts Outdated
Comment thread apps/cli/remote-session/tests/daemon.test.ts
Comment thread apps/cli/remote-session/daemon.ts Outdated
gcsecsey added 2 commits May 1, 2026 11:08
- 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
@gcsecsey
gcsecsey marked this pull request as ready for review May 1, 2026 10:20
@wpmobilebot

Copy link
Copy Markdown
Collaborator

📊 Performance Test Results

Comparing aec0bdc vs trunk

app-size

Metric trunk aec0bdc Diff Change
App Size (Mac) 1511.24 MB 1511.26 MB +0.01 MB ⚪ 0.0%

site-editor

Metric trunk aec0bdc Diff Change
load 1728 ms 1881 ms +153 ms 🔴 8.9%

site-startup

Metric trunk aec0bdc Diff Change
siteCreation 8100 ms 8086 ms 14 ms ⚪ 0.0%
siteStartup 4946 ms 4951 ms +5 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

@epeicher epeicher left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @gcsecsey, this is working great! I have tested all scenarios and they ar working as expected:

Scenario Output
No daemon yet Image
Start as daemon Image
Confirm from a fresh terminal Image
Bridge end-to-end Image
Graceful stop Image

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! 🙌

@gcsecsey

gcsecsey commented May 5, 2026

Copy link
Copy Markdown
Member Author

Thanks for the review @epeicher! 🙌

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?

Absolutely, I'll create a follow-up for this.

@gcsecsey
gcsecsey merged commit 17b8a5c into trunk May 5, 2026
10 checks passed
@gcsecsey
gcsecsey deleted the gcsecsey/stu-1649 branch May 5, 2026 12:05
gcsecsey added a commit that referenced this pull request May 5, 2026
## 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).
gcsecsey added a commit that referenced this pull request May 6, 2026
## 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?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

4 participants