@rynx-ai/server 0.1.0 → 0.1.10-beta.2

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 (65) hide show
  1. package/dist/browser-surface-control-queue.d.ts +42 -0
  2. package/dist/browser-surface-control-queue.js +187 -0
  3. package/dist/browser-surface-ws.d.ts +16 -0
  4. package/dist/browser-surface-ws.js +356 -0
  5. package/dist/control-api.d.ts +76 -4
  6. package/dist/control-api.js +3014 -133
  7. package/dist/control-web/assets/highlighted-body-OFNGDK62-DHu2g-rk.js +1 -0
  8. package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
  9. package/dist/control-web/assets/index-BU-_Qud_.css +32 -0
  10. package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  11. package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  12. package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  13. package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  14. package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  15. package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  16. package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  17. package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  18. package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  19. package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  20. package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  21. package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  22. package/dist/control-web/assets/mermaid-GHXKKRXX-CSYzOmBR.js +131 -0
  23. package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
  24. package/dist/control-web/favicon.png +0 -0
  25. package/dist/control-web/favicon.svg +38 -0
  26. package/dist/control-web/index.html +16 -0
  27. package/dist/control-web-dist.d.ts +1 -1
  28. package/dist/control-web-dist.js +19 -14
  29. package/dist/desktop-browser-host.d.ts +170 -0
  30. package/dist/desktop-browser-host.js +1018 -0
  31. package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
  32. package/dist/direct-runtime-browser-inspect-server.js +749 -0
  33. package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
  34. package/dist/direct-runtime-browser-surface-server.js +821 -0
  35. package/dist/direct-runtime-server.d.ts +143 -0
  36. package/dist/direct-runtime-server.js +1959 -0
  37. package/dist/emulator-surface-ws.d.ts +8 -0
  38. package/dist/emulator-surface-ws.js +198 -0
  39. package/dist/machine-session-service.d.ts +265 -0
  40. package/dist/machine-session-service.js +822 -0
  41. package/dist/provider-cli-host.d.ts +10 -0
  42. package/dist/provider-cli-host.js +1 -0
  43. package/dist/remote-runtime-dispatcher.d.ts +94 -0
  44. package/dist/remote-runtime-dispatcher.js +403 -0
  45. package/dist/remote-runtime-session-projection.d.ts +19 -0
  46. package/dist/remote-runtime-session-projection.js +566 -0
  47. package/dist/remote-runtime.d.ts +22 -0
  48. package/dist/remote-runtime.js +32 -0
  49. package/dist/runtime-web-auth.d.ts +26 -0
  50. package/dist/runtime-web-auth.js +132 -0
  51. package/dist/server.d.ts +364 -12
  52. package/dist/server.js +341 -100
  53. package/dist/session-browser-service.d.ts +248 -0
  54. package/dist/session-browser-service.js +772 -0
  55. package/dist/session-browser-surface-coordinator.d.ts +24 -0
  56. package/dist/session-browser-surface-coordinator.js +669 -0
  57. package/dist/session-emulator-service.d.ts +167 -0
  58. package/dist/session-emulator-service.js +464 -0
  59. package/dist/session-runtime-index.d.ts +23 -0
  60. package/dist/session-runtime-index.js +96 -0
  61. package/dist/session-terminal-host.d.ts +51 -0
  62. package/dist/session-terminal-host.js +270 -0
  63. package/dist/terminal-ws.d.ts +12 -20
  64. package/dist/terminal-ws.js +421 -97
  65. package/package.json +14 -7
package/dist/server.js CHANGED
@@ -1,19 +1,87 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import Koa from "koa";
3
3
  import Router from "@koa/router";
4
- import { loadConfig, resolveRuntimeModel, ConversationRuntime, InMemorySessionBus, persistSessionEvent, } from "@rynx-ai/core";
4
+ import { loadConfig, listAgentSpecs, loadAgentSpec, resolveSessionExecution, resolveRuntimeModel, ConversationRuntime, InMemorySessionBus, persistSessionEvent, } from "@rynx-ai/core";
5
5
  import { RunnerManager, FileCodexSessionStore, resolveCodexSessionStorePath, } from "@rynx-ai/runtime";
6
6
  import { ChannelManager } from "./channel-manager.js";
7
- import { createControlRouter } from "./control-api.js";
7
+ import { createControlRouter, } from "./control-api.js";
8
8
  import { attachEmulatorTouchWs } from "./emulator-touch-ws.js";
9
9
  import { attachTerminalWs } from "./terminal-ws.js";
10
+ import { attachBrowserSurfaceWs } from "./browser-surface-ws.js";
11
+ import { attachEmulatorSurfaceWs } from "./emulator-surface-ws.js";
12
+ import { attachDesktopBrowserHostServer, } from "./desktop-browser-host.js";
10
13
  import { resolveControlWebDist, sendSpaFile } from "./control-web-dist.js";
14
+ import { SessionRuntimeIndex } from "./session-runtime-index.js";
15
+ import { createRunnerSessionTerminalHost, } from "./session-terminal-host.js";
16
+ import { runtimeWebAccessPolicyForBindHost, } from "./runtime-web-auth.js";
11
17
  export { ChannelManager } from "./channel-manager.js";
12
- function toErrorMessage(error) {
13
- if (error instanceof Error) {
14
- return error.message;
18
+ export { MachineSessionService, MachineSessionServiceFailure, MachineSessionServiceInputError, synthesizeSessionTitle, } from "./machine-session-service.js";
19
+ export { SessionBrowserHostError, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
20
+ export { SessionEmulatorBindingConflictError, SessionEmulatorService, SessionEmulatorServiceError, } from "./session-emulator-service.js";
21
+ export { SessionBrowserSurfaceCoordinator } from "./session-browser-surface-coordinator.js";
22
+ export { attachDesktopBrowserHostServer, createDesktopFirstSessionBrowserHost, DesktopBrowserHostAbsentError, DesktopBrowserHostProtocolError, DesktopBrowserHostRegistry, } from "./desktop-browser-host.js";
23
+ export { createDaemonRuntimeHost, encodeDaemonStatus } from "./remote-runtime.js";
24
+ export { DIRECT_RUNTIME_CONTROL_PATH, DirectRuntimeConnectionHub, startDirectRuntimeServer, } from "./direct-runtime-server.js";
25
+ export { attachDirectRuntimeBrowserSurfaceServer, DirectRuntimeBrowserSurfaceHostError, } from "./direct-runtime-browser-surface-server.js";
26
+ export { attachDirectRuntimeBrowserInspectServer, DirectRuntimeBrowserInspectHostError, } from "./direct-runtime-browser-inspect-server.js";
27
+ export { RemoteRuntimeDispatcher } from "./remote-runtime-dispatcher.js";
28
+ export { createRunnerSessionTerminalHost, SessionTerminalOpenError, SessionTerminalStreamError, } from "./session-terminal-host.js";
29
+ /** Preserve one session's mirror order without serializing unrelated sessions. */
30
+ export class SessionMirrorQueue {
31
+ persist;
32
+ tails = new Map();
33
+ constructor(persist) {
34
+ this.persist = persist;
15
35
  }
16
- return "Unknown error";
36
+ enqueue(sessionId, event) {
37
+ const previous = this.tails.get(sessionId) ?? Promise.resolve();
38
+ const next = previous
39
+ .catch(() => undefined)
40
+ .then(() => this.persist(sessionId, event));
41
+ this.tails.set(sessionId, next);
42
+ const cleanup = () => {
43
+ if (this.tails.get(sessionId) === next)
44
+ this.tails.delete(sessionId);
45
+ };
46
+ void next.then(cleanup, cleanup);
47
+ return next;
48
+ }
49
+ }
50
+ /**
51
+ * Compose the process-owned Session runtime once. The daemon may call this
52
+ * before creating its Direct dispatcher; {@link startServer} accepts the same
53
+ * bundle and therefore does not create a second bus, runtime index, or runner.
54
+ */
55
+ export function createSessionRuntimeServices(input) {
56
+ const sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(input.config));
57
+ const runnerManager = new RunnerManager({
58
+ config: input.config,
59
+ sessionStore,
60
+ ...(input.sessionContextProvider
61
+ ? { sessionContextProvider: input.sessionContextProvider }
62
+ : {}),
63
+ });
64
+ const sessionBus = new InMemorySessionBus();
65
+ const sessionRuntimeIndex = new SessionRuntimeIndex();
66
+ const conversationRuntime = new ConversationRuntime({
67
+ config: input.config,
68
+ executor: runnerManager,
69
+ sessionBus,
70
+ });
71
+ const sessionMirrorQueue = new SessionMirrorQueue((sessionId, event) => persistSessionEvent(sessionId, event, { sessionLog: input.sessionLog, sessionBus }));
72
+ runnerManager.onMirror((sessionId, event) => {
73
+ sessionRuntimeIndex.observe(sessionId, event);
74
+ void sessionMirrorQueue.enqueue(sessionId, event).catch((error) => {
75
+ input.onMirrorError?.(error, sessionId);
76
+ });
77
+ });
78
+ return {
79
+ sessionStore,
80
+ runnerManager,
81
+ sessionBus,
82
+ sessionRuntimeIndex,
83
+ conversationRuntime,
84
+ };
17
85
  }
18
86
  /**
19
87
  * The HTTP surface is just a liveness probe for container orchestration — all
@@ -21,7 +89,7 @@ function toErrorMessage(error) {
21
89
  * Lark bot reaches its server over an outbound WebSocket long connection), so
22
90
  * no agent request / response / SSE endpoints are exposed here.
23
91
  */
24
- export function createApp({ control } = {}) {
92
+ export function createApp({ config = loadConfig(), control } = {}) {
25
93
  const app = new Koa();
26
94
  const router = new Router();
27
95
  router.get("/health", (ctx) => {
@@ -30,16 +98,24 @@ export function createApp({ control } = {}) {
30
98
  app.use(router.routes());
31
99
  app.use(router.allowedMethods());
32
100
  if (control) {
33
- const controlRouter = createControlRouter(control);
101
+ const controlRouter = createControlRouter({
102
+ ...control,
103
+ runtimeWebAccess: control.runtimeWebAccess ??
104
+ runtimeWebAccessPolicyForBindHost(config.HOST),
105
+ });
34
106
  app.use(controlRouter.routes());
35
107
  app.use(controlRouter.allowedMethods());
36
- // Serve the built control-web SPA for everything that isn't /health or /api.
108
+ }
109
+ if (control) {
110
+ // Serve the built control-web SPA outside the server-owned health, API, and
111
+ // one-shot control-link namespaces.
37
112
  const distDir = resolveControlWebDist();
38
113
  app.use(async (ctx, next) => {
39
114
  if (ctx.method !== "GET" && ctx.method !== "HEAD")
40
115
  return next();
41
- if (ctx.path === "/health" || ctx.path.startsWith("/api/"))
116
+ if (["/health", "/api", "/control"].some((prefix) => ctx.path === prefix || ctx.path.startsWith(`${prefix}/`))) {
42
117
  return next();
118
+ }
43
119
  if (!distDir) {
44
120
  if (ctx.path === "/") {
45
121
  ctx.type = "html";
@@ -55,46 +131,66 @@ export function createApp({ control } = {}) {
55
131
  }
56
132
  return app;
57
133
  }
58
- export async function startServer({ config = loadConfig(), channelFactories = [], channelInstances, loadInstances, control, sessionLog, sessionRegistry, } = {}) {
59
- // One shared session store, owned here and handed to the channels via the
60
- // ChannelContext (mirror index / active-session reads). Runner children open
61
- // the same on-disk store for their session bindings.
62
- const sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(config));
63
- // The executor + capability surface. Holds no execution backend itself: it
64
- // spawns a runner child per session for turns, forwards per-thread
65
- // capabilities to a runner, and answers status/model-list locally.
66
- const runnerManager = new RunnerManager({
134
+ export async function startServer({ config = loadConfig(), channelInstances, loadInstances, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, localSessionResources, sessionEmulators, sessionRuntimeServices, sessionLifecycle, sessionTerminals, runtimeLocalBrowser, providerClis, desktopBrowserHost, } = {}) {
135
+ const runtimeWebAccess = runtimeWebAccessPolicyForBindHost(config.HOST);
136
+ const desktopBrowserManagementToken = desktopBrowserHost
137
+ ? requireDaemonManagementToken(daemonManagement)
138
+ : undefined;
139
+ const sessionServices = sessionRuntimeServices ?? createSessionRuntimeServices({
67
140
  config,
68
- sessionStore,
69
- // Give runner children the control-plane URL so a claude-native
70
- // PermissionRequest hook subprocess can POST approvals back to this daemon.
71
- childEnv: {
72
- RYNX_CONTROL_URL: `http://127.0.0.1:${config.PORT}`,
141
+ sessionLog,
142
+ onMirrorError: (error, sessionId) => {
143
+ console.error(`[session-mirror] session=${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
73
144
  },
74
145
  });
75
- // One canonical event bus shared by every mounted instance, so a future
76
- // observer (web console) sees every channel's turns over one fan-out.
77
- const sessionBus = new InMemorySessionBus();
78
- const conversationRuntime = new ConversationRuntime({
79
- config,
80
- executor: runnerManager,
81
- // The live co-drive path (`runLive`) reads a turn's mirrored events off this bus.
82
- sessionBus,
83
- });
84
- // codex-native single-writer: a session's persistent forwarder mirrors EVERY
85
- // turn (web- AND co-driving-TUI-initiated) as canonical SessionEvents; persist
86
- // + publish each so the web console's /stream renders them uniformly.
87
- runnerManager.onMirror((sessionId, event) => {
88
- void persistSessionEvent(sessionId, event, { sessionLog, sessionBus });
89
- });
146
+ const { sessionStore, runnerManager, sessionBus, sessionRuntimeIndex, conversationRuntime, } = sessionServices;
147
+ const legacyLocalTerminalHost = sessionTerminals ??
148
+ createRunnerSessionTerminalHost({
149
+ runnerManager,
150
+ resolveLiveSession: async (sessionId) => {
151
+ const session = await sessionStore.get(sessionId);
152
+ if (!session || !runnerManager.hasLiveSession(sessionId))
153
+ return undefined;
154
+ return { cwd: session.cwd };
155
+ },
156
+ });
157
+ if (pluginRuntime) {
158
+ if (!sessionRegistry)
159
+ throw new Error("plugin runtime requires a machine session registry");
160
+ pluginRuntime.bindHostServices({
161
+ conversationRuntime,
162
+ capabilities: runnerManager,
163
+ sessionStore,
164
+ sessionRegistry,
165
+ agentCatalog: {
166
+ list: async () => (await listAgentSpecs()).map(toPluginAgentSummary),
167
+ get: async (id) => {
168
+ const spec = await loadAgentSpec(id);
169
+ return spec ? toPluginAgentSummary(spec) : null;
170
+ },
171
+ resolveExecution: (input) => resolveSessionExecution({
172
+ config,
173
+ ...(input.defaultAgent ? { defaultAgent: input.defaultAgent } : {}),
174
+ session: input.session,
175
+ }),
176
+ },
177
+ interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
178
+ resolveSessionInteraction: (sessionId, interactionId, resolution) => runnerManager.resolveInteraction(sessionId, interactionId, resolution),
179
+ });
180
+ }
90
181
  // claude `/clear`·`/fork` rotates to a fresh machine-session; record its meta so
91
- // the console list shows it with the carried-over agent/model + a source tag.
182
+ // the console list shows it with the carried-over launch target/model + a source tag.
92
183
  // (It also surfaces via the log-merge once its first turn mirrors.)
93
184
  runnerManager.onRotate((r) => {
185
+ const source = control?.getSessionMeta(r.from);
94
186
  control?.createSessionMeta({
95
187
  id: r.to,
96
188
  source: r.kind === "fork" ? "fork" : "clear",
97
- ...(r.agent ? { agent: r.agent } : {}),
189
+ ...(r.agent
190
+ ? { agent: r.agent }
191
+ : source?.provider
192
+ ? { provider: source.provider }
193
+ : {}),
98
194
  ...(r.model ? { model: r.model } : {}),
99
195
  createdAt: new Date().toISOString(),
100
196
  });
@@ -103,6 +199,7 @@ export async function startServer({ config = loadConfig(), channelFactories = []
103
199
  // `loadInstances`; the control plane drives live start/stop/reload).
104
200
  const resolveInstances = loadInstances ?? (channelInstances ? async () => channelInstances : null);
105
201
  if (resolveInstances) {
202
+ const controlShutdown = new AbortController();
106
203
  const manager = new ChannelManager({
107
204
  config,
108
205
  conversationRuntime,
@@ -124,6 +221,27 @@ export async function startServer({ config = loadConfig(), channelFactories = []
124
221
  sessionLog,
125
222
  runnerManager,
126
223
  sessionStore,
224
+ pluginRuntimeStatus: pluginRuntime
225
+ ? () => pluginRuntime.statusSnapshot()
226
+ : undefined,
227
+ pluginRuntimeReload: pluginRuntime
228
+ ? (pluginId) => pluginRuntime.reloadPluginRuntime(pluginId)
229
+ : undefined,
230
+ pluginRuntimeEnsure: pluginRuntime
231
+ ? (pluginId) => pluginRuntime.ensurePluginRuntime(pluginId)
232
+ : undefined,
233
+ daemonManagement,
234
+ remoteRuntime,
235
+ remoteRuntimeAdmin,
236
+ runtimeTargetControl,
237
+ runtimeConnectionResolver,
238
+ localSessionResources,
239
+ sessionEmulators,
240
+ shutdownSignal: controlShutdown.signal,
241
+ sessionRuntimeIndex,
242
+ sessionLifecycle,
243
+ runtimeLocalBrowser,
244
+ providerClis,
127
245
  }
128
246
  : undefined,
129
247
  });
@@ -137,44 +255,59 @@ export async function startServer({ config = loadConfig(), channelFactories = []
137
255
  runtime: config.AGENT_RUNTIME,
138
256
  model: resolveRuntimeModel(config, config.AGENT_RUNTIME),
139
257
  }));
140
- void manager.startAll();
141
- // Live WS bridges share this http.Server (no second port).
142
- attachTerminalWs(server, { runnerManager });
143
- if (control?.emulator)
144
- attachEmulatorTouchWs(server, { emulator: control.emulator });
145
- server.on("close", () => {
146
- void manager.stopAll();
147
- void runnerManager.stop();
258
+ const startup = (async () => {
259
+ await pluginRuntime?.startPluginRuntimes();
260
+ await manager.startAll();
261
+ })();
262
+ void startup.catch((error) => {
263
+ console.error(JSON.stringify({
264
+ level: "error",
265
+ type: "plugin-runtime",
266
+ event: "startup_failed",
267
+ error: error instanceof Error ? error.message : String(error),
268
+ }));
148
269
  });
149
- resolve(server);
270
+ // Live WS bridges share this http.Server (no second port).
271
+ const webSocketServers = [
272
+ attachTerminalWs(server, {
273
+ runtimeConnectionResolver,
274
+ legacyLocalTerminalHost,
275
+ runtimeWebAccess,
276
+ }),
277
+ attachBrowserSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
278
+ attachEmulatorSurfaceWs(server, { runtimeConnectionResolver, runtimeWebAccess }),
279
+ ];
280
+ if (desktopBrowserHost && desktopBrowserManagementToken) {
281
+ webSocketServers.push(attachDesktopBrowserHostServer(server, {
282
+ ...desktopBrowserHost,
283
+ managementToken: desktopBrowserManagementToken,
284
+ }));
285
+ }
286
+ if (control?.emulator) {
287
+ webSocketServers.push(attachEmulatorTouchWs(server, { emulator: control.emulator }));
288
+ }
289
+ resolve(bindServerLifecycle(server, {
290
+ fence: () => {
291
+ controlShutdown.abort(new Error("Rynx server is shutting down"));
292
+ },
293
+ closeUpgrades: async () => {
294
+ await closeWebSocketServers(webSocketServers);
295
+ },
296
+ cleanup: async () => {
297
+ await runCleanupSteps([
298
+ () => startup.catch(() => undefined),
299
+ () => manager.stopAll(),
300
+ () => Promise.resolve(daemonManagement?.pluginManagement?.dispose?.()),
301
+ () => pluginRuntime?.dispose() ?? Promise.resolve(),
302
+ () => runnerManager.stop(),
303
+ ]);
304
+ },
305
+ }));
150
306
  });
151
307
  });
152
308
  }
153
- // Legacy path: one shared context for all factories (unchanged behavior).
154
- const channelContext = {
155
- config,
156
- conversationRuntime,
157
- capabilities: runnerManager,
158
- sessionStore,
159
- };
160
- // Construct each channel defensively: a plugin that throws while building
161
- // (e.g. Lark with missing credentials) is isolated and logged, so one broken
162
- // channel can't take down the host or its sibling channels.
163
- const channels = [];
164
- for (const factory of channelFactories) {
165
- try {
166
- channels.push(factory(channelContext));
167
- }
168
- catch (error) {
169
- console.error(JSON.stringify({
170
- level: "error",
171
- type: "channel",
172
- event: "construct_failed",
173
- error: toErrorMessage(error),
174
- }));
175
- }
176
- }
177
- const app = createApp({ config });
309
+ // Core-only mode: no chat instances and no plugin code are required.
310
+ const app = createApp();
178
311
  return new Promise((resolve) => {
179
312
  const server = app.listen(config.PORT, config.HOST, () => {
180
313
  console.log(JSON.stringify({
@@ -184,37 +317,145 @@ export async function startServer({ config = loadConfig(), channelFactories = []
184
317
  port: config.PORT,
185
318
  runtime: config.AGENT_RUNTIME,
186
319
  model: resolveRuntimeModel(config, config.AGENT_RUNTIME),
187
- channels: channels.map((channel) => channel.id),
188
320
  }));
189
- for (const channel of channels) {
190
- void channel.start().catch((error) => {
191
- console.error(JSON.stringify({
192
- level: "error",
193
- type: "channel",
194
- channel: channel.id,
195
- event: "runtime.start_failed",
196
- error: toErrorMessage(error),
197
- }));
198
- });
321
+ const webSocketServers = [];
322
+ if (desktopBrowserHost && desktopBrowserManagementToken) {
323
+ webSocketServers.push(attachDesktopBrowserHostServer(server, {
324
+ ...desktopBrowserHost,
325
+ managementToken: desktopBrowserManagementToken,
326
+ }));
199
327
  }
200
- server.on("close", () => {
201
- void runnerManager.stop();
202
- for (const channel of channels) {
203
- void channel.stop().catch((error) => {
204
- console.error(JSON.stringify({
205
- level: "error",
206
- type: "channel",
207
- channel: channel.id,
208
- event: "runtime.stop_failed",
209
- error: toErrorMessage(error),
210
- }));
211
- });
212
- }
328
+ resolve(bindServerLifecycle(server, {
329
+ ...(webSocketServers.length > 0
330
+ ? { closeUpgrades: () => closeWebSocketServers(webSocketServers) }
331
+ : {}),
332
+ cleanup: () => runnerManager.stop(),
333
+ }));
334
+ });
335
+ });
336
+ }
337
+ function requireDaemonManagementToken(host) {
338
+ const token = host?.managementToken;
339
+ const bytes = typeof token === "string" ? Buffer.byteLength(token, "utf8") : 0;
340
+ if (bytes < 32 || bytes > 256) {
341
+ throw new Error("Desktop Browser Host requires configured daemon management auth");
342
+ }
343
+ return token;
344
+ }
345
+ function bindServerLifecycle(server, lifecycle) {
346
+ let closed = false;
347
+ let acceptanceClosePromise;
348
+ let shutdownPromise;
349
+ const nativeClose = server.close.bind(server);
350
+ server.once("close", () => {
351
+ closed = true;
352
+ });
353
+ const managed = server;
354
+ managed.stopAccepting = () => {
355
+ if (acceptanceClosePromise)
356
+ return;
357
+ lifecycle.fence?.();
358
+ acceptanceClosePromise = closeHttpServer(server, nativeClose, () => closed);
359
+ // The full shutdown joins this promise immediately; retain a rejection
360
+ // handler so a caller cannot create an unhandled rejection in between.
361
+ void acceptanceClosePromise.catch(() => undefined);
362
+ };
363
+ managed.shutdown = () => {
364
+ if (shutdownPromise)
365
+ return shutdownPromise;
366
+ managed.stopAccepting();
367
+ shutdownPromise = (async () => {
368
+ // Stop accepting first, close upgraded sockets so they cannot hold the
369
+ // HTTP drain open, then keep runtime dependencies alive until every
370
+ // in-flight HTTP request has completed.
371
+ const closeResult = acceptanceClosePromise;
372
+ const upgradeResult = lifecycle.closeUpgrades?.() ?? Promise.resolve();
373
+ const drainResults = await Promise.allSettled([upgradeResult, closeResult]);
374
+ const errors = drainResults
375
+ .filter((result) => result.status === "rejected")
376
+ .map((result) => result.reason);
377
+ try {
378
+ await lifecycle.cleanup();
379
+ }
380
+ catch (error) {
381
+ errors.push(error);
382
+ }
383
+ if (errors.length > 0)
384
+ throw new AggregateError(errors, "Rynx server shutdown failed");
385
+ })();
386
+ return shutdownPromise;
387
+ };
388
+ managed.close = ((callback) => {
389
+ const completion = managed.shutdown();
390
+ if (callback) {
391
+ void completion.then(() => callback(), (error) => callback(asError(error)));
392
+ }
393
+ else {
394
+ void completion.catch((error) => {
395
+ console.error(JSON.stringify({
396
+ level: "error",
397
+ type: "shutdown",
398
+ event: "cleanup_failed",
399
+ error: error instanceof Error ? error.message : String(error),
400
+ }));
213
401
  });
214
- resolve(server);
402
+ }
403
+ return managed;
404
+ });
405
+ return managed;
406
+ }
407
+ async function closeHttpServer(server, nativeClose, isClosed) {
408
+ if (isClosed())
409
+ return;
410
+ if (!server.listening) {
411
+ await new Promise((resolve) => server.once("close", resolve));
412
+ return;
413
+ }
414
+ await new Promise((resolve, reject) => {
415
+ nativeClose((error) => {
416
+ if (error)
417
+ reject(error);
418
+ else
419
+ resolve();
215
420
  });
216
421
  });
217
422
  }
423
+ function asError(error) {
424
+ return error instanceof Error ? error : new Error(String(error));
425
+ }
426
+ async function runCleanupSteps(steps) {
427
+ const errors = [];
428
+ for (const step of steps) {
429
+ try {
430
+ await step();
431
+ }
432
+ catch (error) {
433
+ errors.push(error);
434
+ }
435
+ }
436
+ if (errors.length > 0)
437
+ throw new AggregateError(errors, "Rynx resource cleanup failed");
438
+ }
439
+ async function closeWebSocketServers(servers) {
440
+ await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
441
+ for (const client of server.clients)
442
+ client.terminate();
443
+ server.close((error) => {
444
+ if (error)
445
+ reject(error);
446
+ else
447
+ resolve();
448
+ });
449
+ })));
450
+ }
451
+ function toPluginAgentSummary(spec) {
452
+ return {
453
+ id: spec.id,
454
+ ...(spec.name ? { name: spec.name } : {}),
455
+ ...(spec.executor?.runtime ? { runtime: spec.executor.runtime } : {}),
456
+ ...(spec.executor?.model ? { model: spec.executor.model } : {}),
457
+ };
458
+ }
218
459
  const entrypoint = process.argv[1];
219
460
  if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
220
461
  startServer().catch((error) => {