@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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/{editor.d.ts → vgai-live/src/editor.d.ts} +5 -8
- package/dist/{editor.js → vgai-live/src/editor.js} +5 -15
- package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.d.ts +1 -1
- package/dist/{game-client → vgai-live/src/game-client}/client.d.ts +4 -6
- package/dist/{game-client → vgai-live/src/game-client}/client.js +6 -8
- package/dist/{game-client → vgai-live/src/game-client}/relay-transport.d.ts +13 -2
- package/dist/{game-client → vgai-live/src/game-client}/relay-transport.js +21 -3
- package/dist/{game-client → vgai-live/src/game-client}/types.d.ts +5 -5
- package/dist/{game-client → vgai-live/src/game-client}/wait-for.js +2 -2
- package/dist/{game.d.ts → vgai-live/src/game.d.ts} +1 -1
- package/dist/{index.d.ts → vgai-live/src/index.d.ts} +2 -2
- package/dist/{index.js → vgai-live/src/index.js} +2 -2
- package/dist/{recording.js → vgai-live/src/recording.js} +3 -10
- package/dist/{session.d.ts → vgai-live/src/session.d.ts} +0 -7
- package/dist/{session.js → vgai-live/src/session.js} +56 -19
- package/dist/{tools.d.ts → vgai-live/src/tools.d.ts} +1 -1
- package/dist/{tools.js → vgai-live/src/tools.js} +1 -1
- package/dist/vgai-sdk/src/editor/session-registry-format.d.ts +81 -0
- package/dist/vgai-sdk/src/editor/session-registry-format.js +158 -0
- package/package.json +3 -3
- package/src/editor.ts +5 -16
- package/src/game-client/bridge-transport.ts +1 -1
- package/src/game-client/client.ts +6 -8
- package/src/game-client/relay-transport.ts +21 -3
- package/src/game-client/types.ts +5 -5
- package/src/game-client/wait-for.ts +2 -2
- package/src/game.ts +1 -1
- package/src/index.ts +2 -2
- package/src/recording.ts +3 -10
- package/src/session.ts +67 -24
- package/src/tools.ts +1 -1
- /package/dist/{editor-document.d.ts → vgai-live/src/editor-document.d.ts} +0 -0
- /package/dist/{editor-document.js → vgai-live/src/editor-document.js} +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-heartbeat.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/bridge-transport.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/capture-notes.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/errors.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/errors.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/events-matcher.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/failure-block.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/failure-block.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/fast-forward.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/hidden-recovery.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/index.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/index.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/perf-sampling.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/screenshot-target.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/state-cap.d.ts +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/state-cap.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/types.js +0 -0
- /package/dist/{game-client → vgai-live/src/game-client}/wait-for.d.ts +0 -0
- /package/dist/{game.js → vgai-live/src/game.js} +0 -0
- /package/dist/{lazy-proxy.d.ts → vgai-live/src/lazy-proxy.d.ts} +0 -0
- /package/dist/{lazy-proxy.js → vgai-live/src/lazy-proxy.js} +0 -0
- /package/dist/{recording.d.ts → vgai-live/src/recording.d.ts} +0 -0
- /package/dist/{singleton.d.ts → vgai-live/src/singleton.d.ts} +0 -0
- /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 }
|
|
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
|
|
11
|
-
*
|
|
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 }
|
|
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
|
|
11
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
45
|
-
* at the start of that tick's input phase. Declared here
|
|
46
|
-
* bridge↔wire coverage-parity gate
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
|
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
|
|
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)
|
|
38
|
-
*
|
|
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)
|
|
38
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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>;
|