@xmoxmo/bncr 0.3.6 → 0.3.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.
Files changed (165) hide show
  1. package/README.md +5 -0
  2. package/dist/index.js +28 -5
  3. package/index.ts +55 -721
  4. package/openclaw.plugin.json +1 -0
  5. package/package.json +8 -4
  6. package/scripts/check-pack.mjs +93 -18
  7. package/scripts/check-register-drift.mjs +35 -13
  8. package/scripts/selfcheck.mjs +80 -11
  9. package/src/bootstrap/channel-plugin-runtime.ts +81 -0
  10. package/src/bootstrap/cli.ts +97 -0
  11. package/src/bootstrap/register-runtime-gateway.ts +129 -0
  12. package/src/bootstrap/register-runtime-helpers.ts +140 -0
  13. package/src/bootstrap/register-runtime-singleton.ts +137 -0
  14. package/src/bootstrap/register-runtime.ts +201 -0
  15. package/src/bootstrap/runtime-discovery.ts +187 -0
  16. package/src/bootstrap/runtime-loader.ts +54 -0
  17. package/src/channel.ts +1590 -4967
  18. package/src/core/accounts.ts +23 -4
  19. package/src/core/dead-letter-diagnostics.ts +37 -5
  20. package/src/core/diagnostics.ts +31 -15
  21. package/src/core/downlink-health.ts +3 -11
  22. package/src/core/extended-diagnostics.ts +78 -36
  23. package/src/core/file-transfer-payloads.ts +1 -1
  24. package/src/core/logging.ts +1 -0
  25. package/src/core/outbox-enqueue.ts +13 -2
  26. package/src/core/outbox-entry-builders.ts +2 -0
  27. package/src/core/outbox-summary.ts +75 -3
  28. package/src/core/permissions.ts +15 -2
  29. package/src/core/persisted-outbox-entry.ts +21 -6
  30. package/src/core/policy.ts +45 -4
  31. package/src/core/probe.ts +3 -15
  32. package/src/core/register-trace.ts +3 -3
  33. package/src/core/status.ts +43 -4
  34. package/src/core/targets.ts +216 -205
  35. package/src/core/types.ts +221 -0
  36. package/src/core/value-sanitize.ts +29 -0
  37. package/src/messaging/inbound/commands.ts +147 -172
  38. package/src/messaging/inbound/context-facts.ts +4 -2
  39. package/src/messaging/inbound/contracts.ts +70 -0
  40. package/src/messaging/inbound/dispatch-prep.ts +303 -0
  41. package/src/messaging/inbound/dispatch.ts +49 -462
  42. package/src/messaging/inbound/gate.ts +18 -5
  43. package/src/messaging/inbound/last-route.ts +10 -4
  44. package/src/messaging/inbound/media-url-download.ts +109 -0
  45. package/src/messaging/inbound/native-command-runtime.ts +225 -0
  46. package/src/messaging/inbound/parse.ts +2 -1
  47. package/src/messaging/inbound/remote-media.ts +49 -0
  48. package/src/messaging/inbound/reply-config.ts +16 -4
  49. package/src/messaging/inbound/reply-dispatch.ts +162 -0
  50. package/src/messaging/inbound/runtime-compat.ts +31 -10
  51. package/src/messaging/inbound/session-label.ts +15 -7
  52. package/src/messaging/inbound/turn-context.ts +131 -0
  53. package/src/messaging/outbound/actions.ts +24 -10
  54. package/src/messaging/outbound/diagnostics-debug-builders.ts +365 -0
  55. package/src/messaging/outbound/diagnostics.ts +31 -355
  56. package/src/messaging/outbound/durable-message-adapter.ts +20 -16
  57. package/src/messaging/outbound/durable-queue-adapter.ts +20 -7
  58. package/src/messaging/outbound/media.ts +24 -13
  59. package/src/messaging/outbound/reply-enqueue-media.ts +181 -0
  60. package/src/messaging/outbound/reply-enqueue.ts +46 -155
  61. package/src/messaging/outbound/send-params.ts +3 -0
  62. package/src/messaging/outbound/send.ts +19 -10
  63. package/src/messaging/outbound/session-route.ts +18 -3
  64. package/src/openclaw/channel-runtime-contracts.ts +76 -0
  65. package/src/openclaw/config-runtime.ts +13 -7
  66. package/src/openclaw/inbound-session-runtime.ts +7 -3
  67. package/src/openclaw/ingress-runtime.ts +17 -27
  68. package/src/openclaw/reply-runtime.ts +54 -59
  69. package/src/openclaw/routing-runtime.ts +35 -18
  70. package/src/openclaw/runtime-surface.ts +156 -12
  71. package/src/openclaw/sdk-helpers.ts +8 -1
  72. package/src/openclaw/session-route-runtime.ts +12 -12
  73. package/src/plugin/ack-outbox-runtime-group.ts +264 -0
  74. package/src/plugin/bridge-ack-facade.ts +137 -0
  75. package/src/plugin/bridge-connection-facade.ts +111 -0
  76. package/src/plugin/bridge-diagnostics-facade.ts +23 -0
  77. package/src/plugin/bridge-drain-facade.ts +98 -0
  78. package/src/plugin/bridge-extended-diagnostics-facade.ts +149 -0
  79. package/src/plugin/bridge-file-transfer-push-facade.ts +140 -0
  80. package/src/plugin/bridge-lifecycle.ts +156 -0
  81. package/src/plugin/bridge-media-facade.ts +241 -0
  82. package/src/plugin/bridge-outbox-facade.ts +182 -0
  83. package/src/plugin/bridge-runtime-helpers.ts +266 -0
  84. package/src/plugin/bridge-runtime-snapshots.ts +104 -0
  85. package/src/plugin/bridge-runtime-surface-facade.ts +8 -0
  86. package/src/plugin/bridge-status-facade.ts +76 -0
  87. package/src/plugin/bridge-status-worker-facade.ts +72 -0
  88. package/src/plugin/bridge-support-runtime.ts +137 -0
  89. package/src/plugin/bridge-surface-handlers-group.ts +242 -0
  90. package/src/plugin/bridge-surface-helpers.ts +28 -0
  91. package/src/plugin/capabilities.ts +1 -3
  92. package/src/plugin/channel-components.ts +289 -0
  93. package/src/plugin/channel-inbound-helpers.ts +149 -0
  94. package/src/plugin/channel-plugin-bridge-group.ts +129 -0
  95. package/src/plugin/channel-plugin-surface-group.ts +202 -0
  96. package/src/plugin/channel-runtime-builders-delivery.ts +513 -0
  97. package/src/plugin/channel-runtime-builders-status.ts +331 -0
  98. package/src/plugin/channel-runtime-builders.ts +25 -0
  99. package/src/plugin/channel-runtime-constants.ts +40 -0
  100. package/src/plugin/channel-runtime-types.ts +146 -0
  101. package/src/plugin/channel-send-runtime-group.ts +37 -0
  102. package/src/plugin/channel-send.ts +226 -0
  103. package/src/plugin/channel-utils.ts +102 -0
  104. package/src/plugin/config.ts +24 -3
  105. package/src/plugin/connection-handlers-helpers.ts +254 -0
  106. package/src/plugin/connection-handlers.ts +440 -0
  107. package/src/plugin/connection-state-helpers.ts +159 -0
  108. package/src/plugin/connection-state-runtime-group.ts +51 -0
  109. package/src/plugin/connection-state.ts +527 -0
  110. package/src/plugin/diagnostics-handlers.ts +211 -0
  111. package/src/plugin/error-message.ts +15 -0
  112. package/src/plugin/file-ack-runtime.ts +284 -0
  113. package/src/plugin/file-inbound-abort.ts +112 -0
  114. package/src/plugin/file-inbound-chunk.ts +146 -0
  115. package/src/plugin/file-inbound-complete.ts +153 -0
  116. package/src/plugin/file-inbound-handlers.ts +19 -0
  117. package/src/plugin/file-inbound-init.ts +122 -0
  118. package/src/plugin/file-inbound-runtime.ts +51 -0
  119. package/src/plugin/file-inbound-state.ts +62 -0
  120. package/src/plugin/file-transfer-logs.ts +227 -0
  121. package/src/plugin/file-transfer-orchestrator-chunk.ts +135 -0
  122. package/src/plugin/file-transfer-orchestrator.ts +304 -0
  123. package/src/plugin/file-transfer-runtime-group.ts +102 -0
  124. package/src/plugin/file-transfer-send.ts +89 -0
  125. package/src/plugin/file-transfer-setup.ts +206 -0
  126. package/src/plugin/gateway-event-context.ts +41 -0
  127. package/src/plugin/gateway-runtime.ts +17 -4
  128. package/src/plugin/inbound-acceptance.ts +107 -0
  129. package/src/plugin/inbound-handlers.ts +248 -0
  130. package/src/plugin/inbound-surface-handlers-group.ts +152 -0
  131. package/src/plugin/media-dedupe-runtime.ts +90 -0
  132. package/src/plugin/media-orchestrators-runtime-group.ts +316 -0
  133. package/src/plugin/message-ack-runtime.ts +284 -0
  134. package/src/plugin/message-send.ts +16 -6
  135. package/src/plugin/messaging.ts +98 -36
  136. package/src/plugin/outbound.ts +50 -8
  137. package/src/plugin/outbox-ack-logs.ts +136 -0
  138. package/src/plugin/outbox-ack-outcome.ts +128 -0
  139. package/src/plugin/outbox-drain-ack.ts +145 -0
  140. package/src/plugin/outbox-drain-failure.ts +84 -0
  141. package/src/plugin/outbox-drain-loop.ts +554 -0
  142. package/src/plugin/outbox-drain-post-push.ts +159 -0
  143. package/src/plugin/outbox-drain-runtime.ts +141 -0
  144. package/src/plugin/outbox-drain-schedule.ts +116 -0
  145. package/src/plugin/outbox-file-push-flow.ts +69 -0
  146. package/src/plugin/outbox-push-route-runtime-group.ts +81 -0
  147. package/src/plugin/outbox-push.ts +267 -0
  148. package/src/plugin/outbox-route.ts +181 -0
  149. package/src/plugin/outbox-text-push-flow.ts +90 -0
  150. package/src/plugin/runtime-diagnostics-assembler.ts +183 -0
  151. package/src/plugin/runtime-diagnostics-helpers.ts +302 -0
  152. package/src/plugin/runtime-diagnostics-payload-builders.ts +171 -0
  153. package/src/plugin/runtime-diagnostics-snapshot.ts +31 -0
  154. package/src/plugin/setup.ts +33 -6
  155. package/src/plugin/state-store.ts +249 -0
  156. package/src/plugin/state-transient-runtime-group.ts +105 -0
  157. package/src/plugin/status-runtime.ts +251 -0
  158. package/src/plugin/status.ts +33 -7
  159. package/src/plugin/target-runtime.ts +141 -0
  160. package/src/plugin/target-status-runtime-group.ts +130 -0
  161. package/src/plugin/transient-state-runtime.ts +82 -0
  162. package/src/runtime/outbound-ack-timeout.ts +5 -3
  163. package/src/runtime/outbound-flags.ts +24 -8
  164. package/src/runtime/status-snapshots.ts +36 -7
  165. package/src/runtime/status-worker.ts +34 -4
@@ -0,0 +1,129 @@
1
+ import { emitBncrLogLine } from '../core/logging.ts';
2
+ import type { BridgeRegisterStateCarrier } from './register-runtime-helpers.ts';
3
+ import type { ChannelModule } from './runtime-loader.ts';
4
+
5
+ type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
6
+ type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
7
+ type BridgeGatewayHandlerContext = Parameters<BridgeSingleton['handleConnect']>[0];
8
+ type BridgeGatewayHandlerResult = Awaited<ReturnType<BridgeSingleton['handleConnect']>>;
9
+ type BridgeStateReader = { gatewayPid?: number; getBridgeId?: () => string };
10
+ type OpenClawGatewayMethodMirror = {
11
+ name?: string;
12
+ handler?: (opts: BridgeGatewayHandlerContext) => unknown;
13
+ };
14
+ type OpenClawPluginApiWithGatewayMirror = OpenClawPluginApi & {
15
+ methods?: OpenClawGatewayMethodMirror[];
16
+ };
17
+
18
+ export type BncrGatewayMethodName =
19
+ | 'bncr.connect'
20
+ | 'bncr.inbound'
21
+ | 'bncr.activity'
22
+ | 'bncr.ack'
23
+ | 'bncr.diagnostics'
24
+ | 'bncr.deadLetter.inspect'
25
+ | 'bncr.deadLetter.prune'
26
+ | 'bncr.file.init'
27
+ | 'bncr.file.chunk'
28
+ | 'bncr.file.complete'
29
+ | 'bncr.file.abort'
30
+ | 'bncr.file.ack';
31
+
32
+ export function createBncrGatewayMethodRegistry(runtime: {
33
+ getRegisterMeta: (api: OpenClawPluginApi) => {
34
+ methods?: Set<string>;
35
+ registryFingerprint?: string;
36
+ };
37
+ getRegistryFingerprint: (api: OpenClawPluginApi) => string;
38
+ getGatewayRuntime: () => {
39
+ currentBridge?: BridgeSingleton;
40
+ registeredMethodsByRegistry: Map<string, Set<BncrGatewayMethodName>>;
41
+ };
42
+ gatewayMethodDispatchers: Record<
43
+ BncrGatewayMethodName,
44
+ (bridge: BridgeSingleton, opts: BridgeGatewayHandlerContext) => BridgeGatewayHandlerResult
45
+ >;
46
+ getBridgeRegisterStateCarrier: (bridge: BridgeSingleton) => BridgeRegisterStateCarrier;
47
+ }) {
48
+ const dispatchGatewayMethod = (
49
+ name: BncrGatewayMethodName,
50
+ opts: BridgeGatewayHandlerContext,
51
+ ) => {
52
+ const gatewayRuntime = runtime.getGatewayRuntime();
53
+ const bridge = gatewayRuntime.currentBridge;
54
+ if (!bridge) {
55
+ throw new Error(`bncr gateway runtime unavailable for ${name}`);
56
+ }
57
+ try {
58
+ return runtime.gatewayMethodDispatchers[name](bridge, opts);
59
+ } catch (error) {
60
+ const state = runtime.getBridgeRegisterStateCarrier(bridge) as BridgeStateReader;
61
+ const detail =
62
+ error instanceof Error
63
+ ? { name: error.name, message: error.message, stack: error.stack || null }
64
+ : { name: 'NonError', message: String(error), stack: null };
65
+ emitBncrLogLine(
66
+ 'error',
67
+ `[bncr] gateway method error method=${name}|bridgeId=${state.getBridgeId?.() || '-'}|gatewayPid=${state.gatewayPid ?? '-'}|err=${detail.message}`,
68
+ );
69
+ emitBncrLogLine(
70
+ 'error',
71
+ `[bncr] gateway method error ${JSON.stringify({
72
+ method: name,
73
+ bridgeId: state.getBridgeId?.() || null,
74
+ gatewayPid: state.gatewayPid ?? null,
75
+ detail,
76
+ })}`,
77
+ { debugOnly: true },
78
+ () => false,
79
+ );
80
+ throw error;
81
+ }
82
+ };
83
+
84
+ const mirrorGatewayMethodForMockApi = (
85
+ api: OpenClawPluginApiWithGatewayMirror,
86
+ name: BncrGatewayMethodName,
87
+ ) => {
88
+ if (!Array.isArray(api?.methods)) return;
89
+ if (api.methods.some((item) => item?.name === name)) return;
90
+ api.methods.push({ name, handler: (opts) => dispatchGatewayMethod(name, opts) });
91
+ };
92
+
93
+ const ensureGatewayMethodRegistered = (
94
+ api: OpenClawPluginApiWithGatewayMirror,
95
+ name: BncrGatewayMethodName,
96
+ debugLog: (...args: unknown[]) => void,
97
+ ) => {
98
+ const meta = runtime.getRegisterMeta(api);
99
+ const gatewayRuntime = runtime.getGatewayRuntime();
100
+ const registryFingerprint = meta.registryFingerprint || runtime.getRegistryFingerprint(api);
101
+ let registryMethods = gatewayRuntime.registeredMethodsByRegistry.get(registryFingerprint);
102
+ if (!registryMethods) {
103
+ registryMethods = new Set<BncrGatewayMethodName>();
104
+ gatewayRuntime.registeredMethodsByRegistry.set(registryFingerprint, registryMethods);
105
+ }
106
+ if (meta.methods?.has(name)) {
107
+ debugLog(`register method skip ${name} (already registered on this api)`);
108
+ return;
109
+ }
110
+ if (registryMethods.has(name)) {
111
+ mirrorGatewayMethodForMockApi(api, name);
112
+ meta.methods?.add(name);
113
+ debugLog(`register method reuse ${name} (already registered in registry)`);
114
+ return;
115
+ }
116
+ api.registerGatewayMethod(name, (opts: BridgeGatewayHandlerContext) =>
117
+ dispatchGatewayMethod(name, opts),
118
+ );
119
+ mirrorGatewayMethodForMockApi(api, name);
120
+ registryMethods.add(name);
121
+ meta.methods?.add(name);
122
+ debugLog(`register method ok ${name}`);
123
+ };
124
+
125
+ return {
126
+ dispatchGatewayMethod,
127
+ ensureGatewayMethodRegistered,
128
+ };
129
+ }
@@ -0,0 +1,140 @@
1
+ export type BridgeOwner = {
2
+ moduleEpoch: string;
3
+ bridgeFactoryId: string;
4
+ apiInstanceId: string;
5
+ registryFingerprint: string;
6
+ registrationMode?: string;
7
+ };
8
+
9
+ export type BridgeRegisterStateSnapshot = {
10
+ registerCount: number;
11
+ apiGeneration: number;
12
+ firstRegisterAt: number | null;
13
+ lastRegisterAt: number | null;
14
+ lastApiRebindAt: number | null;
15
+ pluginSource: string | null;
16
+ pluginVersion: string | null;
17
+ lastApiInstanceId: string | null;
18
+ lastRegistryFingerprint: string | null;
19
+ lastDriftSnapshot: unknown;
20
+ registerTraceRecent: Array<Record<string, unknown>>;
21
+ };
22
+
23
+ export type BridgeRegisterStateCarrier = {
24
+ registerCount?: number;
25
+ apiGeneration?: number;
26
+ firstRegisterAt?: number | null;
27
+ lastRegisterAt?: number | null;
28
+ lastApiRebindAt?: number | null;
29
+ pluginSource?: string | null;
30
+ pluginVersion?: string | null;
31
+ lastApiInstanceId?: string | null;
32
+ lastRegistryFingerprint?: string | null;
33
+ lastDriftSnapshot?: unknown;
34
+ registerTraceRecent?: Array<Record<string, unknown>>;
35
+ gatewayPid?: number;
36
+ };
37
+
38
+ export function getProcessOwnerApiInstanceId(args: {
39
+ serviceOwnerApiInstanceId?: string;
40
+ channelOwnerApiInstanceId?: string;
41
+ }) {
42
+ return args.serviceOwnerApiInstanceId || args.channelOwnerApiInstanceId || undefined;
43
+ }
44
+
45
+ export function sameBridgeOwner(left?: BridgeOwner, right?: BridgeOwner) {
46
+ if (!left || !right) return false;
47
+ return (
48
+ left.moduleEpoch === right.moduleEpoch &&
49
+ left.bridgeFactoryId === right.bridgeFactoryId &&
50
+ left.apiInstanceId === right.apiInstanceId &&
51
+ left.registryFingerprint === right.registryFingerprint
52
+ );
53
+ }
54
+
55
+ export function snapshotBridgeRegisterState(
56
+ bridge?: BridgeRegisterStateCarrier,
57
+ ): BridgeRegisterStateSnapshot | null {
58
+ if (!bridge) return null;
59
+ return {
60
+ registerCount: Number(bridge.registerCount || 0),
61
+ apiGeneration: Number(bridge.apiGeneration || 0),
62
+ firstRegisterAt:
63
+ typeof bridge.firstRegisterAt === 'number'
64
+ ? bridge.firstRegisterAt
65
+ : (bridge.firstRegisterAt ?? null),
66
+ lastRegisterAt:
67
+ typeof bridge.lastRegisterAt === 'number'
68
+ ? bridge.lastRegisterAt
69
+ : (bridge.lastRegisterAt ?? null),
70
+ lastApiRebindAt:
71
+ typeof bridge.lastApiRebindAt === 'number'
72
+ ? bridge.lastApiRebindAt
73
+ : (bridge.lastApiRebindAt ?? null),
74
+ pluginSource: typeof bridge.pluginSource === 'string' ? bridge.pluginSource : null,
75
+ pluginVersion: typeof bridge.pluginVersion === 'string' ? bridge.pluginVersion : null,
76
+ lastApiInstanceId:
77
+ typeof bridge.lastApiInstanceId === 'string' ? bridge.lastApiInstanceId : null,
78
+ lastRegistryFingerprint:
79
+ typeof bridge.lastRegistryFingerprint === 'string' ? bridge.lastRegistryFingerprint : null,
80
+ lastDriftSnapshot: bridge.lastDriftSnapshot ?? null,
81
+ registerTraceRecent: Array.isArray(bridge.registerTraceRecent)
82
+ ? bridge.registerTraceRecent.map((trace) => ({ ...trace }))
83
+ : [],
84
+ };
85
+ }
86
+
87
+ export function hydrateBridgeRegisterState<T extends BridgeRegisterStateCarrier>(
88
+ bridge: T,
89
+ snapshot: BridgeRegisterStateSnapshot | null,
90
+ ) {
91
+ if (!snapshot) return bridge;
92
+ bridge.registerCount = snapshot.registerCount;
93
+ bridge.apiGeneration = snapshot.apiGeneration;
94
+ bridge.firstRegisterAt = snapshot.firstRegisterAt;
95
+ bridge.lastRegisterAt = snapshot.lastRegisterAt;
96
+ bridge.lastApiRebindAt = snapshot.lastApiRebindAt;
97
+ bridge.pluginSource = snapshot.pluginSource;
98
+ bridge.pluginVersion = snapshot.pluginVersion;
99
+ bridge.lastApiInstanceId = snapshot.lastApiInstanceId;
100
+ bridge.lastRegistryFingerprint = snapshot.lastRegistryFingerprint;
101
+ bridge.lastDriftSnapshot = snapshot.lastDriftSnapshot;
102
+ bridge.registerTraceRecent = snapshot.registerTraceRecent.map((trace) => ({ ...trace }));
103
+ return bridge;
104
+ }
105
+
106
+ export function shouldAdoptProcessOwner(args: {
107
+ apiInstanceId: string;
108
+ serviceRegistered?: boolean;
109
+ channelRegistered?: boolean;
110
+ serviceOwnerApiInstanceId?: string;
111
+ channelOwnerApiInstanceId?: string;
112
+ }) {
113
+ const existingOwnerApiInstanceId = getProcessOwnerApiInstanceId({
114
+ serviceOwnerApiInstanceId: args.serviceOwnerApiInstanceId,
115
+ channelOwnerApiInstanceId: args.channelOwnerApiInstanceId,
116
+ });
117
+ const hasSingletonOwner = Boolean(args.serviceRegistered) || Boolean(args.channelRegistered);
118
+
119
+ if (!hasSingletonOwner) {
120
+ return {
121
+ adoptOwner: true,
122
+ existingOwnerApiInstanceId,
123
+ reason: 'no-singleton-owner',
124
+ };
125
+ }
126
+
127
+ if (existingOwnerApiInstanceId && existingOwnerApiInstanceId === args.apiInstanceId) {
128
+ return {
129
+ adoptOwner: true,
130
+ existingOwnerApiInstanceId,
131
+ reason: 'same-owner-api',
132
+ };
133
+ }
134
+
135
+ return {
136
+ adoptOwner: false,
137
+ existingOwnerApiInstanceId,
138
+ reason: 'singleton-owned-by-other-api',
139
+ };
140
+ }
@@ -0,0 +1,137 @@
1
+ import {
2
+ type BridgeOwner,
3
+ type BridgeRegisterStateCarrier,
4
+ hydrateBridgeRegisterState,
5
+ sameBridgeOwner,
6
+ snapshotBridgeRegisterState,
7
+ } from './register-runtime-helpers.ts';
8
+ import type { ChannelModule, LoadedRuntime } from './runtime-loader.ts';
9
+
10
+ type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
11
+ type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
12
+ type BridgeOwnedCarrier = BridgeRegisterStateCarrier & {
13
+ [key: symbol]: unknown;
14
+ stopService?: () => Promise<unknown> | unknown;
15
+ bindApi?: (api: OpenClawPluginApi) => void;
16
+ bindRuntimePaths?: (paths: { pluginRoot: string; pluginFile: string }) => void;
17
+ };
18
+ type GlobalBridgeStore = typeof globalThis & { __bncrBridge?: BridgeSingleton };
19
+
20
+ function isBridgeOwner(value: unknown): value is BridgeOwner {
21
+ return Boolean(
22
+ value &&
23
+ typeof value === 'object' &&
24
+ 'moduleEpoch' in value &&
25
+ 'bridgeFactoryId' in value &&
26
+ 'apiInstanceId' in value &&
27
+ 'registryFingerprint' in value,
28
+ );
29
+ }
30
+
31
+ function getBridgeOwnedCarrier(bridge: BridgeSingleton): BridgeOwnedCarrier {
32
+ return bridge as unknown as BridgeOwnedCarrier;
33
+ }
34
+
35
+ function getBridgeRegisterStateCarrier(bridge: BridgeSingleton): BridgeRegisterStateCarrier {
36
+ return bridge as unknown as BridgeRegisterStateCarrier;
37
+ }
38
+
39
+ export function createBncrBridgeSingletonManager(runtime: {
40
+ bridgeOwnerSymbol: symbol;
41
+ pluginRoot: string;
42
+ pluginFile: string;
43
+ loadBncrRuntimeSync: () => LoadedRuntime;
44
+ getBridgeOwner: (api: OpenClawPluginApi, loaded: LoadedRuntime) => BridgeOwner;
45
+ }) {
46
+ const assignBridgeOwner = <T extends BridgeSingleton>(bridge: T, owner: BridgeOwner) => {
47
+ getBridgeOwnedCarrier(bridge)[runtime.bridgeOwnerSymbol] = owner;
48
+ return bridge;
49
+ };
50
+
51
+ const getBridgeSingleton = (api: OpenClawPluginApi) => {
52
+ const loaded = runtime.loadBncrRuntimeSync();
53
+ const g = globalThis as GlobalBridgeStore;
54
+ const owner = runtime.getBridgeOwner(api, loaded);
55
+ const previousOwnerRaw = g.__bncrBridge
56
+ ? getBridgeOwnedCarrier(g.__bncrBridge)[runtime.bridgeOwnerSymbol]
57
+ : undefined;
58
+ const previousOwner = isBridgeOwner(previousOwnerRaw) ? previousOwnerRaw : undefined;
59
+
60
+ let created = false;
61
+ let rebuilt = false;
62
+
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(
98
+ loaded.createBncrBridge(api, {
99
+ pluginRoot: runtime.pluginRoot,
100
+ pluginFile: runtime.pluginFile,
101
+ }),
102
+ owner,
103
+ );
104
+ created = true;
105
+ }
106
+
107
+ g.__bncrBridge?.bindRuntimePaths?.({
108
+ pluginRoot: runtime.pluginRoot,
109
+ pluginFile: runtime.pluginFile,
110
+ });
111
+
112
+ return { bridge: g.__bncrBridge, runtime: loaded, created, rebuilt, owner, previousOwner };
113
+ };
114
+
115
+ const getExistingBridgeSingleton = () => {
116
+ const g = globalThis as GlobalBridgeStore;
117
+ return g.__bncrBridge;
118
+ };
119
+
120
+ const getBridgeOwnerFromBridge = (bridge?: BridgeSingleton): BridgeOwner | undefined => {
121
+ if (!bridge) return undefined;
122
+ const bridgeCarrier = getBridgeOwnedCarrier(bridge);
123
+ for (const symbol of Object.getOwnPropertySymbols(bridge)) {
124
+ const owner = bridgeCarrier[symbol];
125
+ if (isBridgeOwner(owner)) return owner;
126
+ }
127
+ return undefined;
128
+ };
129
+
130
+ return {
131
+ assignBridgeOwner,
132
+ getBridgeRegisterStateCarrier,
133
+ getBridgeSingleton,
134
+ getExistingBridgeSingleton,
135
+ getBridgeOwnerFromBridge,
136
+ };
137
+ }
@@ -0,0 +1,201 @@
1
+ import {
2
+ type BncrGatewayMethodName,
3
+ createBncrGatewayMethodRegistry,
4
+ } from './register-runtime-gateway.ts';
5
+ import {
6
+ type BridgeOwner,
7
+ type BridgeRegisterStateCarrier,
8
+ shouldAdoptProcessOwner,
9
+ } from './register-runtime-helpers.ts';
10
+ import { createBncrBridgeSingletonManager } from './register-runtime-singleton.ts';
11
+ import {
12
+ type ChannelModule,
13
+ type LoadedRuntime,
14
+ loadBncrRuntimeSync,
15
+ pluginFile,
16
+ pluginRoot,
17
+ } from './runtime-loader.ts';
18
+
19
+ type OpenClawPluginApi = Parameters<ChannelModule['createBncrBridge']>[0];
20
+ type BridgeSingleton = ReturnType<ChannelModule['createBncrBridge']>;
21
+ type BridgeGatewayHandlerContext = Parameters<BridgeSingleton['handleConnect']>[0];
22
+ type BridgeGatewayHandlerResult = Awaited<ReturnType<BridgeSingleton['handleConnect']>>;
23
+ type BridgeSingletonWithState = BridgeSingleton;
24
+
25
+ type RegisterMeta = {
26
+ service?: boolean;
27
+ channel?: boolean;
28
+ methods?: Set<string>;
29
+ apiInstanceId?: string;
30
+ registryFingerprint?: string;
31
+ registrationMode?: string;
32
+ };
33
+
34
+ type GlobalRegisterTrace = {
35
+ lastApiInstanceId?: string;
36
+ lastRegistryFingerprint?: string;
37
+ seenRegistryFingerprints: Set<string>;
38
+ seenApiInstanceIds: Set<string>;
39
+ };
40
+
41
+ type OpenClawPluginApiWithMeta = OpenClawPluginApi & {
42
+ [registerMetaSymbol]?: RegisterMeta;
43
+ };
44
+
45
+ type BncrGatewayRuntime = {
46
+ currentBridge?: BridgeSingletonWithState;
47
+ registeredMethodsByRegistry: Map<string, Set<BncrGatewayMethodName>>;
48
+ serviceRegistered?: boolean;
49
+ channelRegistered?: boolean;
50
+ serviceOwnerApiInstanceId?: string;
51
+ channelOwnerApiInstanceId?: string;
52
+ };
53
+
54
+ const registerMetaSymbol = Symbol.for('bncr.register.meta');
55
+ const globalRegisterTraceSymbol = Symbol.for('bncr.global.register.trace');
56
+ const bridgeOwnerSymbol = Symbol.for('bncr.bridge.owner');
57
+ const gatewayRuntimeSymbol = Symbol.for('bncr.gateway.runtime');
58
+ const moduleEpoch = `${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
59
+ const identityIds = new WeakMap<object, string>();
60
+ let identitySeq = 0;
61
+
62
+ const getIdentityId = (obj: object, prefix: string) => {
63
+ const existing = identityIds.get(obj);
64
+ if (existing) return existing;
65
+ const next = `${prefix}_${moduleEpoch}_${++identitySeq}`;
66
+ identityIds.set(obj, next);
67
+ return next;
68
+ };
69
+
70
+ const getRegistryFingerprint = (api: OpenClawPluginApi) => {
71
+ const serviceId = getIdentityId(api.registerService as object, 'svc');
72
+ const channelId = getIdentityId(api.registerChannel as object, 'chn');
73
+ const methodId = getIdentityId(api.registerGatewayMethod as object, 'mth');
74
+ return `${serviceId}:${channelId}:${methodId}`;
75
+ };
76
+
77
+ const getProcessStore = () => {
78
+ const p = process as NodeJS.Process & {
79
+ [globalRegisterTraceSymbol]?: GlobalRegisterTrace;
80
+ [gatewayRuntimeSymbol]?: BncrGatewayRuntime;
81
+ };
82
+ return p;
83
+ };
84
+
85
+ export function createBncrRegisterRuntime() {
86
+ const gatewayMethodDispatchers: Record<
87
+ BncrGatewayMethodName,
88
+ (
89
+ bridge: BridgeSingletonWithState,
90
+ opts: BridgeGatewayHandlerContext,
91
+ ) => BridgeGatewayHandlerResult
92
+ > = {
93
+ 'bncr.connect': (bridge, opts) => bridge.handleConnect(opts),
94
+ 'bncr.inbound': (bridge, opts) => bridge.handleInbound(opts),
95
+ 'bncr.activity': (bridge, opts) => bridge.handleActivity(opts),
96
+ 'bncr.ack': (bridge, opts) => bridge.handleAck(opts),
97
+ 'bncr.diagnostics': (bridge, opts) => bridge.handleDiagnostics(opts),
98
+ 'bncr.deadLetter.inspect': (bridge, opts) => bridge.handleDeadLetterInspect(opts),
99
+ 'bncr.deadLetter.prune': (bridge, opts) => bridge.handleDeadLetterPrune(opts),
100
+ 'bncr.file.init': (bridge, opts) => bridge.handleFileInit(opts),
101
+ 'bncr.file.chunk': (bridge, opts) => bridge.handleFileChunk(opts),
102
+ 'bncr.file.complete': (bridge, opts) => bridge.handleFileComplete(opts),
103
+ 'bncr.file.abort': (bridge, opts) => bridge.handleFileAbort(opts),
104
+ 'bncr.file.ack': (bridge, opts) => bridge.handleFileAck(opts),
105
+ };
106
+
107
+ const getRegisterMeta = (api: OpenClawPluginApi): RegisterMeta => {
108
+ const host = api as OpenClawPluginApiWithMeta;
109
+ if (!host[registerMetaSymbol]) {
110
+ host[registerMetaSymbol] = { methods: new Set<string>() };
111
+ }
112
+ if (!host[registerMetaSymbol]!.methods) {
113
+ host[registerMetaSymbol]!.methods = new Set<string>();
114
+ }
115
+ if (!host[registerMetaSymbol]!.apiInstanceId) {
116
+ host[registerMetaSymbol]!.apiInstanceId = getIdentityId(api as object, 'api');
117
+ }
118
+ if (!host[registerMetaSymbol]!.registryFingerprint) {
119
+ host[registerMetaSymbol]!.registryFingerprint = getRegistryFingerprint(api);
120
+ }
121
+ return host[registerMetaSymbol]!;
122
+ };
123
+
124
+ const getGlobalRegisterTrace = () => {
125
+ const p = getProcessStore();
126
+ if (!p[globalRegisterTraceSymbol]) {
127
+ p[globalRegisterTraceSymbol] = {
128
+ seenRegistryFingerprints: new Set<string>(),
129
+ seenApiInstanceIds: new Set<string>(),
130
+ };
131
+ }
132
+ return p[globalRegisterTraceSymbol]!;
133
+ };
134
+
135
+ const getGatewayRuntime = (): BncrGatewayRuntime => {
136
+ const p = getProcessStore();
137
+ if (!p[gatewayRuntimeSymbol]) {
138
+ p[gatewayRuntimeSymbol] = {
139
+ registeredMethodsByRegistry: new Map<string, Set<BncrGatewayMethodName>>(),
140
+ serviceRegistered: false,
141
+ channelRegistered: false,
142
+ };
143
+ }
144
+ return p[gatewayRuntimeSymbol]!;
145
+ };
146
+
147
+ const getBridgeRegisterStateCarrier = (bridge: BridgeSingleton): BridgeRegisterStateCarrier =>
148
+ bridge as unknown as BridgeRegisterStateCarrier;
149
+
150
+ const gatewayMethodRegistry = createBncrGatewayMethodRegistry({
151
+ getRegisterMeta,
152
+ getRegistryFingerprint,
153
+ getGatewayRuntime,
154
+ gatewayMethodDispatchers,
155
+ getBridgeRegisterStateCarrier,
156
+ });
157
+
158
+ const getBridgeOwner = (api: OpenClawPluginApi, loaded: LoadedRuntime): BridgeOwner => {
159
+ const meta = getRegisterMeta(api);
160
+ return {
161
+ moduleEpoch,
162
+ bridgeFactoryId: getIdentityId(loaded.createBncrBridge as object, 'bridgeFactory'),
163
+ apiInstanceId: meta.apiInstanceId || 'unknown',
164
+ registryFingerprint: meta.registryFingerprint || 'unknown',
165
+ registrationMode: meta.registrationMode,
166
+ };
167
+ };
168
+
169
+ const bridgeSingletonManager = createBncrBridgeSingletonManager({
170
+ bridgeOwnerSymbol,
171
+ pluginRoot,
172
+ pluginFile,
173
+ loadBncrRuntimeSync,
174
+ getBridgeOwner,
175
+ });
176
+
177
+ const getCurrentBridge = (): BridgeSingletonWithState => {
178
+ const bridge = getGatewayRuntime().currentBridge;
179
+ if (!bridge) throw new Error('bncr current bridge unavailable');
180
+ return bridge;
181
+ };
182
+
183
+ return {
184
+ getRegisterMeta,
185
+ getGlobalRegisterTrace,
186
+ getGatewayRuntime,
187
+ shouldAdoptProcessOwner: (apiInstanceId: string, gatewayRuntime: BncrGatewayRuntime) =>
188
+ shouldAdoptProcessOwner({
189
+ apiInstanceId,
190
+ serviceRegistered: gatewayRuntime.serviceRegistered,
191
+ channelRegistered: gatewayRuntime.channelRegistered,
192
+ serviceOwnerApiInstanceId: gatewayRuntime.serviceOwnerApiInstanceId,
193
+ channelOwnerApiInstanceId: gatewayRuntime.channelOwnerApiInstanceId,
194
+ }),
195
+ ensureGatewayMethodRegistered: gatewayMethodRegistry.ensureGatewayMethodRegistered,
196
+ getBridgeSingleton: bridgeSingletonManager.getBridgeSingleton,
197
+ getBridgeOwnerFromBridge: bridgeSingletonManager.getBridgeOwnerFromBridge,
198
+ getExistingBridgeSingleton: bridgeSingletonManager.getExistingBridgeSingleton,
199
+ getCurrentBridge,
200
+ };
201
+ }