@xmoxmo/bncr 0.6.6 → 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.
@@ -0,0 +1,170 @@
1
+ export const REGISTRY_RUNTIME_OBSERVATION_TIMEOUT_MS = 30_000;
2
+ export const REGISTRY_RUNTIME_OBSERVATION_POLL_MS = 5_000;
3
+
4
+ export type RegistryRuntimeObservationPhase = 'awaiting' | 'polling' | 'ready' | 'retired';
5
+
6
+ export type RegistryRuntimeProbe = {
7
+ serviceRunning?: boolean;
8
+ channelRunning?: boolean;
9
+ activeChannelAccounts?: number;
10
+ };
11
+
12
+ export type RegistryRuntimeObservation = {
13
+ phase: RegistryRuntimeObservationPhase;
14
+ startedAt: number;
15
+ deadlineAt: number;
16
+ pollingStartedAt: number | null;
17
+ readyAt: number | null;
18
+ retiredAt: number | null;
19
+ lastProbeAt: number | null;
20
+ probeCount: number;
21
+ serviceObservedAt: number | null;
22
+ channelObservedAt: number | null;
23
+ };
24
+
25
+ export type RegistryRuntimeObservationDecision = {
26
+ observation: RegistryRuntimeObservation;
27
+ action: 'wait' | 'poll' | 'ready' | 'retired';
28
+ transitionedToPolling: boolean;
29
+ transitionedToReady: boolean;
30
+ };
31
+
32
+ function finiteTimestamp(value: unknown): number | null {
33
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
34
+ }
35
+
36
+ function nonNegativeInteger(value: unknown): number {
37
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
38
+ }
39
+
40
+ export function createRegistryRuntimeObservation(now: number): RegistryRuntimeObservation {
41
+ return {
42
+ phase: 'awaiting',
43
+ startedAt: now,
44
+ deadlineAt: now + REGISTRY_RUNTIME_OBSERVATION_TIMEOUT_MS,
45
+ pollingStartedAt: null,
46
+ readyAt: null,
47
+ retiredAt: null,
48
+ lastProbeAt: null,
49
+ probeCount: 0,
50
+ serviceObservedAt: null,
51
+ channelObservedAt: null,
52
+ };
53
+ }
54
+
55
+ export function normalizeRegistryRuntimeObservation(
56
+ value: unknown,
57
+ ): RegistryRuntimeObservation | undefined {
58
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
59
+ const source = value as Record<string, unknown>;
60
+ const phase = source.phase;
61
+ if (phase !== 'awaiting' && phase !== 'polling' && phase !== 'ready' && phase !== 'retired') {
62
+ return undefined;
63
+ }
64
+ const startedAt = finiteTimestamp(source.startedAt);
65
+ const deadlineAt = finiteTimestamp(source.deadlineAt);
66
+ if (startedAt === null || deadlineAt === null) return undefined;
67
+ return {
68
+ phase,
69
+ startedAt,
70
+ deadlineAt,
71
+ pollingStartedAt: finiteTimestamp(source.pollingStartedAt),
72
+ readyAt: finiteTimestamp(source.readyAt),
73
+ retiredAt: finiteTimestamp(source.retiredAt),
74
+ lastProbeAt: finiteTimestamp(source.lastProbeAt),
75
+ probeCount: nonNegativeInteger(source.probeCount),
76
+ serviceObservedAt: finiteTimestamp(source.serviceObservedAt),
77
+ channelObservedAt: finiteTimestamp(source.channelObservedAt),
78
+ };
79
+ }
80
+
81
+ function retireObservation(
82
+ observation: RegistryRuntimeObservation,
83
+ now: number,
84
+ ): RegistryRuntimeObservationDecision {
85
+ return {
86
+ observation: {
87
+ ...observation,
88
+ phase: 'retired',
89
+ retiredAt: observation.retiredAt ?? now,
90
+ lastProbeAt: now,
91
+ probeCount: observation.probeCount + 1,
92
+ },
93
+ action: 'retired',
94
+ transitionedToPolling: false,
95
+ transitionedToReady: false,
96
+ };
97
+ }
98
+
99
+ export function evaluateRegistryRuntimeObservation(args: {
100
+ observation: RegistryRuntimeObservation;
101
+ now: number;
102
+ lifecycleActive: boolean;
103
+ probe: RegistryRuntimeProbe;
104
+ }): RegistryRuntimeObservationDecision {
105
+ const { observation, now, lifecycleActive, probe } = args;
106
+ if (observation.phase === 'retired') {
107
+ return {
108
+ observation,
109
+ action: 'retired',
110
+ transitionedToPolling: false,
111
+ transitionedToReady: false,
112
+ };
113
+ }
114
+ if (!lifecycleActive) {
115
+ return retireObservation(observation, now);
116
+ }
117
+ if (observation.phase === 'ready') {
118
+ return {
119
+ observation,
120
+ action: 'ready',
121
+ transitionedToPolling: false,
122
+ transitionedToReady: false,
123
+ };
124
+ }
125
+
126
+ const serviceRunning = probe.serviceRunning === true;
127
+ const channelRunning = probe.channelRunning === true || (probe.activeChannelAccounts ?? 0) > 0;
128
+ const serviceObservedAt = observation.serviceObservedAt ?? (serviceRunning ? now : null);
129
+ const channelObservedAt = observation.channelObservedAt ?? (channelRunning ? now : null);
130
+ const observed = {
131
+ ...observation,
132
+ lastProbeAt: now,
133
+ probeCount: observation.probeCount + 1,
134
+ serviceObservedAt,
135
+ channelObservedAt,
136
+ };
137
+
138
+ if (serviceRunning && channelRunning) {
139
+ return {
140
+ observation: {
141
+ ...observed,
142
+ phase: 'ready',
143
+ readyAt: observed.readyAt ?? now,
144
+ },
145
+ action: 'ready',
146
+ transitionedToPolling: false,
147
+ transitionedToReady: true,
148
+ };
149
+ }
150
+
151
+ if (now >= observation.deadlineAt) {
152
+ return {
153
+ observation: {
154
+ ...observed,
155
+ phase: 'polling',
156
+ pollingStartedAt: observed.pollingStartedAt ?? now,
157
+ },
158
+ action: 'poll',
159
+ transitionedToPolling: observation.phase === 'awaiting',
160
+ transitionedToReady: false,
161
+ };
162
+ }
163
+
164
+ return {
165
+ observation: observed,
166
+ action: 'wait',
167
+ transitionedToPolling: false,
168
+ transitionedToReady: false,
169
+ };
170
+ }
@@ -2,7 +2,6 @@ import {
2
2
  type BridgeOwner,
3
3
  type BridgeRegisterStateCarrier,
4
4
  hydrateBridgeRegisterState,
5
- sameBridgeOwner,
6
5
  snapshotBridgeRegisterState,
7
6
  } from './register-runtime-helpers.ts';
8
7
  import type { ChannelModule, LoadedRuntime } from './runtime-loader.ts';
@@ -17,6 +16,8 @@ type BridgeOwnedCarrier = BridgeRegisterStateCarrier & {
17
16
  };
18
17
  type GlobalBridgeStore = typeof globalThis & { __bncrBridge?: BridgeSingleton };
19
18
 
19
+ export type BridgeAdoptionMode = 'create' | 'reuse' | 'replace';
20
+
20
21
  function isBridgeOwner(value: unknown): value is BridgeOwner {
21
22
  return Boolean(
22
23
  value &&
@@ -48,10 +49,13 @@ export function createBncrBridgeSingletonManager(runtime: {
48
49
  return bridge;
49
50
  };
50
51
 
51
- const getBridgeSingleton = (api: OpenClawPluginApi) => {
52
+ const adoptBridgeSingleton = (
53
+ api: OpenClawPluginApi,
54
+ owner: BridgeOwner,
55
+ mode: BridgeAdoptionMode,
56
+ ) => {
52
57
  const loaded = runtime.loadBncrRuntimeSync();
53
58
  const g = globalThis as GlobalBridgeStore;
54
- const owner = runtime.getBridgeOwner(api, loaded);
55
59
  const previousOwnerRaw = g.__bncrBridge
56
60
  ? getBridgeOwnedCarrier(g.__bncrBridge)[runtime.bridgeOwnerSymbol]
57
61
  : undefined;
@@ -59,57 +63,56 @@ export function createBncrBridgeSingletonManager(runtime: {
59
63
 
60
64
  let created = false;
61
65
  let rebuilt = false;
66
+ let bridge: BridgeSingleton | undefined;
62
67
 
63
- if (g.__bncrBridge) {
64
- const mustRebuild =
65
- !sameBridgeOwner(previousOwner, owner) &&
66
- (previousOwner?.moduleEpoch !== owner.moduleEpoch ||
67
- previousOwner?.bridgeFactoryId !== owner.bridgeFactoryId ||
68
- previousOwner?.registrationMode !== owner.registrationMode ||
69
- previousOwner?.apiInstanceId !== owner.apiInstanceId ||
70
- previousOwner?.registryFingerprint !== owner.registryFingerprint);
71
-
72
- if (mustRebuild) {
73
- const registerState = snapshotBridgeRegisterState(
74
- getBridgeRegisterStateCarrier(g.__bncrBridge),
75
- );
76
- try {
77
- g.__bncrBridge.stopService?.();
78
- } catch {
79
- // ignore stop errors during hot-restart recovery
80
- }
81
- const rebuiltBridge = assignBridgeOwner(
82
- loaded.createBncrBridge(api, {
83
- pluginRoot: runtime.pluginRoot,
84
- pluginFile: runtime.pluginFile,
85
- }),
86
- owner,
87
- );
88
- hydrateBridgeRegisterState(getBridgeRegisterStateCarrier(rebuiltBridge), registerState);
89
- g.__bncrBridge = rebuiltBridge;
90
- created = true;
91
- rebuilt = true;
92
- } else {
93
- g.__bncrBridge.bindApi?.(api);
94
- assignBridgeOwner(g.__bncrBridge, owner);
95
- }
96
- } else {
97
- g.__bncrBridge = assignBridgeOwner(
68
+ if (!g.__bncrBridge || mode === 'create' || mode === 'replace') {
69
+ const registerState =
70
+ mode === 'replace' && g.__bncrBridge
71
+ ? snapshotBridgeRegisterState(getBridgeRegisterStateCarrier(g.__bncrBridge))
72
+ : null;
73
+ bridge = assignBridgeOwner(
98
74
  loaded.createBncrBridge(api, {
99
75
  pluginRoot: runtime.pluginRoot,
100
76
  pluginFile: runtime.pluginFile,
101
77
  }),
102
78
  owner,
103
79
  );
80
+ hydrateBridgeRegisterState(getBridgeRegisterStateCarrier(bridge), registerState);
81
+ g.__bncrBridge = bridge;
104
82
  created = true;
83
+ rebuilt = mode === 'replace' && Boolean(previousOwner);
84
+ } else {
85
+ bridge = g.__bncrBridge;
86
+ bridge.bindApi?.(api);
87
+ assignBridgeOwner(bridge, owner);
105
88
  }
106
89
 
107
- g.__bncrBridge?.bindRuntimePaths?.({
90
+ bridge.bindRuntimePaths?.({
108
91
  pluginRoot: runtime.pluginRoot,
109
92
  pluginFile: runtime.pluginFile,
110
93
  });
111
94
 
112
- return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
95
+ return { bridge, runtime: loaded, created, rebuilt, owner, previousOwner };
96
+ };
97
+
98
+ const getBridgeSingleton = (api: OpenClawPluginApi) => {
99
+ const loaded = runtime.loadBncrRuntimeSync();
100
+ const owner = runtime.getBridgeOwner(api, loaded);
101
+ const g = globalThis as GlobalBridgeStore;
102
+ const previous = getBridgeOwnerFromBridge(g.__bncrBridge);
103
+ const sameGeneration =
104
+ previous?.moduleEpoch === owner.moduleEpoch &&
105
+ previous?.bridgeFactoryId === owner.bridgeFactoryId &&
106
+ previous?.pluginVersion === owner.pluginVersion &&
107
+ previous?.registrationMode === owner.registrationMode &&
108
+ previous?.pluginRoot === owner.pluginRoot &&
109
+ previous?.pluginFile === owner.pluginFile;
110
+ const mode: BridgeAdoptionMode = !g.__bncrBridge
111
+ ? 'create'
112
+ : sameGeneration
113
+ ? 'reuse'
114
+ : 'replace';
115
+ return adoptBridgeSingleton(api, owner, mode);
113
116
  };
114
117
 
115
118
  const getExistingBridgeSingleton = () => {
@@ -129,6 +132,7 @@ export function createBncrBridgeSingletonManager(runtime: {
129
132
 
130
133
  return {
131
134
  assignBridgeOwner,
135
+ adoptBridgeSingleton,
132
136
  getBridgeRegisterStateCarrier,
133
137
  getBridgeSingleton,
134
138
  getExistingBridgeSingleton,