zcode-acp-server 0.75.0 → 0.76.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/README.md +1 -1
- package/dist/code-fingerprint.json +2 -2
- package/dist/remote/hub-server.d.ts +5 -0
- package/dist/remote/hub-server.d.ts.map +1 -1
- package/dist/remote/hub-server.js +113 -37
- package/dist/remote/hub-server.js.map +1 -1
- package/dist/remote/settings-endpoint.d.ts.map +1 -1
- package/dist/remote/settings-endpoint.js +104 -2
- package/dist/remote/settings-endpoint.js.map +1 -1
- package/dist/settings/agents-config.d.ts +53 -18
- package/dist/settings/agents-config.d.ts.map +1 -1
- package/dist/settings/agents-config.js +161 -53
- package/dist/settings/agents-config.js.map +1 -1
- package/dist/settings/model-connectivity.d.ts +79 -0
- package/dist/settings/model-connectivity.d.ts.map +1 -0
- package/dist/settings/model-connectivity.js +170 -0
- package/dist/settings/model-connectivity.js.map +1 -0
- package/dist/settings/provider-config.d.ts +16 -2
- package/dist/settings/provider-config.d.ts.map +1 -1
- package/dist/settings/provider-config.js +68 -2
- package/dist/settings/provider-config.js.map +1 -1
- package/docs/BACKLOG.md +18 -0
- package/docs/REMOTE-CLIENTS.md +65 -27
- package/docs/REMOTE.md +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -315,7 +315,7 @@ Thanks to everyone who has contributed (updated automatically from the
|
|
|
315
315
|
<a href="https://github.com/william0wang/zcode-acp/graphs/contributors">
|
|
316
316
|
<!-- release-please re-stamps &v= on every release: the new URL busts the
|
|
317
317
|
contrib.rocks and GitHub camo image caches. -->
|
|
318
|
-
<img src="https://contrib.rocks/image?repo=william0wang/zcode-acp&v=0.
|
|
318
|
+
<img src="https://contrib.rocks/image?repo=william0wang/zcode-acp&v=0.76.0" alt="Contributors" /> <!-- x-release-please-version -->
|
|
319
319
|
</a>
|
|
320
320
|
|
|
321
321
|
## Related Projects
|
|
@@ -57,6 +57,11 @@ export interface HubOptions {
|
|
|
57
57
|
* shrink it).
|
|
58
58
|
*/
|
|
59
59
|
tuiRegisterTimeoutMs?: number;
|
|
60
|
+
/**
|
|
61
|
+
* How long after its last stamped HTTP call the desktop client still counts
|
|
62
|
+
* as present (default DESKTOP_PRESENCE_TTL_MS; tests shrink it).
|
|
63
|
+
*/
|
|
64
|
+
desktopPresenceTtlMs?: number;
|
|
60
65
|
/**
|
|
61
66
|
* Fires when the hub decided it should restart onto newer on-disk code
|
|
62
67
|
* (a newer bridge registered, or POST /api/upgrade found the dist newer).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AA8B9D,OAAO,EAA8B,KAAK,OAAO,EAAE,MAAM,IAAI,CAAC;AAK9D,OAAO,EAAkB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAuB9E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;IAC/B;;;;;;;;;OASG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE;QAClB,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QACvB,sFAAsF;QACtF,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;QACtB,gEAAgE;QAChE,MAAM,CAAC,EAAE,cAAc,CAAC;KACzB,KAAK,YAAY,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACzD;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,cAAc,EAAE,CAAC;IACpC;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;
|
|
1
|
+
{"version":3,"file":"hub-server.d.ts","sourceRoot":"","sources":["../../src/remote/hub-server.ts"],"names":[],"mappings":"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"}
|
|
@@ -77,6 +77,19 @@ const PROBE_TIMEOUT_MS = 2_000;
|
|
|
77
77
|
*/
|
|
78
78
|
const PROBE_GRACE_MS = 8_000;
|
|
79
79
|
const MAX_BODY_BYTES = 1024 * 1024;
|
|
80
|
+
/**
|
|
81
|
+
* Desktop Presence by recent request (amendment to ADR-0016): the desktop app
|
|
82
|
+
* polls GET /api/instances every 15s for its whole lifetime (DISCOVERY_REFRESH_MS
|
|
83
|
+
* in the app's runtime.rs), every call stamped with DESKTOP_CLIENT_HEADER. That
|
|
84
|
+
* poll is the one presence signal an IDLE desktop still emits: connection
|
|
85
|
+
* presence (`desktopPairs`) ends with the last proxied ACP connection, and a
|
|
86
|
+
* serve bridge self-exits once its last session closes — so a running desktop
|
|
87
|
+
* app with no open tab was invisible, and the next phone-originated
|
|
88
|
+
* create/resume opened a terminal window next to it. Three poll intervals of
|
|
89
|
+
* slack: the app schedules each poll AFTER the previous answer lands, so a
|
|
90
|
+
* slow hub stretches the gap past 15s.
|
|
91
|
+
*/
|
|
92
|
+
const DESKTOP_PRESENCE_TTL_MS = 45_000;
|
|
80
93
|
/** Constant-time token compare (hash both to equal length first). */
|
|
81
94
|
function tokenEquals(a, b) {
|
|
82
95
|
const ha = createHash("sha256").update(a).digest();
|
|
@@ -930,7 +943,9 @@ export function initializeClientName(data) {
|
|
|
930
943
|
* presence (`desktopPairs`) needs a proxied ACP connection to a bridge, so a
|
|
931
944
|
* cold hub (instances: 0, nothing to connect to) would open a terminal for
|
|
932
945
|
* the very FIRST session create — the request itself is the one presence
|
|
933
|
-
* signal that always exists.
|
|
946
|
+
* signal that always exists. Every authorized stamped call also refreshes the
|
|
947
|
+
* recent-request presence (DESKTOP_PRESENCE_TTL_MS), which is what keeps an
|
|
948
|
+
* idle desktop — no open tab, no proxied connection — visible to the hub.
|
|
934
949
|
*/
|
|
935
950
|
const DESKTOP_CLIENT_HEADER = "x-zcode-client";
|
|
936
951
|
function requestIsDesktopClient(req) {
|
|
@@ -944,7 +959,7 @@ function requestIsDesktopClient(req) {
|
|
|
944
959
|
* EADDRINUSE when another hub already owns the port).
|
|
945
960
|
*/
|
|
946
961
|
export function startHub(options) {
|
|
947
|
-
const { port, host, token, webDir, heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, probeGraceMs = PROBE_GRACE_MS, idleExitMs = IDLE_EXIT_MS, pingIntervalMs = PING_INTERVAL_MS, tuiRegisterTimeoutMs = TUI_REGISTER_TIMEOUT_MS, onIdleExit, onRestart, codePaths = defaultCodePaths(), projectsDbPath, stayAliveCheck = () => remoteEnabledLive(), spawnServe = defaultSpawnServe, terminalLaunches, staleVoteCooldownMs = STALE_VOTE_COOLDOWN_MS, } = options;
|
|
962
|
+
const { port, host, token, webDir, heartbeatTimeoutMs = HEARTBEAT_TIMEOUT_MS, probeGraceMs = PROBE_GRACE_MS, idleExitMs = IDLE_EXIT_MS, pingIntervalMs = PING_INTERVAL_MS, tuiRegisterTimeoutMs = TUI_REGISTER_TIMEOUT_MS, desktopPresenceTtlMs = DESKTOP_PRESENCE_TTL_MS, onIdleExit, onRestart, codePaths = defaultCodePaths(), projectsDbPath, stayAliveCheck = () => remoteEnabledLive(), spawnServe = defaultSpawnServe, terminalLaunches, staleVoteCooldownMs = STALE_VOTE_COOLDOWN_MS, } = options;
|
|
948
963
|
/** Frozen at hub start — the anchor the /api/upgrade signals compare to. */
|
|
949
964
|
const startedAt = Date.now();
|
|
950
965
|
/**
|
|
@@ -972,6 +987,27 @@ export function startHub(options) {
|
|
|
972
987
|
* creates. Sessions that already have a window are never touched.
|
|
973
988
|
*/
|
|
974
989
|
const desktopPairs = new Set();
|
|
990
|
+
/**
|
|
991
|
+
* Desktop Presence by recent request: when the last AUTHORIZED call stamped
|
|
992
|
+
* with the desktop identity landed (0 = never). Refreshed by handleHttp for
|
|
993
|
+
* every route — the 15s discovery poll is the steady source, but any
|
|
994
|
+
* stamped call counts.
|
|
995
|
+
*/
|
|
996
|
+
let desktopSeenAt = 0;
|
|
997
|
+
/**
|
|
998
|
+
* The one incubation-time presence verdict (amendment to ADR-0016). Checked
|
|
999
|
+
* at the endpoint, once per request — a desktop client dropping mid-flight
|
|
1000
|
+
* never opens a window retroactively.
|
|
1001
|
+
*/
|
|
1002
|
+
const desktopPresence = (req) => {
|
|
1003
|
+
if (desktopPairs.size > 0)
|
|
1004
|
+
return "connection";
|
|
1005
|
+
if (requestIsDesktopClient(req))
|
|
1006
|
+
return "request";
|
|
1007
|
+
if (desktopSeenAt > 0 && Date.now() - desktopSeenAt < desktopPresenceTtlMs)
|
|
1008
|
+
return "recent";
|
|
1009
|
+
return null;
|
|
1010
|
+
};
|
|
975
1011
|
const timers = [];
|
|
976
1012
|
/**
|
|
977
1013
|
* Single-flight incubation per workspace (remote session-create,
|
|
@@ -1005,7 +1041,7 @@ export function startHub(options) {
|
|
|
1005
1041
|
* requested session (ADR-0017), "serve" the detached headless bridge
|
|
1006
1042
|
* (background listing, ADR-0015).
|
|
1007
1043
|
*/
|
|
1008
|
-
const incubateServe = async (workspacePath, kind, sessionId, tabTitle,
|
|
1044
|
+
const incubateServe = async (workspacePath, kind, sessionId, tabTitle, desktop = null) => {
|
|
1009
1045
|
// Async spawn failures (ENOENT — the dir vanished between the whitelist
|
|
1010
1046
|
// check and the spawn) arrive as an 'error' event with exitCode still
|
|
1011
1047
|
// null; without this flag the poll burns the full budget.
|
|
@@ -1097,23 +1133,23 @@ export function startHub(options) {
|
|
|
1097
1133
|
// never churns when the bridge takes over the script's initial printf.
|
|
1098
1134
|
env.ZCODE_ACP_TAB_TITLE = sessionTabTitle(tabTitle, workspacePath);
|
|
1099
1135
|
}
|
|
1100
|
-
// Desktop Presence (amendment to ADR-0016): while
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
// AND resume incubate in the headless serve form: the
|
|
1104
|
-
// presenting surface, so a terminal window would
|
|
1105
|
-
// stream for nobody (a resume's window was ADR-0017's
|
|
1106
|
-
// desktop-originated resume loads the session itself
|
|
1107
|
-
// Equivalent to the exhausted-terminal-list fallback: the
|
|
1108
|
-
// empty, so openNextTerminal yields to the detached serve
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
//
|
|
1112
|
-
const desktopPresent = (kind === "tui" || kind === "resume") &&
|
|
1136
|
+
// Desktop Presence (amendment to ADR-0016): while the desktop app is
|
|
1137
|
+
// present — a proxied connection, the request's own identity header, or a
|
|
1138
|
+
// stamped call within the presence TTL (see desktopPresence) —
|
|
1139
|
+
// session-create AND resume incubate in the headless serve form: the
|
|
1140
|
+
// desktop app is the presenting surface, so a terminal window would
|
|
1141
|
+
// re-render the same stream for nobody (a resume's window was ADR-0017's
|
|
1142
|
+
// mobile surface; a desktop-originated resume loads the session itself
|
|
1143
|
+
// after attach). Equivalent to the exhausted-terminal-list fallback: the
|
|
1144
|
+
// launch list is empty, so openNextTerminal yields to the detached serve
|
|
1145
|
+
// spawn at once. The verdict was taken at the endpoint, once per request.
|
|
1146
|
+
// The listing was already headless; `ZCODE_ACP_HUB_TERMINAL=0` stays the
|
|
1147
|
+
// static variant.
|
|
1148
|
+
const desktopPresent = (kind === "tui" || kind === "resume") && desktop !== null;
|
|
1113
1149
|
if (desktopPresent) {
|
|
1114
1150
|
// warn (not log): the daemon's stderr is null in production, so the
|
|
1115
1151
|
// diary is the only place this state change is observable.
|
|
1116
|
-
warn(`hub: desktop
|
|
1152
|
+
warn(`hub: desktop app present (${desktop}) — incubating ${workspacePath} headless (Desktop Presence)`);
|
|
1117
1153
|
}
|
|
1118
1154
|
// Terminal preference list (ADR-0016 amendment): visible-terminal kinds
|
|
1119
1155
|
// walk it — a failed launch moves down at once, a window that never
|
|
@@ -1290,14 +1326,14 @@ export function startHub(options) {
|
|
|
1290
1326
|
* key carries the session id, so resumes of different sessions each get
|
|
1291
1327
|
* their own window.
|
|
1292
1328
|
*/
|
|
1293
|
-
const joinIncubation = (workspacePath, kind, sessionId, tabTitle,
|
|
1329
|
+
const joinIncubation = (workspacePath, kind, sessionId, tabTitle, desktop = null) => {
|
|
1294
1330
|
const mapKey = kind === "resume" ? `${workspacePath}\u0000resume\u0000${sessionId ?? ""}` : workspacePath;
|
|
1295
1331
|
const inflight = serveIncubations.get(mapKey);
|
|
1296
1332
|
if (inflight && (inflight.kind === kind || kind === "serve")) {
|
|
1297
1333
|
log(`hub: joining the incubating serve bridge for ${workspacePath}`);
|
|
1298
1334
|
return inflight.promise;
|
|
1299
1335
|
}
|
|
1300
|
-
const promise = incubateServe(workspacePath, kind, sessionId, tabTitle,
|
|
1336
|
+
const promise = incubateServe(workspacePath, kind, sessionId, tabTitle, desktop);
|
|
1301
1337
|
serveIncubations.set(mapKey, { kind, promise });
|
|
1302
1338
|
// Drop the entry once settled (identity-checked — a newer incubation may
|
|
1303
1339
|
// already have replaced it). The catch keeps the DERIVED promise handled;
|
|
@@ -1467,6 +1503,13 @@ export function startHub(options) {
|
|
|
1467
1503
|
res.end("ok");
|
|
1468
1504
|
return;
|
|
1469
1505
|
}
|
|
1506
|
+
// Desktop Presence by recent request: every AUTHORIZED call the desktop
|
|
1507
|
+
// client stamped refreshes the TTL (the 15s discovery poll in practice).
|
|
1508
|
+
// Authorization first — a bare header must never suppress windows. The
|
|
1509
|
+
// routes re-check authorization themselves; this extra compare only runs
|
|
1510
|
+
// for stamped requests.
|
|
1511
|
+
if (requestIsDesktopClient(req) && authorized(req, url, token))
|
|
1512
|
+
desktopSeenAt = Date.now();
|
|
1470
1513
|
if (url.pathname === "/api/instances" && req.method === "GET") {
|
|
1471
1514
|
if (!authorized(req, url, token)) {
|
|
1472
1515
|
res.writeHead(401, { "Content-Type": "text/plain" });
|
|
@@ -1850,6 +1893,9 @@ export function startHub(options) {
|
|
|
1850
1893
|
// reuses a live serve bridge: the App flow always runs the history
|
|
1851
1894
|
// listing first, which incubates a headless serve instance, and reuse
|
|
1852
1895
|
// would mean the window can never open (the pre-amendment behaviour).
|
|
1896
|
+
// Desktop Presence inverts both rules (see desktopPresence below): no
|
|
1897
|
+
// window is coming, so the workspace's live serve bridge hosts the session
|
|
1898
|
+
// too, and only a workspace without one spawns — headless.
|
|
1853
1899
|
// The hub waits for the bridge's heartbeat registration; the bridge lives
|
|
1854
1900
|
// its own life afterwards (a terminal TUI until the window closes, a
|
|
1855
1901
|
// serve bridge on its idle timer). Concurrent identical POSTs join one
|
|
@@ -1903,6 +1949,35 @@ export function startHub(options) {
|
|
|
1903
1949
|
res.end("session deleted");
|
|
1904
1950
|
return;
|
|
1905
1951
|
}
|
|
1952
|
+
// Desktop Presence (amendment to ADR-0016): the desktop app is the
|
|
1953
|
+
// presenting surface, so no window opens — and with no window to be the
|
|
1954
|
+
// feature, the pre-amendment reuse is right again: the workspace's live
|
|
1955
|
+
// serve bridge (the listing's, an earlier create's, an open window's)
|
|
1956
|
+
// hosts this session too. The client attaches and session/new-s or
|
|
1957
|
+
// session/loads there, exactly as it already does after a headless
|
|
1958
|
+
// rescue — one bridge and one backend process per project instead of
|
|
1959
|
+
// one per session. A resume prefers the bridge that holds the session
|
|
1960
|
+
// LIVE (any origin): two bridges must never load one conversation. Only
|
|
1961
|
+
// a workspace with no live serve bridge still spawns, headless. An
|
|
1962
|
+
// unhealthy-marked bridge is skipped (a dead one would fail the dial
|
|
1963
|
+
// and only be pruned afterwards); a stalled-but-alive one costs a
|
|
1964
|
+
// duplicate, the status quo before this reuse.
|
|
1965
|
+
const desktop = desktopPresence(req);
|
|
1966
|
+
if (desktop) {
|
|
1967
|
+
const holder = resumeSid
|
|
1968
|
+
? Array.from(instances.values()).find((e) => e.sessions.some((s) => s.sessionId === resumeSid))
|
|
1969
|
+
: undefined;
|
|
1970
|
+
const live = holder ?? findServeInstances(workspacePath).find((e) => e.unhealthySince === undefined);
|
|
1971
|
+
if (live) {
|
|
1972
|
+
// warn (not log): the diary is the only place this decision is
|
|
1973
|
+
// observable on the detached daemon (the phone's create that
|
|
1974
|
+
// deliberately did NOT open a window).
|
|
1975
|
+
warn(`hub: desktop app present (${desktop}) — ${resumeSid ? `resume ${resumeSid.slice(0, 8)}` : "create"} for ${workspacePath} reuses ${live.origin} bridge ${live.id}`);
|
|
1976
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1977
|
+
res.end(JSON.stringify({ id: live.id, reused: true }));
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1906
1981
|
if (resumeSid) {
|
|
1907
1982
|
// Tab title, best-effort: the App browsed this conversation through
|
|
1908
1983
|
// the serve bridge's /sessions history, which is normally still live
|
|
@@ -1920,12 +1995,13 @@ export function startHub(options) {
|
|
|
1920
1995
|
// Resume never reuses the live serve bridge: the window IS the
|
|
1921
1996
|
// feature for MOBILE-originated resumes (the attaching client and the
|
|
1922
1997
|
// terminal share one bridge and one backend process for the session;
|
|
1923
|
-
// ADR-0017)
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
// the same honesty as a window
|
|
1928
|
-
|
|
1998
|
+
// ADR-0017). Under Desktop Presence the reuse above already answered
|
|
1999
|
+
// when a live bridge existed; what reaches here incubates headless
|
|
2000
|
+
// (the client loads the session itself after attach). A bogus id
|
|
2001
|
+
// still gets its honest spawn — the surface shows the load failure
|
|
2002
|
+
// and falls back to a fresh session, the same honesty as a window
|
|
2003
|
+
// that dies early (ADR-0016 §4).
|
|
2004
|
+
const incubation = joinIncubation(workspacePath, "resume", resumeSid, tabTitle, desktop);
|
|
1929
2005
|
try {
|
|
1930
2006
|
const out = await incubation;
|
|
1931
2007
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -1938,18 +2014,18 @@ export function startHub(options) {
|
|
|
1938
2014
|
return;
|
|
1939
2015
|
}
|
|
1940
2016
|
// Session-create ALWAYS incubates a VISIBLE terminal TUI (ADR-0016 as
|
|
1941
|
-
// amended) — UNLESS Desktop Presence applies (
|
|
1942
|
-
//
|
|
1943
|
-
//
|
|
1944
|
-
//
|
|
1945
|
-
//
|
|
1946
|
-
//
|
|
1947
|
-
//
|
|
1948
|
-
//
|
|
1949
|
-
//
|
|
1950
|
-
//
|
|
1951
|
-
//
|
|
1952
|
-
const incubation = joinIncubation(workspacePath, "tui", undefined, undefined,
|
|
2017
|
+
// amended) — UNLESS Desktop Presence applies (see desktopPresence; a
|
|
2018
|
+
// workspace with a live bridge was already answered above, so what
|
|
2019
|
+
// reaches here under presence spawns headless). The App flow lists the
|
|
2020
|
+
// project's history first, which incubates a headless serve bridge —
|
|
2021
|
+
// reusing that instance (the original behaviour) meant a window could
|
|
2022
|
+
// NEVER open once the project was browsed, and every create ran
|
|
2023
|
+
// invisibly in the background. The answer is the NEW spawn's instance;
|
|
2024
|
+
// concurrent identical POSTs join one incubation, and the detached
|
|
2025
|
+
// headless serve spawn remains the fallback (no GUI / gated off / open
|
|
2026
|
+
// failure). Clients that want a headless attach use the listing's
|
|
2027
|
+
// instance id instead of this POST.
|
|
2028
|
+
const incubation = joinIncubation(workspacePath, "tui", undefined, undefined, desktop);
|
|
1953
2029
|
try {
|
|
1954
2030
|
const out = await incubation;
|
|
1955
2031
|
res.writeHead(200, { "Content-Type": "application/json" });
|