@vgai/editor-sdk 0.5.4 → 0.5.6

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@vgai/editor-sdk",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "Apache-2.0",
5
- "version": "0.5.4",
5
+ "version": "0.5.6",
6
6
  "type": "module",
7
7
  "repository": {
8
8
  "type": "git",
@@ -22,7 +22,7 @@
22
22
  },
23
23
  "dependencies": {
24
24
  "@types/three": "^0.180.0",
25
- "@vgai/sdk": "0.5.4"
25
+ "@vgai/sdk": "0.5.6"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": "^19.0.0",
package/src/client.ts CHANGED
@@ -51,13 +51,51 @@ const DEFAULT_URL = 'http://127.0.0.1:20173';
51
51
  */
52
52
  export class EditorCommandError extends Error {
53
53
  readonly code: string | undefined;
54
- constructor(message: string, code?: string | undefined) {
54
+ /**
55
+ * True when the RELAY ended the command itself rather than the editor
56
+ * answering it — the HTTP 504 that `server/server-utils.ts`'s
57
+ * `commandResponseFor` gives any `timedOut` result, or this client's own
58
+ * deadline below.
59
+ *
60
+ * Read it as "no answer", not as "the budget expired". `editor-server.ts`
61
+ * raises `timedOut` for five conditions and only one of them takes the full
62
+ * budget: the command's timer expiring, the controlling tab's socket dying,
63
+ * the receipt window closing unanswered, a beating-but-dead tab, and no tab
64
+ * present at all. The last four can fail in milliseconds.
65
+ *
66
+ * A caller that converges by retrying (`vgai restart`) needs the distinction
67
+ * because a refusal the editor ANSWERED may go differently next time, while
68
+ * a command the relay abandoned tells you nothing new on a second identical
69
+ * attempt — and when the abandonment was a 120s budget, re-running it three
70
+ * times is `restart-readiness.ts`'s 361-seconds-of-silence defect.
71
+ */
72
+ readonly timedOut: boolean;
73
+ constructor(message: string, code?: string | undefined, timedOut = false) {
55
74
  super(message);
56
75
  this.name = 'EditorCommandError';
57
76
  this.code = code;
77
+ this.timedOut = timedOut;
58
78
  }
59
79
  }
60
80
 
81
+ /**
82
+ * The client's own ceiling on ONE relayed command.
83
+ *
84
+ * A backstop for a LOST server, not a per-command budget: the server already
85
+ * owns per-type budgets (`server/server-utils.ts`'s `relayCommandTimeoutMs`)
86
+ * and its timer must always be the one that fires, because its message names
87
+ * the tab and the remedy while this one can only say "no answer". So this is
88
+ * deliberately ONE number, comfortably above the longest server budget
89
+ * (120s, `play`/`capture-story-variants`) rather than a mirror of that table —
90
+ * a second copy of it would drift silently, and the drift would show up as
91
+ * this timer winning a race it must always lose.
92
+ *
93
+ * Without it a `fetch` with no `AbortSignal` waits on the OS: a dev server
94
+ * that stops answering mid-command holds the CLI open indefinitely, with no
95
+ * output and nothing to read.
96
+ */
97
+ const COMMAND_DEADLINE_MS = 150_000;
98
+
61
99
  /**
62
100
  * The GAME DEBUG PLANE, as a contribution's client sees it.
63
101
  *
@@ -131,14 +169,36 @@ export class EditorClient {
131
169
  private async command<T extends object = Record<string, never>>(
132
170
  body: Record<string, unknown>,
133
171
  ): Promise<T> {
134
- const res = await fetch(`${this.baseUrl}/__editor/command`, {
135
- method: 'POST',
136
- headers: { 'Content-Type': 'application/json' },
137
- body: JSON.stringify(body),
138
- });
172
+ let res: Response;
173
+ try {
174
+ res = await fetch(`${this.baseUrl}/__editor/command`, {
175
+ method: 'POST',
176
+ headers: { 'Content-Type': 'application/json' },
177
+ body: JSON.stringify(body),
178
+ signal: AbortSignal.timeout(COMMAND_DEADLINE_MS),
179
+ });
180
+ } catch (error) {
181
+ // Only the deadline is reshaped; a connection refused / DNS failure
182
+ // still surfaces as itself, because those name their own cause.
183
+ if ((error as { name?: string } | null)?.name !== 'TimeoutError') throw error;
184
+ throw new EditorCommandError(
185
+ `The editor at ${this.baseUrl} never answered "${String(body['type'] ?? 'command')}" ` +
186
+ `within ${Math.round(COMMAND_DEADLINE_MS / 1000)}s — past every server-side budget, so ` +
187
+ 'the server itself is not answering. Check the terminal running `vgai edit`.',
188
+ undefined,
189
+ true,
190
+ );
191
+ }
139
192
  const data = (await res.json()) as { ok: boolean; error?: string; code?: string } & T;
140
193
  if (!data.ok) {
141
- throw new EditorCommandError(data.error ?? `Editor command failed: ${res.status}`, data.code);
194
+ throw new EditorCommandError(
195
+ data.error ?? `Editor command failed: ${res.status}`,
196
+ data.code,
197
+ // 504 is every `timedOut` result (`commandResponseFor`) — the relay
198
+ // gave up, on any of its five grounds. A 200 body with `ok: false` is
199
+ // an ANSWER from the editor, however unwelcome. See `timedOut` above.
200
+ res.status === 504,
201
+ );
142
202
  }
143
203
  return data;
144
204
  }
@@ -149,9 +209,19 @@ export class EditorClient {
149
209
  * explicit config leg, relayed as `cmd['seed']`; `handleCommand`'s
150
210
  * `'play'` case threads it into `enterPlayMode`'s highest-precedence seed
151
211
  * argument (beats manifest.determinism.defaultSeed/?vgai-seed=). Omitted,
152
- * boot seeding falls back to that precedence unchanged. */
153
- async play(opts?: { seed?: number }): Promise<void> {
154
- await this.command({ type: 'play', ...(opts?.seed !== undefined ? { seed: opts.seed } : {}) });
212
+ * boot seeding falls back to that precedence unchanged.
213
+ *
214
+ * `opts.name` (`vgai play --name <text>`) an OPTIONAL label for this run,
215
+ * relayed as `cmd['name']` and slugified server-side into the run's
216
+ * `logs/play-*.jsonl` filename and its session-journal line. Findability
217
+ * only: no registry, no uniqueness, no lookup verb — grep and `ls` are the
218
+ * query engine. Omitted, the filename keeps its exact unnamed shape. */
219
+ async play(opts?: { seed?: number; name?: string | null }): Promise<void> {
220
+ await this.command({
221
+ type: 'play',
222
+ ...(opts?.seed !== undefined ? { seed: opts.seed } : {}),
223
+ ...(opts?.name ? { name: opts.name } : {}),
224
+ });
155
225
  }
156
226
 
157
227
  /** Dispose the current play session and mount it again from fresh project entry source. */
package/src/types.ts CHANGED
@@ -356,15 +356,77 @@ export interface EditorState {
356
356
  /** Flattened live authoring hierarchy, useful for agent entity discovery. */
357
357
  entities?: EditorEntitySummary[];
358
358
  /**
359
- * Live count of editor browser tabs holding an SSE connection right now.
360
- * The rest of this object is the last snapshot a browser POSTed and persists
361
- * even after every tab closes — so a `0` here means the other fields are
362
- * stale cache and commands (`play`, `scene`, …) will fail with "no editor
363
- * connected". Added by the server on every `/__editor/state` read.
359
+ * How many browser tabs are PRESENT for this session right now, read from
360
+ * the server's tab table (`server/tab-presence.ts`) rather than from a
361
+ * socket count — so a tab mid-reload still counts (it is beating), and a
362
+ * socket with no tab behind it does not.
363
+ *
364
+ * The rest of this object is the last snapshot a browser POSTed and
365
+ * persists even after every tab goes — so a `0` here means the other fields
366
+ * are stale cache and commands (`play`, `scene`, …) will refuse with the
367
+ * table's own reason. Added by the server on every `/__editor/state` read.
364
368
  */
365
369
  editorsConnected?: number;
366
370
  /** Convenience: `editorsConnected > 0`. */
367
371
  connected?: boolean;
372
+ /**
373
+ * ONE ROW PER PRESENT TAB — the whole table, because the owner's rule for
374
+ * this seam is "if they DO get disconnected, make it clear that it
375
+ * happened" and a single boolean can never say that.
376
+ *
377
+ * `lastBeatAgo` is the heartbeat age (null for a tab that cannot beat, e.g.
378
+ * one bridged through the share tunnel); a number climbing past a second or
379
+ * two is a gap in progress. `epochCount` counts page-loads, so a number
380
+ * that keeps rising is a reload loop. `channel: 'down'` with a fresh beat
381
+ * is a tab mid-reload — present, and briefly unable to take a command.
382
+ * `unresponsive` is the zombie: beating, but its page has never opened a
383
+ * command channel this page-load. Absent against an older server.
384
+ *
385
+ * `commandListener` is the standing verdict on the DOCUMENT, and it is a
386
+ * different question from all of the above: the control channel is opened by
387
+ * the tiny pre-React entry before any module loads, so a page that dies
388
+ * during boot beats, holds a channel, gets blessed, and executes nothing. It
389
+ * reads `'not attached'` for that page, `'silent since <t>'` for one whose
390
+ * listener stopped acknowledging relayed commands, and `'ready'` otherwise —
391
+ * each from a timestamp the server already stamps (the listener's own
392
+ * attach/detach report, and the command receipts). Absent against an older
393
+ * server, and absent for a tab the server cannot measure.
394
+ *
395
+ * `census` is the tab's RESOURCE PROFILE, sampled by the page every five
396
+ * seconds and carried on the heartbeat: what a browser-level renderer death
397
+ * would otherwise leave unexplained. `heapUsedMB`/`heapLimitMB` are null off
398
+ * Chromium (`performance.memory` is non-standard); the renderer counts are
399
+ * absent, never zero, when no game has registered a render-debug adapter.
400
+ * `censusAgeMs` says how stale the profile is — a hidden tab is not sampled.
401
+ */
402
+ tabs?: Array<{
403
+ tabId8: string;
404
+ presentFor: number;
405
+ lastBeatAgo: number | null;
406
+ epochCount: number;
407
+ visibility: 'visible' | 'hidden';
408
+ route: 'project' | 'no-project' | 'unknown';
409
+ blessed: boolean;
410
+ channel: 'open' | 'down';
411
+ unresponsive: boolean;
412
+ commandListener?: 'ready' | 'not attached' | (string & {});
413
+ census?: {
414
+ heapUsedMB: number | null;
415
+ heapLimitMB: number | null;
416
+ canvases: number;
417
+ canvasMB: number;
418
+ textures?: number;
419
+ geometries?: number;
420
+ programs?: number;
421
+ } | null;
422
+ censusAgeMs?: number | null;
423
+ }>;
424
+ /**
425
+ * The auto-open runaway guard. `stopped` means this session opened
426
+ * `attempts` tabs, none of them ever appeared in the table, and it has
427
+ * stopped trying — the browser, not the editor, is what to check.
428
+ */
429
+ tabAutoOpen?: { attempts: number; stopped: boolean };
368
430
  /**
369
431
  * Epoch ms when the server last received a state POST from a browser tab —
370
432
  * i.e. the age of the cached snapshot above. Omitted if no tab has ever
@@ -412,6 +474,24 @@ export interface EditorState {
412
474
  * neighbors above.
413
475
  */
414
476
  sourceValidation?: 'active' | 'awaiting-src' | 'no-project';
477
+ /**
478
+ * The compatibility verdict between this editor and the project it serves —
479
+ * `null` when they agree, otherwise the refusal the browser renders when it
480
+ * declines to activate the project, with its recovery guidance.
481
+ *
482
+ * Server-computed per read like its neighbors above. It is here because the
483
+ * gate was previously reported ONLY in the browser: an editor started on an
484
+ * incompatible project serves happily (it activates nothing), so the tab
485
+ * showed "This project is pinned to @vgai/engine X, but this editor is
486
+ * running Y" while `vgai status` reported a connected session with empty
487
+ * validation and `vgai play` timed out into a retry message about the tab
488
+ * reloading. An agent drives this editor through the CLI, so a gate visible
489
+ * only in pixels is invisible by construction.
490
+ */
491
+ projectCompatibility?: {
492
+ error: string;
493
+ recovery?: { kind: string; title: string; guidance: string; command?: string };
494
+ } | null;
415
495
  /**
416
496
  * #124: the absolute path of the project this editor server currently has
417
497
  * open — server-computed (never part of the browser-POSTed snapshot,