@skillcap/gdh 0.13.2 → 0.14.0
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/INSTALL-BUNDLE.json +1 -1
- package/README.md +4 -4
- package/RELEASE-SPAN-UPDATE-CONTRACTS.json +121 -0
- package/node_modules/@gdh/adapters/package.json +8 -8
- package/node_modules/@gdh/authoring/package.json +2 -2
- package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/cli/dist/index.js +11 -7
- package/node_modules/@gdh/cli/dist/index.js.map +1 -1
- package/node_modules/@gdh/cli/package.json +10 -10
- package/node_modules/@gdh/core/dist/index.d.ts +44 -4
- package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/core/dist/index.js +2 -2
- package/node_modules/@gdh/core/dist/index.js.map +1 -1
- package/node_modules/@gdh/core/package.json +1 -1
- package/node_modules/@gdh/docs/dist/guidance.js +1 -1
- package/node_modules/@gdh/docs/dist/guidance.js.map +1 -1
- package/node_modules/@gdh/docs/package.json +2 -2
- package/node_modules/@gdh/mcp/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/mcp/dist/index.js +13 -0
- package/node_modules/@gdh/mcp/dist/index.js.map +1 -1
- package/node_modules/@gdh/mcp/package.json +8 -8
- package/node_modules/@gdh/observability/dist/runtime-bundles.d.ts.map +1 -1
- package/node_modules/@gdh/observability/dist/runtime-bundles.js +28 -2
- package/node_modules/@gdh/observability/dist/runtime-bundles.js.map +1 -1
- package/node_modules/@gdh/observability/package.json +2 -2
- package/node_modules/@gdh/runtime/dist/bridge-surface.js +187 -9
- package/node_modules/@gdh/runtime/dist/bridge-surface.js.map +1 -1
- package/node_modules/@gdh/runtime/dist/docker-provider.d.ts +4 -2
- package/node_modules/@gdh/runtime/dist/docker-provider.d.ts.map +1 -1
- package/node_modules/@gdh/runtime/dist/docker-provider.js +21 -10
- package/node_modules/@gdh/runtime/dist/docker-provider.js.map +1 -1
- package/node_modules/@gdh/runtime/dist/index.d.ts +1 -1
- package/node_modules/@gdh/runtime/dist/index.d.ts.map +1 -1
- package/node_modules/@gdh/runtime/dist/index.js +561 -23
- package/node_modules/@gdh/runtime/dist/index.js.map +1 -1
- package/node_modules/@gdh/runtime/package.json +2 -2
- package/node_modules/@gdh/scan/package.json +3 -3
- package/node_modules/@gdh/verify/dist/scenarios.d.ts +4 -1
- package/node_modules/@gdh/verify/dist/scenarios.d.ts.map +1 -1
- package/node_modules/@gdh/verify/dist/scenarios.js +447 -69
- package/node_modules/@gdh/verify/dist/scenarios.js.map +1 -1
- package/node_modules/@gdh/verify/package.json +7 -7
- package/package.json +11 -11
|
@@ -90,7 +90,8 @@ export async function checkRuntimeRecipe(input) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
export async function runRuntimeRecipe(input) {
|
|
93
|
-
const
|
|
93
|
+
const screenshotCapture = resolveRuntimeScreenshotCapture(input);
|
|
94
|
+
const screenshotRequested = screenshotCapture === "rendered";
|
|
94
95
|
const liveCheck = await checkRuntimeRecipe(input);
|
|
95
96
|
if (liveCheck.state !== "runnable" || liveCheck.recipe === null) {
|
|
96
97
|
return {
|
|
@@ -161,7 +162,7 @@ export async function runRuntimeRecipe(input) {
|
|
|
161
162
|
const artifactDirectory = input.artifactDirectory ??
|
|
162
163
|
(await createRunArtifactDirectory(input.targetPath, `recipe-${input.recipeId}`));
|
|
163
164
|
await fs.mkdir(artifactDirectory, { recursive: true });
|
|
164
|
-
const preparedScreenshot = buildRuntimeScreenshotCapture(executionCheck.launchPreview, artifactDirectory,
|
|
165
|
+
const preparedScreenshot = buildRuntimeScreenshotCapture(executionCheck.launchPreview, artifactDirectory, screenshotCapture);
|
|
165
166
|
const startedAt = new Date().toISOString();
|
|
166
167
|
let execution = null;
|
|
167
168
|
try {
|
|
@@ -786,12 +787,23 @@ async function buildLaunchPreview(input) {
|
|
|
786
787
|
}));
|
|
787
788
|
const environmentEntries = new Map(environment.map((entry) => [entry.name, entry.value]));
|
|
788
789
|
if (input.provider.resolved === DOCKER_RUNTIME_PROVIDER) {
|
|
790
|
+
const dockerGodotProjectRecipe = input.recipe.launch.kind === "godot_project"
|
|
791
|
+
? input.recipe
|
|
792
|
+
: null;
|
|
793
|
+
const dockerGodotProjectLaunch = dockerGodotProjectRecipe === null
|
|
794
|
+
? null
|
|
795
|
+
: await prepareDockerGodotProjectLaunch({
|
|
796
|
+
evaluationRootPath: input.evaluationRootPath,
|
|
797
|
+
recipe: dockerGodotProjectRecipe,
|
|
798
|
+
});
|
|
789
799
|
const dockerCommand = input.recipe.launch.kind === "godot_project"
|
|
790
800
|
? [
|
|
791
801
|
"/usr/local/bin/godot-linux",
|
|
792
802
|
"--path",
|
|
793
803
|
toDockerContainerPath(input.evaluationRootPath, path.join(input.evaluationRootPath, input.recipe.projectPath)),
|
|
794
|
-
|
|
804
|
+
"--audio-driver",
|
|
805
|
+
"Dummy",
|
|
806
|
+
dockerGodotProjectLaunch?.scenePath ?? input.recipe.launch.scenePath,
|
|
795
807
|
...input.recipe.launch.arguments,
|
|
796
808
|
...parameterArguments,
|
|
797
809
|
...featureArguments,
|
|
@@ -817,6 +829,8 @@ async function buildLaunchPreview(input) {
|
|
|
817
829
|
command: dockerCommand,
|
|
818
830
|
environment: environmentEntries,
|
|
819
831
|
workspaceMode: input.workspaceMode,
|
|
832
|
+
extraDockerArgs: dockerGodotProjectLaunch?.extraDockerArgs ?? [],
|
|
833
|
+
extraCleanupHints: dockerGodotProjectLaunch?.cleanupHints ?? [],
|
|
820
834
|
});
|
|
821
835
|
}
|
|
822
836
|
if (input.recipe.launch.kind === "godot_project") {
|
|
@@ -870,6 +884,143 @@ async function buildLaunchPreview(input) {
|
|
|
870
884
|
cleanupHints: [],
|
|
871
885
|
};
|
|
872
886
|
}
|
|
887
|
+
async function prepareDockerGodotProjectLaunch(input) {
|
|
888
|
+
const projectDirectory = path.join(input.evaluationRootPath, input.recipe.projectPath);
|
|
889
|
+
const projectGodotPath = path.join(projectDirectory, "project.godot");
|
|
890
|
+
const extensionListPath = path.join(projectDirectory, ".godot", "extension_list.cfg");
|
|
891
|
+
const needsUidResolution = input.recipe.launch.scenePath.startsWith("uid://");
|
|
892
|
+
const projectGodotContent = await fs.readFile(projectGodotPath, "utf8").catch(() => null);
|
|
893
|
+
const extensionListContent = await fs.readFile(extensionListPath, "utf8").catch(() => null);
|
|
894
|
+
const projectNeedsAutoloadUidRepair = projectGodotContent !== null && projectGodotContent.includes("uid://");
|
|
895
|
+
if (!needsUidResolution &&
|
|
896
|
+
!projectNeedsAutoloadUidRepair &&
|
|
897
|
+
(extensionListContent === null || !extensionListContent.includes("res://temp/"))) {
|
|
898
|
+
return {
|
|
899
|
+
scenePath: input.recipe.launch.scenePath,
|
|
900
|
+
extraDockerArgs: [],
|
|
901
|
+
cleanupHints: [],
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
const uidLookup = needsUidResolution || projectNeedsAutoloadUidRepair
|
|
905
|
+
? await collectRuntimeUidLookup(projectDirectory)
|
|
906
|
+
: new Map();
|
|
907
|
+
const resolvedScenePath = uidLookup.get(input.recipe.launch.scenePath) ?? input.recipe.launch.scenePath;
|
|
908
|
+
const sanitizedProjectGodot = projectGodotContent === null
|
|
909
|
+
? null
|
|
910
|
+
: sanitizeProjectGodotAutoloadUids(projectGodotContent, uidLookup);
|
|
911
|
+
const sanitizedExtensionList = extensionListContent === null
|
|
912
|
+
? null
|
|
913
|
+
: sanitizeDockerExtensionList(extensionListContent);
|
|
914
|
+
const needsProjectGodotOverlay = projectGodotContent !== null && sanitizedProjectGodot !== projectGodotContent;
|
|
915
|
+
const needsExtensionListOverlay = extensionListContent !== null && sanitizedExtensionList !== extensionListContent;
|
|
916
|
+
if (!needsProjectGodotOverlay && !needsExtensionListOverlay) {
|
|
917
|
+
return {
|
|
918
|
+
scenePath: resolvedScenePath,
|
|
919
|
+
extraDockerArgs: [],
|
|
920
|
+
cleanupHints: [],
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const overlayRoot = await fs.mkdtemp(path.join(os.tmpdir(), "gdh-docker-overlay-"));
|
|
924
|
+
const extraDockerArgs = [];
|
|
925
|
+
if (needsProjectGodotOverlay && sanitizedProjectGodot !== null) {
|
|
926
|
+
const overlayProjectGodotPath = path.join(overlayRoot, "project.godot");
|
|
927
|
+
await fs.writeFile(overlayProjectGodotPath, sanitizedProjectGodot, "utf8");
|
|
928
|
+
extraDockerArgs.push("--mount", `type=bind,src=${overlayProjectGodotPath},dst=${toDockerContainerPath(input.evaluationRootPath, projectGodotPath)},readonly`);
|
|
929
|
+
}
|
|
930
|
+
if (needsExtensionListOverlay && sanitizedExtensionList !== null) {
|
|
931
|
+
const overlayExtensionListPath = path.join(overlayRoot, ".godot", "extension_list.cfg");
|
|
932
|
+
await fs.mkdir(path.dirname(overlayExtensionListPath), { recursive: true });
|
|
933
|
+
await fs.writeFile(overlayExtensionListPath, sanitizedExtensionList, "utf8");
|
|
934
|
+
extraDockerArgs.push("--mount", `type=bind,src=${overlayExtensionListPath},dst=${toDockerContainerPath(input.evaluationRootPath, extensionListPath)},readonly`);
|
|
935
|
+
}
|
|
936
|
+
return {
|
|
937
|
+
scenePath: resolvedScenePath,
|
|
938
|
+
extraDockerArgs,
|
|
939
|
+
cleanupHints: [
|
|
940
|
+
{
|
|
941
|
+
kind: "temporary_path",
|
|
942
|
+
path: overlayRoot,
|
|
943
|
+
},
|
|
944
|
+
],
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
async function collectRuntimeUidLookup(projectDirectory) {
|
|
948
|
+
const lookup = new Map();
|
|
949
|
+
async function walk(currentPath) {
|
|
950
|
+
const entries = await fs.readdir(currentPath, { withFileTypes: true }).catch(() => []);
|
|
951
|
+
for (const entry of entries) {
|
|
952
|
+
const absolutePath = path.join(currentPath, entry.name);
|
|
953
|
+
if (entry.isDirectory()) {
|
|
954
|
+
if (entry.name === ".git" || entry.name === ".gdh-state") {
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
await walk(absolutePath);
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (!entry.isFile() || !entry.name.endsWith(".uid")) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const uid = await fs.readFile(absolutePath, "utf8").catch(() => null);
|
|
964
|
+
if (uid === null) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const normalizedUid = uid.trim();
|
|
968
|
+
if (!normalizedUid.startsWith("uid://")) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const relativePath = path.relative(projectDirectory, absolutePath).split(path.sep).join("/");
|
|
972
|
+
lookup.set(normalizedUid, `res://${relativePath.slice(0, -".uid".length)}`);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
await walk(projectDirectory);
|
|
976
|
+
return lookup;
|
|
977
|
+
}
|
|
978
|
+
function sanitizeProjectGodotAutoloadUids(content, uidLookup) {
|
|
979
|
+
const lines = content.split(/\r?\n/);
|
|
980
|
+
const section = findProjectSettingSection(lines, "autoload");
|
|
981
|
+
if (section === null) {
|
|
982
|
+
return content;
|
|
983
|
+
}
|
|
984
|
+
const nextLines = [...lines];
|
|
985
|
+
for (let index = section.start + 1; index < section.end; index += 1) {
|
|
986
|
+
const line = lines[index] ?? "";
|
|
987
|
+
const match = /^(\s*[^=\s]+)\s*=\s*"(\*?)(uid:\/\/[^"]+)"\s*$/.exec(line);
|
|
988
|
+
if (match === null) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
const key = match[1] ?? "";
|
|
992
|
+
const prefix = match[2] ?? "";
|
|
993
|
+
const uid = match[3] ?? "";
|
|
994
|
+
const resolvedPath = uidLookup.get(uid) ?? null;
|
|
995
|
+
if (resolvedPath === null) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
nextLines[index] = `${key}="${prefix}${resolvedPath}"`;
|
|
999
|
+
}
|
|
1000
|
+
return `${nextLines.join("\n").replace(/\n+$/, "\n")}`;
|
|
1001
|
+
}
|
|
1002
|
+
function sanitizeDockerExtensionList(content) {
|
|
1003
|
+
const nextLines = content
|
|
1004
|
+
.split(/\r?\n/)
|
|
1005
|
+
.filter((line) => !line.startsWith("res://temp/"));
|
|
1006
|
+
return `${nextLines.join("\n").replace(/\n+$/, "\n")}`;
|
|
1007
|
+
}
|
|
1008
|
+
function findProjectSettingSection(lines, name) {
|
|
1009
|
+
const header = `[${name}]`;
|
|
1010
|
+
const start = lines.findIndex((line) => line.trim() === header);
|
|
1011
|
+
if (start < 0) {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
let end = lines.length;
|
|
1015
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
1016
|
+
const line = lines[index]?.trim() ?? "";
|
|
1017
|
+
if (line.startsWith("[") && line.endsWith("]")) {
|
|
1018
|
+
end = index;
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
return { start, end };
|
|
1023
|
+
}
|
|
873
1024
|
function bridgeNotRequired() {
|
|
874
1025
|
return {
|
|
875
1026
|
required: false,
|
|
@@ -927,8 +1078,8 @@ async function createRunArtifactDirectory(targetPath, recipeId) {
|
|
|
927
1078
|
await fs.mkdir(directory, { recursive: true });
|
|
928
1079
|
return directory;
|
|
929
1080
|
}
|
|
930
|
-
function buildRuntimeScreenshotCapture(preview, artifactDirectory,
|
|
931
|
-
if (
|
|
1081
|
+
function buildRuntimeScreenshotCapture(preview, artifactDirectory, screenshotCapture) {
|
|
1082
|
+
if (screenshotCapture !== "rendered") {
|
|
932
1083
|
return {
|
|
933
1084
|
preview,
|
|
934
1085
|
result: buildOmittedRuntimeScreenshotResult(),
|
|
@@ -1206,6 +1357,16 @@ function buildUnavailableRuntimeScreenshotResult(summary, reason) {
|
|
|
1206
1357
|
metadataPath: null,
|
|
1207
1358
|
};
|
|
1208
1359
|
}
|
|
1360
|
+
function resolveRuntimeScreenshotCapture(input) {
|
|
1361
|
+
if (input.screenshotCapture === "rendered") {
|
|
1362
|
+
return "rendered";
|
|
1363
|
+
}
|
|
1364
|
+
if (input.screenshotCapture === "never") {
|
|
1365
|
+
return "never";
|
|
1366
|
+
}
|
|
1367
|
+
const legacyScreenshotPolicy = input.screenshotPolicy;
|
|
1368
|
+
return legacyScreenshotPolicy === "fallback" ? "rendered" : "never";
|
|
1369
|
+
}
|
|
1209
1370
|
async function executeCommand(preview, artifactDirectory, launchKind, maxRuntimeSeconds) {
|
|
1210
1371
|
const command = preview.command;
|
|
1211
1372
|
if (command === null || command.length === 0) {
|
|
@@ -1353,6 +1514,10 @@ async function applyLaunchCleanupHints(hints) {
|
|
|
1353
1514
|
for (const hint of hints) {
|
|
1354
1515
|
if (hint.kind === "docker_container") {
|
|
1355
1516
|
await removeDockerContainer(hint.containerName);
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (hint.kind === "temporary_path") {
|
|
1520
|
+
await fs.rm(hint.path, { recursive: true, force: true });
|
|
1356
1521
|
}
|
|
1357
1522
|
}
|
|
1358
1523
|
}
|
|
@@ -1376,6 +1541,7 @@ async function removeDockerContainer(containerName) {
|
|
|
1376
1541
|
const BRIDGE_SESSION_DIRECTORY = ".gdh-state/bridge-sessions";
|
|
1377
1542
|
const BRIDGE_LAUNCH_HOLD_ITERATIONS = "3600";
|
|
1378
1543
|
const BRIDGE_HANDSHAKE_TIMEOUT_MS = 30000;
|
|
1544
|
+
const BRIDGE_DOCKER_HANDSHAKE_TIMEOUT_MS = 90000;
|
|
1379
1545
|
const HOST_RUNTIME_BRIDGE_ENTRIES = [
|
|
1380
1546
|
{
|
|
1381
1547
|
id: "state.node_property.await",
|
|
@@ -1386,6 +1552,24 @@ const HOST_RUNTIME_BRIDGE_ENTRIES = [
|
|
|
1386
1552
|
executionRoute: null,
|
|
1387
1553
|
safetyLevel: "read_only",
|
|
1388
1554
|
},
|
|
1555
|
+
{
|
|
1556
|
+
id: "state.node_presence.await",
|
|
1557
|
+
summary: "Wait for one exact node path to become present or absent through bounded polling.",
|
|
1558
|
+
kind: "waiter",
|
|
1559
|
+
shape: "composite",
|
|
1560
|
+
source: "core",
|
|
1561
|
+
executionRoute: null,
|
|
1562
|
+
safetyLevel: "read_only",
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
id: "state.signal.await",
|
|
1566
|
+
summary: "Wait for one observed signal emission to occur after an optional observed-count baseline.",
|
|
1567
|
+
kind: "waiter",
|
|
1568
|
+
shape: "composite",
|
|
1569
|
+
source: "core",
|
|
1570
|
+
executionRoute: null,
|
|
1571
|
+
safetyLevel: "read_only",
|
|
1572
|
+
},
|
|
1389
1573
|
];
|
|
1390
1574
|
export function createRuntimeBridgeManager() {
|
|
1391
1575
|
const sessions = new Map();
|
|
@@ -1515,7 +1699,9 @@ export function createRuntimeBridgeManager() {
|
|
|
1515
1699
|
port: bridgePort,
|
|
1516
1700
|
token: bridgeToken,
|
|
1517
1701
|
sessionId,
|
|
1518
|
-
timeoutMs:
|
|
1702
|
+
timeoutMs: executionCheck.provider?.resolved === DOCKER_RUNTIME_PROVIDER
|
|
1703
|
+
? BRIDGE_DOCKER_HANDSHAKE_TIMEOUT_MS
|
|
1704
|
+
: BRIDGE_HANDSHAKE_TIMEOUT_MS,
|
|
1519
1705
|
});
|
|
1520
1706
|
startedSocket = ws;
|
|
1521
1707
|
const entries = mergeRuntimeBridgeEntries(await listBridgeEntriesFromSocket(ws, sessionId));
|
|
@@ -1648,6 +1834,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1648
1834
|
summary: `Runtime bridge session "${sessionId}" was not found.`,
|
|
1649
1835
|
reasons: ["bridge_session_not_found"],
|
|
1650
1836
|
result: null,
|
|
1837
|
+
waiterEvidence: null,
|
|
1651
1838
|
transcriptPath: null,
|
|
1652
1839
|
};
|
|
1653
1840
|
}
|
|
@@ -1659,6 +1846,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1659
1846
|
summary: `Runtime bridge session "${sessionId}" is no longer active.`,
|
|
1660
1847
|
reasons: ["bridge_session_not_active"],
|
|
1661
1848
|
result: null,
|
|
1849
|
+
waiterEvidence: null,
|
|
1662
1850
|
transcriptPath: session.transcriptPath,
|
|
1663
1851
|
};
|
|
1664
1852
|
}
|
|
@@ -1670,6 +1858,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1670
1858
|
summary: `Runtime bridge session "${sessionId}" is already handling another invocation.`,
|
|
1671
1859
|
reasons: ["bridge_session_busy"],
|
|
1672
1860
|
result: null,
|
|
1861
|
+
waiterEvidence: null,
|
|
1673
1862
|
transcriptPath: session.transcriptPath,
|
|
1674
1863
|
};
|
|
1675
1864
|
}
|
|
@@ -1696,6 +1885,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1696
1885
|
: `Bridge entry "${entryId}" returned state "${syntheticResponse.state}".`,
|
|
1697
1886
|
reasons: syntheticResponse.error ? [syntheticResponse.error] : [],
|
|
1698
1887
|
result: syntheticResponse.result,
|
|
1888
|
+
waiterEvidence: syntheticResponse.waiterEvidence,
|
|
1699
1889
|
transcriptPath: session.transcriptPath,
|
|
1700
1890
|
};
|
|
1701
1891
|
}
|
|
@@ -1715,6 +1905,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1715
1905
|
: `Bridge entry "${entryId}" returned state "${response.state}".`,
|
|
1716
1906
|
reasons: response.error ? [response.error] : [],
|
|
1717
1907
|
result: response.result,
|
|
1908
|
+
waiterEvidence: null,
|
|
1718
1909
|
transcriptPath: session.transcriptPath,
|
|
1719
1910
|
};
|
|
1720
1911
|
}
|
|
@@ -1730,6 +1921,7 @@ export function createRuntimeBridgeManager() {
|
|
|
1730
1921
|
summary: `Bridge entry "${entryId}" failed: ${formatBridgeError(error)}.`,
|
|
1731
1922
|
reasons: ["bridge_invoke_failed"],
|
|
1732
1923
|
result: null,
|
|
1924
|
+
waiterEvidence: null,
|
|
1733
1925
|
transcriptPath: session.transcriptPath,
|
|
1734
1926
|
};
|
|
1735
1927
|
}
|
|
@@ -1833,6 +2025,12 @@ async function invokeSyntheticBridgeEntry(session, entryId, inputValue) {
|
|
|
1833
2025
|
if (entryId === "state.node_property.await") {
|
|
1834
2026
|
return invokeNodePropertyAwaitEntry(session, inputValue);
|
|
1835
2027
|
}
|
|
2028
|
+
if (entryId === "state.node_presence.await") {
|
|
2029
|
+
return invokeNodePresenceAwaitEntry(session, inputValue);
|
|
2030
|
+
}
|
|
2031
|
+
if (entryId === "state.signal.await") {
|
|
2032
|
+
return invokeSignalAwaitEntry(session, inputValue);
|
|
2033
|
+
}
|
|
1836
2034
|
return null;
|
|
1837
2035
|
}
|
|
1838
2036
|
async function invokeNodePropertyAwaitEntry(session, inputValue) {
|
|
@@ -1848,6 +2046,7 @@ async function invokeNodePropertyAwaitEntry(session, inputValue) {
|
|
|
1848
2046
|
state: "failed",
|
|
1849
2047
|
result: null,
|
|
1850
2048
|
error: "state.node_property.await requires nodePath and property.",
|
|
2049
|
+
waiterEvidence: null,
|
|
1851
2050
|
};
|
|
1852
2051
|
}
|
|
1853
2052
|
if (!hasExpected) {
|
|
@@ -1855,12 +2054,20 @@ async function invokeNodePropertyAwaitEntry(session, inputValue) {
|
|
|
1855
2054
|
state: "failed",
|
|
1856
2055
|
result: null,
|
|
1857
2056
|
error: "state.node_property.await requires an expected value.",
|
|
2057
|
+
waiterEvidence: null,
|
|
1858
2058
|
};
|
|
1859
2059
|
}
|
|
1860
|
-
const
|
|
2060
|
+
const target = {
|
|
2061
|
+
nodePath,
|
|
2062
|
+
property,
|
|
2063
|
+
expected,
|
|
2064
|
+
};
|
|
2065
|
+
const startedAt = new Date().toISOString();
|
|
2066
|
+
const startedAtMs = Date.now();
|
|
2067
|
+
recordWaiterStarted(session, "state.node_property.await", target, timeoutMs, pollIntervalMs);
|
|
1861
2068
|
let attempts = 0;
|
|
1862
2069
|
let lastValue = null;
|
|
1863
|
-
while (Date.now() -
|
|
2070
|
+
while (Date.now() - startedAtMs <= timeoutMs) {
|
|
1864
2071
|
attempts += 1;
|
|
1865
2072
|
recordBridgeEvent(session, "invoke_expanded_leaf_step", {
|
|
1866
2073
|
parentEntryId: "state.node_property.await",
|
|
@@ -1888,37 +2095,326 @@ async function invokeNodePropertyAwaitEntry(session, inputValue) {
|
|
|
1888
2095
|
state: response.state,
|
|
1889
2096
|
result: response.result,
|
|
1890
2097
|
error: response.error,
|
|
2098
|
+
waiterEvidence: null,
|
|
1891
2099
|
};
|
|
1892
2100
|
}
|
|
1893
2101
|
const payload = toJsonRecord(response.result);
|
|
1894
2102
|
lastValue = (payload["value"] ?? null);
|
|
1895
2103
|
if (jsonValuesEqual(lastValue, expected)) {
|
|
2104
|
+
const result = {
|
|
2105
|
+
nodePath,
|
|
2106
|
+
property,
|
|
2107
|
+
expected,
|
|
2108
|
+
value: lastValue,
|
|
2109
|
+
attempts,
|
|
2110
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2111
|
+
};
|
|
2112
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2113
|
+
waiterId: "state.node_property.await",
|
|
2114
|
+
outcome: "satisfied",
|
|
2115
|
+
startedAt,
|
|
2116
|
+
timeoutMs,
|
|
2117
|
+
pollCount: attempts,
|
|
2118
|
+
target,
|
|
2119
|
+
result,
|
|
2120
|
+
});
|
|
2121
|
+
recordWaiterSatisfied(session, "state.node_property.await", target, attempts, result);
|
|
1896
2122
|
return {
|
|
1897
2123
|
state: "ok",
|
|
1898
|
-
result
|
|
1899
|
-
nodePath,
|
|
1900
|
-
property,
|
|
1901
|
-
expected,
|
|
1902
|
-
value: lastValue,
|
|
1903
|
-
attempts,
|
|
1904
|
-
elapsedMs: Date.now() - startedAt,
|
|
1905
|
-
},
|
|
2124
|
+
result,
|
|
1906
2125
|
error: null,
|
|
2126
|
+
waiterEvidence,
|
|
1907
2127
|
};
|
|
1908
2128
|
}
|
|
1909
2129
|
await waitMs(pollIntervalMs);
|
|
1910
2130
|
}
|
|
2131
|
+
const result = {
|
|
2132
|
+
nodePath,
|
|
2133
|
+
property,
|
|
2134
|
+
expected,
|
|
2135
|
+
value: lastValue,
|
|
2136
|
+
attempts,
|
|
2137
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2138
|
+
};
|
|
2139
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2140
|
+
waiterId: "state.node_property.await",
|
|
2141
|
+
outcome: "timed_out",
|
|
2142
|
+
startedAt,
|
|
2143
|
+
timeoutMs,
|
|
2144
|
+
pollCount: attempts,
|
|
2145
|
+
target,
|
|
2146
|
+
result,
|
|
2147
|
+
});
|
|
2148
|
+
recordWaiterTimedOut(session, "state.node_property.await", target, attempts, result);
|
|
1911
2149
|
return {
|
|
1912
2150
|
state: "unavailable",
|
|
1913
|
-
result
|
|
2151
|
+
result,
|
|
2152
|
+
error: "Timed out waiting for the requested node property value.",
|
|
2153
|
+
waiterEvidence,
|
|
2154
|
+
};
|
|
2155
|
+
}
|
|
2156
|
+
async function invokeNodePresenceAwaitEntry(session, inputValue) {
|
|
2157
|
+
const input = toJsonRecord(inputValue);
|
|
2158
|
+
const nodePath = typeof input["nodePath"] === "string" ? input["nodePath"] : "";
|
|
2159
|
+
const expected = typeof input["expected"] === "string" ? input["expected"] : "";
|
|
2160
|
+
const timeoutMs = clampNumber(input["timeoutMs"], 1500, 100, 5000);
|
|
2161
|
+
const pollIntervalMs = clampNumber(input["pollIntervalMs"], 50, 25, 250);
|
|
2162
|
+
if (nodePath.length === 0) {
|
|
2163
|
+
return {
|
|
2164
|
+
state: "failed",
|
|
2165
|
+
result: null,
|
|
2166
|
+
error: "state.node_presence.await requires nodePath.",
|
|
2167
|
+
waiterEvidence: null,
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
if (expected !== "present" && expected !== "absent") {
|
|
2171
|
+
return {
|
|
2172
|
+
state: "failed",
|
|
2173
|
+
result: null,
|
|
2174
|
+
error: 'state.node_presence.await requires expected = "present" or "absent".',
|
|
2175
|
+
waiterEvidence: null,
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
const target = {
|
|
2179
|
+
nodePath,
|
|
2180
|
+
expected,
|
|
2181
|
+
};
|
|
2182
|
+
recordWaiterStarted(session, "state.node_presence.await", target, timeoutMs, pollIntervalMs);
|
|
2183
|
+
const startedAt = new Date().toISOString();
|
|
2184
|
+
const startedAtMs = Date.now();
|
|
2185
|
+
let attempts = 0;
|
|
2186
|
+
let lastSnapshot = null;
|
|
2187
|
+
while (Date.now() - startedAtMs <= timeoutMs) {
|
|
2188
|
+
attempts += 1;
|
|
2189
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_step", {
|
|
2190
|
+
parentEntryId: "state.node_presence.await",
|
|
2191
|
+
leafEntryId: "state.node_presence.get",
|
|
2192
|
+
attempt: attempts,
|
|
2193
|
+
input: {
|
|
2194
|
+
nodePath,
|
|
2195
|
+
},
|
|
2196
|
+
});
|
|
2197
|
+
const response = await invokeBridgeEntryOverSocket(session.ws, session.sessionId, "state.node_presence.get", {
|
|
1914
2198
|
nodePath,
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
2199
|
+
});
|
|
2200
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_response", {
|
|
2201
|
+
parentEntryId: "state.node_presence.await",
|
|
2202
|
+
leafEntryId: "state.node_presence.get",
|
|
2203
|
+
attempt: attempts,
|
|
2204
|
+
state: response.state,
|
|
2205
|
+
result: response.result,
|
|
2206
|
+
error: response.error,
|
|
2207
|
+
});
|
|
2208
|
+
if (response.state !== "ok") {
|
|
2209
|
+
return {
|
|
2210
|
+
state: response.state,
|
|
2211
|
+
result: response.result,
|
|
2212
|
+
error: response.error,
|
|
2213
|
+
waiterEvidence: null,
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
const payload = toJsonRecord(response.result);
|
|
2217
|
+
lastSnapshot = payload;
|
|
2218
|
+
const present = payload["present"] === true;
|
|
2219
|
+
const matches = expected === "present" ? present : !present;
|
|
2220
|
+
if (matches) {
|
|
2221
|
+
const result = {
|
|
2222
|
+
nodePath,
|
|
2223
|
+
expected,
|
|
2224
|
+
present,
|
|
2225
|
+
name: (payload["name"] ?? null),
|
|
2226
|
+
className: (payload["className"] ?? null),
|
|
2227
|
+
attempts,
|
|
2228
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2229
|
+
};
|
|
2230
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2231
|
+
waiterId: "state.node_presence.await",
|
|
2232
|
+
outcome: "satisfied",
|
|
2233
|
+
startedAt,
|
|
2234
|
+
timeoutMs,
|
|
2235
|
+
pollCount: attempts,
|
|
2236
|
+
target,
|
|
2237
|
+
result,
|
|
2238
|
+
});
|
|
2239
|
+
recordWaiterSatisfied(session, "state.node_presence.await", target, attempts, result);
|
|
2240
|
+
return {
|
|
2241
|
+
state: "ok",
|
|
2242
|
+
result,
|
|
2243
|
+
error: null,
|
|
2244
|
+
waiterEvidence,
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
await waitMs(pollIntervalMs);
|
|
2248
|
+
}
|
|
2249
|
+
const result = {
|
|
2250
|
+
nodePath,
|
|
2251
|
+
expected,
|
|
2252
|
+
present: lastSnapshot?.["present"] === true,
|
|
2253
|
+
name: (lastSnapshot?.["name"] ?? null),
|
|
2254
|
+
className: (lastSnapshot?.["className"] ?? null),
|
|
2255
|
+
attempts,
|
|
2256
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2257
|
+
};
|
|
2258
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2259
|
+
waiterId: "state.node_presence.await",
|
|
2260
|
+
outcome: "timed_out",
|
|
2261
|
+
startedAt,
|
|
2262
|
+
timeoutMs,
|
|
2263
|
+
pollCount: attempts,
|
|
2264
|
+
target,
|
|
2265
|
+
result,
|
|
2266
|
+
});
|
|
2267
|
+
recordWaiterTimedOut(session, "state.node_presence.await", target, attempts, result);
|
|
2268
|
+
return {
|
|
2269
|
+
state: "unavailable",
|
|
2270
|
+
result,
|
|
2271
|
+
error: "Timed out waiting for the requested node presence state.",
|
|
2272
|
+
waiterEvidence,
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
async function invokeSignalAwaitEntry(session, inputValue) {
|
|
2276
|
+
const input = toJsonRecord(inputValue);
|
|
2277
|
+
const nodePath = typeof input["nodePath"] === "string" ? input["nodePath"] : "";
|
|
2278
|
+
const signalName = typeof input["signalName"] === "string" ? input["signalName"] : "";
|
|
2279
|
+
const afterCount = clampCount(input["afterCount"], 0);
|
|
2280
|
+
const timeoutMs = clampNumber(input["timeoutMs"], 1500, 100, 5000);
|
|
2281
|
+
const pollIntervalMs = clampNumber(input["pollIntervalMs"], 50, 25, 250);
|
|
2282
|
+
if (nodePath.length === 0 || signalName.length === 0) {
|
|
2283
|
+
return {
|
|
2284
|
+
state: "failed",
|
|
2285
|
+
result: null,
|
|
2286
|
+
error: "state.signal.await requires nodePath and signalName.",
|
|
2287
|
+
waiterEvidence: null,
|
|
2288
|
+
};
|
|
2289
|
+
}
|
|
2290
|
+
const target = {
|
|
2291
|
+
nodePath,
|
|
2292
|
+
signalName,
|
|
2293
|
+
afterCount,
|
|
2294
|
+
};
|
|
2295
|
+
recordWaiterStarted(session, "state.signal.await", target, timeoutMs, pollIntervalMs);
|
|
2296
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_step", {
|
|
2297
|
+
parentEntryId: "state.signal.await",
|
|
2298
|
+
leafEntryId: "state.signal_observation.start",
|
|
2299
|
+
attempt: 1,
|
|
2300
|
+
input: {
|
|
2301
|
+
nodePath,
|
|
2302
|
+
signalName,
|
|
1920
2303
|
},
|
|
1921
|
-
|
|
2304
|
+
});
|
|
2305
|
+
const startResponse = await invokeBridgeEntryOverSocket(session.ws, session.sessionId, "state.signal_observation.start", {
|
|
2306
|
+
nodePath,
|
|
2307
|
+
signalName,
|
|
2308
|
+
});
|
|
2309
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_response", {
|
|
2310
|
+
parentEntryId: "state.signal.await",
|
|
2311
|
+
leafEntryId: "state.signal_observation.start",
|
|
2312
|
+
attempt: 1,
|
|
2313
|
+
state: startResponse.state,
|
|
2314
|
+
result: startResponse.result,
|
|
2315
|
+
error: startResponse.error,
|
|
2316
|
+
});
|
|
2317
|
+
if (startResponse.state !== "ok") {
|
|
2318
|
+
return {
|
|
2319
|
+
state: startResponse.state,
|
|
2320
|
+
result: startResponse.result,
|
|
2321
|
+
error: startResponse.error,
|
|
2322
|
+
waiterEvidence: null,
|
|
2323
|
+
};
|
|
2324
|
+
}
|
|
2325
|
+
const startedAt = new Date().toISOString();
|
|
2326
|
+
const startedAtMs = Date.now();
|
|
2327
|
+
let attempts = 0;
|
|
2328
|
+
let lastSnapshot = null;
|
|
2329
|
+
while (Date.now() - startedAtMs <= timeoutMs) {
|
|
2330
|
+
attempts += 1;
|
|
2331
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_step", {
|
|
2332
|
+
parentEntryId: "state.signal.await",
|
|
2333
|
+
leafEntryId: "state.signal_observation.get",
|
|
2334
|
+
attempt: attempts,
|
|
2335
|
+
input: {
|
|
2336
|
+
nodePath,
|
|
2337
|
+
signalName,
|
|
2338
|
+
},
|
|
2339
|
+
});
|
|
2340
|
+
const response = await invokeBridgeEntryOverSocket(session.ws, session.sessionId, "state.signal_observation.get", {
|
|
2341
|
+
nodePath,
|
|
2342
|
+
signalName,
|
|
2343
|
+
});
|
|
2344
|
+
recordBridgeEvent(session, "invoke_expanded_leaf_response", {
|
|
2345
|
+
parentEntryId: "state.signal.await",
|
|
2346
|
+
leafEntryId: "state.signal_observation.get",
|
|
2347
|
+
attempt: attempts,
|
|
2348
|
+
state: response.state,
|
|
2349
|
+
result: response.result,
|
|
2350
|
+
error: response.error,
|
|
2351
|
+
});
|
|
2352
|
+
if (response.state !== "ok") {
|
|
2353
|
+
return {
|
|
2354
|
+
state: response.state,
|
|
2355
|
+
result: response.result,
|
|
2356
|
+
error: response.error,
|
|
2357
|
+
waiterEvidence: null,
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
const payload = toJsonRecord(response.result);
|
|
2361
|
+
lastSnapshot = payload;
|
|
2362
|
+
const count = clampCount(payload["count"], 0);
|
|
2363
|
+
if (count > afterCount) {
|
|
2364
|
+
const result = {
|
|
2365
|
+
nodePath,
|
|
2366
|
+
signalName,
|
|
2367
|
+
afterCount,
|
|
2368
|
+
count,
|
|
2369
|
+
lastObservedAt: (payload["lastObservedAt"] ?? null),
|
|
2370
|
+
lastPayload: (payload["lastPayload"] ?? null),
|
|
2371
|
+
attempts,
|
|
2372
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2373
|
+
};
|
|
2374
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2375
|
+
waiterId: "state.signal.await",
|
|
2376
|
+
outcome: "satisfied",
|
|
2377
|
+
startedAt,
|
|
2378
|
+
timeoutMs,
|
|
2379
|
+
pollCount: attempts,
|
|
2380
|
+
target,
|
|
2381
|
+
result,
|
|
2382
|
+
});
|
|
2383
|
+
recordWaiterSatisfied(session, "state.signal.await", target, attempts, result);
|
|
2384
|
+
return {
|
|
2385
|
+
state: "ok",
|
|
2386
|
+
result,
|
|
2387
|
+
error: null,
|
|
2388
|
+
waiterEvidence,
|
|
2389
|
+
};
|
|
2390
|
+
}
|
|
2391
|
+
await waitMs(pollIntervalMs);
|
|
2392
|
+
}
|
|
2393
|
+
const result = {
|
|
2394
|
+
nodePath,
|
|
2395
|
+
signalName,
|
|
2396
|
+
afterCount,
|
|
2397
|
+
count: clampCount(lastSnapshot?.["count"], 0),
|
|
2398
|
+
lastObservedAt: (lastSnapshot?.["lastObservedAt"] ?? null),
|
|
2399
|
+
lastPayload: (lastSnapshot?.["lastPayload"] ?? null),
|
|
2400
|
+
attempts,
|
|
2401
|
+
elapsedMs: Date.now() - startedAtMs,
|
|
2402
|
+
};
|
|
2403
|
+
const waiterEvidence = buildWaiterEvidence({
|
|
2404
|
+
waiterId: "state.signal.await",
|
|
2405
|
+
outcome: "timed_out",
|
|
2406
|
+
startedAt,
|
|
2407
|
+
timeoutMs,
|
|
2408
|
+
pollCount: attempts,
|
|
2409
|
+
target,
|
|
2410
|
+
result,
|
|
2411
|
+
});
|
|
2412
|
+
recordWaiterTimedOut(session, "state.signal.await", target, attempts, result);
|
|
2413
|
+
return {
|
|
2414
|
+
state: "unavailable",
|
|
2415
|
+
result,
|
|
2416
|
+
error: "Timed out waiting for the requested signal emission.",
|
|
2417
|
+
waiterEvidence,
|
|
1922
2418
|
};
|
|
1923
2419
|
}
|
|
1924
2420
|
function toJsonRecord(value) {
|
|
@@ -1933,6 +2429,12 @@ function clampNumber(value, fallback, minimum, maximum) {
|
|
|
1933
2429
|
}
|
|
1934
2430
|
return Math.max(minimum, Math.min(maximum, Math.trunc(value)));
|
|
1935
2431
|
}
|
|
2432
|
+
function clampCount(value, fallback) {
|
|
2433
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2434
|
+
return fallback;
|
|
2435
|
+
}
|
|
2436
|
+
return Math.max(0, Math.trunc(value));
|
|
2437
|
+
}
|
|
1936
2438
|
function jsonValuesEqual(left, right) {
|
|
1937
2439
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
1938
2440
|
}
|
|
@@ -1957,6 +2459,42 @@ function recordBridgeEvent(session, event, payload) {
|
|
|
1957
2459
|
...payload,
|
|
1958
2460
|
});
|
|
1959
2461
|
}
|
|
2462
|
+
function recordWaiterStarted(session, entryId, target, timeoutMs, pollIntervalMs) {
|
|
2463
|
+
recordBridgeEvent(session, "invoke_waiter_started", {
|
|
2464
|
+
entryId,
|
|
2465
|
+
target,
|
|
2466
|
+
timeoutMs,
|
|
2467
|
+
pollIntervalMs,
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
function recordWaiterSatisfied(session, entryId, target, attempts, result) {
|
|
2471
|
+
recordBridgeEvent(session, "invoke_waiter_satisfied", {
|
|
2472
|
+
entryId,
|
|
2473
|
+
target,
|
|
2474
|
+
attempts,
|
|
2475
|
+
result,
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
function recordWaiterTimedOut(session, entryId, target, attempts, result) {
|
|
2479
|
+
recordBridgeEvent(session, "invoke_waiter_timed_out", {
|
|
2480
|
+
entryId,
|
|
2481
|
+
target,
|
|
2482
|
+
attempts,
|
|
2483
|
+
result,
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
function buildWaiterEvidence(input) {
|
|
2487
|
+
return {
|
|
2488
|
+
waiterId: input.waiterId,
|
|
2489
|
+
outcome: input.outcome,
|
|
2490
|
+
startedAt: input.startedAt,
|
|
2491
|
+
finishedAt: new Date().toISOString(),
|
|
2492
|
+
timeoutMs: input.timeoutMs,
|
|
2493
|
+
pollCount: input.pollCount,
|
|
2494
|
+
target: input.target,
|
|
2495
|
+
result: input.result,
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
1960
2498
|
function bridgeMissing(reason) {
|
|
1961
2499
|
return {
|
|
1962
2500
|
required: true,
|