@vgai/sdk 0.5.53 → 0.5.57
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/sdk",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
|
-
"version": "0.5.
|
|
5
|
+
"version": "0.5.57",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
36
|
-
"@vgai/engine": "0.5.
|
|
36
|
+
"@vgai/engine": "0.5.57",
|
|
37
37
|
"playwright": "^1.58.2",
|
|
38
38
|
"zod": "^4.3.6"
|
|
39
39
|
}
|
|
@@ -14,9 +14,17 @@
|
|
|
14
14
|
* project.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
realpathSync,
|
|
22
|
+
unlinkSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from 'node:fs';
|
|
18
25
|
import { homedir } from 'node:os';
|
|
19
26
|
import { join, resolve } from 'node:path';
|
|
27
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
20
28
|
|
|
21
29
|
/** One registry entry, exactly as the server's write half records it. */
|
|
22
30
|
export interface EditorSessionEntry {
|
|
@@ -103,6 +111,61 @@ export function readLiveRegisteredSessions(): EditorSessionEntry[] {
|
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
|
|
114
|
+
/** Announce a launch before the server can register. Each launcher owns its file. */
|
|
115
|
+
export function announceEditorLaunch(project: string): () => void {
|
|
116
|
+
const directory = join(project, '.vgai', 'editor-launches');
|
|
117
|
+
mkdirSync(directory, { recursive: true });
|
|
118
|
+
const file = join(directory, `${process.pid}.json`);
|
|
119
|
+
writeFileSync(file, JSON.stringify({ pid: process.pid }));
|
|
120
|
+
const clear = () => {
|
|
121
|
+
process.removeListener('exit', clear);
|
|
122
|
+
try {
|
|
123
|
+
unlinkSync(file);
|
|
124
|
+
} catch {
|
|
125
|
+
// Already cleared when the server became ready.
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
process.once('exit', clear);
|
|
129
|
+
return clear;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function hasPendingEditorLaunch(project: string): boolean {
|
|
133
|
+
const directory = join(project, '.vgai', 'editor-launches');
|
|
134
|
+
try {
|
|
135
|
+
return readdirSync(directory).some((name) => {
|
|
136
|
+
if (!/^\d+\.json$/.test(name)) return false;
|
|
137
|
+
const pid = Number(name.slice(0, -5));
|
|
138
|
+
return pid !== process.pid && pidAlive(pid);
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Wait only for an observed live launcher, never for a missing editor. */
|
|
146
|
+
export async function waitForPendingEditorLaunch(project: string): Promise<void> {
|
|
147
|
+
const canon = canonicalizeProjectPath(project);
|
|
148
|
+
const ready = () =>
|
|
149
|
+
readLiveRegisteredSessions().some(
|
|
150
|
+
(session) => session.project !== null && canonicalizeProjectPath(session.project) === canon,
|
|
151
|
+
);
|
|
152
|
+
if (ready() || !hasPendingEditorLaunch(canon)) return;
|
|
153
|
+
const deadline = Date.now() + 180_000;
|
|
154
|
+
while (!ready()) {
|
|
155
|
+
if (!hasPendingEditorLaunch(canon)) {
|
|
156
|
+
// The launcher clears its announcement only after registration.
|
|
157
|
+
if (ready()) return;
|
|
158
|
+
throw new Error(`The editor launch for ${canon} ended before registering a session.`);
|
|
159
|
+
}
|
|
160
|
+
if (Date.now() >= deadline) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`The editor launch for ${canon} is still running but has not registered within 180s.`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
await delay(100);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
106
169
|
/**
|
|
107
170
|
* WHICH PROJECT a `/__editor/project` body says its server is serving.
|
|
108
171
|
*
|
|
@@ -186,6 +249,10 @@ export async function resolveRegisteredSession<
|
|
|
186
249
|
return options.notRunning();
|
|
187
250
|
}
|
|
188
251
|
|
|
252
|
+
if (editorUrl === undefined && ctx.projectRoot !== undefined) {
|
|
253
|
+
await waitForPendingEditorLaunch(ctx.projectRoot);
|
|
254
|
+
}
|
|
255
|
+
|
|
189
256
|
let sessions: T[];
|
|
190
257
|
try {
|
|
191
258
|
sessions = await options.withTimeout(
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { appendFileSync, mkdirSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
|
|
36
36
|
import { join } from 'node:path';
|
|
37
37
|
import type { TripwireTier } from './build-discipline';
|
|
38
|
-
import type { RecordedTabCensus } from './tab-census';
|
|
38
|
+
import type { BlenderTabMetrics, RecordedTabCensus } from './tab-census';
|
|
39
39
|
|
|
40
40
|
/** `editor-` + an ISO instant with `:`/`.` flattened + `.jsonl` — the same
|
|
41
41
|
* lexicographic-order-is-chronological-order shape `play-*.jsonl` uses. */
|
|
@@ -709,6 +709,35 @@ function tabDeathProfileBody(line: SessionJournalEvent & { kind: 'tab-death-prof
|
|
|
709
709
|
* phrasings of the same numbers is how a reader ends up believing there are
|
|
710
710
|
* two measurements. Renderer counts appear only when the page could read them
|
|
711
711
|
* (no mounted render-debug adapter = the words are absent, never a zero). */
|
|
712
|
+
/**
|
|
713
|
+
* The Blender block's words: what the twin's worker and this tab's main thread
|
|
714
|
+
* have been doing. Absent entirely in a tab with no Blender session.
|
|
715
|
+
*
|
|
716
|
+
* `in flight` is the field that speaks during a wedge — the 2026-09-16 call
|
|
717
|
+
* that held the worker 1,800s produced no other number anywhere in the product.
|
|
718
|
+
* A field the page could not measure is OMITTED rather than printed as 0: no
|
|
719
|
+
* call yet, or no Long Tasks API (Safari/Firefox), is not "never stalled".
|
|
720
|
+
*/
|
|
721
|
+
function formatBlenderMetrics(blender: BlenderTabMetrics): string {
|
|
722
|
+
const s = (ms: number): string => `${Math.round(ms / 100) / 10}s`;
|
|
723
|
+
const parts = [
|
|
724
|
+
blender.inFlightMs === null ? 'idle' : `IN FLIGHT ${s(blender.inFlightMs)}`,
|
|
725
|
+
...(blender.lastCallMs === null ? [] : [`last call ${s(blender.lastCallMs)}`]),
|
|
726
|
+
...(blender.maxCallMs === null ? [] : [`max ${s(blender.maxCallMs)}`]),
|
|
727
|
+
`over 5s ${blender.callsOver5s}`,
|
|
728
|
+
`over 30s ${blender.callsOver30s}`,
|
|
729
|
+
...(blender.longestTaskMs === null
|
|
730
|
+
? []
|
|
731
|
+
: [
|
|
732
|
+
`longest main-thread task ${s(blender.longestTaskMs)} (${blender.tasksOver100ms} over 100ms)`,
|
|
733
|
+
]),
|
|
734
|
+
...(blender.lastCallLongestTaskMs === null
|
|
735
|
+
? []
|
|
736
|
+
: [`${s(blender.lastCallLongestTaskMs)} of that during the last call`]),
|
|
737
|
+
];
|
|
738
|
+
return `blender: ${parts.join(', ')}`;
|
|
739
|
+
}
|
|
740
|
+
|
|
712
741
|
export function formatTabCensus(census: RecordedTabCensus | null): string {
|
|
713
742
|
if (census === null) return 'no resource census';
|
|
714
743
|
const parts = [
|
|
@@ -721,7 +750,13 @@ export function formatTabCensus(census: RecordedTabCensus | null): string {
|
|
|
721
750
|
if (census.textures !== undefined) parts.push(`tex ${census.textures}`);
|
|
722
751
|
if (census.geometries !== undefined) parts.push(`geo ${census.geometries}`);
|
|
723
752
|
if (census.programs !== undefined) parts.push(`prog ${census.programs}`);
|
|
724
|
-
|
|
753
|
+
const line = parts.join(', ');
|
|
754
|
+
// On its own line: the Blender block answers a different question from the
|
|
755
|
+
// resource profile (is the tab ANSWERING, not what is it holding), and
|
|
756
|
+
// running the two together is how a reader stops seeing either.
|
|
757
|
+
return census.blender === undefined
|
|
758
|
+
? line
|
|
759
|
+
: `${line}\n ${formatBlenderMetrics(census.blender)}`;
|
|
725
760
|
}
|
|
726
761
|
|
|
727
762
|
/**
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* Deliberately import-free so a browser bundle can take it: the census's other
|
|
26
26
|
* `@vgai/sdk` home, `project/session-journal.ts`, reads `node:fs`.
|
|
27
27
|
*
|
|
28
|
+
* WHAT ELSE RIDES IT. The beat is the one channel that still moves when the
|
|
29
|
+
* page's main thread or the Blender worker is blocked, so `blender` — how long
|
|
30
|
+
* the twin's calls are taking and how long the main thread has been stalled —
|
|
31
|
+
* is carried here too (absent unless a Blender session exists in the tab). Same
|
|
32
|
+
* rule as the rest of this file: measurement only, no budget anywhere.
|
|
33
|
+
*
|
|
28
34
|
* ABSENT IS NOT ZERO. `heapUsedMB`/`heapLimitMB` are null off Chromium
|
|
29
35
|
* (`performance.memory` is non-standard). The renderer counts are ABSENT rather
|
|
30
36
|
* than zero when no game has registered a render-debug adapter, and `programs`
|
|
@@ -32,6 +38,47 @@
|
|
|
32
38
|
* a different claim from "nobody measured".
|
|
33
39
|
*/
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* THE BLENDER TWIN'S STALLS, as numbers.
|
|
43
|
+
*
|
|
44
|
+
* WHY (measured 2026-09-16): one `blender-execute` held the twin's worker for
|
|
45
|
+
* over 1,800s and wedged the tab, and a 0.86s-per-call scene stretched into
|
|
46
|
+
* heartbeat timeouts that read as "tab present, did not respond". The product
|
|
47
|
+
* had no number for either. Owner ruling: a tab that stops answering is the
|
|
48
|
+
* product's defect regardless of what the machine is doing, and the product has
|
|
49
|
+
* to surface it — so these ride the census to `vgai status`.
|
|
50
|
+
*
|
|
51
|
+
* MEASURED BY THE PAGE, because neither blocked party can report on itself: the
|
|
52
|
+
* worker's own loop is what is stuck, and a stalled main thread cannot send.
|
|
53
|
+
* The call half comes from `BlenderRuntime.metrics()`
|
|
54
|
+
* (`packages/bpy-twin/browser/runtime.ts`), the stall half from a `longtask`
|
|
55
|
+
* PerformanceObserver (`packages/editor/src/blender-tab-metrics.ts`).
|
|
56
|
+
*
|
|
57
|
+
* MEASUREMENT ONLY. No threshold here cancels, kills or budgets a call.
|
|
58
|
+
*/
|
|
59
|
+
export interface BlenderTabMetrics {
|
|
60
|
+
/** Age of the oldest OUTSTANDING worker call, or null when the twin is idle.
|
|
61
|
+
* The only field with a number during a wedge. */
|
|
62
|
+
readonly inFlightMs: number | null;
|
|
63
|
+
/** Duration of the newest completed call; null before the first one. */
|
|
64
|
+
readonly lastCallMs: number | null;
|
|
65
|
+
/** The longest call this page has seen, counting an outstanding one. */
|
|
66
|
+
readonly maxCallMs: number | null;
|
|
67
|
+
/** Calls past 5s, and past 30s, since this page loaded. */
|
|
68
|
+
readonly callsOver5s: number;
|
|
69
|
+
readonly callsOver30s: number;
|
|
70
|
+
/** Longest `longtask` entry since page load, in ms; null off Chromium (the
|
|
71
|
+
* Long Tasks API is not implemented everywhere) — never 0, which would read
|
|
72
|
+
* as "the main thread never stalled". */
|
|
73
|
+
readonly longestTaskMs: number | null;
|
|
74
|
+
/** How many long tasks ran past 100ms since page load. */
|
|
75
|
+
readonly tasksOver100ms: number;
|
|
76
|
+
/** The longest long task that overlapped the newest call's window — what the
|
|
77
|
+
* MAIN thread was doing while the worker was busy. Null when there has been
|
|
78
|
+
* no call yet, or no long task during it. */
|
|
79
|
+
readonly lastCallLongestTaskMs: number | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
35
82
|
/**
|
|
36
83
|
* A census as a CURRENT page produces it and the server files it.
|
|
37
84
|
*
|
|
@@ -57,6 +104,8 @@ export interface TabCensus {
|
|
|
57
104
|
readonly geometries?: number;
|
|
58
105
|
/** Compiled programs — absent with no adapter, or before the first render. */
|
|
59
106
|
readonly programs?: number;
|
|
107
|
+
/** {@link BlenderTabMetrics} — absent unless this tab has a Blender session. */
|
|
108
|
+
readonly blender?: BlenderTabMetrics;
|
|
60
109
|
}
|
|
61
110
|
|
|
62
111
|
/**
|