apps/cli: Telegram remote-session bridge for studio code (PoC) - #3196
Merged
gcsecsey merged 14 commits intoApr 28, 2026
Merged
Conversation
Lets the user drive studio code from Telegram. Each polled message resumes the bound chat's session via studio code --json --resume-session, and the final result text (or flattened question.asked) is posted back. Reachable via a top-level --remote-session flag for launchd-style use, or via /remote-session attach inside the interactive REPL (blocks the REPL until detach). Includes the spec the implementation was built from.
The server returns { messages: [ { message, chat_id, bot, user_id, timestamp } ] },
not the flat { chat_id, text, bot } the spec drafted. The user's text is in
`message`, not `text`, and a single poll can carry several pending messages.
pollMessage → pollMessages (returns []), and the poll loop now drains the batch
sequentially — one studio code --json turn per entry — before polling again.
Spec and tests updated to match.
- Architecture diagram and poll-loop pseudocode now drain a batch instead of handling one message at a time, matching the real /local-agent-poll shape. - Added a "Why chat_id and bot are pre-configured" section so future readers don't repeat the question of why those values aren't taken straight from the polled message (scoping/security fence and reply identity pinning).
The server's bearer token is the auth boundary; any message returned by /local-agent-poll is already authorized for this local agent. Drop the requirement to pre-configure chat_id and bot: - token is the only required config field; still falls back to the WP.com OAuth access token from ~/.studio/shared.json when unset - chat_id and bot become optional pins. When unset, the poll loop processes any polled message and echoes the reply target (chat_id + bot) from the inbound payload. When set, they keep their prior role: chat_id filters inbound messages, bot overrides outgoing identity - attach/detach/process-exit POSTs only fire when chat_id is pinned (there is no chat to post to otherwise) - state is now keyed by each polled chat_id, not by config With this change a logged-in user can run `node apps/cli/dist/cli/main.mjs code --remote-session` with no config at all and it Just Works. Spec and tests updated.
gcsecsey
reviewed
Apr 23, 2026
…ills in-flight child Without a signal, Ctrl-C / detach only aborted the poll fetch; an active turn ran until turn_timeout_seconds (default 15 min). handleTurn now takes the controller's signal, forwards it to both runTurn calls, skips the stale-session retry once aborted, and returns silently — announceDetach posts the detach notice instead of a misleading "Turn took too long" reply.
gcsecsey
reviewed
Apr 27, 2026
…SION env flag (STU-1654) Default OFF so the experimental Telegram bridge stays out of the stable CLI experience. With the flag unset, the --remote-session/--remote-chat-id/--remote-bot/--message-from-stdin options are not registered on `studio code`, the /remote-session slash command is hidden from autocomplete and the dispatcher won't match it, and the corresponding handler branches are no-ops even if the flags somehow reach argv. Internal users opt in by setting STUDIO_ENABLE_REMOTE_SESSION=true with no rebuild needed.
…c-for-telegram-to-studio-code-e2e-communication # Conflicts: # apps/cli/commands/ai/index.ts
…ile to fix Windows race `fs.createWriteStream` opens lazily and `.end()` queues the close — neither was awaited, so when a safe entry was the last operation in `extractFiles` (e.g. one blocked + one safe entry), the caller's synchronous `existsSync` could race the NTFS open on Windows. Linux/macOS's libuv worker happened to flush fast enough to mask it. Awaiting the stream's `close` event before returning closes the race cross-platform.
gcsecsey
marked this pull request as ready for review
April 28, 2026 10:36
…o-code-e2e-communication
Collaborator
📊 Performance Test ResultsComparing 34484f6 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) |
Per review feedback: with the poll loop blocking the REPL, attaching from inside `studio code` doesn't add much over the `--remote-session` flag, so defer the slash command (and the dispatcher's whitespace tokenization that existed only to support its subcommands) until a non-blocking REPL mode lands. The `enabled?: () => boolean` gate on SlashCommandDef and the getActiveSlashCommands helper go with it — `--remote-session` itself is still gated by STUDIO_ENABLE_REMOTE_SESSION via the existing feature-flags helper. Spec gets a status note marking the slash sections as deferred so the design isn't lost.
Member
|
As discussed on p1777374476404299/1777372569.384689-slack-C0AULRXLJQL, I removed the slash-commands from this initial version, and also removed these from the PR description. I'll merge this once CI passes. |
…mplete-poc-for-telegram-to-studio-code-e2e-communication
gcsecsey
approved these changes
Apr 28, 2026
gcsecsey
enabled auto-merge (squash)
April 28, 2026 12:40
gcsecsey
deleted the
stu-1612-complete-poc-for-telegram-to-studio-code-e2e-communication
branch
April 28, 2026 12:45
This was referenced Apr 28, 2026
gcsecsey
added a commit
that referenced
this pull request
May 1, 2026
…3272) ## Related issues - Related to STU-1652 - Builds on the Telegram remote-session bridge from #3196 - Companion PR: 213611-ghe-Automattic/wpcom ## How AI was used in this PR Claude wrote the bulk of the implementation and the tests. I reviewed and tested the changes end-to-end in my sandbox using the companion PR. ## Proposed Changes The Telegram remote-session bridge is currently text-only. When the agent finishes a visible task, the user gets a prose summary but no image. This PR lets the local agent deliver screenshots inline: - New `share_screenshot` tool. Captures a 16:9 above-the-fold view of a URL by default and emits a `media.share` JSON event. `fullPage: true` is opt-in for the rare case where the user wants the whole scroll length. `take_screenshot` stays unchanged as the model-internal reasoning tool. - Remote-session controller (`turn-runner` + `poll-loop`) collects `media.share` events from the spawned `studio code --json` child and posts each photo before the text reply. - `respondMessage` now picks transport based on payload. Photo present means `multipart/form-data` with a `photo` file part (matches the wpcom contract); text-only stays on the existing JSON path. - Spawned child gets `STUDIO_REMOTE_SESSION=1` so the system prompt knows to keep replies short, deliver visible work via `share_screenshot`, follow up with a "Want me to publish this as a preview site?" line, and stop fabricating "gist stored / preview link saved" epilogues that aren't backed by any actual storage. ## Testing Instructions #### Prerequisites - Be an Automattician (backend gates on `is_automattician()`) - Be logged in via `studio auth login` so the bearer falls through from `~/.studio/shared.json` - Follow the testing steps on 213611-ghe-Automattic/wpcom to apply the backend changes on your sandbox #### Testing - Build the CLI: `STUDIO_ENABLE_REMOTE_SESSION=true npm run cli:build` - Start the bridge: `STUDIO_ENABLE_REMOTE_SESSION=true node apps/cli/dist/cli/main.mjs code --remote-session` - In a second terminal, tail the log: `tail -F ~/.studio/remote-session.log` - From Telegram, send: "send to my local agent: take a screenshot of my site and show me" - Check in Telegram: - A 1280x720 above-the-fold screenshot arrives inline - A follow-up text message asks about publishing a preview site - Test for the text-only regressions by sending a non-visual request like "list my local sites" - Ask explicitly for a full page screenshot and confirm `share_screenshot` is called with `fullPage: true` and the long capture is delivered ## Pre-merge Checklist - [ ] Have you checked for TypeScript, React or other console errors? --------- Co-authored-by: Roberto Aranda <roberto.aranda@automattic.com>
gcsecsey
added a commit
that referenced
this pull request
May 5, 2026
) ## Related issues - Fixes STU-1649 ## 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` | <img width="1000" height="148" alt="CleanShot 2026-05-01 at 10 48 35@2x" src="https://github.com/user-attachments/assets/e8d48b10-0d92-46a2-ba1e-c8bfc16d9c46" /> | | Start as daemon | `studio code remote-session start --detach` | <img width="1510" height="146" alt="CleanShot 2026-05-01 at 10 48 56@2x" src="https://github.com/user-attachments/assets/cb9104b0-92c1-4aa0-a7d9-f5c398789bb8" /> | | Confirm from a fresh terminal | `studio code remote-session status` | <img width="1506" height="208" alt="CleanShot 2026-05-01 at 10 49 30@2x" src="https://github.com/user-attachments/assets/a3e9fc7d-ca6a-4474-8ec3-27b8d772f2b8" /> | | Bridge end-to-end | Telegram chat | <img width="1736" height="1538" alt="CleanShot 2026-05-01 at 10 53 52@2x" src="https://github.com/user-attachments/assets/7f4ac4e5-b4fd-45fa-a924-642fe90ff586" /> | | Graceful stop | `studio code remote-session stop` | <img width="1506" height="144" alt="CleanShot 2026-05-01 at 10 54 09@2x" src="https://github.com/user-attachments/assets/f70e1885-88d1-4ffb-acb3-96350fdffb18" /> | | Back to clean state | `studio code remote-session status` | <img width="1506" height="144" alt="CleanShot 2026-05-01 at 10 54 20@2x" src="https://github.com/user-attachments/assets/1a2f5f42-a0d2-4fd5-9ad6-228877918ee5" /> | | Already running | `studio code remote-session start --detach` (second time) | <img width="1618" height="144" alt="CleanShot 2026-05-01 at 10 54 39@2x" src="https://github.com/user-attachments/assets/17ee49cf-b2f5-4e22-b8cc-67907ce6ffc4" /> | | No token configured | `studio code remote-session start --detach` (clean DEV_CONFIG_DIR) | <img width="1618" height="212" alt="CleanShot 2026-05-01 at 10 55 38@2x" src="https://github.com/user-attachments/assets/b549acc2-1e85-4d8f-80ad-1e8fd1c888b6" /> | | Stop when not running | `studio code remote-session stop` | <img width="1486" height="124" alt="CleanShot 2026-05-01 at 10 56 02@2x" src="https://github.com/user-attachments/assets/1cacc9fd-090f-4f9b-915e-33c6b39b36fa" /> | | Stale PID file | `studio code remote-session status` (after editing PID file to a dead PID) | <img width="1486" height="126" alt="CleanShot 2026-05-01 at 10 57 52@2x" src="https://github.com/user-attachments/assets/c438569a-aa90-4575-9adf-4d2f198d5a7c" /> | | Feature flag off | `studio code --help` | <img width="1486" height="674" alt="CleanShot 2026-05-01 at 10 59 31@2x" src="https://github.com/user-attachments/assets/80490b88-042c-4e14-99aa-23fcef87ce60" /> | | Feature flag on | `studio code --help` | <img width="1486" height="874" alt="CleanShot 2026-05-01 at 10 58 09@2x" src="https://github.com/user-attachments/assets/c6483ee1-e204-4eb3-8c94-ed5e6fe6d1a8" /> | ## Pre-merge Checklist - [x] Have you checked for TypeScript, React or other console errors?
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Related issues
How AI was used in this PR
This is a Proof of Concept built end-to-end by Claude Code from a written spec at
specs/studio-code-remote-session-spec.md. The spec itself was iterated against the actualstudio code --jsonevent stream and the real/local-agent-pollresponse shape; the design then pivoted twice based on review feedback (token fallback to the WP.com OAuth login, thenchat_id/botderived per-message instead of pre-bound). All 4 commits show that progression.Reviewers should focus on:
chat_idpin should be required.apps/cli/remote-session/turn-runner.ts(re-execingprocess.execPath+process.argv[1]) and the stale---resume-sessionheuristic./remote-session attach.Proposed Changes
A new
apps/cli/remote-session/module that letsstudio codebe driven from a Telegram chat by polling the existingwpcom/v2/telegram-bot/local-agent-pollendpoint. Per-message turn = spawnstudio code --json [--resume-session <id>] -- <text>, parse NDJSON, post the result (or flattenedquestion.asked) back via/local-agent-respond.Entry point:
studio code --remote-session [--remote-chat-id <id>] [--remote-bot <name>]— headless mode, intended for launchd or a dedicated terminal tab.Important
The entry point is gated behind
STUDIO_ENABLE_REMOTE_SESSION=true(default OFF). With the flag unset, the experimental options don't appear instudio code --help.Zero-config for logged-in users:
tokenfalls back to the WP.com OAuthaccessTokenin~/.studio/shared.json.botandchat_idare optional pins (unset = derive both from each polled message; set = filter/override).New module:
config.ts— zod-validated config + WP.com OAuth fallback.state.ts— per-chat session-id store, lockfile-guarded, mode 0600.telegram-client.ts—pollMessages/respondMessagewith explicit error taxonomy (TelegramAuthError,TelegramTransientError,TelegramBadRequestError).turn-runner.ts— subprocess + NDJSON parse + SIGTERM/SIGKILL on timeout.reply-formatter.ts—extractReply()(success / paused / null) + chunker that preserves fenced code blocks.poll-loop.ts— controller: drain batch → /new ack → handle turn → post chunks → stale-session retry → backoff/auth/fatal handling.index.ts— entry that wires signals (SIGINT/SIGTERM → graceful detach) and the best-effort exit POST.Touchpoints in existing code:
apps/cli/commands/ai/index.ts— adds--remote-session,--remote-chat-id,--remote-bot; extends the slash-command dispatcher to split on whitespace before lookup (was exact-match only) so subcommand arguments work.apps/cli/ai/slash-commands.ts— registers/remote-sessionwith handler.tools/common/constants.ts+tools/common/lib/well-known-paths.ts— adds path helpers + lockfile name.specs/studio-code-remote-session-spec.md— full spec the implementation tracks.Testing Instructions
Build and run:
Expected with WP.com login already present in
~/.studio/shared.json: the process attaches and starts polling silently (no Telegram attach POST when chat is not pinned).Then send a message from Telegram to the bot. The reply should appear in the same chat within a few seconds. Subsequent messages share the session — verify by referring back to earlier turns. Send
/newfrom Telegram to reset the session.Optional kiosk-style pinning:
With pinning, the controller posts
🟢 Local agent attached.on attach and drops messages from other chats.To verify the feature is hidden by default:
To exercise the test suite:
npm test -- --project cli apps/cli/remote-session(56 tests, all passing locally; full CLI suite at 482/482.)
Pre-merge Checklist
chat_idshould be required.--resume-sessionsignal, normal-stderr behavior./remote-session attachblocking the REPL is acceptable for v1 (vs. a parallel mode).