@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.
Files changed (49) hide show
  1. package/bundled-plugins/plugins/lark/dist/runtime.js +2 -5
  2. package/bundled-plugins/plugins/lark/dist/settings.js +2 -2
  3. package/bundled-plugins/plugins/lark/package.json +1 -1
  4. package/dist/browser-artifact-management.d.ts +4 -1
  5. package/dist/browser-artifact-management.js +18 -10
  6. package/dist/browser-artifacts.d.ts +1 -0
  7. package/dist/browser-artifacts.js +1 -0
  8. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  9. package/dist/chrome-for-testing-runtime.js +193 -0
  10. package/dist/chrome-for-testing-store.d.ts +2 -0
  11. package/dist/chrome-for-testing-store.js +9 -0
  12. package/dist/daemon-server.d.ts +4 -1
  13. package/dist/daemon-server.js +137 -32
  14. package/dist/db.js +2 -0
  15. package/dist/entry-path.d.ts +3 -0
  16. package/dist/entry-path.js +12 -0
  17. package/dist/index-daemon.js +3 -87
  18. package/dist/lifecycle.d.ts +12 -0
  19. package/dist/lifecycle.js +16 -0
  20. package/dist/maintenance-lease.d.ts +39 -0
  21. package/dist/maintenance-lease.js +250 -0
  22. package/dist/plugin-host-rpc.d.ts +19 -1
  23. package/dist/plugin-host-rpc.js +163 -23
  24. package/dist/plugin-installer.js +59 -0
  25. package/dist/plugin-supervisor.d.ts +5 -0
  26. package/dist/plugin-supervisor.js +218 -29
  27. package/dist/pm2.d.ts +11 -2
  28. package/dist/pm2.js +116 -19
  29. package/dist/process.d.ts +12 -0
  30. package/dist/process.js +91 -0
  31. package/dist/session-launch-operations.d.ts +10 -1
  32. package/dist/session-launch-operations.js +15 -6
  33. package/dist/setup-service.d.ts +20 -0
  34. package/dist/setup-service.js +38 -0
  35. package/package.json +33 -20
  36. package/dist/browser-cli-args.d.ts +0 -20
  37. package/dist/browser-cli-args.js +0 -100
  38. package/dist/cli.d.ts +0 -2
  39. package/dist/cli.js +0 -550
  40. package/dist/control-client.d.ts +0 -122
  41. package/dist/control-client.js +0 -855
  42. package/dist/control-endpoint.d.ts +0 -14
  43. package/dist/control-endpoint.js +0 -124
  44. package/dist/desktop-browser-host-client.d.ts +0 -35
  45. package/dist/desktop-browser-host-client.js +0 -415
  46. package/dist/setup.d.ts +0 -20
  47. package/dist/setup.js +0 -386
  48. package/dist/update.d.ts +0 -25
  49. package/dist/update.js +0 -133
@@ -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 || activity.pendingInteractions > 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.AGENT_RUNTIME }
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,3 @@
1
+ /** Resolve the executable daemon module without importing its side-effectful entry. */
2
+ export declare function resolveDaemonEntry(moduleUrl?: string): string;
3
+ export declare function installedDaemonVersion(): string;
@@ -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
+ }
@@ -1,91 +1,7 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * The resident daemon entry — this is what pm2 keeps alive.
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
+ }