super-dsh 0.1.5-a → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/index.js CHANGED
@@ -268,7 +268,9 @@ const NATIVE_LINK = {
268
268
  key: "native",
269
269
  label: "DSH",
270
270
  path: "/",
271
- machine: "local"
271
+ machine: "local",
272
+ ready: true,
273
+ failed: false
272
274
  };
273
275
  /** Labels for the read-only face's bare keys (it reports keys, not labels). */
274
276
  const FALLBACK_LABELS = {
@@ -292,7 +294,9 @@ function coerceAgents(raw) {
292
294
  key,
293
295
  label: typeof label === "string" ? label : FALLBACK_LABELS[key] ?? key,
294
296
  path,
295
- machine: typeof machine === "string" ? machine : "local"
297
+ machine: typeof machine === "string" ? machine : "local",
298
+ ready: entry.ready === true,
299
+ failed: entry.failed === true
296
300
  });
297
301
  }
298
302
  return links.length > 0 ? links : void 0;
@@ -329,7 +333,8 @@ async function fetchRoster() {
329
333
  key,
330
334
  label: FALLBACK_LABELS[key] ?? key,
331
335
  path: `/${key}/`,
332
- machine: "local"
336
+ machine: "local",
337
+ ready: key === "native"
333
338
  });
334
339
  }
335
340
  if (links.length > 0) return links;
@@ -931,10 +936,10 @@ function machineGroups(rows) {
931
936
  function createSwitchController(host, options = {}) {
932
937
  const fallbackMs = options.fallbackMs ?? SWITCH_FALLBACK_MS;
933
938
  const quietWindowMs = options.quietWindowMs ?? LANDING_QUIET_MS;
934
- let current;
939
+ let current$1;
935
940
  let armedLanding;
936
941
  return { run(label) {
937
- const prior = current;
942
+ const prior = current$1;
938
943
  return new Promise((resolve) => {
939
944
  let timer;
940
945
  let watchdog;
@@ -961,7 +966,7 @@ function createSwitchController(host, options = {}) {
961
966
  attempt.finished = true;
962
967
  host.clearTimeout(timer);
963
968
  disposer();
964
- if (current === attempt) current = void 0;
969
+ if (current$1 === attempt) current$1 = void 0;
965
970
  if (landingArmed) watchdog = host.setTimeout(() => {
966
971
  attempt.cancelLanding();
967
972
  }, fallbackMs);
@@ -974,9 +979,9 @@ function createSwitchController(host, options = {}) {
974
979
  cancelPriorLanding();
975
980
  }
976
981
  if (prior !== void 0 && !prior.finished) prior.finish();
977
- current = attempt;
982
+ current$1 = attempt;
978
983
  const revert = (reason) => {
979
- if (attempt.finished || current !== attempt) return;
984
+ if (attempt.finished || current$1 !== attempt) return;
980
985
  attempt.cancelLanding();
981
986
  try {
982
987
  host.setAgent(NATIVE_AGENT);
@@ -999,7 +1004,7 @@ function createSwitchController(host, options = {}) {
999
1004
  revert("timeout");
1000
1005
  }, fallbackMs);
1001
1006
  disposer = host.onGenerationEstablished(() => {
1002
- if (attempt.finished || current !== attempt) return;
1007
+ if (attempt.finished || current$1 !== attempt) return;
1003
1008
  attempt.finish();
1004
1009
  });
1005
1010
  landingArmed = true;
@@ -1056,6 +1061,121 @@ function createSwitchController(host, options = {}) {
1056
1061
  });
1057
1062
  } };
1058
1063
  }
1064
+ /**
1065
+ * The pure pre-flight decision. `native` always runs; an absent row (roster
1066
+ * fetch failed, or the key is not listed) also runs — the machine's roster
1067
+ * probe is the authority on unknown targets, exactly as before C1.
1068
+ */
1069
+ function decidePreFlight(label, row) {
1070
+ if (label === NATIVE_AGENT) return "run";
1071
+ if (row === void 0) return "run";
1072
+ if (row.failed === true) return "failed";
1073
+ if (row.ready !== true) return "wait";
1074
+ return "run";
1075
+ }
1076
+ /** Default bound on one readiness wait (the page never locks behind a stuck world). */
1077
+ const WAIT_BOUND_MS = 12e4;
1078
+ /** Default poll interval for one readiness wait. */
1079
+ const WAIT_POLL_MS = 1e3;
1080
+ /**
1081
+ * Build a bounded readiness waiter over injected faces. The poll re-reads
1082
+ * the roster row every {@link ReadinessWaiterOptions.pollMs}; `failed`
1083
+ * short-circuits immediately; the bound always terminates the wait.
1084
+ */
1085
+ function createReadinessWaiter(faces, options = {}) {
1086
+ const boundMs = options.boundMs ?? WAIT_BOUND_MS;
1087
+ const pollMs = options.pollMs ?? WAIT_POLL_MS;
1088
+ let pending;
1089
+ let pollHandle;
1090
+ let boundHandle;
1091
+ let settled = false;
1092
+ const settle = (outcome) => {
1093
+ if (settled) return;
1094
+ settled = true;
1095
+ if (pollHandle !== void 0) faces.clearTimeout(pollHandle);
1096
+ if (boundHandle !== void 0) faces.clearTimeout(boundHandle);
1097
+ pending?.(outcome);
1098
+ pending = void 0;
1099
+ };
1100
+ return {
1101
+ wait(label) {
1102
+ return new Promise((resolve) => {
1103
+ pending = resolve;
1104
+ boundHandle = faces.setTimeout(() => {
1105
+ settle("bound");
1106
+ }, boundMs);
1107
+ const tick = () => {
1108
+ if (settled) return;
1109
+ faces.fetchRow().then((row) => {
1110
+ if (settled) return;
1111
+ if (row?.failed === true) {
1112
+ settle("failed");
1113
+ return;
1114
+ }
1115
+ if (row?.ready === true) {
1116
+ settle("ready");
1117
+ return;
1118
+ }
1119
+ pollHandle = faces.setTimeout(tick, pollMs);
1120
+ }, () => {
1121
+ if (!settled) pollHandle = faces.setTimeout(tick, pollMs);
1122
+ });
1123
+ };
1124
+ tick();
1125
+ });
1126
+ },
1127
+ cancel() {
1128
+ settle("cancelled");
1129
+ }
1130
+ };
1131
+ }
1132
+
1133
+ //#endregion
1134
+ //#region src/toast-core.ts
1135
+ /** Failures that name what broke need a longer read than a one-liner. */
1136
+ const TOAST_DEFAULT_HOLD_MS = 6e3;
1137
+ /** The exact props the official primitive receives (pure — the unit seam). */
1138
+ function toastProps(text, options = {}) {
1139
+ const props = {
1140
+ text,
1141
+ holdMs: options.holdMs ?? TOAST_DEFAULT_HOLD_MS
1142
+ };
1143
+ if (options.tone !== void 0) props.tone = options.tone;
1144
+ return props;
1145
+ }
1146
+ let seq = 0;
1147
+ let current;
1148
+ const listeners = /* @__PURE__ */ new Set();
1149
+ function emit() {
1150
+ for (const fn of [...listeners]) try {
1151
+ fn(current);
1152
+ } catch {}
1153
+ }
1154
+ /** Fire one toast (best-effort: without a mounted owner nothing renders). */
1155
+ function showSuperdToast(text, options = {}) {
1156
+ current = {
1157
+ seq: ++seq,
1158
+ text,
1159
+ options
1160
+ };
1161
+ emit();
1162
+ }
1163
+ /** Clear a shown toast (the fade's `onDone`); stale sequences are no-ops. */
1164
+ function clearSuperdToast(done) {
1165
+ if (current === void 0 || current.seq !== done) return;
1166
+ current = void 0;
1167
+ emit();
1168
+ }
1169
+ /** Subscribe to the store (the host's effect face); returns unsubscribe. */
1170
+ function onSuperdToast(fn) {
1171
+ listeners.add(fn);
1172
+ try {
1173
+ fn(current);
1174
+ } catch {}
1175
+ return () => {
1176
+ listeners.delete(fn);
1177
+ };
1178
+ }
1059
1179
 
1060
1180
  //#endregion
1061
1181
  //#region src/client/switch.ts
@@ -1071,39 +1191,14 @@ function uiState() {
1071
1191
  if (typeof state.subscribe !== "function" || typeof state.activeAgent !== "string") return void 0;
1072
1192
  return state;
1073
1193
  }
1074
- /** How long one revert toast stays on screen. */
1075
- const TOAST_HOLD_MS = 6e3;
1076
1194
  /**
1077
- * One transient message, fixed above the sidebar footer. Self-owned
1078
- * presentation only — no read from any foreign DOM, nothing is a state input.
1195
+ * One transient message through the official `Toast` primitive (C1 task 4:
1196
+ * the platform's top-center banner — dark surface in both themes, warn icon
1197
+ * seat — rendered by the host's React tree from the Hub sidebar row's
1198
+ * always-mounted owner; see client/toast.ts).
1079
1199
  */
1080
1200
  function showToast(message) {
1081
- try {
1082
- const doc = globalThis.document;
1083
- if (doc === void 0 || doc.body === null) return;
1084
- const el = doc.createElement("div");
1085
- el.textContent = message;
1086
- el.setAttribute("style", [
1087
- "position:fixed",
1088
- "left:50%",
1089
- "bottom:64px",
1090
- "transform:translateX(-50%)",
1091
- "z-index:9999",
1092
- "max-width:80vw",
1093
- "padding:10px 14px",
1094
- "border-radius:8px",
1095
- "background:rgba(24,24,28,.94)",
1096
- "color:#ececf0",
1097
- "font:13px/1.4 system-ui,sans-serif",
1098
- "box-shadow:0 8px 24px rgba(0,0,0,.35)"
1099
- ].join(";"));
1100
- doc.body.appendChild(el);
1101
- globalThis.setTimeout(() => {
1102
- try {
1103
- el.remove();
1104
- } catch {}
1105
- }, TOAST_HOLD_MS);
1106
- } catch {}
1201
+ showSuperdToast(message);
1107
1202
  }
1108
1203
  /** The bound `switchAgent`, or `undefined` until the entry binds it. */
1109
1204
  let bound;
@@ -1211,10 +1306,63 @@ function bindSwitchAgent(deps) {
1211
1306
  * `false` means the click is swallowed, never that the anchor navigates
1212
1307
  * the fused page away (review round 1).
1213
1308
  */
1309
+ let pendingWait;
1310
+ /**
1311
+ * The Hub switch surfaces' entry point. Attempts the in-page switch for
1312
+ * `label`, gated on the target world's readiness (C1): a not-ready world
1313
+ * holds the UI on native with starting feedback and a bounded poll; a
1314
+ * failed world reports and never switches; a ready/native/unknown target
1315
+ * runs the machine (whose roster probe stays the fallback).
1316
+ * @returns `true` when the fusion switch took the click over; `false` when
1317
+ * this page is not a fusion page or the orchestration is unbound.
1318
+ * Informational only — the interception contract (`seatClickDecision`)
1319
+ * prevents navigation on every fusion-page left click regardless: a
1320
+ * `false` means the click is swallowed, never that the anchor navigates
1321
+ * the fused page away (review round 1).
1322
+ */
1214
1323
  function attemptSwitch(label) {
1215
1324
  const run = bound;
1216
1325
  if (run === void 0 || !fusionPage()) return false;
1217
- run(label).catch(() => {});
1326
+ pendingWait?.cancel();
1327
+ pendingWait = void 0;
1328
+ (async () => {
1329
+ let row;
1330
+ try {
1331
+ row = (await fetchRoster()).find((agent) => agent.key === label);
1332
+ } catch {
1333
+ row = void 0;
1334
+ }
1335
+ const decision = decidePreFlight(label, row);
1336
+ if (decision === "failed") {
1337
+ showToast(`Super D: ${label} failed to start — see the service log.`);
1338
+ return;
1339
+ }
1340
+ if (decision === "wait") {
1341
+ showToast(`Super D: ${label} is starting…`);
1342
+ const waiter = createReadinessWaiter({
1343
+ fetchRow: async () => {
1344
+ try {
1345
+ return (await fetchRoster()).find((agent) => agent.key === label);
1346
+ } catch {
1347
+ return;
1348
+ }
1349
+ },
1350
+ setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
1351
+ clearTimeout: (handle) => globalThis.clearTimeout(handle)
1352
+ });
1353
+ pendingWait = waiter;
1354
+ const outcome = await waiter.wait(label);
1355
+ if (pendingWait === waiter) pendingWait = void 0;
1356
+ if (outcome === "ready") {
1357
+ run(label).catch(() => {});
1358
+ return;
1359
+ }
1360
+ if (outcome === "failed") showToast(`Super D: ${label} failed to start — see the service log.`);
1361
+ else if (outcome === "bound") showToast(`Super D: ${label} is still starting — try again in a moment.`);
1362
+ return;
1363
+ }
1364
+ run(label).catch(() => {});
1365
+ })();
1218
1366
  return true;
1219
1367
  }
1220
1368
 
@@ -1847,6 +1995,34 @@ function HubPage() {
1847
1995
  });
1848
1996
  }
1849
1997
 
1998
+ //#endregion
1999
+ //#region src/client/toast.ts
2000
+ /**
2001
+ * The element for one pending toast: keyed by sequence so a newer toast
2002
+ * remounts the primitive and restarts its slide-in cycle (the primitive's
2003
+ * own re-show contract).
2004
+ */
2005
+ function superdToastElement(toast) {
2006
+ return react.createElement(__deepseek_ai_dsh_client_ui_primitives.Toast, {
2007
+ key: toast.seq,
2008
+ ...toastProps(toast.text, toast.options),
2009
+ onDone: () => {
2010
+ clearSuperdToast(toast.seq);
2011
+ }
2012
+ });
2013
+ }
2014
+ /**
2015
+ * The always-mounted owner: subscribes to the store and renders the current
2016
+ * toast (nothing while the store is empty). Mounted inside the Hub sidebar
2017
+ * row — the primitive's body portal places the banner top-center regardless
2018
+ * of where the owner sits.
2019
+ */
2020
+ function SuperdToastHost() {
2021
+ const [toast, setToast] = react.useState(void 0);
2022
+ react.useEffect(() => onSuperdToast(setToast), []);
2023
+ return toast === void 0 ? null : superdToastElement(toast);
2024
+ }
2025
+
1850
2026
  //#endregion
1851
2027
  //#region src/client/HubPanelIcon.tsx
1852
2028
  /** Runtime-mark edge inside the selector, and the chevron's own default edge. */
@@ -1986,61 +2162,65 @@ function HubPanelIcon({ size }) {
1986
2162
  alignItems: "center",
1987
2163
  justifyContent: "center"
1988
2164
  },
1989
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(HubGlyph, { size }), wide && box !== null && switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Menu, {
1990
- open,
1991
- onClose: () => {
1992
- setOpen(false);
1993
- },
1994
- items,
1995
- selectedIds: activeKey === void 0 ? [] : [activeKey],
1996
- onSelect: select,
1997
- dense: true,
1998
- portal: true,
1999
- align: "end",
2000
- getAnchorRect: anchorRect,
2001
- anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2002
- ref: selectorRef,
2003
- role: "button",
2004
- tabIndex: 0,
2005
- "aria-label": "Agent selector",
2006
- "aria-haspopup": "menu",
2007
- "aria-expanded": open,
2008
- title: activeLabel === void 0 ? "Agent selector" : `Agent selector: ${activeLabel}`,
2009
- style: {
2010
- position: "fixed",
2011
- left: box.right - ROW_INSET - selectorWidth,
2012
- top: box.top + (box.bottom - box.top) / 2,
2013
- transform: "translateY(-50%)",
2014
- display: "inline-flex",
2015
- alignItems: "center",
2016
- justifyContent: "center",
2017
- gap: SELECTOR_GAP,
2018
- boxSizing: "border-box",
2019
- width: selectorWidth,
2020
- height: SELECTOR_HEIGHT,
2021
- padding: `0 ${SELECTOR_PAD}px`,
2022
- borderRadius: 16,
2023
- background: hover || open ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
2024
- color: "var(--dsw-alias-label-caption)",
2025
- cursor: "pointer",
2026
- zIndex: 50
2027
- },
2028
- onMouseEnter: () => {
2029
- setHover(true);
2030
- },
2031
- onMouseLeave: () => {
2032
- setHover(false);
2033
- },
2034
- onClick: toggle,
2035
- onKeyDown: (event) => {
2036
- if (event.key === "Enter" || event.key === " ") toggle(event);
2165
+ children: [
2166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(HubGlyph, { size }),
2167
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SuperdToastHost, {}),
2168
+ wide && box !== null && switchUi.available && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.Menu, {
2169
+ open,
2170
+ onClose: () => {
2171
+ setOpen(false);
2037
2172
  },
2038
- children: [activeKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
2039
- agentKey: activeKey,
2040
- size: MARK_SIZE
2041
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: CHEVRON_SIZE })]
2173
+ items,
2174
+ selectedIds: activeKey === void 0 ? [] : [activeKey],
2175
+ onSelect: select,
2176
+ dense: true,
2177
+ portal: true,
2178
+ align: "end",
2179
+ getAnchorRect: anchorRect,
2180
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2181
+ ref: selectorRef,
2182
+ role: "button",
2183
+ tabIndex: 0,
2184
+ "aria-label": "Agent selector",
2185
+ "aria-haspopup": "menu",
2186
+ "aria-expanded": open,
2187
+ title: activeLabel === void 0 ? "Agent selector" : `Agent selector: ${activeLabel}`,
2188
+ style: {
2189
+ position: "fixed",
2190
+ left: box.right - ROW_INSET - selectorWidth,
2191
+ top: box.top + (box.bottom - box.top) / 2,
2192
+ transform: "translateY(-50%)",
2193
+ display: "inline-flex",
2194
+ alignItems: "center",
2195
+ justifyContent: "center",
2196
+ gap: SELECTOR_GAP,
2197
+ boxSizing: "border-box",
2198
+ width: selectorWidth,
2199
+ height: SELECTOR_HEIGHT,
2200
+ padding: `0 ${SELECTOR_PAD}px`,
2201
+ borderRadius: 16,
2202
+ background: hover || open ? "var(--dsw-alias-interactive-bg-hover)" : "transparent",
2203
+ color: "var(--dsw-alias-label-caption)",
2204
+ cursor: "pointer",
2205
+ zIndex: 50
2206
+ },
2207
+ onMouseEnter: () => {
2208
+ setHover(true);
2209
+ },
2210
+ onMouseLeave: () => {
2211
+ setHover(false);
2212
+ },
2213
+ onClick: toggle,
2214
+ onKeyDown: (event) => {
2215
+ if (event.key === "Enter" || event.key === " ") toggle(event);
2216
+ },
2217
+ children: [activeKey !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RuntimeMark, {
2218
+ agentKey: activeKey,
2219
+ size: MARK_SIZE
2220
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: CHEVRON_SIZE })]
2221
+ })
2042
2222
  })
2043
- })]
2223
+ ]
2044
2224
  });
2045
2225
  }
2046
2226
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "super-dsh",
3
- "version": "0.1.5-a",
3
+ "version": "0.1.6",
4
4
  "description": "Super D for the DeepSeek Harness (DSH) — one dsh plugin that mounts the superd hub (runtime selector, roster, per-runtime agents) and joins local foreign-agent runtimes (OMP, Codex, Claude, Pi, Hermes, Antigravity) into the DSH Web UI. Each runtime keeps its native home and session store; the hub only bridges.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/pgmi-builds/superd.git"
9
+ "url": "git+https://github.com/pgmi-builds/super-dsh.git"
10
10
  },
11
11
  "exports": {
12
12
  ".": "./superd-hub/dist/index.js",
@@ -22,6 +22,10 @@ export interface AgentLink {
22
22
  path: string;
23
23
  /** Machine the runtime runs on — the Hub UI's grouping key. */
24
24
  machine: string;
25
+ /** Whether the world's data plane is up (spawn resolved). Native is always ready. */
26
+ ready: boolean;
27
+ /** Whether the world's spawn REJECTED (mount stays for diagnosis). */
28
+ failed: boolean;
25
29
  }
26
30
  /** The global this module publishes the roster under. */
27
31
  export declare const AGENT_ROSTER_GLOBAL = "__DSH_AGENT_ROSTER__";
@@ -3,7 +3,7 @@ import { takeHostMount } from './world-host.js';
3
3
  /** Machine of every roster row today — the local constant; the extension point a multi-machine registry replaces. */
4
4
  const LOCAL_MACHINE = 'local';
5
5
  /** The native runtime's synthetic entry (no adapter plugin backs it). */
6
- const NATIVE = { key: 'native', label: 'DSH', path: '/', machine: LOCAL_MACHINE };
6
+ const NATIVE = { key: 'native', label: 'DSH', path: '/', machine: LOCAL_MACHINE, ready: true, failed: false };
7
7
  /** The global this module publishes the roster under. */
8
8
  export const AGENT_ROSTER_GLOBAL = '__DSH_AGENT_ROSTER__';
9
9
  /**
@@ -19,7 +19,7 @@ export function agentLinks() {
19
19
  if (mount === undefined)
20
20
  continue;
21
21
  const path = mount.labelPath.endsWith('/') ? mount.labelPath : `${mount.labelPath}/`;
22
- links.push({ key: agent.key, label: agent.label, path, machine: LOCAL_MACHINE });
22
+ links.push({ key: agent.key, label: agent.label, path, machine: LOCAL_MACHINE, ready: agent.ready === true, failed: agent.failed === true });
23
23
  }
24
24
  return links;
25
25
  }
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export { handleAgentRuntime, registerSelectorRpc, AGENT_RUNTIME_RPC_PATH, registryTargets, type RuntimeTargetSource } from './rpc.js';
18
18
  export { registerForeignTarget, unregisterForeignTarget, getTarget, listRuntimeKeys, type ForeignTarget, type GatewayFace } from './targets.js';
19
- export { registerAgent, unregisterAgent, setReady, listAgents, onRosterChanged, type RosterEntry } from './roster.js';
19
+ export { registerAgent, unregisterAgent, setReady, setFailed, listAgents, onRosterChanged, type RosterEntry } from './roster.js';
20
20
  export { spawnWorld, type SpawnWorldOptions } from './spawn-world.js';
21
21
  export { provisionWorldProfile, resolveInstallAnchor, type ProvisionWorldOptions } from './world-provision.js';
22
22
  export { WorldWebServer, stripLabelFromUrl, type WorldWebRoute, type WorldWebUpgradeRoute, type RealWebServerFace } from './world-web-server.js';
@@ -29,7 +29,7 @@ export { createSwitchController, parseLandingTarget, namespacedKey, storageNames
29
29
  export { agentLinks, agentRosterRow, AGENT_ROSTER_GLOBAL, type AgentLink } from './agent-roster.js';
30
30
  export { createOwnershipIndex, type OwnershipIndex, type OwnershipRow } from './ownership.js';
31
31
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf, type HostMount } from './world-host.js';
32
- export { worldMountPatches, superdTrustedHosts, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, type WorldPatchLayer, } from './world-mount.js';
32
+ export { worldMountPatches, superdTrustedHosts, WORLD_WAVE1_DISABLED_ROWS, WORLD_WAVE2_DISABLED_ROWS, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, type WorldPatchLayer, } from './world-mount.js';
33
33
  export { buildTrustScript, deriveDefaultPageAuthorities, type BootScriptRow, type TrustConfig, } from './web-trust.js';
34
34
  export { withSystemHead, type HeadAwareSessionFace, type HeadPlaceholder, type WithSystemHead } from './system-head.js';
35
35
  export { default as worldApply, name as worldPluginName, type Config as WorldEntryConfig } from './world-entry.js';
@@ -16,7 +16,7 @@
16
16
  */
17
17
  export { handleAgentRuntime, registerSelectorRpc, AGENT_RUNTIME_RPC_PATH, registryTargets } from './rpc.js';
18
18
  export { registerForeignTarget, unregisterForeignTarget, getTarget, listRuntimeKeys } from './targets.js';
19
- export { registerAgent, unregisterAgent, setReady, listAgents, onRosterChanged } from './roster.js';
19
+ export { registerAgent, unregisterAgent, setReady, setFailed, listAgents, onRosterChanged } from './roster.js';
20
20
  export { spawnWorld } from './spawn-world.js';
21
21
  export { provisionWorldProfile, resolveInstallAnchor } from './world-provision.js';
22
22
  export { WorldWebServer, stripLabelFromUrl } from './world-web-server.js';
@@ -29,7 +29,7 @@ export { createSwitchController, parseLandingTarget, namespacedKey, storageNames
29
29
  export { agentLinks, agentRosterRow, AGENT_ROSTER_GLOBAL } from './agent-roster.js';
30
30
  export { createOwnershipIndex } from './ownership.js';
31
31
  export { registerHostMount, takeHostMount, listHostMounts, noteWorldServer, worldServerOf } from './world-host.js';
32
- export { worldMountPatches, superdTrustedHosts, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, } from './world-mount.js';
32
+ export { worldMountPatches, superdTrustedHosts, WORLD_WAVE1_DISABLED_ROWS, WORLD_WAVE2_DISABLED_ROWS, WORLD_MOUNT_PLUGIN, WORLD_MOUNT_ROW_ID, WORLD_HUB_PLUGIN, WORLD_HUB_ROW_ID, } from './world-mount.js';
33
33
  export { buildTrustScript, deriveDefaultPageAuthorities, } from './web-trust.js';
34
34
  export { withSystemHead } from './system-head.js';
35
35
  export { default as worldApply, name as worldPluginName } from './world-entry.js';
@@ -15,6 +15,8 @@ export interface RosterEntry {
15
15
  label: string;
16
16
  /** True once the adapter's world context is spawned and its target registered. */
17
17
  ready: boolean;
18
+ /** True once the world's spawn REJECTED (world-join catch) — stays mounted for diagnosis. */
19
+ failed?: boolean;
18
20
  }
19
21
  /** Register an agent. Duplicate keys are a plugin bug — throw loud. */
20
22
  export declare function registerAgent(entry: RosterEntry): void;
@@ -22,6 +24,8 @@ export declare function registerAgent(entry: RosterEntry): void;
22
24
  export declare function unregisterAgent(key: string): void;
23
25
  /** Flip readiness (world spawned / disposed). Unknown keys are a plugin bug. */
24
26
  export declare function setReady(key: string, ready: boolean): void;
27
+ /** Flip failure (world spawn rejected). Unknown keys are a plugin bug. */
28
+ export declare function setFailed(key: string, failed: boolean): void;
25
29
  /** Snapshot copy — callers can hold it without aliasing the roster. */
26
30
  export declare function listAgents(): RosterEntry[];
27
31
  /** Subscribe to roster changes; returns an unsubscribe function. */
@@ -9,7 +9,7 @@ function emit() {
9
9
  export function registerAgent(entry) {
10
10
  if (entries.has(entry.key))
11
11
  throw new Error(`roster: duplicate agent key ${entry.key}`);
12
- entries.set(entry.key, { ...entry });
12
+ entries.set(entry.key, { ...entry, failed: entry.failed ?? false });
13
13
  emit();
14
14
  }
15
15
  /** Drop an agent (adapter plugin disposed). */
@@ -25,6 +25,14 @@ export function setReady(key, ready) {
25
25
  e.ready = ready;
26
26
  emit();
27
27
  }
28
+ /** Flip failure (world spawn rejected). Unknown keys are a plugin bug. */
29
+ export function setFailed(key, failed) {
30
+ const e = entries.get(key);
31
+ if (!e)
32
+ throw new Error(`roster: unknown agent ${key}`);
33
+ e.failed = failed;
34
+ emit();
35
+ }
28
36
  /** Snapshot copy — callers can hold it without aliasing the roster. */
29
37
  export function listAgents() {
30
38
  return [...entries.values()].map((e) => ({ ...e }));
@@ -628,3 +628,43 @@ export interface SwitchController {
628
628
  * switch's pointer-restored view).
629
629
  */
630
630
  export declare function createSwitchController(host: SwitchHost, options?: SwitchOptions): SwitchController;
631
+ /** What attemptSwitch does with a roster row: run the machine / report failure / wait for readiness. */
632
+ export type PreFlightDecision = 'run' | 'failed' | 'wait';
633
+ /** The roster fields the pre-flight reads (the projection carries both since C1 task 3). */
634
+ export interface PreFlightRow {
635
+ ready?: boolean;
636
+ failed?: boolean;
637
+ }
638
+ /**
639
+ * The pure pre-flight decision. `native` always runs; an absent row (roster
640
+ * fetch failed, or the key is not listed) also runs — the machine's roster
641
+ * probe is the authority on unknown targets, exactly as before C1.
642
+ */
643
+ export declare function decidePreFlight(label: string, row: PreFlightRow | undefined): PreFlightDecision;
644
+ /** Default bound on one readiness wait (the page never locks behind a stuck world). */
645
+ export declare const WAIT_BOUND_MS = 120000;
646
+ /** Default poll interval for one readiness wait. */
647
+ export declare const WAIT_POLL_MS = 1000;
648
+ /** The waiter's injected faces (tests drive time and the roster by hand). */
649
+ export interface ReadinessWaiterFaces {
650
+ /** Read the target's roster row; undefined on any failure. */
651
+ fetchRow(): Promise<PreFlightRow | undefined>;
652
+ setTimeout(fn: () => void, ms: number): unknown;
653
+ clearTimeout(handle: unknown): void;
654
+ }
655
+ /** One bounded readiness wait. */
656
+ export interface ReadinessWaiter {
657
+ /** Resolves when the row flips ready ('ready'), reports failure ('failed'), the bound expires ('bound'), or the waiter is cancelled ('cancelled'). */
658
+ wait(label: string): Promise<'ready' | 'failed' | 'bound' | 'cancelled'>;
659
+ /** A newer switch attempt cancels a pending wait (it resolves 'cancelled'). */
660
+ cancel(): void;
661
+ }
662
+ /**
663
+ * Build a bounded readiness waiter over injected faces. The poll re-reads
664
+ * the roster row every {@link ReadinessWaiterOptions.pollMs}; `failed`
665
+ * short-circuits immediately; the bound always terminates the wait.
666
+ */
667
+ export declare function createReadinessWaiter(faces: ReadinessWaiterFaces, options?: {
668
+ boundMs?: number;
669
+ pollMs?: number;
670
+ }): ReadinessWaiter;