@rynx-ai/daemon 0.1.11-beta.42 → 0.1.11-beta.44

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/plugin-channel-lark",
3
- "version": "0.1.11-beta.42",
3
+ "version": "0.1.11-beta.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -23,7 +23,6 @@ import { APP_BROWSER_HOST_BOOTSTRAP_TYPE, AppBrowserHostSupervisor, parseAppBrow
23
23
  import { buildControlDeps, listControlAgentModels } from "./control-deps.js";
24
24
  import { createBrowserArtifactManagementService } from "./browser-artifact-management.js";
25
25
  import { createDaemonBuildStatus } from "./daemon-build-status.js";
26
- import { MaintenanceLeaseStore } from "./maintenance-lease.js";
27
26
  import { BrowserRunnerSessionContextProvider } from "./browser-runner-session-context.js";
28
27
  import { removeLegacyPluginSkillSnapshot, snapshotEnabledPluginSkillSources, } from "./plugin-skill-context.js";
29
28
  import { installedDaemonVersion } from "./entry-path.js";
@@ -259,13 +258,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
259
258
  const appConfig = config ?? loadConfig();
260
259
  const admissionGate = new CloseableAdmissionGate();
261
260
  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();
261
+ const admissionOpen = () => !shutdownAccepted;
269
262
  const reserveAdmission = () => !admissionOpen()
270
263
  ? undefined
271
264
  : admissionGate.tryReserve();
@@ -606,35 +599,12 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
606
599
  });
607
600
  let requestServerShutdown;
608
601
  let shutdownAttempt;
609
- let shutdownAttemptLeaseToken;
610
- const shutdownIfIdle = (maintenanceLeaseToken) => {
611
- if (maintenanceLease.isActive()) {
612
- if (!maintenanceLeaseToken || !maintenanceLease.isHeldBy(maintenanceLeaseToken)) {
613
- return Promise.reject(new Error("a different maintenance lease is already active"));
614
- }
615
- // The previous daemon may have persisted the lease and exited after
616
- // its 202 response was lost. A replacement process holding the same
617
- // token must perform its own close/drain/shutdown; returning accepted
618
- // without scheduling this PID would strand the updater waiting for a
619
- // process that can never exit.
620
- }
602
+ const shutdownIfIdle = () => {
621
603
  if (shutdownAccepted) {
622
- if (maintenanceLeaseToken) {
623
- maintenanceLease.acquire(maintenanceLeaseToken);
624
- return Promise.resolve({
625
- outcome: "accepted",
626
- maintenanceLease: "active",
627
- });
628
- }
629
604
  return Promise.resolve({ outcome: "accepted" });
630
605
  }
631
- if (shutdownAttempt) {
632
- if (maintenanceLeaseToken === shutdownAttemptLeaseToken) {
633
- return shutdownAttempt;
634
- }
635
- return Promise.reject(new Error("another idle shutdown check is already in progress"));
636
- }
637
- shutdownAttemptLeaseToken = maintenanceLeaseToken;
606
+ if (shutdownAttempt)
607
+ return shutdownAttempt;
638
608
  const attempt = (async () => {
639
609
  await admissionGate.closeAndDrain();
640
610
  const runtimeActivity = sessionRuntimeServices.sessionRuntimeIndex.activitySnapshot();
@@ -658,37 +628,25 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
658
628
  admissionGate.reopen();
659
629
  throw new Error("idle daemon shutdown was requested before server lifecycle was ready");
660
630
  }
661
- const acquiredLease = maintenanceLeaseToken
662
- ? maintenanceLease.acquire(maintenanceLeaseToken)
663
- : false;
664
631
  shutdownAccepted = true;
665
632
  try {
666
633
  void shutdown().catch((error) => {
667
634
  shutdownAccepted = false;
668
- if (acquiredLease && maintenanceLeaseToken) {
669
- maintenanceLease.release(maintenanceLeaseToken);
670
- }
671
635
  admissionGate.reopen();
672
636
  warn(`idle daemon shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
673
637
  });
674
638
  }
675
639
  catch (error) {
676
640
  shutdownAccepted = false;
677
- if (acquiredLease && maintenanceLeaseToken) {
678
- maintenanceLease.release(maintenanceLeaseToken);
679
- }
680
641
  admissionGate.reopen();
681
642
  throw error;
682
643
  }
683
- return maintenanceLeaseToken
684
- ? { outcome: "accepted", maintenanceLease: "active" }
685
- : { outcome: "accepted" };
644
+ return { outcome: "accepted" };
686
645
  })();
687
646
  shutdownAttempt = attempt;
688
647
  void attempt.finally(() => {
689
648
  if (shutdownAttempt === attempt) {
690
649
  shutdownAttempt = undefined;
691
- shutdownAttemptLeaseToken = undefined;
692
650
  }
693
651
  }).catch(() => undefined);
694
652
  return attempt;
@@ -701,14 +659,6 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
701
659
  publicKey: installationIdentity.publicKey,
702
660
  }),
703
661
  buildStatus: daemonBuildStatus,
704
- releaseMaintenanceLease: (token) => ({
705
- outcome: (() => {
706
- const outcome = maintenanceLease.release(token);
707
- if (outcome === "released")
708
- admissionGate.reopen();
709
- return outcome;
710
- })(),
711
- }),
712
662
  pluginManagement: {
713
663
  listPlugins: () => pluginManagement.list(),
714
664
  setPluginEnabled: (pluginId, enabled) => pluginManagement.setEnabled(pluginId, enabled),
@@ -914,6 +864,7 @@ export async function startRynxDaemonServer({ config, warn = defaultWarn, onShut
914
864
  server.shutdown = () => {
915
865
  if (shutdownPromise)
916
866
  return shutdownPromise;
867
+ shutdownAccepted = true;
917
868
  // Fence Local admission without closing the Desktop Browser Host WS.
918
869
  // Browser handles can therefore close gracefully before full teardown.
919
870
  server.stopAccepting();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rynx-ai/daemon",
3
- "version": "0.1.11-beta.42",
3
+ "version": "0.1.11-beta.44",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/rynx-ai/rynx.git",
@@ -51,18 +51,18 @@
51
51
  "tar": "^7.5.19",
52
52
  "undici": "^7.28.0",
53
53
  "ws": "^8.21.0",
54
- "@rynx-ai/core": "0.1.11-beta.42",
55
- "@rynx-ai/emulator": "0.1.11-beta.42",
56
- "@rynx-ai/plugin-runner": "0.1.11-beta.42",
57
- "@rynx-ai/plugin-sdk": "0.1.11-beta.42",
58
- "@rynx-ai/protocol": "0.1.11-beta.42",
59
- "@rynx-ai/remote-runtime-client": "0.1.11-beta.42",
60
- "@rynx-ai/server": "0.1.11-beta.42"
54
+ "@rynx-ai/core": "0.1.11-beta.44",
55
+ "@rynx-ai/emulator": "0.1.11-beta.44",
56
+ "@rynx-ai/plugin-runner": "0.1.11-beta.44",
57
+ "@rynx-ai/plugin-sdk": "0.1.11-beta.44",
58
+ "@rynx-ai/protocol": "0.1.11-beta.44",
59
+ "@rynx-ai/remote-runtime-client": "0.1.11-beta.44",
60
+ "@rynx-ai/server": "0.1.11-beta.44"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/ws": "^8.18.1",
64
- "@rynx-ai/plugin-channel-lark": "0.1.11-beta.42",
65
- "@rynx-ai/browser-cdp": "0.1.11-beta.42"
64
+ "@rynx-ai/plugin-channel-lark": "0.1.11-beta.44",
65
+ "@rynx-ai/browser-cdp": "0.1.11-beta.44"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "rm -rf dist bundled-plugins && tsc -p tsconfig.json && chmod +x dist/index-daemon.js && node ../../scripts/stage-bundled-plugins.mjs",
@@ -1,39 +0,0 @@
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
- }
@@ -1,263 +0,0 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
2
- import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { rynxHome } from "@rynx-ai/core";
5
- const LEASE_SCHEMA_VERSION = 1;
6
- // The lease is acquired only after shutdown-if-idle has closed admission and
7
- // confirmed the daemon is idle. Keep a bounded cutover/rollback window without
8
- // fencing a recovered daemon for hours when an updater disappears.
9
- const DEFAULT_LEASE_TTL_MS = 15 * 60 * 1_000;
10
- // Accept the previous production bound during rolling upgrades, then shorten
11
- // it on load. Reject anything beyond that historical contract as corrupt.
12
- const LEGACY_MAX_LEASE_TTL_MS = 30 * 60 * 60 * 1_000;
13
- const EXPIRY_RETRY_MS = 1_000;
14
- const MAX_TIMER_DELAY_MS = 2_147_483_647;
15
- const LEASE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,256}$/;
16
- export function maintenanceLeaseFile(env = process.env) {
17
- return join(rynxHome(env), "maintenance-lease.json");
18
- }
19
- /**
20
- * Cross-process admission fence used only during daemon replacement.
21
- *
22
- * The raw bearer token is never persisted. The bounded expiry means a crashed
23
- * upgrader cannot leave the daemon permanently unavailable, while atomic rename
24
- * ensures a replacement daemon sees either the complete lease or no lease.
25
- */
26
- export class MaintenanceLeaseStore {
27
- filePath;
28
- now;
29
- ttlMs;
30
- syncDirectory;
31
- onExpired;
32
- lease;
33
- expiryTimer;
34
- constructor(options = {}) {
35
- this.filePath = options.filePath ?? maintenanceLeaseFile();
36
- this.now = options.now ?? (() => Date.now());
37
- this.ttlMs = options.ttlMs ?? DEFAULT_LEASE_TTL_MS;
38
- this.syncDirectory = options.syncDirectory ?? fsyncDirectory;
39
- this.onExpired = options.onExpired ?? (() => undefined);
40
- if (!Number.isSafeInteger(this.ttlMs) || this.ttlMs <= 0) {
41
- throw new Error("maintenance lease TTL must be a positive safe integer");
42
- }
43
- this.lease = this.readPersistedLease();
44
- this.scheduleExpiry();
45
- }
46
- isActive() {
47
- if (!this.lease)
48
- return false;
49
- if (this.lease.expiresAt > this.now())
50
- return true;
51
- this.expire();
52
- return false;
53
- }
54
- isHeldBy(token) {
55
- let tokenHash;
56
- try {
57
- tokenHash = hashLeaseToken(token);
58
- }
59
- catch {
60
- return false;
61
- }
62
- return Boolean(this.isActive()
63
- && this.lease
64
- && hashesEqual(this.lease.tokenHash, tokenHash));
65
- }
66
- acquire(token) {
67
- const tokenHash = hashLeaseToken(token);
68
- if (this.isActive()) {
69
- if (this.lease && hashesEqual(this.lease.tokenHash, tokenHash)) {
70
- const createdAt = this.now();
71
- const renewed = {
72
- schemaVersion: LEASE_SCHEMA_VERSION,
73
- tokenHash,
74
- createdAt,
75
- expiresAt: createdAt + this.ttlMs,
76
- };
77
- // Fail closed in memory if persistence reaches an indeterminate I/O
78
- // outcome; the caller sees the error, but this process must not reopen
79
- // admission around a lease file that may already have been renamed.
80
- this.lease = renewed;
81
- try {
82
- this.writePersistedLease(renewed);
83
- }
84
- finally {
85
- this.scheduleExpiry();
86
- }
87
- return false;
88
- }
89
- throw new Error("a different maintenance lease is already active");
90
- }
91
- const createdAt = this.now();
92
- const lease = {
93
- schemaVersion: LEASE_SCHEMA_VERSION,
94
- tokenHash,
95
- createdAt,
96
- expiresAt: createdAt + this.ttlMs,
97
- };
98
- this.lease = lease;
99
- try {
100
- this.writePersistedLease(lease);
101
- }
102
- finally {
103
- this.scheduleExpiry();
104
- }
105
- return true;
106
- }
107
- release(token) {
108
- if (!this.isActive())
109
- return "not_active";
110
- const lease = this.lease;
111
- if (!lease)
112
- return "not_active";
113
- let tokenHash;
114
- try {
115
- tokenHash = hashLeaseToken(token);
116
- }
117
- catch {
118
- return "denied";
119
- }
120
- if (!hashesEqual(lease.tokenHash, tokenHash))
121
- return "denied";
122
- this.clear();
123
- return "released";
124
- }
125
- readPersistedLease() {
126
- let parsed;
127
- try {
128
- parsed = JSON.parse(readFileSync(this.filePath, "utf8"));
129
- }
130
- catch (error) {
131
- if (error
132
- && typeof error === "object"
133
- && "code" in error
134
- && error.code === "ENOENT")
135
- return undefined;
136
- throw new Error("maintenance lease state is unreadable", { cause: error });
137
- }
138
- if (!isPersistedLease(parsed, Math.max(this.ttlMs, LEGACY_MAX_LEASE_TTL_MS))) {
139
- throw new Error("maintenance lease state is invalid");
140
- }
141
- const now = this.now();
142
- if (parsed.expiresAt <= now) {
143
- this.clearFile();
144
- return undefined;
145
- }
146
- if (parsed.expiresAt - parsed.createdAt > this.ttlMs) {
147
- const shortened = {
148
- ...parsed,
149
- createdAt: now,
150
- expiresAt: Math.min(parsed.expiresAt, now + this.ttlMs),
151
- };
152
- this.writePersistedLease(shortened);
153
- return shortened;
154
- }
155
- return parsed;
156
- }
157
- writePersistedLease(lease) {
158
- const parent = dirname(this.filePath);
159
- mkdirSync(parent, { recursive: true, mode: 0o700 });
160
- const temporary = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
161
- let descriptor;
162
- try {
163
- descriptor = openSync(temporary, "wx", 0o600);
164
- writeFileSync(descriptor, `${JSON.stringify(lease)}\n`, "utf8");
165
- fsyncSync(descriptor);
166
- closeSync(descriptor);
167
- descriptor = undefined;
168
- renameSync(temporary, this.filePath);
169
- this.syncDirectory(parent);
170
- }
171
- finally {
172
- if (descriptor !== undefined)
173
- closeSync(descriptor);
174
- rmSync(temporary, { force: true });
175
- }
176
- }
177
- clear() {
178
- this.clearFile();
179
- this.lease = undefined;
180
- this.cancelExpiry();
181
- }
182
- clearFile() {
183
- rmSync(this.filePath, { force: true });
184
- this.syncDirectory(dirname(this.filePath));
185
- }
186
- expire() {
187
- this.clear();
188
- this.onExpired();
189
- }
190
- scheduleExpiry(delayOverride) {
191
- this.cancelExpiry();
192
- if (!this.lease)
193
- return;
194
- const delay = delayOverride ?? Math.max(0, this.lease.expiresAt - this.now());
195
- this.expiryTimer = setTimeout(() => {
196
- this.expiryTimer = undefined;
197
- if (!this.lease)
198
- return;
199
- if (this.lease.expiresAt > this.now()) {
200
- this.scheduleExpiry();
201
- return;
202
- }
203
- try {
204
- this.expire();
205
- }
206
- catch {
207
- // Keep the in-memory fence and retry cleanup. A failed durability
208
- // operation must not turn an expired lease into open admission.
209
- this.scheduleExpiry(EXPIRY_RETRY_MS);
210
- }
211
- }, Math.min(delay, MAX_TIMER_DELAY_MS));
212
- this.expiryTimer.unref?.();
213
- }
214
- cancelExpiry() {
215
- if (this.expiryTimer)
216
- clearTimeout(this.expiryTimer);
217
- this.expiryTimer = undefined;
218
- }
219
- }
220
- function fsyncDirectory(directory) {
221
- let descriptor;
222
- try {
223
- descriptor = openSync(directory, "r");
224
- fsyncSync(descriptor);
225
- }
226
- catch (error) {
227
- if (error
228
- && typeof error === "object"
229
- && "code" in error
230
- && error.code === "ENOENT")
231
- return;
232
- throw error;
233
- }
234
- finally {
235
- if (descriptor !== undefined)
236
- closeSync(descriptor);
237
- }
238
- }
239
- function hashLeaseToken(token) {
240
- if (!LEASE_TOKEN_PATTERN.test(token)) {
241
- throw new Error("maintenance lease token must be 32-256 base64url characters");
242
- }
243
- return createHash("sha256").update(token, "utf8").digest("hex");
244
- }
245
- function hashesEqual(left, right) {
246
- const leftBuffer = Buffer.from(left, "hex");
247
- const rightBuffer = Buffer.from(right, "hex");
248
- return leftBuffer.length === rightBuffer.length
249
- && timingSafeEqual(leftBuffer, rightBuffer);
250
- }
251
- function isPersistedLease(value, maximumTtlMs) {
252
- if (!value || typeof value !== "object")
253
- return false;
254
- const candidate = value;
255
- return candidate.schemaVersion === LEASE_SCHEMA_VERSION
256
- && typeof candidate.tokenHash === "string"
257
- && /^[a-f0-9]{64}$/.test(candidate.tokenHash)
258
- && Number.isSafeInteger(candidate.createdAt)
259
- && Number.isSafeInteger(candidate.expiresAt)
260
- && candidate.createdAt >= 0
261
- && candidate.expiresAt > candidate.createdAt
262
- && candidate.expiresAt - candidate.createdAt <= maximumTtlMs;
263
- }