fix(runner): fail loudly when no game client is attached; document starting one

Following the README, `bun bots/<name>/script.ts` with no browser tab or lite
runner attached printed "Connected", ran every action against nothing, and
exited 0. runScript never launches a client (autoLaunchBrowser: false), and
the README said the SDK would.

There are two ways that happened:
- No client ever attached: the gateway has no state, connect() times out its
  ready wait and then "continues anyway".
- A client attached earlier but has gone: the gateway replays the state it
  cached, so the ready check passes on a snapshot minutes old.

runScript now waits up to 10s for `tick` to advance after connecting. It
compares ticks, not getStateAge(), because that mixes gateway and local
clocks; a live run measured -343ms. If no tick arrives it throws an error
that names both ways to start a client, and a failed connect now sets
exitCode 1.

The README Getting Started and CLAUDE.md now include the lite runner step,
the README no longer claims scripts launch a client, and the CLAUDE.md
troubleshooting entry no longer points at an option runScript doesn't expose.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
max bittkerandClaude Opus 5.5 committed 2026-10-06 21:16:33 -04:00
1 parent a132676216
commit 1ea0edff1f
3 files changed
+78 -4

No files matched your search

+16 -1
View File
@@ -29,6 +29,21 @@ This automatically creates:
- `bots/{username}/lab_log.md` - Session notes template
- `bots/{username}/script.ts` - Ready-to-run starter script
### Start a Game Client (scripts only)
Scripts (`bun bots/{username}/*.ts`) and `bun sdk/cli.ts` need a running game
client; they never start one. Before running a script, start the headless lite
client in the background and leave it running:
```bash
cd server/webclient && bun install && bun src/lite/runner.ts {username}
```
It logs `Gateway connected, registering as '{username}'` once it's ready. One
process per bot; it exits non-zero if the game session ends. Without it, a
script exits with `No game client is attached`. MCP `execute_code` doesn't need
this step; it opens a browser client itself.
### Quick Start
1. Install dependencies: `bun install` (from project root)
@@ -326,7 +341,7 @@ wiki/
## Troubleshooting
**"No state received"** - Bot isn't connected to game. Open browser first or use `autoLaunchBrowser: true`.
**"No game client is attached" / "No state received"** - No client is publishing state for the bot. Start the lite client (`cd server/webclient && bun src/lite/runner.ts {username}`) or open the bot's browser URL, then rerun.
**Script stalls** - Check for open dialogs (`state.dialog.isOpen`). Level-ups block everything.
+16 -3
View File
@@ -38,9 +38,22 @@ Manually:
```sh
bun install
bun bots/create-bot.ts {username}
bun bots/{username}/script.ts
```
A script drives a running game client; it does not start one. Start the
headless lite client in its own terminal and leave it running:
```sh
cd server/webclient && bun install && bun src/lite/runner.ts {username}
```
(Or open `https://rs-sdk-demo.fly.dev/bot?bot={username}&password={PASSWORD}`
in a browser, with the password from `bots/{username}/bot.env`.)
Then, from the repo root:
```sh
bun bots/{username}/script.ts
```
Without a client the script exits with `No game client is attached`.
## Agent API and knowledge
The exact generated API surface is documented in
@@ -80,9 +93,9 @@ Once connected to the gateway, the botclient relays game state to the SDK and
dispatches low-level actions such as `sendWalk(x, z)` from the SDK. Dispatch
success is not confirmation that the game server applied the intended effect.
This means that the SDK can't talk directly to the game server, but must go through the botclient. It will attempt to launch the botclient on startup if one is not already running.
This means that the SDK can't talk directly to the game server, but must go through a client: the browser botclient or the headless lite client (`server/webclient/src/lite`). The MCP server opens a browser tab if none is running; standalone `runScript` scripts never launch one and exit with an error when no client is attached.
You don't need to run the gateway/botclient in order to run automations against the demo server, but you may choose to if you are fixing bugs or adding features to the rs-sdk project
You don't need to run your own gateway or web client bundler in order to run automations against the demo server, but you may choose to if you are fixing bugs or adding features to the rs-sdk project
## Running the server locally
+46
View File
@@ -190,6 +190,7 @@ async function getOrCreateConnection(): Promise<BotConnection> {
try {
await Promise.race([sdk.connect(), timeoutPromise]);
await requireLiveClient(sdk, username, server);
} catch (error) {
// The BotSDK was never registered in `connections`, so nothing else will
// ever tear it down - and with autoReconnect on, its reconnect timers
@@ -209,6 +210,49 @@ async function getOrCreateConnection(): Promise<BotConnection> {
return connection;
}
/** How long to wait for a fresh state frame before deciding no client is attached. */
const LIVE_CLIENT_WAIT_MS = 10_000;
/**
* Throw unless a game client (browser tab or lite runner) is publishing state
* for this bot right now.
*
* connect() succeeds against the gateway alone, so without this check a script
* with no client "connects" and every action silently does nothing. Two shapes:
* the gateway has no state at all (no client ever attached; connect() already
* waited out its ready timeout), or it replays the last state it cached before
* the client left, which passes the ready check. A live client advances
* `tick` every game tick, so wait for one - this compares ticks rather than
* `getStateAge()`, which mixes gateway and local clocks.
*/
async function requireLiveClient(sdk: BotSDK, username: string, server: string | undefined): Promise<void> {
const cached = sdk.getState();
if (cached) {
try {
await sdk.waitForCondition(s => s.tick !== cached.tick, LIVE_CLIENT_WAIT_MS);
return;
} catch {}
}
const why = cached
? `its last state is ${Math.round(sdk.getStateAge() / 1000)}s old and no new tick arrived in ${LIVE_CLIENT_WAIT_MS / 1000}s`
: 'no game state was received';
// Same derivation as BotSDK's browser launch URL: SERVER may be a bare
// host or a ws(s):// gateway URL.
let origin = 'http://localhost:8888';
if (server && !server.startsWith('localhost') && !server.startsWith('127.')) {
origin = /^wss?:\/\//.test(server)
? server.replace(/^ws/, 'http').replace(/\/gateway$/, '')
: `https://${server}`;
}
throw new Error(
`No game client is attached to "${username}" (${why}).\n` +
`Scripts drive a running client; they don't start one. Start one, leave it running, then rerun:\n` +
` headless: cd server/webclient && bun src/lite/runner.ts ${username}\n` +
` browser: ${origin}/bot?bot=${encodeURIComponent(username)}&password=<PASSWORD from bot.env>`
);
}
// ============ Core Runner ============
/**
@@ -285,6 +329,8 @@ export async function runScript(
}
} catch (error: any) {
console.error(`[Runner] Failed to connect: ${error?.message ?? error}`);
// Same as a script error below: `bun script.ts` must exit non-zero.
process.exitCode = 1;
return {
success: false,
error,