Skip to content

apps/cli: Route studio_mobile_* bot replies to studio-mobile-client endpoint (PoC)#3493

Open
gcsecsey wants to merge 5 commits into
trunkfrom
gcsecsey/cli-mobile-respond-routing
Open

apps/cli: Route studio_mobile_* bot replies to studio-mobile-client endpoint (PoC)#3493
gcsecsey wants to merge 5 commits into
trunkfrom
gcsecsey/cli-mobile-respond-routing

Conversation

@gcsecsey
Copy link
Copy Markdown
Member

@gcsecsey gcsecsey commented May 14, 2026

Related issues

How AI was used in this PR

Used Claude Code to implement the flow described in the SPEC. Reviewed the diff manually, and tested end-to-end against a local daemon talking to mobile app through wpcom.

Proposed Changes

The Studio CLI's remote-session loop currently posts every reply to /wpcom/v2/telegram-bot/local-agent-respond regardless of which client originated the message. That endpoint only knows how to forward to Telegram, so replies destined for the studio-mobile PoC client (polled with bot: "studio_mobile_<machine_id>") get rejected with rest_missing_callback_param. The wpcom side now exposes a parallel /wpcom/v2/studio-mobile-client/respond endpoint that accepts an agent_message envelope; this PR teaches the CLI to use it.

  • Forks the outbound URL in respondMessage(): when the effective bot starts with studio_mobile_, hit /wpcom/v2/studio-mobile-client/respond (derived by swapping the trailing telegram-bot segment in base_url so dev hosts still resolve). Other bots keep using /local-agent-respond unchanged.
  • Forks the body builder. Mobile path sends { chat_id, bot, machine_id, envelope: { type: "agent_message", id, text } }. chat_id and bot are the queue routing keys today; machine_id is forward-compat for when wpcom switches the queue key in Phase 2 of the SPEC; envelope.text is what the server reads instead of the old top-level text field. Telegram path is unchanged.
  • Photos are out of scope for the studio-mobile v1 surface, so when a media.share event lands on a studio_mobile_* bot the photo bytes are dropped and the caption (or a placeholder) is promoted to the envelope text, with a warning logged. Telegram still gets the multipart sendPhoto path.

Testing Instructions

  • Follow the testing steps on 217387-ghe-Automattic/wpcom
  • Pull this branch and rebuild the CLI: npm run cli:build.
  • Apply the env var to enable remote sessions: export STUDIO_ENABLE_REMOTE_SESSION=true
  • Stop any running remote-session daemon: node apps/cli/dist/cli/main.mjs code remote-session stop.
  • Start a fresh daemon: node apps/cli/dist/cli/main.mjs code remote-session start.
  • Send a message from your Telegram bot to verify the existing path is unchanged. The reply should land in Telegram and ~/.studio/remote-session.log should show a successful Posting reply followed by no Respond 4xx.
  • Send a message from the studio-mobile PoC client (which polls with a studio_mobile_* bot suffix). The reply should land in the mobile app, and the log should again show a successful Posting reply with no 4xx.
  • Optional: run npm test -- apps/cli/remote-session to exercise the unit tests covering the routing and body shape.
Scenario Log tail Reply visible in client
Telegram bot reply (regression) CleanShot 2026-05-14 at 15 30 35@2x CleanShot 2026-05-14 at 15 29 56@2x
studio_mobile_* bot reply (new path) CleanShot 2026-05-14 at 15 35 13@2x CleanShot 2026-05-14 at 15 35 26@2x

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

…ndpoint

When the polled message comes from a bot whose name starts with
studio_mobile_, the remote-session controller now POSTs the reply to
/wpcom/v2/studio-mobile-client/respond with an agent_message envelope
alongside the existing chat_id and bot routing keys. Other bots keep
hitting /local-agent-respond unchanged.
@gcsecsey gcsecsey requested a review from epeicher May 14, 2026 14:25
@gcsecsey gcsecsey changed the title apps/cli: Route studio_mobile_* bot replies to studio-mobile-client endpoint apps/cli: Route studio_mobile_* bot replies to studio-mobile-client endpoint (PoC) May 14, 2026
@gcsecsey gcsecsey marked this pull request as ready for review May 14, 2026 14:36
@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented May 14, 2026

📊 Performance Test Results

Comparing eeb277f vs trunk

app-size

Metric trunk eeb277f Diff Change
App Size (Mac) 1339.07 MB 1339.08 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk eeb277f Diff Change
load 1687 ms 1792 ms +105 ms 🔴 6.2%

site-startup

Metric trunk eeb277f Diff Change
siteCreation 9590 ms 9599 ms +9 ms ⚪ 0.0%
siteStartup 4921 ms 4914 ms 7 ms ⚪ 0.0%

Results are median values from multiple test runs.

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

gcsecsey added 2 commits May 19, 2026 15:51
…ing fork

Move machine_id into the remote-session config schema with a sanitized
os.hostname() default and a STUDIO_REMOTE_MACHINE_ID env override. Removes
the MobileTarget struct, the empty-suffix throw in the NDJSON reader path,
and the silent base_url misroute when /telegram-bot is missing.
…pond router

The mobile and Telegram message shapes were both living inside telegram-client.ts
alongside the dispatch logic, which made the file the de-facto home for the
entire wire protocol. Move the studio-mobile shape into its own
studio-mobile-client.ts, the Telegram-only code stays in telegram-client.ts,
and introduce respond-router.ts as the dispatcher (fork + retry loop). Shared
HTTP primitives (errors, URL helpers, backoff) move to remote-http.ts; the
Telegram-flavored error class names become Remote*Error to reflect that they're
transport-layer concerns shared by both clients.
@gcsecsey
Copy link
Copy Markdown
Member Author

@claude please review these changes

@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

Claude finished @gcsecsey's task in 4m 38s —— View job


Code Review

  • Read PR diff against c8c90bb (the merge-base on this branch)
  • Read new files (respond-router.ts, studio-mobile-client.ts, remote-http.ts)
  • Read modified telegram-client.ts, config.ts, poll-loop.ts
  • Read new + updated tests
  • Post review

Summary

Clean, well-scoped refactor. The previous "fork inside respondMessage" approach has been split into three single-responsibility modules:

  • remote-http.ts — transport-layer primitives (error classes, URL helpers, backoff, assertSameHost, composeSignals)
  • studio-mobile-client.ts — studio-mobile wire format only
  • respond-router.ts — picks the client per bot, owns the retry loop, and handles Telegram's partial-success payload
  • telegram-client.ts — now only the Telegram poll + Telegram body builder

The split is justified by the actual axes of change (transport vs. wire-format vs. retry policy), not premature abstraction, and the public respondMessage surface stays compatible with its callers (media-streamer, progress-streamer, poll-loop). The new respond-router.test.ts covers both transports plus input-validation edge cases without over-mocking.

The config.ts change to derive machine_id from os.hostname() (sanitized to [a-z0-9_]{1,32}) is a reasonable default, validated by Zod, and overridable via STUDIO_REMOTE_MACHINE_ID. The schema regex permits trailing underscores, which is consistent with the slice-after-strip ordering — looks intentional.

Findings

Posted two minor inline nits on studio-mobile-client.ts:

  1. Dead defensive throw after flattenPhotoToTexttext is always non-empty there by construction, and the empty-text case is already filtered upstream by respond-router.ts:85-87.
  2. Hardcoded '📷 (image omitted)' is user-visible in the mobile chat and could be a __() candidate. Consistent with surrounding code, so not a blocker.

A couple of other observations that didn't need inline comments:

  • PR description vs. code: the description still describes the body-builder fork as happening "in respondMessage()", but after commit 98dee4c the routing lives in respond-router.ts and delegates to the per-transport modules. Worth updating before review-merge.
  • bot merge precedence (respond-router.ts:89, poll-loop.ts:368-371): params.bot ?? config.bot (router) and config.bot ?? polled.bot (poll loop) — opposite orderings, but both intentional. The router lets per-call overrides win; the loop lets the operator's pinned config.bot win over what the server echoed. Worth a one-line comment somewhere if anyone gets confused later, but not a bug.
  • Routing safety: assertSameHost runs against the resolved URL (mobile or telegram), so the mobile-base swap can't smuggle in a different host. ✓
  • No file-locking concerns: remote-session.json has its own file with atomically writes; no shared lockfile contract to honor.

Nothing blocking. The bigger architectural question — whether studio-mobile should ever ride the Telegram poll loop, or get its own inbound channel as Phase 2 of the SPEC implies — is correctly out-of-scope for this PoC routing change.
· branch: gcsecsey/cli-mobile-respond-routing

Comment on lines +65 to +67
if ( ! text ) {
throw new Error( 'Studio mobile respond requires `text` (photos are not supported yet)' );
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor — this branch is unreachable in practice. flattenPhotoToText always returns a non-empty string (defaults to the 📷 (image omitted) placeholder), and when photo is absent the empty-text case is already filtered at respond-router.ts:85-87 before this function is called. The guard only fires if someone calls buildMobileRespondBody directly with { text: undefined, photo: undefined }. Either fine to keep as defense-in-depth, or you could drop it and rely on the router-level precondition.

if ( text && trimmedCaption ) {
return `${ text }\n\n${ trimmedCaption }`;
}
return text || trimmedCaption || '📷 (image omitted)';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: this '📷 (image omitted)' placeholder lands directly in the mobile chat as user-visible text. The rest of the file (and surrounding poll-loop strings like 🟢 Local agent attached.) doesn't use __() either, so this is consistent, but if/when this surface gets localized it's worth running these through @wordpress/i18n.

…respond-routing

# Conflicts:
#	apps/cli/remote-session/telegram-client.ts
#	apps/cli/remote-session/tests/telegram-client.test.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants