@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.
- package/dist/browser-surface-control-queue.d.ts +42 -0
- package/dist/browser-surface-control-queue.js +187 -0
- package/dist/browser-surface-ws.d.ts +14 -0
- package/dist/browser-surface-ws.js +355 -0
- package/dist/control-api.d.ts +59 -4
- package/dist/control-api.js +2213 -132
- package/dist/control-web/assets/highlighted-body-OFNGDK62-e-w61YLy.js +1 -0
- package/dist/control-web/assets/index-BQ7XVBKO.css +32 -0
- package/dist/control-web/assets/index-ClpzuBJx.js +606 -0
- package/dist/control-web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/control-web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/control-web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/control-web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/control-web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/control-web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/control-web/assets/mermaid-GHXKKRXX-DZn7Hbr2.js +131 -0
- package/dist/control-web/assets/rynx-wordmark-LL1QRYHD.png +0 -0
- package/dist/control-web/favicon.png +0 -0
- package/dist/control-web/favicon.svg +38 -0
- package/dist/control-web/index.html +16 -0
- package/dist/control-web-dist.d.ts +1 -1
- package/dist/control-web-dist.js +19 -14
- package/dist/desktop-browser-host.d.ts +153 -0
- package/dist/desktop-browser-host.js +936 -0
- package/dist/direct-runtime-browser-surface-server.d.ts +74 -0
- package/dist/direct-runtime-browser-surface-server.js +821 -0
- package/dist/direct-runtime-server.d.ts +137 -0
- package/dist/direct-runtime-server.js +1931 -0
- package/dist/emulator-surface-ws.d.ts +6 -0
- package/dist/emulator-surface-ws.js +197 -0
- package/dist/machine-session-service.d.ts +182 -0
- package/dist/machine-session-service.js +559 -0
- package/dist/provider-cli-host.d.ts +10 -0
- package/dist/provider-cli-host.js +1 -0
- package/dist/remote-runtime-dispatcher.d.ts +94 -0
- package/dist/remote-runtime-dispatcher.js +380 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +556 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +21 -0
- package/dist/runtime-web-auth.js +92 -0
- package/dist/server.d.ts +292 -10
- package/dist/server.js +314 -98
- package/dist/session-browser-service.d.ts +218 -0
- package/dist/session-browser-service.js +686 -0
- package/dist/session-browser-surface-coordinator.d.ts +23 -0
- package/dist/session-browser-surface-coordinator.js +563 -0
- package/dist/session-emulator-service.d.ts +167 -0
- package/dist/session-emulator-service.js +464 -0
- package/dist/session-runtime-index.d.ts +17 -0
- package/dist/session-runtime-index.js +86 -0
- package/dist/session-terminal-host.d.ts +46 -0
- package/dist/session-terminal-host.js +252 -0
- package/dist/terminal-ws.d.ts +9 -20
- package/dist/terminal-ws.js +420 -97
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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(),
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
154
|
-
const
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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) => {
|