@xmoxmo/bncr 0.6.7 → 0.6.8

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,12 +1,35 @@
1
+ import { emitBncrLogLine } from '../core/logging.ts';
2
+ import {
3
+ activatePendingLifecycle,
4
+ beginLifecycleRetirement,
5
+ commitLifecycleAdoption,
6
+ createRegisterLifecycleState,
7
+ failLifecycle,
8
+ getBridgeGenerationKey,
9
+ getPendingLifecycle,
10
+ isLifecycleRegistryActive,
11
+ isLifecycleRegistryStoppable,
12
+ type LifecycleAdoptionDecision,
13
+ type LifecycleOwner,
14
+ type LifecyclePlanInput,
15
+ type LifecycleRetirementOutcome,
16
+ planLifecycleAdoption,
17
+ reactivateStoppedLifecycle,
18
+ settleLifecycleRetirement,
19
+ } from './register-lifecycle.ts';
1
20
  import {
2
21
  type BncrGatewayMethodName,
3
22
  createBncrGatewayMethodRegistry,
4
23
  } from './register-runtime-gateway.ts';
24
+ import type { BridgeOwner, BridgeRegisterStateCarrier } from './register-runtime-helpers.ts';
5
25
  import {
6
- type BridgeOwner,
7
- type BridgeRegisterStateCarrier,
8
- shouldAdoptProcessOwner,
9
- } from './register-runtime-helpers.ts';
26
+ createRegistryRuntimeObservation,
27
+ evaluateRegistryRuntimeObservation,
28
+ normalizeRegistryRuntimeObservation,
29
+ REGISTRY_RUNTIME_OBSERVATION_POLL_MS,
30
+ type RegistryRuntimeObservation,
31
+ type RegistryRuntimeProbe,
32
+ } from './register-runtime-observation.ts';
10
33
  import { createBncrBridgeSingletonManager } from './register-runtime-singleton.ts';
11
34
  import {
12
35
  type ChannelModule,
@@ -14,6 +37,7 @@ import {
14
37
  loadBncrRuntimeSync,
15
38
  pluginFile,
16
39
  pluginRoot,
40
+ pluginVersion,
17
41
  } from './runtime-loader.ts';
18
42
 
19
43
  type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
@@ -42,9 +66,43 @@ type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
42
66
  [registerMetaSymbol]?: RegisterMeta;
43
67
  };
44
68
 
69
+ type RegistryDeclarationSlot = 'missing' | 'declared';
70
+ type RegistryDeclarationRegistration = 'pending' | 'complete' | 'failed';
71
+
72
+ type RegistryDeclaration = {
73
+ bridgeGenerationKey: string;
74
+ lifecycleGeneration: number;
75
+ service: RegistryDeclarationSlot;
76
+ channel: RegistryDeclarationSlot;
77
+ registration: RegistryDeclarationRegistration;
78
+ state: 'shadow' | 'active' | 'retired';
79
+ runtimeObservation?: RegistryRuntimeObservation;
80
+ };
81
+
82
+ type RegistryRuntimeObservationTimer = {
83
+ timer: NodeJS.Timeout;
84
+ bridgeGenerationKey: string;
85
+ lifecycleGeneration: number;
86
+ };
87
+
45
88
  type BncrGatewayRuntime = {
46
89
  currentBridge?: BridgeSingletonWithState;
47
90
  registeredMethodsByRegistry: Map<string, Set<BncrGatewayMethodName>>;
91
+ gatewayMethodDispatchers?: Partial<
92
+ Record<
93
+ BncrGatewayMethodName,
94
+ (
95
+ bridge: BridgeSingletonWithState,
96
+ opts: BridgeGatewayHandlerContext,
97
+ ) => BridgeGatewayHandlerResult
98
+ >
99
+ >;
100
+ registryDeclarations: Map<string, RegistryDeclaration>;
101
+ runtimeObservationTimers: Map<string, RegistryRuntimeObservationTimer>;
102
+ lifecycle: ReturnType<typeof createRegisterLifecycleState>;
103
+ };
104
+
105
+ type LegacyBncrGatewayRuntime = Partial<BncrGatewayRuntime> & {
48
106
  serviceRegistered?: boolean;
49
107
  channelRegistered?: boolean;
50
108
  serviceOwnerApiInstanceId?: string;
@@ -55,6 +113,8 @@ const registerMetaSymbol = Symbol.for('bncr.register.meta');
55
113
  const globalRegisterTraceSymbol = Symbol.for('bncr.global.register.trace');
56
114
  const bridgeOwnerSymbol = Symbol.for('bncr.bridge.owner');
57
115
  const gatewayRuntimeSymbol = Symbol.for('bncr.gateway.runtime');
116
+ const maxRegistryDeclarations = 6;
117
+ const maxRegisterTraceEntries = 64;
58
118
  const moduleEpoch = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
59
119
  const identityIds = new WeakMap<object, string>();
60
120
  let identitySeq = 0;
@@ -82,6 +142,90 @@ const getProcessStore = () => {
82
142
  return p;
83
143
  };
84
144
 
145
+ const rememberRegisterTraceValue = (values: Set<string>, value: string) => {
146
+ // The trace is diagnostic only. Remember recency without retaining every
147
+ // registration fingerprint for the lifetime of the Gateway process.
148
+ values.delete(value);
149
+ values.add(value);
150
+ while (values.size > maxRegisterTraceEntries) {
151
+ const oldest = values.values().next().value;
152
+ if (oldest === undefined) break;
153
+ values.delete(oldest);
154
+ }
155
+ };
156
+
157
+ const normalizeRegistryDeclaration = (value: unknown): RegistryDeclaration => {
158
+ const legacy = (value || {}) as {
159
+ bridgeGenerationKey?: unknown;
160
+ lifecycleGeneration?: unknown;
161
+ generation?: unknown;
162
+ service?: unknown;
163
+ channel?: unknown;
164
+ registration?: unknown;
165
+ state?: unknown;
166
+ runtimeObservation?: unknown;
167
+ };
168
+ const service = legacy.service === 'declared' || legacy.service === true ? 'declared' : 'missing';
169
+ const channel = legacy.channel === 'declared' || legacy.channel === true ? 'declared' : 'missing';
170
+ const registration =
171
+ legacy.registration === 'pending' ||
172
+ legacy.registration === 'complete' ||
173
+ legacy.registration === 'failed'
174
+ ? legacy.registration
175
+ : service === 'declared' && channel === 'declared'
176
+ ? 'complete'
177
+ : 'failed';
178
+ const state = legacy.state === 'active' || legacy.state === 'retired' ? legacy.state : 'shadow';
179
+ const lifecycleGeneration =
180
+ typeof legacy.lifecycleGeneration === 'number'
181
+ ? legacy.lifecycleGeneration
182
+ : typeof legacy.generation === 'number'
183
+ ? legacy.generation
184
+ : 0;
185
+ return {
186
+ bridgeGenerationKey:
187
+ typeof legacy.bridgeGenerationKey === 'string' ? legacy.bridgeGenerationKey : '',
188
+ lifecycleGeneration,
189
+ service,
190
+ channel,
191
+ registration,
192
+ state,
193
+ runtimeObservation: normalizeRegistryRuntimeObservation(legacy.runtimeObservation),
194
+ };
195
+ };
196
+
197
+ const normalizeRegistryDeclarationInPlace = (value: unknown): RegistryDeclaration => {
198
+ const normalized = normalizeRegistryDeclaration(value);
199
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return normalized;
200
+
201
+ const target = value as Record<string, unknown>;
202
+ delete target.generation;
203
+ Object.assign(target, normalized);
204
+ return value as RegistryDeclaration;
205
+ };
206
+
207
+ const pruneRegistryLedgers = (runtime: BncrGatewayRuntime) => {
208
+ const protectedRegistries = new Set(
209
+ [
210
+ runtime.lifecycle.active?.key.registryFingerprint,
211
+ runtime.lifecycle.pending?.key.registryFingerprint,
212
+ ].filter((value): value is string => Boolean(value)),
213
+ );
214
+ for (const ledger of [runtime.registryDeclarations, runtime.registeredMethodsByRegistry]) {
215
+ for (const registryFingerprint of ledger.keys()) {
216
+ if (ledger.size <= maxRegistryDeclarations) break;
217
+ if (!protectedRegistries.has(registryFingerprint)) {
218
+ const timer = runtime.runtimeObservationTimers.get(registryFingerprint);
219
+ if (timer) {
220
+ clearTimeout(timer.timer);
221
+ runtime.runtimeObservationTimers.delete(registryFingerprint);
222
+ }
223
+ ledger.delete(registryFingerprint);
224
+ }
225
+ }
226
+ }
227
+ };
228
+
85
229
  export function createBncrRegisterRuntime() {
86
230
  const gatewayMethodDispatchers: Record<
87
231
  BncrGatewayMethodName,
@@ -138,11 +282,32 @@ export function createBncrRegisterRuntime() {
138
282
  if (!p[gatewayRuntimeSymbol]) {
139
283
  p[gatewayRuntimeSymbol] = {
140
284
  registeredMethodsByRegistry: new Map<string, Set<BncrGatewayMethodName>>(),
141
- serviceRegistered: false,
142
- channelRegistered: false,
285
+ registryDeclarations: new Map<string, RegistryDeclaration>(),
286
+ runtimeObservationTimers: new Map<string, RegistryRuntimeObservationTimer>(),
287
+ lifecycle: createRegisterLifecycleState(),
143
288
  };
144
289
  }
145
- return p[gatewayRuntimeSymbol]!;
290
+ const runtime = p[gatewayRuntimeSymbol] as LegacyBncrGatewayRuntime;
291
+ if (!(runtime.registeredMethodsByRegistry instanceof Map)) {
292
+ runtime.registeredMethodsByRegistry = new Map<string, Set<BncrGatewayMethodName>>();
293
+ }
294
+ if (!(runtime.registryDeclarations instanceof Map)) {
295
+ runtime.registryDeclarations = new Map<string, RegistryDeclaration>();
296
+ } else {
297
+ for (const [registryFingerprint, declaration] of runtime.registryDeclarations) {
298
+ const normalized = normalizeRegistryDeclarationInPlace(declaration);
299
+ if (normalized !== declaration) {
300
+ runtime.registryDeclarations.set(registryFingerprint, normalized);
301
+ }
302
+ }
303
+ }
304
+ if (!(runtime.runtimeObservationTimers instanceof Map)) {
305
+ runtime.runtimeObservationTimers = new Map<string, RegistryRuntimeObservationTimer>();
306
+ }
307
+ if (!runtime.lifecycle) {
308
+ runtime.lifecycle = createRegisterLifecycleState();
309
+ }
310
+ return runtime as BncrGatewayRuntime;
146
311
  };
147
312
 
148
313
  const getBridgeRegisterStateCarrier = (bridge: BridgeSingleton): BridgeRegisterStateCarrier =>
@@ -154,6 +319,8 @@ export function createBncrRegisterRuntime() {
154
319
  getGatewayRuntime,
155
320
  gatewayMethodDispatchers,
156
321
  getBridgeRegisterStateCarrier,
322
+ getRegistryRuntimeObservation: (registryFingerprint) =>
323
+ getRegistryRuntimeObservation(registryFingerprint),
157
324
  });
158
325
 
159
326
  const getBridgeOwner = (api: OpenClawPluginApi, loaded: LoadedRuntime): BridgeOwner => {
@@ -164,6 +331,9 @@ export function createBncrRegisterRuntime() {
164
331
  apiInstanceId: meta.apiInstanceId || 'unknown',
165
332
  registryFingerprint: meta.registryFingerprint || 'unknown',
166
333
  registrationMode: meta.registrationMode,
334
+ pluginVersion,
335
+ pluginRoot,
336
+ pluginFile,
167
337
  };
168
338
  };
169
339
 
@@ -175,24 +345,757 @@ export function createBncrRegisterRuntime() {
175
345
  getBridgeOwner,
176
346
  });
177
347
 
348
+ const toBridgeOwner = (owner: LifecycleOwner): BridgeOwner => ({
349
+ moduleEpoch: owner.bridgeGeneration.moduleEpoch,
350
+ bridgeFactoryId: owner.bridgeGeneration.bridgeFactoryId,
351
+ apiInstanceId: owner.key.apiInstanceId,
352
+ registryFingerprint: owner.key.registryFingerprint,
353
+ registrationMode: owner.bridgeGeneration.registrationMode,
354
+ pluginVersion: owner.bridgeGeneration.pluginVersion,
355
+ pluginRoot: owner.bridgeGeneration.pluginRoot,
356
+ pluginFile: owner.bridgeGeneration.pluginFile,
357
+ });
358
+
178
359
  const getCurrentBridge = (): BridgeSingletonWithState => {
179
360
  const bridge = getGatewayRuntime().currentBridge;
180
361
  if (!bridge) throw new Error('bncr current bridge unavailable');
181
362
  return bridge;
182
363
  };
183
364
 
365
+ const clearRegistryRuntimeObservationTimer = (registryFingerprint: string) => {
366
+ const runtime = getGatewayRuntime();
367
+ const entry = runtime.runtimeObservationTimers.get(registryFingerprint);
368
+ if (entry) {
369
+ clearTimeout(entry.timer);
370
+ runtime.runtimeObservationTimers.delete(registryFingerprint);
371
+ }
372
+ };
373
+
374
+ const readRegistryRuntimeProbe = (): RegistryRuntimeProbe => {
375
+ const bridge = getGatewayRuntime().currentBridge as
376
+ | (BridgeSingletonWithState & {
377
+ getRuntimeObservation?: () => RegistryRuntimeProbe | undefined;
378
+ })
379
+ | undefined;
380
+ const probe = bridge?.getRuntimeObservation?.();
381
+ return probe && typeof probe === 'object' ? probe : {};
382
+ };
383
+
384
+ const isRegistryRuntimeLifecycleActive = (
385
+ registryFingerprint: string,
386
+ lifecycleGeneration: number,
387
+ ) => {
388
+ const active = getGatewayRuntime().lifecycle.active;
389
+ return Boolean(
390
+ active &&
391
+ active.key.registryFingerprint === registryFingerprint &&
392
+ active.key.generation === lifecycleGeneration &&
393
+ (active.phase === 'active' || active.phase === 'registering'),
394
+ );
395
+ };
396
+
397
+ const probeRegistryRuntimeObservation = (
398
+ registryFingerprint: string,
399
+ bridgeGenerationKey: string,
400
+ lifecycleGeneration: number,
401
+ now = Date.now(),
402
+ ): RegistryRuntimeObservation | null => {
403
+ try {
404
+ const runtime = getGatewayRuntime();
405
+ const declaration = runtime.registryDeclarations.get(registryFingerprint);
406
+ if (
407
+ !declaration ||
408
+ declaration.bridgeGenerationKey !== bridgeGenerationKey ||
409
+ declaration.lifecycleGeneration !== lifecycleGeneration ||
410
+ !declaration.runtimeObservation
411
+ ) {
412
+ return null;
413
+ }
414
+
415
+ const decision = evaluateRegistryRuntimeObservation({
416
+ observation: declaration.runtimeObservation,
417
+ now,
418
+ lifecycleActive:
419
+ declaration.registration === 'complete' &&
420
+ declaration.state === 'active' &&
421
+ isRegistryRuntimeLifecycleActive(registryFingerprint, lifecycleGeneration),
422
+ probe: readRegistryRuntimeProbe(),
423
+ });
424
+ declaration.runtimeObservation = decision.observation;
425
+
426
+ if (decision.transitionedToPolling) {
427
+ emitBncrLogLine(
428
+ 'warn',
429
+ `[bncr] runtime observation timeout registry=${registryFingerprint} generation=${lifecycleGeneration} service=${decision.observation.serviceObservedAt !== null} channel=${decision.observation.channelObservedAt !== null} action=poll intervalMs=${REGISTRY_RUNTIME_OBSERVATION_POLL_MS}`,
430
+ );
431
+ }
432
+ if (decision.transitionedToReady) {
433
+ emitBncrLogLine(
434
+ 'info',
435
+ `[bncr] runtime observation ready registry=${registryFingerprint} generation=${lifecycleGeneration} probes=${decision.observation.probeCount}`,
436
+ );
437
+ }
438
+
439
+ if (decision.action === 'wait' || decision.action === 'poll') {
440
+ clearRegistryRuntimeObservationTimer(registryFingerprint);
441
+ const delay =
442
+ decision.action === 'poll'
443
+ ? REGISTRY_RUNTIME_OBSERVATION_POLL_MS
444
+ : Math.max(0, decision.observation.deadlineAt - now);
445
+ const timer = setTimeout(() => {
446
+ const current = runtime.runtimeObservationTimers.get(registryFingerprint);
447
+ if (current?.timer !== timer) return;
448
+ runtime.runtimeObservationTimers.delete(registryFingerprint);
449
+ probeRegistryRuntimeObservation(
450
+ registryFingerprint,
451
+ bridgeGenerationKey,
452
+ lifecycleGeneration,
453
+ Date.now(),
454
+ );
455
+ }, delay);
456
+ timer.unref?.();
457
+ runtime.runtimeObservationTimers.set(registryFingerprint, {
458
+ timer,
459
+ bridgeGenerationKey,
460
+ lifecycleGeneration,
461
+ });
462
+ } else {
463
+ clearRegistryRuntimeObservationTimer(registryFingerprint);
464
+ }
465
+
466
+ return { ...decision.observation };
467
+ } catch (error) {
468
+ try {
469
+ emitBncrLogLine(
470
+ 'warn',
471
+ `[bncr] runtime observation probe failed registry=${registryFingerprint} generation=${lifecycleGeneration} error=${error instanceof Error ? error.message : String(error)}`,
472
+ );
473
+ } catch {
474
+ // Runtime observation is diagnostic-only and must never affect startup.
475
+ }
476
+ try {
477
+ const runtime = getGatewayRuntime();
478
+ const declaration = runtime.registryDeclarations.get(registryFingerprint);
479
+ if (
480
+ declaration?.bridgeGenerationKey === bridgeGenerationKey &&
481
+ declaration.lifecycleGeneration === lifecycleGeneration &&
482
+ declaration.registration === 'complete' &&
483
+ declaration.state === 'active'
484
+ ) {
485
+ clearRegistryRuntimeObservationTimer(registryFingerprint);
486
+ const timer = setTimeout(() => {
487
+ const current = runtime.runtimeObservationTimers.get(registryFingerprint);
488
+ if (current?.timer !== timer) return;
489
+ runtime.runtimeObservationTimers.delete(registryFingerprint);
490
+ probeRegistryRuntimeObservation(
491
+ registryFingerprint,
492
+ bridgeGenerationKey,
493
+ lifecycleGeneration,
494
+ Date.now(),
495
+ );
496
+ }, REGISTRY_RUNTIME_OBSERVATION_POLL_MS);
497
+ timer.unref?.();
498
+ runtime.runtimeObservationTimers.set(registryFingerprint, {
499
+ timer,
500
+ bridgeGenerationKey,
501
+ lifecycleGeneration,
502
+ });
503
+ }
504
+ } catch {
505
+ // A failed diagnostic probe must not affect service or channel startup.
506
+ }
507
+ return null;
508
+ }
509
+ };
510
+
511
+ const startRegistryRuntimeObservation = (
512
+ registryFingerprint: string,
513
+ bridgeGenerationKey: string,
514
+ lifecycleGeneration: number,
515
+ now = Date.now(),
516
+ reset = false,
517
+ ) => {
518
+ const runtime = getGatewayRuntime();
519
+ const declaration = runtime.registryDeclarations.get(registryFingerprint);
520
+ if (
521
+ !declaration ||
522
+ declaration.bridgeGenerationKey !== bridgeGenerationKey ||
523
+ declaration.lifecycleGeneration !== lifecycleGeneration ||
524
+ declaration.registration !== 'complete' ||
525
+ declaration.state !== 'active'
526
+ ) {
527
+ return null;
528
+ }
529
+ if (!reset && declaration.runtimeObservation) {
530
+ return { ...declaration.runtimeObservation };
531
+ }
532
+
533
+ clearRegistryRuntimeObservationTimer(registryFingerprint);
534
+ declaration.runtimeObservation = createRegistryRuntimeObservation(now);
535
+ return probeRegistryRuntimeObservation(
536
+ registryFingerprint,
537
+ bridgeGenerationKey,
538
+ lifecycleGeneration,
539
+ now,
540
+ );
541
+ };
542
+
543
+ const retireRegistryRuntimeObservation = (registryFingerprint: string, now = Date.now()) => {
544
+ clearRegistryRuntimeObservationTimer(registryFingerprint);
545
+ const declaration = getGatewayRuntime().registryDeclarations.get(registryFingerprint);
546
+ if (!declaration?.runtimeObservation) return;
547
+ declaration.runtimeObservation = {
548
+ ...declaration.runtimeObservation,
549
+ phase: 'retired',
550
+ retiredAt: declaration.runtimeObservation.retiredAt ?? now,
551
+ };
552
+ };
553
+
554
+ const getRegistryRuntimeObservation = (registryFingerprint: string) => {
555
+ const runtime = getGatewayRuntime();
556
+ const declaration = runtime.registryDeclarations.get(registryFingerprint);
557
+ if (declaration?.state === 'active' && declaration.runtimeObservation) {
558
+ return { ...declaration.runtimeObservation };
559
+ }
560
+
561
+ const activeRegistry = runtime.lifecycle.active?.key.registryFingerprint;
562
+ if (activeRegistry && activeRegistry !== registryFingerprint) {
563
+ const activeObservation =
564
+ runtime.registryDeclarations.get(activeRegistry)?.runtimeObservation;
565
+ if (activeObservation) return { ...activeObservation };
566
+ }
567
+
568
+ return declaration?.runtimeObservation ? { ...declaration.runtimeObservation } : null;
569
+ };
570
+
571
+ const planLifecycle = (api: OpenClawPluginApi, now = Date.now()): LifecycleAdoptionDecision => {
572
+ const loaded = loadBncrRuntimeSync();
573
+ const owner = getBridgeOwner(api, loaded);
574
+ const runtime = getGatewayRuntime();
575
+ const input: LifecyclePlanInput = {
576
+ apiInstanceId: owner.apiInstanceId,
577
+ registryFingerprint: owner.registryFingerprint,
578
+ bridgeGeneration: {
579
+ moduleEpoch: owner.moduleEpoch,
580
+ bridgeFactoryId: owner.bridgeFactoryId,
581
+ pluginVersion: owner.pluginVersion || 'unknown',
582
+ registrationMode: owner.registrationMode || 'unknown',
583
+ pluginRoot: owner.pluginRoot || pluginRoot,
584
+ pluginFile: owner.pluginFile || pluginFile,
585
+ },
586
+ now,
587
+ };
588
+ const decision = planLifecycleAdoption(runtime.lifecycle, input);
589
+ const declaration = runtime.registryDeclarations.get(owner.registryFingerprint);
590
+ if (!declaration) return decision;
591
+
592
+ const bridgeGenerationKey = getBridgeGenerationKey(input.bridgeGeneration);
593
+ if (!declaration.bridgeGenerationKey) {
594
+ // Legacy runtimes did not persist the bridge identity in the ledger.
595
+ declaration.bridgeGenerationKey = bridgeGenerationKey;
596
+ } else if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
597
+ return {
598
+ kind: 'reject',
599
+ owner: decision.owner,
600
+ reason: 'registration-generation-conflict',
601
+ };
602
+ }
603
+ if (declaration.registration === 'failed') {
604
+ return {
605
+ kind: 'reject',
606
+ owner: decision.owner,
607
+ reason: 'registration-failed',
608
+ };
609
+ }
610
+ if (declaration.registration === 'pending') {
611
+ return {
612
+ kind: 'reject',
613
+ owner: decision.owner,
614
+ reason: 'registration-in-progress',
615
+ };
616
+ }
617
+ return decision;
618
+ };
619
+
620
+ const commitLifecycle = (decision: LifecycleAdoptionDecision) => {
621
+ const runtime = getGatewayRuntime();
622
+ if (decision.kind === 'initialize' || decision.kind === 'takeover') {
623
+ runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
624
+ }
625
+ const owner = commitLifecycleAdoption(runtime.lifecycle, decision);
626
+ const requestedRegistry = decision.owner.key.registryFingerprint;
627
+ const requestedGeneration = decision.owner.key.generation;
628
+ const requestedBridgeGenerationKey = getBridgeGenerationKey(decision.owner.bridgeGeneration);
629
+
630
+ if (decision.kind === 'initialize' || decision.kind === 'takeover') {
631
+ for (const [registryFingerprint, declaration] of runtime.registryDeclarations) {
632
+ if (registryFingerprint !== owner?.key.registryFingerprint) {
633
+ declaration.state = 'retired';
634
+ retireRegistryRuntimeObservation(registryFingerprint);
635
+ }
636
+ }
637
+ const declaration = getRegistryDeclaration(
638
+ requestedRegistry,
639
+ requestedBridgeGenerationKey,
640
+ requestedGeneration,
641
+ );
642
+ if (
643
+ declaration.bridgeGenerationKey &&
644
+ declaration.bridgeGenerationKey !== requestedBridgeGenerationKey
645
+ ) {
646
+ declaration.service = 'missing';
647
+ declaration.channel = 'missing';
648
+ }
649
+ declaration.bridgeGenerationKey = requestedBridgeGenerationKey;
650
+ declaration.lifecycleGeneration = requestedGeneration;
651
+ declaration.registration = 'pending';
652
+ declaration.state = 'active';
653
+ declaration.runtimeObservation = undefined;
654
+ } else if (decision.kind === 'defer') {
655
+ const declaration = getRegistryDeclaration(
656
+ requestedRegistry,
657
+ requestedBridgeGenerationKey,
658
+ requestedGeneration,
659
+ );
660
+ if (
661
+ declaration.bridgeGenerationKey &&
662
+ declaration.bridgeGenerationKey !== requestedBridgeGenerationKey
663
+ ) {
664
+ declaration.service = 'missing';
665
+ declaration.channel = 'missing';
666
+ }
667
+ declaration.bridgeGenerationKey = requestedBridgeGenerationKey;
668
+ declaration.lifecycleGeneration = requestedGeneration;
669
+ declaration.registration = 'pending';
670
+ declaration.state = 'shadow';
671
+ declaration.runtimeObservation = undefined;
672
+ } else if (decision.kind === 'duplicate') {
673
+ const existing = runtime.registryDeclarations.get(requestedRegistry);
674
+ if (!existing) {
675
+ const declaration = getRegistryDeclaration(
676
+ requestedRegistry,
677
+ requestedBridgeGenerationKey,
678
+ requestedGeneration,
679
+ );
680
+ declaration.registration = 'pending';
681
+ declaration.state = 'shadow';
682
+ } else if (
683
+ !existing.bridgeGenerationKey ||
684
+ existing.bridgeGenerationKey === requestedBridgeGenerationKey
685
+ ) {
686
+ existing.bridgeGenerationKey ||= requestedBridgeGenerationKey;
687
+ const activeRegistry = runtime.lifecycle.active?.key.registryFingerprint;
688
+ const pendingRegistry = runtime.lifecycle.pending?.key.registryFingerprint;
689
+ if (requestedRegistry === activeRegistry && runtime.lifecycle.active?.phase !== 'stopped') {
690
+ existing.state = 'active';
691
+ } else if (requestedRegistry === pendingRegistry) {
692
+ existing.state = 'shadow';
693
+ }
694
+ }
695
+ }
696
+ pruneRegistryLedgers(runtime);
697
+ return owner;
698
+ };
699
+
700
+ const adoptLifecycleBridge = (
701
+ api: OpenClawPluginApi,
702
+ owner: LifecycleOwner,
703
+ mode: 'reuse' | 'replace' | 'create',
704
+ ) => {
705
+ try {
706
+ const adopted = bridgeSingletonManager.adoptBridgeSingleton(api, toBridgeOwner(owner), mode);
707
+ getGatewayRuntime().currentBridge = adopted.bridge;
708
+ return adopted;
709
+ } catch (error) {
710
+ const lifecycle = getGatewayRuntime().lifecycle;
711
+ if (
712
+ lifecycle.active?.key.generation === owner.key.generation &&
713
+ lifecycle.active.key.registryFingerprint === owner.key.registryFingerprint
714
+ ) {
715
+ failLifecycle(lifecycle, owner.key.registryFingerprint, error, owner.key.generation);
716
+ const declaration = getRegistryDeclaration(
717
+ owner.key.registryFingerprint,
718
+ getBridgeGenerationKey(owner.bridgeGeneration),
719
+ owner.key.generation,
720
+ );
721
+ declaration.registration = 'failed';
722
+ declaration.state = 'retired';
723
+ retireRegistryRuntimeObservation(owner.key.registryFingerprint);
724
+ }
725
+ throw error;
726
+ }
727
+ };
728
+
729
+ const beginRetirement = (
730
+ registryFingerprint: string,
731
+ expectedGeneration: number,
732
+ now = Date.now(),
733
+ ) => {
734
+ const retirement = beginLifecycleRetirement(
735
+ getGatewayRuntime().lifecycle,
736
+ registryFingerprint,
737
+ now,
738
+ expectedGeneration,
739
+ );
740
+ if (retirement) {
741
+ retireRegistryRuntimeObservation(registryFingerprint, now);
742
+ }
743
+ return retirement;
744
+ };
745
+
746
+ const settleRetirement = (
747
+ registryFingerprint: string,
748
+ outcome: LifecycleRetirementOutcome,
749
+ expectedGeneration: number,
750
+ now = Date.now(),
751
+ ) => {
752
+ const settled = settleLifecycleRetirement(
753
+ getGatewayRuntime().lifecycle,
754
+ registryFingerprint,
755
+ outcome,
756
+ now,
757
+ expectedGeneration,
758
+ );
759
+ if (settled) {
760
+ const lifecycle = getGatewayRuntime().lifecycle;
761
+ const declaration = getRegistryDeclaration(registryFingerprint);
762
+ if (!outcome.ok) {
763
+ declaration.state = 'retired';
764
+ retireRegistryRuntimeObservation(registryFingerprint, now);
765
+ const pendingRegistry = lifecycle.pending?.key.registryFingerprint;
766
+ if (pendingRegistry && pendingRegistry !== registryFingerprint) {
767
+ getRegistryDeclaration(pendingRegistry).state = 'retired';
768
+ retireRegistryRuntimeObservation(pendingRegistry, now);
769
+ }
770
+ } else {
771
+ declaration.state =
772
+ lifecycle.pending?.key.registryFingerprint === registryFingerprint ? 'shadow' : 'retired';
773
+ retireRegistryRuntimeObservation(registryFingerprint, now);
774
+ }
775
+ }
776
+ return settled;
777
+ };
778
+
779
+ const getLifecycleBridgeForStart = async (
780
+ api: OpenClawPluginApi,
781
+ registryFingerprint: string,
782
+ expectedGeneration: number,
783
+ ) => {
784
+ const runtime = getGatewayRuntime();
785
+ const state = runtime.lifecycle;
786
+ if (runtime.registryDeclarations.get(registryFingerprint)?.registration === 'failed') {
787
+ state.staleCallbackSuppressions += 1;
788
+ return null;
789
+ }
790
+
791
+ if (isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration)) {
792
+ getRegistryDeclaration(registryFingerprint).state = 'active';
793
+ startRegistryRuntimeObservation(
794
+ registryFingerprint,
795
+ getBridgeGenerationKey(state.active!.bridgeGeneration),
796
+ expectedGeneration,
797
+ );
798
+ return runtime.currentBridge || null;
799
+ }
800
+
801
+ const pending = getPendingLifecycle(state);
802
+ if (
803
+ pending?.key.registryFingerprint === registryFingerprint &&
804
+ pending.key.generation === expectedGeneration
805
+ ) {
806
+ const pendingMode = state.pendingMode || 'reuse';
807
+ const retirement = state.retirement;
808
+ const outcome = retirement ? await retirement : { ok: true as const };
809
+ if (!outcome.ok) {
810
+ throw new Error(`bncr lifecycle predecessor stop failed: ${outcome.error}`);
811
+ }
812
+ const activated = activatePendingLifecycle(
813
+ state,
814
+ registryFingerprint,
815
+ Date.now(),
816
+ expectedGeneration,
817
+ );
818
+ if (activated) {
819
+ getRegistryDeclaration(registryFingerprint).state = 'active';
820
+ const bridge = adoptLifecycleBridge(api, activated, pendingMode).bridge;
821
+ runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
822
+ startRegistryRuntimeObservation(
823
+ registryFingerprint,
824
+ getBridgeGenerationKey(activated.bridgeGeneration),
825
+ expectedGeneration,
826
+ Date.now(),
827
+ true,
828
+ );
829
+ return bridge;
830
+ }
831
+ // Another concurrent start for this same pending generation may already
832
+ // have won the activation race while this call awaited the barrier.
833
+ }
834
+
835
+ const reactivated = reactivateStoppedLifecycle(
836
+ state,
837
+ registryFingerprint,
838
+ Date.now(),
839
+ expectedGeneration,
840
+ );
841
+ if (reactivated) {
842
+ getRegistryDeclaration(registryFingerprint).state = 'active';
843
+ const bridge = adoptLifecycleBridge(api, reactivated, 'reuse').bridge;
844
+ runtime.gatewayMethodDispatchers = gatewayMethodDispatchers;
845
+ startRegistryRuntimeObservation(
846
+ registryFingerprint,
847
+ getBridgeGenerationKey(reactivated.bridgeGeneration),
848
+ expectedGeneration,
849
+ Date.now(),
850
+ true,
851
+ );
852
+ return bridge;
853
+ }
854
+
855
+ // Re-read ownership after every await/activation attempt: a concurrent
856
+ // start for the same generation can legitimately have activated it first.
857
+ if (isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration)) {
858
+ getRegistryDeclaration(registryFingerprint).state = 'active';
859
+ startRegistryRuntimeObservation(
860
+ registryFingerprint,
861
+ getBridgeGenerationKey(state.active!.bridgeGeneration),
862
+ expectedGeneration,
863
+ );
864
+ return runtime.currentBridge || null;
865
+ }
866
+
867
+ state.staleCallbackSuppressions += 1;
868
+ return null;
869
+ };
870
+
871
+ const failLifecycleStart = (
872
+ registryFingerprint: string,
873
+ error: unknown,
874
+ expectedGeneration: number,
875
+ ) => {
876
+ const failed = failLifecycle(
877
+ getGatewayRuntime().lifecycle,
878
+ registryFingerprint,
879
+ error,
880
+ expectedGeneration,
881
+ );
882
+ if (failed) {
883
+ getRegistryDeclaration(registryFingerprint).state = 'retired';
884
+ retireRegistryRuntimeObservation(registryFingerprint);
885
+ }
886
+ return failed;
887
+ };
888
+
889
+ /*
890
+ * Best-effort repair for a registration that committed its lifecycle but
891
+ * failed before finishing its service/channel declarations. Nothing of this
892
+ * generation has started yet, so returning to a normal state is safe and
893
+ * lets the next registration recover instead of being suppressed forever.
894
+ */
895
+ const recoverLifecycleRegistration = (decision: LifecycleAdoptionDecision, error: unknown) => {
896
+ const runtime = getGatewayRuntime();
897
+ const state = runtime.lifecycle;
898
+ const owner = decision.owner;
899
+ const ownsActive = Boolean(
900
+ state.active &&
901
+ state.active.key.registryFingerprint === owner.key.registryFingerprint &&
902
+ state.active.key.generation === owner.key.generation,
903
+ );
904
+ const ownsPending = Boolean(
905
+ state.pending &&
906
+ state.pending.key.registryFingerprint === owner.key.registryFingerprint &&
907
+ state.pending.key.generation === owner.key.generation,
908
+ );
909
+
910
+ let outcome: 'stopped' | 'pending-dropped' | 'failed' | 'none' = 'none';
911
+ if (ownsActive && (decision.kind === 'initialize' || decision.kind === 'takeover')) {
912
+ // Keep the generation bound but inert. Clearing `active` outright would
913
+ // re-open the "no declaration + no active lifecycle" dispatch fallback
914
+ // for stale gateway method handlers; `stopped` is recoverable by the
915
+ // next takeover and keeps dispatch gated.
916
+ if (state.active) {
917
+ state.active.phase = 'stopped';
918
+ state.active.stopRequestedAt = null;
919
+ state.active.stoppedAt = Date.now();
920
+ state.active.failure = null;
921
+ }
922
+ state.pending = undefined;
923
+ state.pendingMode = undefined;
924
+ state.retirement = undefined;
925
+ state.resolveRetirement = undefined;
926
+ outcome = 'stopped';
927
+ } else if (ownsPending && decision.kind === 'defer') {
928
+ state.pending = undefined;
929
+ state.pendingMode = undefined;
930
+ outcome = 'pending-dropped';
931
+ } else if (ownsActive) {
932
+ failLifecycle(state, owner.key.registryFingerprint, error, owner.key.generation);
933
+ outcome = 'failed';
934
+ }
935
+
936
+ const declaration = runtime.registryDeclarations.get(owner.key.registryFingerprint);
937
+ const ownsDeclaration = Boolean(
938
+ declaration &&
939
+ declaration.bridgeGenerationKey === getBridgeGenerationKey(owner.bridgeGeneration) &&
940
+ declaration.lifecycleGeneration === owner.key.generation,
941
+ );
942
+ if (ownsDeclaration && declaration) {
943
+ // The registration call did not complete successfully, even if it had
944
+ // already published all declarations. Keep the whole scope fail-closed;
945
+ // otherwise a stopped lifecycle can leave an active gateway declaration.
946
+ declaration.registration = 'failed';
947
+ declaration.state = 'retired';
948
+ retireRegistryRuntimeObservation(owner.key.registryFingerprint);
949
+ }
950
+ return outcome;
951
+ };
952
+
953
+ const canStopLifecycleRegistry = (registryFingerprint: string, expectedGeneration: number) => {
954
+ const state = getGatewayRuntime().lifecycle;
955
+ const allowed = isLifecycleRegistryStoppable(state, registryFingerprint, expectedGeneration);
956
+ if (!allowed) {
957
+ state.staleCallbackSuppressions += 1;
958
+ }
959
+ return allowed;
960
+ };
961
+
962
+ const canStartLifecycleRegistry = (registryFingerprint: string, expectedGeneration: number) => {
963
+ const state = getGatewayRuntime().lifecycle;
964
+ const allowed =
965
+ getGatewayRuntime().registryDeclarations.get(registryFingerprint)?.registration !==
966
+ 'failed' && isLifecycleRegistryActive(state, registryFingerprint, expectedGeneration);
967
+ if (!allowed) {
968
+ state.staleCallbackSuppressions += 1;
969
+ }
970
+ return allowed;
971
+ };
972
+
973
+ const isLifecycleRegistryCurrent = (registryFingerprint: string, expectedGeneration: number) =>
974
+ isLifecycleRegistryActive(
975
+ getGatewayRuntime().lifecycle,
976
+ registryFingerprint,
977
+ expectedGeneration,
978
+ );
979
+
980
+ const getRegistryDeclaration = (
981
+ registryFingerprint: string,
982
+ bridgeGenerationKey = '',
983
+ lifecycleGeneration = 0,
984
+ ) => {
985
+ const runtime = getGatewayRuntime();
986
+ let declaration = runtime.registryDeclarations.get(registryFingerprint);
987
+ if (!declaration) {
988
+ declaration = {
989
+ bridgeGenerationKey,
990
+ lifecycleGeneration,
991
+ service: 'missing',
992
+ channel: 'missing',
993
+ registration: 'pending',
994
+ state: 'shadow',
995
+ };
996
+ runtime.registryDeclarations.set(registryFingerprint, declaration);
997
+ } else {
998
+ declaration = normalizeRegistryDeclarationInPlace(declaration);
999
+ if (!declaration.bridgeGenerationKey) {
1000
+ declaration.bridgeGenerationKey = bridgeGenerationKey;
1001
+ }
1002
+ if (!declaration.lifecycleGeneration) {
1003
+ declaration.lifecycleGeneration = lifecycleGeneration;
1004
+ }
1005
+ }
1006
+ return declaration;
1007
+ };
1008
+
1009
+ const markRegistryServiceDeclared = (
1010
+ registryFingerprint: string,
1011
+ bridgeGenerationKey: string,
1012
+ lifecycleGeneration: number,
1013
+ ) => {
1014
+ const declaration = getRegistryDeclaration(
1015
+ registryFingerprint,
1016
+ bridgeGenerationKey,
1017
+ lifecycleGeneration,
1018
+ );
1019
+ if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
1020
+ throw new Error(`bncr service declaration scope mismatch: ${registryFingerprint}`);
1021
+ }
1022
+ declaration.lifecycleGeneration = lifecycleGeneration;
1023
+ declaration.service = 'declared';
1024
+ return declaration;
1025
+ };
1026
+
1027
+ const markRegistryChannelDeclared = (
1028
+ registryFingerprint: string,
1029
+ bridgeGenerationKey: string,
1030
+ lifecycleGeneration: number,
1031
+ ) => {
1032
+ const declaration = getRegistryDeclaration(
1033
+ registryFingerprint,
1034
+ bridgeGenerationKey,
1035
+ lifecycleGeneration,
1036
+ );
1037
+ if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
1038
+ throw new Error(`bncr channel declaration scope mismatch: ${registryFingerprint}`);
1039
+ }
1040
+ declaration.lifecycleGeneration = lifecycleGeneration;
1041
+ declaration.channel = 'declared';
1042
+ return declaration;
1043
+ };
1044
+
1045
+ const markRegistryRegistrationComplete = (
1046
+ registryFingerprint: string,
1047
+ bridgeGenerationKey: string,
1048
+ lifecycleGeneration: number,
1049
+ ) => {
1050
+ const declaration = getRegistryDeclaration(
1051
+ registryFingerprint,
1052
+ bridgeGenerationKey,
1053
+ lifecycleGeneration,
1054
+ );
1055
+ if (declaration.bridgeGenerationKey !== bridgeGenerationKey) {
1056
+ throw new Error(`bncr register scope mismatch: ${registryFingerprint}`);
1057
+ }
1058
+ // Duplicate registrations do not redeclare callbacks. Keep the generation
1059
+ // that owns the existing service/channel callbacks instead of advancing the
1060
+ // declaration to a new planning generation.
1061
+ declaration.lifecycleGeneration ||= lifecycleGeneration;
1062
+ declaration.registration = 'complete';
1063
+ if (declaration.state === 'active') {
1064
+ startRegistryRuntimeObservation(
1065
+ registryFingerprint,
1066
+ bridgeGenerationKey,
1067
+ declaration.lifecycleGeneration,
1068
+ );
1069
+ }
1070
+ return declaration;
1071
+ };
1072
+
184
1073
  return {
185
1074
  getRegisterMeta,
186
1075
  getGlobalRegisterTrace,
1076
+ noteRegisterTraceValue: rememberRegisterTraceValue,
1077
+ getBridgeGenerationKey,
187
1078
  getGatewayRuntime,
188
- shouldAdoptProcessOwner: (apiInstanceId: string, gatewayRuntime: BncrGatewayRuntime) =>
189
- shouldAdoptProcessOwner({
190
- apiInstanceId,
191
- serviceRegistered: gatewayRuntime.serviceRegistered,
192
- channelRegistered: gatewayRuntime.channelRegistered,
193
- serviceOwnerApiInstanceId: gatewayRuntime.serviceOwnerApiInstanceId,
194
- channelOwnerApiInstanceId: gatewayRuntime.channelOwnerApiInstanceId,
195
- }),
1079
+ planLifecycle,
1080
+ commitLifecycle,
1081
+ adoptLifecycleBridge,
1082
+ beginRetirement,
1083
+ settleRetirement,
1084
+ getLifecycleBridgeForStart,
1085
+ failLifecycleStart,
1086
+ recoverLifecycleRegistration,
1087
+ canStopLifecycleRegistry,
1088
+ canStartLifecycleRegistry,
1089
+ isLifecycleRegistryCurrent,
1090
+ getRegistryDeclaration,
1091
+ getRegistryRuntimeObservation,
1092
+ startRegistryRuntimeObservation,
1093
+ probeRegistryRuntimeObservation,
1094
+ retireRegistryRuntimeObservation,
1095
+ markRegistryServiceDeclared,
1096
+ markRegistryChannelDeclared,
1097
+ markRegistryRegistrationComplete,
1098
+ pruneRegistryLedgers: () => pruneRegistryLedgers(getGatewayRuntime()),
196
1099
  ensureGatewayMethodRegistered: gatewayMethodRegistry.ensureGatewayMethodRegistered,
197
1100
  getBridgeSingleton: bridgeSingletonManager.getBridgeSingleton,
198
1101
  getBridgeOwnerFromBridge: bridgeSingletonManager.getBridgeOwnerFromBridge,