@rynx-ai/daemon 0.1.0 → 0.1.9

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 (89) hide show
  1. package/dist/browser-cli-args.d.ts +20 -0
  2. package/dist/browser-cli-args.js +100 -0
  3. package/dist/browser-runner-session-context.d.ts +24 -0
  4. package/dist/browser-runner-session-context.js +113 -0
  5. package/dist/cdp-keyboard-input.d.ts +6 -0
  6. package/dist/cdp-keyboard-input.js +209 -0
  7. package/dist/channel-store.d.ts +0 -6
  8. package/dist/channel-store.js +2 -18
  9. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  10. package/dist/chrome-for-testing-release-resolver.js +214 -0
  11. package/dist/chrome-for-testing-store.d.ts +85 -0
  12. package/dist/chrome-for-testing-store.js +836 -0
  13. package/dist/cli.js +467 -30
  14. package/dist/control-client.d.ts +113 -0
  15. package/dist/control-client.js +792 -0
  16. package/dist/control-deps.js +37 -6
  17. package/dist/control-endpoint.d.ts +14 -0
  18. package/dist/control-endpoint.js +124 -0
  19. package/dist/control-link-store.d.ts +16 -0
  20. package/dist/control-link-store.js +53 -0
  21. package/dist/daemon-owner.d.ts +14 -0
  22. package/dist/daemon-owner.js +104 -0
  23. package/dist/daemon-server.d.ts +12 -2
  24. package/dist/daemon-server.js +559 -28
  25. package/dist/db.js +140 -10
  26. package/dist/desktop-browser-host-client.d.ts +34 -0
  27. package/dist/desktop-browser-host-client.js +413 -0
  28. package/dist/direct-runtime-authenticator.d.ts +13 -0
  29. package/dist/direct-runtime-authenticator.js +127 -0
  30. package/dist/direct-runtime-config.d.ts +20 -0
  31. package/dist/direct-runtime-config.js +101 -0
  32. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  33. package/dist/embedded-browser-profile-cleanup.js +338 -0
  34. package/dist/headless-browser-host.d.ts +14 -0
  35. package/dist/headless-browser-host.js +2156 -0
  36. package/dist/index-daemon.js +28 -5
  37. package/dist/installation-id.d.ts +33 -0
  38. package/dist/installation-id.js +692 -0
  39. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  40. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  41. package/dist/plugin-cli.d.ts +30 -0
  42. package/dist/plugin-cli.js +270 -0
  43. package/dist/plugin-host-rpc.d.ts +85 -0
  44. package/dist/plugin-host-rpc.js +878 -0
  45. package/dist/plugin-install-restart.d.ts +13 -0
  46. package/dist/plugin-install-restart.js +24 -0
  47. package/dist/plugin-installer.d.ts +36 -15
  48. package/dist/plugin-installer.js +420 -155
  49. package/dist/plugin-package.d.ts +38 -0
  50. package/dist/plugin-package.js +553 -0
  51. package/dist/plugin-runtime-control.d.ts +2 -0
  52. package/dist/plugin-runtime-control.js +37 -0
  53. package/dist/plugin-store.d.ts +37 -8
  54. package/dist/plugin-store.js +80 -15
  55. package/dist/plugin-supervisor.d.ts +160 -0
  56. package/dist/plugin-supervisor.js +1143 -0
  57. package/dist/pm2.d.ts +3 -1
  58. package/dist/pm2.js +55 -13
  59. package/dist/provider-cli-service.d.ts +46 -0
  60. package/dist/provider-cli-service.js +510 -0
  61. package/dist/registry.d.ts +6 -19
  62. package/dist/registry.js +22 -87
  63. package/dist/remote-runtime-access-store.d.ts +61 -0
  64. package/dist/remote-runtime-access-store.js +386 -0
  65. package/dist/remote-runtime-admin.d.ts +32 -0
  66. package/dist/remote-runtime-admin.js +107 -0
  67. package/dist/remote-runtime-connection-manager.d.ts +92 -0
  68. package/dist/remote-runtime-connection-manager.js +1409 -0
  69. package/dist/remote-runtime-credential-store.d.ts +45 -0
  70. package/dist/remote-runtime-credential-store.js +639 -0
  71. package/dist/remote-runtime-target-control.d.ts +47 -0
  72. package/dist/remote-runtime-target-control.js +433 -0
  73. package/dist/remote-runtime-target-store.d.ts +53 -0
  74. package/dist/remote-runtime-target-store.js +307 -0
  75. package/dist/runtime-share-addresses.d.ts +21 -0
  76. package/dist/runtime-share-addresses.js +50 -0
  77. package/dist/session-browser-capability-store.d.ts +13 -0
  78. package/dist/session-browser-capability-store.js +58 -0
  79. package/dist/session-emulator-binding-store.d.ts +10 -0
  80. package/dist/session-emulator-binding-store.js +61 -0
  81. package/dist/session-launch-operations.d.ts +41 -0
  82. package/dist/session-launch-operations.js +124 -0
  83. package/dist/session-meta-store.d.ts +3 -2
  84. package/dist/session-meta-store.js +57 -7
  85. package/dist/setup.js +9 -4
  86. package/dist/skills-catalog.js +8 -4
  87. package/dist/update.d.ts +11 -2
  88. package/dist/update.js +0 -27
  89. package/package.json +18 -10
@@ -0,0 +1,1143 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import path from "node:path";
3
+ import { resolveAllowedRoots, rynxHome, } from "@rynx-ai/core";
4
+ import { PLUGIN_RUNNER_API_VERSION, spawnPluginRunner, } from "@rynx-ai/plugin-runner";
5
+ import { normalizePluginConfig, } from "@rynx-ai/plugin-sdk";
6
+ import { MAX_PLUGIN_HOST_EVENT_BYTES, PluginHostRpc, } from "./plugin-host-rpc.js";
7
+ const DEFAULT_RESTART_BASE_MS = 1_000;
8
+ const MAX_RESTART_DELAY_MS = 30_000;
9
+ const DEFAULT_STABLE_RESET_MS = 60_000;
10
+ const MAX_HOST_EVENT_QUEUE_ITEMS = 64;
11
+ const MAX_HOST_EVENT_QUEUE_BYTES = 8 * 1024 * 1024;
12
+ const MAX_STDERR_LINE_BYTES = 64 * 1024;
13
+ const MAX_STATUS_ERROR_BYTES = 16 * 1024;
14
+ const DEFAULT_READY_TIMEOUT_MS = 15_000;
15
+ const MAX_READY_TIMEOUT_MS = 60_000;
16
+ const RUNTIME_STATUS_POLL_MS = 50;
17
+ const TARGET_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
18
+ /**
19
+ * Owns isolated plugin child processes and projects their static chat-channel
20
+ * declarations onto trusted host-side ChannelManager handles.
21
+ *
22
+ * There is at most one resident child per enabled plugin. Chat-channel
23
+ * instances are independent desired-state entries inside that child, so
24
+ * stopping the final channel does not tear down the plugin runtime itself.
25
+ */
26
+ export class PluginSupervisor {
27
+ states = new Map();
28
+ services;
29
+ spawnRunner;
30
+ logger;
31
+ restartBaseMs;
32
+ restartMaxMs;
33
+ stableResetMs;
34
+ daemonInstanceId;
35
+ runtimeOperations = new Map();
36
+ disposed = false;
37
+ constructor(plugins, options) {
38
+ this.services = options.services;
39
+ this.spawnRunner = options.spawnRunner ?? spawnPluginRunner;
40
+ this.logger = options.log;
41
+ this.restartBaseMs = Math.min(positiveDelay(options.restartBaseMs, DEFAULT_RESTART_BASE_MS), MAX_RESTART_DELAY_MS);
42
+ this.restartMaxMs = Math.max(this.restartBaseMs, Math.min(positiveDelay(options.restartMaxMs, MAX_RESTART_DELAY_MS), MAX_RESTART_DELAY_MS));
43
+ this.stableResetMs = positiveDelay(options.stableResetMs, DEFAULT_STABLE_RESET_MS);
44
+ this.daemonInstanceId = options.daemonInstanceId?.trim() || randomUUID();
45
+ for (const record of plugins) {
46
+ validateRecord(record);
47
+ if (this.states.has(record.manifest.id)) {
48
+ throw new Error(`duplicate runnable plugin id: ${record.manifest.id}`);
49
+ }
50
+ this.states.set(record.manifest.id, createPluginState(record));
51
+ }
52
+ }
53
+ /** Static channel catalog. Calling this never loads or executes plugin code. */
54
+ createChannelTypeDescriptors() {
55
+ const descriptors = [];
56
+ for (const state of this.states.values()) {
57
+ for (const contribution of state.record.manifest.contributions?.chatChannels ?? []) {
58
+ descriptors.push(this.channelDescriptor(state, contribution));
59
+ }
60
+ }
61
+ return descriptors;
62
+ }
63
+ /** Bind the trusted host once, after the server composition root constructs it. */
64
+ bindHostServices(services) {
65
+ this.assertOpen();
66
+ if (this.services)
67
+ throw new Error("plugin host services are already bound");
68
+ this.services = services;
69
+ return this;
70
+ }
71
+ /** Cached, non-blocking runner/contribution health for the generic control API. */
72
+ statusSnapshot() {
73
+ const plugins = [];
74
+ for (const state of this.states.values()) {
75
+ if (state.runner)
76
+ void this.refreshRuntimeStatus(state).catch(() => undefined);
77
+ for (const desired of state.desired.values()) {
78
+ if (state.runner && desired.startedGeneration === state.generation) {
79
+ void this.refreshContributionStatus(state, desired).catch(() => undefined);
80
+ }
81
+ }
82
+ const contributions = [...state.desired.values()].map((desired) => ({
83
+ key: desired.key,
84
+ target: { ...desired.target },
85
+ desired: true,
86
+ ...(desired.startedGeneration === undefined
87
+ ? {}
88
+ : { generation: desired.startedGeneration }),
89
+ failureCount: desired.failureCount,
90
+ status: { ...desired.status },
91
+ }));
92
+ plugins.push({
93
+ pluginId: state.record.manifest.id,
94
+ runner: {
95
+ state: runnerSnapshotState(state, this.disposed),
96
+ generation: state.generation,
97
+ failureCount: state.failureCount,
98
+ ...(state.lastRunnerError ? { lastError: state.lastRunnerError } : {}),
99
+ },
100
+ runtimeStatus: { ...state.runtimeStatus },
101
+ contributions,
102
+ });
103
+ }
104
+ return { capturedAt: new Date().toISOString(), plugins };
105
+ }
106
+ /** Start one resident runner for every enabled runtime plugin. */
107
+ async startPluginRuntimes() {
108
+ this.assertReady();
109
+ await Promise.all([...this.states.values()].map(async (state) => {
110
+ try {
111
+ await this.ensureRunner(state);
112
+ await this.refreshRuntimeStatus(state);
113
+ }
114
+ catch (error) {
115
+ this.writeLog({
116
+ level: "error",
117
+ event: "plugin.runner.start_failed",
118
+ pluginId: state.record.manifest.id,
119
+ message: messageOf(error),
120
+ });
121
+ }
122
+ }));
123
+ }
124
+ /**
125
+ * Atomically replace or add one plugin runtime, preserving its live chat
126
+ * desired state. Concurrent reloads of the same plugin are applied in call
127
+ * order; other plugins continue running independently.
128
+ */
129
+ reloadPluginRuntime(record, { readyTimeoutMs } = {}) {
130
+ this.assertReady();
131
+ validateRecord(record);
132
+ const pluginId = record.manifest.id;
133
+ return this.enqueueRuntimeOperation(pluginId, () => this.performPluginRuntimeReload(record, createRuntimeReadyDeadline(boundedReadyTimeout(readyTimeoutMs))));
134
+ }
135
+ /**
136
+ * Make one enabled plugin runtime ready without replacing an already healthy
137
+ * runner. This is the idempotent operation plugin CLIs use when their durable
138
+ * configuration has not changed.
139
+ */
140
+ ensurePluginRuntime(record, { readyTimeoutMs } = {}) {
141
+ this.assertReady();
142
+ validateRecord(record);
143
+ const pluginId = record.manifest.id;
144
+ return this.enqueueRuntimeOperation(pluginId, () => this.performPluginRuntimeEnsure(record, createRuntimeReadyDeadline(boundedReadyTimeout(readyTimeoutMs))));
145
+ }
146
+ enqueueRuntimeOperation(pluginId, execute) {
147
+ const preceding = this.runtimeOperations.get(pluginId);
148
+ const operation = (preceding ? preceding.catch(() => undefined) : Promise.resolve())
149
+ .then(execute);
150
+ this.runtimeOperations.set(pluginId, operation);
151
+ void operation.finally(() => {
152
+ if (this.runtimeOperations.get(pluginId) === operation) {
153
+ this.runtimeOperations.delete(pluginId);
154
+ }
155
+ }).catch(() => undefined);
156
+ return operation;
157
+ }
158
+ async performPluginRuntimeEnsure(record, deadline) {
159
+ this.assertReady();
160
+ const state = this.states.get(record.manifest.id);
161
+ if (!state || !sameRuntimeRecord(state.record, record)) {
162
+ return this.performPluginRuntimeReload(record, deadline);
163
+ }
164
+ if (!state.runner) {
165
+ return this.performPluginRuntimeReload(record, deadline);
166
+ }
167
+ const status = await this.refreshRuntimeStatus(state, deadline);
168
+ if (status.state === "unconfigured" ||
169
+ status.state === "failed" ||
170
+ status.state === "stopped") {
171
+ return this.performPluginRuntimeReload(record, deadline);
172
+ }
173
+ // A starting or degraded runtime may be recovering its own transport. Do
174
+ // not destroy it (and any active task leases) merely because the user ran
175
+ // the same connect command twice; wait for its bounded readiness result.
176
+ return this.waitForRuntimeReady(state, deadline);
177
+ }
178
+ async performPluginRuntimeReload(record, deadline) {
179
+ this.assertReady();
180
+ const pluginId = record.manifest.id;
181
+ let state = this.states.get(pluginId);
182
+ if (!state) {
183
+ state = createPluginState(record);
184
+ this.states.set(pluginId, state);
185
+ }
186
+ else {
187
+ await this.joinExitTransition(state);
188
+ const spawning = state.spawnPromise;
189
+ this.cancelRestart(state);
190
+ await this.closeRunner(state);
191
+ await spawning?.catch(() => undefined);
192
+ // A runner that was already spawning may have bound after the first
193
+ // close. Close once more before switching the immutable package record.
194
+ await this.closeRunner(state);
195
+ this.cancelRestart(state);
196
+ state.record = record;
197
+ state.failureCount = 0;
198
+ state.lastRunnerError = undefined;
199
+ state.runtimeStatusRefresh = undefined;
200
+ for (const desired of state.desired.values()) {
201
+ desired.startedGeneration = undefined;
202
+ desired.status = { state: "starting", connected: false, lastError: null };
203
+ state.channelStatus.set(desired.key, {
204
+ ...(state.channelStatus.get(desired.key) ?? {}),
205
+ connected: false,
206
+ lastError: null,
207
+ });
208
+ }
209
+ }
210
+ state.runtimeStatus = { state: "starting", connected: false, lastError: null };
211
+ try {
212
+ const runner = await this.ensureRunner(state);
213
+ await this.restoreDesiredContributions(state, runner);
214
+ }
215
+ catch {
216
+ // ensureRunner already cached the failure and scheduled resident backoff.
217
+ // The bounded readiness loop remains authoritative and can observe a
218
+ // successful retry before the caller's timeout expires.
219
+ }
220
+ return this.waitForRuntimeReady(state, deadline);
221
+ }
222
+ /** Permanently stop all children and cancel every pending automatic restart. */
223
+ async dispose() {
224
+ if (this.disposed)
225
+ return;
226
+ this.disposed = true;
227
+ const closes = [];
228
+ for (const state of this.states.values()) {
229
+ state.desired.clear();
230
+ for (const [key, status] of state.channelStatus) {
231
+ state.channelStatus.set(key, { ...status, connected: false });
232
+ }
233
+ this.cancelRestart(state);
234
+ clearTimer(state.stableTimer);
235
+ state.stableTimer = undefined;
236
+ state.runtimeStatus = { state: "stopping", connected: false };
237
+ closes.push(this.disposeState(state));
238
+ }
239
+ await Promise.all(closes);
240
+ for (const state of this.states.values()) {
241
+ state.runtimeStatus = { state: "stopped", connected: false };
242
+ }
243
+ }
244
+ channelDescriptor(state, contribution) {
245
+ return {
246
+ type: contribution.type,
247
+ factory: (context) => this.createRemoteChannel(state, contribution, context),
248
+ ...(contribution.configSchema ? { configSchema: contribution.configSchema } : {}),
249
+ ...(contribution.authorize
250
+ ? {
251
+ authorize: (emit, signal, context) => this.authorize(state, contribution, emit, signal, context),
252
+ }
253
+ : {}),
254
+ };
255
+ }
256
+ createRemoteChannel(state, contribution, context) {
257
+ const instanceId = context.instanceId ?? contribution.type;
258
+ if (!TARGET_ID_PATTERN.test(instanceId)) {
259
+ throw new Error(`invalid plugin channel instance id: ${instanceId}`);
260
+ }
261
+ const key = channelKey(contribution.type, instanceId);
262
+ const target = {
263
+ kind: "chat-channel",
264
+ contributionId: contribution.type,
265
+ instanceId,
266
+ };
267
+ const request = {
268
+ ...target,
269
+ config: normalizeChatChannelConfig(contribution, context.options),
270
+ ...(context.defaultAgent ? { defaultAgent: context.defaultAgent } : {}),
271
+ executionDefaults: {
272
+ runtime: context.config.AGENT_RUNTIME,
273
+ ...(context.config.RYNX_DEFAULT_AGENT
274
+ ? { defaultAgent: context.config.RYNX_DEFAULT_AGENT }
275
+ : {}),
276
+ allowedRoots: resolveAllowedRoots(context.config),
277
+ },
278
+ };
279
+ return {
280
+ id: contribution.type,
281
+ start: async () => {
282
+ this.assertReady();
283
+ const existing = state.desired.get(key);
284
+ const desired = existing ?? this.rememberDesired(state, { key, request, target });
285
+ if (existing && !sameStartRequest(existing.request, request)) {
286
+ throw new Error(`plugin channel instance is already started with different config: ${instanceId}`);
287
+ }
288
+ await this.ensureDesiredStarted(state, desired);
289
+ },
290
+ stop: async () => {
291
+ const desired = state.desired.get(key);
292
+ if (!desired) {
293
+ state.channelStatus.set(key, { connected: false, lastError: null });
294
+ return;
295
+ }
296
+ await this.stopDesired(state, desired);
297
+ },
298
+ getStatus: () => {
299
+ const cached = state.channelStatus.get(key) ?? {
300
+ connected: false,
301
+ lastError: null,
302
+ lastEventAt: null,
303
+ };
304
+ const desired = state.desired.get(key);
305
+ if (desired)
306
+ void this.refreshContributionStatus(state, desired).catch(() => undefined);
307
+ return { ...cached };
308
+ },
309
+ };
310
+ }
311
+ async authorize(state, contribution, emit, signal, context) {
312
+ this.assertReady();
313
+ const runner = await this.ensureRunner(state);
314
+ return runner.client.authorize({
315
+ kind: "chat-channel",
316
+ contributionId: contribution.type,
317
+ ...(context?.instanceId ? { instanceId: context.instanceId } : {}),
318
+ config: clonePluginConfig(context?.config),
319
+ }, (material) => emit(material), { signal });
320
+ }
321
+ rememberDesired(state, input) {
322
+ const desired = {
323
+ ...input,
324
+ status: { state: "stopped" },
325
+ failureCount: 0,
326
+ };
327
+ state.desired.set(desired.key, desired);
328
+ if (desired.target.kind === "chat-channel") {
329
+ state.channelStatus.set(desired.key, { connected: false, lastError: null });
330
+ }
331
+ return desired;
332
+ }
333
+ async ensureDesiredStarted(state, desired) {
334
+ const runner = await this.ensureRunner(state);
335
+ if (state.desired.get(desired.key) !== desired) {
336
+ return;
337
+ }
338
+ await this.startDesiredOnRunner(state, desired, runner);
339
+ await this.refreshContributionStatus(state, desired);
340
+ }
341
+ async startDesiredOnRunner(state, desired, runner) {
342
+ const generation = state.generation;
343
+ if (desired.startedGeneration === generation)
344
+ return;
345
+ if (desired.starting && desired.startingGeneration === generation) {
346
+ await desired.starting;
347
+ return;
348
+ }
349
+ desired.status = { state: "starting" };
350
+ const operation = runner.client.startContribution(desired.request).then(() => {
351
+ if (state.runner === runner && state.desired.get(desired.key) === desired) {
352
+ desired.startedGeneration = generation;
353
+ desired.failureCount = 0;
354
+ desired.status = {
355
+ state: "running",
356
+ connected: false,
357
+ lastError: null,
358
+ };
359
+ state.channelStatus.set(desired.key, {
360
+ ...(state.channelStatus.get(desired.key) ?? {}),
361
+ connected: state.channelStatus.get(desired.key)?.connected ?? false,
362
+ lastError: null,
363
+ });
364
+ }
365
+ }, (error) => {
366
+ if (state.runner === runner && state.desired.get(desired.key) === desired) {
367
+ this.markContributionFailed(state, desired, error);
368
+ }
369
+ throw error;
370
+ });
371
+ desired.starting = operation;
372
+ desired.startingGeneration = generation;
373
+ try {
374
+ await operation;
375
+ }
376
+ finally {
377
+ if (desired.starting === operation) {
378
+ desired.starting = undefined;
379
+ desired.startingGeneration = undefined;
380
+ }
381
+ }
382
+ }
383
+ async stopDesired(state, desired) {
384
+ if (state.desired.get(desired.key) !== desired)
385
+ return;
386
+ state.desired.delete(desired.key);
387
+ state.channelStatus.set(desired.key, {
388
+ connected: false,
389
+ lastError: null,
390
+ lastEventAt: state.channelStatus.get(desired.key)?.lastEventAt ?? null,
391
+ });
392
+ let stopError;
393
+ const runner = state.runner;
394
+ if (runner) {
395
+ try {
396
+ await runner.client.stopContribution(desired.target);
397
+ }
398
+ catch (error) {
399
+ stopError = error;
400
+ }
401
+ }
402
+ if (stopError)
403
+ throw stopError;
404
+ }
405
+ async refreshContributionStatus(state, desired) {
406
+ if (desired.statusRefresh)
407
+ return desired.statusRefresh;
408
+ const runner = state.runner;
409
+ if (!runner || desired.startedGeneration !== state.generation)
410
+ return;
411
+ const refresh = runner.client.status(desired.target).then((status) => {
412
+ if (state.runner !== runner || state.desired.get(desired.key) !== desired)
413
+ return;
414
+ desired.status = boundedContributionStatus(status);
415
+ state.channelStatus.set(desired.key, statusToChannelStatus(desired.status));
416
+ }, (error) => {
417
+ if (state.runner !== runner || state.desired.get(desired.key) !== desired)
418
+ return;
419
+ desired.status = {
420
+ state: "degraded",
421
+ connected: false,
422
+ lastError: messageOf(error),
423
+ };
424
+ state.channelStatus.set(desired.key, {
425
+ ...(state.channelStatus.get(desired.key) ?? {}),
426
+ connected: false,
427
+ lastError: messageOf(error),
428
+ });
429
+ });
430
+ desired.statusRefresh = refresh;
431
+ try {
432
+ await refresh;
433
+ }
434
+ finally {
435
+ if (desired.statusRefresh === refresh)
436
+ desired.statusRefresh = undefined;
437
+ }
438
+ }
439
+ async refreshRuntimeStatus(state, deadline) {
440
+ if (deadline && deadline.expiresAt - Date.now() <= 0) {
441
+ throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
442
+ }
443
+ const runner = state.runner;
444
+ if (!runner)
445
+ return state.runtimeStatus;
446
+ let refresh = state.runtimeStatusRefresh;
447
+ if (!refresh) {
448
+ refresh = runner.client.runtimeStatus().then((status) => {
449
+ if (state.runner !== runner)
450
+ return state.runtimeStatus;
451
+ state.runtimeStatus = boundedRuntimeStatus(status);
452
+ return state.runtimeStatus;
453
+ }, (error) => {
454
+ if (state.runner !== runner)
455
+ return state.runtimeStatus;
456
+ state.runtimeStatus = {
457
+ state: "degraded",
458
+ connected: false,
459
+ lastError: messageOf(error),
460
+ };
461
+ return state.runtimeStatus;
462
+ });
463
+ state.runtimeStatusRefresh = refresh;
464
+ void refresh.finally(() => {
465
+ if (state.runtimeStatusRefresh === refresh)
466
+ state.runtimeStatusRefresh = undefined;
467
+ }).catch(() => undefined);
468
+ }
469
+ return deadline
470
+ ? await settleBeforeDeadline(refresh, deadline.expiresAt, () => this.runtimeReadyTimeoutError(state, deadline.timeoutMs))
471
+ : await refresh;
472
+ }
473
+ async waitForRuntimeReady(state, deadline) {
474
+ while (true) {
475
+ this.assertOpen();
476
+ const remainingBeforeRefresh = deadline.expiresAt - Date.now();
477
+ if (remainingBeforeRefresh <= 0) {
478
+ throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
479
+ }
480
+ const status = state.runner
481
+ ? await this.refreshRuntimeStatus(state, deadline)
482
+ : state.runtimeStatus;
483
+ if (status.state === "running" && status.connected !== false)
484
+ return { ...status };
485
+ const remaining = deadline.expiresAt - Date.now();
486
+ if (remaining <= 0) {
487
+ throw this.runtimeReadyTimeoutError(state, deadline.timeoutMs);
488
+ }
489
+ await delay(Math.min(RUNTIME_STATUS_POLL_MS, remaining));
490
+ }
491
+ }
492
+ runtimeReadyTimeoutError(state, timeoutMs) {
493
+ const status = state.runtimeStatus;
494
+ const detail = status.lastError ? `: ${status.lastError}` : ` (state=${status.state})`;
495
+ return new Error(`plugin runtime "${state.record.manifest.id}" did not become ready within ${timeoutMs}ms${detail}`);
496
+ }
497
+ ensureRunner(state) {
498
+ this.assertReady();
499
+ if (state.exitTransition) {
500
+ return this.joinExitTransition(state).then(() => this.ensureRunner(state));
501
+ }
502
+ if (state.runner)
503
+ return Promise.resolve(state.runner);
504
+ if (state.spawnPromise)
505
+ return state.spawnPromise;
506
+ state.runtimeStatus = { state: "starting", connected: false, lastError: null };
507
+ const spawn = this.spawnAndBind(state).catch((error) => {
508
+ if (!this.disposed) {
509
+ state.failureCount += 1;
510
+ this.markRunnerUnavailable(state, `plugin runner failed to start: ${messageOf(error)}`);
511
+ this.scheduleRestart(state);
512
+ }
513
+ throw error;
514
+ });
515
+ state.spawnPromise = spawn;
516
+ void spawn.finally(() => {
517
+ if (state.spawnPromise === spawn)
518
+ state.spawnPromise = undefined;
519
+ }).catch(() => undefined);
520
+ return spawn;
521
+ }
522
+ async spawnAndBind(state) {
523
+ const pluginId = state.record.manifest.id;
524
+ const eventQueue = new BoundedHostEventQueue();
525
+ const host = new PluginHostRpc({
526
+ pluginId,
527
+ daemonInstanceId: this.daemonInstanceId,
528
+ grants: state.record.grants,
529
+ services: this.requireHostServices(),
530
+ emit: (event) => eventQueue.enqueue(event),
531
+ });
532
+ const stderr = this.stderrSink(pluginId);
533
+ let runner;
534
+ try {
535
+ runner = await this.spawnRunner({
536
+ pluginId,
537
+ packageRoot: state.record.installPath,
538
+ runtimeEntry: path.resolve(state.record.installPath, state.record.manifest.runtimeEntry),
539
+ dataDir: path.join(rynxHome(), "plugin-data", pluginId),
540
+ config: {},
541
+ grantedCapabilities: [...state.record.grants],
542
+ handleHostCall: (method, params) => host.call(method, params),
543
+ onStderr: stderr.write,
544
+ });
545
+ assertHandshake(pluginId, state.record.grants, runner.handshake);
546
+ }
547
+ catch (error) {
548
+ eventQueue.close(error);
549
+ stderr.flush();
550
+ await runner?.close().catch(() => undefined);
551
+ await host.dispose();
552
+ throw error;
553
+ }
554
+ if (this.disposed) {
555
+ eventQueue.close(new Error("plugin supervisor is disposed"));
556
+ stderr.flush();
557
+ await runner.close().catch(() => undefined);
558
+ await host.dispose();
559
+ throw new Error("plugin supervisor is disposed");
560
+ }
561
+ eventQueue.bind(runner.client);
562
+ state.runner = runner;
563
+ state.host = host;
564
+ state.eventQueue = eventQueue;
565
+ state.lastRunnerError = undefined;
566
+ state.generation += 1;
567
+ const generation = state.generation;
568
+ clearTimer(state.stableTimer);
569
+ state.stableTimer = setTimeout(() => {
570
+ if (state.runner === runner)
571
+ state.failureCount = 0;
572
+ }, this.stableResetMs);
573
+ state.stableTimer.unref?.();
574
+ void runner.completion.then((outcome) => {
575
+ eventQueue.close(new Error("plugin runner exited"));
576
+ stderr.flush();
577
+ this.trackRunnerExit(state, runner, host, generation, outcome);
578
+ }, (error) => {
579
+ eventQueue.close(error);
580
+ stderr.flush();
581
+ this.trackRunnerExit(state, runner, host, generation, undefined, error);
582
+ }).catch((error) => {
583
+ this.writeLog({
584
+ level: "error",
585
+ event: "plugin.runner.exit_handler_failed",
586
+ pluginId,
587
+ message: messageOf(error),
588
+ });
589
+ });
590
+ return runner;
591
+ }
592
+ trackRunnerExit(state, runner, host, generation, outcome, error) {
593
+ const preceding = state.exitTransition;
594
+ const transition = (preceding
595
+ ? preceding.catch(() => undefined)
596
+ : Promise.resolve()).then(() => this.runnerExited(state, runner, host, generation, outcome, error));
597
+ state.exitTransition = transition;
598
+ void transition.then(() => {
599
+ if (state.exitTransition === transition)
600
+ state.exitTransition = undefined;
601
+ }, (transitionError) => {
602
+ if (state.exitTransition === transition)
603
+ state.exitTransition = undefined;
604
+ this.writeLog({
605
+ level: "error",
606
+ event: "plugin.runner.exit_handler_failed",
607
+ pluginId: state.record.manifest.id,
608
+ message: messageOf(transitionError),
609
+ });
610
+ }).catch(() => undefined);
611
+ }
612
+ async runnerExited(state, runner, host, generation, outcome, error) {
613
+ if (state.runner !== runner || state.generation !== generation)
614
+ return;
615
+ state.runner = undefined;
616
+ if (state.host === host)
617
+ state.host = undefined;
618
+ state.eventQueue?.close(error ?? new Error("plugin runner exited"));
619
+ state.eventQueue = undefined;
620
+ state.runtimeStatusRefresh = undefined;
621
+ clearTimer(state.stableTimer);
622
+ state.stableTimer = undefined;
623
+ await host.dispose();
624
+ // Reload, explicit start, and dispose all join this transition. Keep the
625
+ // generation guard as a final defence against any future call path that
626
+ // can bind a successor without waiting for the old host cleanup.
627
+ if (this.disposed || state.runner || state.generation !== generation)
628
+ return;
629
+ const message = error
630
+ ? messageOf(error)
631
+ : `plugin runner exited (code=${String(outcome?.code)}, signal=${String(outcome?.signal)})`;
632
+ this.markRunnerUnavailable(state, message);
633
+ this.writeLog({
634
+ level: outcome?.code === 0 && !error ? "warn" : "error",
635
+ event: "plugin.runner.exited",
636
+ pluginId: state.record.manifest.id,
637
+ message,
638
+ code: outcome?.code,
639
+ signal: outcome?.signal,
640
+ });
641
+ state.failureCount += 1;
642
+ this.scheduleRestart(state);
643
+ }
644
+ async joinExitTransition(state) {
645
+ while (state.exitTransition) {
646
+ const transition = state.exitTransition;
647
+ await transition.catch(() => undefined);
648
+ if (state.exitTransition === transition)
649
+ state.exitTransition = undefined;
650
+ }
651
+ }
652
+ scheduleRestart(state) {
653
+ if (this.disposed || state.restartTimer)
654
+ return;
655
+ const exponent = Math.min(Math.max(state.failureCount - 1, 0), 30);
656
+ const delayMs = Math.min(this.restartMaxMs, this.restartBaseMs * 2 ** exponent);
657
+ this.writeLog({
658
+ level: "warn",
659
+ event: "plugin.runner.restart_scheduled",
660
+ pluginId: state.record.manifest.id,
661
+ delayMs,
662
+ });
663
+ state.restartTimer = setTimeout(() => {
664
+ state.restartTimer = undefined;
665
+ void this.restart(state);
666
+ }, delayMs);
667
+ state.restartTimer.unref?.();
668
+ }
669
+ async restart(state) {
670
+ if (this.disposed)
671
+ return;
672
+ let runner;
673
+ try {
674
+ runner = await this.ensureRunner(state);
675
+ }
676
+ catch (error) {
677
+ this.writeLog({
678
+ level: "error",
679
+ event: "plugin.runner.restart_failed",
680
+ pluginId: state.record.manifest.id,
681
+ message: messageOf(error),
682
+ });
683
+ return;
684
+ }
685
+ await this.refreshRuntimeStatus(state);
686
+ await this.restoreDesiredContributions(state, runner);
687
+ }
688
+ async restoreDesiredContributions(state, runner) {
689
+ for (const desired of [...state.desired.values()]) {
690
+ try {
691
+ await this.startDesiredOnRunner(state, desired, runner);
692
+ await this.refreshContributionStatus(state, desired);
693
+ }
694
+ catch (error) {
695
+ this.writeLog({
696
+ level: "error",
697
+ event: "plugin.contribution.restore_failed",
698
+ pluginId: state.record.manifest.id,
699
+ message: messageOf(error),
700
+ });
701
+ }
702
+ }
703
+ }
704
+ async closeRunner(state) {
705
+ const runner = state.runner;
706
+ const host = state.host;
707
+ const eventQueue = state.eventQueue;
708
+ state.runner = undefined;
709
+ state.host = undefined;
710
+ state.eventQueue = undefined;
711
+ state.runtimeStatusRefresh = undefined;
712
+ eventQueue?.close(new Error("plugin runner is closing"));
713
+ clearTimer(state.stableTimer);
714
+ state.stableTimer = undefined;
715
+ if (runner) {
716
+ try {
717
+ await runner.close();
718
+ }
719
+ catch (error) {
720
+ this.writeLog({
721
+ level: "warn",
722
+ event: "plugin.runner.close_failed",
723
+ pluginId: state.record.manifest.id,
724
+ message: messageOf(error),
725
+ });
726
+ }
727
+ }
728
+ await host?.dispose();
729
+ }
730
+ async disposeState(state) {
731
+ await this.joinExitTransition(state);
732
+ const spawning = state.spawnPromise;
733
+ await this.closeRunner(state);
734
+ await spawning?.catch(() => undefined);
735
+ // A spawn can bind between the first close and the disposed check inside
736
+ // spawnAndBind. A final idempotent close makes dispose a true join point.
737
+ await this.closeRunner(state);
738
+ await this.joinExitTransition(state);
739
+ }
740
+ cancelRestart(state) {
741
+ clearTimer(state.restartTimer);
742
+ state.restartTimer = undefined;
743
+ }
744
+ markRunnerUnavailable(state, message) {
745
+ state.lastRunnerError = message;
746
+ state.runtimeStatus = {
747
+ state: "failed",
748
+ connected: false,
749
+ lastError: message,
750
+ };
751
+ for (const desired of state.desired.values()) {
752
+ desired.startedGeneration = undefined;
753
+ desired.status = {
754
+ state: "failed",
755
+ connected: false,
756
+ lastError: message,
757
+ };
758
+ state.channelStatus.set(desired.key, {
759
+ ...(state.channelStatus.get(desired.key) ?? {}),
760
+ connected: false,
761
+ lastError: message,
762
+ });
763
+ }
764
+ }
765
+ markContributionFailed(state, desired, error) {
766
+ desired.startedGeneration = undefined;
767
+ desired.failureCount += 1;
768
+ desired.status = {
769
+ state: "failed",
770
+ connected: false,
771
+ lastError: messageOf(error),
772
+ };
773
+ state.channelStatus.set(desired.key, {
774
+ ...(state.channelStatus.get(desired.key) ?? {}),
775
+ connected: false,
776
+ lastError: messageOf(error),
777
+ });
778
+ }
779
+ stderrSink(pluginId) {
780
+ let buffered = "";
781
+ const continuation = " …[continued]";
782
+ const contentLimit = MAX_STDERR_LINE_BYTES - Buffer.byteLength(continuation, "utf8");
783
+ const emitLine = (line) => {
784
+ const message = line.trimEnd();
785
+ if (!message)
786
+ return;
787
+ this.writeLog({
788
+ level: "warn",
789
+ event: "plugin.runner.stderr",
790
+ pluginId,
791
+ message,
792
+ });
793
+ };
794
+ const appendSegment = (segment) => {
795
+ let remaining = segment;
796
+ while (remaining.length > 0) {
797
+ const bufferedBytes = Buffer.byteLength(buffered, "utf8");
798
+ const budget = Math.max(1, contentLimit - bufferedBytes);
799
+ const [prefix, rest] = splitUtf8AtMost(remaining, budget);
800
+ if (!prefix && rest) {
801
+ if (buffered) {
802
+ emitLine(`${buffered}${continuation}`);
803
+ buffered = "";
804
+ continue;
805
+ }
806
+ // `contentLimit` is much larger than a single UTF-8 code point, so
807
+ // this branch is defensive only.
808
+ const first = String.fromCodePoint(remaining.codePointAt(0));
809
+ buffered = first;
810
+ remaining = remaining.slice(first.length);
811
+ continue;
812
+ }
813
+ buffered += prefix;
814
+ remaining = rest;
815
+ if (remaining.length > 0 || Buffer.byteLength(buffered, "utf8") >= contentLimit) {
816
+ emitLine(`${buffered}${continuation}`);
817
+ buffered = "";
818
+ }
819
+ }
820
+ };
821
+ return {
822
+ write: (chunk) => {
823
+ let offset = 0;
824
+ while (offset < chunk.length) {
825
+ const newline = chunk.indexOf("\n", offset);
826
+ if (newline < 0) {
827
+ appendSegment(chunk.slice(offset));
828
+ return;
829
+ }
830
+ appendSegment(chunk.slice(offset, newline));
831
+ emitLine(buffered);
832
+ buffered = "";
833
+ offset = newline + 1;
834
+ }
835
+ },
836
+ flush: () => {
837
+ if (buffered)
838
+ emitLine(buffered);
839
+ buffered = "";
840
+ },
841
+ };
842
+ }
843
+ writeLog(entry) {
844
+ if (this.logger) {
845
+ this.logger(entry);
846
+ return;
847
+ }
848
+ console.error(JSON.stringify({ timestamp: new Date().toISOString(), ...entry }));
849
+ }
850
+ assertOpen() {
851
+ if (this.disposed)
852
+ throw new Error("plugin supervisor is disposed");
853
+ }
854
+ assertReady() {
855
+ this.assertOpen();
856
+ this.requireHostServices();
857
+ }
858
+ requireHostServices() {
859
+ if (!this.services)
860
+ throw new Error("plugin host services are not bound");
861
+ return this.services;
862
+ }
863
+ }
864
+ /** A single-writer queue: bounded buffering plus an awaitable delivery ack. */
865
+ class BoundedHostEventQueue {
866
+ items = [];
867
+ client;
868
+ queuedBytes = 0;
869
+ draining = false;
870
+ closed = false;
871
+ closeError;
872
+ bind(client) {
873
+ if (this.client)
874
+ throw new Error("plugin host event queue is already bound");
875
+ if (this.closed)
876
+ throw this.closeError ?? new Error("plugin host event queue is closed");
877
+ this.client = client;
878
+ void this.drain();
879
+ }
880
+ enqueue(event) {
881
+ if (this.closed) {
882
+ return Promise.reject(this.closeError ?? new Error("plugin host event queue is closed"));
883
+ }
884
+ let bytes;
885
+ try {
886
+ bytes = Buffer.byteLength(JSON.stringify(event), "utf8");
887
+ }
888
+ catch {
889
+ return Promise.reject(new Error("plugin host event must be JSON serializable"));
890
+ }
891
+ if (bytes > MAX_PLUGIN_HOST_EVENT_BYTES) {
892
+ return Promise.reject(new Error("plugin host event exceeds the bounded event budget"));
893
+ }
894
+ if (this.items.length + (this.draining ? 1 : 0) >= MAX_HOST_EVENT_QUEUE_ITEMS ||
895
+ this.queuedBytes + bytes > MAX_HOST_EVENT_QUEUE_BYTES) {
896
+ return Promise.reject(new Error("plugin host event queue is full"));
897
+ }
898
+ const promise = new Promise((resolve, reject) => {
899
+ this.items.push({ event, bytes, resolve, reject });
900
+ this.queuedBytes += bytes;
901
+ });
902
+ void this.drain();
903
+ return promise;
904
+ }
905
+ close(error) {
906
+ if (this.closed)
907
+ return;
908
+ this.closed = true;
909
+ this.closeError = error instanceof Error
910
+ ? error
911
+ : new Error(error === undefined ? "plugin host event queue is closed" : String(error));
912
+ for (const item of this.items.splice(0)) {
913
+ this.queuedBytes -= item.bytes;
914
+ item.reject(this.closeError);
915
+ }
916
+ }
917
+ async drain() {
918
+ if (this.draining || this.closed || !this.client)
919
+ return;
920
+ this.draining = true;
921
+ try {
922
+ while (!this.closed && this.client && this.items.length > 0) {
923
+ const item = this.items.shift();
924
+ try {
925
+ await this.client.emitHostEvent(item.event.event, item.event.payload);
926
+ item.resolve();
927
+ }
928
+ catch (error) {
929
+ const failure = error instanceof Error ? error : new Error(String(error));
930
+ item.reject(failure);
931
+ this.queuedBytes -= item.bytes;
932
+ this.close(failure);
933
+ return;
934
+ }
935
+ this.queuedBytes -= item.bytes;
936
+ }
937
+ }
938
+ finally {
939
+ this.draining = false;
940
+ if (!this.closed && this.client && this.items.length > 0)
941
+ void this.drain();
942
+ }
943
+ }
944
+ }
945
+ /** Convenience export for composition roots that only need the static catalog. */
946
+ export function createChannelTypeDescriptors(supervisor) {
947
+ return supervisor.createChannelTypeDescriptors();
948
+ }
949
+ function createPluginState(record) {
950
+ return {
951
+ record,
952
+ desired: new Map(),
953
+ channelStatus: new Map(),
954
+ generation: 0,
955
+ failureCount: 0,
956
+ runtimeStatus: { state: "stopped", connected: false },
957
+ };
958
+ }
959
+ function validateRecord(record) {
960
+ if (record.name !== record.manifest.id) {
961
+ throw new Error(`runnable plugin id mismatch: ${record.name} != ${record.manifest.id}`);
962
+ }
963
+ if (!record.enabled) {
964
+ throw new Error(`plugin ${record.manifest.id} is disabled`);
965
+ }
966
+ const requested = new Set(record.manifest.requestedCapabilities);
967
+ for (const grant of record.grants) {
968
+ if (!requested.has(grant)) {
969
+ throw new Error(`plugin ${record.manifest.id} was granted an unrequested capability: ${grant}`);
970
+ }
971
+ }
972
+ }
973
+ function sameRuntimeRecord(left, right) {
974
+ return left.installPath === right.installPath &&
975
+ left.integrity === right.integrity &&
976
+ JSON.stringify(left.manifest) === JSON.stringify(right.manifest) &&
977
+ JSON.stringify(left.grants) === JSON.stringify(right.grants);
978
+ }
979
+ function assertHandshake(pluginId, grants, handshake) {
980
+ if (handshake.pluginId !== pluginId) {
981
+ throw new Error(`plugin runner identity mismatch: ${handshake.pluginId} != ${pluginId}`);
982
+ }
983
+ if (handshake.apiVersion !== PLUGIN_RUNNER_API_VERSION) {
984
+ throw new Error(`plugin runner API mismatch: ${handshake.apiVersion} != ${PLUGIN_RUNNER_API_VERSION}`);
985
+ }
986
+ if (handshake.grantedCapabilities.length !== grants.length ||
987
+ handshake.grantedCapabilities.some((capability, index) => capability !== grants[index])) {
988
+ throw new Error(`plugin runner capability handshake mismatch: ${pluginId}`);
989
+ }
990
+ }
991
+ function statusToChannelStatus(status) {
992
+ return {
993
+ connected: status.connected ?? status.state === "running",
994
+ lastError: status.lastError ?? null,
995
+ lastEventAt: status.lastEventAt ?? null,
996
+ };
997
+ }
998
+ function boundedContributionStatus(status) {
999
+ return {
1000
+ ...status,
1001
+ ...(typeof status.lastError === "string"
1002
+ ? { lastError: boundedText(status.lastError, MAX_STATUS_ERROR_BYTES) }
1003
+ : {}),
1004
+ ...(typeof status.lastEventAt === "string"
1005
+ ? { lastEventAt: boundedText(status.lastEventAt, 256) }
1006
+ : {}),
1007
+ };
1008
+ }
1009
+ function boundedRuntimeStatus(status) {
1010
+ return {
1011
+ ...status,
1012
+ ...(typeof status.lastError === "string"
1013
+ ? { lastError: boundedText(status.lastError, MAX_STATUS_ERROR_BYTES) }
1014
+ : {}),
1015
+ ...(typeof status.lastEventAt === "string"
1016
+ ? { lastEventAt: boundedText(status.lastEventAt, 256) }
1017
+ : {}),
1018
+ };
1019
+ }
1020
+ function channelKey(contributionId, instanceId) {
1021
+ return `channel:${contributionId}:${instanceId}`;
1022
+ }
1023
+ function positiveDelay(value, fallback) {
1024
+ return value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback;
1025
+ }
1026
+ function boundedReadyTimeout(value) {
1027
+ if (value === undefined)
1028
+ return DEFAULT_READY_TIMEOUT_MS;
1029
+ if (!Number.isFinite(value) || value < 0) {
1030
+ throw new Error("plugin runtime ready timeout must be a non-negative finite number");
1031
+ }
1032
+ return Math.min(value, MAX_READY_TIMEOUT_MS);
1033
+ }
1034
+ function createRuntimeReadyDeadline(timeoutMs) {
1035
+ return { timeoutMs, expiresAt: Date.now() + timeoutMs };
1036
+ }
1037
+ function settleBeforeDeadline(promise, expiresAt, timeoutError) {
1038
+ const remaining = expiresAt - Date.now();
1039
+ if (remaining <= 0)
1040
+ return Promise.reject(timeoutError());
1041
+ return new Promise((resolve, reject) => {
1042
+ const timer = setTimeout(() => reject(timeoutError()), remaining);
1043
+ promise.then((value) => {
1044
+ clearTimeout(timer);
1045
+ resolve(value);
1046
+ }, (error) => {
1047
+ clearTimeout(timer);
1048
+ reject(error);
1049
+ });
1050
+ });
1051
+ }
1052
+ function delay(milliseconds) {
1053
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
1054
+ }
1055
+ function clearTimer(timer) {
1056
+ if (timer)
1057
+ clearTimeout(timer);
1058
+ }
1059
+ function runnerSnapshotState(state, disposed) {
1060
+ if (state.runner)
1061
+ return "running";
1062
+ if (state.spawnPromise)
1063
+ return "starting";
1064
+ if (state.restartTimer)
1065
+ return "backoff";
1066
+ if (state.lastRunnerError && !disposed)
1067
+ return "failed";
1068
+ return "stopped";
1069
+ }
1070
+ function splitUtf8AtMost(value, maxBytes) {
1071
+ if (Buffer.byteLength(value, "utf8") <= maxBytes)
1072
+ return [value, ""];
1073
+ let low = 0;
1074
+ let high = value.length;
1075
+ while (low < high) {
1076
+ const middle = Math.ceil((low + high) / 2);
1077
+ if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maxBytes)
1078
+ low = middle;
1079
+ else
1080
+ high = middle - 1;
1081
+ }
1082
+ if (low > 0 && low < value.length && isHighSurrogate(value.charCodeAt(low - 1)))
1083
+ low -= 1;
1084
+ return [value.slice(0, low), value.slice(low)];
1085
+ }
1086
+ function isHighSurrogate(value) {
1087
+ return value >= 0xd800 && value <= 0xdbff;
1088
+ }
1089
+ function messageOf(error) {
1090
+ return boundedText(error instanceof Error ? error.message : String(error), MAX_STATUS_ERROR_BYTES);
1091
+ }
1092
+ function boundedText(value, maxBytes) {
1093
+ if (Buffer.byteLength(value, "utf8") <= maxBytes)
1094
+ return value;
1095
+ const marker = "…[truncated]";
1096
+ const budget = maxBytes - Buffer.byteLength(marker, "utf8");
1097
+ const [prefix] = splitUtf8AtMost(value, Math.max(1, budget));
1098
+ return `${prefix}${marker}`;
1099
+ }
1100
+ function sameStartRequest(left, right) {
1101
+ return JSON.stringify(left) === JSON.stringify(right);
1102
+ }
1103
+ function clonePluginConfig(value) {
1104
+ if (!value)
1105
+ return {};
1106
+ return cloneJsonValue(value, "options", new WeakSet());
1107
+ }
1108
+ function normalizeChatChannelConfig(contribution, value) {
1109
+ const config = clonePluginConfig(value);
1110
+ const schema = contribution.configSchema ?? [];
1111
+ return schema.length > 0 ? normalizePluginConfig(schema, config) : config;
1112
+ }
1113
+ function cloneJsonValue(value, pathLabel, ancestors) {
1114
+ if (value === null || typeof value === "string" || typeof value === "boolean")
1115
+ return value;
1116
+ if (typeof value === "number") {
1117
+ if (!Number.isFinite(value))
1118
+ throw new Error(`${pathLabel} must contain finite numbers`);
1119
+ return value;
1120
+ }
1121
+ if (typeof value !== "object") {
1122
+ throw new Error(`${pathLabel} must contain only JSON values`);
1123
+ }
1124
+ if (ancestors.has(value))
1125
+ throw new Error(`${pathLabel} must not be cyclic`);
1126
+ ancestors.add(value);
1127
+ try {
1128
+ if (Array.isArray(value)) {
1129
+ return value.map((item, index) => cloneJsonValue(item, `${pathLabel}[${index}]`, ancestors));
1130
+ }
1131
+ if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
1132
+ throw new Error(`${pathLabel} must contain only plain objects`);
1133
+ }
1134
+ const output = {};
1135
+ for (const [key, item] of Object.entries(value)) {
1136
+ output[key] = cloneJsonValue(item, `${pathLabel}.${key}`, ancestors);
1137
+ }
1138
+ return output;
1139
+ }
1140
+ finally {
1141
+ ancestors.delete(value);
1142
+ }
1143
+ }