@maintainer-pro/ai-bridge 0.1.3 → 0.1.5

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/daemon.mjs +215 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainer-pro/ai-bridge",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Local bridge daemon that pairs a machine to Maintainer Pro and configures multiple client sandboxes.",
5
5
  "keywords": [
6
6
  "maintainer-pro",
package/src/daemon.mjs CHANGED
@@ -850,7 +850,65 @@ const launchedAt = new Map();
850
850
  /** Last process problems to send on heartbeat. Key: sandboxId::code::role */
851
851
  const processProblems = new Map();
852
852
 
853
- function forgetLaunch(sandboxId) {
853
+ /** @type {Map<string, Set<string>>} sandboxId -> CMD/terminal titles we opened */
854
+ const openedTerminalTitles = new Map();
855
+
856
+ function rememberTerminalTitle(sandboxId, title) {
857
+ const id = String(sandboxId || "").trim();
858
+ const name = String(title || "").trim();
859
+ if (!id || !name) return;
860
+ let titles = openedTerminalTitles.get(id);
861
+ if (!titles) {
862
+ titles = new Set();
863
+ openedTerminalTitles.set(id, titles);
864
+ }
865
+ titles.add(name);
866
+ }
867
+
868
+ function closeWindowsByTitle(title) {
869
+ const name = String(title || "").trim();
870
+ if (!name) return Promise.resolve();
871
+ return new Promise((resolve) => {
872
+ const done = () => resolve();
873
+ if (process.platform === "win32") {
874
+ const child = spawn(
875
+ "taskkill",
876
+ ["/F", "/T", "/FI", `WINDOWTITLE eq ${name}*`],
877
+ { windowsHide: true, stdio: "ignore" }
878
+ );
879
+ child.on("exit", done);
880
+ child.on("error", done);
881
+ setTimeout(done, 4000);
882
+ return;
883
+ }
884
+ if (process.platform === "darwin") {
885
+ const child = spawn(
886
+ "osascript",
887
+ [
888
+ "-e",
889
+ `tell application "Terminal" to close (every window whose name contains ${JSON.stringify(name)})`,
890
+ ],
891
+ { stdio: "ignore" }
892
+ );
893
+ child.on("exit", done);
894
+ child.on("error", done);
895
+ setTimeout(done, 4000);
896
+ return;
897
+ }
898
+ done();
899
+ });
900
+ }
901
+
902
+ async function closeRememberedTerminals(sandboxId) {
903
+ const titles = openedTerminalTitles.get(sandboxId);
904
+ if (!titles) return;
905
+ for (const title of titles) {
906
+ await closeWindowsByTitle(title);
907
+ }
908
+ openedTerminalTitles.delete(sandboxId);
909
+ }
910
+
911
+ async function forgetLaunch(sandboxId) {
854
912
  for (const key of [...launchedAt.keys()]) {
855
913
  if (key === sandboxId || key.startsWith(`${sandboxId}:`)) {
856
914
  launchedAt.delete(key);
@@ -859,7 +917,8 @@ function forgetLaunch(sandboxId) {
859
917
  for (const key of [...processProblems.keys()]) {
860
918
  if (key.startsWith(`${sandboxId}::`)) processProblems.delete(key);
861
919
  }
862
- stopCloudflare(sandboxId);
920
+ await closeRememberedTerminals(sandboxId);
921
+ await stopCloudflare(sandboxId);
863
922
  }
864
923
 
865
924
  function problemKey(sandboxId, code, role = "") {
@@ -1041,6 +1100,7 @@ function runLauncher(command, args, extra = {}) {
1041
1100
  child = spawn(command, args, {
1042
1101
  stdio: ["ignore", "pipe", "pipe"],
1043
1102
  windowsHide: extra.windowsHide,
1103
+ windowsVerbatimArguments: Boolean(extra.windowsVerbatimArguments),
1044
1104
  });
1045
1105
  } catch (err) {
1046
1106
  done({
@@ -1073,7 +1133,7 @@ function runLauncher(command, args, extra = {}) {
1073
1133
  }
1074
1134
 
1075
1135
  async function openInNewTerminal(opts) {
1076
- const { title, folder, command, env = {}, launchKey } = opts;
1136
+ const { title, folder, command, env = {}, launchKey, sandboxId } = opts;
1077
1137
  if (launchKey) {
1078
1138
  if (!opts.force && recentlyLaunched(launchKey)) {
1079
1139
  return { ok: true, skipped: true };
@@ -1086,6 +1146,13 @@ async function openInNewTerminal(opts) {
1086
1146
  return { ok: false, error };
1087
1147
  }
1088
1148
 
1149
+ await closeWindowsByTitle(title);
1150
+ await sleep(250);
1151
+ rememberTerminalTitle(
1152
+ sandboxId || String(launchKey || "").split(":")[0],
1153
+ title
1154
+ );
1155
+
1089
1156
  const envWin = Object.entries(env)
1090
1157
  .map(([key, value]) => `set ${key}=${value}`)
1091
1158
  .join("&& ");
@@ -1097,18 +1164,16 @@ async function openInNewTerminal(opts) {
1097
1164
 
1098
1165
  try {
1099
1166
  if (process.platform === "win32") {
1100
- const inner = `cd /d "${folder}" && ${envWin ? `${envWin}&& ` : ""}title ${title}&& ${command}`;
1101
- const escaped = inner.replace(/'/g, "''");
1167
+ const safeTitle =
1168
+ String(title || "Maintainer Pro").replace(/["&<>|^]/g, " ").trim() ||
1169
+ "Maintainer Pro";
1170
+ const body = [envWin, `title ${safeTitle}`, command]
1171
+ .filter(Boolean)
1172
+ .join("&& ");
1102
1173
  const opened = await runLauncher(
1103
- "powershell.exe",
1104
- [
1105
- "-NoProfile",
1106
- "-WindowStyle",
1107
- "Hidden",
1108
- "-Command",
1109
- `Start-Process -FilePath $env:ComSpec -WorkingDirectory ${JSON.stringify(folder)} -ArgumentList @('/k', '${escaped}')`,
1110
- ],
1111
- { windowsHide: true }
1174
+ process.env.ComSpec || "cmd.exe",
1175
+ ["/d", "/s", "/c", `start "${safeTitle}" /D "${folder}" cmd.exe /k ${body}`],
1176
+ { windowsVerbatimArguments: true }
1112
1177
  );
1113
1178
  if (!opened.ok) {
1114
1179
  return { ok: false, error: friendlyLaunchError(opened.error, title) };
@@ -1255,6 +1320,7 @@ async function startAiServerForWorkspace(ws, opts = {}) {
1255
1320
  NEXT_PUBLIC_AI_SERVER_URL: `http://localhost:${port}`,
1256
1321
  },
1257
1322
  launchKey,
1323
+ sandboxId: ws.sandboxId,
1258
1324
  });
1259
1325
  if (opened.skipped) {
1260
1326
  return { port, up: false, launched: false, starting: true };
@@ -1382,6 +1448,7 @@ async function stopWorkspaceApps(ws) {
1382
1448
  }
1383
1449
  }
1384
1450
  log(`stopping local apps on ports ${[...ports].join(", ") || "(none)"}`);
1451
+ await closeRememberedTerminals(ws.sandboxId);
1385
1452
  for (const port of ports) {
1386
1453
  await killPort(port);
1387
1454
  }
@@ -1413,12 +1480,10 @@ async function waitForUrlInFile(file, timeoutMs = 90_000) {
1413
1480
  }
1414
1481
 
1415
1482
  function cloudflaredCommand(localUrl, logFile) {
1416
- const run = `npx --yes cloudflared tunnel --url ${localUrl} --no-autoupdate`;
1417
- if (process.platform === "win32") {
1418
- const dest = String(logFile).replace(/'/g, "''");
1419
- return `powershell -NoProfile -Command "${run} 2>&1 | Tee-Object -FilePath '${dest}'"`;
1420
- }
1421
- return `${run} 2>&1 | tee ${JSON.stringify(logFile)}`;
1483
+ const logArg = JSON.stringify(logFile);
1484
+ const run = `npx --yes cloudflared tunnel --url ${localUrl} --no-autoupdate --logfile ${logArg}`;
1485
+ if (process.platform === "win32") return run;
1486
+ return `${run} 2>&1 | tee ${logArg}`;
1422
1487
  }
1423
1488
 
1424
1489
  async function startCloudflareTerminal(ws, role, localUrl) {
@@ -1439,6 +1504,7 @@ async function startCloudflareTerminal(ws, role, localUrl) {
1439
1504
  folder,
1440
1505
  command: cloudflaredCommand(localUrl, logFile),
1441
1506
  launchKey: `${ws.sandboxId}:cf:${role}`,
1507
+ sandboxId: ws.sandboxId,
1442
1508
  force: true,
1443
1509
  });
1444
1510
  if (!opened.ok) {
@@ -1501,22 +1567,52 @@ function writeTunnelEnv(ws, tunnels) {
1501
1567
  }
1502
1568
  }
1503
1569
 
1504
- async function configureCloudflareForWorkspace(ws, cfg) {
1505
- const sandboxId = ws.sandboxId;
1506
- const label = ws.sandboxName || "this sandbox";
1507
- const folder = path.resolve(ws.folderPath || "");
1570
+ function reservedPortsFor(cfg, sandboxId) {
1508
1571
  const reserved = new Set();
1509
1572
  for (const other of cfg.workspaces || []) {
1510
1573
  if (other.sandboxId !== sandboxId && other.port) {
1511
1574
  reserved.add(Number(other.port));
1512
1575
  }
1513
1576
  }
1577
+ return reserved;
1578
+ }
1514
1579
 
1515
- log(`Cloudflare setup for ${label}: stop apps, then tunnel ai-server/backend before UI`);
1516
- try {
1580
+ function appsWanted(ws) {
1581
+ return Boolean(ws?.appsRequested);
1582
+ }
1583
+
1584
+ async function configureCloudflareForWorkspace(ws, cfg) {
1585
+ const sandboxId = ws.sandboxId;
1586
+ const label = ws.sandboxName || "this sandbox";
1587
+
1588
+ log(`Cloudflare queued for ${label}: stop apps and wait for Start`);
1517
1589
  await stopCloudflare(sandboxId);
1518
1590
  await stopWorkspaceApps(ws);
1591
+ await forgetLaunch(sandboxId);
1592
+ ws.cloudflarePending = true;
1593
+ ws.appsRequested = false;
1594
+ persistWorkspaceEntry(cfg, ws);
1519
1595
 
1596
+ return {
1597
+ sandboxId,
1598
+ folderPath: ws.folderPath,
1599
+ port: ws.port,
1600
+ pending: true,
1601
+ cloudflarePending: true,
1602
+ waitingForStart: true,
1603
+ warning:
1604
+ "Cloudflare is ready in Maintainer Pro. Use Start chat server when you want to launch the apps and create the public URLs.",
1605
+ };
1606
+ }
1607
+
1608
+ async function launchCloudflareTunnels(ws, cfg) {
1609
+ const sandboxId = ws.sandboxId;
1610
+ const label = ws.sandboxName || "this sandbox";
1611
+ const folder = path.resolve(ws.folderPath || "");
1612
+ const reserved = reservedPortsFor(cfg, sandboxId);
1613
+
1614
+ log(`Cloudflare start for ${label}: tunnel chat script/backend before UI`);
1615
+ try {
1520
1616
  if (!cfg.noAiServer) {
1521
1617
  await startAiServerForWorkspace(ws, { reserved, cfg });
1522
1618
  await waitUntilReachable(
@@ -1596,6 +1692,8 @@ async function configureCloudflareForWorkspace(ws, cfg) {
1596
1692
  ws.cloudflareUrl = appUrl;
1597
1693
  ws.cloudflare = tunnels;
1598
1694
  ws.appUrl = appUrl;
1695
+ ws.cloudflarePending = false;
1696
+ ws.appsRequested = true;
1599
1697
  persistWorkspaceEntry(cfg, ws);
1600
1698
  cloudflareTunnels.set(sandboxId, { tunnels: started });
1601
1699
  clearProcessProblem(sandboxId, "cloudflare_launch", "tunnel");
@@ -1603,6 +1701,7 @@ async function configureCloudflareForWorkspace(ws, cfg) {
1603
1701
  return {
1604
1702
  sandboxId,
1605
1703
  folderPath: ws.folderPath,
1704
+ port: ws.port,
1606
1705
  appUrl,
1607
1706
  origins: Object.values(tunnels).filter(Boolean),
1608
1707
  tunnels,
@@ -1618,13 +1717,52 @@ async function configureCloudflareForWorkspace(ws, cfg) {
1618
1717
  title: `Could not start Cloudflare (${label})`,
1619
1718
  message,
1620
1719
  resolution:
1621
- "Install cloudflared or allow npx to download it, then try Share with Cloudflare again.",
1622
- actionCode: "configure_cloudflare",
1720
+ "Install cloudflared or allow npx to download it, then use Start chat server again.",
1721
+ actionCode: "start_ai_server",
1623
1722
  });
1624
1723
  throw err;
1625
1724
  }
1626
1725
  }
1627
1726
 
1727
+ async function startAppsForWorkspace(ws, cfg) {
1728
+ if (ws.cloudflarePending) {
1729
+ return launchCloudflareTunnels(ws, cfg);
1730
+ }
1731
+ ws.appsRequested = true;
1732
+ persistWorkspaceEntry(cfg, ws);
1733
+ const reserved = reservedPortsFor(cfg, ws.sandboxId);
1734
+ if (!cfg.noAiServer) {
1735
+ await startAiServerForWorkspace(ws, { reserved, cfg });
1736
+ await sleep(1500);
1737
+ }
1738
+ const startedHosts = await ensureHostProcesses(ws, {
1739
+ reserved,
1740
+ cfg,
1741
+ force: true,
1742
+ });
1743
+ await sleep(800);
1744
+ await inspectHostJobs(ws);
1745
+ const up = await probeUrl(`http://127.0.0.1:${ws.port}/embed-config.js`);
1746
+ if (up) {
1747
+ clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
1748
+ clearProcessProblem(ws.sandboxId, "apps_not_started");
1749
+ }
1750
+ const processIssues = issuesForSandbox(ws.sandboxId).map(
1751
+ ({ role: _role, ...issue }) => issue
1752
+ );
1753
+ const warning = processIssues[0]?.message || null;
1754
+ return {
1755
+ up,
1756
+ startedHosts,
1757
+ sandboxId: ws.sandboxId,
1758
+ folderPath: ws.folderPath,
1759
+ port: ws.port,
1760
+ appUrl: ws.appUrl,
1761
+ processIssues,
1762
+ warning,
1763
+ };
1764
+ }
1765
+
1628
1766
  async function ensureHostProcesses(ws, opts = {}) {
1629
1767
  const reserved = opts.reserved instanceof Set ? opts.reserved : new Set();
1630
1768
  const cfg = opts.cfg || null;
@@ -1704,6 +1842,7 @@ async function ensureHostProcesses(ws, opts = {}) {
1704
1842
  command,
1705
1843
  env: { PORT: String(port), ...extraEnv },
1706
1844
  launchKey,
1845
+ sandboxId: ws.sandboxId,
1707
1846
  force: Boolean(opts.force),
1708
1847
  });
1709
1848
  if (opened.skipped) continue;
@@ -1763,6 +1902,11 @@ async function inspectHostJobs(ws) {
1763
1902
  clearProcessProblem(ws.sandboxId, "host_process_down", job.role);
1764
1903
  continue;
1765
1904
  }
1905
+ if (!appsWanted(ws)) {
1906
+ clearProcessProblem(ws.sandboxId, "host_process_launch", job.role);
1907
+ clearProcessProblem(ws.sandboxId, "host_process_down", job.role);
1908
+ continue;
1909
+ }
1766
1910
  if (starting) continue;
1767
1911
  const tried = launchedAt.has(launchKey);
1768
1912
  recordProcessProblem({
@@ -1863,6 +2007,8 @@ async function setupWorkspace(cfg, action) {
1863
2007
  clientKind: client.kind,
1864
2008
  appUrl,
1865
2009
  sameOrigin: Boolean(client.sameOrigin),
2010
+ appsRequested: false,
2011
+ cloudflarePending: false,
1866
2012
  };
1867
2013
  if (existing >= 0) cfg.workspaces[existing] = entry;
1868
2014
  else cfg.workspaces.push(entry);
@@ -1881,12 +2027,6 @@ async function setupWorkspace(cfg, action) {
1881
2027
  .join("\n"),
1882
2028
  });
1883
2029
 
1884
- if (!cfg.noAiServer) {
1885
- await startAiServerForWorkspace(entry, { reserved, cfg });
1886
- await sleep(1500);
1887
- }
1888
- const startedHosts = await ensureHostProcesses(entry, { reserved, cfg });
1889
- await sleep(800);
1890
2030
  await inspectHostJobs(entry);
1891
2031
 
1892
2032
  const openUrl = client.sameOrigin
@@ -1895,23 +2035,22 @@ async function setupWorkspace(cfg, action) {
1895
2035
  const aiServerUp = await probeUrl(`http://127.0.0.1:${entry.port}/embed-config.js`);
1896
2036
  if (aiServerUp) {
1897
2037
  clearProcessProblem(sandboxId, "ai_server_launch", "ai");
2038
+ clearProcessProblem(sandboxId, "apps_not_started");
1898
2039
  }
1899
2040
 
1900
2041
  const processIssues = issuesForSandbox(sandboxId).map(
1901
2042
  ({ role: _role, ...issue }) => issue
1902
2043
  );
1903
- const warning = processIssues[0]?.message || null;
2044
+ const waitingForStart = !aiServerUp;
2045
+ const warning = waitingForStart
2046
+ ? "Folder is attached in Maintainer Pro. Use Start chat server when you want to launch the apps."
2047
+ : processIssues[0]?.message || null;
1904
2048
 
1905
2049
  for (const note of client.notes) log(note);
1906
- if (startedHosts.length) {
1907
- log(`started ${startedHosts.join(" + ")} in separate terminals`);
1908
- }
1909
- if (aiServerUp) {
1910
- log(`ai-server up — open ${openUrl}`);
1911
- } else if (warning) {
1912
- log(`process issue: ${warning}`);
2050
+ if (waitingForStart) {
2051
+ log(`folder attached waiting for Start (${openUrl})`);
1913
2052
  } else {
1914
- log("ai-server not reachable yet; it may still be starting");
2053
+ log(`ai-server already up open ${openUrl}`);
1915
2054
  }
1916
2055
 
1917
2056
  return {
@@ -1925,10 +2064,11 @@ async function setupWorkspace(cfg, action) {
1925
2064
  clientFiles: client.filesWritten,
1926
2065
  clientNotes: client.notes,
1927
2066
  aiServerUp,
1928
- startedHosts,
2067
+ startedHosts: [],
1929
2068
  openUrl,
1930
2069
  processIssues,
1931
2070
  warning,
2071
+ waitingForStart,
1932
2072
  projectInfo,
1933
2073
  };
1934
2074
  }
@@ -1970,12 +2110,6 @@ async function runActions(cfg, actions) {
1970
2110
  ok = false;
1971
2111
  result = { error: "No workspace or --no-ai-server" };
1972
2112
  } else {
1973
- const reserved = new Set();
1974
- for (const other of cfg.workspaces || []) {
1975
- if (other.sandboxId !== ws.sandboxId && other.port) {
1976
- reserved.add(Number(other.port));
1977
- }
1978
- }
1979
2113
  const problem = issuesForSandbox(ws.sandboxId)
1980
2114
  .map((issue) => issue.message)
1981
2115
  .join("\n");
@@ -1985,32 +2119,15 @@ async function runActions(cfg, actions) {
1985
2119
  problem ||
1986
2120
  "Local processes are not running or the project setup looks incomplete.",
1987
2121
  });
1988
- await startAiServerForWorkspace(ws, { reserved, cfg });
1989
- await sleep(1500);
1990
- const startedHosts = await ensureHostProcesses(ws, { reserved, cfg });
1991
- await sleep(800);
1992
- await inspectHostJobs(ws);
1993
- const up = await probeUrl(
1994
- `http://127.0.0.1:${ws.port}/embed-config.js`
1995
- );
1996
- if (up) clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
1997
- const processIssues = issuesForSandbox(ws.sandboxId).map(
1998
- ({ role: _role, ...issue }) => issue
1999
- );
2000
- const warning = processIssues[0]?.message || null;
2001
2122
  result = {
2002
- up,
2003
- startedHosts,
2004
- sandboxId: ws.sandboxId,
2005
- folderPath: ws.folderPath,
2006
- port: ws.port,
2007
- appUrl: ws.appUrl,
2008
- processIssues,
2009
- warning,
2123
+ ...(await startAppsForWorkspace(ws, cfg)),
2010
2124
  projectInfo,
2011
2125
  };
2012
- if (processIssues.some((issue) => issue.code === "ai_server_launch")) {
2013
- result.error = warning;
2126
+ if (
2127
+ Array.isArray(result.processIssues) &&
2128
+ result.processIssues.some((issue) => issue.code === "ai_server_launch")
2129
+ ) {
2130
+ result.error = result.warning;
2014
2131
  ok = false;
2015
2132
  }
2016
2133
  }
@@ -2051,7 +2168,7 @@ async function runActions(cfg, actions) {
2051
2168
  const sandboxId = String(
2052
2169
  action.sandboxId || action.payload?.sandboxId || ""
2053
2170
  );
2054
- forgetLaunch(sandboxId);
2171
+ await forgetLaunch(sandboxId);
2055
2172
  cfg.workspaces = (cfg.workspaces || []).filter(
2056
2173
  (w) => w.sandboxId !== sandboxId
2057
2174
  );
@@ -2100,6 +2217,7 @@ async function collectWorkspaceStates(cfg) {
2100
2217
  aiServerUp: up,
2101
2218
  startingAi: recentlyLaunched(`${ws.sandboxId}:${folder}:ai`),
2102
2219
  appUrl: ws.appUrl || null,
2220
+ appsRequested: appsWanted(ws),
2103
2221
  });
2104
2222
  }
2105
2223
  return localStates;
@@ -2122,6 +2240,7 @@ async function sendHeartbeat(cfg, folders, localStates) {
2122
2240
  aiServerUp: st.aiServerUp,
2123
2241
  port: st.port,
2124
2242
  appUrl: st.appUrl || undefined,
2243
+ appsRequested: Boolean(st.appsRequested),
2125
2244
  })),
2126
2245
  }
2127
2246
  );
@@ -2145,6 +2264,19 @@ async function buildIssues(cfg, workspaceStates) {
2145
2264
  }
2146
2265
  for (const st of workspaceStates) {
2147
2266
  if (st.aiServerUp || st.startingAi) continue;
2267
+ if (!st.appsRequested) {
2268
+ issues.push({
2269
+ code: "apps_not_started",
2270
+ severity: "info",
2271
+ title: `Apps are not running (${st.sandboxName || "sandbox"})`,
2272
+ message:
2273
+ "This folder is attached in Maintainer Pro. Use Start chat server when you want to launch the local apps.",
2274
+ resolution: "Use Start chat server.",
2275
+ actionCode: "start_ai_server",
2276
+ sandboxId: st.sandboxId,
2277
+ });
2278
+ continue;
2279
+ }
2148
2280
  const launch = [...processProblems.values()].find(
2149
2281
  (issue) =>
2150
2282
  issue.sandboxId === st.sandboxId && issue.code === "ai_server_launch"
@@ -2281,13 +2413,18 @@ async function main() {
2281
2413
  const up = await probeUrl(
2282
2414
  `http://127.0.0.1:${ws.port}/embed-config.js`
2283
2415
  );
2284
- if (!cfg.noAiServer && !up) {
2416
+ if (appsWanted(ws) && !cfg.noAiServer && !up) {
2285
2417
  await startAiServerForWorkspace(ws, { reserved, cfg });
2286
2418
  } else if (ws.port) {
2287
2419
  reserved.add(Number(ws.port));
2288
- if (up) clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
2420
+ if (up) {
2421
+ clearProcessProblem(ws.sandboxId, "ai_server_launch", "ai");
2422
+ clearProcessProblem(ws.sandboxId, "apps_not_started");
2423
+ }
2424
+ }
2425
+ if (appsWanted(ws)) {
2426
+ await ensureHostProcesses(ws, { reserved, cfg });
2289
2427
  }
2290
- await ensureHostProcesses(ws, { reserved, cfg });
2291
2428
  await inspectHostJobs(ws);
2292
2429
  localStates.push({
2293
2430
  sandboxId: ws.sandboxId,
@@ -2297,6 +2434,7 @@ async function main() {
2297
2434
  aiServerUp: up,
2298
2435
  startingAi: recentlyLaunched(`${ws.sandboxId}:${folder}:ai`),
2299
2436
  appUrl: ws.appUrl || null,
2437
+ appsRequested: appsWanted(ws),
2300
2438
  });
2301
2439
  }
2302
2440