Skip to content

feat(studio-bridge): add persistent sessions with v2 protocol#669

Open
Quenty wants to merge 92 commits intomainfrom
feat/studio-bridge
Open

feat(studio-bridge): add persistent sessions with v2 protocol#669
Quenty wants to merge 92 commits intomainfrom
feat/studio-bridge

Conversation

@Quenty
Copy link
Owner

@Quenty Quenty commented Feb 24, 2026

Summary

Adds persistent sessions to studio-bridge. The plugin now stays running in Roblox Studio and maintains a WebSocket connection to the bridge server, replacing the one-shot launch-execute-exit workflow. This enables multiple concurrent Studio instances, structured commands (exec, query, state, logs, screenshot), split-server mode for devcontainers, host failover, and MCP integration for AI agents.

  • v2 protocol: Typed message codec with handshake, capability negotiation, and request/response correlation
  • Plugin modules: Discovery state machine, action router, message buffer, and execute handler — all pure-logic Luau with Lune tests (118 tests)
  • Bridge network: BridgeConnection entry point with automatic host/client role detection, session tracking, and health endpoint
  • Failover: HandOffManager detects host crashes and promotes standby clients with in-flight request recovery
  • Commands: 14 command handlers (sessions, state, exec, run, query, logs, screenshot, serve, launch, connect, disconnect, install-plugin, uninstall-plugin, mcp) with shared CLI utilities
  • MCP server: Generic tool adapter that wraps command handlers as MCP tools via @modelcontextprotocol/sdk
  • Tests: 545 TypeScript tests (45 files) + 118 Lune tests, including E2E suites for persistent sessions, split-server, and failover

Also includes cli-output-helpers package for structured output (table, JSON, watch renderers).

Test plan

  • All 545 TypeScript tests pass (npx vitest run)
  • All 118 Lune plugin tests pass
  • Manual: install plugin into Studio, verify it discovers the bridge server
  • Manual: run studio-bridge sessions and verify connected instance appears
  • Manual: verify plugin survives Play/Stop transitions without disconnecting

PRD, 9 tech specs, 9 execution phases with agent prompts and validation
criteria, and research notes for evolving studio-bridge from a
launch-per-session tool into a persistent session system.
…ture

Cover persistent plugin, host/client topology, session management,
all 13 CLI commands, MCP server integration, v2 WebSocket protocol,
plugin discovery, and programmatic API.
…nnect

Health endpoint returns 503 and /plugin rejects WebSocket upgrades when
the host is shutting down, preventing the plugin from rediscovering and
reconnecting to a host that's about to close.
Surface plugin error code and message instead of generic "Unexpected
response type" errors. Increase shutdown drain from 100ms to 250ms to
give the plugin more time to close its WebSocket cleanly.
- Fix execute field name mismatch: read payload.script (spec) with
  fallback to payload.code (backward compat)
- Add QueryStateAction: returns Studio run mode, placeId, placeName, gameId
- Add QueryLogsAction: reads from MessageBuffer with direction, count,
  level filtering, and includeInternal support
- Add CaptureScreenshotAction stub: returns CAPABILITY_NOT_SUPPORTED
- Add SubscribeAction stubs: echo back requested events for subscribe
  and unsubscribe (actual event push not yet implemented)
- Wire all handlers into StudioBridgePlugin.server.lua
- Update capabilities list to include all supported actions
- Fix resolveSession test timeout (waitForSession adds 5s)
…+ EditableImage

Captures the Studio viewport using CaptureService:CaptureScreenshot(),
loads the temp content into an EditableImage, scales down if larger than
1024x1024, reads RGBA pixels via ReadPixelsBuffer, base64-encodes with
an optimized buffer-based encoder, and sends over WebSocket. The CLI
converts RGBA to PNG using a minimal zlib-based encoder before saving.

Also fixes response matching in sendToPluginAsync to normalize empty
requestId as absent, matching the plugin convention.
Vendors png-luau v0.2.1 single-file bundle and uses it to compress RGBA
pixel data into PNG before base64-encoding and sending over WebSocket.
Reduces wire bandwidth ~3x (2.2MB → 712KB for a 1024x411 screenshot).
Falls back to raw RGBA if the PNG encoder is unavailable (Lune tests).
- Add GUID nonce to unpublished place session IDs for uniqueness
- Handle duplicate sessionId in bridge-host by closing old connection
- Replace Heartbeat tick loop with task.spawn polling loop that threads
  remaining execution time through all async calls
- Simplify DiscoveryStateMachine: remove backoff/reconnecting state,
  always poll at consistent interval, rename deadlineSec to
  remainingExecTimeSec
- Fix waitForSessionsToSettleAsync cleanup bug (closures captured stale
  done reference)
- Add waitForSessions option to BridgeConnection.connectAsync
Extract withSessionAsync/withConnectionAsync and addSessionOptions into
with-connection.ts to eliminate repeated connect/resolve/disconnect
boilerplate across all 7 CLI commands.
…lution

Move LogService.MessageOut listener to top-level scope in the plugin so
logs are captured from the moment the plugin loads, not only after a
WebSocket connects. Use a hybrid clock (os.time + os.clock delta) for
correct wall-clock timestamps with sub-second precision, and fix the CLI
to convert epoch seconds to milliseconds for Date parsing.

Hoist the wait-for-sessions logic to the top of resolveSessionAsync so
--session and --instance targeting also wait for persistent plugins to
discover the host before performing lookups.
The error listed instance IDs but suggested --session, so users would
copy an instance ID into --session and get a not-found error. List
session IDs instead so they can be copied directly.
…exts

Use task.defer instead of task.spawn to resume the caller thread in
scanPortsAsync, preventing "cannot resume non-suspended coroutine"
errors. Skip plugin initialization in client/server play-mode contexts
since HttpService is restricted to the game server.

Increase session settle timeout from 2.5s to 4s to allow more time for
persistent plugins to discover the ephemeral host.
…ands

Replace 13 flat CLI commands with a declarative defineCommand() system
that drives CLI, MCP, and terminal from a single source per command.
Commands are now grouped (console, explorer, viewport, process, plugin)
and co-located with their Luau action files, which are pushed to the
plugin dynamically over the wire instead of bundled statically.
…d watch mode

Wire the --format, --output, --open, --watch, and --interval flags that
were declared but ignored in the CLI adapter. Restructure the handler
into composable helpers (extractCommandArgs, executeCommandAsync,
formatForOutput, outputResult) and add rich text formatters to all six
commands: exec (output lines), logs (timestamped colored entries), list
(session table), info (key-value state), query (indented tree), and
screenshot (summary text + binary file writes via binaryField).
@Quenty Quenty deployed to integration February 26, 2026 01:04 — with GitHub Actions Active
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant