@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.
- 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 +16 -0
- package/dist/browser-surface-ws.js +356 -0
- package/dist/control-api.d.ts +76 -4
- package/dist/control-api.js +3014 -133
- package/dist/control-web/assets/highlighted-body-OFNGDK62-DHu2g-rk.js +1 -0
- package/dist/control-web/assets/index-B6Pb5j9M.js +666 -0
- package/dist/control-web/assets/index-BU-_Qud_.css +32 -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-CSYzOmBR.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 +170 -0
- package/dist/desktop-browser-host.js +1018 -0
- package/dist/direct-runtime-browser-inspect-server.d.ts +72 -0
- package/dist/direct-runtime-browser-inspect-server.js +749 -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 +143 -0
- package/dist/direct-runtime-server.js +1959 -0
- package/dist/emulator-surface-ws.d.ts +8 -0
- package/dist/emulator-surface-ws.js +198 -0
- package/dist/machine-session-service.d.ts +265 -0
- package/dist/machine-session-service.js +822 -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 +403 -0
- package/dist/remote-runtime-session-projection.d.ts +19 -0
- package/dist/remote-runtime-session-projection.js +566 -0
- package/dist/remote-runtime.d.ts +22 -0
- package/dist/remote-runtime.js +32 -0
- package/dist/runtime-web-auth.d.ts +26 -0
- package/dist/runtime-web-auth.js +132 -0
- package/dist/server.d.ts +364 -12
- package/dist/server.js +341 -100
- package/dist/session-browser-service.d.ts +248 -0
- package/dist/session-browser-service.js +772 -0
- package/dist/session-browser-surface-coordinator.d.ts +24 -0
- package/dist/session-browser-surface-coordinator.js +669 -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 +23 -0
- package/dist/session-runtime-index.js +96 -0
- package/dist/session-terminal-host.d.ts +51 -0
- package/dist/session-terminal-host.js +270 -0
- package/dist/terminal-ws.d.ts +12 -20
- package/dist/terminal-ws.js +421 -97
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 ===
|
|
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(),
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 });
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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) => {
|