@rynx-ai/daemon 0.1.11-beta.2 → 0.1.11-beta.21

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 (88) hide show
  1. package/bundled-plugins/plugins/lark/.rynx-plugin/plugin.json +0 -12
  2. package/bundled-plugins/plugins/lark/dist/cli-mirror/mirror-index.js +3 -1
  3. package/bundled-plugins/plugins/lark/dist/host-adapters.d.ts +3 -3
  4. package/bundled-plugins/plugins/lark/dist/host-adapters.js +20 -18
  5. package/bundled-plugins/plugins/lark/dist/lark.js +18 -12
  6. package/bundled-plugins/plugins/lark/dist/runtime.js +6046 -6022
  7. package/bundled-plugins/plugins/lark/dist/settings.js +6006 -5988
  8. package/bundled-plugins/plugins/lark/package.json +1 -1
  9. package/bundled-plugins/plugins/lark/ui/assets/index-CGUVAMsv.css +1 -0
  10. package/bundled-plugins/plugins/lark/ui/index.html +2 -2
  11. package/dist/agent-file.js +0 -1
  12. package/dist/browser-artifact-management.d.ts +4 -1
  13. package/dist/browser-artifact-management.js +22 -11
  14. package/dist/browser-artifacts.d.ts +1 -0
  15. package/dist/browser-artifacts.js +1 -0
  16. package/dist/chrome-for-testing-runtime.d.ts +27 -0
  17. package/dist/chrome-for-testing-runtime.js +194 -0
  18. package/dist/chrome-for-testing-store.d.ts +2 -0
  19. package/dist/chrome-for-testing-store.js +37 -6
  20. package/dist/control-deps.d.ts +6 -2
  21. package/dist/control-deps.js +87 -4
  22. package/dist/control-link-store.js +4 -3
  23. package/dist/daemon-owner.js +4 -7
  24. package/dist/daemon-server.d.ts +4 -1
  25. package/dist/daemon-server.js +158 -45
  26. package/dist/db.d.ts +2 -2
  27. package/dist/db.js +55 -20
  28. package/dist/entry-path.d.ts +3 -0
  29. package/dist/entry-path.js +12 -0
  30. package/dist/index-daemon.js +3 -87
  31. package/dist/lifecycle.d.ts +13 -0
  32. package/dist/lifecycle.js +19 -0
  33. package/dist/maintenance-lease.d.ts +39 -0
  34. package/dist/maintenance-lease.js +250 -0
  35. package/dist/migrations/cleanup-legacy-sessions.js +4 -3
  36. package/dist/plugin-cli.d.ts +10 -0
  37. package/dist/plugin-cli.js +19 -2
  38. package/dist/plugin-host-rpc.d.ts +35 -6
  39. package/dist/plugin-host-rpc.js +222 -59
  40. package/dist/plugin-installer.js +62 -2
  41. package/dist/plugin-management-service.js +0 -2
  42. package/dist/plugin-marketplace.js +2 -1
  43. package/dist/plugin-store.js +2 -4
  44. package/dist/plugin-supervisor.d.ts +5 -0
  45. package/dist/plugin-supervisor.js +218 -33
  46. package/dist/pm2.d.ts +11 -2
  47. package/dist/pm2.js +116 -19
  48. package/dist/process.d.ts +12 -0
  49. package/dist/process.js +91 -0
  50. package/dist/remote-runtime-access-store.d.ts +2 -2
  51. package/dist/remote-runtime-access-store.js +6 -9
  52. package/dist/remote-runtime-target-store.d.ts +2 -2
  53. package/dist/remote-runtime-target-store.js +6 -6
  54. package/dist/session-emulator-binding-store.js +4 -6
  55. package/dist/session-fork-store.js +24 -8
  56. package/dist/session-launch-operations.d.ts +10 -1
  57. package/dist/session-launch-operations.js +21 -11
  58. package/dist/session-log-store.js +2 -1
  59. package/dist/session-meta-store.d.ts +5 -0
  60. package/dist/session-meta-store.js +25 -2
  61. package/dist/session-portal-grant-store.d.ts +29 -0
  62. package/dist/session-portal-grant-store.js +85 -0
  63. package/dist/session-resource-store.js +3 -2
  64. package/dist/setup-service.d.ts +21 -0
  65. package/dist/setup-service.js +39 -0
  66. package/dist/skills-catalog.d.ts +11 -2
  67. package/dist/skills-catalog.js +307 -10
  68. package/dist/sqlite.d.ts +22 -0
  69. package/dist/sqlite.js +57 -0
  70. package/dist/system-dependencies.d.ts +22 -0
  71. package/dist/system-dependencies.js +79 -0
  72. package/package.json +37 -21
  73. package/bundled-plugins/plugins/lark/ui/assets/index-B2dhMlMU.css +0 -1
  74. package/dist/browser-cli-args.d.ts +0 -20
  75. package/dist/browser-cli-args.js +0 -100
  76. package/dist/cli.d.ts +0 -2
  77. package/dist/cli.js +0 -550
  78. package/dist/control-client.d.ts +0 -122
  79. package/dist/control-client.js +0 -855
  80. package/dist/control-endpoint.d.ts +0 -14
  81. package/dist/control-endpoint.js +0 -124
  82. package/dist/desktop-browser-host-client.d.ts +0 -35
  83. package/dist/desktop-browser-host-client.js +0 -415
  84. package/dist/setup.d.ts +0 -20
  85. package/dist/setup.js +0 -386
  86. package/dist/update.d.ts +0 -25
  87. package/dist/update.js +0 -133
  88. /package/bundled-plugins/plugins/lark/ui/assets/{index-D9N-AkLX.js → index-D8Amoe6a.js} +0 -0
@@ -6,32 +6,73 @@
6
6
  */
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { listAgentSpecs, loadAgentSpec, rynxHome } from "@rynx-ai/core";
9
+ import { listAgentSpecs, loadAgentSpec, rynxHome, scanLocalRuntimesInWorker, } from "@rynx-ai/core";
10
+ import { listRuntimeModels } from "@rynx-ai/server";
10
11
  import { createEmulatorControlDeps } from "@rynx-ai/emulator";
11
12
  import { removeAgentSpec, writeAgentSpec } from "./agent-file.js";
12
- import { getCatalogSkill, installCatalogSkill, listCatalogSkills, readCatalogSkillFile, removeCatalogSkill, } from "./skills-catalog.js";
13
+ import { getCatalogSkill, importLocalCatalogSkills, importCatalogSkillsFromArchive, installCatalogSkill, listCatalogSkills, readCatalogSkillFile, removeCatalogSkill, scanLocalCatalogSkillCandidates, } from "./skills-catalog.js";
13
14
  import { createSessionMeta, getSessionMeta, listSessionMetas, removeSessionMeta, setSessionTitle, } from "./session-meta-store.js";
14
15
  import { consumeControlLink } from "./control-link-store.js";
15
- export function buildControlDeps() {
16
+ const RUNTIME_OPTIONS_CACHE_MS = 30_000;
17
+ export function buildControlDeps(config) {
18
+ let runtimeOptionsCache;
19
+ let runtimeOptionsInFlight;
20
+ const listAgentRuntimeOptions = async (options = {}) => {
21
+ const now = Date.now();
22
+ if (!options.refresh && runtimeOptionsCache && runtimeOptionsCache.expiresAt > now) {
23
+ return runtimeOptionsCache.value;
24
+ }
25
+ if (!options.refresh && runtimeOptionsInFlight)
26
+ return runtimeOptionsInFlight;
27
+ const scan = async () => {
28
+ const runtimes = await scanLocalRuntimesInWorker();
29
+ const value = await Promise.all(runtimes.map(async (runtime) => {
30
+ return {
31
+ id: runtime.id,
32
+ name: runtime.displayName,
33
+ binary: runtime.binary,
34
+ ...(runtime.executablePath ? { executablePath: runtime.executablePath } : {}),
35
+ ...(runtime.version ? { version: runtime.version } : {}),
36
+ available: runtime.available,
37
+ models: await listControlAgentModels(config, runtime.id),
38
+ };
39
+ }));
40
+ runtimeOptionsCache = { expiresAt: Date.now() + RUNTIME_OPTIONS_CACHE_MS, value };
41
+ return value;
42
+ };
43
+ runtimeOptionsInFlight = scan();
44
+ try {
45
+ return await runtimeOptionsInFlight;
46
+ }
47
+ finally {
48
+ runtimeOptionsInFlight = undefined;
49
+ }
50
+ };
16
51
  return {
17
52
  // ── declarative agents ───────────────────────────────────────────────
18
53
  listAgents: async () => (await listAgentSpecs()).map((spec) => ({
19
54
  id: spec.id,
20
55
  name: spec.name,
56
+ description: spec.description,
21
57
  runtime: spec.executor?.runtime,
22
- model: spec.executor?.model,
58
+ ...(spec.executor?.model ? { model: spec.executor.model } : {}),
59
+ skillCount: spec.skills?.length ?? 0,
23
60
  })),
24
61
  getAgent: (id) => loadAgentSpec(id),
25
62
  writeAgent: (id, spec) => {
26
63
  writeAgentSpec(id, spec);
27
64
  },
28
65
  removeAgent: (id) => removeAgentSpec(id),
66
+ listAgentRuntimeOptions,
29
67
  // ── skills catalog ───────────────────────────────────────────────────
30
68
  listSkills: () => listCatalogSkills(),
31
69
  getSkill: (name) => getCatalogSkill(name),
32
70
  readSkillFile: (name, file) => readCatalogSkillFile(name, file),
33
71
  installSkill: (recipe) => installCatalogSkill(recipe),
34
72
  removeSkill: (name) => removeCatalogSkill(name),
73
+ scanLocalSkills: (runtime) => scanLocalCatalogSkillCandidates(runtime),
74
+ importLocalSkills: (runtime, keys) => importLocalCatalogSkills(runtime, keys),
75
+ importSkillArchive: (filename, contentBase64) => importCatalogSkillsFromArchive(filename, contentBase64),
35
76
  // ── emulator proxy (single seam: the emulator package owns the surface) ─
36
77
  emulator: createEmulatorControlDeps(),
37
78
  // ── control-plane sessions (meta persisted; status held by the server) ─
@@ -44,6 +85,48 @@ export function buildControlDeps() {
44
85
  tailLogs: (instanceId, lines) => tailLogs(instanceId, lines),
45
86
  };
46
87
  }
88
+ /** Project a Runtime-native model catalog into the control-plane option shape. */
89
+ export async function listControlAgentModels(config, runtime) {
90
+ const catalog = await listRuntimeModels(config, runtime);
91
+ return (catalog?.data ?? [])
92
+ .filter((model) => model.hidden !== true)
93
+ .map((model) => ({
94
+ id: model.model || model.id,
95
+ label: model.displayName || model.model || model.id,
96
+ ...(model.description ? { description: model.description } : {}),
97
+ ...(model.isDefault ? { isDefault: true } : {}),
98
+ ...(model.defaultReasoningEffort
99
+ ? { defaultReasoningEffort: model.defaultReasoningEffort }
100
+ : {}),
101
+ reasoningOptions: model.supportedReasoningEfforts?.length
102
+ ? model.supportedReasoningEfforts.map((effort) => ({
103
+ value: effort.reasoningEffort,
104
+ label: reasoningLabel(effort.reasoningEffort),
105
+ ...(effort.description ? { description: effort.description } : {}),
106
+ }))
107
+ : fallbackReasoningOptions(runtime, model.model || model.id),
108
+ }));
109
+ }
110
+ function fallbackReasoningOptions(runtime, model) {
111
+ const values = runtime === "claude"
112
+ ? model.toLowerCase().includes("opus")
113
+ ? ["low", "medium", "high", "xhigh"]
114
+ : ["low", "medium", "high"]
115
+ : ["none", "minimal", "low", "medium", "high", "xhigh"];
116
+ return values.map((value) => ({ value, label: reasoningLabel(value) }));
117
+ }
118
+ function reasoningLabel(value) {
119
+ return {
120
+ none: "None",
121
+ minimal: "Minimal",
122
+ low: "Low",
123
+ medium: "Medium",
124
+ high: "High",
125
+ xhigh: "Extra high",
126
+ max: "Max",
127
+ ultra: "Ultra",
128
+ }[value] ?? value;
129
+ }
47
130
  /** Tail the daemon's merged pm2 logs, best-effort filtered by instance id. */
48
131
  async function tailLogs(instanceId, lines) {
49
132
  const dir = join(rynxHome(), "logs");
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { db } from "./db.js";
3
+ import { transaction } from "./sqlite.js";
3
4
  const DEFAULT_TTL_MS = 5 * 60_000;
4
5
  const MAX_TTL_MS = 15 * 60_000;
5
6
  /** Create a short-lived, one-use link into the generic session console. */
@@ -23,7 +24,7 @@ export function consumeControlLink(tokenInput) {
23
24
  if (!token || token.length > 512)
24
25
  return null;
25
26
  const now = new Date().toISOString();
26
- const consume = db().transaction(() => {
27
+ const consume = transaction(db(), () => {
27
28
  const row = db()
28
29
  .prepare(`SELECT session_id
29
30
  FROM control_links
@@ -39,9 +40,9 @@ export function consumeControlLink(tokenInput) {
39
40
  return consume.immediate();
40
41
  }
41
42
  export function pruneControlLinks(now = new Date().toISOString()) {
42
- return db()
43
+ return Number(db()
43
44
  .prepare("DELETE FROM control_links WHERE expires_at <= ? OR used_at IS NOT NULL")
44
- .run(now).changes;
45
+ .run(now).changes);
45
46
  }
46
47
  function required(value, label) {
47
48
  const normalized = value?.trim();
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
4
5
  import { rynxHome } from "@rynx-ai/core";
5
- import Database from "better-sqlite3";
6
+ import { hasSqlitePrimaryCode, SQLITE_BUSY, SQLITE_LOCKED } from "./sqlite.js";
6
7
  const LOCK_FILE = "daemon-owner.sqlite";
7
8
  const OWNER_FILE = "daemon-owner.json";
8
9
  /**
@@ -18,16 +19,15 @@ export async function acquireDaemonOwner(env = process.env) {
18
19
  const lockPath = path.join(home, LOCK_FILE);
19
20
  const ownerFile = path.join(home, OWNER_FILE);
20
21
  await mkdir(home, { recursive: true, mode: 0o700 });
21
- const connection = new Database(lockPath, { timeout: 0 });
22
+ const connection = new DatabaseSync(lockPath, { timeout: 0 });
22
23
  try {
23
- connection.pragma("busy_timeout = 0");
24
24
  connection.exec("BEGIN EXCLUSIVE");
25
25
  // Force this transaction to hold a write lock even on a brand-new file.
26
26
  connection.exec("CREATE TABLE IF NOT EXISTS daemon_owner_guard (id INTEGER PRIMARY KEY)");
27
27
  }
28
28
  catch (error) {
29
29
  connection.close();
30
- if (hasCode(error, "SQLITE_BUSY") || hasCode(error, "SQLITE_LOCKED")) {
30
+ if (hasSqlitePrimaryCode(error, SQLITE_BUSY) || hasSqlitePrimaryCode(error, SQLITE_LOCKED)) {
31
31
  throw await ownerConflictError(home, ownerFile);
32
32
  }
33
33
  throw error;
@@ -99,6 +99,3 @@ function rollbackAndClose(connection) {
99
99
  connection.close();
100
100
  }
101
101
  }
102
- function hasCode(error, code) {
103
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
104
- }
@@ -9,6 +9,9 @@ export declare function daemonActivityBlocksShutdown(activity: DaemonActivitySna
9
9
  export interface StartRynxDaemonServerOptions {
10
10
  config?: AppConfig;
11
11
  warn?: (message: string) => void;
12
+ /** Process-owner callback after every daemon resource and owner lease has
13
+ * shut down successfully. Library callers normally leave this unset. */
14
+ onShutdownComplete?: () => void;
12
15
  /** Test/deployment seam; failures roll back the already-listening server. */
13
16
  writeEndpoint?: (config: AppConfig, managementToken: string) => Promise<void>;
14
17
  /** Test seam for owner-lock failure injection. Production always uses the RYNX_HOME lease. */
@@ -25,7 +28,7 @@ export interface StartRynxDaemonServerOptions {
25
28
  /** Test seam for the App-only embedded Chromium process supervisor. */
26
29
  createBrowserHostSupervisor?: (options: AppBrowserHostSupervisorOptions) => Pick<AppBrowserHostSupervisor, "ensureConnected" | "restart" | "stop">;
27
30
  }
28
- export declare function startRynxDaemonServer({ config, warn, writeEndpoint, acquireOwner, preparePlugins, loadDirectConfig, startDirectServer, createChromeInspectionManager, createBrowserHostSupervisor, }?: StartRynxDaemonServerOptions): Promise<Awaited<ReturnType<typeof startServer>>>;
31
+ export declare function startRynxDaemonServer({ config, warn, onShutdownComplete, writeEndpoint, acquireOwner, preparePlugins, loadDirectConfig, startDirectServer, createChromeInspectionManager, createBrowserHostSupervisor, }?: StartRynxDaemonServerOptions): Promise<Awaited<ReturnType<typeof startServer>>>;
29
32
  export declare function createSupervisedAppSessionBrowserHost(desktop: SessionBrowserHost, supervisor: Pick<AppBrowserHostSupervisor, "ensureConnected" | "restart">): SessionBrowserHost;
30
33
  /**
31
34
  * Resolve the authenticated semantic target inside its owning Runtime and
@@ -11,19 +11,22 @@ 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";
18
18
  import { RUNTIME_BROWSER_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-browser-surface";
19
19
  import { RUNTIME_EMULATOR_SURFACE_SEMANTIC_CAPABILITY } from "@rynx-ai/protocol/runtime-emulator-surface";
20
- import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, listRuntimeModels, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
20
+ import { createDaemonRuntimeHost, createDesktopFirstSessionBrowserHost, createRunnerSessionTerminalHost, createSessionRuntimeServices, DesktopBrowserHostAbsentError, DesktopBrowserHostRegistry, DirectRuntimeConnectionHub, DirectRuntimeBrowserInspectHostError, MachineSessionService, MachineSessionServiceFailure, RemoteRuntimeDispatcher, SessionBrowserService, SessionBrowserServiceError, SessionBrowserSurfaceCoordinator, SessionEmulatorService, startServer, startDirectRuntimeServer, } from "@rynx-ai/server";
21
+ import { authorizeSessionPortalGrant, redeemSessionPortalTicket, } from "./session-portal-grant-store.js";
21
22
  import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrowserHostLaunchConfig, } from "./app-browser-host-supervisor.js";
22
- import { buildControlDeps } from "./control-deps.js";
23
+ import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
23
24
  import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
24
25
  import { createDaemonBuildStatus } from "./daemon-build-status.js";
26
+ import { MaintenanceLeaseStore } from "./maintenance-lease.js";
25
27
  import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
26
28
  import { removeLegacyPluginSkillSnapshot, snapshotEnabledPluginSkillSources, } from "./plugin-skill-context.js";
29
+ import { installedDaemonVersion } from "./entry-path.js";
27
30
  import { isChromeForTestingHostSupported } from "./chrome-for-testing-store.js";
28
31
  import { ChromeInspectionManager, } from "./chrome-inspection-manager.js";
29
32
  import { createHeadlessBrowserHost } from "./headless-browser-host.js";
@@ -47,7 +50,7 @@ import { SqliteSessionResourceStore } from "./session-resource-store.js";
47
50
  import { createSessionFallbackWorkspace, discardSessionFallbackWorkspace, } from "./session-fallback-workspace.js";
48
51
  import { SessionBrowserCapabilityStore } from "./session-browser-capability-store.js";
49
52
  import { SqliteSessionEmulatorBindingStore } from "./session-emulator-binding-store.js";
50
- import { sessionRegistry } from "./session-meta-store.js";
53
+ import { sessionBelongsToPlugin, sessionRegistry } from "./session-meta-store.js";
51
54
  import { sessionPendingMessageStore } from "./session-pending-message-store.js";
52
55
  import { RemoteRuntimeAccessStore } from "./remote-runtime-access-store.js";
53
56
  import { DaemonRemoteRuntimeAdmin } from "./remote-runtime-admin.js";
@@ -66,17 +69,14 @@ const require = createRequire(import.meta.url);
66
69
  const DAEMON_PACKAGE_VERSION = readDaemonPackageVersion();
67
70
  async function resolveSessionExecutionSnapshot(input) {
68
71
  return {
69
- ...await resolveExecutionSnapshot({
70
- ...input,
71
- resolveDefaultModel: async (provider) => {
72
- const models = await listRuntimeModels(input.config, provider);
73
- return models?.data.find((candidate) => candidate.isDefault)?.model;
74
- },
75
- }),
72
+ ...await resolveExecutionSnapshot(input),
76
73
  pluginSkills: snapshotEnabledPluginSkillSources(),
77
74
  };
78
75
  }
79
- async function resolveSessionWorkspace(projects, projectId, managedCwd) {
76
+ async function resolveSessionWorkspace(projects, projectId, managedCwd, workspace) {
77
+ if (workspace) {
78
+ return sessionWorkspaceSnapshotSchema.parse(workspace);
79
+ }
80
80
  if (projectId) {
81
81
  const project = await projects.get(projectId);
82
82
  if (!project) {
@@ -131,7 +131,8 @@ function toMarketplaceItem(record) {
131
131
  };
132
132
  }
133
133
  export function daemonActivityBlocksShutdown(activity) {
134
- return activity.runningTurns > 0 || activity.pendingInteractions > 0;
134
+ return activity.runningTurns > 0
135
+ || activity.pendingInteractions > 0;
135
136
  }
136
137
  function defaultWarn(message) {
137
138
  console.warn(JSON.stringify({ level: "warn", type: "channel-registry", msg: message }));
@@ -250,19 +251,31 @@ async function removeControlEndpointForCurrentProcess() {
250
251
  // Best effort only; stale endpoint files are ignored by the client fallback.
251
252
  }
252
253
  }
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), } = {}) {
254
+ 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
255
  const owner = await acquireOwner();
255
256
  let ownerManagedByServer = false;
256
257
  try {
257
258
  await preparePlugins?.();
258
259
  const appConfig = config ?? loadConfig();
260
+ const admissionGate = new CloseableAdmissionGate();
261
+ let shutdownAccepted = false;
262
+ const maintenanceLease = new MaintenanceLeaseStore({
263
+ // Expiry restores the gate primitive for a process that outlives its
264
+ // upgrader. The independent accepted-shutdown fence below still prevents
265
+ // new work after an irreversible server shutdown has begun.
266
+ onExpired: () => admissionGate.reopen(),
267
+ });
268
+ const admissionOpen = () => !shutdownAccepted && !maintenanceLease.isActive();
269
+ const reserveAdmission = () => !admissionOpen()
270
+ ? undefined
271
+ : admissionGate.tryReserve();
259
272
  const appBrowserHostLaunch = parseAppBrowserHostLaunchConfig();
260
273
  const directHostingSupported = isDaemonDirectRuntimeHostingSupported();
261
274
  const runtimeBrowserSupported = appBrowserHostLaunch !== undefined || isChromeForTestingHostSupported();
262
275
  const directConfig = directHostingSupported
263
276
  ? await loadDirectConfig()
264
277
  : undefined;
265
- const control = buildControlDeps();
278
+ const control = buildControlDeps(appConfig);
266
279
  const installationIdentity = await loadOrCreateDaemonInstallationIdentity();
267
280
  const directEncryptionIdentity = directHostingSupported
268
281
  ? await loadOrCreateDaemonDirectRuntimeEncryptionIdentity()
@@ -286,6 +299,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
286
299
  "semantic.session.interrupt.v1",
287
300
  "semantic.session.agent-options.v1",
288
301
  "semantic.session.launch-options.v2",
302
+ "semantic.session.execution-settings.v1",
289
303
  "semantic.session.create.v1",
290
304
  "semantic.session.fork.v1",
291
305
  "semantic.session.message.v1",
@@ -335,6 +349,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
335
349
  }
336
350
  const pluginSupervisor = new PluginSupervisor(listPlugins(), {
337
351
  daemonInstanceId,
352
+ productVersion: controlEndpointDistributionMetadata().productVersion,
353
+ cliVersion: boundedEnvironmentMetadata(process.env.RYNX_CLI_VERSION, 128)
354
+ ?? installedDaemonVersion(),
338
355
  log: (entry) => {
339
356
  const output = entry.level === "error"
340
357
  ? console.error
@@ -379,7 +396,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
379
396
  },
380
397
  sessionLaunch: {
381
398
  resolve: async (selection) => {
382
- const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd);
399
+ const workspace = await resolveSessionWorkspace(runtimeProjects, selection.projectId, selection.managedCwd, selection.workspace);
383
400
  return {
384
401
  workspace,
385
402
  execution: await resolveSessionExecutionSnapshot({
@@ -387,9 +404,9 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
387
404
  ...(selection.agent ? { agentName: selection.agent } : {}),
388
405
  ...(selection.config ? { spec: selection.config } : {}),
389
406
  ...((selection.provider || (!selection.agent && !selection.config))
390
- ? { provider: selection.provider ?? appConfig.AGENT_RUNTIME }
407
+ ? { provider: selection.provider ?? appConfig.DEFAULT_RUNTIME }
391
408
  : {}),
392
- ...(selection.model ? { model: selection.model } : {}),
409
+ ...(selection.model !== undefined ? { model: selection.model } : {}),
393
410
  ...(selection.reasoningEffort
394
411
  ? { reasoningEffort: selection.reasoningEffort }
395
412
  : {}),
@@ -398,7 +415,14 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
398
415
  };
399
416
  },
400
417
  },
418
+ sessionExecution: {
419
+ update: async (input) => {
420
+ await machineSessions.updateExecutionSettings(input);
421
+ },
422
+ },
401
423
  sessionFork: (input) => machineSessions.fork(input),
424
+ admissionOpen,
425
+ admissionReserve: reserveAdmission,
402
426
  }),
403
427
  startPluginRuntimes: pluginSupervisor.startPluginRuntimes.bind(pluginSupervisor),
404
428
  statusSnapshot: pluginSupervisor.statusSnapshot.bind(pluginSupervisor),
@@ -421,6 +445,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
421
445
  config: appConfig,
422
446
  sessionLog,
423
447
  sessionContextProvider: browserRunnerSessionContexts,
448
+ admissionOpen,
449
+ admissionReserve: reserveAdmission,
424
450
  onMirrorError: (error, sessionId) => warn(`Session mirror failed for ${sessionId}: ${error instanceof Error ? error.message : String(error)}`),
425
451
  });
426
452
  const sessionBrowserHost = appBrowserHostSupervisor
@@ -432,6 +458,8 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
432
458
  exists: async (sessionId) => Boolean(sessionRegistry.get(sessionId) ??
433
459
  await sessionRuntimeServices.sessionStore.get(sessionId)),
434
460
  },
461
+ }, {
462
+ admissionReserve: reserveAdmission,
435
463
  });
436
464
  const sessionEmulators = control.emulator
437
465
  ? new SessionEmulatorService({
@@ -529,24 +557,30 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
529
557
  forks: sessionForks,
530
558
  execution: {
531
559
  runtime: sessionRuntimeServices.conversationRuntime,
532
- resolve: ({ agent, provider, model, reasoningEffort, cwd }) => resolveSessionExecutionSnapshot({
560
+ resolve: ({ agent, provider, model, reasoningEffort, permissionPreset, cwd }) => resolveSessionExecutionSnapshot({
533
561
  config: appConfig,
534
562
  ...(agent ? { agentName: agent } : {}),
535
563
  ...(provider ? { provider } : {}),
536
564
  ...(model ? { model } : {}),
537
565
  ...(reasoningEffort ? { reasoningEffort } : {}),
566
+ ...(permissionPreset ? { permissionPreset } : {}),
538
567
  cwd,
539
568
  }),
569
+ listModels: (provider) => listControlAgentModels(appConfig, provider),
540
570
  runner: sessionRuntimeServices.runnerManager,
541
571
  },
542
572
  workspace: {
543
573
  resolve: (projectId) => resolveSessionWorkspace(runtimeProjects, projectId),
544
574
  discardUnbound: discardSessionFallbackWorkspace,
545
575
  },
576
+ }, {
577
+ admissionOpen,
578
+ admissionReserve: reserveAdmission,
546
579
  });
547
580
  const directAuthenticator = new DaemonDirectRuntimeAuthenticator(installationIdentity, remoteRuntime, remoteRuntimeAccess);
548
581
  const sessionTerminals = createRunnerSessionTerminalHost({
549
582
  runnerManager: sessionRuntimeServices.runnerManager,
583
+ admissionReserve: reserveAdmission,
550
584
  resolveLiveSession: async (sessionId) => {
551
585
  const registered = sessionRegistry.get(sessionId);
552
586
  if (!registered ||
@@ -556,8 +590,95 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
556
590
  return { cwd: registered.workspace.cwd };
557
591
  },
558
592
  });
559
- let shutdownAccepted = false;
560
593
  let requestServerShutdown;
594
+ let shutdownAttempt;
595
+ let shutdownAttemptLeaseToken;
596
+ const shutdownIfIdle = (maintenanceLeaseToken) => {
597
+ if (maintenanceLease.isActive()) {
598
+ if (!maintenanceLeaseToken || !maintenanceLease.isHeldBy(maintenanceLeaseToken)) {
599
+ return Promise.reject(new Error("a different maintenance lease is already active"));
600
+ }
601
+ // The previous daemon may have persisted the lease and exited after
602
+ // its 202 response was lost. A replacement process holding the same
603
+ // token must perform its own close/drain/shutdown; returning accepted
604
+ // without scheduling this PID would strand the updater waiting for a
605
+ // process that can never exit.
606
+ }
607
+ if (shutdownAccepted) {
608
+ if (maintenanceLeaseToken) {
609
+ maintenanceLease.acquire(maintenanceLeaseToken);
610
+ return Promise.resolve({
611
+ outcome: "accepted",
612
+ maintenanceLease: "active",
613
+ });
614
+ }
615
+ return Promise.resolve({ outcome: "accepted" });
616
+ }
617
+ if (shutdownAttempt) {
618
+ if (maintenanceLeaseToken === shutdownAttemptLeaseToken) {
619
+ return shutdownAttempt;
620
+ }
621
+ return Promise.reject(new Error("another idle shutdown check is already in progress"));
622
+ }
623
+ shutdownAttemptLeaseToken = maintenanceLeaseToken;
624
+ const attempt = (async () => {
625
+ await admissionGate.closeAndDrain();
626
+ const runtimeActivity = sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot();
627
+ const activity = {
628
+ ...runtimeActivity,
629
+ // A terminal submission keeps its own admission reservation until the
630
+ // first response or native rotation is visible. Failed process-tree
631
+ // cleanup releases the reservation but remains pinned here, so an
632
+ // idle check returns busy instead of hanging or killing hidden work.
633
+ runningTurns: runtimeActivity.runningTurns
634
+ + sessionRuntimeServices.runnerManager.pendingTerminalInputCount(),
635
+ activeTerminals: sessionTerminals.activeTerminalCount(),
636
+ activeBrowsers: sessionBrowsers.activeBrowserCount(),
637
+ };
638
+ if (daemonActivityBlocksShutdown(activity)) {
639
+ admissionGate.reopen();
640
+ return { outcome: "busy", activity };
641
+ }
642
+ const shutdown = requestServerShutdown;
643
+ if (!shutdown) {
644
+ admissionGate.reopen();
645
+ throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
646
+ }
647
+ const acquiredLease = maintenanceLeaseToken
648
+ ? maintenanceLease.acquire(maintenanceLeaseToken)
649
+ : false;
650
+ shutdownAccepted = true;
651
+ try {
652
+ void shutdown().catch((error) => {
653
+ shutdownAccepted = false;
654
+ if (acquiredLease && maintenanceLeaseToken) {
655
+ maintenanceLease.release(maintenanceLeaseToken);
656
+ }
657
+ admissionGate.reopen();
658
+ warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
659
+ });
660
+ }
661
+ catch (error) {
662
+ shutdownAccepted = false;
663
+ if (acquiredLease && maintenanceLeaseToken) {
664
+ maintenanceLease.release(maintenanceLeaseToken);
665
+ }
666
+ admissionGate.reopen();
667
+ throw error;
668
+ }
669
+ return maintenanceLeaseToken
670
+ ? { outcome: "accepted", maintenanceLease: "active" }
671
+ : { outcome: "accepted" };
672
+ })();
673
+ shutdownAttempt = attempt;
674
+ void attempt.finally(() => {
675
+ if (shutdownAttempt === attempt) {
676
+ shutdownAttempt = undefined;
677
+ shutdownAttemptLeaseToken = undefined;
678
+ }
679
+ }).catch(() => undefined);
680
+ return attempt;
681
+ };
561
682
  const daemonManagement = {
562
683
  managementToken,
563
684
  identity: () => ({
@@ -566,6 +687,14 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
566
687
  publicKey: installationIdentity.publicKey,
567
688
  }),
568
689
  buildStatus: daemonBuildStatus,
690
+ releaseMaintenanceLease: (token) => ({
691
+ outcome: (() => {
692
+ const outcome = maintenanceLease.release(token);
693
+ if (outcome === "released")
694
+ admissionGate.reopen();
695
+ return outcome;
696
+ })(),
697
+ }),
569
698
  pluginManagement: {
570
699
  listPlugins: () => pluginManagement.list(),
571
700
  setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
@@ -591,30 +720,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
591
720
  },
592
721
  browserArtifacts: browserArtifactManagement,
593
722
  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
- },
723
+ shutdownIfIdle,
618
724
  invokePluginCli,
619
725
  };
620
726
  const remoteRuntimeDispatcher = new RemoteRuntimeDispatcher({
@@ -691,6 +797,11 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
691
797
  registry: desktopBrowserHosts,
692
798
  daemonInstanceId,
693
799
  },
800
+ sessionPortalAuthorization: {
801
+ redeem: redeemSessionPortalTicket,
802
+ authorize: authorizeSessionPortalGrant,
803
+ ownsSession: sessionBelongsToPlugin,
804
+ },
694
805
  });
695
806
  baseShutdown = server.shutdown.bind(server);
696
807
  if (directConfig && directEncryptionIdentity) {
@@ -853,8 +964,10 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, writeE
853
964
  // A failed resource shutdown may leave plugin transports alive. Keep
854
965
  // the process-scoped owner lease in that fail-closed state; it will be
855
966
  // released by the OS when the process exits.
856
- if (shutdownComplete)
967
+ if (shutdownComplete) {
857
968
  await owner.release();
969
+ onShutdownComplete?.();
970
+ }
858
971
  }
859
972
  })();
860
973
  return shutdownPromise;
package/dist/db.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import Database from "better-sqlite3";
1
+ import { DatabaseSync } from "node:sqlite";
2
2
  /** Absolute path to the SQLite file (override via `RYNX_DB`, else `~/.rynx/rynx.db`). */
3
3
  export declare function dbPath(): string;
4
4
  /** Open (or reuse) the database at the current path, migrating + importing on first open. */
5
- export declare function db(): Database.Database;
5
+ export declare function db(): DatabaseSync;