@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.
Files changed (43) hide show
  1. package/INSTALL-BUNDLE.json +1 -1
  2. package/README.md +4 -4
  3. package/RELEASE-SPAN-UPDATE-CONTRACTS.json +121 -0
  4. package/node_modules/@gdh/adapters/package.json +8 -8
  5. package/node_modules/@gdh/authoring/package.json +2 -2
  6. package/node_modules/@gdh/cli/dist/index.d.ts.map +1 -1
  7. package/node_modules/@gdh/cli/dist/index.js +11 -7
  8. package/node_modules/@gdh/cli/dist/index.js.map +1 -1
  9. package/node_modules/@gdh/cli/package.json +10 -10
  10. package/node_modules/@gdh/core/dist/index.d.ts +44 -4
  11. package/node_modules/@gdh/core/dist/index.d.ts.map +1 -1
  12. package/node_modules/@gdh/core/dist/index.js +2 -2
  13. package/node_modules/@gdh/core/dist/index.js.map +1 -1
  14. package/node_modules/@gdh/core/package.json +1 -1
  15. package/node_modules/@gdh/docs/dist/guidance.js +1 -1
  16. package/node_modules/@gdh/docs/dist/guidance.js.map +1 -1
  17. package/node_modules/@gdh/docs/package.json +2 -2
  18. package/node_modules/@gdh/mcp/dist/index.d.ts.map +1 -1
  19. package/node_modules/@gdh/mcp/dist/index.js +13 -0
  20. package/node_modules/@gdh/mcp/dist/index.js.map +1 -1
  21. package/node_modules/@gdh/mcp/package.json +8 -8
  22. package/node_modules/@gdh/observability/dist/runtime-bundles.d.ts.map +1 -1
  23. package/node_modules/@gdh/observability/dist/runtime-bundles.js +28 -2
  24. package/node_modules/@gdh/observability/dist/runtime-bundles.js.map +1 -1
  25. package/node_modules/@gdh/observability/package.json +2 -2
  26. package/node_modules/@gdh/runtime/dist/bridge-surface.js +187 -9
  27. package/node_modules/@gdh/runtime/dist/bridge-surface.js.map +1 -1
  28. package/node_modules/@gdh/runtime/dist/docker-provider.d.ts +4 -2
  29. package/node_modules/@gdh/runtime/dist/docker-provider.d.ts.map +1 -1
  30. package/node_modules/@gdh/runtime/dist/docker-provider.js +21 -10
  31. package/node_modules/@gdh/runtime/dist/docker-provider.js.map +1 -1
  32. package/node_modules/@gdh/runtime/dist/index.d.ts +1 -1
  33. package/node_modules/@gdh/runtime/dist/index.d.ts.map +1 -1
  34. package/node_modules/@gdh/runtime/dist/index.js +561 -23
  35. package/node_modules/@gdh/runtime/dist/index.js.map +1 -1
  36. package/node_modules/@gdh/runtime/package.json +2 -2
  37. package/node_modules/@gdh/scan/package.json +3 -3
  38. package/node_modules/@gdh/verify/dist/scenarios.d.ts +4 -1
  39. package/node_modules/@gdh/verify/dist/scenarios.d.ts.map +1 -1
  40. package/node_modules/@gdh/verify/dist/scenarios.js +447 -69
  41. package/node_modules/@gdh/verify/dist/scenarios.js.map +1 -1
  42. package/node_modules/@gdh/verify/package.json +7 -7
  43. 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 screenshotRequested = input.screenshotPolicy === "fallback";
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, input.screenshotPolicy ?? "never");
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
- input.recipe.launch.scenePath,
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, screenshotPolicy) {
931
- if (screenshotPolicy !== "fallback") {
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: BRIDGE_HANDSHAKE_TIMEOUT_MS,
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 startedAt = Date.now();
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() - startedAt <= timeoutMs) {
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
- property,
1916
- expected,
1917
- value: lastValue,
1918
- attempts,
1919
- elapsedMs: Date.now() - startedAt,
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
- error: "Timed out waiting for the requested node property value.",
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,