@rynx-ai/server 0.1.0 → 0.1.9

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 (63) 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 +14 -0
  4. package/dist/browser-surface-ws.js +355 -0
  5. package/dist/control-api.d.ts +59 -4
  6. package/dist/control-api.js +2213 -132
  7. package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
  8. package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
  9. package/dist/control-web/assets/index-ClpzuBJx.js +606 -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-DZn7Hbr2.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 +153 -0
  30. package/dist/desktop-browser-host.js +936 -0
  31. package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
  32. package/dist/direct-runtime-browser-surface-server.js +821 -0
  33. package/dist/direct-runtime-server.d.ts +137 -0
  34. package/dist/direct-runtime-server.js +1931 -0
  35. package/dist/emulator-surface-ws.d.ts +6 -0
  36. package/dist/emulator-surface-ws.js +197 -0
  37. package/dist/machine-session-service.d.ts +182 -0
  38. package/dist/machine-session-service.js +559 -0
  39. package/dist/provider-cli-host.d.ts +10 -0
  40. package/dist/provider-cli-host.js +1 -0
  41. package/dist/remote-runtime-dispatcher.d.ts +94 -0
  42. package/dist/remote-runtime-dispatcher.js +380 -0
  43. package/dist/remote-runtime-session-projection.d.ts +19 -0
  44. package/dist/remote-runtime-session-projection.js +556 -0
  45. package/dist/remote-runtime.d.ts +22 -0
  46. package/dist/remote-runtime.js +32 -0
  47. package/dist/runtime-web-auth.d.ts +21 -0
  48. package/dist/runtime-web-auth.js +92 -0
  49. package/dist/server.d.ts +292 -10
  50. package/dist/server.js +314 -98
  51. package/dist/session-browser-service.d.ts +218 -0
  52. package/dist/session-browser-service.js +686 -0
  53. package/dist/session-browser-surface-coordinator.d.ts +23 -0
  54. package/dist/session-browser-surface-coordinator.js +563 -0
  55. package/dist/session-emulator-service.d.ts +167 -0
  56. package/dist/session-emulator-service.js +464 -0
  57. package/dist/session-runtime-index.d.ts +17 -0
  58. package/dist/session-runtime-index.js +86 -0
  59. package/dist/session-terminal-host.d.ts +46 -0
  60. package/dist/session-terminal-host.js +252 -0
  61. package/dist/terminal-ws.d.ts +9 -20
  62. package/dist/terminal-ws.js +420 -97
  63. package/package.json +9 -7
package/dist/server.js CHANGED
@@ -1,19 +1,85 @@
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";
11
16
  export { ChannelManager } from "./channel-manager.js";
12
- function toErrorMessage(error) {
13
- if (error instanceof Error) {
14
- return error.message;
17
+ export { MachineSessionService, MachineSessionServiceFailure, MachineSessionServiceInputError, synthesizeSessionTitle, } from "./machine-session-service.js";
18
+ export { SessionBrowserHostError, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceUnsupportedError, } from "./session-browser-service.js";
19
+ export { SessionEmulatorBindingConflictError, SessionEmulatorService, SessionEmulatorServiceError, } from "./session-emulator-service.js";
20
+ export { SessionBrowserSurfaceCoordinator } from "./session-browser-surface-coordinator.js";
21
+ export { attachDesktopBrowserHostServer, createDesktopFirstSessionBrowserHost, DesktopBrowserHostAbsentError, DesktopBrowserHostProtocolError, DesktopBrowserHostRegistry, } from "./desktop-browser-host.js";
22
+ export { createDaemonRuntimeHost, encodeDaemonStatus } from "./remote-runtime.js";
23
+ export { DIRECT_RUNTIME_CONTROL_PATH, DirectRuntimeConnectionHub, startDirectRuntimeServer, } from "./direct-runtime-server.js";
24
+ export { attachDirectRuntimeBrowserSurfaceServer, DirectRuntimeBrowserSurfaceHostError, } from "./direct-runtime-browser-surface-server.js";
25
+ export { RemoteRuntimeDispatcher } from "./remote-runtime-dispatcher.js";
26
+ export { createRunnerSessionTerminalHost, SessionTerminalOpenError, SessionTerminalStreamError, } from "./session-terminal-host.js";
27
+ /** Preserve one session's mirror order without serializing unrelated sessions. */
28
+ export class SessionMirrorQueue {
29
+ persist;
30
+ tails = new Map();
31
+ constructor(persist) {
32
+ this.persist = persist;
33
+ }
34
+ enqueue(sessionId, event) {
35
+ const previous = this.tails.get(sessionId) ?? Promise.resolve();
36
+ const next = previous
37
+ .catch(() => undefined)
38
+ .then(() => this.persist(sessionId, event));
39
+ this.tails.set(sessionId, next);
40
+ const cleanup = () => {
41
+ if (this.tails.get(sessionId) === next)
42
+ this.tails.delete(sessionId);
43
+ };
44
+ void next.then(cleanup, cleanup);
45
+ return next;
15
46
  }
16
- return "Unknown error";
47
+ }
48
+ /**
49
+ * Compose the process-owned Session runtime once. The daemon may call this
50
+ * before creating its Direct dispatcher; {@link startServer} accepts the same
51
+ * bundle and therefore does not create a second bus, runtime index, or runner.
52
+ */
53
+ export function createSessionRuntimeServices(input) {
54
+ const sessionStore = new FileCodexSessionStore(resolveCodexSessionStorePath(input.config));
55
+ const runnerManager = new RunnerManager({
56
+ config: input.config,
57
+ sessionStore,
58
+ ...(input.sessionContextProvider
59
+ ? { sessionContextProvider: input.sessionContextProvider }
60
+ : {}),
61
+ });
62
+ const sessionBus = new InMemorySessionBus();
63
+ const sessionRuntimeIndex = new SessionRuntimeIndex();
64
+ const conversationRuntime = new ConversationRuntime({
65
+ config: input.config,
66
+ executor: runnerManager,
67
+ sessionBus,
68
+ });
69
+ const sessionMirrorQueue = new SessionMirrorQueue((sessionId, event) => persistSessionEvent(sessionId, event, { sessionLog: input.sessionLog, sessionBus }));
70
+ runnerManager.onMirror((sessionId, event) => {
71
+ sessionRuntimeIndex.observe(sessionId, event);
72
+ void sessionMirrorQueue.enqueue(sessionId, event).catch((error) => {
73
+ input.onMirrorError?.(error, sessionId);
74
+ });
75
+ });
76
+ return {
77
+ sessionStore,
78
+ runnerManager,
79
+ sessionBus,
80
+ sessionRuntimeIndex,
81
+ conversationRuntime,
82
+ };
17
83
  }
18
84
  /**
19
85
  * The HTTP surface is just a liveness probe for container orchestration — all
@@ -33,13 +99,17 @@ export function createApp({ control } = {}) {
33
99
  const controlRouter = createControlRouter(control);
34
100
  app.use(controlRouter.routes());
35
101
  app.use(controlRouter.allowedMethods());
36
- // Serve the built control-web SPA for everything that isn't /health or /api.
102
+ }
103
+ if (control) {
104
+ // Serve the built control-web SPA outside the server-owned health, API, and
105
+ // one-shot control-link namespaces.
37
106
  const distDir = resolveControlWebDist();
38
107
  app.use(async (ctx, next) => {
39
108
  if (ctx.method !== "GET" && ctx.method !== "HEAD")
40
109
  return next();
41
- if (ctx.path === "/health" || ctx.path.startsWith("/api/"))
110
+ if (["/health", "/api", "/control"].some((prefix) => ctx.path === prefix || ctx.path.startsWith(`${prefix}/`))) {
42
111
  return next();
112
+ }
43
113
  if (!distDir) {
44
114
  if (ctx.path === "/") {
45
115
  ctx.type = "html";
@@ -55,46 +125,64 @@ export function createApp({ control } = {}) {
55
125
  }
56
126
  return app;
57
127
  }
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({
128
+ export async function startServer({ config = loadConfig(), channelInstances, loadInstances, control, sessionLog, sessionRegistry, pluginRuntime, daemonManagement, remoteRuntime, remoteRuntimeAdmin, runtimeTargetControl, runtimeConnectionResolver, sessionEmulators, sessionRuntimeServices, sessionLifecycle, runtimeLocalBrowser, providerClis, desktopBrowserHost, } = {}) {
129
+ const desktopBrowserManagementToken = desktopBrowserHost
130
+ ? requireDaemonManagementToken(daemonManagement)
131
+ : undefined;
132
+ const sessionServices = sessionRuntimeServices ?? createSessionRuntimeServices({
67
133
  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}`,
134
+ sessionLog,
135
+ onMirrorError: (error, sessionId) => {
136
+ console.error(`[session-mirror] session=${sessionId} failed: ${error instanceof Error ? error.message : String(error)}`);
73
137
  },
74
138
  });
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 });
139
+ const { sessionStore, runnerManager, sessionBus, sessionRuntimeIndex, conversationRuntime, } = sessionServices;
140
+ const legacyLocalTerminalHost = createRunnerSessionTerminalHost({
141
+ runnerManager,
142
+ resolveLiveSession: async (sessionId) => {
143
+ const session = await sessionStore.get(sessionId);
144
+ if (!session || !runnerManager.hasLiveSession(sessionId))
145
+ return undefined;
146
+ return { cwd: session.cwd };
147
+ },
89
148
  });
149
+ if (pluginRuntime) {
150
+ if (!sessionRegistry)
151
+ throw new Error("plugin runtime requires a machine session registry");
152
+ pluginRuntime.bindHostServices({
153
+ conversationRuntime,
154
+ capabilities: runnerManager,
155
+ sessionStore,
156
+ sessionRegistry,
157
+ agentCatalog: {
158
+ list: async () => (await listAgentSpecs()).map(toPluginAgentSummary),
159
+ get: async (id) => {
160
+ const spec = await loadAgentSpec(id);
161
+ return spec ? toPluginAgentSummary(spec) : null;
162
+ },
163
+ resolveExecution: (input) => resolveSessionExecution({
164
+ config,
165
+ ...(input.defaultAgent ? { defaultAgent: input.defaultAgent } : {}),
166
+ session: input.session,
167
+ }),
168
+ },
169
+ interruptSession: (sessionId) => runnerManager.interruptLiveSession(sessionId),
170
+ resolveSessionInteraction: (sessionId, interactionId, resolution) => runnerManager.resolveInteraction(sessionId, interactionId, resolution),
171
+ });
172
+ }
90
173
  // 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.
174
+ // the console list shows it with the carried-over launch target/model + a source tag.
92
175
  // (It also surfaces via the log-merge once its first turn mirrors.)
93
176
  runnerManager.onRotate((r) => {
177
+ const source = control?.getSessionMeta(r.from);
94
178
  control?.createSessionMeta({
95
179
  id: r.to,
96
180
  source: r.kind === "fork" ? "fork" : "clear",
97
- ...(r.agent ? { agent: r.agent } : {}),
181
+ ...(r.agent
182
+ ? { agent: r.agent }
183
+ : source?.provider
184
+ ? { provider: source.provider }
185
+ : {}),
98
186
  ...(r.model ? { model: r.model } : {}),
99
187
  createdAt: new Date().toISOString(),
100
188
  });
@@ -103,6 +191,7 @@ export async function startServer({ config = loadConfig(), channelFactories = []
103
191
  // `loadInstances`; the control plane drives live start/stop/reload).
104
192
  const resolveInstances = loadInstances ?? (channelInstances ? async () => channelInstances : null);
105
193
  if (resolveInstances) {
194
+ const controlShutdown = new AbortController();
106
195
  const manager = new ChannelManager({
107
196
  config,
108
197
  conversationRuntime,
@@ -114,7 +203,6 @@ export async function startServer({ config = loadConfig(), channelFactories = []
114
203
  loadInstances: resolveInstances,
115
204
  });
116
205
  const app = createApp({
117
- config,
118
206
  control: control
119
207
  ? {
120
208
  manager,
@@ -124,6 +212,26 @@ export async function startServer({ config = loadConfig(), channelFactories = []
124
212
  sessionLog,
125
213
  runnerManager,
126
214
  sessionStore,
215
+ pluginRuntimeStatus: pluginRuntime
216
+ ? () => pluginRuntime.statusSnapshot()
217
+ : undefined,
218
+ pluginRuntimeReload: pluginRuntime
219
+ ? (pluginId) => pluginRuntime.reloadPluginRuntime(pluginId)
220
+ : undefined,
221
+ pluginRuntimeEnsure: pluginRuntime
222
+ ? (pluginId) => pluginRuntime.ensurePluginRuntime(pluginId)
223
+ : undefined,
224
+ daemonManagement,
225
+ remoteRuntime,
226
+ remoteRuntimeAdmin,
227
+ runtimeTargetControl,
228
+ runtimeConnectionResolver,
229
+ sessionEmulators,
230
+ shutdownSignal: controlShutdown.signal,
231
+ sessionRuntimeIndex,
232
+ sessionLifecycle,
233
+ runtimeLocalBrowser,
234
+ providerClis,
127
235
  }
128
236
  : undefined,
129
237
  });
@@ -137,44 +245,55 @@ export async function startServer({ config = loadConfig(), channelFactories = []
137
245
  runtime: config.AGENT_RUNTIME,
138
246
  model: resolveRuntimeModel(config, config.AGENT_RUNTIME),
139
247
  }));
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();
248
+ const startup = (async () => {
249
+ await pluginRuntime?.startPluginRuntimes();
250
+ await manager.startAll();
251
+ })();
252
+ void startup.catch((error) => {
253
+ console.error(JSON.stringify({
254
+ level: "error",
255
+ type: "plugin-runtime",
256
+ event: "startup_failed",
257
+ error: error instanceof Error ? error.message : String(error),
258
+ }));
148
259
  });
149
- resolve(server);
260
+ // Live WS bridges share this http.Server (no second port).
261
+ const webSocketServers = [
262
+ attachTerminalWs(server, {
263
+ runtimeConnectionResolver,
264
+ legacyLocalTerminalHost,
265
+ }),
266
+ attachBrowserSurfaceWs(server, { runtimeConnectionResolver }),
267
+ attachEmulatorSurfaceWs(server, { runtimeConnectionResolver }),
268
+ ];
269
+ if (desktopBrowserHost && desktopBrowserManagementToken) {
270
+ webSocketServers.push(attachDesktopBrowserHostServer(server, {
271
+ ...desktopBrowserHost,
272
+ managementToken: desktopBrowserManagementToken,
273
+ }));
274
+ }
275
+ if (control?.emulator) {
276
+ webSocketServers.push(attachEmulatorTouchWs(server, { emulator: control.emulator }));
277
+ }
278
+ resolve(bindServerLifecycle(server, {
279
+ closeUpgrades: async () => {
280
+ controlShutdown.abort(new Error("Rynx server is shutting down"));
281
+ await closeWebSocketServers(webSocketServers);
282
+ },
283
+ cleanup: async () => {
284
+ await runCleanupSteps([
285
+ () => startup.catch(() => undefined),
286
+ () => manager.stopAll(),
287
+ () => pluginRuntime?.dispose() ?? Promise.resolve(),
288
+ () => runnerManager.stop(),
289
+ ]);
290
+ },
291
+ }));
150
292
  });
151
293
  });
152
294
  }
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 });
295
+ // Core-only mode: no chat instances and no plugin code are required.
296
+ const app = createApp();
178
297
  return new Promise((resolve) => {
179
298
  const server = app.listen(config.PORT, config.HOST, () => {
180
299
  console.log(JSON.stringify({
@@ -184,37 +303,134 @@ export async function startServer({ config = loadConfig(), channelFactories = []
184
303
  port: config.PORT,
185
304
  runtime: config.AGENT_RUNTIME,
186
305
  model: resolveRuntimeModel(config, config.AGENT_RUNTIME),
187
- channels: channels.map((channel) => channel.id),
188
306
  }));
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
- });
307
+ const webSocketServers = [];
308
+ if (desktopBrowserHost && desktopBrowserManagementToken) {
309
+ webSocketServers.push(attachDesktopBrowserHostServer(server, {
310
+ ...desktopBrowserHost,
311
+ managementToken: desktopBrowserManagementToken,
312
+ }));
199
313
  }
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
- }
314
+ resolve(bindServerLifecycle(server, {
315
+ ...(webSocketServers.length > 0
316
+ ? { closeUpgrades: () => closeWebSocketServers(webSocketServers) }
317
+ : {}),
318
+ cleanup: () => runnerManager.stop(),
319
+ }));
320
+ });
321
+ });
322
+ }
323
+ function requireDaemonManagementToken(host) {
324
+ const token = host?.managementToken;
325
+ const bytes = typeof token === "string" ? Buffer.byteLength(token, "utf8") : 0;
326
+ if (bytes < 32 || bytes > 256) {
327
+ throw new Error("Desktop Browser Host requires configured daemon management auth");
328
+ }
329
+ return token;
330
+ }
331
+ function bindServerLifecycle(server, lifecycle) {
332
+ let closed = false;
333
+ let shutdownPromise;
334
+ const nativeClose = server.close.bind(server);
335
+ server.once("close", () => {
336
+ closed = true;
337
+ });
338
+ const managed = server;
339
+ managed.shutdown = () => {
340
+ if (shutdownPromise)
341
+ return shutdownPromise;
342
+ shutdownPromise = (async () => {
343
+ // Stop accepting first, close upgraded sockets so they cannot hold the
344
+ // HTTP drain open, then keep runtime dependencies alive until every
345
+ // in-flight HTTP request has completed.
346
+ const closeResult = closeHttpServer(server, nativeClose, () => closed);
347
+ const upgradeResult = lifecycle.closeUpgrades?.() ?? Promise.resolve();
348
+ const drainResults = await Promise.allSettled([upgradeResult, closeResult]);
349
+ const errors = drainResults
350
+ .filter((result) => result.status === "rejected")
351
+ .map((result) => result.reason);
352
+ try {
353
+ await lifecycle.cleanup();
354
+ }
355
+ catch (error) {
356
+ errors.push(error);
357
+ }
358
+ if (errors.length > 0)
359
+ throw new AggregateError(errors, "Rynx server shutdown failed");
360
+ })();
361
+ return shutdownPromise;
362
+ };
363
+ managed.close = ((callback) => {
364
+ const completion = managed.shutdown();
365
+ if (callback) {
366
+ void completion.then(() => callback(), (error) => callback(asError(error)));
367
+ }
368
+ else {
369
+ void completion.catch((error) => {
370
+ console.error(JSON.stringify({
371
+ level: "error",
372
+ type: "shutdown",
373
+ event: "cleanup_failed",
374
+ error: error instanceof Error ? error.message : String(error),
375
+ }));
213
376
  });
214
- resolve(server);
377
+ }
378
+ return managed;
379
+ });
380
+ return managed;
381
+ }
382
+ async function closeHttpServer(server, nativeClose, isClosed) {
383
+ if (isClosed())
384
+ return;
385
+ if (!server.listening) {
386
+ await new Promise((resolve) => server.once("close", resolve));
387
+ return;
388
+ }
389
+ await new Promise((resolve, reject) => {
390
+ nativeClose((error) => {
391
+ if (error)
392
+ reject(error);
393
+ else
394
+ resolve();
215
395
  });
216
396
  });
217
397
  }
398
+ function asError(error) {
399
+ return error instanceof Error ? error : new Error(String(error));
400
+ }
401
+ async function runCleanupSteps(steps) {
402
+ const errors = [];
403
+ for (const step of steps) {
404
+ try {
405
+ await step();
406
+ }
407
+ catch (error) {
408
+ errors.push(error);
409
+ }
410
+ }
411
+ if (errors.length > 0)
412
+ throw new AggregateError(errors, "Rynx resource cleanup failed");
413
+ }
414
+ async function closeWebSocketServers(servers) {
415
+ await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
416
+ for (const client of server.clients)
417
+ client.terminate();
418
+ server.close((error) => {
419
+ if (error)
420
+ reject(error);
421
+ else
422
+ resolve();
423
+ });
424
+ })));
425
+ }
426
+ function toPluginAgentSummary(spec) {
427
+ return {
428
+ id: spec.id,
429
+ ...(spec.name ? { name: spec.name } : {}),
430
+ ...(spec.executor?.runtime ? { runtime: spec.executor.runtime } : {}),
431
+ ...(spec.executor?.model ? { model: spec.executor.model } : {}),
432
+ };
433
+ }
218
434
  const entrypoint = process.argv[1];
219
435
  if (entrypoint && import.meta.url === pathToFileURL(entrypoint).href) {
220
436
  startServer().catch((error) => {