@vgai/live 0.5.22 → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/{editor.d.ts → vgai-live/src/editor.d.ts} +5 -8
  3. package/dist/{editor.js → vgai-live/src/editor.js} +5 -15
  4. package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.d.ts +10 -1
  5. package/dist/{game-client → vgai-live/src/game-client}/client.d.ts +36 -6
  6. package/dist/{game-client → vgai-live/src/game-client}/client.js +80 -9
  7. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.d.ts +15 -2
  8. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.js +38 -3
  9. package/dist/{game-client → vgai-live/src/game-client}/types.d.ts +5 -5
  10. package/dist/{game-client → vgai-live/src/game-client}/wait-for.js +2 -2
  11. package/dist/{game.d.ts → vgai-live/src/game.d.ts} +1 -1
  12. package/dist/{index.d.ts → vgai-live/src/index.d.ts} +2 -2
  13. package/dist/{index.js → vgai-live/src/index.js} +2 -2
  14. package/dist/{recording.js → vgai-live/src/recording.js} +3 -10
  15. package/dist/{session.d.ts → vgai-live/src/session.d.ts} +0 -7
  16. package/dist/{session.js → vgai-live/src/session.js} +56 -19
  17. package/dist/{tools.d.ts → vgai-live/src/tools.d.ts} +1 -1
  18. package/dist/{tools.js → vgai-live/src/tools.js} +1 -1
  19. package/dist/vgai-sdk/src/editor/session-registry-format.d.ts +81 -0
  20. package/dist/vgai-sdk/src/editor/session-registry-format.js +158 -0
  21. package/package.json +3 -3
  22. package/src/editor.ts +5 -16
  23. package/src/game-client/bridge-transport.ts +14 -1
  24. package/src/game-client/client.ts +98 -9
  25. package/src/game-client/relay-transport.ts +46 -3
  26. package/src/game-client/types.ts +5 -5
  27. package/src/game-client/wait-for.ts +2 -2
  28. package/src/game.ts +1 -1
  29. package/src/index.ts +2 -2
  30. package/src/recording.ts +3 -10
  31. package/src/session.ts +67 -24
  32. package/src/tools.ts +1 -1
  33. /package/dist/{editor-document.d.ts → vgai-live/src/editor-document.d.ts} +0 -0
  34. /package/dist/{editor-document.js → vgai-live/src/editor-document.js} +0 -0
  35. /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.d.ts +0 -0
  36. /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.js +0 -0
  37. /package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.js +0 -0
  38. /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.d.ts +0 -0
  39. /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.js +0 -0
  40. /package/dist/{game-client → vgai-live/src/game-client}/errors.d.ts +0 -0
  41. /package/dist/{game-client → vgai-live/src/game-client}/errors.js +0 -0
  42. /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.d.ts +0 -0
  43. /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.js +0 -0
  44. /package/dist/{game-client → vgai-live/src/game-client}/failure-block.d.ts +0 -0
  45. /package/dist/{game-client → vgai-live/src/game-client}/failure-block.js +0 -0
  46. /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.d.ts +0 -0
  47. /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.js +0 -0
  48. /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.d.ts +0 -0
  49. /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.js +0 -0
  50. /package/dist/{game-client → vgai-live/src/game-client}/index.d.ts +0 -0
  51. /package/dist/{game-client → vgai-live/src/game-client}/index.js +0 -0
  52. /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.d.ts +0 -0
  53. /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.js +0 -0
  54. /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.d.ts +0 -0
  55. /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.js +0 -0
  56. /package/dist/{game-client → vgai-live/src/game-client}/state-cap.d.ts +0 -0
  57. /package/dist/{game-client → vgai-live/src/game-client}/state-cap.js +0 -0
  58. /package/dist/{game-client → vgai-live/src/game-client}/types.js +0 -0
  59. /package/dist/{game-client → vgai-live/src/game-client}/wait-for.d.ts +0 -0
  60. /package/dist/{game.js → vgai-live/src/game.js} +0 -0
  61. /package/dist/{lazy-proxy.d.ts → vgai-live/src/lazy-proxy.d.ts} +0 -0
  62. /package/dist/{lazy-proxy.js → vgai-live/src/lazy-proxy.js} +0 -0
  63. /package/dist/{recording.d.ts → vgai-live/src/recording.d.ts} +0 -0
  64. /package/dist/{singleton.d.ts → vgai-live/src/singleton.d.ts} +0 -0
  65. /package/dist/{singleton.js → vgai-live/src/singleton.js} +0 -0
@@ -102,6 +102,17 @@ export class RelayTransport implements BridgeTransport {
102
102
  this.instance = opts.instance;
103
103
  }
104
104
 
105
+ /**
106
+ * Deliberately NOT guarded against a page-fallback answer the way the browser
107
+ * (`packages/editor/src/editor-server-response.ts`) and the CLI client
108
+ * (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
109
+ * at an arbitrary URL — a share tunnel, a static host — where a `200
110
+ * text/html` for an unserved route is real. This `baseUrl` is
111
+ * `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
112
+ * PROVED that port is this project's editor server (`probeServedProject`, whose
113
+ * own unparseable-answer path is exactly the refusal): a foreign occupant is
114
+ * refused there, before any command reaches here.
115
+ */
105
116
  private async postCommand(
106
117
  body: Record<string, unknown>,
107
118
  timeoutMs: number,
@@ -176,7 +187,7 @@ export class RelayTransport implements BridgeTransport {
176
187
  *
177
188
  * It runs on every leg, not just `call('snapshot')`, because that is where
178
189
  * the measured gap was: a bot that drives the game with `hold`/`command` and
179
- * reads through them — the shape `npm run playtest` actually has — could run
190
+ * reads through them could run
180
191
  * its entire session against a backgrounded tab and never be told, so a
181
192
  * later failure read as a generic relay timeout instead of "your tab is
182
193
  * hidden". Returns whether the tab is hidden so `call` can decide whether to
@@ -189,7 +200,14 @@ export class RelayTransport implements BridgeTransport {
189
200
  // P21: what was measured, and what this run is doing about it — not a
190
201
  // claim about where the tab is. The reading is a `presence` snapshot
191
202
  // (`isHidden` below), which ages between the tab's own reports.
192
- process.stdout.write(
203
+ // STDERR, not stdout. `vgai status` and every other `--json`-shaped verb
204
+ // put their PAYLOAD on stdout and every banner on stderr; this notice went
205
+ // to stdout and prepended a prose sentence to the JSON, so any machine
206
+ // consumer piping `vgai status` into a parser got a SyntaxError the moment
207
+ // the tab happened to be hidden — measured while reading the coverage
208
+ // table on a backgrounded session. A diagnostic that breaks the payload it
209
+ // annotates is worse than no diagnostic.
210
+ process.stderr.write(
193
211
  'vgai: the editor page last REPORTED document.visibilityState "hidden" — the engine ' +
194
212
  'stops its loop while the page reports itself hidden, so this run drives ' +
195
213
  'deterministic runTicks through the session relay instead of wall clock. Sim time ' +
@@ -302,7 +320,7 @@ export class RelayTransport implements BridgeTransport {
302
320
  }
303
321
 
304
322
  /**
305
- * Wave-2: ships `src` (`step.toString()`) to the editor dev server's
323
+ * Ships `src` (`step.toString()`) to the editor dev server's
306
324
  * `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
307
325
  * above), not a `bridge-call` method (see `command-listener.ts`'s
308
326
  * `handlePageScript` doc comment for why). `step` itself is unused on this
@@ -327,6 +345,31 @@ export class RelayTransport implements BridgeTransport {
327
345
  }
328
346
  }
329
347
 
348
+ /** THE MODULE LANE over the relay — same wire shape as `page-script`. */
349
+ async runGameScript(
350
+ src: string,
351
+ _step: (scope: unknown) => unknown,
352
+ instance?: string,
353
+ ): Promise<BridgeCallOutcome> {
354
+ try {
355
+ const body = await this.postCommand(
356
+ { type: 'game-eval', src, ...(instance === undefined ? {} : { instance }) },
357
+ PAGE_SCRIPT_TIMEOUT_MS,
358
+ );
359
+ return this.toBridgeOutcome(body);
360
+ } catch (err) {
361
+ return {
362
+ ok: false,
363
+ error: {
364
+ code: 'RELAY_UNREACHABLE',
365
+ message:
366
+ `vgai: could not reach the editor dev server relay at ${this.baseUrl} — ` +
367
+ `${err instanceof Error ? err.message : String(err)}`,
368
+ },
369
+ };
370
+ }
371
+ }
372
+
330
373
  /**
331
374
  * P20 — order the tab to reload, then wait for EVIDENCE that it came back.
332
375
  *
@@ -45,22 +45,22 @@ export interface DebugBridgeInput {
45
45
  setVirtualAction(action: string, value: VirtualActionValue): VirtualActionResult;
46
46
  tapVirtualAction(action: string): VirtualActionResult;
47
47
  clearVirtualActions(): void;
48
- /** D15/T-D15.5 — schedule a virtual actuation for a specific tick, applied
49
- * at the start of that tick's input phase. Declared here (Wave-2
50
- * bridge↔wire coverage-parity gate) for type-shape completeness with
48
+ /** Schedule a virtual actuation for a specific tick, applied
49
+ * at the start of that tick's input phase. Declared here for the
50
+ * bridge↔wire coverage-parity gate type-shape completeness with
51
51
  * `runtime/debug-bridge.ts`'s `VgaiDebugInputHandle` — this package still
52
52
  * exposes no client-side convenience wrapper around it (deliberately
53
53
  * parked; see `GameInput` in `client.ts`), this is pure type-shape
54
54
  * mirroring. */
55
55
  scheduleActionAtTick(tick: number, action: string, value: VirtualActionValue): void;
56
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
56
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
57
57
  * `VgaiDebugInputHandle.injectPointerDelta`: accumulates a synthetic
58
58
  * pointer delta for a named test source (sums within a frame, clears each
59
59
  * frame). Declared here for type-shape completeness with the bridge, same
60
60
  * precedent as `scheduleActionAtTick` above — no client-side convenience
61
61
  * wrapper in `client.ts` (deliberately parked). */
62
62
  injectPointerDelta(sourceId: string, delta: { x: number; y: number }): void;
63
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
63
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
64
64
  * `VgaiDebugInputHandle.injectPointerPosition`: sets a synthetic absolute
65
65
  * pointer position for a named test source (last-write-wins, persists
66
66
  * until changed). Same type-shape-only precedent as `scheduleActionAtTick`. */
@@ -168,7 +168,7 @@ export interface WaitForClock {
168
168
  }
169
169
 
170
170
  // ---------------------------------------------------------------------------
171
- // Fixture heartbeat (Wave-6 findings ledger: "Watchdog sim-awareness / fixture
171
+ // Fixture heartbeat ("Watchdog sim-awareness / fixture
172
172
  // heartbeat for long silent tests" — Session C hit exit-5 on the runner's
173
173
  // 2x90s stdout-liveness watchdog during a legitimately silent 5-minute test).
174
174
  //
@@ -179,7 +179,7 @@ export interface WaitForClock {
179
179
  // advancing test can never false-wedge regardless of duration, no matter how
180
180
  // long a single `simSeconds` budget runs.
181
181
  //
182
- // INVARIANT (tested below and in fixture.test.ts): a heartbeat requires BOTH
182
+ // INVARIANT (tested below): a heartbeat requires BOTH
183
183
  // (a) >= HEARTBEAT_INTERVAL_MS of wall time since the last heartbeat, AND
184
184
  // (b) the tick has ADVANCED since the last heartbeat (not merely since the
185
185
  // last poll). Without (b), the poll loop itself — which keeps running
package/src/game.ts CHANGED
@@ -22,7 +22,7 @@ import { GameClient, RelayTransport } from './game-client/index.js';
22
22
  /** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
23
23
  * it drives. `id` is the whole point of the handle to a CALLER: it is what
24
24
  * `tools.run(name, args, { instance: handle.id })` passes to scope a
25
- * node-hosted tool (e.g. `project.autoplay`) to this seat — the two-seat
25
+ * node-hosted tool to this seat — the two-seat
26
26
  * multiplayer verification pattern. Without it the caller has a client it can
27
27
  * drive but no id to hand a tool, so the seat-scoped path silently degrades to
28
28
  * the sole-instance one. */
package/src/index.ts CHANGED
@@ -34,8 +34,8 @@
34
34
  * and driven through the product. It is scoped to the ACTIVE document and
35
35
  * refuses anything outside it by name (`editor-document.ts`).
36
36
  *
37
- * `page(step)` = `GameClient.page(step)` (Wave-2's playwright-shim surface,
38
- * PR #166) — write `step` as a literal `async (page) => {...}` and inline
37
+ * `page(step)` = `GameClient.page(step)`, the playwright-shim surface
38
+ * write `step` as a literal `async (page) => {...}` and inline
39
39
  * every value it needs. KNOWN WIRE LIMITATION: under the relay transport this
40
40
  * binds to, `step` is shipped to the editor dev server as `step.toString()`
41
41
  * and reconstructed there — closures over outer variables do NOT survive that
package/src/recording.ts CHANGED
@@ -54,17 +54,10 @@ export class LiveGameplayRecording {
54
54
  await copyFile(capture.path, path);
55
55
  }
56
56
  return {
57
+ ...capture,
58
+ // The caller's `destination` copy, when there was one — otherwise the
59
+ // project path the recorder wrote. Spread first so this wins.
57
60
  path,
58
- startedAt: capture.startedAt,
59
- mimeType: capture.mimeType,
60
- width: capture.width,
61
- height: capture.height,
62
- fps: capture.fps,
63
- audio: capture.audio,
64
- durationMs: capture.durationMs,
65
- droppedFrames: capture.droppedFrames,
66
- frameErrors: capture.frameErrors,
67
- layers: capture.layers,
68
61
  };
69
62
  }
70
63
  }
package/src/session.ts CHANGED
@@ -54,6 +54,7 @@ import {
54
54
  HttpEditorTransport,
55
55
  withTimeout,
56
56
  } from '@vgai/sdk';
57
+ import { servedProjectAnswer } from '../../vgai-sdk/src/editor/session-registry-format';
57
58
 
58
59
  /**
59
60
  * Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
@@ -144,16 +145,6 @@ function readProjectSession(projectRoot: string): ProjectSessionHint | null {
144
145
  * but none open this project)" — sending the operator to start an editor that
145
146
  * was already running, with the real defect (their own manifest) never named.
146
147
  */
147
- function servedProject(body: unknown): { path: string | null; manifestError: string | null } {
148
- const b = body as {
149
- project?: { path?: string } | null;
150
- serving?: { path?: string; error?: string } | null;
151
- };
152
- return {
153
- path: b.project?.path ?? b.serving?.path ?? null,
154
- manifestError: b.project ? null : (b.serving?.error ?? null),
155
- };
156
- }
157
148
 
158
149
  /**
159
150
  * What the server on `port` says it serves — `undefined` when it did not
@@ -168,7 +159,7 @@ async function probeServedProject(
168
159
  signal: AbortSignal.timeout(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
169
160
  });
170
161
  if (!response.ok) return undefined;
171
- return servedProject(await response.json());
162
+ return servedProjectAnswer(await response.json());
172
163
  } catch {
173
164
  return undefined;
174
165
  }
@@ -200,6 +191,62 @@ function manifestRefusal(projectRoot: string, manifestError: string): Error {
200
191
  * silently attaches to an unrelated project's session — see the module doc
201
192
  * above) when no vgai.project.json is found, or no live session covers it.
202
193
  */
194
+ /**
195
+ * THE REFUSAL WHEN NOTHING MATCHED — and it says WHICH nothing.
196
+ *
197
+ * "No live editor session found … run `vgai edit`" used to be the answer to
198
+ * four different states, only one of which it described. The other three sent
199
+ * the operator to start an editor that was already running:
200
+ *
201
+ * - discovery FAILED (a probe timeout under load) — nothing was learned, so
202
+ * "no session is running" is not a fact anyone established;
203
+ * - the registry was read and is genuinely empty — the one case the old text
204
+ * was right about;
205
+ * - sessions exist, but every one resolves to a different canonical path. In
206
+ * a repo worked through git worktrees this is the ORDINARY miss: two
207
+ * checkouts of the same project differ only in a path prefix, and a
208
+ * symlinked worktree's `realpath` diverges from the path the caller typed.
209
+ * Naming both sides is what makes it a two-second diagnosis instead of a
210
+ * hunt.
211
+ *
212
+ * Each branch prescribes only what its own state supports.
213
+ */
214
+ function noMatchingSessionRefusal(
215
+ projectRoot: string,
216
+ canon: string,
217
+ sessions: readonly EditorSessionInfo[],
218
+ discoveryFailure: string | null,
219
+ ): Error {
220
+ if (discoveryFailure !== null) {
221
+ return new Error(
222
+ `@vgai/live: could not READ the editor session registry while looking for ${projectRoot} — ` +
223
+ `${discoveryFailure}. This is not the answer "no editor is running": the question went ` +
224
+ 'unanswered, so nothing is known about what is live. Retry (a probe can time out while ' +
225
+ 'the box is loaded); if it keeps failing, `vgai sessions` asks the same question directly.',
226
+ );
227
+ }
228
+ if (sessions.length === 0) {
229
+ return new Error(
230
+ `@vgai/live: the editor session registry is readable and lists NO live sessions, so none ` +
231
+ `covers ${projectRoot}. @vgai/live only attaches to an already-running session — it ` +
232
+ 'never starts one — so run `vgai edit` in that project first, then retry.',
233
+ );
234
+ }
235
+ const listed = sessions
236
+ .map((s) => ` port ${s.port} → ${s.project === null ? '(no project)' : s.project}`)
237
+ .join('\n');
238
+ return new Error(
239
+ `@vgai/live: ${sessions.length} live editor session(s) are running, but none of them opens ` +
240
+ `${projectRoot}. @vgai/live never silently attaches to a different project.\n` +
241
+ ` looking for (resolved): ${canon}\n` +
242
+ ` live sessions:\n${listed}\n` +
243
+ ' If one of those is meant to be this project, the two paths differ after resolution — ' +
244
+ 'the usual cause is a git worktree or a symlink, where the session was opened through a ' +
245
+ 'different path to the same files. Run `vgai edit` from THIS path, or use the path the ' +
246
+ 'session lists.',
247
+ );
248
+ }
249
+
203
250
  export async function resolveSession(
204
251
  projectDir: string = process.cwd(),
205
252
  deps: SessionResolutionDeps = {},
@@ -239,30 +286,26 @@ export async function resolveSession(
239
286
  }
240
287
 
241
288
  let sessions: EditorSessionInfo[];
289
+ // A FAILED discovery is not an empty one. Collapsing the two into `[]` is
290
+ // what made this door answer "no live editor session found" — and prescribe
291
+ // `vgai edit` — for a probe that merely timed out under load, sending the
292
+ // operator to start an editor that was already running while the real defect
293
+ // went unnamed. The same collapse the manifest refusal above was added for.
294
+ let discoveryFailure: string | null = null;
242
295
  try {
243
296
  sessions = await withTimeout(
244
297
  transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS),
245
298
  EDITOR_SESSION_DISCOVERY_TIMEOUT_MS,
246
299
  'editor session discovery',
247
300
  );
248
- } catch {
301
+ } catch (err) {
249
302
  sessions = [];
303
+ discoveryFailure = err instanceof Error ? err.message : String(err);
250
304
  }
251
305
 
252
306
  const canon = canonicalPath(projectRoot);
253
307
  const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
254
- if (!match) {
255
- const otherCount = sessions.length;
256
- throw new Error(
257
- `@vgai/live: no live editor session found for ${projectRoot}. @vgai/live only attaches to ` +
258
- 'an already-running session — it never starts one — so run `vgai edit` in that project ' +
259
- 'first, then retry.' +
260
- (otherCount > 0
261
- ? ` (${otherCount} other live session(s) found, but none open this project — @vgai/live ` +
262
- 'never silently attaches to a different project.)'
263
- : ''),
264
- );
265
- }
308
+ if (!match) throw noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure);
266
309
 
267
310
  if (match.manifestError != null) throw manifestRefusal(projectRoot, match.manifestError);
268
311
 
package/src/tools.ts CHANGED
@@ -31,7 +31,7 @@ export class LiveTools {
31
31
  *
32
32
  * `instance` names WHICH mounted instance the tool should drive when several
33
33
  * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
34
- * and a tool that drives the game (e.g. `project.autoplay`) binds
34
+ * and a tool that drives the game binds
35
35
  * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
36
36
  * the tool then targets the sole live instance, exactly as before.
37
37
  */
File without changes