@vgai/live 0.5.41 → 0.5.42

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/editor.d.ts CHANGED
@@ -178,6 +178,8 @@ export declare class LiveEditor {
178
178
  * tree would be a fabricated answer about a surface nobody is being shown.
179
179
  */
180
180
  hierarchy(): Promise<InspectedHierarchy>;
181
+ /** Expand every branch through the Hierarchy panel's own action. */
182
+ expandHierarchyAll(): Promise<void>;
181
183
  /**
182
184
  * Write one editable field from `inspect()` by its stable path, through the
183
185
  * same Inspector IO and persistence boundary the human control uses.
package/dist/editor.js CHANGED
@@ -289,6 +289,10 @@ export class LiveEditor {
289
289
  async hierarchy() {
290
290
  return this.#client.hierarchy();
291
291
  }
292
+ /** Expand every branch through the Hierarchy panel's own action. */
293
+ async expandHierarchyAll() {
294
+ await this.#client.expandHierarchyAll();
295
+ }
292
296
  /**
293
297
  * Write one editable field from `inspect()` by its stable path, through the
294
298
  * same Inspector IO and persistence boundary the human control uses.
@@ -9,7 +9,7 @@
9
9
  import type { Page } from '@playwright/test';
10
10
  import type { BridgeCallOutcome, BridgeTransport } from './bridge-transport.js';
11
11
  import { type CaptureListener, type CaptureNotes } from './capture-notes.js';
12
- import { type FastForwardBudget, type FastForwardOptions } from './fast-forward.js';
12
+ import { type FastForwardBudget, type FastForwardOptions, type FastForwardTime } from './fast-forward.js';
13
13
  import { type TpsStats } from './perf-sampling.js';
14
14
  import type { DebugCommandInfo, DebugSnapshot, ProviderInfo, VirtualActionValue } from './types.js';
15
15
  import { type WaitForBudget } from './wait-for.js';
@@ -252,6 +252,9 @@ export declare class GameClient {
252
252
  * AFTER the tps baseline reset below, so it never itself corrupts the tps
253
253
  * stats either).
254
254
  */
255
+ fastForward(budget: FastForwardBudget, opts: FastForwardOptions & {
256
+ result: 'time';
257
+ }): Promise<FastForwardTime>;
255
258
  fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot>;
256
259
  waitFor(pred: (s: (name: string) => unknown) => boolean, budget: WaitForBudget): Promise<void>;
257
260
  /** Used by `input.hold` — waits for a sim-time delta to pass with no
@@ -77,6 +77,24 @@ async function bridgeCallInPageAsync(args) {
77
77
  };
78
78
  }
79
79
  }
80
+ /**
81
+ * A relay step crosses the wire as function source, so compiler-owned helpers that live beside the
82
+ * function in its Node module are closures too. esbuild's `keepNames` transform is the live case:
83
+ * a named helper inside an otherwise literal callback becomes `__name(fn, "helper")`, while the
84
+ * module-level `__name` implementation is absent after `step.toString()`. Seat that exact compiler
85
+ * primitive inside the serialized function instead of installing a page global or asking every
86
+ * caller to avoid ordinary named local helpers.
87
+ *
88
+ * Ordinary source stays byte-for-byte unchanged. That preserves the public wire account and keeps
89
+ * unsupported user closures loud; this only completes the source for a compiler helper whose
90
+ * semantics are intrinsic and deterministic.
91
+ */
92
+ function selfContainedStepSource(step) {
93
+ const source = step.toString();
94
+ if (!/\b__name\s*\(/u.test(source))
95
+ return source;
96
+ return `(scope) => { const __name = (target, value) => Object.defineProperty(target, "name", { value, configurable: true }); return (${source})(scope); }`;
97
+ }
80
98
  /**
81
99
  * #140 — the `BridgeTransport` `page.evaluate` implementation. This is the
82
100
  * ONE place a `Page` is ever touched to drive `window.__vgai` (module doc
@@ -527,18 +545,6 @@ export class GameClient {
527
545
  hiddenRecoveryTriggered() {
528
546
  return this.#hiddenRecovery.wasTriggered();
529
547
  }
530
- /**
531
- * D15/T-D15.4 — synchronously drives `budget` worth of sim time/ticks via
532
- * the live `Game`'s `runTicks` (through the debug bridge), instead of
533
- * waiting for real wall-clock time to pass. Doctrine (see
534
- * `fast-forward.ts`'s module doc, which also documents the two honesty
535
- * decisions this wraps): SETUP/STAGING traversal — reaching a known
536
- * late-game state fast — not a substitute for real-input proofs, which
537
- * still run in real ticks. Returns the final `{time, state, events,
538
- * pageErrors}` snapshot (a completely ordinary `snapshot()` read, taken
539
- * AFTER the tps baseline reset below, so it never itself corrupts the tps
540
- * stats either).
541
- */
542
548
  async fastForward(budget, opts) {
543
549
  // Same options-object-only contract as `waitSimTime` (`ticksForBudget`
544
550
  // would otherwise fail on `'simTicks' in 0.5` with a raw TypeError that
@@ -570,21 +576,26 @@ export class GameClient {
570
576
  }
571
577
  },
572
578
  readTime: async () => {
573
- // Raw bridge read — deliberately NOT `this.snapshot()`, which would
574
- // feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
575
- const snap = await this.callBridge('snapshot');
576
- return { tick: snap.time.tick, simSeconds: snap.time.simSeconds };
579
+ // Raw, clock-only bridge read — deliberately NOT `this.snapshot()`,
580
+ // which both feeds the TpsAccumulator and serializes every declared
581
+ // provider. Large imported worlds can carry megabytes of census state;
582
+ // the batching math needs only these two numbers.
583
+ const time = await this.callBridge('state', 'time');
584
+ return { tick: time.tick, simSeconds: time.simSeconds };
577
585
  },
578
586
  heartbeat: (info) => {
579
587
  console.log(`vgai fastForward: ${info.ticksDone}/${info.ticksTotal} ticks driven`);
580
588
  },
581
589
  };
582
- await runFastForward(budget, opts ?? {}, clock);
590
+ const finalTime = await runFastForward(budget, opts ?? {}, clock);
583
591
  // The burst is over — reset the baseline so the very next ordinary poll
584
592
  // (including the `snapshot()` call right below) treats itself as a fresh
585
593
  // "first observation" rather than diffing across the burst's enormous
586
594
  // tick delta over a near-zero wall delta.
587
595
  this.#tps.resetBaseline();
596
+ if (opts?.result === 'time') {
597
+ return finalTime;
598
+ }
588
599
  return this.snapshot();
589
600
  }
590
601
  async waitFor(pred, budget) {
@@ -626,7 +637,13 @@ export class GameClient {
626
637
  // fires either. Refuse in the caller's own vocabulary instead of hanging.
627
638
  assertValidWaitForBudget(budget, 'waitSimTime');
628
639
  const startWall = Date.now();
629
- const start = await this.snapshot();
640
+ // Predicate-free means clock-only from the FIRST read, not merely from the
641
+ // second poll onward. A full initial snapshot serializes every provider;
642
+ // the Unity FPS provider alone carries ~4,500 objects and measured a
643
+ // 150-300ms main-thread hitch each time a caller began an otherwise cheap
644
+ // wait. Failure diagnostics still take one complete terminal snapshot
645
+ // below, only on the failure path where its state is actually printed.
646
+ const startTime = await this.readTime();
630
647
  let lastTick = null;
631
648
  let stalledPolls = 0;
632
649
  // Fixture heartbeat — same invariant as
@@ -634,10 +651,10 @@ export class GameClient {
634
651
  // silence AND the tick having advanced since the last one emitted, so a
635
652
  // genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
636
653
  // goes heartbeat-silent well before this loop's own guard ever needs to.
637
- let heartbeat = { lastEmitWallMs: startWall, lastEmitTick: start.time.tick };
654
+ let heartbeat = { lastEmitWallMs: startWall, lastEmitTick: startTime.tick };
638
655
  for (;;) {
639
656
  const currentTime = await this.readTime();
640
- if (currentTime.simSeconds - start.time.simSeconds >= budget.simSeconds)
657
+ if (currentTime.simSeconds - startTime.simSeconds >= budget.simSeconds)
641
658
  return;
642
659
  stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
643
660
  lastTick = currentTime.tick;
@@ -647,7 +664,10 @@ export class GameClient {
647
664
  const current = await this.snapshot();
648
665
  throw await this.toSessionFailure(new WaitForTimeoutError({
649
666
  budget,
650
- startSnapshot: start,
667
+ // The timeout renderer reads only the start clock; provider state
668
+ // is intentionally terminal-only because no initial provider read
669
+ // occurred. Empty collections state that absence honestly.
670
+ startSnapshot: { time: startTime, state: {}, events: [], pageErrors: [] },
651
671
  lastSnapshot: current,
652
672
  wallElapsedMs: Date.now() - startWall,
653
673
  predicateSource: '(no predicate — game.input.hold is waiting for a sim-time delta to pass)',
@@ -740,7 +760,7 @@ export class GameClient {
740
760
  */
741
761
  async page(step) {
742
762
  const erased = (arg) => step(arg);
743
- const outcome = await this.#transport.runPageScript(step.toString(), erased);
763
+ const outcome = await this.#transport.runPageScript(selfContainedStepSource(step), erased);
744
764
  return this.unwrap(outcome);
745
765
  }
746
766
  /**
@@ -763,7 +783,7 @@ export class GameClient {
763
783
  */
764
784
  async run(step, opts) {
765
785
  const erased = (arg) => step(arg);
766
- const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
786
+ const outcome = await this.#transport.runGameScript(selfContainedStepSource(step), erased, opts?.instance);
767
787
  return this.unwrap(outcome);
768
788
  }
769
789
  /**
@@ -64,6 +64,11 @@ export interface FastForwardOptions {
64
64
  /** Overrides `DEFAULT_FAST_FORWARD_BATCH_TICKS` — test-only seam; real
65
65
  * callers should leave this unset. */
66
66
  batchTicks?: number;
67
+ /** What the caller needs back after the burst. Defaults to the complete
68
+ * debug snapshot. Tick-by-tick orchestration that samples providers only at
69
+ * explicit boundaries selects `'time'` so it does not serialize every large
70
+ * state provider after every staging tick. */
71
+ result?: 'snapshot' | 'time';
67
72
  }
68
73
  /** One fixed timestep, matching every real host's loop construction
69
74
  * (`createGameLoop`'s own `fixedTimestep ?? 1/60` default: "fixedDt = the
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.41",
5
+ "version": "0.5.42",
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.41",
36
- "@vgai/sdk": "0.5.41"
35
+ "@vgai/editor-sdk": "0.5.42",
36
+ "@vgai/sdk": "0.5.42"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@playwright/test": ">=1.58.2 <2"
@@ -41,6 +41,7 @@
41
41
  "devDependencies": {
42
42
  "@playwright/test": "^1.58.2",
43
43
  "@types/node": "^25.3.0",
44
+ "esbuild": "^0.25.12",
44
45
  "typescript": "^5.6.0"
45
46
  },
46
47
  "description": "Live-session client for VGAI projects: { editor, game, page, tools } over the vgai edit session wire.",
package/src/editor.ts CHANGED
@@ -353,6 +353,11 @@ export class LiveEditor {
353
353
  return this.#client.hierarchy();
354
354
  }
355
355
 
356
+ /** Expand every branch through the Hierarchy panel's own action. */
357
+ async expandHierarchyAll(): Promise<void> {
358
+ await this.#client.expandHierarchyAll();
359
+ }
360
+
356
361
  /**
357
362
  * Write one editable field from `inspect()` by its stable path, through the
358
363
  * same Inspector IO and persistence boundary the human control uses.
@@ -20,6 +20,7 @@ import {
20
20
  type FastForwardBudget,
21
21
  type FastForwardClock,
22
22
  type FastForwardOptions,
23
+ type FastForwardTime,
23
24
  runFastForward,
24
25
  } from './fast-forward.js';
25
26
  import { HiddenRecoveryDriver } from './hidden-recovery.js';
@@ -104,6 +105,24 @@ async function bridgeCallInPageAsync(args: {
104
105
  }
105
106
  }
106
107
 
108
+ /**
109
+ * A relay step crosses the wire as function source, so compiler-owned helpers that live beside the
110
+ * function in its Node module are closures too. esbuild's `keepNames` transform is the live case:
111
+ * a named helper inside an otherwise literal callback becomes `__name(fn, "helper")`, while the
112
+ * module-level `__name` implementation is absent after `step.toString()`. Seat that exact compiler
113
+ * primitive inside the serialized function instead of installing a page global or asking every
114
+ * caller to avoid ordinary named local helpers.
115
+ *
116
+ * Ordinary source stays byte-for-byte unchanged. That preserves the public wire account and keeps
117
+ * unsupported user closures loud; this only completes the source for a compiler helper whose
118
+ * semantics are intrinsic and deterministic.
119
+ */
120
+ function selfContainedStepSource(step: (scope: unknown) => unknown): string {
121
+ const source = step.toString();
122
+ if (!/\b__name\s*\(/u.test(source)) return source;
123
+ return `(scope) => { const __name = (target, value) => Object.defineProperty(target, "name", { value, configurable: true }); return (${source})(scope); }`;
124
+ }
125
+
107
126
  /**
108
127
  * #140 — the `BridgeTransport` `page.evaluate` implementation. This is the
109
128
  * ONE place a `Page` is ever touched to drive `window.__vgai` (module doc
@@ -666,7 +685,15 @@ export class GameClient {
666
685
  * AFTER the tps baseline reset below, so it never itself corrupts the tps
667
686
  * stats either).
668
687
  */
669
- async fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot> {
688
+ async fastForward(
689
+ budget: FastForwardBudget,
690
+ opts: FastForwardOptions & { result: 'time' },
691
+ ): Promise<FastForwardTime>;
692
+ async fastForward(budget: FastForwardBudget, opts?: FastForwardOptions): Promise<DebugSnapshot>;
693
+ async fastForward(
694
+ budget: FastForwardBudget,
695
+ opts?: FastForwardOptions,
696
+ ): Promise<DebugSnapshot | FastForwardTime> {
670
697
  // Same options-object-only contract as `waitSimTime` (`ticksForBudget`
671
698
  // would otherwise fail on `'simTicks' in 0.5` with a raw TypeError that
672
699
  // names neither the method nor the shape).
@@ -696,21 +723,26 @@ export class GameClient {
696
723
  }
697
724
  },
698
725
  readTime: async () => {
699
- // Raw bridge read — deliberately NOT `this.snapshot()`, which would
700
- // feed the TpsAccumulator (see fast-forward.ts's module doc, point 2).
701
- const snap = await this.callBridge<DebugSnapshot>('snapshot');
702
- return { tick: snap.time.tick, simSeconds: snap.time.simSeconds };
726
+ // Raw, clock-only bridge read — deliberately NOT `this.snapshot()`,
727
+ // which both feeds the TpsAccumulator and serializes every declared
728
+ // provider. Large imported worlds can carry megabytes of census state;
729
+ // the batching math needs only these two numbers.
730
+ const time = await this.callBridge<DebugSnapshot['time']>('state', 'time');
731
+ return { tick: time.tick, simSeconds: time.simSeconds };
703
732
  },
704
733
  heartbeat: (info) => {
705
734
  console.log(`vgai fastForward: ${info.ticksDone}/${info.ticksTotal} ticks driven`);
706
735
  },
707
736
  };
708
- await runFastForward(budget, opts ?? {}, clock);
737
+ const finalTime = await runFastForward(budget, opts ?? {}, clock);
709
738
  // The burst is over — reset the baseline so the very next ordinary poll
710
739
  // (including the `snapshot()` call right below) treats itself as a fresh
711
740
  // "first observation" rather than diffing across the burst's enormous
712
741
  // tick delta over a near-zero wall delta.
713
742
  this.#tps.resetBaseline();
743
+ if (opts?.result === 'time') {
744
+ return finalTime;
745
+ }
714
746
  return this.snapshot();
715
747
  }
716
748
 
@@ -760,7 +792,13 @@ export class GameClient {
760
792
  // fires either. Refuse in the caller's own vocabulary instead of hanging.
761
793
  assertValidWaitForBudget(budget, 'waitSimTime');
762
794
  const startWall = Date.now();
763
- const start = await this.snapshot();
795
+ // Predicate-free means clock-only from the FIRST read, not merely from the
796
+ // second poll onward. A full initial snapshot serializes every provider;
797
+ // the Unity FPS provider alone carries ~4,500 objects and measured a
798
+ // 150-300ms main-thread hitch each time a caller began an otherwise cheap
799
+ // wait. Failure diagnostics still take one complete terminal snapshot
800
+ // below, only on the failure path where its state is actually printed.
801
+ const startTime = await this.readTime();
764
802
  let lastTick: number | null = null;
765
803
  let stalledPolls = 0;
766
804
  // Fixture heartbeat — same invariant as
@@ -768,10 +806,10 @@ export class GameClient {
768
806
  // silence AND the tick having advanced since the last one emitted, so a
769
807
  // genuinely stalled sim clock (caught by `stalledPolls` above, ~30s)
770
808
  // goes heartbeat-silent well before this loop's own guard ever needs to.
771
- let heartbeat: HeartbeatState = { lastEmitWallMs: startWall, lastEmitTick: start.time.tick };
809
+ let heartbeat: HeartbeatState = { lastEmitWallMs: startWall, lastEmitTick: startTime.tick };
772
810
  for (;;) {
773
811
  const currentTime = await this.readTime();
774
- if (currentTime.simSeconds - start.time.simSeconds >= budget.simSeconds) return;
812
+ if (currentTime.simSeconds - startTime.simSeconds >= budget.simSeconds) return;
775
813
  stalledPolls = lastTick !== null && currentTime.tick === lastTick ? stalledPolls + 1 : 0;
776
814
  lastTick = currentTime.tick;
777
815
  if (stalledPolls >= WAIT_FOR_STALL_POLL_LIMIT) {
@@ -781,7 +819,10 @@ export class GameClient {
781
819
  throw await this.toSessionFailure(
782
820
  new WaitForTimeoutError({
783
821
  budget,
784
- startSnapshot: start,
822
+ // The timeout renderer reads only the start clock; provider state
823
+ // is intentionally terminal-only because no initial provider read
824
+ // occurred. Empty collections state that absence honestly.
825
+ startSnapshot: { time: startTime, state: {}, events: [], pageErrors: [] },
785
826
  lastSnapshot: current,
786
827
  wallElapsedMs: Date.now() - startWall,
787
828
  predicateSource:
@@ -876,7 +917,10 @@ export class GameClient {
876
917
  */
877
918
  async page<T = unknown>(step: (page: Page) => T | Promise<T>): Promise<T> {
878
919
  const erased = (arg: unknown) => step(arg as Page);
879
- const outcome = await this.#transport.runPageScript(step.toString(), erased);
920
+ const outcome = await this.#transport.runPageScript(
921
+ selfContainedStepSource(step as (scope: unknown) => unknown),
922
+ erased,
923
+ );
880
924
  return this.unwrap<T>(outcome);
881
925
  }
882
926
 
@@ -907,7 +951,11 @@ export class GameClient {
907
951
  opts?: { instance?: string },
908
952
  ): Promise<T> {
909
953
  const erased = (arg: unknown) => step(arg as Parameters<typeof step>[0]);
910
- const outcome = await this.#transport.runGameScript(step.toString(), erased, opts?.instance);
954
+ const outcome = await this.#transport.runGameScript(
955
+ selfContainedStepSource(step as (scope: unknown) => unknown),
956
+ erased,
957
+ opts?.instance,
958
+ );
911
959
  return this.unwrap<T>(outcome);
912
960
  }
913
961
 
@@ -63,6 +63,11 @@ export interface FastForwardOptions {
63
63
  /** Overrides `DEFAULT_FAST_FORWARD_BATCH_TICKS` — test-only seam; real
64
64
  * callers should leave this unset. */
65
65
  batchTicks?: number;
66
+ /** What the caller needs back after the burst. Defaults to the complete
67
+ * debug snapshot. Tick-by-tick orchestration that samples providers only at
68
+ * explicit boundaries selects `'time'` so it does not serialize every large
69
+ * state provider after every staging tick. */
70
+ result?: 'snapshot' | 'time';
66
71
  }
67
72
 
68
73
  /** One fixed timestep, matching every real host's loop construction