@vgai/live 0.5.21 → 0.5.23

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 +1 -1
  5. package/dist/{game-client → vgai-live/src/game-client}/client.d.ts +4 -6
  6. package/dist/{game-client → vgai-live/src/game-client}/client.js +6 -8
  7. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.d.ts +13 -2
  8. package/dist/{game-client → vgai-live/src/game-client}/relay-transport.js +21 -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 +1 -1
  24. package/src/game-client/client.ts +6 -8
  25. package/src/game-client/relay-transport.ts +21 -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
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (Wave 2). Methods are named after the
3
+ * page }`. Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
5
  * separate `selectAll`, `showPanel('viewport-play')` rather than
6
6
  * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
9
  * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
- * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
- * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
10
+ * hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
11
+ * escape hatch: every method goes over the wire.
12
12
  */
13
13
  import type { ActiveDocumentCapture, AnimationCaptureAction, AssetKind, AssetPreviewCapture, AssetPreviewOptions, AssetPreviewSource, EditorClient, EditorState, EditorView, HistoryStep, InspectedFieldWrite, InspectedHierarchy, InspectedInspection, OpenedDocument, PresentedEditorView, RagdollGenerationResult, ShadingMode, ViewPreset, ViewportCapture } from '@vgai/editor-sdk';
14
14
  import { LiveEditorDocument } from './editor-document.js';
@@ -40,11 +40,8 @@ export declare class LiveEditor {
40
40
  constructor(client: EditorClient);
41
41
  /**
42
42
  * The active authoring adapter's persistence destination — where a save would
43
- * land (`status().savePath`).
44
- *
45
- * WO-8: this also took a `path` and OPENED that scene through
46
- * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
47
- * (the `.vscn.json` format is deleted), so only the read remains.
43
+ * land (`status().savePath`). A read only: a three root has no scene document
44
+ * to open, and its root is activated instead.
48
45
  */
49
46
  scene(): Promise<string | null>;
50
47
  /**
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * `LiveEditor` — the editor-control half of `@vgai/live`'s `{ editor, game,
3
- * page }` (Wave 2). Methods are named after the
3
+ * page }`. Methods are named after the
4
4
  * ACTION, not the CLI flag spelling (e.g. `select('all')` rather than a
5
5
  * separate `selectAll`, `showPanel('viewport-play')` rather than
6
6
  * `vgai show viewport play`'s two-token shape) — see each method's own doc
7
7
  * comment for the exact CLI verb / `EditorClient` call it mirrors.
8
8
  *
9
9
  * Every method reuses `@vgai/editor-sdk`'s `EditorClient` — this module never
10
- * hand-rolls a `fetch` to `/__editor/command` itself. (WO-8 removed the one
11
- * exception, `applyDiff`, which was FILE mode rather than a live wire command.)
10
+ * hand-rolls a `fetch` to `/__editor/command` itself, and has no FILE-mode
11
+ * escape hatch: every method goes over the wire.
12
12
  */
13
13
  import { LiveEditorDocument } from './editor-document.js';
14
14
  import { LiveGameplayRecording } from './recording.js';
@@ -72,11 +72,8 @@ export class LiveEditor {
72
72
  }
73
73
  /**
74
74
  * The active authoring adapter's persistence destination — where a save would
75
- * land (`status().savePath`).
76
- *
77
- * WO-8: this also took a `path` and OPENED that scene through
78
- * `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
79
- * (the `.vscn.json` format is deleted), so only the read remains.
75
+ * land (`status().savePath`). A read only: a three root has no scene document
76
+ * to open, and its root is activated instead.
80
77
  */
81
78
  async scene() {
82
79
  const state = await this.#client.getState();
@@ -98,13 +95,6 @@ export class LiveEditor {
98
95
  async captureActiveDocument(size) {
99
96
  return this.#client.captureActiveDocument(size);
100
97
  }
101
- /*
102
- * `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
103
- * twelve `.vscn` document tools deleted with the format, so there is nothing
104
- * left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
105
- * so did `projectRoot` — this was the ONE method that used it (every other
106
- * method goes over the wire).
107
- */
108
98
  async play(opts) {
109
99
  await this.#client.play(opts);
110
100
  }
@@ -52,7 +52,7 @@ export interface BridgeTransport {
52
52
  * blank/corrupt file. */
53
53
  screenshot(path: string): Promise<CaptureNotes>;
54
54
  /**
55
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
55
+ * One dialect, full capability — runs a UI-automation step
56
56
  * written as a literal `async (page) => {...}` (`GameClient.page()`,
57
57
  * `client.ts`). `src` is `step.toString()`; `step` is the ORIGINAL
58
58
  * function, wrapped so its own parameter type is erased to `unknown` (only
@@ -33,7 +33,7 @@ export declare class PageTransport implements BridgeTransport {
33
33
  * (`capture-notes.ts`) exists on this leg. `{}` says that honestly rather
34
34
  * than inventing a clean bill of health. */
35
35
  screenshot(path: string): Promise<CaptureNotes>;
36
- /** Wave-2: the ONE transport that runs a `game.page()` step against a REAL
36
+ /** The ONE transport that runs a `game.page()` step against a REAL
37
37
  * Playwright `Page` — no serialization, so `step`'s own closures work
38
38
  * here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
39
39
  * full honesty-boundary contract; `src` is unused on this leg, kept only
@@ -275,14 +275,12 @@ export declare class GameClient {
275
275
  * `events.expect`'s failure path each shoot a frame on their own
276
276
  * (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
277
277
  * observer may shoot one from inside a callback the driver never sees. The
278
- * only place that sees ALL of them is here, which is why an autoplay run
279
- * stamps its captures by listening rather than by intercepting
280
- * (`src/tools/route-context.ts` in a scaffolded project is the shipped
281
- * listener).
278
+ * only place that sees ALL of them is here, which is why a driving tool
279
+ * stamps its captures by listening rather than by intercepting.
282
280
  */
283
281
  onCapture(listener: CaptureListener): () => void;
284
282
  /**
285
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
283
+ * One dialect, full capability — runs a UI-automation step
286
284
  * written as a literal Playwright `async (page) => {...}` (interface
287
285
  * doctrine §3.2/§4 rung 4: "the AI should think it is basically just
288
286
  * executing Playwright"). Under `PageTransport` this drives the REAL
@@ -111,7 +111,7 @@ export class PageTransport {
111
111
  await this.page.screenshot({ path });
112
112
  return {};
113
113
  }
114
- /** Wave-2: the ONE transport that runs a `game.page()` step against a REAL
114
+ /** The ONE transport that runs a `game.page()` step against a REAL
115
115
  * Playwright `Page` — no serialization, so `step`'s own closures work
116
116
  * here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
117
117
  * full honesty-boundary contract; `src` is unused on this leg, kept only
@@ -529,7 +529,7 @@ export class GameClient {
529
529
  snapshot: () => this.snapshot(),
530
530
  now: () => Date.now(),
531
531
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
532
- // Fixture heartbeat (Wave-6 findings ledger) — real stdout, the
532
+ // Fixture heartbeat — real stdout, the
533
533
  // same channel a caller watching stdout already treats as
534
534
  // liveness (see wait-for.ts's module doc for the emission
535
535
  // invariant `maybeHeartbeat` enforces).
@@ -565,7 +565,7 @@ export class GameClient {
565
565
  const start = await this.snapshot();
566
566
  let lastTick = null;
567
567
  let stalledPolls = 0;
568
- // Fixture heartbeat (Wave-6 findings ledger) — same invariant as
568
+ // Fixture heartbeat — same invariant as
569
569
  // `wait-for.ts`'s `runWaitFor`: a heartbeat requires BOTH 60s of wall
570
570
  // silence AND the tick having advanced since the last one emitted, so a
571
571
  // genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
@@ -646,10 +646,8 @@ export class GameClient {
646
646
  * `events.expect`'s failure path each shoot a frame on their own
647
647
  * (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
648
648
  * observer may shoot one from inside a callback the driver never sees. The
649
- * only place that sees ALL of them is here, which is why an autoplay run
650
- * stamps its captures by listening rather than by intercepting
651
- * (`src/tools/route-context.ts` in a scaffolded project is the shipped
652
- * listener).
649
+ * only place that sees ALL of them is here, which is why a driving tool
650
+ * stamps its captures by listening rather than by intercepting.
653
651
  */
654
652
  onCapture(listener) {
655
653
  this.#captureListeners.add(listener);
@@ -658,7 +656,7 @@ export class GameClient {
658
656
  };
659
657
  }
660
658
  /**
661
- * Wave-2 "one dialect, full capability" — runs a UI-automation step
659
+ * One dialect, full capability — runs a UI-automation step
662
660
  * written as a literal Playwright `async (page) => {...}` (interface
663
661
  * doctrine §3.2/§4 rung 4: "the AI should think it is basically just
664
662
  * executing Playwright"). Under `PageTransport` this drives the REAL
@@ -52,6 +52,17 @@ export declare class RelayTransport implements BridgeTransport {
52
52
  private hiddenCache;
53
53
  private readonly instance;
54
54
  constructor(opts: RelayTransportOptions);
55
+ /**
56
+ * Deliberately NOT guarded against a page-fallback answer the way the browser
57
+ * (`packages/editor/src/editor-server-response.ts`) and the CLI client
58
+ * (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
59
+ * at an arbitrary URL — a share tunnel, a static host — where a `200
60
+ * text/html` for an unserved route is real. This `baseUrl` is
61
+ * `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
62
+ * PROVED that port is this project's editor server (`probeServedProject`, whose
63
+ * own unparseable-answer path is exactly the refusal): a foreign occupant is
64
+ * refused there, before any command reaches here.
65
+ */
55
66
  private postCommand;
56
67
  /** Maps the relay's wire body back onto the transport-neutral
57
68
  * `BridgeCallOutcome` — `result` on success, `code`/`error`/the rest of
@@ -67,7 +78,7 @@ export declare class RelayTransport implements BridgeTransport {
67
78
  *
68
79
  * It runs on every leg, not just `call('snapshot')`, because that is where
69
80
  * the measured gap was: a bot that drives the game with `hold`/`command` and
70
- * reads through them — the shape `npm run playtest` actually has — could run
81
+ * reads through them could run
71
82
  * its entire session against a backgrounded tab and never be told, so a
72
83
  * later failure read as a generic relay timeout instead of "your tab is
73
84
  * hidden". Returns whether the tab is hidden so `call` can decide whether to
@@ -89,7 +100,7 @@ export declare class RelayTransport implements BridgeTransport {
89
100
  bringToFront(): Promise<void>;
90
101
  screenshot(path: string): Promise<CaptureNotes>;
91
102
  /**
92
- * Wave-2: ships `src` (`step.toString()`) to the editor dev server's
103
+ * Ships `src` (`step.toString()`) to the editor dev server's
93
104
  * `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
94
105
  * above), not a `bridge-call` method (see `command-listener.ts`'s
95
106
  * `handlePageScript` doc comment for why). `step` itself is unused on this
@@ -57,6 +57,17 @@ export class RelayTransport {
57
57
  this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
58
  this.instance = opts.instance;
59
59
  }
60
+ /**
61
+ * Deliberately NOT guarded against a page-fallback answer the way the browser
62
+ * (`packages/editor/src/editor-server-response.ts`) and the CLI client
63
+ * (`@vgai/editor-sdk`'s `EditorClient.readJson`) are. Those two can be pointed
64
+ * at an arbitrary URL — a share tunnel, a static host — where a `200
65
+ * text/html` for an unserved route is real. This `baseUrl` is
66
+ * `http://127.0.0.1:<port>` and nothing else, and `session.ts` has already
67
+ * PROVED that port is this project's editor server (`probeServedProject`, whose
68
+ * own unparseable-answer path is exactly the refusal): a foreign occupant is
69
+ * refused there, before any command reaches here.
70
+ */
60
71
  async postCommand(body, timeoutMs) {
61
72
  const res = await fetch(`${this.baseUrl}/__editor/command`, {
62
73
  method: 'POST',
@@ -119,7 +130,7 @@ export class RelayTransport {
119
130
  *
120
131
  * It runs on every leg, not just `call('snapshot')`, because that is where
121
132
  * the measured gap was: a bot that drives the game with `hold`/`command` and
122
- * reads through them — the shape `npm run playtest` actually has — could run
133
+ * reads through them could run
123
134
  * its entire session against a backgrounded tab and never be told, so a
124
135
  * later failure read as a generic relay timeout instead of "your tab is
125
136
  * hidden". Returns whether the tab is hidden so `call` can decide whether to
@@ -132,7 +143,14 @@ export class RelayTransport {
132
143
  // P21: what was measured, and what this run is doing about it — not a
133
144
  // claim about where the tab is. The reading is a `presence` snapshot
134
145
  // (`isHidden` below), which ages between the tab's own reports.
135
- process.stdout.write('vgai: the editor page last REPORTED document.visibilityState "hidden" the engine ' +
146
+ // STDERR, not stdout. `vgai status` and every other `--json`-shaped verb
147
+ // put their PAYLOAD on stdout and every banner on stderr; this notice went
148
+ // to stdout and prepended a prose sentence to the JSON, so any machine
149
+ // consumer piping `vgai status` into a parser got a SyntaxError the moment
150
+ // the tab happened to be hidden — measured while reading the coverage
151
+ // table on a backgrounded session. A diagnostic that breaks the payload it
152
+ // annotates is worse than no diagnostic.
153
+ process.stderr.write('vgai: the editor page last REPORTED document.visibilityState "hidden" — the engine ' +
136
154
  'stops its loop while the page reports itself hidden, so this run drives ' +
137
155
  'deterministic runTicks through the session relay instead of wall clock. Sim time ' +
138
156
  'advances either way; `vgai status` prints that reading with its age.\n');
@@ -228,7 +246,7 @@ export class RelayTransport {
228
246
  return notes;
229
247
  }
230
248
  /**
231
- * Wave-2: ships `src` (`step.toString()`) to the editor dev server's
249
+ * Ships `src` (`step.toString()`) to the editor dev server's
232
250
  * `page-script` op — a STANDALONE relay command (like `bridge-screenshot`
233
251
  * above), not a `bridge-call` method (see `command-listener.ts`'s
234
252
  * `handlePageScript` doc comment for why). `step` itself is unused on this
@@ -41,15 +41,15 @@ export interface DebugBridgeInput {
41
41
  setVirtualAction(action: string, value: VirtualActionValue): VirtualActionResult;
42
42
  tapVirtualAction(action: string): VirtualActionResult;
43
43
  clearVirtualActions(): void;
44
- /** D15/T-D15.5 — schedule a virtual actuation for a specific tick, applied
45
- * at the start of that tick's input phase. Declared here (Wave-2
46
- * bridge↔wire coverage-parity gate) for type-shape completeness with
44
+ /** Schedule a virtual actuation for a specific tick, applied
45
+ * at the start of that tick's input phase. Declared here for the
46
+ * bridge↔wire coverage-parity gate type-shape completeness with
47
47
  * `runtime/debug-bridge.ts`'s `VgaiDebugInputHandle` — this package still
48
48
  * exposes no client-side convenience wrapper around it (deliberately
49
49
  * parked; see `GameInput` in `client.ts`), this is pure type-shape
50
50
  * mirroring. */
51
51
  scheduleActionAtTick(tick: number, action: string, value: VirtualActionValue): void;
52
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
52
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
53
53
  * `VgaiDebugInputHandle.injectPointerDelta`: accumulates a synthetic
54
54
  * pointer delta for a named test source (sums within a frame, clears each
55
55
  * frame). Declared here for type-shape completeness with the bridge, same
@@ -59,7 +59,7 @@ export interface DebugBridgeInput {
59
59
  x: number;
60
60
  y: number;
61
61
  }): void;
62
- /** Wave-2 pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
62
+ /** Pointer-dispatch op — mirrors `runtime/debug-bridge.ts`'s
63
63
  * `VgaiDebugInputHandle.injectPointerPosition`: sets a synthetic absolute
64
64
  * pointer position for a named test source (last-write-wins, persists
65
65
  * until changed). Same type-shape-only precedent as `scheduleActionAtTick`. */
@@ -96,7 +96,7 @@ export class WaitForTimeoutError extends Error {
96
96
  }
97
97
  }
98
98
  // ---------------------------------------------------------------------------
99
- // Fixture heartbeat (Wave-6 findings ledger: "Watchdog sim-awareness / fixture
99
+ // Fixture heartbeat ("Watchdog sim-awareness / fixture
100
100
  // heartbeat for long silent tests" — Session C hit exit-5 on the runner's
101
101
  // 2x90s stdout-liveness watchdog during a legitimately silent 5-minute test).
102
102
  //
@@ -107,7 +107,7 @@ export class WaitForTimeoutError extends Error {
107
107
  // advancing test can never false-wedge regardless of duration, no matter how
108
108
  // long a single `simSeconds` budget runs.
109
109
  //
110
- // INVARIANT (tested below and in fixture.test.ts): a heartbeat requires BOTH
110
+ // INVARIANT (tested below): a heartbeat requires BOTH
111
111
  // (a) >= HEARTBEAT_INTERVAL_MS of wall time since the last heartbeat, AND
112
112
  // (b) the tick has ADVANCED since the last heartbeat (not merely since the
113
113
  // last poll). Without (b), the poll loop itself — which keeps running
@@ -20,7 +20,7 @@ import { GameClient } from './game-client/index.js';
20
20
  /** A `GameClient` addressing ONE mounted instance, tagged with the mount `id`
21
21
  * it drives. `id` is the whole point of the handle to a CALLER: it is what
22
22
  * `tools.run(name, args, { instance: handle.id })` passes to scope a
23
- * node-hosted tool (e.g. `project.autoplay`) to this seat — the two-seat
23
+ * node-hosted tool to this seat — the two-seat
24
24
  * multiplayer verification pattern. Without it the caller has a client it can
25
25
  * drive but no id to hand a tool, so the seat-scoped path silently degrades to
26
26
  * the sole-instance one. */
@@ -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
@@ -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
@@ -36,17 +36,10 @@ export class LiveGameplayRecording {
36
36
  await copyFile(capture.path, path);
37
37
  }
38
38
  return {
39
+ ...capture,
40
+ // The caller's `destination` copy, when there was one — otherwise the
41
+ // project path the recorder wrote. Spread first so this wins.
39
42
  path,
40
- startedAt: capture.startedAt,
41
- mimeType: capture.mimeType,
42
- width: capture.width,
43
- height: capture.height,
44
- fps: capture.fps,
45
- audio: capture.audio,
46
- durationMs: capture.durationMs,
47
- droppedFrames: capture.droppedFrames,
48
- frameErrors: capture.frameErrors,
49
- layers: capture.layers,
50
43
  };
51
44
  }
52
45
  }
@@ -76,11 +76,4 @@ export interface ProjectSessionHint {
76
76
  url: string;
77
77
  startedAt: string;
78
78
  }
79
- /**
80
- * Resolve `projectDir` (default `process.cwd()`) to the port of its already-
81
- * running `vgai edit` session. Throws a descriptive error (never hangs
82
- * indefinitely — bounded by `EDITOR_SESSION_DISCOVERY_TIMEOUT_MS`, and never
83
- * silently attaches to an unrelated project's session — see the module doc
84
- * above) when no vgai.project.json is found, or no live session covers it.
85
- */
86
79
  export declare function resolveSession(projectDir?: string, deps?: SessionResolutionDeps): Promise<ResolvedSession>;
@@ -47,6 +47,7 @@
47
47
  import { existsSync, readFileSync, realpathSync } from 'node:fs';
48
48
  import { dirname, join, resolve } from 'node:path';
49
49
  import { EDITOR_SESSION_DISCOVERY_TIMEOUT_MS, HttpEditorTransport, withTimeout, } from '@vgai/sdk';
50
+ import { servedProjectAnswer } from '../../vgai-sdk/src/editor/session-registry-format';
50
51
  /**
51
52
  * Lifted verbatim from `packages/vgai-cli/src/project-root.ts` (see this
52
53
  * module's doc comment above for why it's duplicated rather than imported).
@@ -106,13 +107,6 @@ function readProjectSession(projectRoot) {
106
107
  * but none open this project)" — sending the operator to start an editor that
107
108
  * was already running, with the real defect (their own manifest) never named.
108
109
  */
109
- function servedProject(body) {
110
- const b = body;
111
- return {
112
- path: b.project?.path ?? b.serving?.path ?? null,
113
- manifestError: b.project ? null : (b.serving?.error ?? null),
114
- };
115
- }
116
110
  /**
117
111
  * What the server on `port` says it serves — `undefined` when it did not
118
112
  * answer at all. One probe: the manifest failure arrives with the path, so
@@ -125,7 +119,7 @@ async function probeServedProject(port) {
125
119
  });
126
120
  if (!response.ok)
127
121
  return undefined;
128
- return servedProject(await response.json());
122
+ return servedProjectAnswer(await response.json());
129
123
  }
130
124
  catch {
131
125
  return undefined;
@@ -154,6 +148,50 @@ function manifestRefusal(projectRoot, manifestError) {
154
148
  * silently attaches to an unrelated project's session — see the module doc
155
149
  * above) when no vgai.project.json is found, or no live session covers it.
156
150
  */
151
+ /**
152
+ * THE REFUSAL WHEN NOTHING MATCHED — and it says WHICH nothing.
153
+ *
154
+ * "No live editor session found … run `vgai edit`" used to be the answer to
155
+ * four different states, only one of which it described. The other three sent
156
+ * the operator to start an editor that was already running:
157
+ *
158
+ * - discovery FAILED (a probe timeout under load) — nothing was learned, so
159
+ * "no session is running" is not a fact anyone established;
160
+ * - the registry was read and is genuinely empty — the one case the old text
161
+ * was right about;
162
+ * - sessions exist, but every one resolves to a different canonical path. In
163
+ * a repo worked through git worktrees this is the ORDINARY miss: two
164
+ * checkouts of the same project differ only in a path prefix, and a
165
+ * symlinked worktree's `realpath` diverges from the path the caller typed.
166
+ * Naming both sides is what makes it a two-second diagnosis instead of a
167
+ * hunt.
168
+ *
169
+ * Each branch prescribes only what its own state supports.
170
+ */
171
+ function noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure) {
172
+ if (discoveryFailure !== null) {
173
+ return new Error(`@vgai/live: could not READ the editor session registry while looking for ${projectRoot} — ` +
174
+ `${discoveryFailure}. This is not the answer "no editor is running": the question went ` +
175
+ 'unanswered, so nothing is known about what is live. Retry (a probe can time out while ' +
176
+ 'the box is loaded); if it keeps failing, `vgai sessions` asks the same question directly.');
177
+ }
178
+ if (sessions.length === 0) {
179
+ return new Error(`@vgai/live: the editor session registry is readable and lists NO live sessions, so none ` +
180
+ `covers ${projectRoot}. @vgai/live only attaches to an already-running session — it ` +
181
+ 'never starts one — so run `vgai edit` in that project first, then retry.');
182
+ }
183
+ const listed = sessions
184
+ .map((s) => ` port ${s.port} → ${s.project === null ? '(no project)' : s.project}`)
185
+ .join('\n');
186
+ return new Error(`@vgai/live: ${sessions.length} live editor session(s) are running, but none of them opens ` +
187
+ `${projectRoot}. @vgai/live never silently attaches to a different project.\n` +
188
+ ` looking for (resolved): ${canon}\n` +
189
+ ` live sessions:\n${listed}\n` +
190
+ ' If one of those is meant to be this project, the two paths differ after resolution — ' +
191
+ 'the usual cause is a git worktree or a symlink, where the session was opened through a ' +
192
+ 'different path to the same files. Run `vgai edit` from THIS path, or use the path the ' +
193
+ 'session lists.');
194
+ }
157
195
  export async function resolveSession(projectDir = process.cwd(), deps = {}) {
158
196
  const findRoot = deps.findProjectRootFrom ?? findProjectRootFrom;
159
197
  const transport = deps.transport ?? new HttpEditorTransport();
@@ -187,24 +225,23 @@ export async function resolveSession(projectDir = process.cwd(), deps = {}) {
187
225
  }
188
226
  }
189
227
  let sessions;
228
+ // A FAILED discovery is not an empty one. Collapsing the two into `[]` is
229
+ // what made this door answer "no live editor session found" — and prescribe
230
+ // `vgai edit` — for a probe that merely timed out under load, sending the
231
+ // operator to start an editor that was already running while the real defect
232
+ // went unnamed. The same collapse the manifest refusal above was added for.
233
+ let discoveryFailure = null;
190
234
  try {
191
235
  sessions = await withTimeout(transport.listSessions(EDITOR_SESSION_DISCOVERY_TIMEOUT_MS), EDITOR_SESSION_DISCOVERY_TIMEOUT_MS, 'editor session discovery');
192
236
  }
193
- catch {
237
+ catch (err) {
194
238
  sessions = [];
239
+ discoveryFailure = err instanceof Error ? err.message : String(err);
195
240
  }
196
241
  const canon = canonicalPath(projectRoot);
197
242
  const match = sessions.find((s) => s.project !== null && canonicalPath(s.project) === canon);
198
- if (!match) {
199
- const otherCount = sessions.length;
200
- throw new Error(`@vgai/live: no live editor session found for ${projectRoot}. @vgai/live only attaches to ` +
201
- 'an already-running session — it never starts one — so run `vgai edit` in that project ' +
202
- 'first, then retry.' +
203
- (otherCount > 0
204
- ? ` (${otherCount} other live session(s) found, but none open this project — @vgai/live ` +
205
- 'never silently attaches to a different project.)'
206
- : ''));
207
- }
243
+ if (!match)
244
+ throw noMatchingSessionRefusal(projectRoot, canon, sessions, discoveryFailure);
208
245
  if (match.manifestError != null)
209
246
  throw manifestRefusal(projectRoot, match.manifestError);
210
247
  return { port: match.port, projectRoot };
@@ -12,7 +12,7 @@ export declare class LiveTools {
12
12
  *
13
13
  * `instance` names WHICH mounted instance the tool should drive when several
14
14
  * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
15
- * and a tool that drives the game (e.g. `project.autoplay`) binds
15
+ * and a tool that drives the game binds
16
16
  * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
17
17
  * the tool then targets the sole live instance, exactly as before.
18
18
  */
@@ -19,7 +19,7 @@ export class LiveTools {
19
19
  *
20
20
  * `instance` names WHICH mounted instance the tool should drive when several
21
21
  * are live (multiplayer authoring) — it reaches the tool as `ctx.instance`,
22
- * and a tool that drives the game (e.g. `project.autoplay`) binds
22
+ * and a tool that drives the game binds
23
23
  * `game.instance(ctx.instance)` from it. Omitted is the single-instance case;
24
24
  * the tool then targets the sole live instance, exactly as before.
25
25
  */
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `~/.vgai/editor-sessions.json` — the ONE spelling of the editor-session
3
+ * registry contract, and the shared machinery every reader was hand-copying.
4
+ *
5
+ * The registry's WRITE half stays with its owner
6
+ * (`packages/editor/server/session-registry.ts`); this module owns the
7
+ * FORMAT: the entry shape, the shape guards, the file path, the
8
+ * liveness-filtered read, the `/__editor/project` answer parser, and the
9
+ * deterministic session-selection core. It existed as FOUR drifting copies
10
+ * (server registry, the CLI, and both vgai-sdk transports) held together by
11
+ * a "the CLI has no editor dependency" premise that had stopped being true —
12
+ * and the drift was already real: one copy's `/__editor/project` parser
13
+ * dropped `manifestError`, reporting a degraded session as belonging to no
14
+ * project.
15
+ */
16
+ /** One registry entry, exactly as the server's write half records it. */
17
+ export interface EditorSessionEntry {
18
+ /** Canonical project root currently open, or null when none. */
19
+ project: string | null;
20
+ port: number;
21
+ /** PID of the dev-server process. */
22
+ pid: number;
23
+ /** ISO timestamp of server start. */
24
+ startedAt: string;
25
+ sessionId: string | null;
26
+ /** Private loopback control credential — never returned by an editor HTTP
27
+ * route; exists only in this user-private registry. */
28
+ controlSecret?: string | null;
29
+ repositoryId: string | null;
30
+ worktreeId: string | null;
31
+ worktreeRoot: string | null;
32
+ projectRelativePath: string | null;
33
+ branch: string | null;
34
+ headCommit: string | null;
35
+ baseCommit: string | null;
36
+ }
37
+ export declare const EDITOR_SESSIONS_REGISTRY_FILE: string;
38
+ export declare function isEditorSessionEntry(v: unknown): v is EditorSessionEntry;
39
+ export declare function normalizeEditorSessionEntry(session: EditorSessionEntry): EditorSessionEntry;
40
+ export declare function pidAlive(pid: number): boolean;
41
+ /** The registry's live entries — shape-validated and PID-liveness-filtered. */
42
+ export declare function readLiveRegisteredSessions(): EditorSessionEntry[];
43
+ /**
44
+ * WHICH PROJECT a `/__editor/project` body says its server is serving.
45
+ *
46
+ * `serving` is that server's own statement of "I AM serving this project, I
47
+ * just cannot describe it" (its manifest is unparseable or fails strict
48
+ * validation) — added to the route precisely because a bare
49
+ * `{ project: null }` is indistinguishable from "no project open". Reading
50
+ * only `project.path` collapses the two, and the cost is that every
51
+ * project-matched command loses a live session the moment a save breaks its
52
+ * manifest, reporting it as belonging to no project rather than as this
53
+ * project's degraded session.
54
+ */
55
+ export declare function servedProjectAnswer(body: unknown): {
56
+ path: string | null;
57
+ manifestError: string | null;
58
+ };
59
+ /**
60
+ * Deterministic session selection — the ONE precedence order every surface
61
+ * follows: explicit `ctx.editorUrl` probe > port match against the listing >
62
+ * `ctx.projectRoot` match > lowest live port > not-running. Generic over the
63
+ * session shape so the editor and play transports (whose probe results
64
+ * differ) share the algorithm rather than a copy of it.
65
+ */
66
+ export declare function resolveRegisteredSession<T extends {
67
+ port: number;
68
+ project: string | null;
69
+ url?: string;
70
+ }>(ctx: {
71
+ editorUrl?: string | undefined;
72
+ projectRoot?: string | undefined;
73
+ }, transport: {
74
+ listSessions(timeoutMs: number): Promise<T[]>;
75
+ probeSessionUrl?: ((url: string, timeoutMs: number) => Promise<T | null | undefined>) | undefined;
76
+ }, options: {
77
+ probeTimeoutMs: number;
78
+ discoveryTimeoutMs: number;
79
+ withTimeout: <V>(work: Promise<V>, timeoutMs: number, what: string) => Promise<V>;
80
+ notRunning: () => never;
81
+ }): Promise<T>;