@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.
package/index.ts CHANGED
@@ -26,15 +26,32 @@ type BridgeSingletonWithOwner = NonNullable<
26
26
  >;
27
27
  type BridgeOwner = ReturnType<typeof registerRuntime.getBridgeOwnerFromBridge>;
28
28
  const {
29
+ adoptLifecycleBridge,
30
+ beginRetirement,
31
+ canStartLifecycleRegistry,
32
+ canStopLifecycleRegistry,
33
+ commitLifecycle,
29
34
  ensureGatewayMethodRegistered,
30
- getBridgeSingleton,
31
35
  getBridgeOwnerFromBridge,
36
+ getBridgeGenerationKey,
32
37
  getCurrentBridge,
33
38
  getExistingBridgeSingleton,
34
39
  getGatewayRuntime,
35
40
  getGlobalRegisterTrace,
41
+ getLifecycleBridgeForStart,
36
42
  getRegisterMeta,
37
- shouldAdoptProcessOwner,
43
+ getRegistryDeclaration,
44
+ isLifecycleRegistryCurrent,
45
+ markRegistryChannelDeclared,
46
+ markRegistryRegistrationComplete,
47
+ markRegistryServiceDeclared,
48
+ noteRegisterTraceValue,
49
+ planLifecycle,
50
+ probeRegistryRuntimeObservation,
51
+ pruneRegistryLedgers,
52
+ recoverLifecycleRegistration,
53
+ settleRetirement,
54
+ failLifecycleStart,
38
55
  } = registerRuntime;
39
56
 
40
57
  type BncrDebugConfigRoot = {
@@ -71,150 +88,293 @@ const plugin = {
71
88
  const registryFingerprint = meta.registryFingerprint || 'unknown';
72
89
  const sameApiAsPrevious = previousApiInstanceId === apiInstanceId;
73
90
  const sameRegistryAsPrevious = previousRegistryFingerprint === registryFingerprint;
74
- const firstSeenApi = !globalTrace.seenApiInstanceIds.has(apiInstanceId);
75
- const firstSeenRegistry = !globalTrace.seenRegistryFingerprints.has(registryFingerprint);
76
-
77
- const gatewayRuntime = getGatewayRuntime();
78
- const ownerDecision = shouldAdoptProcessOwner(apiInstanceId, gatewayRuntime);
79
-
80
- let bridge: BridgeSingletonWithOwner | undefined;
81
- let runtime: LoadedRuntime;
82
- let created = false;
83
- let rebuilt = false;
84
- let owner: BridgeOwner | undefined;
85
- let previousOwner: BridgeOwner | undefined;
86
-
87
- if (ownerDecision.adoptOwner) {
88
- const adopted = getBridgeSingleton(api);
89
- bridge = adopted.bridge;
90
- runtime = adopted.runtime;
91
- created = adopted.created;
92
- rebuilt = adopted.rebuilt;
93
- owner = adopted.owner;
94
- previousOwner = adopted.previousOwner;
95
- gatewayRuntime.currentBridge = bridge;
96
- if (rebuilt) {
97
- gatewayRuntime.serviceRegistered = false;
98
- gatewayRuntime.channelRegistered = false;
99
- gatewayRuntime.serviceOwnerApiInstanceId = undefined;
100
- gatewayRuntime.channelOwnerApiInstanceId = undefined;
91
+ const apiSeenRecently = globalTrace.seenApiInstanceIds.has(apiInstanceId);
92
+ const registrySeenRecently = globalTrace.seenRegistryFingerprints.has(registryFingerprint);
93
+
94
+ let ownerDecision: ReturnType<typeof planLifecycle> | undefined;
95
+
96
+ try {
97
+ const gatewayRuntime = getGatewayRuntime();
98
+ ownerDecision = planLifecycle(api);
99
+ const lifecycleOwner = commitLifecycle(ownerDecision);
100
+ let bridge: BridgeSingletonWithOwner | undefined;
101
+ const runtime: LoadedRuntime = loadBncrRuntimeSync();
102
+ let created = false;
103
+ let rebuilt = false;
104
+ let owner: BridgeOwner | undefined;
105
+ let previousOwner: BridgeOwner | undefined;
106
+ const shouldDeclareLifecycle =
107
+ ownerDecision.kind === 'initialize' ||
108
+ ownerDecision.kind === 'takeover' ||
109
+ ownerDecision.kind === 'defer';
110
+
111
+ if (ownerDecision.kind === 'initialize') {
112
+ if (lifecycleOwner) {
113
+ const adopted = adoptLifecycleBridge(api, lifecycleOwner, 'create');
114
+ bridge = adopted.bridge;
115
+ created = adopted.created;
116
+ rebuilt = adopted.rebuilt;
117
+ owner = adopted.owner;
118
+ previousOwner = adopted.previousOwner;
119
+ }
120
+ } else if (ownerDecision.kind === 'takeover') {
121
+ if (lifecycleOwner) {
122
+ const adopted = adoptLifecycleBridge(api, lifecycleOwner, ownerDecision.mode);
123
+ bridge = adopted.bridge;
124
+ created = adopted.created;
125
+ rebuilt = adopted.rebuilt;
126
+ owner = adopted.owner;
127
+ previousOwner = adopted.previousOwner;
128
+ }
129
+ } else if (ownerDecision.kind === 'defer') {
130
+ bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
131
+ previousOwner = getBridgeOwnerFromBridge(bridge);
132
+ owner = previousOwner;
133
+ } else {
134
+ bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
135
+ previousOwner = getBridgeOwnerFromBridge(bridge);
136
+ owner = previousOwner;
137
+ if (bridge && !gatewayRuntime.currentBridge) {
138
+ gatewayRuntime.currentBridge = bridge;
139
+ }
101
140
  }
102
- } else {
103
- runtime = loadBncrRuntimeSync();
104
- bridge = gatewayRuntime.currentBridge || getExistingBridgeSingleton();
105
- previousOwner = getBridgeOwnerFromBridge(bridge);
106
- owner = previousOwner;
141
+
142
+ /*
143
+ * A duplicate or rejected registration must never adopt or rebuild the
144
+ * process bridge. It still registers methods for its own registry.
145
+ */
107
146
  if (bridge && !gatewayRuntime.currentBridge) {
108
147
  gatewayRuntime.currentBridge = bridge;
109
148
  }
110
- }
111
149
 
112
- globalTrace.seenApiInstanceIds.add(apiInstanceId);
113
- globalTrace.seenRegistryFingerprints.add(registryFingerprint);
114
- globalTrace.lastApiInstanceId = apiInstanceId;
115
- globalTrace.lastRegistryFingerprint = registryFingerprint;
116
- bridge?.noteRegister?.({
117
- source: '@xmoxmo/bncr',
118
- pluginVersion,
119
- apiRebound: ownerDecision.adoptOwner ? !created && !rebuilt : false,
120
- apiInstanceId: meta.apiInstanceId,
121
- registryFingerprint: meta.registryFingerprint,
122
- });
123
- const debugLog = (...args: unknown[]) => {
124
- const rendered = args
125
- .map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
126
- .join(' ')
127
- .trim();
128
- if (!rendered) return;
129
- emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
130
- Boolean(bridge?.isDebugEnabled?.()),
131
- );
132
- };
150
+ noteRegisterTraceValue(globalTrace.seenApiInstanceIds, apiInstanceId);
151
+ noteRegisterTraceValue(globalTrace.seenRegistryFingerprints, registryFingerprint);
152
+ globalTrace.lastApiInstanceId = apiInstanceId;
153
+ globalTrace.lastRegistryFingerprint = registryFingerprint;
154
+ bridge?.noteRegister?.({
155
+ source: '@xmoxmo/bncr',
156
+ pluginVersion,
157
+ apiRebound: ownerDecision.kind === 'takeover' && !created && !rebuilt,
158
+ apiInstanceId: meta.apiInstanceId,
159
+ registryFingerprint: meta.registryFingerprint,
160
+ });
161
+ const debugLog = (...args: unknown[]) => {
162
+ const rendered = args
163
+ .map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg)))
164
+ .join(' ')
165
+ .trim();
166
+ if (!rendered) return;
167
+ emitBncrLogLine('info', `[bncr] debug ${rendered}`, { debugOnly: true }, () =>
168
+ Boolean(bridge?.isDebugEnabled?.()),
169
+ );
170
+ };
133
171
 
134
- debugLog(
135
- `register begin bridge=${bridge?.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
136
- `ownerApi=${owner?.apiInstanceId || 'none'} ownerRegistry=${owner?.registryFingerprint || 'none'} ` +
137
- `previousOwnerApi=${previousOwner?.apiInstanceId || 'none'} previousOwnerRegistry=${previousOwner?.registryFingerprint || 'none'}`,
138
- );
139
- debugLog(
140
- `register classify mode=${meta.registrationMode || 'unknown'} api=${apiInstanceId} registry=${registryFingerprint} ` +
141
- `sameApiAsPrevious=${sameApiAsPrevious} sameRegistryAsPrevious=${sameRegistryAsPrevious} ` +
142
- `firstSeenApi=${firstSeenApi} firstSeenRegistry=${firstSeenRegistry}`,
143
- );
144
- debugLog(
145
- `register owner adopt=${ownerDecision.adoptOwner} reason=${ownerDecision.reason} ` +
146
- `existingOwnerApi=${ownerDecision.existingOwnerApiInstanceId || 'none'}`,
147
- );
148
- if (!ownerDecision.adoptOwner) {
149
172
  debugLog(
150
- `bridge rebuild suppressed due to existing singleton owner api ${ownerDecision.existingOwnerApiInstanceId || 'unknown'}`,
173
+ `register begin bridge=${bridge?.getBridgeId?.() || 'unknown'} created=${created} rebuilt=${rebuilt} ` +
174
+ `ownerApi=${owner?.apiInstanceId || 'none'} ownerRegistry=${owner?.registryFingerprint || 'none'} ` +
175
+ `previousOwnerApi=${previousOwner?.apiInstanceId || 'none'} previousOwnerRegistry=${previousOwner?.registryFingerprint || 'none'}`,
151
176
  );
152
- } else {
153
- if (!created && !rebuilt) debugLog('bridge api rebound');
154
- if (rebuilt) debugLog('bridge rebuilt due to owner/runtime change');
155
- }
156
-
157
- const resolveDebug = async () => {
158
- try {
159
- const cfg = getOpenClawRuntimeConfig(api) as BncrDebugConfigRoot | null | undefined;
160
- return Boolean(cfg?.channels?.bncr?.debug?.verbose);
161
- } catch {
162
- return false;
177
+ debugLog(
178
+ `register classify mode=${meta.registrationMode || 'unknown'} api=${apiInstanceId} registry=${registryFingerprint} ` +
179
+ `sameApiAsPrevious=${sameApiAsPrevious} sameRegistryAsPrevious=${sameRegistryAsPrevious} ` +
180
+ `apiSeenRecently=${apiSeenRecently} registrySeenRecently=${registrySeenRecently}`,
181
+ );
182
+ debugLog(
183
+ `register lifecycle decision=${ownerDecision.kind} reason=${ownerDecision.reason} ` +
184
+ `generation=${ownerDecision.owner.key.generation} ` +
185
+ `existingOwnerApi=${previousOwner?.apiInstanceId || 'none'}`,
186
+ );
187
+ if (!shouldDeclareLifecycle) {
188
+ debugLog(
189
+ `service/channel declaration suppressed for ${ownerDecision.kind} registry=${registryFingerprint}`,
190
+ );
163
191
  }
164
- };
165
192
 
166
- if (!gatewayRuntime.serviceRegistered) {
167
- const serviceStopHandler = async () => {
168
- await getCurrentBridge().stopService?.();
193
+ const resolveDebug = async () => {
194
+ try {
195
+ const cfg = getOpenClawRuntimeConfig(api) as BncrDebugConfigRoot | null | undefined;
196
+ return Boolean(cfg?.channels?.bncr?.debug?.verbose);
197
+ } catch {
198
+ return false;
199
+ }
169
200
  };
170
- api.registerService({
171
- id: 'bncr-bridge-service',
172
- start: async (ctx) => {
173
- const debug = await resolveDebug();
174
- await getCurrentBridge().startService(ctx, debug);
175
- },
176
- stop: serviceStopHandler,
177
- });
178
- gatewayRuntime.serviceRegistered = true;
179
- gatewayRuntime.serviceOwnerApiInstanceId = apiInstanceId;
180
- meta.service = true;
181
- debugLog(`register service ok ownerApi=${apiInstanceId}`);
182
- } else {
183
- meta.service = true;
184
- debugLog(
185
- `register service skip (process singleton already registered by api ${gatewayRuntime.serviceOwnerApiInstanceId || 'unknown'})`,
201
+
202
+ const registryGeneration = ownerDecision.owner.key.generation;
203
+ const registryBridgeGenerationKey = getBridgeGenerationKey(
204
+ ownerDecision.owner.bridgeGeneration,
186
205
  );
187
- }
206
+ /*
207
+ * A rejected registration must remain fail-closed. Creating a shadow
208
+ * declaration here would make its gateway methods dispatch to the
209
+ * current bridge even though the registration never owned a lifecycle.
210
+ */
211
+ const registryDeclaration =
212
+ ownerDecision.kind === 'reject'
213
+ ? gatewayRuntime.registryDeclarations.get(registryFingerprint)
214
+ : getRegistryDeclaration(
215
+ registryFingerprint,
216
+ registryBridgeGenerationKey,
217
+ registryGeneration,
218
+ );
219
+ const serviceDeclaredForGeneration = registryDeclaration?.service === 'declared';
220
+ const channelDeclaredForGeneration = registryDeclaration?.channel === 'declared';
188
221
 
189
- if (!gatewayRuntime.channelRegistered) {
190
- api.registerChannel({
191
- plugin: createDynamicChannelPlugin({ loaded: runtime, getCurrentBridge }) as ChannelPlugin,
192
- });
193
- gatewayRuntime.channelRegistered = true;
194
- gatewayRuntime.channelOwnerApiInstanceId = apiInstanceId;
195
- meta.channel = true;
196
- debugLog(`register channel ok ownerApi=${apiInstanceId}`);
197
- } else {
198
- meta.channel = true;
199
- debugLog(
200
- `register channel skip (process singleton already registered by api ${gatewayRuntime.channelOwnerApiInstanceId || 'unknown'})`,
222
+ if (shouldDeclareLifecycle && !serviceDeclaredForGeneration) {
223
+ const serviceStopHandler = async () => {
224
+ const retirement = beginRetirement(registryFingerprint, registryGeneration);
225
+ if (!retirement) {
226
+ debugLog(`service stop suppressed for stale registry=${registryFingerprint}`);
227
+ return;
228
+ }
229
+
230
+ try {
231
+ await getCurrentBridge().stopService?.();
232
+ settleRetirement(registryFingerprint, { ok: true }, registryGeneration);
233
+ } catch (error) {
234
+ const detail = error instanceof Error ? error.message : String(error);
235
+ settleRetirement(registryFingerprint, { ok: false, error: detail }, registryGeneration);
236
+ throw error;
237
+ }
238
+ };
239
+ api.registerService({
240
+ id: 'bncr-bridge-service',
241
+ start: async (ctx) => {
242
+ const lifecycleBridge = await getLifecycleBridgeForStart(
243
+ api,
244
+ registryFingerprint,
245
+ registryGeneration,
246
+ );
247
+ if (!lifecycleBridge) {
248
+ debugLog(`service start suppressed for stale registry=${registryFingerprint}`);
249
+ return;
250
+ }
251
+ const debug = await resolveDebug();
252
+ if (!canStartLifecycleRegistry(registryFingerprint, registryGeneration)) {
253
+ debugLog(
254
+ `service start suppressed after await for stale registry=${registryFingerprint}`,
255
+ );
256
+ return;
257
+ }
258
+ try {
259
+ const started = await lifecycleBridge.startService(ctx, debug, () =>
260
+ isLifecycleRegistryCurrent(registryFingerprint, registryGeneration),
261
+ );
262
+ if (
263
+ started === false ||
264
+ !canStartLifecycleRegistry(registryFingerprint, registryGeneration)
265
+ ) {
266
+ debugLog(
267
+ `service start suppressed after await for stale registry=${registryFingerprint}`,
268
+ );
269
+ return;
270
+ }
271
+ probeRegistryRuntimeObservation(
272
+ registryFingerprint,
273
+ registryBridgeGenerationKey,
274
+ registryGeneration,
275
+ );
276
+ } catch (error) {
277
+ failLifecycleStart(registryFingerprint, error, registryGeneration);
278
+ throw error;
279
+ }
280
+ },
281
+ stop: serviceStopHandler,
282
+ });
283
+ markRegistryServiceDeclared(
284
+ registryFingerprint,
285
+ registryBridgeGenerationKey,
286
+ registryGeneration,
287
+ );
288
+ meta.service = true;
289
+ debugLog(`register service ok ownerApi=${apiInstanceId}`);
290
+ } else {
291
+ meta.service = registryDeclaration?.service === 'declared';
292
+ debugLog(
293
+ `register service skip registry=${registryFingerprint} declared=${registryDeclaration?.service || 'missing'}`,
294
+ );
295
+ }
296
+
297
+ if (shouldDeclareLifecycle && !channelDeclaredForGeneration) {
298
+ api.registerChannel({
299
+ plugin: createDynamicChannelPlugin({
300
+ loaded: runtime,
301
+ getCurrentBridge,
302
+ resolveBridgeForStart: async () => {
303
+ const lifecycleBridge = await getLifecycleBridgeForStart(
304
+ api,
305
+ registryFingerprint,
306
+ registryGeneration,
307
+ );
308
+ if (!lifecycleBridge) {
309
+ throw new Error(`bncr lifecycle registry is inactive: ${registryFingerprint}`);
310
+ }
311
+ return lifecycleBridge;
312
+ },
313
+ isBridgeForStartCurrent: () =>
314
+ canStartLifecycleRegistry(registryFingerprint, registryGeneration),
315
+ onBridgeStartObserved: () =>
316
+ probeRegistryRuntimeObservation(
317
+ registryFingerprint,
318
+ registryBridgeGenerationKey,
319
+ registryGeneration,
320
+ ),
321
+ resolveBridgeForStop: () => {
322
+ if (!canStopLifecycleRegistry(registryFingerprint, registryGeneration)) {
323
+ debugLog(`channel stop suppressed for stale registry=${registryFingerprint}`);
324
+ return null;
325
+ }
326
+ return getCurrentBridge();
327
+ },
328
+ isBridgeForStopCurrent: () =>
329
+ canStopLifecycleRegistry(registryFingerprint, registryGeneration),
330
+ }) as ChannelPlugin,
331
+ });
332
+ markRegistryChannelDeclared(
333
+ registryFingerprint,
334
+ registryBridgeGenerationKey,
335
+ registryGeneration,
336
+ );
337
+ meta.channel = true;
338
+ debugLog(`register channel ok ownerApi=${apiInstanceId}`);
339
+ } else {
340
+ meta.channel = registryDeclaration?.channel === 'declared';
341
+ debugLog(
342
+ `register channel skip registry=${registryFingerprint} declared=${registryDeclaration?.channel || 'missing'}`,
343
+ );
344
+ }
345
+
346
+ ensureGatewayMethodRegistered(api, 'bncr.connect', debugLog);
347
+ ensureGatewayMethodRegistered(api, 'bncr.inbound', debugLog);
348
+ ensureGatewayMethodRegistered(api, 'bncr.activity', debugLog);
349
+ ensureGatewayMethodRegistered(api, 'bncr.ack', debugLog);
350
+ ensureGatewayMethodRegistered(api, 'bncr.diagnostics', debugLog);
351
+ ensureGatewayMethodRegistered(api, 'bncr.deadLetter.inspect', debugLog);
352
+ ensureGatewayMethodRegistered(api, 'bncr.deadLetter.prune', debugLog);
353
+ ensureGatewayMethodRegistered(api, 'bncr.rpc.response', debugLog);
354
+ ensureGatewayMethodRegistered(api, 'bncr.file.init', debugLog);
355
+ ensureGatewayMethodRegistered(api, 'bncr.file.chunk', debugLog);
356
+ ensureGatewayMethodRegistered(api, 'bncr.file.complete', debugLog);
357
+ ensureGatewayMethodRegistered(api, 'bncr.file.abort', debugLog);
358
+ ensureGatewayMethodRegistered(api, 'bncr.file.ack', debugLog);
359
+ if (ownerDecision.kind !== 'reject') {
360
+ markRegistryRegistrationComplete(
361
+ registryFingerprint,
362
+ registryBridgeGenerationKey,
363
+ registryGeneration,
364
+ );
365
+ }
366
+ pruneRegistryLedgers();
367
+ debugLog('register done');
368
+ } catch (error) {
369
+ const recovered = ownerDecision
370
+ ? recoverLifecycleRegistration(ownerDecision, error)
371
+ : 'plan-not-committed';
372
+ emitBncrLogLine(
373
+ 'error',
374
+ `[bncr] register failed recovered=${recovered} error=${error instanceof Error ? error.message : String(error)}`,
201
375
  );
376
+ throw error;
202
377
  }
203
-
204
- ensureGatewayMethodRegistered(api, 'bncr.connect', debugLog);
205
- ensureGatewayMethodRegistered(api, 'bncr.inbound', debugLog);
206
- ensureGatewayMethodRegistered(api, 'bncr.activity', debugLog);
207
- ensureGatewayMethodRegistered(api, 'bncr.ack', debugLog);
208
- ensureGatewayMethodRegistered(api, 'bncr.diagnostics', debugLog);
209
- ensureGatewayMethodRegistered(api, 'bncr.deadLetter.inspect', debugLog);
210
- ensureGatewayMethodRegistered(api, 'bncr.deadLetter.prune', debugLog);
211
- ensureGatewayMethodRegistered(api, 'bncr.rpc.response', debugLog);
212
- ensureGatewayMethodRegistered(api, 'bncr.file.init', debugLog);
213
- ensureGatewayMethodRegistered(api, 'bncr.file.chunk', debugLog);
214
- ensureGatewayMethodRegistered(api, 'bncr.file.complete', debugLog);
215
- ensureGatewayMethodRegistered(api, 'bncr.file.abort', debugLog);
216
- ensureGatewayMethodRegistered(api, 'bncr.file.ack', debugLog);
217
- debugLog('register done');
218
378
  },
219
379
  };
220
380
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,9 @@ const requiredScriptFiles = [
18
18
  const requiredSourceFiles = [
19
19
  'src/bootstrap/channel-plugin-runtime.ts',
20
20
  'src/bootstrap/cli.ts',
21
+ 'src/bootstrap/register-lifecycle.ts',
21
22
  'src/bootstrap/register-runtime-helpers.ts',
23
+ 'src/bootstrap/register-runtime-observation.ts',
22
24
  'src/bootstrap/register-runtime.ts',
23
25
  'src/bootstrap/runtime-discovery.ts',
24
26
  'src/bootstrap/runtime-loader.ts',
@@ -14,8 +14,10 @@ const requiredRootFiles = ['index.ts', 'openclaw.plugin.json'];
14
14
  const requiredSourceFiles = [
15
15
  'src/bootstrap/channel-plugin-runtime.ts',
16
16
  'src/bootstrap/cli.ts',
17
+ 'src/bootstrap/register-lifecycle.ts',
17
18
  'src/bootstrap/register-runtime-gateway.ts',
18
19
  'src/bootstrap/register-runtime-helpers.ts',
20
+ 'src/bootstrap/register-runtime-observation.ts',
19
21
  'src/bootstrap/register-runtime-singleton.ts',
20
22
  'src/bootstrap/register-runtime.ts',
21
23
  'src/bootstrap/runtime-discovery.ts',
@@ -10,8 +10,17 @@ type LoadedRuntime = {
10
10
  export function createDynamicChannelPlugin(args: {
11
11
  loaded: LoadedRuntime;
12
12
  getCurrentBridge: () => BridgeSingleton;
13
+ resolveBridgeForStart?: () => BridgeSingleton | Promise<BridgeSingleton>;
14
+ isBridgeForStartCurrent?: () => boolean;
15
+ onBridgeStartObserved?: () => void;
16
+ resolveBridgeForStop?: () => BridgeSingleton | null | Promise<BridgeSingleton | null>;
17
+ isBridgeForStopCurrent?: () => boolean;
13
18
  }): ChannelPlugin {
14
19
  const { loaded, getCurrentBridge } = args;
20
+ const resolveBridgeForStart = args.resolveBridgeForStart || getCurrentBridge;
21
+ const isBridgeForStartCurrent = args.isBridgeForStartCurrent || (() => true);
22
+ const resolveBridgeForStop = args.resolveBridgeForStop || getCurrentBridge;
23
+ const isBridgeForStopCurrent = args.isBridgeForStopCurrent || (() => true);
15
24
  const base = loaded.createBncrChannelPlugin(() => getCurrentBridge());
16
25
  const plugin = { ...base } as ChannelPlugin;
17
26
  const outbound = base.outbound as ChannelPlugin['outbound'];
@@ -67,14 +76,26 @@ export function createDynamicChannelPlugin(args: {
67
76
 
68
77
  plugin.gateway = {
69
78
  ...baseGateway,
70
- startAccount: (ctx: GatewayStartAccountArgs) =>
71
- getCurrentBridge().channelStartAccount(
79
+ startAccount: async (ctx: GatewayStartAccountArgs) => {
80
+ const bridge = await resolveBridgeForStart();
81
+ if (!isBridgeForStartCurrent()) return;
82
+ const task = bridge.channelStartAccount(
72
83
  ctx as Parameters<BridgeSingleton['channelStartAccount']>[0],
73
- ),
74
- stopAccount: (ctx: GatewayStopAccountArgs) =>
75
- getCurrentBridge().channelStopAccount(
76
- ctx as Parameters<BridgeSingleton['channelStopAccount']>[0],
77
- ),
84
+ );
85
+ if (isBridgeForStartCurrent()) {
86
+ try {
87
+ args.onBridgeStartObserved?.();
88
+ } catch {
89
+ // Observation is diagnostic-only and must not fail channel startup.
90
+ }
91
+ }
92
+ return task;
93
+ },
94
+ stopAccount: async (ctx: GatewayStopAccountArgs) => {
95
+ const bridge = await resolveBridgeForStop();
96
+ if (!bridge || !isBridgeForStopCurrent()) return;
97
+ return bridge.channelStopAccount(ctx as Parameters<BridgeSingleton['channelStopAccount']>[0]);
98
+ },
78
99
  };
79
100
 
80
101
  return plugin;