@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
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
18
|
+
import { join, resolve } from 'node:path';
|
|
19
|
+
export const EDITOR_SESSIONS_REGISTRY_FILE = join(homedir(), '.vgai', 'editor-sessions.json');
|
|
20
|
+
export function isEditorSessionEntry(v) {
|
|
21
|
+
if (typeof v !== 'object' || v === null)
|
|
22
|
+
return false;
|
|
23
|
+
const s = v;
|
|
24
|
+
const optionalIdentity = (value) => value === undefined || value === null || typeof value === 'string';
|
|
25
|
+
return ((typeof s['project'] === 'string' || s['project'] === null) &&
|
|
26
|
+
typeof s['port'] === 'number' &&
|
|
27
|
+
typeof s['pid'] === 'number' &&
|
|
28
|
+
typeof s['startedAt'] === 'string' &&
|
|
29
|
+
optionalIdentity(s['sessionId']) &&
|
|
30
|
+
optionalIdentity(s['controlSecret']) &&
|
|
31
|
+
optionalIdentity(s['repositoryId']) &&
|
|
32
|
+
optionalIdentity(s['worktreeId']) &&
|
|
33
|
+
optionalIdentity(s['worktreeRoot']) &&
|
|
34
|
+
optionalIdentity(s['projectRelativePath']) &&
|
|
35
|
+
optionalIdentity(s['branch']) &&
|
|
36
|
+
optionalIdentity(s['headCommit']) &&
|
|
37
|
+
optionalIdentity(s['baseCommit']));
|
|
38
|
+
}
|
|
39
|
+
export function normalizeEditorSessionEntry(session) {
|
|
40
|
+
return {
|
|
41
|
+
...session,
|
|
42
|
+
sessionId: session.sessionId ?? null,
|
|
43
|
+
controlSecret: session.controlSecret ?? null,
|
|
44
|
+
repositoryId: session.repositoryId ?? null,
|
|
45
|
+
worktreeId: session.worktreeId ?? null,
|
|
46
|
+
worktreeRoot: session.worktreeRoot ?? null,
|
|
47
|
+
projectRelativePath: session.projectRelativePath ?? null,
|
|
48
|
+
branch: session.branch ?? null,
|
|
49
|
+
headCommit: session.headCommit ?? null,
|
|
50
|
+
baseCommit: session.baseCommit ?? null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function pidAlive(pid) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** The registry's live entries — shape-validated and PID-liveness-filtered. */
|
|
63
|
+
export function readLiveRegisteredSessions() {
|
|
64
|
+
try {
|
|
65
|
+
const raw = JSON.parse(readFileSync(EDITOR_SESSIONS_REGISTRY_FILE, 'utf8'));
|
|
66
|
+
return Array.isArray(raw)
|
|
67
|
+
? raw
|
|
68
|
+
.filter(isEditorSessionEntry)
|
|
69
|
+
.map(normalizeEditorSessionEntry)
|
|
70
|
+
.filter((s) => pidAlive(s.pid))
|
|
71
|
+
: [];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* WHICH PROJECT a `/__editor/project` body says its server is serving.
|
|
79
|
+
*
|
|
80
|
+
* `serving` is that server's own statement of "I AM serving this project, I
|
|
81
|
+
* just cannot describe it" (its manifest is unparseable or fails strict
|
|
82
|
+
* validation) — added to the route precisely because a bare
|
|
83
|
+
* `{ project: null }` is indistinguishable from "no project open". Reading
|
|
84
|
+
* only `project.path` collapses the two, and the cost is that every
|
|
85
|
+
* project-matched command loses a live session the moment a save breaks its
|
|
86
|
+
* manifest, reporting it as belonging to no project rather than as this
|
|
87
|
+
* project's degraded session.
|
|
88
|
+
*/
|
|
89
|
+
export function servedProjectAnswer(body) {
|
|
90
|
+
const b = body;
|
|
91
|
+
return {
|
|
92
|
+
path: b.project?.path ?? b.serving?.path ?? null,
|
|
93
|
+
manifestError: b.project ? null : (b.serving?.error ?? null),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Canonical (realpathed when possible) absolute form of a project path. */
|
|
97
|
+
function canonicalizeProjectPath(p) {
|
|
98
|
+
const absolute = resolve(p);
|
|
99
|
+
try {
|
|
100
|
+
return realpathSync(absolute);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return absolute;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Extract the port from an `http(s)://host:port` URL, or undefined if unparseable. */
|
|
107
|
+
function portOfUrl(url) {
|
|
108
|
+
try {
|
|
109
|
+
const port = new URL(url).port;
|
|
110
|
+
return port ? Number(port) : undefined;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Deterministic session selection — the ONE precedence order every surface
|
|
118
|
+
* follows: explicit `ctx.editorUrl` probe > port match against the listing >
|
|
119
|
+
* `ctx.projectRoot` match > lowest live port > not-running. Generic over the
|
|
120
|
+
* session shape so the editor and play transports (whose probe results
|
|
121
|
+
* differ) share the algorithm rather than a copy of it.
|
|
122
|
+
*/
|
|
123
|
+
export async function resolveRegisteredSession(ctx, transport, options) {
|
|
124
|
+
const editorUrl = ctx.editorUrl;
|
|
125
|
+
if (editorUrl !== undefined && transport.probeSessionUrl) {
|
|
126
|
+
try {
|
|
127
|
+
const session = await options.withTimeout(transport.probeSessionUrl(editorUrl, options.probeTimeoutMs), options.probeTimeoutMs, 'explicit editor URL probe');
|
|
128
|
+
if (session)
|
|
129
|
+
return session;
|
|
130
|
+
}
|
|
131
|
+
catch { }
|
|
132
|
+
return options.notRunning();
|
|
133
|
+
}
|
|
134
|
+
let sessions;
|
|
135
|
+
try {
|
|
136
|
+
sessions = await options.withTimeout(transport.listSessions(options.discoveryTimeoutMs), options.discoveryTimeoutMs, 'editor session discovery');
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return options.notRunning();
|
|
140
|
+
}
|
|
141
|
+
if (sessions.length === 0)
|
|
142
|
+
return options.notRunning();
|
|
143
|
+
if (editorUrl !== undefined) {
|
|
144
|
+
const port = portOfUrl(editorUrl);
|
|
145
|
+
const match = sessions.find((s) => s.port === port);
|
|
146
|
+
if (!match)
|
|
147
|
+
return options.notRunning();
|
|
148
|
+
return { ...match, url: editorUrl.replace(/\/+$/, '') };
|
|
149
|
+
}
|
|
150
|
+
if (ctx.projectRoot !== undefined) {
|
|
151
|
+
const canon = canonicalizeProjectPath(ctx.projectRoot);
|
|
152
|
+
const match = sessions.find((s) => s.project !== null && canonicalizeProjectPath(s.project) === canon);
|
|
153
|
+
if (!match)
|
|
154
|
+
return options.notRunning();
|
|
155
|
+
return match;
|
|
156
|
+
}
|
|
157
|
+
return [...sessions].sort((a, b) => a.port - b.port)[0];
|
|
158
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@vgai/live",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.23",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"prepack": "npm run build"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@vgai/editor-sdk": "0.5.
|
|
36
|
-
"@vgai/sdk": "0.5.
|
|
35
|
+
"@vgai/editor-sdk": "0.5.23",
|
|
36
|
+
"@vgai/sdk": "0.5.23"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@playwright/test": ">=1.58.2 <2"
|
package/src/editor.ts
CHANGED
|
@@ -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
|
|
|
14
14
|
import type {
|
|
@@ -103,11 +103,8 @@ export class LiveEditor {
|
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
105
|
* The active authoring adapter's persistence destination — where a save would
|
|
106
|
-
* land (`status().savePath`).
|
|
107
|
-
*
|
|
108
|
-
* WO-8: this also took a `path` and OPENED that scene through
|
|
109
|
-
* `EditorClient.openScene` -> the `open-scene` relay verb. That verb now rejects
|
|
110
|
-
* (the `.vscn.json` format is deleted), so only the read remains.
|
|
106
|
+
* land (`status().savePath`). A read only: a three root has no scene document
|
|
107
|
+
* to open, and its root is activated instead.
|
|
111
108
|
*/
|
|
112
109
|
async scene(): Promise<string | null> {
|
|
113
110
|
const state = await this.#client.getState();
|
|
@@ -133,14 +130,6 @@ export class LiveEditor {
|
|
|
133
130
|
return this.#client.captureActiveDocument(size);
|
|
134
131
|
}
|
|
135
132
|
|
|
136
|
-
/*
|
|
137
|
-
* `applyDiff` lived here. It dispatched `project.scene.apply`, one of the
|
|
138
|
-
* twelve `.vscn` document tools deleted with the format, so there is nothing
|
|
139
|
-
* left for it to call. `ApplyDiffInput` / `ApplyDiffResult` went with it, and
|
|
140
|
-
* so did `projectRoot` — this was the ONE method that used it (every other
|
|
141
|
-
* method goes over the wire).
|
|
142
|
-
*/
|
|
143
|
-
|
|
144
133
|
async play(opts?: { seed?: number }): Promise<void> {
|
|
145
134
|
await this.#client.play(opts);
|
|
146
135
|
}
|
|
@@ -51,7 +51,7 @@ export interface BridgeTransport {
|
|
|
51
51
|
* blank/corrupt file. */
|
|
52
52
|
screenshot(path: string): Promise<CaptureNotes>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
54
|
+
* One dialect, full capability — runs a UI-automation step
|
|
55
55
|
* written as a literal `async (page) => {...}` (`GameClient.page()`,
|
|
56
56
|
* `client.ts`). `src` is `step.toString()`; `step` is the ORIGINAL
|
|
57
57
|
* function, wrapped so its own parameter type is erased to `unknown` (only
|
|
@@ -141,7 +141,7 @@ export class PageTransport implements BridgeTransport {
|
|
|
141
141
|
return {};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
/**
|
|
144
|
+
/** The ONE transport that runs a `game.page()` step against a REAL
|
|
145
145
|
* Playwright `Page` — no serialization, so `step`'s own closures work
|
|
146
146
|
* here (see `bridge-transport.ts`'s `runPageScript` doc comment for the
|
|
147
147
|
* full honesty-boundary contract; `src` is unused on this leg, kept only
|
|
@@ -651,7 +651,7 @@ export class GameClient {
|
|
|
651
651
|
snapshot: () => this.snapshot(),
|
|
652
652
|
now: () => Date.now(),
|
|
653
653
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
654
|
-
// Fixture heartbeat
|
|
654
|
+
// Fixture heartbeat — real stdout, the
|
|
655
655
|
// same channel a caller watching stdout already treats as
|
|
656
656
|
// liveness (see wait-for.ts's module doc for the emission
|
|
657
657
|
// invariant `maybeHeartbeat` enforces).
|
|
@@ -688,7 +688,7 @@ export class GameClient {
|
|
|
688
688
|
const start = await this.snapshot();
|
|
689
689
|
let lastTick: number | null = null;
|
|
690
690
|
let stalledPolls = 0;
|
|
691
|
-
// Fixture heartbeat
|
|
691
|
+
// Fixture heartbeat — same invariant as
|
|
692
692
|
// `wait-for.ts`'s `runWaitFor`: a heartbeat requires BOTH 60s of wall
|
|
693
693
|
// silence AND the tick having advanced since the last one emitted, so a
|
|
694
694
|
// genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
|
|
@@ -770,10 +770,8 @@ export class GameClient {
|
|
|
770
770
|
* `events.expect`'s failure path each shoot a frame on their own
|
|
771
771
|
* (`toSessionFailure`, `GameEvents.expect`), and a route's `whileHeld`
|
|
772
772
|
* observer may shoot one from inside a callback the driver never sees. The
|
|
773
|
-
* only place that sees ALL of them is here, which is why
|
|
774
|
-
* stamps its captures by listening rather than by intercepting
|
|
775
|
-
* (`src/tools/route-context.ts` in a scaffolded project is the shipped
|
|
776
|
-
* listener).
|
|
773
|
+
* only place that sees ALL of them is here, which is why a driving tool
|
|
774
|
+
* stamps its captures by listening rather than by intercepting.
|
|
777
775
|
*/
|
|
778
776
|
onCapture(listener: CaptureListener): () => void {
|
|
779
777
|
this.#captureListeners.add(listener);
|
|
@@ -783,7 +781,7 @@ export class GameClient {
|
|
|
783
781
|
}
|
|
784
782
|
|
|
785
783
|
/**
|
|
786
|
-
*
|
|
784
|
+
* One dialect, full capability — runs a UI-automation step
|
|
787
785
|
* written as a literal Playwright `async (page) => {...}` (interface
|
|
788
786
|
* doctrine §3.2/§4 rung 4: "the AI should think it is basically just
|
|
789
787
|
* executing Playwright"). Under `PageTransport` this drives the REAL
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
package/src/game-client/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
49
|
-
* at the start of that tick's input phase. Declared here
|
|
50
|
-
* bridge↔wire coverage-parity gate
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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 (
|
|
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
|
|
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
|
|
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)
|
|
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
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|