@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.3
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/bundled-plugins/plugins/lark/dist/runtime.js +2 -5
- package/bundled-plugins/plugins/lark/dist/settings.js +2 -2
- package/bundled-plugins/plugins/lark/package.json +1 -1
- package/dist/browser-artifact-management.d.ts +4 -1
- package/dist/browser-artifact-management.js +18 -10
- package/dist/browser-artifacts.d.ts +1 -0
- package/dist/browser-artifacts.js +1 -0
- package/dist/chrome-for-testing-runtime.d.ts +27 -0
- package/dist/chrome-for-testing-runtime.js +193 -0
- package/dist/chrome-for-testing-store.d.ts +2 -0
- package/dist/chrome-for-testing-store.js +9 -0
- package/dist/daemon-server.d.ts +4 -1
- package/dist/daemon-server.js +137 -32
- package/dist/db.js +2 -0
- package/dist/entry-path.d.ts +3 -0
- package/dist/entry-path.js +12 -0
- package/dist/index-daemon.js +3 -87
- package/dist/lifecycle.d.ts +12 -0
- package/dist/lifecycle.js +16 -0
- package/dist/maintenance-lease.d.ts +39 -0
- package/dist/maintenance-lease.js +250 -0
- package/dist/plugin-host-rpc.d.ts +19 -1
- package/dist/plugin-host-rpc.js +163 -23
- package/dist/plugin-installer.js +59 -0
- package/dist/plugin-supervisor.d.ts +5 -0
- package/dist/plugin-supervisor.js +218 -29
- package/dist/pm2.d.ts +11 -2
- package/dist/pm2.js +116 -19
- package/dist/process.d.ts +12 -0
- package/dist/process.js +91 -0
- package/dist/session-launch-operations.d.ts +10 -1
- package/dist/session-launch-operations.js +15 -6
- package/dist/setup-service.d.ts +20 -0
- package/dist/setup-service.js +38 -0
- package/package.json +33 -20
- package/dist/browser-cli-args.d.ts +0 -20
- package/dist/browser-cli-args.js +0 -100
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -550
- package/dist/control-client.d.ts +0 -122
- package/dist/control-client.js +0 -855
- package/dist/control-endpoint.d.ts +0 -14
- package/dist/control-endpoint.js +0 -124
- package/dist/desktop-browser-host-client.d.ts +0 -35
- package/dist/desktop-browser-host-client.js +0 -415
- package/dist/setup.d.ts +0 -20
- package/dist/setup.js +0 -386
- package/dist/update.d.ts +0 -25
- package/dist/update.js +0 -133
package/dist/daemon-server.js
CHANGED
|
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
13
|
import WebSocket from "ws";
|
|
14
|
-
import { loadConfig, resolveExecutionSnapshot, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
14
|
+
import { CloseableAdmissionGate, loadConfig, resolveExecutionSnapshot, rynxHome, sessionWorkspaceSnapshotSchema, } from "@rynx-ai/core";
|
|
15
15
|
import { LOCAL_MANAGEMENT_PROTOCOL, } from "@rynx-ai/protocol/control";
|
|
16
16
|
import { REMOTE_RUNTIME_CORE_PROTOCOL } from "@rynx-ai/protocol/remote-runtime";
|
|
17
17
|
import { RUNTIME_BROWSER_INSPECT_MAX_MESSAGE_BYTES, RUNTIME_BROWSER_INSPECT_SEMANTIC_CAPABILITY, } from "@rynx-ai/protocol/runtime-browser-inspect";
|
|
@@ -22,8 +22,10 @@ import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrow
|
|
|
22
22
|
import { buildControlDeps } from "./control-deps.js";
|
|
23
23
|
import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
|
|
24
24
|
import { createDaemonBuildStatus } from "./daemon-build-status.js";
|
|
25
|
+
import { MaintenanceLeaseStore } from "./maintenance-lease.js";
|
|
25
26
|
import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
|
|
26
27
|
import { removeLegacyPluginSkillSnapshot, snapshotEnabledPluginSkillSources, } from "./plugin-skill-context.js";
|
|
28
|
+
import { installedDaemonVersion } from "./entry-path.js";
|
|
27
29
|
import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
|
|
28
30
|
import { ChromeInspectionManager, } from "./chrome-inspection-manager.js";
|
|
29
31
|
import { createHeadlessBrowserHost } from "./headless-browser-host.js";
|
|
@@ -76,7 +78,10 @@ async function resolveSessionExecutionSnapshot(input) {
|
|
|
76
78
|
pluginSkills: snapshotEnabledPluginSkillSources(),
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
|
-
async function resolveSessionWorkspace(projects, projectId, managedCwd) {
|
|
81
|
+
async function resolveSessionWorkspace(projects, projectId, managedCwd, workspace) {
|
|
82
|
+
if (workspace) {
|
|
83
|
+
return sessionWorkspaceSnapshotSchema.parse(workspace);
|
|
84
|
+
}
|
|
80
85
|
if (projectId) {
|
|
81
86
|
const project = await projects.get(projectId);
|
|
82
87
|
if (!project) {
|
|
@@ -131,7 +136,8 @@ function toMarketplaceItem(record) {
|
|
|
131
136
|
};
|
|
132
137
|
}
|
|
133
138
|
export function daemonActivityBlocksShutdown(activity) {
|
|
134
|
-
return activity.runningTurns > 0
|
|
139
|
+
return activity.runningTurns > 0
|
|
140
|
+
|| activity.pendingInteractions > 0;
|
|
135
141
|
}
|
|
136
142
|
function defaultWarn(message) {
|
|
137
143
|
console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
|
|
@@ -250,12 +256,24 @@ async function removeControlEndpointForCurrentProcess() {
|
|
|
250
256
|
// Best effort only; stale endpoint files are ignored by the client fallback.
|
|
251
257
|
}
|
|
252
258
|
}
|
|
253
|
-
export async function startRynxDaemonServer({ config, warn = defaultWarn, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, preparePlugins, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, createChromeInspectionManager = (options) => ChromeInspectionManager.create(options), createBrowserHostSupervisor = (options) => new AppBrowserHostSupervisor(options), } = {}) {
|
|
259
|
+
export async function startRynxDaemonServer({ config, warn = defaultWarn, onShutdownComplete, writeEndpoint = writeControlEndpoint, acquireOwner = acquireDaemonOwner, preparePlugins, loadDirectConfig = loadDirectRuntimeConfig, startDirectServer = startDirectRuntimeServer, createChromeInspectionManager = (options) => ChromeInspectionManager.create(options), createBrowserHostSupervisor = (options) => new AppBrowserHostSupervisor(options), } = {}) {
|
|
254
260
|
const owner = await acquireOwner();
|
|
255
261
|
let ownerManagedByServer = false;
|
|
256
262
|
try {
|
|
257
263
|
await preparePlugins?.();
|
|
258
264
|
const appConfig = config ?? loadConfig();
|
|
265
|
+
const admissionGate = new CloseableAdmissionGate();
|
|
266
|
+
let shutdownAccepted = false;
|
|
267
|
+
const maintenanceLease = new MaintenanceLeaseStore({
|
|
268
|
+
// Expiry restores the gate primitive for a process that outlives its
|
|
269
|
+
// upgrader. The independent accepted-shutdown fence below still prevents
|
|
270
|
+
// new work after an irreversible server shutdown has begun.
|
|
271
|
+
onExpired: () => admissionGate.reopen(),
|
|
272
|
+
});
|
|
273
|
+
const admissionOpen = () => !shutdownAccepted && !maintenanceLease.isActive();
|
|
274
|
+
const reserveAdmission = () => !admissionOpen()
|
|
275
|
+
? undefined
|
|
276
|
+
: admissionGate.tryReserve();
|
|
259
277
|
const appBrowserHostLaunch = parseAppBrowserHostLaunchConfig();
|
|
260
278
|
const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
|
|
261
279
|
const runtimeBrowserSupported = appBrowserHostLaunch !== undefined || isChromeForTestingHostSupported();
|
|
@@ -335,6 +353,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
335
353
|
}
|
|
336
354
|
const pluginSupervisor = new PluginSupervisor(listPlugins(), {
|
|
337
355
|
daemonInstanceId,
|
|
356
|
+
productVersion: controlEndpointDistributionMetadata().productVersion,
|
|
357
|
+
cliVersion: boundedEnvironmentMetadata(process.env.RYNX_CLI_VERSION, 128)
|
|
358
|
+
?? installedDaemonVersion(),
|
|
338
359
|
log: (entry) => {
|
|
339
360
|
const output = entry.level === "error"
|
|
340
361
|
? console.error
|
|
@@ -379,7 +400,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
379
400
|
},
|
|
380
401
|
sessionLaunch: {
|
|
381
402
|
resolve: async (selection) => {
|
|
382
|
-
const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd);
|
|
403
|
+
const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd, selection.workspace);
|
|
383
404
|
return {
|
|
384
405
|
workspace,
|
|
385
406
|
execution: await resolveSessionExecutionSnapshot({
|
|
@@ -387,7 +408,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
387
408
|
...(selection.agent ? { agentName: selection.agent } : {}),
|
|
388
409
|
...(selection.config ? { spec: selection.config } : {}),
|
|
389
410
|
...((selection.provider || (!selection.agent && !selection.config))
|
|
390
|
-
? { provider: selection.provider ?? appConfig.
|
|
411
|
+
? { provider: selection.provider ?? appConfig.DEFAULT_RUNTIME }
|
|
391
412
|
: {}),
|
|
392
413
|
...(selection.model ? { model: selection.model } : {}),
|
|
393
414
|
...(selection.reasoningEffort
|
|
@@ -399,6 +420,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
399
420
|
},
|
|
400
421
|
},
|
|
401
422
|
sessionFork: (input) => machineSessions.fork(input),
|
|
423
|
+
admissionOpen,
|
|
424
|
+
admissionReserve: reserveAdmission,
|
|
402
425
|
}),
|
|
403
426
|
startPluginRuntimes: pluginSupervisor.startPluginRuntimes.bind(pluginSupervisor),
|
|
404
427
|
statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
|
|
@@ -421,6 +444,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
421
444
|
config: appConfig,
|
|
422
445
|
sessionLog,
|
|
423
446
|
sessionContextProvider: browserRunnerSessionContexts,
|
|
447
|
+
admissionOpen,
|
|
448
|
+
admissionReserve: reserveAdmission,
|
|
424
449
|
onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
|
|
425
450
|
});
|
|
426
451
|
const sessionBrowserHost = appBrowserHostSupervisor
|
|
@@ -432,6 +457,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
432
457
|
exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
|
|
433
458
|
await sessionRuntimeServices.sessionStore.get(sessionId)),
|
|
434
459
|
},
|
|
460
|
+
}, {
|
|
461
|
+
admissionReserve: reserveAdmission,
|
|
435
462
|
});
|
|
436
463
|
const sessionEmulators = control.emulator
|
|
437
464
|
? new SessionEmulatorService({
|
|
@@ -543,10 +570,14 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
543
570
|
resolve: (projectId) => resolveSessionWorkspace(runtimeProjects, projectId),
|
|
544
571
|
discardUnbound: discardSessionFallbackWorkspace,
|
|
545
572
|
},
|
|
573
|
+
}, {
|
|
574
|
+
admissionOpen,
|
|
575
|
+
admissionReserve: reserveAdmission,
|
|
546
576
|
});
|
|
547
577
|
const directAuthenticator = new DaemonDirectRuntimeAuthenticator(installationIdentity, remoteRuntime, remoteRuntimeAccess);
|
|
548
578
|
const sessionTerminals = createRunnerSessionTerminalHost({
|
|
549
579
|
runnerManager: sessionRuntimeServices.runnerManager,
|
|
580
|
+
admissionReserve: reserveAdmission,
|
|
550
581
|
resolveLiveSession: async (sessionId) => {
|
|
551
582
|
const registered = sessionRegistry.get(sessionId);
|
|
552
583
|
if (!registered ||
|
|
@@ -556,8 +587,95 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
556
587
|
return { cwd: registered.workspace.cwd };
|
|
557
588
|
},
|
|
558
589
|
});
|
|
559
|
-
let shutdownAccepted = false;
|
|
560
590
|
let requestServerShutdown;
|
|
591
|
+
let shutdownAttempt;
|
|
592
|
+
let shutdownAttemptLeaseToken;
|
|
593
|
+
const shutdownIfIdle = (maintenanceLeaseToken) => {
|
|
594
|
+
if (maintenanceLease.isActive()) {
|
|
595
|
+
if (!maintenanceLeaseToken || !maintenanceLease.isHeldBy(maintenanceLeaseToken)) {
|
|
596
|
+
return Promise.reject(new Error("a different maintenance lease is already active"));
|
|
597
|
+
}
|
|
598
|
+
// The previous daemon may have persisted the lease and exited after
|
|
599
|
+
// its 202 response was lost. A replacement process holding the same
|
|
600
|
+
// token must perform its own close/drain/shutdown; returning accepted
|
|
601
|
+
// without scheduling this PID would strand the updater waiting for a
|
|
602
|
+
// process that can never exit.
|
|
603
|
+
}
|
|
604
|
+
if (shutdownAccepted) {
|
|
605
|
+
if (maintenanceLeaseToken) {
|
|
606
|
+
maintenanceLease.acquire(maintenanceLeaseToken);
|
|
607
|
+
return Promise.resolve({
|
|
608
|
+
outcome: "accepted",
|
|
609
|
+
maintenanceLease: "active",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return Promise.resolve({ outcome: "accepted" });
|
|
613
|
+
}
|
|
614
|
+
if (shutdownAttempt) {
|
|
615
|
+
if (maintenanceLeaseToken === shutdownAttemptLeaseToken) {
|
|
616
|
+
return shutdownAttempt;
|
|
617
|
+
}
|
|
618
|
+
return Promise.reject(new Error("another idle shutdown check is already in progress"));
|
|
619
|
+
}
|
|
620
|
+
shutdownAttemptLeaseToken = maintenanceLeaseToken;
|
|
621
|
+
const attempt = (async () => {
|
|
622
|
+
await admissionGate.closeAndDrain();
|
|
623
|
+
const runtimeActivity = sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot();
|
|
624
|
+
const activity = {
|
|
625
|
+
...runtimeActivity,
|
|
626
|
+
// A terminal submission keeps its own admission reservation until the
|
|
627
|
+
// first response or native rotation is visible. Failed process-tree
|
|
628
|
+
// cleanup releases the reservation but remains pinned here, so an
|
|
629
|
+
// idle check returns busy instead of hanging or killing hidden work.
|
|
630
|
+
runningTurns: runtimeActivity.runningTurns
|
|
631
|
+
+ sessionRuntimeServices.runnerManager.pendingTerminalInputCount(),
|
|
632
|
+
activeTerminals: sessionTerminals.activeTerminalCount(),
|
|
633
|
+
activeBrowsers: sessionBrowsers.activeBrowserCount(),
|
|
634
|
+
};
|
|
635
|
+
if (daemonActivityBlocksShutdown(activity)) {
|
|
636
|
+
admissionGate.reopen();
|
|
637
|
+
return { outcome: "busy", activity };
|
|
638
|
+
}
|
|
639
|
+
const shutdown = requestServerShutdown;
|
|
640
|
+
if (!shutdown) {
|
|
641
|
+
admissionGate.reopen();
|
|
642
|
+
throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
|
|
643
|
+
}
|
|
644
|
+
const acquiredLease = maintenanceLeaseToken
|
|
645
|
+
? maintenanceLease.acquire(maintenanceLeaseToken)
|
|
646
|
+
: false;
|
|
647
|
+
shutdownAccepted = true;
|
|
648
|
+
try {
|
|
649
|
+
void shutdown().catch((error) => {
|
|
650
|
+
shutdownAccepted = false;
|
|
651
|
+
if (acquiredLease && maintenanceLeaseToken) {
|
|
652
|
+
maintenanceLease.release(maintenanceLeaseToken);
|
|
653
|
+
}
|
|
654
|
+
admissionGate.reopen();
|
|
655
|
+
warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
shutdownAccepted = false;
|
|
660
|
+
if (acquiredLease && maintenanceLeaseToken) {
|
|
661
|
+
maintenanceLease.release(maintenanceLeaseToken);
|
|
662
|
+
}
|
|
663
|
+
admissionGate.reopen();
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
return maintenanceLeaseToken
|
|
667
|
+
? { outcome: "accepted", maintenanceLease: "active" }
|
|
668
|
+
: { outcome: "accepted" };
|
|
669
|
+
})();
|
|
670
|
+
shutdownAttempt = attempt;
|
|
671
|
+
void attempt.finally(() => {
|
|
672
|
+
if (shutdownAttempt === attempt) {
|
|
673
|
+
shutdownAttempt = undefined;
|
|
674
|
+
shutdownAttemptLeaseToken = undefined;
|
|
675
|
+
}
|
|
676
|
+
}).catch(() => undefined);
|
|
677
|
+
return attempt;
|
|
678
|
+
};
|
|
561
679
|
const daemonManagement = {
|
|
562
680
|
managementToken,
|
|
563
681
|
identity: () => ({
|
|
@@ -566,6 +684,14 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
566
684
|
publicKey: installationIdentity.publicKey,
|
|
567
685
|
}),
|
|
568
686
|
buildStatus: daemonBuildStatus,
|
|
687
|
+
releaseMaintenanceLease: (token) => ({
|
|
688
|
+
outcome: (() => {
|
|
689
|
+
const outcome = maintenanceLease.release(token);
|
|
690
|
+
if (outcome === "released")
|
|
691
|
+
admissionGate.reopen();
|
|
692
|
+
return outcome;
|
|
693
|
+
})(),
|
|
694
|
+
}),
|
|
569
695
|
pluginManagement: {
|
|
570
696
|
listPlugins: () => pluginManagement.list(),
|
|
571
697
|
setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
|
|
@@ -591,30 +717,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
591
717
|
},
|
|
592
718
|
browserArtifacts: browserArtifactManagement,
|
|
593
719
|
maintenance: maintenanceManagement,
|
|
594
|
-
shutdownIfIdle
|
|
595
|
-
if (shutdownAccepted)
|
|
596
|
-
return { outcome: "accepted" };
|
|
597
|
-
const activity = {
|
|
598
|
-
...sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot(),
|
|
599
|
-
activeTerminals: sessionTerminals.activeTerminalCount(),
|
|
600
|
-
activeBrowsers: sessionBrowsers.activeBrowserCount(),
|
|
601
|
-
};
|
|
602
|
-
if (daemonActivityBlocksShutdown(activity)) {
|
|
603
|
-
return { outcome: "busy", activity };
|
|
604
|
-
}
|
|
605
|
-
shutdownAccepted = true;
|
|
606
|
-
const shutdown = requestServerShutdown;
|
|
607
|
-
if (!shutdown) {
|
|
608
|
-
shutdownAccepted = false;
|
|
609
|
-
throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
|
|
610
|
-
}
|
|
611
|
-
// Start admission fences in this event-loop turn, before another
|
|
612
|
-
// request or Direct dispatch can be accepted.
|
|
613
|
-
void shutdown().catch((error) => {
|
|
614
|
-
warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
615
|
-
});
|
|
616
|
-
return { outcome: "accepted" };
|
|
617
|
-
},
|
|
720
|
+
shutdownIfIdle,
|
|
618
721
|
invokePluginCli,
|
|
619
722
|
};
|
|
620
723
|
const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
|
|
@@ -853,8 +956,10 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
|
|
|
853
956
|
// A failed resource shutdown may leave plugin transports alive. Keep
|
|
854
957
|
// the process-scoped owner lease in that fail-closed state; it will be
|
|
855
958
|
// released by the OS when the process exits.
|
|
856
|
-
if (shutdownComplete)
|
|
959
|
+
if (shutdownComplete) {
|
|
857
960
|
await owner.release();
|
|
961
|
+
onShutdownComplete?.();
|
|
962
|
+
}
|
|
858
963
|
}
|
|
859
964
|
})();
|
|
860
965
|
return shutdownPromise;
|
package/dist/db.js
CHANGED
|
@@ -195,6 +195,7 @@ function migrate(conn) {
|
|
|
195
195
|
scope_id TEXT NOT NULL,
|
|
196
196
|
operation_id TEXT NOT NULL,
|
|
197
197
|
request_hash TEXT NOT NULL,
|
|
198
|
+
raw_request_hash TEXT,
|
|
198
199
|
session_id TEXT NOT NULL,
|
|
199
200
|
state TEXT NOT NULL DEFAULT 'active' CHECK(state IN ('active', 'deleted')),
|
|
200
201
|
created_at TEXT NOT NULL,
|
|
@@ -316,6 +317,7 @@ function migrate(conn) {
|
|
|
316
317
|
`);
|
|
317
318
|
addColumnIfMissing(conn, "plugin_marketplaces", "built_in", "INTEGER NOT NULL DEFAULT 0");
|
|
318
319
|
addColumnIfMissing(conn, "sessions", "forked_from_session_id", "TEXT");
|
|
320
|
+
addColumnIfMissing(conn, "plugin_session_launch_operations", "raw_request_hash", "TEXT");
|
|
319
321
|
}
|
|
320
322
|
/**
|
|
321
323
|
* Breaking Session cutover: the previous rows contain mutable Agent/Provider
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
/** Resolve the executable daemon module without importing its side-effectful entry. */
|
|
6
|
+
export function resolveDaemonEntry(moduleUrl = import.meta.url) {
|
|
7
|
+
return join(dirname(fileURLToPath(moduleUrl)), "index-daemon.js");
|
|
8
|
+
}
|
|
9
|
+
export function installedDaemonVersion() {
|
|
10
|
+
const manifest = require("../package.json");
|
|
11
|
+
return typeof manifest.version === "string" ? manifest.version : "0.0.0";
|
|
12
|
+
}
|
package/dist/index-daemon.js
CHANGED
|
@@ -1,91 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Boot order matters: install the EPIPE guard first (we're a pm2 child).
|
|
6
|
-
* The composition root reads static plugin manifests and supervises one runtime
|
|
7
|
-
* process for each enabled runtime plugin. Installed plugin code is loaded only
|
|
8
|
-
* in child processes; plugin-owned channel instances stay inside that process.
|
|
9
|
-
*/
|
|
10
|
-
import { startRynxDaemonServer } from "./daemon-server.js";
|
|
11
|
-
import { ensureBundledOfficialPlugins } from "./bundled-plugins.js";
|
|
12
|
-
import { installStdioEpipeGuard } from "./stdio-epipe-guard.js";
|
|
13
|
-
installStdioEpipeGuard();
|
|
14
|
-
async function main() {
|
|
15
|
-
const followsApp = process.env.RYNX_DAEMON_LIFECYCLE === "follow-app";
|
|
16
|
-
let parentDisconnected = followsApp && !process.connected;
|
|
17
|
-
const recordParentDisconnect = () => {
|
|
18
|
-
parentDisconnected = true;
|
|
19
|
-
};
|
|
20
|
-
if (followsApp)
|
|
21
|
-
process.once("disconnect", recordParentDisconnect);
|
|
22
|
-
const preparePlugins = async () => {
|
|
23
|
-
try {
|
|
24
|
-
const bundled = await ensureBundledOfficialPlugins({ reclaimHostLeases: true });
|
|
25
|
-
if (bundled.status === "ready") {
|
|
26
|
-
console.log(JSON.stringify({
|
|
27
|
-
level: "info",
|
|
28
|
-
type: "bundled_plugins_ready",
|
|
29
|
-
installed: bundled.installed,
|
|
30
|
-
updated: bundled.updated,
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch (error) {
|
|
35
|
-
console.error(JSON.stringify({
|
|
36
|
-
level: "error",
|
|
37
|
-
type: "bundled_plugins_failed",
|
|
38
|
-
error: error instanceof Error ? error.message : String(error),
|
|
39
|
-
}));
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
let server;
|
|
43
|
-
try {
|
|
44
|
-
server = await startRynxDaemonServer({ preparePlugins });
|
|
45
|
-
}
|
|
46
|
-
catch (error) {
|
|
47
|
-
process.off("disconnect", recordParentDisconnect);
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
let shutdownPromise;
|
|
51
|
-
const shutdown = (signal) => {
|
|
52
|
-
if (shutdownPromise)
|
|
53
|
-
return shutdownPromise;
|
|
54
|
-
process.off("SIGINT", onSigint);
|
|
55
|
-
process.off("SIGTERM", onSigterm);
|
|
56
|
-
process.off("disconnect", onDisconnect);
|
|
57
|
-
shutdownPromise = (async () => {
|
|
58
|
-
console.log(JSON.stringify({ level: "info", type: "shutdown", signal }));
|
|
59
|
-
await server.shutdown();
|
|
60
|
-
})();
|
|
61
|
-
shutdownPromise.catch((error) => {
|
|
62
|
-
process.exitCode = 1;
|
|
63
|
-
console.error(`Shutdown failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
64
|
-
});
|
|
65
|
-
return shutdownPromise;
|
|
66
|
-
};
|
|
67
|
-
const onSigint = () => {
|
|
68
|
-
void shutdown("SIGINT");
|
|
69
|
-
};
|
|
70
|
-
const onSigterm = () => {
|
|
71
|
-
void shutdown("SIGTERM");
|
|
72
|
-
};
|
|
73
|
-
const onDisconnect = () => {
|
|
74
|
-
if (process.env.RYNX_DAEMON_LIFECYCLE === "follow-app") {
|
|
75
|
-
void shutdown("APP_DISCONNECT");
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
process.once("SIGINT", onSigint);
|
|
79
|
-
process.once("SIGTERM", onSigterm);
|
|
80
|
-
if (followsApp) {
|
|
81
|
-
process.once("disconnect", onDisconnect);
|
|
82
|
-
process.off("disconnect", recordParentDisconnect);
|
|
83
|
-
if (parentDisconnected || !process.connected) {
|
|
84
|
-
void shutdown("APP_DISCONNECT");
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
main().catch((err) => {
|
|
2
|
+
/** Direct daemon executable retained only for standalone PM2 supervision. */
|
|
3
|
+
import { runDaemonProcess } from "./process.js";
|
|
4
|
+
runDaemonProcess().catch((err) => {
|
|
89
5
|
console.error(`Fatal error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
|
|
90
6
|
process.exit(1);
|
|
91
7
|
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { formatControlUrl, isPm2DaemonOnline, statusDaemon, stopDaemon, streamLogs, } from "./pm2.js";
|
|
2
|
+
export { resolveDaemonEntry } from "./entry-path.js";
|
|
3
|
+
export interface StandaloneDaemonLaunchContext {
|
|
4
|
+
builtinSkillsDir: string;
|
|
5
|
+
cliVersion: string;
|
|
6
|
+
productVersion: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function startDaemon(options: {
|
|
9
|
+
force: boolean;
|
|
10
|
+
quiet?: boolean;
|
|
11
|
+
context: StandaloneDaemonLaunchContext;
|
|
12
|
+
}): number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { startDaemon as startPm2Daemon, } from "./pm2.js";
|
|
2
|
+
export { formatControlUrl, isPm2DaemonOnline, statusDaemon, stopDaemon, streamLogs, } from "./pm2.js";
|
|
3
|
+
export { resolveDaemonEntry } from "./entry-path.js";
|
|
4
|
+
export function startDaemon(options) {
|
|
5
|
+
return startPm2Daemon({
|
|
6
|
+
force: options.force,
|
|
7
|
+
quiet: options.quiet,
|
|
8
|
+
launchEnvironment: {
|
|
9
|
+
RYNX_BUILTIN_SKILLS_DIR: options.context.builtinSkillsDir,
|
|
10
|
+
RYNX_CLI_VERSION: options.context.cliVersion,
|
|
11
|
+
RYNX_DISTRIBUTION: "standalone",
|
|
12
|
+
RYNX_PRODUCT_VERSION: options.context.productVersion,
|
|
13
|
+
RYNX_DAEMON_LIFECYCLE: "standalone",
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type MaintenanceLeaseReleaseOutcome = "released" | "not_active" | "denied";
|
|
2
|
+
export interface MaintenanceLeaseStoreOptions {
|
|
3
|
+
filePath?: string;
|
|
4
|
+
now?: () => number;
|
|
5
|
+
ttlMs?: number;
|
|
6
|
+
/** Test seam; production fsyncs the lease directory after rename/remove. */
|
|
7
|
+
syncDirectory?: (directory: string) => void;
|
|
8
|
+
/** Process-local recovery hook, invoked once when a live lease is observed expired. */
|
|
9
|
+
onExpired?: () => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function maintenanceLeaseFile(env?: NodeJS.ProcessEnv): string;
|
|
12
|
+
/**
|
|
13
|
+
* Cross-process admission fence used only during daemon replacement.
|
|
14
|
+
*
|
|
15
|
+
* The raw bearer token is never persisted. The bounded expiry means a crashed
|
|
16
|
+
* upgrader cannot leave the daemon permanently unavailable, while atomic rename
|
|
17
|
+
* ensures a replacement daemon sees either the complete lease or no lease.
|
|
18
|
+
*/
|
|
19
|
+
export declare class MaintenanceLeaseStore {
|
|
20
|
+
private readonly filePath;
|
|
21
|
+
private readonly now;
|
|
22
|
+
private readonly ttlMs;
|
|
23
|
+
private readonly syncDirectory;
|
|
24
|
+
private readonly onExpired;
|
|
25
|
+
private lease;
|
|
26
|
+
private expiryTimer;
|
|
27
|
+
constructor(options?: MaintenanceLeaseStoreOptions);
|
|
28
|
+
isActive(): boolean;
|
|
29
|
+
isHeldBy(token: string): boolean;
|
|
30
|
+
acquire(token: string): boolean;
|
|
31
|
+
release(token: string): MaintenanceLeaseReleaseOutcome;
|
|
32
|
+
private readPersistedLease;
|
|
33
|
+
private writePersistedLease;
|
|
34
|
+
private clear;
|
|
35
|
+
private clearFile;
|
|
36
|
+
private expire;
|
|
37
|
+
private scheduleExpiry;
|
|
38
|
+
private cancelExpiry;
|
|
39
|
+
}
|