@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
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
4
4
  import { MARKETPLACE_ID_PATTERN, pluginCliInvocationLeasePid, readPluginRuntimeLeases, removePluginRuntimeLease, } from "@rynx-ai/plugin-sdk";
5
5
  import { rynxHome } from "@rynx-ai/core";
6
6
  import { db } from "./db.js";
7
+ import { transaction } from "./sqlite.js";
7
8
  import { isBuiltInPluginMarketplace } from "./plugin-marketplace.js";
8
9
  import { detectPluginSource, inspectPluginPackage, materializePluginPackage, } from "./plugin-package.js";
9
10
  import { canonicalPluginId, getPlugin, removePlugin, upsertPlugin, } from "./plugin-store.js";
@@ -117,7 +118,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
117
118
  promotedByThisTransaction = true;
118
119
  }
119
120
  promotedPath = realpathSync(target);
120
- db().transaction(() => {
121
+ transaction(db(), () => {
121
122
  const current = getPlugin(canonicalId);
122
123
  if (!samePluginGeneration(existing, current)) {
123
124
  throw new Error(`plugin "${canonicalId}" changed concurrently; retry`);
@@ -172,7 +173,7 @@ export async function preparePluginInstallation(spec, onProgress = () => { }, op
172
173
  }
173
174
  export async function uninstallPlugin(identity, onProgress = () => { }, options = {}) {
174
175
  let record;
175
- const removed = db().transaction(() => {
176
+ const removed = transaction(db(), () => {
176
177
  record = getPlugin(identity);
177
178
  if (!record)
178
179
  return false;
@@ -180,6 +181,8 @@ export async function uninstallPlugin(identity, onProgress = () => { }, options
180
181
  throw new Error(`bundled plugin "${record.canonicalId}" cannot be uninstalled; disable it instead`);
181
182
  }
182
183
  assertNoActiveLeases(record);
184
+ if (options.purgeData)
185
+ assertNoOwnedSessions(record);
183
186
  return removePlugin(record.canonicalId);
184
187
  }).immediate();
185
188
  if (!removed)
@@ -193,9 +196,37 @@ export async function uninstallPlugin(identity, onProgress = () => { }, options
193
196
  onProgress(`uninstalled plugin "${record.canonicalId}"`);
194
197
  return true;
195
198
  }
199
+ function assertNoOwnedSessions(record) {
200
+ const row = db()
201
+ .prepare(`WITH RECURSIVE owned(id) AS (
202
+ SELECT session_id
203
+ FROM plugin_session_launch_operations
204
+ WHERE plugin_id = ?
205
+ UNION
206
+ SELECT id FROM sessions WHERE source = ?
207
+ UNION
208
+ SELECT sessions.id
209
+ FROM sessions
210
+ JOIN owned ON sessions.forked_from_session_id = owned.id
211
+ UNION
212
+ SELECT session_fork_operations.target_session_id
213
+ FROM session_fork_operations
214
+ JOIN owned ON session_fork_operations.source_session_id = owned.id
215
+ )
216
+ SELECT COUNT(*) AS count
217
+ FROM sessions
218
+ JOIN owned ON sessions.id = owned.id`)
219
+ .get(record.canonicalId, `plugin:${record.canonicalId}`);
220
+ if (row.count > 0) {
221
+ throw new Error(`cannot purge plugin "${record.canonicalId}": ${row.count} plugin-owned Session(s) still exist`);
222
+ }
223
+ }
196
224
  export function pruneInactivePluginInstallations() {
197
225
  const { cacheRoot } = ensureStorageRoots();
198
226
  const active = new Set(listActivePaths().map(canonicalFilesystemPath));
227
+ for (const leased of listActiveRuntimeLeasePaths(cacheRoot)) {
228
+ active.add(leased);
229
+ }
199
230
  const removed = [];
200
231
  for (const marketplace of safeDirectories(cacheRoot)) {
201
232
  for (const plugin of safeDirectories(join(cacheRoot, marketplace))) {
@@ -327,6 +358,35 @@ function listActivePaths() {
327
358
  }
328
359
  return paths;
329
360
  }
361
+ function listActiveRuntimeLeasePaths(cacheRoot) {
362
+ const dataRoot = join(rynxHome(), "plugins", "data");
363
+ const active = [];
364
+ for (const marketplace of safeDirectories(dataRoot)) {
365
+ for (const plugin of safeDirectories(join(dataRoot, marketplace))) {
366
+ const generationRoot = join(cacheRoot, marketplace, plugin);
367
+ const managedGenerations = new Set(safeDirectories(generationRoot).map((generation) => canonicalFilesystemPath(join(generationRoot, generation))));
368
+ if (managedGenerations.size === 0)
369
+ continue;
370
+ const dataDir = join(dataRoot, marketplace, plugin);
371
+ for (const lease of readPluginRuntimeLeases(dataDir)) {
372
+ // A lease is plugin-owned input. It may pin only the same plugin
373
+ // namespace from which it was read and only one exact host-owned,
374
+ // immutable generation directory.
375
+ if (lease.pluginId !== plugin)
376
+ continue;
377
+ const pid = pluginCliInvocationLeasePid(lease.leaseId);
378
+ if (pid !== undefined && !isProcessAlive(pid)) {
379
+ removePluginRuntimeLease(dataDir, lease.leaseId);
380
+ continue;
381
+ }
382
+ const installPath = canonicalFilesystemPath(lease.installPath);
383
+ if (managedGenerations.has(installPath))
384
+ active.push(installPath);
385
+ }
386
+ }
387
+ }
388
+ return active;
389
+ }
330
390
  function safeProgress(callback) {
331
391
  return (line) => {
332
392
  try {
@@ -295,7 +295,6 @@ function toManagementItem(record, settingsAvailable = false, updateState = "unkn
295
295
  enabled: record.enabled,
296
296
  runtimeState: record.manifest.runtime ? record.runtimeState : "absent",
297
297
  ...(record.runtimeError ? { runtimeError: record.runtimeError } : {}),
298
- capabilities: [...record.manifest.capabilities],
299
298
  hasSettingsUi: settingsAvailable,
300
299
  updatedAt: record.updatedAt,
301
300
  };
@@ -322,7 +321,6 @@ function toInstallPreparation(token, expiresAtMs, prepared) {
322
321
  subdirectory: prepared.requestedSource.subdir ?? ".",
323
322
  },
324
323
  digest: prepared.integrity,
325
- capabilities: [...prepared.manifest.capabilities],
326
324
  hasRuntime: Boolean(prepared.manifest.runtime),
327
325
  hasSettingsUi: Boolean(prepared.manifest.settings),
328
326
  ...(prepared.manifest.runtime ? {
@@ -3,6 +3,7 @@ import { isAbsolute, join, posix, relative, resolve, sep } from "node:path";
3
3
  import { MARKETPLACE_ID_PATTERN, parsePluginMarketplace, } from "@rynx-ai/plugin-sdk";
4
4
  import { rynxHome } from "@rynx-ai/core";
5
5
  import { db } from "./db.js";
6
+ import { transaction } from "./sqlite.js";
6
7
  import { detectPluginSource, materializePluginPackage, parsePluginSourceSpec, pluginSourceFingerprint, } from "./plugin-package.js";
7
8
  export function listPluginMarketplaces() {
8
9
  return db().prepare("SELECT * FROM plugin_marketplaces ORDER BY marketplace_id").all().map(toRecord);
@@ -27,7 +28,7 @@ export async function addPluginMarketplace(spec, options = {}) {
27
28
  throw new Error(`built-in marketplace "${marketplaceId}" is managed by Rynx`);
28
29
  }
29
30
  const now = new Date().toISOString();
30
- db().transaction(() => {
31
+ transaction(db(), () => {
31
32
  const binding = db().prepare(`SELECT source_fingerprint AS sourceFingerprint
32
33
  FROM plugin_marketplace_bindings
33
34
  WHERE marketplace_id = ?`).get(marketplaceId);
@@ -57,11 +57,11 @@ export function upsertPlugin(rec) {
57
57
  db().prepare(`INSERT INTO plugin_installations (
58
58
  canonical_id, marketplace_id, plugin_id, source_type, requested_source,
59
59
  resolved_revision, source_fingerprint, version, enabled, installed_at,
60
- updated_at, install_path, manifest, integrity, capabilities, runtime_state, runtime_error
60
+ updated_at, install_path, manifest, integrity, runtime_state, runtime_error
61
61
  ) VALUES (
62
62
  @canonicalId, @marketplaceId, @pluginId, @source, @requestedSource,
63
63
  @resolvedRevision, @sourceFingerprint, @version, @enabled, @now,
64
- @now, @installPath, @manifest, @integrity, @capabilities, 'stopped', NULL
64
+ @now, @installPath, @manifest, @integrity, 'stopped', NULL
65
65
  )
66
66
  ON CONFLICT(canonical_id) DO UPDATE SET
67
67
  source_type = excluded.source_type,
@@ -74,7 +74,6 @@ export function upsertPlugin(rec) {
74
74
  install_path = excluded.install_path,
75
75
  manifest = excluded.manifest,
76
76
  integrity = excluded.integrity,
77
- capabilities = excluded.capabilities,
78
77
  runtime_state = 'stopped',
79
78
  runtime_error = NULL`).run({
80
79
  canonicalId: rec.canonicalId,
@@ -90,7 +89,6 @@ export function upsertPlugin(rec) {
90
89
  installPath: rec.installPath,
91
90
  manifest: JSON.stringify(rec.manifest),
92
91
  integrity: rec.integrity,
93
- capabilities: JSON.stringify(rec.manifest.capabilities),
94
92
  });
95
93
  }
96
94
  export function setPluginEnabled(identity, enabled) {
@@ -20,6 +20,8 @@ export interface PluginSupervisorOptions {
20
20
  restartMaxMs?: number;
21
21
  healthPollMs?: number;
22
22
  daemonInstanceId?: string;
23
+ productVersion?: string;
24
+ cliVersion?: string;
23
25
  verifyPackage?: (record: RunnablePluginRecord) => Promise<void>;
24
26
  }
25
27
  export interface PluginRuntimeStatusSnapshot {
@@ -42,12 +44,15 @@ export declare class PluginSupervisor {
42
44
  private readonly spawnRunner;
43
45
  private readonly log;
44
46
  private readonly daemonInstanceId;
47
+ private readonly productVersion;
48
+ private readonly cliVersion;
45
49
  private readonly restartBaseMs;
46
50
  private readonly restartMaxMs;
47
51
  private readonly healthPollMs;
48
52
  private readonly verifyPackage;
49
53
  private services?;
50
54
  private disposed;
55
+ private disposePromise?;
51
56
  constructor(records: readonly RunnablePluginRecord[], options?: PluginSupervisorOptions);
52
57
  bindHostServices(services: PluginHostServices): void;
53
58
  startPluginRuntimes(): Promise<void>;
@@ -23,17 +23,22 @@ export class PluginSupervisor {
23
23
  spawnRunner;
24
24
  log;
25
25
  daemonInstanceId;
26
+ productVersion;
27
+ cliVersion;
26
28
  restartBaseMs;
27
29
  restartMaxMs;
28
30
  healthPollMs;
29
31
  verifyPackage;
30
32
  services;
31
33
  disposed = false;
34
+ disposePromise;
32
35
  constructor(records, options = {}) {
33
36
  this.services = options.services;
34
37
  this.spawnRunner = options.spawnRunner ?? spawnPluginRunner;
35
38
  this.log = options.log ?? (() => { });
36
39
  this.daemonInstanceId = options.daemonInstanceId ?? randomUUID();
40
+ this.productVersion = options.productVersion ?? "development";
41
+ this.cliVersion = options.cliVersion ?? "development";
37
42
  this.restartBaseMs = options.restartBaseMs ?? DEFAULT_RESTART_BASE_MS;
38
43
  this.restartMaxMs = Math.min(options.restartMaxMs ?? MAX_RESTART_DELAY_MS, MAX_RESTART_DELAY_MS);
39
44
  this.healthPollMs = options.healthPollMs ?? DEFAULT_HEALTH_POLL_MS;
@@ -211,13 +216,27 @@ export class PluginSupervisor {
211
216
  };
212
217
  }
213
218
  async dispose() {
214
- if (this.disposed)
215
- return;
219
+ if (this.disposePromise)
220
+ return this.disposePromise;
216
221
  this.disposed = true;
217
- await Promise.allSettled([...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, async () => {
218
- await this.stopState(state);
219
- await this.stopSettingsState(state);
220
- })));
222
+ const attempt = (async () => {
223
+ const stopped = await Promise.allSettled([...this.states.values()].map((state) => this.runSerialized(state.record.canonicalId, async () => {
224
+ await this.stopState(state);
225
+ await this.stopSettingsState(state);
226
+ })));
227
+ const failures = stopped
228
+ .filter((result) => result.status === "rejected")
229
+ .map((result) => result.reason);
230
+ if (failures.length > 0) {
231
+ throw new AggregateError(failures, "plugin supervisor could not stop every runtime");
232
+ }
233
+ })();
234
+ this.disposePromise = attempt;
235
+ void attempt.catch(() => {
236
+ if (this.disposePromise === attempt)
237
+ this.disposePromise = undefined;
238
+ });
239
+ return attempt;
221
240
  }
222
241
  upsertState(record) {
223
242
  const current = this.states.get(record.canonicalId);
@@ -237,7 +256,10 @@ export class PluginSupervisor {
237
256
  return state;
238
257
  }
239
258
  async startState(state) {
240
- if (this.disposed || state.runner || !state.record.enabled)
259
+ if (this.disposed
260
+ || state.runner
261
+ || blockingCleanupError(state)
262
+ || !state.record.enabled)
241
263
  return;
242
264
  const runtime = state.record.manifest.runtime;
243
265
  if (!runtime) {
@@ -270,7 +292,8 @@ export class PluginSupervisor {
270
292
  pluginId: state.record.pluginId,
271
293
  principalId: state.record.canonicalId,
272
294
  daemonInstanceId: this.daemonInstanceId,
273
- capabilities: state.record.manifest.capabilities,
295
+ productVersion: this.productVersion,
296
+ cliVersion: this.cliVersion,
274
297
  services: this.services,
275
298
  ...(state.record.pluginId === "workspace"
276
299
  && isBuiltInPluginMarketplace(state.record.marketplaceId)
@@ -300,7 +323,6 @@ export class PluginSupervisor {
300
323
  role: "runtime",
301
324
  dataDir,
302
325
  cacheDir,
303
- capabilities: state.record.manifest.capabilities,
304
326
  handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
305
327
  onStderr: (line) => this.log({
306
328
  level: "info",
@@ -333,8 +355,21 @@ export class PluginSupervisor {
333
355
  });
334
356
  }
335
357
  catch (error) {
336
- await hostRpc?.dispose().catch(() => undefined);
358
+ try {
359
+ await hostRpc?.dispose();
360
+ }
361
+ catch (cleanupError) {
362
+ const failure = new AggregateError([error, cleanupError], "plugin runtime startup failed and active-run cleanup was unproven");
363
+ state.runtimeCleanupError = failure;
364
+ this.failState(state, failure);
365
+ return;
366
+ }
337
367
  state.hostRpc = undefined;
368
+ if (isUnprovenProcessCleanup(error)) {
369
+ state.runtimeCleanupError = error;
370
+ this.failState(state, error);
371
+ return;
372
+ }
338
373
  removePluginRuntimeLease(dataDir, RUNTIME_LEASE_ID);
339
374
  this.failState(state, error);
340
375
  this.scheduleRestart(state);
@@ -343,6 +378,9 @@ export class PluginSupervisor {
343
378
  async startSettingsState(state) {
344
379
  if (this.disposed)
345
380
  throw new Error("plugin supervisor is disposed");
381
+ const cleanupError = blockingCleanupError(state);
382
+ if (cleanupError)
383
+ throw cleanupError;
346
384
  if (state.settingsRunner)
347
385
  return state.settingsRunner;
348
386
  const background = state.record.manifest.settings?.background;
@@ -369,7 +407,8 @@ export class PluginSupervisor {
369
407
  pluginId: state.record.pluginId,
370
408
  principalId: state.record.canonicalId,
371
409
  daemonInstanceId: this.daemonInstanceId,
372
- capabilities: state.record.manifest.capabilities,
410
+ productVersion: this.productVersion,
411
+ cliVersion: this.cliVersion,
373
412
  services: this.services,
374
413
  ...(state.record.pluginId === "workspace"
375
414
  && isBuiltInPluginMarketplace(state.record.marketplaceId)
@@ -398,7 +437,6 @@ export class PluginSupervisor {
398
437
  role: "settings",
399
438
  dataDir,
400
439
  cacheDir,
401
- capabilities: state.record.manifest.capabilities,
402
440
  handleHostCall: (method, params, signal) => hostRpc.call(method, params, signal),
403
441
  onStderr: (line) => this.log({
404
442
  level: "info",
@@ -429,8 +467,32 @@ export class PluginSupervisor {
429
467
  return runner;
430
468
  }
431
469
  catch (error) {
432
- await runner?.close().catch(() => undefined);
433
- await hostRpc?.dispose().catch(() => undefined);
470
+ const cleanupErrors = [];
471
+ if (runner) {
472
+ try {
473
+ await runner.close();
474
+ }
475
+ catch (cleanupError) {
476
+ state.settingsRunner = runner;
477
+ cleanupErrors.push(cleanupError);
478
+ }
479
+ }
480
+ try {
481
+ await hostRpc?.dispose();
482
+ }
483
+ catch (cleanupError) {
484
+ state.settingsHostRpc = hostRpc;
485
+ cleanupErrors.push(cleanupError);
486
+ }
487
+ if (cleanupErrors.length > 0) {
488
+ const failure = new AggregateError([error, ...cleanupErrors], "plugin settings startup failed and cleanup was unproven");
489
+ state.settingsCleanupError = failure;
490
+ throw failure;
491
+ }
492
+ if (isUnprovenProcessCleanup(error)) {
493
+ state.settingsCleanupError = error;
494
+ throw error;
495
+ }
434
496
  removePluginRuntimeLease(dataDir, SETTINGS_LEASE_ID);
435
497
  throw error;
436
498
  }
@@ -438,20 +500,71 @@ export class PluginSupervisor {
438
500
  async stopSettingsState(state) {
439
501
  const runner = state.settingsRunner;
440
502
  const hostRpc = state.settingsHostRpc;
441
- state.settingsRunner = undefined;
442
- state.settingsHostRpc = undefined;
443
- await runner?.close().catch(() => undefined);
444
- await hostRpc?.dispose().catch(() => undefined);
503
+ if (state.settingsCleanupError && !runner && !hostRpc) {
504
+ throw state.settingsCleanupError;
505
+ }
506
+ const errors = [];
507
+ if (runner) {
508
+ try {
509
+ await runner.close();
510
+ if (state.settingsRunner === runner)
511
+ state.settingsRunner = undefined;
512
+ }
513
+ catch (error) {
514
+ errors.push(error);
515
+ }
516
+ }
517
+ if (hostRpc) {
518
+ try {
519
+ await hostRpc.dispose();
520
+ if (state.settingsHostRpc === hostRpc)
521
+ state.settingsHostRpc = undefined;
522
+ }
523
+ catch (error) {
524
+ errors.push(error);
525
+ }
526
+ }
527
+ if (errors.length > 0) {
528
+ const failure = cleanupFailure(errors, "plugin settings cleanup was unproven");
529
+ state.settingsCleanupError = failure;
530
+ throw failure;
531
+ }
532
+ state.settingsCleanupError = undefined;
445
533
  removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
446
534
  }
447
535
  async handleSettingsExit(state, runner, hostRpc, exit) {
448
536
  if (state.settingsRunner !== runner)
449
537
  return;
538
+ try {
539
+ await runner.terminateTree();
540
+ }
541
+ catch (error) {
542
+ state.settingsCleanupError = error;
543
+ this.log({
544
+ level: "error",
545
+ event: "plugin-settings-exited",
546
+ pluginId: state.record.canonicalId,
547
+ message: error instanceof Error ? error.message : String(error),
548
+ });
549
+ return;
550
+ }
450
551
  state.settingsRunner = undefined;
451
- if (state.settingsHostRpc === hostRpc)
452
- state.settingsHostRpc = undefined;
453
- await runner.terminateTree().catch(() => undefined);
454
- await hostRpc.dispose().catch(() => undefined);
552
+ try {
553
+ await hostRpc.dispose();
554
+ if (state.settingsHostRpc === hostRpc)
555
+ state.settingsHostRpc = undefined;
556
+ }
557
+ catch (error) {
558
+ state.settingsCleanupError = error;
559
+ this.log({
560
+ level: "error",
561
+ event: "plugin-settings-exited",
562
+ pluginId: state.record.canonicalId,
563
+ message: error instanceof Error ? error.message : String(error),
564
+ });
565
+ return;
566
+ }
567
+ state.settingsCleanupError = undefined;
455
568
  removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), SETTINGS_LEASE_ID);
456
569
  if (!this.disposed) {
457
570
  this.log({
@@ -473,10 +586,38 @@ export class PluginSupervisor {
473
586
  clearTimeout(state.healthTimer);
474
587
  state.healthTimer = undefined;
475
588
  const runner = state.runner;
476
- state.runner = undefined;
477
- await runner?.close().catch(() => undefined);
478
- await state.hostRpc?.dispose().catch(() => undefined);
479
- state.hostRpc = undefined;
589
+ const hostRpc = state.hostRpc;
590
+ if (state.runtimeCleanupError && !runner && !hostRpc) {
591
+ throw state.runtimeCleanupError;
592
+ }
593
+ const errors = [];
594
+ if (runner) {
595
+ try {
596
+ await runner.close();
597
+ if (state.runner === runner)
598
+ state.runner = undefined;
599
+ }
600
+ catch (error) {
601
+ errors.push(error);
602
+ }
603
+ }
604
+ if (hostRpc) {
605
+ try {
606
+ await hostRpc.dispose();
607
+ if (state.hostRpc === hostRpc)
608
+ state.hostRpc = undefined;
609
+ }
610
+ catch (error) {
611
+ errors.push(error);
612
+ }
613
+ }
614
+ if (errors.length > 0) {
615
+ const failure = cleanupFailure(errors, "plugin runtime cleanup was unproven");
616
+ state.runtimeCleanupError = failure;
617
+ this.failState(state, failure);
618
+ throw failure;
619
+ }
620
+ state.runtimeCleanupError = undefined;
480
621
  removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
481
622
  state.health = undefined;
482
623
  state.error = undefined;
@@ -489,10 +630,26 @@ export class PluginSupervisor {
489
630
  if (state.healthTimer)
490
631
  clearTimeout(state.healthTimer);
491
632
  state.healthTimer = undefined;
633
+ try {
634
+ await runner.terminateTree();
635
+ }
636
+ catch (error) {
637
+ state.runtimeCleanupError = error;
638
+ this.failState(state, error);
639
+ return;
640
+ }
492
641
  state.runner = undefined;
493
- await runner.terminateTree().catch(() => undefined);
494
- await state.hostRpc?.dispose().catch(() => undefined);
495
- state.hostRpc = undefined;
642
+ try {
643
+ await state.hostRpc?.dispose();
644
+ state.hostRpc = undefined;
645
+ }
646
+ catch (error) {
647
+ state.runtimeCleanupError = error;
648
+ if (!state.stopping && !this.disposed)
649
+ this.failState(state, error);
650
+ return;
651
+ }
652
+ state.runtimeCleanupError = undefined;
496
653
  removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
497
654
  if (state.stopping || this.disposed)
498
655
  return;
@@ -571,13 +728,29 @@ export class PluginSupervisor {
571
728
  async failRunningState(state, runner, error) {
572
729
  if (state.runner !== runner)
573
730
  return;
574
- state.runner = undefined;
575
731
  if (state.healthTimer)
576
732
  clearTimeout(state.healthTimer);
577
733
  state.healthTimer = undefined;
578
- await runner.close().catch(() => undefined);
579
- await state.hostRpc?.dispose().catch(() => undefined);
580
- state.hostRpc = undefined;
734
+ try {
735
+ await runner.close();
736
+ }
737
+ catch (cleanupError) {
738
+ state.runtimeCleanupError = cleanupError;
739
+ this.failState(state, cleanupError);
740
+ return;
741
+ }
742
+ state.runner = undefined;
743
+ try {
744
+ await state.hostRpc?.dispose();
745
+ state.hostRpc = undefined;
746
+ }
747
+ catch (cleanupError) {
748
+ state.runtimeCleanupError = cleanupError;
749
+ if (!state.stopping && !this.disposed)
750
+ this.failState(state, cleanupError);
751
+ return;
752
+ }
753
+ state.runtimeCleanupError = undefined;
581
754
  removePluginRuntimeLease(pluginDataDir(state.record.marketplaceId, state.record.pluginId), RUNTIME_LEASE_ID);
582
755
  if (state.stopping || this.disposed)
583
756
  return;
@@ -595,6 +768,18 @@ export class PluginSupervisor {
595
768
  return current;
596
769
  }
597
770
  }
771
+ function cleanupFailure(errors, message) {
772
+ return errors.length === 1 ? errors[0] : new AggregateError(errors, message);
773
+ }
774
+ function blockingCleanupError(state) {
775
+ return state.runtimeCleanupError ?? state.settingsCleanupError;
776
+ }
777
+ function isUnprovenProcessCleanup(error) {
778
+ return Boolean(error
779
+ && typeof error === "object"
780
+ && "code" in error
781
+ && error.code === "PROCESS_TREE_STUCK");
782
+ }
598
783
  function healthState(health) {
599
784
  return health.state;
600
785
  }
package/dist/pm2.d.ts CHANGED
@@ -8,12 +8,21 @@ export declare function formatControlUrl(host: string, port: number): string;
8
8
  * entry from scratch (delete + start) so config flags are always fresh; the
9
9
  * `force === false` path short-circuits when it's already online.
10
10
  */
11
- export declare function startDaemon({ force, quiet }: {
11
+ export declare function startDaemon({ force, quiet, launchEnvironment, }: {
12
12
  force: boolean;
13
13
  quiet?: boolean;
14
+ launchEnvironment?: NodeJS.ProcessEnv;
14
15
  }): number;
15
16
  export declare function isPm2DaemonOnline(): boolean;
16
- /** Stop and remove the daemon from pm2's process list. */
17
+ /**
18
+ * Stop the daemon and terminate its dedicated pm2 supervisor.
19
+ *
20
+ * PM2_HOME lives below RYNX_HOME, so this supervisor cannot own unrelated
21
+ * applications. Leaving it alive across an npm replacement is unsafe: the
22
+ * daemon process container path is resolved inside the old pm2 package. Once
23
+ * npm swaps @rynx-ai/daemon, that cached path may no longer exist and every
24
+ * subsequent `rynx start` enters a restart loop before our code can run.
25
+ */
17
26
  export declare function stopDaemon(): number;
18
27
  /** Print a one-line status; exit 0 only when online. */
19
28
  export declare function statusDaemon(): number;